New experimental interrupt handler
authorLiza Carvelli <liza@carvel.li>
Tue, 21 Jan 2025 18:32:28 +0000 (19:32 +0100)
committerLiza Carvelli <liza@carvel.li>
Tue, 21 Jan 2025 18:32:28 +0000 (19:32 +0100)
41 files changed:
Questionable/Controller/GatheringController.cs
Questionable/Controller/InterruptHandler.cs [new file with mode: 0644]
Questionable/Controller/MiniTaskController.cs
Questionable/Controller/QuestController.cs
Questionable/Controller/Steps/Common/Mount.cs
Questionable/Controller/Steps/Common/NextQuest.cs
Questionable/Controller/Steps/Common/SendNotification.cs
Questionable/Controller/Steps/Common/WaitConditionTask.cs
Questionable/Controller/Steps/Gathering/DoGather.cs
Questionable/Controller/Steps/Gathering/DoGatherCollectable.cs
Questionable/Controller/Steps/Gathering/MoveToLandingLocation.cs
Questionable/Controller/Steps/Gathering/TurnInDelivery.cs
Questionable/Controller/Steps/Interactions/Action.cs
Questionable/Controller/Steps/Interactions/AetherCurrent.cs
Questionable/Controller/Steps/Interactions/AethernetShard.cs
Questionable/Controller/Steps/Interactions/Aetheryte.cs
Questionable/Controller/Steps/Interactions/Combat.cs
Questionable/Controller/Steps/Interactions/Dive.cs
Questionable/Controller/Steps/Interactions/Duty.cs
Questionable/Controller/Steps/Interactions/Emote.cs
Questionable/Controller/Steps/Interactions/EquipItem.cs
Questionable/Controller/Steps/Interactions/EquipRecommended.cs
Questionable/Controller/Steps/Interactions/Interact.cs
Questionable/Controller/Steps/Interactions/Jump.cs
Questionable/Controller/Steps/Interactions/Say.cs
Questionable/Controller/Steps/Interactions/StatusOff.cs
Questionable/Controller/Steps/Interactions/UseItem.cs
Questionable/Controller/Steps/Leves/InitiateLeve.cs
Questionable/Controller/Steps/Shared/AethernetShortcut.cs
Questionable/Controller/Steps/Shared/AetheryteShortcut.cs
Questionable/Controller/Steps/Shared/Craft.cs
Questionable/Controller/Steps/Shared/Gather.cs
Questionable/Controller/Steps/Shared/MoveTo.cs
Questionable/Controller/Steps/Shared/RedeemRewardItems.cs
Questionable/Controller/Steps/Shared/SkipCondition.cs
Questionable/Controller/Steps/Shared/StepDisabled.cs
Questionable/Controller/Steps/Shared/SwitchClassJob.cs
Questionable/Controller/Steps/Shared/WaitAtEnd.cs
Questionable/Controller/Steps/Shared/WaitAtStart.cs
Questionable/Controller/Steps/TaskExecutor.cs
Questionable/QuestionablePlugin.cs

index bbe6d2e49d93da1f9bcb43fdb7b0b04c76d354c4..28de31f9e140d1a8037e87c61e9abae63650fe01 100644 (file)
@@ -49,9 +49,10 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
         ILogger<GatheringController> logger,
         ICondition condition,
         IServiceProvider serviceProvider,
+        InterruptHandler interruptHandler,
         IDataManager dataManager,
         IPluginLog pluginLog)
