diff --git a/BossMod/AI/AIController.cs b/BossMod/AI/AIController.cs index fb48d516c7..1c7d08adaa 100644 --- a/BossMod/AI/AIController.cs +++ b/BossMod/AI/AIController.cs @@ -144,7 +144,7 @@ public void Update(Actor? player) else { _amex.InputOverride.GamepadOverridesEnabled = false; - _axisForward.CurDirection = ForceCancelCast && castInProgress ? 1 : 0; // this is a hack to cancel any cast... + _amex.ForceCancelCastNextFrame |= ForceCancelCast && castInProgress; _keyJump.Held = false; } diff --git a/BossMod/ActionTweaks/ActionTweaksConfig.cs b/BossMod/ActionTweaks/ActionTweaksConfig.cs index 0539914d97..e452729a41 100644 --- a/BossMod/ActionTweaks/ActionTweaksConfig.cs +++ b/BossMod/ActionTweaks/ActionTweaksConfig.cs @@ -13,6 +13,9 @@ public sealed class ActionTweaksConfig : ConfigNode [PropertyDisplay("Prevent movement while casting")] public bool PreventMovingWhileCasting = false; + [PropertyDisplay("Automatically cancel a cast when target is dead")] + public bool CancelCastOnDeadTarget = false; + [PropertyDisplay("Restore character orientation after action use (no effect if 'auto face target' in game settings is disabled)")] public bool RestoreRotation = false; diff --git a/BossMod/ActionTweaks/CancelCastTweak.cs b/BossMod/ActionTweaks/CancelCastTweak.cs new file mode 100644 index 0000000000..69c4230cf7 --- /dev/null +++ b/BossMod/ActionTweaks/CancelCastTweak.cs @@ -0,0 +1,23 @@ +namespace BossMod; + +// Utility for automatically cancelling casts in some conditions (when target dies, when ai wants it, etc). +// Since the game API is sending a packet, this implements some rate limiting. +public sealed class CancelCastTweak(WorldState ws) +{ + private readonly ActionTweaksConfig _config = Service.Config.Get(); + private readonly WorldState _ws = ws; + private DateTime _nextCancelAllowed; + + public bool ShouldCancel(DateTime currentTime, bool force) + { + if (currentTime < _nextCancelAllowed) + return false; + + var cancel = force || _config.CancelCastOnDeadTarget && (_ws.Actors.Find(_ws.Party.Player()?.CastInfo?.TargetID ?? 0)?.IsDead ?? false); + if (!cancel) + return false; + + _nextCancelAllowed = currentTime.AddSeconds(0.2f); + return true; + } +} diff --git a/BossMod/Autorotation/UIRotationWindow.cs b/BossMod/Autorotation/UIRotationWindow.cs index da45389c06..658df10bc5 100644 --- a/BossMod/Autorotation/UIRotationWindow.cs +++ b/BossMod/Autorotation/UIRotationWindow.cs @@ -82,7 +82,7 @@ public override void Draw() // TODO: more fancy action history/queue... ImGui.TextUnformatted($"Modules: {_mgr}"); - ImGui.TextUnformatted($"GCD={_mgr.WorldState.Client.Cooldowns[ActionDefinitions.GCDGroup].Remaining:f3}, AnimLock={_mgr.ActionManager.EffectiveAnimationLock:f3}+{_mgr.ActionManager.AnimationLockDelayEstimate:f3}, Combo={_mgr.ActionManager.ComboTimeLeft:f3}"); + ImGui.TextUnformatted($"GCD={_mgr.WorldState.Client.Cooldowns[ActionDefinitions.GCDGroup].Remaining:f3}, AnimLock={_mgr.ActionManager.EffectiveAnimationLock:f3}+{_mgr.ActionManager.AnimationLockDelayEstimate:f3}, Combo={_mgr.ActionManager.ComboTimeLeft:f3}, RBIn={_mgr.Bossmods.RaidCooldowns.NextDamageBuffIn(_mgr.WorldState.CurrentTime):f3}"); foreach (var a in _mgr.Hints.ActionsToExecute.Entries) { ImGui.TextUnformatted($"> {a.Action} ({a.Priority:f2})"); diff --git a/BossMod/Framework/ActionManagerEx.cs b/BossMod/Framework/ActionManagerEx.cs index 5d0297dfb3..517e61970a 100644 --- a/BossMod/Framework/ActionManagerEx.cs +++ b/BossMod/Framework/ActionManagerEx.cs @@ -24,6 +24,7 @@ namespace BossMod; // 6. ground-targeted action queueing // ground-targeted actions can't be queued, making using them efficiently tricky // this feature allows queueing them, plus provides options to execute them automatically either at target's position or at cursor's position +// 7. auto cancel cast utility // TODO: should not be public! public unsafe sealed class ActionManagerEx : IDisposable { @@ -44,6 +45,7 @@ public unsafe sealed class ActionManagerEx : IDisposable public ActionTweaksConfig Config = Service.Config.Get(); public ActionQueue.Entry AutoQueue { get; private set; } public bool MoveMightInterruptCast { get; private set; } // if true, moving now might cause cast interruption (for current or queued cast) + public bool ForceCancelCastNextFrame; private readonly ActionManager* _inst = ActionManager.Instance(); private readonly WorldState _ws; private readonly AIHints _hints; @@ -51,6 +53,7 @@ public unsafe sealed class ActionManagerEx : IDisposable private readonly AnimationLockTweak _animLockTweak = new(); private readonly CooldownDelayTweak _cooldownTweak = new(); private readonly RestoreRotationTweak _restoreRotTweak = new(); + private readonly CancelCastTweak _cancelCastTweak; private readonly HookAddress _updateHook; private readonly HookAddress _useActionHook; @@ -63,6 +66,7 @@ public ActionManagerEx(WorldState ws, AIHints hints) _ws = ws; _hints = hints; _manualQueue = new(ws, hints); + _cancelCastTweak = new(ws); Service.Log($"[AMEx] ActionManager singleton address = 0x{(ulong)_inst:X}"); _updateHook = new(ActionManager.Addresses.Update, UpdateDetour); @@ -279,6 +283,10 @@ private void UpdateDetour(ActionManager* self) InputOverride.BlockMovement(); else InputOverride.UnblockMovement(); + + if (_ws.Party.Player()?.CastInfo != null && _cancelCastTweak.ShouldCancel(_ws.CurrentTime, ForceCancelCastNextFrame)) + UIState.Instance()->Hotbar.CancelCast(); + ForceCancelCastNextFrame = false; } // note: targetId is usually your current primary target (or 0xE0000000 if you don't target anyone), unless you do something like /ac XXX etc