fix(RDM): Use Enchanted melee actions when Black & White mana ≥ 20
Red Mage melee combo (Riposte 7504 → Zwerchhau 7512 → Redoublement 7516) should switch to Enchanted variants (7527, 7528, 7529) when both mana gauges are 20 or higher. The plugin was forcing base actions instead. Changes: - Add IJobGauges to read RDM Black/White mana - ResolveRdmEnchanted(): substitute 7527/7528/7529 when mana ≥ 20 - NormalizeLastComboMoveForSequence(): game/overlay may report Enchanted IDs; map to base for correct sequence index lookup - Apply resolution in UseAction, ExecuteSlotById, GetSlotAppearance, GetIcon, OnFrameworkUpdate - Match combo trigger when slot shows Enchanted Riposte (7527) - ClearOverlayIfActionNotInCombo: treat Enchanted IDs as in-combo - SetOurComboState: Enchanted Redoublement = last step for GCD block Also: remove hardcoded DalamudLibPath so SDK uses platform defaults (~/.xlcore/dalamud/Hooks/dev/ on Linux, %AppData% on Windows) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,10 +16,7 @@
|
||||
<AssemblyName>ConfigurableCombo</AssemblyName>
|
||||
<Version>1.0.0.0</Version>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<DalamudLibPath>$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
|
||||
</PropertyGroup>
|
||||
<!-- FFXIVClientStructs comes from Dalamud dev folder; do not add a second reference or build fails with CS1704 -->
|
||||
<!-- DalamudLibPath: SDK picks ~/.xlcore/dalamud/Hooks/dev/ on Linux, %AppData%\XIVLauncher\addon\Hooks\dev\ on Windows; use DALAMUD_HOME env to override -->
|
||||
<ItemGroup>
|
||||
<Content Include="ConfigurableCombo.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
+95
-23
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud.Game.ClientState.JobGauge.Types;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
@@ -129,6 +130,16 @@ internal unsafe sealed class IconReplacer : IDisposable
|
||||
/// <summary>After setting overlay to the last step (e.g. 2254), don't allow flipping back to step 1 for this long — keeps icon on last step until GCD has finished so the next cast is in the right order. One full GCD + buffer (base 2.5s, can be lower with SkS).</summary>
|
||||
private const int MinMsBeforeOverlayStep1AfterLastStep = 3500;
|
||||
|
||||
// Red Mage melee combo: base (7504, 7512, 7516) vs Enchanted when Black & White mana >= 20 (7527, 7528, 7529)
|
||||
private const uint JobIdRedMage = 35;
|
||||
private const uint RdmRiposte = 7504;
|
||||
private const uint RdmZwerchhau = 7512;
|
||||
private const uint RdmRedoublement = 7516;
|
||||
private const uint RdmEnchantedRiposte = 7527;
|
||||
private const uint RdmEnchantedZwerchhau = 7528;
|
||||
private const uint RdmEnchantedRedoublement = 7529;
|
||||
private const byte RdmEnchantedManaThreshold = 20;
|
||||
|
||||
private static uint GetCurrentJobId()
|
||||
{
|
||||
var player = Service.ClientState.LocalPlayer;
|
||||
@@ -198,7 +209,9 @@ internal unsafe sealed class IconReplacer : IDisposable
|
||||
}
|
||||
_ourComboState[triggerActionId] = (executedActionId, now + ComboWindowMs, setTicks);
|
||||
}
|
||||
if (lastStepActionId.HasValue && executedActionId == lastStepActionId.Value)
|
||||
bool isLastStep = lastStepActionId.HasValue && (executedActionId == lastStepActionId.Value ||
|
||||
(lastStepActionId.Value == RdmRedoublement && executedActionId == RdmEnchantedRedoublement));
|
||||
if (isLastStep)
|
||||
{
|
||||
lock (_lastStepOverlaySetTicks)
|
||||
{
|
||||
@@ -238,7 +251,9 @@ internal unsafe sealed class IconReplacer : IDisposable
|
||||
if (!combo.Enabled || combo.ActionIds.Count == 0 || !ComboAppliesToCurrentJob(combo))
|
||||
continue;
|
||||
uint trigger = combo.ActionIds[0];
|
||||
if (!combo.ActionIds.Contains(executedActionId))
|
||||
bool actionInCombo = combo.ActionIds.Contains(executedActionId) ||
|
||||
(IsRdmMeleeCombo(combo.ActionIds) && (executedActionId == RdmEnchantedRiposte || executedActionId == RdmEnchantedZwerchhau || executedActionId == RdmEnchantedRedoublement));
|
||||
if (!actionInCombo)
|
||||
{
|
||||
_ourComboState.Remove(trigger);
|
||||
lock (_getIconCache) { _getIconCache.Remove(trigger); }
|
||||
@@ -270,7 +285,8 @@ internal unsafe sealed class IconReplacer : IDisposable
|
||||
continue;
|
||||
if (!ComboAppliesToCurrentJob(combo))
|
||||
continue;
|
||||
if (combo.ActionIds[0] != actionId)
|
||||
bool triggerMatches = combo.ActionIds[0] == actionId || (IsRdmMeleeCombo(combo.ActionIds) && actionId == RdmEnchantedRiposte);
|
||||
if (!triggerMatches)
|
||||
continue;
|
||||
matchedCombo = combo;
|
||||
matchedTrigger = combo.ActionIds[0];
|
||||
@@ -286,10 +302,11 @@ internal unsafe sealed class IconReplacer : IDisposable
|
||||
// This serializes mashing so only one action runs at a time and overlay advances only when the game actually executes.
|
||||
lock (_useActionComboLock)
|
||||
{
|
||||
var (lastComboMove, comboTime) = GetEffectiveComboState(actionId);
|
||||
uint triggerForState = matchedTrigger;
|
||||
var (lastComboMove, comboTime) = GetEffectiveComboState(triggerForState);
|
||||
effectiveLastCombo = lastComboMove;
|
||||
effectiveComboTime = comboTime;
|
||||
replaceWith = GetNextActionInSequence(matchedCombo.ActionIds, lastComboMove, comboTime);
|
||||
replaceWith = GetResolvedNextAction(matchedCombo.ActionIds, lastComboMove, comboTime);
|
||||
|
||||
if (Service.Configuration.EnableDebugLogging && replaceWith != 0)
|
||||
{
|
||||
@@ -309,7 +326,8 @@ internal unsafe sealed class IconReplacer : IDisposable
|
||||
// When mashing: block sending step 1 (trigger) only while GCD is still active after we set overlay to the last step, so we allow execution as soon as GCD ends. Cap with timestamp so we don't block forever if GCD isn't available.
|
||||
var am = ActionManager.Instance();
|
||||
long now = Environment.TickCount64;
|
||||
if (replaceWith == matchedTrigger)
|
||||
bool isStep1 = replaceWith == matchedTrigger || (IsRdmMeleeCombo(matchedCombo.ActionIds) && replaceWith == RdmEnchantedRiposte);
|
||||
if (isStep1)
|
||||
{
|
||||
lock (_lastStepOverlaySetTicks)
|
||||
{
|
||||
@@ -406,7 +424,8 @@ internal unsafe sealed class IconReplacer : IDisposable
|
||||
}
|
||||
|
||||
uint triggerActionId = slot->CommandId;
|
||||
var (lastComboMove, comboTime) = GetEffectiveComboState(triggerActionId);
|
||||
uint triggerForState = triggerActionId;
|
||||
var (lastComboMove, comboTime) = GetEffectiveComboState(triggerForState);
|
||||
uint nextActionId = 0;
|
||||
int comboStepCount = 0;
|
||||
foreach (var combo in Service.Configuration.UserCombos)
|
||||
@@ -415,10 +434,15 @@ internal unsafe sealed class IconReplacer : IDisposable
|
||||
continue;
|
||||
if (!ComboAppliesToCurrentJob(combo))
|
||||
continue;
|
||||
if (combo.ActionIds[0] != triggerActionId)
|
||||
bool triggerMatches = combo.ActionIds[0] == triggerActionId || (IsRdmMeleeCombo(combo.ActionIds) && triggerActionId == RdmEnchantedRiposte);
|
||||
if (!triggerMatches)
|
||||
continue;
|
||||
triggerForState = combo.ActionIds[0];
|
||||
var (lastComboMoveInner, comboTimeInner) = GetEffectiveComboState(triggerForState);
|
||||
lastComboMove = lastComboMoveInner;
|
||||
comboTime = comboTimeInner;
|
||||
comboStepCount = combo.ActionIds.Count;
|
||||
nextActionId = GetNextActionInSequence(combo.ActionIds, lastComboMove, comboTime);
|
||||
nextActionId = GetResolvedNextAction(combo.ActionIds, lastComboMove, comboTime);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -452,10 +476,12 @@ internal unsafe sealed class IconReplacer : IDisposable
|
||||
continue;
|
||||
if (!ComboAppliesToCurrentJob(combo))
|
||||
continue;
|
||||
if (combo.ActionIds[0] != triggerActionId)
|
||||
bool triggerMatches = combo.ActionIds[0] == triggerActionId || (IsRdmMeleeCombo(combo.ActionIds) && triggerActionId == RdmEnchantedRiposte);
|
||||
if (!triggerMatches)
|
||||
continue;
|
||||
|
||||
var (slotLastCombo, slotComboTime) = GetEffectiveComboState(triggerActionId);
|
||||
uint triggerForState = combo.ActionIds[0];
|
||||
var (slotLastCombo, slotComboTime) = GetEffectiveComboState(triggerForState);
|
||||
uint nextActionId = GetDisplayActionIdForIcon(combo.ActionIds, slotLastCombo, slotComboTime);
|
||||
// Show next action immediately (e.g. Gust Slash with GCD overlay after Death Blossom); no "hold last step icon while GCD".
|
||||
|
||||
@@ -523,9 +549,11 @@ internal unsafe sealed class IconReplacer : IDisposable
|
||||
continue;
|
||||
if (!ComboAppliesToCurrentJob(combo))
|
||||
continue;
|
||||
if (combo.ActionIds[0] != triggerActionId)
|
||||
bool triggerMatches = combo.ActionIds[0] == triggerActionId || (IsRdmMeleeCombo(combo.ActionIds) && triggerActionId == RdmEnchantedRiposte);
|
||||
if (!triggerMatches)
|
||||
continue;
|
||||
var (slotLastCombo, slotComboTime) = GetEffectiveComboState(triggerActionId);
|
||||
uint triggerForState = combo.ActionIds[0];
|
||||
var (slotLastCombo, slotComboTime) = GetEffectiveComboState(triggerForState);
|
||||
nextActionId = GetDisplayActionIdForIcon(combo.ActionIds, slotLastCombo, slotComboTime);
|
||||
// Show next action immediately (Gust Slash with GCD overlay after Death Blossom).
|
||||
slotMatched = true;
|
||||
@@ -566,9 +594,11 @@ internal unsafe sealed class IconReplacer : IDisposable
|
||||
continue;
|
||||
if (!ComboAppliesToCurrentJob(combo))
|
||||
continue;
|
||||
if (actionID != combo.ActionIds[0])
|
||||
bool triggerMatches = actionID == combo.ActionIds[0] || (IsRdmMeleeCombo(combo.ActionIds) && actionID == RdmEnchantedRiposte);
|
||||
if (!triggerMatches)
|
||||
continue;
|
||||
var (lastComboMove, comboTime) = GetEffectiveComboState(actionID);
|
||||
uint triggerForState = combo.ActionIds[0];
|
||||
var (lastComboMove, comboTime) = GetEffectiveComboState(triggerForState);
|
||||
lock (_getIconCache)
|
||||
{
|
||||
if (throttleMs > 0 && _getIconCache.TryGetValue(actionID, out var cached) &&
|
||||
@@ -625,18 +655,60 @@ internal unsafe sealed class IconReplacer : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the action id to show in the slot icon: the next action in the sequence (what will execute on press).
|
||||
/// That icon persists until that next action actually executes (we only advance lastComboMove on UseAction success).
|
||||
/// </summary>
|
||||
/// <summary>Returns the action id to show in the slot icon (or execute): next in sequence with RDM Enchanted resolution when applicable.</summary>
|
||||
private static uint GetDisplayActionIdForIcon(IList<uint> actionIds, uint lastComboMove, float comboTime)
|
||||
{
|
||||
return GetNextActionInSequence(actionIds, lastComboMove, comboTime);
|
||||
return GetResolvedNextAction(actionIds, lastComboMove, comboTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the next action in the sequence. Resets to step 1 after the last step or when combo expired / another action was used.
|
||||
/// </summary>
|
||||
/// <summary>Next action in sequence, with Red Mage Enchanted melee (7527/7528/7529) when both mana >= 20.</summary>
|
||||
private static uint GetResolvedNextAction(IList<uint> actionIds, uint lastComboMove, float comboTime)
|
||||
{
|
||||
uint normalizedLast = NormalizeLastComboMoveForSequence(actionIds, lastComboMove);
|
||||
uint next = GetNextActionInSequence(actionIds, normalizedLast, comboTime);
|
||||
return ResolveRdmEnchanted(next);
|
||||
}
|
||||
|
||||
/// <summary>For RDM melee combo, game/overlay may report Enchanted IDs (7527/7528/7529); normalize to base (7504/7512/7516) for sequence index lookup.</summary>
|
||||
private static uint NormalizeLastComboMoveForSequence(IList<uint> actionIds, uint lastComboMove)
|
||||
{
|
||||
if (!IsRdmMeleeCombo(actionIds)) return lastComboMove;
|
||||
return lastComboMove switch
|
||||
{
|
||||
RdmEnchantedRiposte => RdmRiposte,
|
||||
RdmEnchantedZwerchhau => RdmZwerchhau,
|
||||
RdmEnchantedRedoublement => RdmRedoublement,
|
||||
_ => lastComboMove
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>When RDM and both Black/White mana >= 20, return Enchanted melee action ID for the given base action.</summary>
|
||||
private static uint ResolveRdmEnchanted(uint actionId)
|
||||
{
|
||||
if (GetCurrentJobId() != JobIdRedMage) return actionId;
|
||||
try
|
||||
{
|
||||
var gauge = Service.JobGauges.Get<RDMGauge>();
|
||||
if (gauge == null || gauge.WhiteMana < RdmEnchantedManaThreshold || gauge.BlackMana < RdmEnchantedManaThreshold)
|
||||
return actionId;
|
||||
}
|
||||
catch { return actionId; }
|
||||
|
||||
return actionId switch
|
||||
{
|
||||
RdmRiposte => RdmEnchantedRiposte,
|
||||
RdmZwerchhau => RdmEnchantedZwerchhau,
|
||||
RdmRedoublement => RdmEnchantedRedoublement,
|
||||
_ => actionId
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsRdmMeleeCombo(IList<uint> actionIds)
|
||||
{
|
||||
return actionIds.Count >= 3 && actionIds[0] == RdmRiposte && actionIds[1] == RdmZwerchhau && actionIds[2] == RdmRedoublement;
|
||||
}
|
||||
|
||||
/// <summary>Returns the next action in the sequence. Resets to step 1 after the last step or when combo expired / another action was used.</summary>
|
||||
private static uint GetNextActionInSequence(IList<uint> actionIds, uint lastComboMove, float comboTime)
|
||||
{
|
||||
// Combo timer expired or no combo in progress -> show first action
|
||||
|
||||
@@ -19,4 +19,5 @@ internal class Service
|
||||
[PluginService] internal static IPluginLog PluginLog { get; private set; } = null!;
|
||||
[PluginService] internal static ITextureProvider TextureProvider { get; private set; } = null!;
|
||||
[PluginService] internal static IFramework Framework { get; private set; } = null!;
|
||||
[PluginService] internal static IJobGauges JobGauges { get; private set; } = null!;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user