-        : base(chatGui, condition, serviceProvider, dataManager, logger)
+        : base(chatGui, condition, serviceProvider, interruptHandler, dataManager, logger)
     {
         _movementController = movementController;
         _gatheringPointRegistry = gatheringPointRegistry;
diff --git a/Questionable/Controller/InterruptHandler.cs b/Questionable/Controller/InterruptHandler.cs
new file mode 100644 (file)
index 0000000..9171432
--- /dev/null
@@ -0,0 +1,165 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+using Dalamud.Game;
+using Dalamud.Hooking;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.Game.Character;
+using FFXIVClientStructs.FFXIV.Common.Math;
+using JetBrains.Annotations;
+using Microsoft.Extensions.Logging;
+using Questionable.Data;
+
+namespace Questionable.Controller;
+
+internal sealed unsafe class InterruptHandler : IDisposable
+{
+    private readonly Hook<ProcessActionEffect> _processActionEffectHook;
+    private readonly IClientState _clientState;
+    private readonly TerritoryData _territoryData;
+    private readonly ILogger<InterruptHandler> _logger;
+
+    private delegate void ProcessActionEffect(uint sourceId, Character* sourceCharacter, Vector3* pos,
+        EffectHeader* effectHeader, EffectEntry* effectArray, ulong* effectTail);
+
+    public InterruptHandler(IGameInteropProvider gameInteropProvider, IClientState clientState,
+        TerritoryData territoryData, ILogger<InterruptHandler> logger)
+    {
+        _clientState = clientState;
+        _territoryData = territoryData;
+        _logger = logger;
+        _processActionEffectHook =
+            gameInteropProvider.HookFromSignature<ProcessActionEffect>(Signatures.ActionEffect,
+                HandleProcessActionEffect);
+        _processActionEffectHook.Enable();
+    }
+
+    public event EventHandler? Interrupted;
+
+    private void HandleProcessActionEffect(uint sourceId, Character* sourceCharacter, Vector3* pos,
+        EffectHeader* effectHeader, EffectEntry* effectArray, ulong* effectTail)
+    {
+        try
+        {
+            if (!_territoryData.IsDutyInstance(_clientState.TerritoryType))
+            {
+                for (int i = 0; i < effectHeader->TargetCount; i++)
+                {
+                    uint targetId = (uint)(effectTail[i] & uint.MaxValue);
+                    EffectEntry* effect = effectArray + 8 * i;
+
+                    if (targetId == _clientState.LocalPlayer?.GameObjectId &&
+                        effect->Type is EActionEffectType.Damage or EActionEffectType.BlockedDamage
+                            or EActionEffectType.ParriedDamage)
+                    {
+                        _logger.LogTrace("Damage action effect on self, from {SourceId} ({EffectType})", sourceId,
+                            effect->Type);
+                        Interrupted?.Invoke(this, EventArgs.Empty);
+                        break;
+                    }
+                }
+            }
+        }
+        catch (Exception e)
+        {
+            _logger.LogWarning(e, "Unable to process action effect");
+        }
+        finally
+        {
+            _processActionEffectHook.Original(sourceId, sourceCharacter, pos, effectHeader, effectArray, effectTail);
+        }
+    }
+
+    public void Dispose()
+    {
+        _processActionEffectHook.Disable();
+        _processActionEffectHook.Dispose();
+    }
+
+    private static class Signatures
+    {
+        internal const string ActionEffect = "40 ?? 56 57 41 ?? 41 ?? 41 ?? 48 ?? ?? ?? ?? ?? ?? ?? 48";
+    }
+
+    [StructLayout(LayoutKind.Explicit)]
+    private struct EffectEntry
+    {
+        [FieldOffset(0)] public EActionEffectType Type;
+        [FieldOffset(1)] public byte Param0;
+        [FieldOffset(2)] public byte Param1;
+        [FieldOffset(3)] public byte Param2;
+        [FieldOffset(4)] public byte Mult;
+        [FieldOffset(5)] public byte Flags;
+        [FieldOffset(6)] public ushort Value;
+
+        public byte AttackType => (byte)(Param1 & 0xF);
+        public uint Damage => Mult == 0 ? Value : Value + ((uint)ushort.MaxValue + 1) * Mult;
+
+        public override string ToString()
+        {
+            return
+                $"Type: {Type}, p0: {Param0:D3}, p1: {Param1:D3}, p2: {Param2:D3} 0x{Param2:X2} '{Convert.ToString(Param2, 2).PadLeft(8, '0')}', mult: {Mult:D3}, flags: {Flags:D3} | {Convert.ToString(Flags, 2).PadLeft(8, '0')}, value: {Value:D6} ATTACK TYPE: {AttackType}";
+        }
+    }
+
+    [StructLayout(LayoutKind.Explicit)]
+    private struct EffectHeader
+    {
+        [FieldOffset(0)] public ulong AnimationTargetId;
+        [FieldOffset(8)] public uint ActionID;
+        [FieldOffset(12)] public uint GlobalEffectCounter;
+        [FieldOffset(16)] public float AnimationLockTime;
+        [FieldOffset(20)] public uint SomeTargetID;
+        [FieldOffset(24)] public ushort SourceSequence;
+        [FieldOffset(26)] public ushort Rotation;
+        [FieldOffset(28)] public ushort AnimationId;
+        [FieldOffset(30)] public byte Variation;
+        [FieldOffset(31)] public ActionType ActionType;
+        [FieldOffset(33)] public byte TargetCount;
+    }
+
+    [UsedImplicitly(ImplicitUseTargetFlags.Members)]
+    private enum EActionEffectType : byte
+    {
+        None = 0,
+        Miss = 1,
+        FullResist = 2,
+        Damage = 3,
+        Heal = 4,
+        BlockedDamage = 5,
+        ParriedDamage = 6,
+        Invulnerable = 7,
+        NoEffectText = 8,
+        Unknown0 = 9,
+        MpLoss = 10,
+        MpGain = 11,
+        TpLoss = 12,
+        TpGain = 13,
+        ApplyStatusEffectTarget = 14,
+        ApplyStatusEffectSource = 15,
+        RecoveredFromStatusEffect = 16,
+        LoseStatusEffectTarget = 17,
+        LoseStatusEffectSource = 18,
+        StatusNoEffect = 20,
+        ThreatPosition = 24,
+        EnmityAmountUp = 25,
+        EnmityAmountDown = 26,
+        StartActionCombo = 27,
+        ComboSucceed = 28,
+        Retaliation = 29,
+        Knockback = 32,
+        Attract1 = 33, //Here is an issue bout knockback. some is 32 some is 33.
+        Attract2 = 34,
+        Mount = 40,
+        FullResistStatus = 52,
+        FullResistStatus2 = 55,
+        VFX = 59,
+        Gauge = 60,
+        JobGauge = 61,
+        SetModelState = 72,
+        SetHP = 73,
+        PartialInvulnerable = 74,
+        Interrupt = 75,
+    }
+}
index 6055a68c64bfb5102b8c033ee52682f675f8a72d..c9aec197ccec489ec09a4912f8c25b228017fab7 100644 (file)
@@ -17,26 +17,29 @@ using Mount = Questionable.Controller.Steps.Common.Mount;
 
 namespace Questionable.Controller;
 
-internal abstract class MiniTaskController<T>
+internal abstract class MiniTaskController<T> : IDisposable
 {
     protected readonly TaskQueue _taskQueue = new();
 
     private readonly IChatGui _chatGui;
     private readonly ICondition _condition;
     private readonly IServiceProvider _serviceProvider;
+    private readonly InterruptHandler _interruptHandler;
     private readonly ILogger<T> _logger;
 
     private readonly string _actionCanceledText;
 
     protected MiniTaskController(IChatGui chatGui, ICondition condition, IServiceProvider serviceProvider,
-        IDataManager dataManager, ILogger<T> logger)
+        InterruptHandler interruptHandler, IDataManager dataManager, ILogger<T> logger)
     {
         _chatGui = chatGui;
         _logger = logger;
         _serviceProvider = serviceProvider;
+        _interruptHandler = interruptHandler;
         _condition = condition;
 
         _actionCanceledText = dataManager.GetString<LogMessage>(1314, x => x.Text)!;
+        _interruptHandler.Interrupted += HandleInterruption;
     }
 
     protected virtual void UpdateCurrentTask()
@@ -198,8 +201,21 @@ internal abstract class MiniTaskController<T>
         if (!isHandled)
         {
             if (GameFunctions.GameStringEquals(_actionCanceledText, message.TextValue) &&
-                !_condition[ConditionFlag.InFlight])
+                !_condition[ConditionFlag.InFlight] &&
+                _taskQueue.CurrentTaskExecutor?.ShouldInterruptOnDamage() == true)
                 InterruptQueueWithCombat();
         }
     }
