feat: Per-combo reset options and preserve-position toggle #2
@@ -195,6 +195,48 @@ public class ConfigWindow : Window
|
|||||||
Service.Configuration.Save();
|
Service.Configuration.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset behavior (per-combo)
|
||||||
|
ImGui.Text("Reset combo when:");
|
||||||
|
ImGui.SameLine();
|
||||||
|
var resetMode = combo.ResetMode;
|
||||||
|
ImGui.SetNextItemWidth(180);
|
||||||
|
if (ImGui.BeginCombo("##resetMode", resetMode.ToString().Replace("_", " ")))
|
||||||
|
{
|
||||||
|
foreach (ComboResetMode mode in Enum.GetValues(typeof(ComboResetMode)))
|
||||||
|
{
|
||||||
|
if (ImGui.Selectable(mode.ToString().Replace("_", " "), resetMode == mode))
|
||||||
|
{
|
||||||
|
combo.ResetMode = mode;
|
||||||
|
Service.Configuration.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.EndCombo();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Game timer: reset when game combo timer expires (~15s).\nNever: always run through sequence (cycle after last step).\nOn target change: reset when you change target.\nAfter seconds: reset after the time below with no combo use.");
|
||||||
|
if (combo.ResetMode == ComboResetMode.AfterSeconds)
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
float sec = combo.ResetAfterSeconds;
|
||||||
|
ImGui.SetNextItemWidth(80);
|
||||||
|
if (ImGui.InputFloat("##resetSec", ref sec, 0.5f, 1f, "%.1fs", ImGuiInputTextFlags.CharsDecimal))
|
||||||
|
{
|
||||||
|
combo.ResetAfterSeconds = Math.Clamp(sec, 0.5f, 300f);
|
||||||
|
Service.Configuration.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Seconds of no combo use before reset (0.5–300).");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool preservePosition = combo.PreservePositionWhenUsingOtherActions;
|
||||||
|
if (ImGui.Checkbox("Preserve position when using other actions", ref preservePosition))
|
||||||
|
{
|
||||||
|
combo.PreservePositionWhenUsingOtherActions = preservePosition;
|
||||||
|
Service.Configuration.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("On (default): Using another combo or any ability never resets this combo's step; it only resets per \"Reset when\" above.\nOff (original): Using an action not in this combo resets this combo to step 1.");
|
||||||
|
|
||||||
// Action list
|
// Action list
|
||||||
ImGui.Text("Action sequence (first = hotbar slot; order: 1, 2, 3a/3b on other keys...)");
|
ImGui.Text("Action sequence (first = hotbar slot; order: 1, 2, 3a/3b on other keys...)");
|
||||||
ImGui.TextColored(new Vector4(0.9f, 0.85f, 0.4f, 1f), "First action must be the combo starter (e.g. Spinning Edge for NIN), or the icon/state won't advance.");
|
ImGui.TextColored(new Vector4(0.9f, 0.85f, 0.4f, 1f), "First action must be the combo starter (e.g. Spinning Edge for NIN), or the icon/state won't advance.");
|
||||||
|
|||||||
@@ -15,10 +15,13 @@ public sealed class ConfigurableComboPlugin : IDalamudPlugin
|
|||||||
public ConfigurableComboPlugin(
|
public ConfigurableComboPlugin(
|
||||||
IDalamudPluginInterface pluginInterface,
|
IDalamudPluginInterface pluginInterface,
|
||||||
ISigScanner sigScanner,
|
ISigScanner sigScanner,
|
||||||
IGameInteropProvider gameInteropProvider)
|
IGameInteropProvider gameInteropProvider,
|
||||||
|
ITargetManager? targetManager = null)
|
||||||
{
|
{
|
||||||
pluginInterface.Create<Service>();
|
pluginInterface.Create<Service>();
|
||||||
|
|
||||||
|
Service.TargetManager = targetManager;
|
||||||
|
|
||||||
Service.Configuration = pluginInterface.GetPluginConfig() as PluginConfiguration ?? new PluginConfiguration();
|
Service.Configuration = pluginInterface.GetPluginConfig() as PluginConfiguration ?? new PluginConfiguration();
|
||||||
Service.Address = new PluginAddressResolver();
|
Service.Address = new PluginAddressResolver();
|
||||||
Service.Address.Setup(sigScanner);
|
Service.Address.Setup(sigScanner);
|
||||||
|
|||||||
+89
-10
@@ -40,6 +40,8 @@ internal unsafe sealed class IconReplacer : IDisposable
|
|||||||
private readonly Dictionary<uint, (uint lastExecutedActionId, int lastExecutedIndex, long expiryTicks, long setTicks)> _ourComboState = new();
|
private readonly Dictionary<uint, (uint lastExecutedActionId, int lastExecutedIndex, long expiryTicks, long setTicks)> _ourComboState = new();
|
||||||
/// <summary>Per-trigger: TickCount64 when we last set overlay to the combo's last step (so we can block flipping to step 1 for one GCD).</summary>
|
/// <summary>Per-trigger: TickCount64 when we last set overlay to the combo's last step (so we can block flipping to step 1 for one GCD).</summary>
|
||||||
private readonly Dictionary<uint, long> _lastStepOverlaySetTicks = new();
|
private readonly Dictionary<uint, long> _lastStepOverlaySetTicks = new();
|
||||||
|
/// <summary>Per-trigger: last target ObjectId when combo uses ResetMode.OnTargetChange; reset when target changes.</summary>
|
||||||
|
private readonly Dictionary<uint, ulong> _ourComboStateTarget = new();
|
||||||
|
|
||||||
/// <summary>Serializes combo replacement so rapid UseAction calls (mashing) don't all read state before any update; second caller waits and sees updated overlay.</summary>
|
/// <summary>Serializes combo replacement so rapid UseAction calls (mashing) don't all read state before any update; second caller waits and sees updated overlay.</summary>
|
||||||
private readonly object _useActionComboLock = new();
|
private readonly object _useActionComboLock = new();
|
||||||
@@ -193,19 +195,70 @@ internal unsafe sealed class IconReplacer : IDisposable
|
|||||||
return combo.JobId == GetCurrentJobId();
|
return combo.JobId == GetCurrentJobId();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Get effective combo state for a trigger: use our overlay if we recently executed an action for this combo, else game state. lastExecutedIndex is -1 when from game (enables first-match fallback for duplicates).</summary>
|
/// <summary>Get the first enabled combo for this trigger that applies to the current job.</summary>
|
||||||
|
private static UserComboDefinition? GetComboForTrigger(uint triggerActionId)
|
||||||
|
{
|
||||||
|
foreach (var combo in Service.Configuration.UserCombos ?? new List<UserComboDefinition>())
|
||||||
|
{
|
||||||
|
if (!combo.Enabled || combo.ActionIds.Count == 0 || combo.ActionIds[0] != triggerActionId)
|
||||||
|
continue;
|
||||||
|
if (!ComboAppliesToCurrentJob(combo))
|
||||||
|
continue;
|
||||||
|
return combo;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Combo window in ms from combo settings. Never/OnTargetChange use a long window; expiry is overridden by target check for OnTargetChange.</summary>
|
||||||
|
private static int GetComboWindowMs(UserComboDefinition? combo)
|
||||||
|
{
|
||||||
|
if (combo == null) return ComboWindowMs;
|
||||||
|
return combo.ResetMode switch
|
||||||
|
{
|
||||||
|
ComboResetMode.Never => 86400000, // 24h so we effectively never expire from time
|
||||||
|
ComboResetMode.OnTargetChange => 86400000,
|
||||||
|
ComboResetMode.AfterSeconds => (int)(Math.Clamp(combo.ResetAfterSeconds, 0.5f, 300f) * 1000),
|
||||||
|
_ => ComboWindowMs
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong GetCurrentTargetObjectId()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tm = Service.TargetManager;
|
||||||
|
if (tm == null) return 0;
|
||||||
|
var target = tm.Target;
|
||||||
|
return target?.GameObjectId ?? 0;
|
||||||
|
}
|
||||||
|
catch { return 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Get effective combo state for a trigger: use our overlay if we recently executed an action for this combo, else game state. lastExecutedIndex is -1 when from game (enables first-match fallback for duplicates). Respects per-combo ResetMode (Never, OnTargetChange, AfterSeconds).</summary>
|
||||||
private unsafe (uint lastComboMove, float comboTime, int lastExecutedIndex) GetEffectiveComboState(uint triggerActionId)
|
private unsafe (uint lastComboMove, float comboTime, int lastExecutedIndex) GetEffectiveComboState(uint triggerActionId)
|
||||||
{
|
{
|
||||||
long now = Environment.TickCount64;
|
long now = Environment.TickCount64;
|
||||||
|
var combo = GetComboForTrigger(triggerActionId);
|
||||||
|
|
||||||
lock (_ourComboState)
|
lock (_ourComboState)
|
||||||
{
|
{
|
||||||
if (_ourComboState.TryGetValue(triggerActionId, out var our) && now < our.expiryTicks)
|
if (_ourComboState.TryGetValue(triggerActionId, out var our) && now < our.expiryTicks)
|
||||||
{
|
{
|
||||||
|
if (combo?.ResetMode == ComboResetMode.OnTargetChange && Service.TargetManager != null)
|
||||||
|
{
|
||||||
|
ulong currentTarget = GetCurrentTargetObjectId();
|
||||||
|
if (_ourComboStateTarget.TryGetValue(triggerActionId, out ulong lastTarget) && currentTarget != lastTarget)
|
||||||
|
{
|
||||||
|
_ourComboState.Remove(triggerActionId);
|
||||||
|
_ourComboStateTarget.Remove(triggerActionId);
|
||||||
|
lock (_getIconCache) { _getIconCache.Remove(triggerActionId); }
|
||||||
|
goto useRaw;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (our.lastExecutedActionId == triggerActionId && (now - our.setTicks) < OverlayIgnoreSameTriggerMs)
|
if (our.lastExecutedActionId == triggerActionId && (now - our.setTicks) < OverlayIgnoreSameTriggerMs)
|
||||||
{
|
{
|
||||||
var rawMove = *(uint*)Service.Address.LastComboMove;
|
var rawMove = *(uint*)Service.Address.LastComboMove;
|
||||||
var rawTime = *(float*)Service.Address.ComboTimer;
|
var rawTime = *(float*)Service.Address.ComboTimer;
|
||||||
// Only use raw state when the game has actually updated (avoids stuck combo when mashing: raw is often still 0,0).
|
|
||||||
if (rawTime > 0 || rawMove != 0)
|
if (rawTime > 0 || rawMove != 0)
|
||||||
return (rawMove, rawTime, -1);
|
return (rawMove, rawTime, -1);
|
||||||
}
|
}
|
||||||
@@ -213,41 +266,63 @@ internal unsafe sealed class IconReplacer : IDisposable
|
|||||||
return (our.lastExecutedActionId, remaining, our.lastExecutedIndex);
|
return (our.lastExecutedActionId, remaining, our.lastExecutedIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
useRaw:
|
||||||
var rawMoveFinal = *(uint*)Service.Address.LastComboMove;
|
var rawMoveFinal = *(uint*)Service.Address.LastComboMove;
|
||||||
var rawTimeFinal = *(float*)Service.Address.ComboTimer;
|
var rawTimeFinal = *(float*)Service.Address.ComboTimer;
|
||||||
// When game state is (0,0) (e.g. after failed UseAction resets it), prefer our overlay so we keep trying the next step instead of resetting to step 1.
|
|
||||||
if (rawMoveFinal == 0 && rawTimeFinal <= 0)
|
if (rawMoveFinal == 0 && rawTimeFinal <= 0)
|
||||||
{
|
{
|
||||||
lock (_ourComboState)
|
lock (_ourComboState)
|
||||||
{
|
{
|
||||||
if (_ourComboState.TryGetValue(triggerActionId, out var fallback) && now < fallback.expiryTicks)
|
if (_ourComboState.TryGetValue(triggerActionId, out var fallback) && now < fallback.expiryTicks)
|
||||||
|
{
|
||||||
|
if (combo?.ResetMode == ComboResetMode.OnTargetChange && Service.TargetManager != null)
|
||||||
|
{
|
||||||
|
ulong currentTarget = GetCurrentTargetObjectId();
|
||||||
|
if (_ourComboStateTarget.TryGetValue(triggerActionId, out ulong lastTarget) && currentTarget != lastTarget)
|
||||||
|
{
|
||||||
|
_ourComboState.Remove(triggerActionId);
|
||||||
|
_ourComboStateTarget.Remove(triggerActionId);
|
||||||
|
lock (_getIconCache) { _getIconCache.Remove(triggerActionId); }
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
float remaining = (fallback.expiryTicks - now) / 1000f;
|
float remaining = (fallback.expiryTicks - now) / 1000f;
|
||||||
return (fallback.lastExecutedActionId, remaining, fallback.lastExecutedIndex);
|
return (fallback.lastExecutedActionId, remaining, fallback.lastExecutedIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float remaining = (fallback.expiryTicks - now) / 1000f;
|
||||||
|
return (fallback.lastExecutedActionId, remaining, fallback.lastExecutedIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (rawMoveFinal, rawTimeFinal, -1);
|
return (rawMoveFinal, rawTimeFinal, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Record that we just executed this action at the given step index for this combo trigger. executedIndex < 0 means unknown (no index tracking).</summary>
|
/// <summary>Record that we just executed this action at the given step index for this combo trigger. executedIndex < 0 means unknown (no index tracking).</summary>
|
||||||
/// <param name="optimistic">If true, we're updating before Original() returns. When cycling back (executed==trigger), backdate setTicks so the 200ms overlay-ignore doesn't trigger and the next UseAction sees our overlay and advances to step 2 instead of raw game state (2254) and replacing with 2242 again.</param>
|
/// <param name="optimistic">If true, we're updating before Original() returns. When cycling back (executed==trigger), backdate setTicks so the 200ms overlay-ignore doesn't trigger and the next UseAction sees our overlay and advances to step 2 instead of raw game state (2254) and replacing with 2242 again.</param>
|
||||||
private void SetOurComboState(uint triggerActionId, uint executedActionId, int executedIndex = -1, bool optimistic = false)
|
/// <param name="combo">When set, combo window and target tracking use this combo's ResetMode / ResetAfterSeconds.</param>
|
||||||
|
/// <param name="targetId">When combo uses OnTargetChange, we store this so the combo resets when target changes.</param>
|
||||||
|
private void SetOurComboState(uint triggerActionId, uint executedActionId, int executedIndex = -1, bool optimistic = false, UserComboDefinition? combo = null, ulong targetId = 0)
|
||||||
{
|
{
|
||||||
long now = Environment.TickCount64;
|
long now = Environment.TickCount64;
|
||||||
|
int windowMs = GetComboWindowMs(combo);
|
||||||
uint? lastStepActionId = GetLastStepActionIdForTrigger(triggerActionId);
|
uint? lastStepActionId = GetLastStepActionIdForTrigger(triggerActionId);
|
||||||
// We only allow sending step 1 (trigger) in UseAction when GCD has ended, so when we get here with executedActionId==trigger it's a real success — always update so the next press advances to step 2 instead of spamming step 1.
|
|
||||||
lock (_ourComboState)
|
lock (_ourComboState)
|
||||||
{
|
{
|
||||||
long setTicks = now;
|
long setTicks = now;
|
||||||
if (executedActionId == triggerActionId)
|
if (executedActionId == triggerActionId)
|
||||||
{
|
{
|
||||||
if (optimistic)
|
if (optimistic)
|
||||||
setTicks = now - OverlayIgnoreSameTriggerMs - 50; // backdate so next GetEffectiveComboState uses overlay, not raw (so we advance 2242->2254 when mashing)
|
setTicks = now - OverlayIgnoreSameTriggerMs - 50;
|
||||||
else if (_ourComboState.TryGetValue(triggerActionId, out var prev) && prev.lastExecutedActionId == triggerActionId)
|
else if (_ourComboState.TryGetValue(triggerActionId, out var prev) && prev.lastExecutedActionId == triggerActionId)
|
||||||
setTicks = prev.setTicks; // keep 200ms window from first execution of step 1
|
setTicks = prev.setTicks;
|
||||||
}
|
}
|
||||||
_ourComboState[triggerActionId] = (executedActionId, executedIndex, now + ComboWindowMs, setTicks);
|
_ourComboState[triggerActionId] = (executedActionId, executedIndex, now + windowMs, setTicks);
|
||||||
|
if (combo?.ResetMode == ComboResetMode.OnTargetChange)
|
||||||
|
_ourComboStateTarget[triggerActionId] = targetId;
|
||||||
}
|
}
|
||||||
bool isLastStep = lastStepActionId.HasValue && (executedActionId == lastStepActionId.Value ||
|
bool isLastStep = lastStepActionId.HasValue && (executedActionId == lastStepActionId.Value ||
|
||||||
(lastStepActionId.Value == RdmRedoublement && executedActionId == RdmEnchantedRedoublement) ||
|
(lastStepActionId.Value == RdmRedoublement && executedActionId == RdmEnchantedRedoublement) ||
|
||||||
@@ -284,7 +359,7 @@ internal unsafe sealed class IconReplacer : IDisposable
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Clear overlay for combo triggers where the executed action is not in that combo's sequence, so the icon resets to step 1.</summary>
|
/// <summary>For each combo with PreservePositionWhenUsingOtherActions false (original): clear overlay when the executed action is not in that combo. Combos with it true keep their position.</summary>
|
||||||
private void ClearOverlayIfActionNotInCombo(uint executedActionId)
|
private void ClearOverlayIfActionNotInCombo(uint executedActionId)
|
||||||
{
|
{
|
||||||
if (executedActionId == 0) return;
|
if (executedActionId == 0) return;
|
||||||
@@ -294,6 +369,8 @@ internal unsafe sealed class IconReplacer : IDisposable
|
|||||||
{
|
{
|
||||||
if (!combo.Enabled || combo.ActionIds.Count == 0 || !ComboAppliesToCurrentJob(combo))
|
if (!combo.Enabled || combo.ActionIds.Count == 0 || !ComboAppliesToCurrentJob(combo))
|
||||||
continue;
|
continue;
|
||||||
|
if (combo.PreservePositionWhenUsingOtherActions)
|
||||||
|
continue; // per-combo: don't clear this combo's position when using other actions
|
||||||
uint trigger = combo.ActionIds[0];
|
uint trigger = combo.ActionIds[0];
|
||||||
bool actionInCombo = combo.ActionIds.Contains(executedActionId) ||
|
bool actionInCombo = combo.ActionIds.Contains(executedActionId) ||
|
||||||
(IsRdmMeleeCombo(combo.ActionIds) && (executedActionId == RdmEnchantedRiposte || executedActionId == RdmEnchantedZwerchhau || executedActionId == RdmEnchantedRedoublement)) ||
|
(IsRdmMeleeCombo(combo.ActionIds) && (executedActionId == RdmEnchantedRiposte || executedActionId == RdmEnchantedZwerchhau || executedActionId == RdmEnchantedRedoublement)) ||
|
||||||
@@ -303,6 +380,7 @@ internal unsafe sealed class IconReplacer : IDisposable
|
|||||||
if (!actionInCombo)
|
if (!actionInCombo)
|
||||||
{
|
{
|
||||||
_ourComboState.Remove(trigger);
|
_ourComboState.Remove(trigger);
|
||||||
|
_ourComboStateTarget.Remove(trigger);
|
||||||
lock (_getIconCache) { _getIconCache.Remove(trigger); }
|
lock (_getIconCache) { _getIconCache.Remove(trigger); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -406,7 +484,7 @@ internal unsafe sealed class IconReplacer : IDisposable
|
|||||||
bool result = _useActionHook!.Original(actionManager, actionType, replaceWith, targetId, extraParam, mode, comboRouteId, outOptAreaTargeted);
|
bool result = _useActionHook!.Original(actionManager, actionType, replaceWith, targetId, extraParam, mode, comboRouteId, outOptAreaTargeted);
|
||||||
if (result)
|
if (result)
|
||||||
{
|
{
|
||||||
SetOurComboState(matchedTrigger, executedActionId, nextIndex, optimistic: false);
|
SetOurComboState(matchedTrigger, executedActionId, nextIndex, optimistic: false, combo: matchedCombo, targetId: targetId);
|
||||||
ClearOverlayIfActionNotInCombo(executedActionId);
|
ClearOverlayIfActionNotInCombo(executedActionId);
|
||||||
}
|
}
|
||||||
// When the action fails (GCD, range, etc.) do NOT clear the overlay: leave last successful state
|
// When the action fails (GCD, range, etc.) do NOT clear the overlay: leave last successful state
|
||||||
@@ -447,6 +525,7 @@ internal unsafe sealed class IconReplacer : IDisposable
|
|||||||
lock (_ourComboState)
|
lock (_ourComboState)
|
||||||
{
|
{
|
||||||
_ourComboState.Remove(triggerActionId);
|
_ourComboState.Remove(triggerActionId);
|
||||||
|
_ourComboStateTarget.Remove(triggerActionId);
|
||||||
}
|
}
|
||||||
lock (_getIconCache) { _getIconCache.Remove(triggerActionId); }
|
lock (_getIconCache) { _getIconCache.Remove(triggerActionId); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ internal class Service
|
|||||||
|
|
||||||
[PluginService] internal static IDalamudPluginInterface Interface { get; private set; } = null!;
|
[PluginService] internal static IDalamudPluginInterface Interface { get; private set; } = null!;
|
||||||
[PluginService] internal static IClientState ClientState { get; private set; } = null!;
|
[PluginService] internal static IClientState ClientState { get; private set; } = null!;
|
||||||
|
/// <summary>Set by plugin if available; not injected to avoid load failure when service missing. When null, OnTargetChange reset is skipped.</summary>
|
||||||
|
internal static ITargetManager? TargetManager { get; set; }
|
||||||
[PluginService] internal static IChatGui ChatGui { get; private set; } = null!;
|
[PluginService] internal static IChatGui ChatGui { get; private set; } = null!;
|
||||||
[PluginService] internal static ICommandManager CommandManager { get; private set; } = null!;
|
[PluginService] internal static ICommandManager CommandManager { get; private set; } = null!;
|
||||||
[PluginService] internal static IDataManager DataManager { get; private set; } = null!;
|
[PluginService] internal static IDataManager DataManager { get; private set; } = null!;
|
||||||
|
|||||||
@@ -4,6 +4,23 @@ using Newtonsoft.Json;
|
|||||||
|
|
||||||
namespace ConfigurableCombo;
|
namespace ConfigurableCombo;
|
||||||
|
|
||||||
|
/// <summary>When to reset the combo sequence back to step 1.</summary>
|
||||||
|
[Serializable]
|
||||||
|
public enum ComboResetMode
|
||||||
|
{
|
||||||
|
/// <summary>Reset when the game combo timer expires (default, ~15s).</summary>
|
||||||
|
GameTimer = 0,
|
||||||
|
|
||||||
|
/// <summary>Never reset from time; always advance through the sequence (cycle after last step).</summary>
|
||||||
|
Never = 1,
|
||||||
|
|
||||||
|
/// <summary>Reset when the current target changes.</summary>
|
||||||
|
OnTargetChange = 2,
|
||||||
|
|
||||||
|
/// <summary>Reset after the configured number of seconds of inactivity.</summary>
|
||||||
|
AfterSeconds = 3,
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A single user-defined combo: put the first action on your hotbar; pressing it advances through the sequence.
|
/// A single user-defined combo: put the first action on your hotbar; pressing it advances through the sequence.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -40,4 +57,21 @@ public class UserComboDefinition
|
|||||||
/// The action ID that is "on the bar" — i.e. the trigger. This is ActionIds[0] when the list is non-empty.
|
/// The action ID that is "on the bar" — i.e. the trigger. This is ActionIds[0] when the list is non-empty.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public uint TriggerActionId => ActionIds.Count > 0 ? ActionIds[0] : 0;
|
public uint TriggerActionId => ActionIds.Count > 0 ? ActionIds[0] : 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When to reset this combo to step 1. Per-combo; e.g. one combo can use "Never" while another uses "After seconds".
|
||||||
|
/// </summary>
|
||||||
|
public ComboResetMode ResetMode { get; set; } = ComboResetMode.GameTimer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seconds after which the combo resets when <see cref="ResetMode"/> is <see cref="ComboResetMode.AfterSeconds"/>.
|
||||||
|
/// Ignored for other modes.
|
||||||
|
/// </summary>
|
||||||
|
public float ResetAfterSeconds { get; set; } = 15f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true (default): using another combo or any action never clears this combo's position; it only resets per its own "Reset when" setting.
|
||||||
|
/// When false (original): using an action that is not part of this combo clears this combo's state back to step 1.
|
||||||
|
/// </summary>
|
||||||
|
public bool PreservePositionWhenUsingOtherActions { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user