+
+    protected virtual void HandleInterruption(object? sender, EventArgs e)
+    {
+        if (!_condition[ConditionFlag.InFlight] &&
+            _taskQueue.CurrentTaskExecutor?.ShouldInterruptOnDamage() == true)
+            InterruptQueueWithCombat();
+    }
+
+    public virtual void Dispose()
+    {
+        _interruptHandler.Interrupted -= HandleInterruption;
+    }
 }
index 9d17fed1dea46592bc154005bf1f3755eb4016b3..d514f267be8e4d124c3c3f4bcc6d513eb4663621 100644 (file)
@@ -75,8 +75,9 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
         YesAlreadyIpc yesAlreadyIpc,
         TaskCreator taskCreator,
         IServiceProvider serviceProvider,
+        InterruptHandler interruptHandler,
         IDataManager dataManager)
-        : base(chatGui, condition, serviceProvider, dataManager, logger)
+        : base(chatGui, condition, serviceProvider, interruptHandler, dataManager, logger)
     {
         _clientState = clientState;
         _gameFunctions = gameFunctions;
@@ -801,11 +802,23 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
         _gatheringController.OnNormalToast(message);
     }
 
-    public void Dispose()
+    protected override void HandleInterruption(object? sender, EventArgs e)
+    {
+        if (!IsRunning)
+            return;
+
+        if (AutomationType == EAutomationType.Manual)
+            return;
+
+        base.HandleInterruption(sender, e);
+    }
+
+    public override void Dispose()
     {
         _toastGui.ErrorToast -= OnErrorToast;
         _toastGui.Toast -= OnNormalToast;
         _condition.ConditionChange -= OnConditionChange;
+        base.Dispose();
     }
 
     public sealed record StepProgress(
index 1e03d8e9708ec6700cfad37fed358f9b97bbf104..4f35f4d97d8bf70742c06169183565af5b90040c 100644 (file)
@@ -110,6 +110,8 @@ internal static class Mount
                 ? ETaskResult.TaskComplete
                 : ETaskResult.StillRunning;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     internal enum MountResult
@@ -197,6 +199,8 @@ internal static class Mount
 
             return false;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     public enum EMountIf
index 7f4b02614b8ac1c4f2c170ad3c661beaa133e8f9..3ac7758d74b2eca67cbda5905ca8d48ebdf38e90 100644 (file)
@@ -61,5 +61,7 @@ internal static class NextQuest
         }
 
         public override ETaskResult Update() => ETaskResult.TaskComplete;
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 }
index cf116028ebc9aaa7ee5a6f8c0efc4f9da65bfadc..6d8bbcec661eb54a1a291cdbf12d42b05dba6b6a 100644 (file)
@@ -104,5 +104,7 @@ internal static class SendNotification
         }
 
         public override ETaskResult Update() => ETaskResult.TaskComplete;
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 }
index 367fdfec0ab62d6bdb85e47046e20c09bf62c060..8203c05688bfec1a19c7fe81ecfe6fb21ba2122a 100644 (file)
@@ -25,5 +25,7 @@ internal static class WaitCondition
 
             return DateTime.Now >= _continueAt ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 }
index 0f4c8c7f2d659ecfd5d0face5728bd6f5da56e32..bf4ab4aa91d86d34249aaaef09940af62f0f45f4 100644 (file)
@@ -236,6 +236,8 @@ internal static class DoGather
             EAction action = PickAction(minerAction, botanistAction);
             return ActionManager.Instance()->GetActionStatus(ActionType.Action, (uint)action) == 0;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     [SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Local")]
index 2b91f3538d579f2c3bb81e138211de5f59a2e2d3..fcd5efadff5ee801a4a03aa4e6fb023fdfb459aa 100644 (file)
@@ -198,6 +198,8 @@ internal static class DoGatherCollectable
             else
                 return botanistAction;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     [SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Local")]
index 38fa30cd65ae44445bfe1f899889fe71a515a18f..bc183013ec31ff9c1d9a3a9f1f874a5d397c8b57 100644 (file)
@@ -59,5 +59,6 @@ internal static class MoveToLandingLocation
 
         public override ETaskResult Update() => moveExecutor.Update();
         public bool OnErrorToast(SeString message) => moveExecutor.OnErrorToast(message);
+        public override bool ShouldInterruptOnDamage() => moveExecutor.ShouldInterruptOnDamage();
     }
 }
index caf2b0f46387ad1c662ea13d6ded1d2c68625310..0483605bd169d997112e7eccfe28dfe40e7f7bd0 100644 (file)
@@ -80,5 +80,8 @@ internal static class TurnInDelivery
             addon->FireCallback(2, pickGatheringItem);
             return ETaskResult.StillRunning;
         }
+
+        // not even sure if any turn-in npcs are NEAR mobs; but we also need to be on a gathering/crafting job
+        public override bool ShouldInterruptOnDamage() => false;
     }
 }
index 7255fa0bfb71f3fdc8a5f8fc72600d3bf049278c..f7f9750208ce32e663740d42aa6b8a3ffcefd3b3 100644 (file)
@@ -124,6 +124,8 @@ internal static class Action
 
             return ETaskResult.TaskComplete;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     internal sealed record UseMudraOnObject(uint DataId, EAction Action) : ITask
@@ -187,5 +189,7 @@ internal static class Action
             logger.LogError("Unable to find relevant combo for {Action}", Task.Action);
             return ETaskResult.TaskComplete;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 }
index b244bbea9f6fe35e0c594c87005fa7141d1f58f8..7632e574978c5cb5700cf85b2da4f148278c067c 100644 (file)
@@ -65,5 +65,7 @@ internal static class AetherCurrent
             gameFunctions.IsAetherCurrentUnlocked(Task.AetherCurrentId)
                 ? ETaskResult.TaskComplete
                 : ETaskResult.StillRunning;
+
+        public override bool ShouldInterruptOnDamage() => true;
     }
 }
index b1af7fe9ad849a7a833b98444881403c1107cdda..db2c5212a1b8497254d956097761f728c798895e 100644 (file)
@@ -53,5 +53,7 @@ internal static class AethernetShard
             aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation)
                 ? ETaskResult.TaskComplete
                 : ETaskResult.StillRunning;
+
+        public override bool ShouldInterruptOnDamage() => true;
     }
 }
index d97547763e730a0b3c39a3b0aa3fa075d9616137..dd40fc69da3456e3dab788d32b95b48e64f909bf 100644 (file)
@@ -52,5 +52,7 @@ internal static class Aetheryte
             aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation)
                 ? ETaskResult.TaskComplete
                 : ETaskResult.StillRunning;
+
+        public override bool ShouldInterruptOnDamage() => true;
     }
 }
index 463c32cec501bd17023a08f6cf32476b4e5f068d..e63058a81f858698ea6f9d3b8b73cedaa58b5661 100644 (file)
@@ -190,5 +190,7 @@ internal static class Combat
                 return ETaskResult.TaskComplete;
             }
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 }
index b5389774cb921ba341e4676e103fc1eeb85c82d1..eea9cd871a50f34ed316101240a6a0f1ea298f00 100644 (file)
@@ -71,6 +71,8 @@ internal static class Dive
             return base.Update();
         }
 
+        public override bool ShouldInterruptOnDamage() => false;
+
         protected override ETaskResult UpdateInternal()
         {
             if (condition[ConditionFlag.Diving])
index 5e20accf0641d94ce87fba92e4776b7ef45fa36c..b59f8ce7047c00012670c66469ec12724a8c0740 100644 (file)
@@ -93,6 +93,8 @@ internal static class Duty
                 ? ETaskResult.TaskComplete
                 : ETaskResult.StillRunning;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     internal sealed record WaitAutoDutyTask(uint ContentFinderConditionId) : ITask
@@ -117,6 +119,8 @@ internal static class Duty
                 ? ETaskResult.TaskComplete
                 : ETaskResult.StillRunning;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     internal sealed record OpenDutyFinderTask(uint ContentFinderConditionId) : ITask
@@ -138,5 +142,7 @@ internal static class Duty
         }
 
         public override ETaskResult Update() => ETaskResult.TaskComplete;
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 }
index 085b0356e714f5bd7c245bb97cbf3312168ae6ea..d6dd7146701247226c8a571cf4b56f80960c2ce3 100644 (file)
@@ -51,6 +51,8 @@ internal static class Emote
             chatFunctions.UseEmote(Task.DataId, Task.Emote);
             return true;
         }
+
+        public override bool ShouldInterruptOnDamage() => true;
     }
 
     internal sealed record UseOnSelf(EEmote Emote) : ITask
@@ -65,5 +67,7 @@ internal static class Emote
             chatFunctions.UseEmote(Task.Emote);
             return true;
         }
+
+        public override bool ShouldInterruptOnDamage() => true;
     }
 }
index f5cd4e111c2ea740fbf26072d04e3344c301dd9f..d761926d61bff8d70c171c34c02a16e2a0bb077c 100644 (file)
@@ -183,5 +183,7 @@ internal static class EquipItem
 
             return false;
         }
+
+        public override bool ShouldInterruptOnDamage() => true;
     }
 }
index 3b2be0f17d65abaef21de55653e2764dd968d1c6..295bb8cc1ef8d000da90a813e2165891455bf9f9 100644 (file)
@@ -98,5 +98,7 @@ internal static class EquipRecommended
 
             return ETaskResult.TaskComplete;
         }
+
+        public override bool ShouldInterruptOnDamage() => true;
     }
 }
index 0ca70c11f0f01aa5e7454c3565b43abaadff22ce..0073349b3eeb1972ec48ac3ca67d7f661764156f 100644 (file)
@@ -228,6 +228,8 @@ internal static class Interact
             }
         }
 
+        public override bool ShouldInterruptOnDamage() => true;
+
         private enum EInteractionState
         {
             None,
index f7b9892d53ed67a39f6cfa2b5e88cfa47cb5cc4f..3238405c8dccc39b3cae70d922f24e8e766ad829 100644 (file)
@@ -80,6 +80,8 @@ internal static class Jump
 
             return ETaskResult.TaskComplete;
         }
+
+        public override bool ShouldInterruptOnDamage() => true;
     }
 
     internal sealed class DoSingleJump(
index f13ab4ab97992618997d1bf6fe9c34047e2c8cb6..ffb5621524d08812870d22354df95297d61be528 100644 (file)
@@ -48,5 +48,7 @@ internal static class Say
             chatFunctions.ExecuteCommand($"/say {Task.ChatMessage}");
             return true;
         }
+
+        public override bool ShouldInterruptOnDamage() => true;
     }
 }
index 746f7394cf387373dd2b936d8f670f81a07bb44b..c9b2b4ca359acee69c706ac3b554a34c7d0cb483 100644 (file)
@@ -43,5 +43,7 @@ internal static class StatusOff
         {
             return gameFunctions.HasStatus(Task.Status) ? ETaskResult.StillRunning : ETaskResult.TaskComplete;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 }
index bf779a0248efecc4e03d4a5f0c865ac8827e037b..abc427ad484feaf0a54a7732a6b4646479e3aa47 100644 (file)
@@ -205,6 +205,8 @@ internal static class UseItem
             else
                 return TimeSpan.FromSeconds(5);
         }
+
+        public override bool ShouldInterruptOnDamage() => true;
     }
 
     internal sealed record UseOnGround(
index ab584cefc69f8e7c9cd83a5e186acfa3ef650e02..31cf47055aff63c824aa8ef1b3c7eb1fc7b0f912 100644 (file)
@@ -50,6 +50,8 @@ internal static class InitiateLeve
 
             return ETaskResult.TaskComplete;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     internal sealed record OpenJournal(ElementId ElementId) : ITask
@@ -85,6 +87,8 @@ internal static class InitiateLeve
 
             return ETaskResult.StillRunning;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     internal sealed record Initiate(ElementId ElementId) : ITask
@@ -111,6 +115,8 @@ internal static class InitiateLeve
 
             return ETaskResult.StillRunning;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     internal sealed class SelectDifficulty : ITask
@@ -138,5 +144,7 @@ internal static class InitiateLeve
 
             return ETaskResult.StillRunning;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 }
index e63b69c0ab31c435a474493669f6c748d7ebcea6..460aa440b1dd694c46d3165cdce36ab75e62d7a8 100644 (file)
@@ -269,5 +269,7 @@ internal static class AethernetShortcut
 
             return ETaskResult.TaskComplete;
         }
+
+        public override bool ShouldInterruptOnDamage() => true;
     }
 }
index af575333413f4f8a7d22a1740841de49a1b385b9..b2748b183626a2a848db9ec13b193d1b1ea36e3a 100644 (file)
@@ -221,6 +221,8 @@ internal static class AetheryteShortcut
         }
 
         public override bool WasInterrupted() => condition[ConditionFlag.InCombat] || base.WasInterrupted();
+
+        public override bool ShouldInterruptOnDamage() => true;
     }
 
     internal sealed record MoveAwayFromAetheryte(EAetheryteLocation TargetAetheryte) : ITask
@@ -264,5 +266,7 @@ internal static class AetheryteShortcut
         }
 
         public override ETaskResult Update() => moveExecutor.Update();
+
+        public override bool ShouldInterruptOnDamage() => true;
     }
 }
index 26493ca0255b632d245aae9c7609b00307254142..d986368f6649620a2283dade6eaa24c430c7f455 100644 (file)
@@ -133,5 +133,8 @@ internal static class Craft
             return inventoryManager->GetInventoryItemCount(Task.ItemId, isHq: false, checkEquipped: false)
                    + inventoryManager->GetInventoryItemCount(Task.ItemId, isHq: true, checkEquipped: false);
         }
+
+        // we're on a crafting class, so combat doesn't make much sense (we also can't change classes in combat...)
+        public override bool ShouldInterruptOnDamage() => false;
     }
 }
index 73dd8d12f4c80884666f47b06ccf358e8b5cafa9..f4aad9c943b190297d8c3dfffa52ccaf8fb26585 100644 (file)
@@ -100,6 +100,8 @@ internal static class Gather
                        minCollectability: (short)itemToGather.Collectability) >=
                    itemToGather.ItemCount;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     internal sealed record GatheringTask(
@@ -140,6 +142,9 @@ internal static class Gather
             gatheringController.OnErrorToast(ref message, ref isHandled);
             return isHandled;
         }
+
+        // we're on a gathering class, so combat doesn't make much sense (we also can't change classes in combat...)
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     /// <summary>
@@ -154,5 +159,7 @@ internal static class Gather
     {
         protected override bool Start() => true;
         public override ETaskResult Update() => ETaskResult.TaskComplete;
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 }
index 4df2870facd576ebc822fc50a9094ccc0830062e..656891080aa982f30bf01abca9ba187fe5c30569 100644 (file)
@@ -286,6 +286,8 @@ internal static class MoveTo
             return base.WasInterrupted();
         }
 
+        public override bool ShouldInterruptOnDamage() => false;
+
         public bool OnErrorToast(SeString message)
         {
             if (GameFunctions.GameStringEquals(_cannotExecuteAtThisTime, message.TextValue))
@@ -302,6 +304,8 @@ internal static class MoveTo
         protected override bool Start() => true;
 
         public override ETaskResult Update() => ETaskResult.TaskComplete;
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     internal sealed record MoveTask(
@@ -361,6 +365,8 @@ internal static class MoveTo
 
             return ETaskResult.TaskComplete;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     internal sealed class LandTask : ITask
@@ -421,5 +427,7 @@ internal static class MoveTo
 
             return false;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 }
index 408b92f7e7ed54bb8d545dbab1afb2308abfdbaf..c7abcae7b029991c08b194a8ab1df206c7c80409 100644 (file)
@@ -74,5 +74,7 @@ internal static class RedeemRewardItems
 
             return DateTime.Now <= _continueAt ? ETaskResult.StillRunning : ETaskResult.TaskComplete;
         }
+
+        public override bool ShouldInterruptOnDamage() => true;
     }
 }
index 5abab059423ce88d1b2adeab388a4fdff8b108b0..ebf1dc1f2c7840f66db1890dadb4b7f1f64a051d 100644 (file)
@@ -315,5 +315,7 @@ internal static class SkipCondition
         }
 
         public override ETaskResult Update() => ETaskResult.SkipRemainingTasksForStep;
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 }
index f706535927a91614bdd5d625002c1e108759ebf3..de58cac36dcab0b0c0b846476d51a5daa7d051f7 100644 (file)
@@ -31,5 +31,7 @@ internal static class StepDisabled
             logger.LogInformation("Skipping step, as it is disabled");
             return ETaskResult.SkipRemainingTasksForStep;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 }
index 59477feca962fc6e739927d73ab33a371aad8ab0..18bfef7e5ca125a2e9a01ee21ae56ae9270b8477 100644 (file)
@@ -52,5 +52,8 @@ internal static class SwitchClassJob
         }
 
         protected override ETaskResult UpdateInternal() => ETaskResult.TaskComplete;
+
+        // can we even take damage while switching jobs? we should be out of combat...
+        public override bool ShouldInterruptOnDamage() => false;
     }
 }
index 0b3a02ba97e551e7c431205abcacafeaeba82541..d39c7c2a36f06b8d4094bcc2b4e5b025b9a91744 100644 (file)
@@ -157,6 +157,8 @@ internal static class WaitAtEnd
             Delay = Task.Delay;
             return true;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     internal sealed class WaitNextStepOrSequence : ITask
@@ -169,6 +171,8 @@ internal static class WaitAtEnd
         protected override bool Start() => true;
 
         public override ETaskResult Update() => ETaskResult.StillRunning;
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     internal sealed record WaitForCompletionFlags(QuestId Quest, QuestStep Step) : ITask
@@ -190,6 +194,8 @@ internal static class WaitAtEnd
                 ? ETaskResult.TaskComplete
                 : ETaskResult.StillRunning;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     internal sealed record WaitObjectAtPosition(
@@ -209,6 +215,8 @@ internal static class WaitAtEnd
             gameFunctions.IsObjectAtPosition(Task.DataId, Task.Destination, Task.Distance)
                 ? ETaskResult.TaskComplete
                 : ETaskResult.StillRunning;
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     internal sealed record WaitQuestAccepted(ElementId ElementId) : ITask
@@ -226,6 +234,8 @@ internal static class WaitAtEnd
                 ? ETaskResult.TaskComplete
                 : ETaskResult.StillRunning;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     internal sealed record WaitQuestCompleted(ElementId ElementId) : ITask
@@ -241,6 +251,8 @@ internal static class WaitAtEnd
         {
             return questFunctions.IsQuestComplete(Task.ElementId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
         }
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     internal sealed record NextStep(ElementId ElementId, int Sequence) : ILastTask
@@ -253,6 +265,8 @@ internal static class WaitAtEnd
         protected override bool Start() => true;
 
         public override ETaskResult Update() => ETaskResult.NextStep;
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 
     internal sealed class EndAutomation : ILastTask
@@ -268,5 +282,7 @@ internal static class WaitAtEnd
         protected override bool Start() => true;
 
         public override ETaskResult Update() => ETaskResult.End;
+
+        public override bool ShouldInterruptOnDamage() => false;
     }
 }
index c2c304b459541a9d0eaccb77112eb6e1debeeb7a..8386e63653f913ae6c77cee561321a1345091fa4 100644 (file)
@@ -31,6 +31,7 @@ internal static class WaitAtStart
             Delay = Task.Delay;
             return true;
         }
-    }
 
+        public override bool ShouldInterruptOnDamage() => false;
+    }
 }
index d0315dbcd1156bbe8c83116729a5df0c02387936..30e10b643f37154c28617b5ba59adecba65b3c9d 100644 (file)
@@ -13,6 +13,8 @@ internal interface ITaskExecutor
 
     bool Start(ITask task);
 
+    bool ShouldInterruptOnDamage();
+
     bool WasInterrupted();
 
     ETaskResult Update();
@@ -56,4 +58,6 @@ internal abstract class TaskExecutor<T> : ITaskExecutor
     }
 
     public abstract ETaskResult Update();
+
+    public abstract bool ShouldInterruptOnDamage();
 }
index a4b5bae9e166f83cd2d68ddbe8a79a937740c232..e0794c2d75aec01d4eaac04cf0911d02e39ec74a 100644 (file)
@@ -247,6 +247,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddSingleton<GatheringController>();
         serviceCollection.AddSingleton<ContextMenuController>();
         serviceCollection.AddSingleton<ShopController>();
+        serviceCollection.AddSingleton<InterruptHandler>();
 
         serviceCollection.AddSingleton<CraftworksSupplyController>();
         serviceCollection.AddSingleton<CreditsController>();