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;
--- /dev/null
+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,
+ }
+}
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()
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;
+ }
}
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;
_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(
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
internal enum MountResult
return false;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
public enum EMountIf
}
public override ETaskResult Update() => ETaskResult.TaskComplete;
+
+ public override bool ShouldInterruptOnDamage() => false;
}
}
}
public override ETaskResult Update() => ETaskResult.TaskComplete;
+
+ public override bool ShouldInterruptOnDamage() => false;
}
}
return DateTime.Now >= _continueAt ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
}
EAction action = PickAction(minerAction, botanistAction);
return ActionManager.Instance()->GetActionStatus(ActionType.Action, (uint)action) == 0;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
[SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Local")]
else
return botanistAction;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
[SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Local")]
public override ETaskResult Update() => moveExecutor.Update();
public bool OnErrorToast(SeString message) => moveExecutor.OnErrorToast(message);
+ public override bool ShouldInterruptOnDamage() => moveExecutor.ShouldInterruptOnDamage();
}
}
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;
}
}
return ETaskResult.TaskComplete;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
internal sealed record UseMudraOnObject(uint DataId, EAction Action) : ITask
logger.LogError("Unable to find relevant combo for {Action}", Task.Action);
return ETaskResult.TaskComplete;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
}
gameFunctions.IsAetherCurrentUnlocked(Task.AetherCurrentId)
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
+
+ public override bool ShouldInterruptOnDamage() => true;
}
}
aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation)
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
+
+ public override bool ShouldInterruptOnDamage() => true;
}
}
aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation)
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
+
+ public override bool ShouldInterruptOnDamage() => true;
}
}
return ETaskResult.TaskComplete;
}
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
}
return base.Update();
}
+ public override bool ShouldInterruptOnDamage() => false;
+
protected override ETaskResult UpdateInternal()
{
if (condition[ConditionFlag.Diving])
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
internal sealed record WaitAutoDutyTask(uint ContentFinderConditionId) : ITask
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
internal sealed record OpenDutyFinderTask(uint ContentFinderConditionId) : ITask
}
public override ETaskResult Update() => ETaskResult.TaskComplete;
+
+ public override bool ShouldInterruptOnDamage() => false;
}
}
chatFunctions.UseEmote(Task.DataId, Task.Emote);
return true;
}
+
+ public override bool ShouldInterruptOnDamage() => true;
}
internal sealed record UseOnSelf(EEmote Emote) : ITask
chatFunctions.UseEmote(Task.Emote);
return true;
}
+
+ public override bool ShouldInterruptOnDamage() => true;
}
}
return false;
}
+
+ public override bool ShouldInterruptOnDamage() => true;
}
}
return ETaskResult.TaskComplete;
}
+
+ public override bool ShouldInterruptOnDamage() => true;
}
}
}
}
+ public override bool ShouldInterruptOnDamage() => true;
+
private enum EInteractionState
{
None,
return ETaskResult.TaskComplete;
}
+
+ public override bool ShouldInterruptOnDamage() => true;
}
internal sealed class DoSingleJump(
chatFunctions.ExecuteCommand($"/say {Task.ChatMessage}");
return true;
}
+
+ public override bool ShouldInterruptOnDamage() => true;
}
}
{
return gameFunctions.HasStatus(Task.Status) ? ETaskResult.StillRunning : ETaskResult.TaskComplete;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
}
else
return TimeSpan.FromSeconds(5);
}
+
+ public override bool ShouldInterruptOnDamage() => true;
}
internal sealed record UseOnGround(
return ETaskResult.TaskComplete;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
internal sealed record OpenJournal(ElementId ElementId) : ITask
return ETaskResult.StillRunning;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
internal sealed record Initiate(ElementId ElementId) : ITask
return ETaskResult.StillRunning;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
internal sealed class SelectDifficulty : ITask
return ETaskResult.StillRunning;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
}
return ETaskResult.TaskComplete;
}
+
+ public override bool ShouldInterruptOnDamage() => true;
}
}
}
public override bool WasInterrupted() => condition[ConditionFlag.InCombat] || base.WasInterrupted();
+
+ public override bool ShouldInterruptOnDamage() => true;
}
internal sealed record MoveAwayFromAetheryte(EAetheryteLocation TargetAetheryte) : ITask
}
public override ETaskResult Update() => moveExecutor.Update();
+
+ public override bool ShouldInterruptOnDamage() => true;
}
}
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;
}
}
minCollectability: (short)itemToGather.Collectability) >=
itemToGather.ItemCount;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
internal sealed record GatheringTask(
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>
{
protected override bool Start() => true;
public override ETaskResult Update() => ETaskResult.TaskComplete;
+
+ public override bool ShouldInterruptOnDamage() => false;
}
}
return base.WasInterrupted();
}
+ public override bool ShouldInterruptOnDamage() => false;
+
public bool OnErrorToast(SeString message)
{
if (GameFunctions.GameStringEquals(_cannotExecuteAtThisTime, message.TextValue))
protected override bool Start() => true;
public override ETaskResult Update() => ETaskResult.TaskComplete;
+
+ public override bool ShouldInterruptOnDamage() => false;
}
internal sealed record MoveTask(
return ETaskResult.TaskComplete;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
internal sealed class LandTask : ITask
return false;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
}
return DateTime.Now <= _continueAt ? ETaskResult.StillRunning : ETaskResult.TaskComplete;
}
+
+ public override bool ShouldInterruptOnDamage() => true;
}
}
}
public override ETaskResult Update() => ETaskResult.SkipRemainingTasksForStep;
+
+ public override bool ShouldInterruptOnDamage() => false;
}
}
logger.LogInformation("Skipping step, as it is disabled");
return ETaskResult.SkipRemainingTasksForStep;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
}
}
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;
}
}
Delay = Task.Delay;
return true;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
internal sealed class WaitNextStepOrSequence : ITask
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
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
internal sealed record WaitObjectAtPosition(
gameFunctions.IsObjectAtPosition(Task.DataId, Task.Destination, Task.Distance)
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
+
+ public override bool ShouldInterruptOnDamage() => false;
}
internal sealed record WaitQuestAccepted(ElementId ElementId) : ITask
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
internal sealed record WaitQuestCompleted(ElementId ElementId) : ITask
{
return questFunctions.IsQuestComplete(Task.ElementId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
}
+
+ public override bool ShouldInterruptOnDamage() => false;
}
internal sealed record NextStep(ElementId ElementId, int Sequence) : ILastTask
protected override bool Start() => true;
public override ETaskResult Update() => ETaskResult.NextStep;
+
+ public override bool ShouldInterruptOnDamage() => false;
}
internal sealed class EndAutomation : ILastTask
protected override bool Start() => true;
public override ETaskResult Update() => ETaskResult.End;
+
+ public override bool ShouldInterruptOnDamage() => false;
}
}
Delay = Task.Delay;
return true;
}
- }
+ public override bool ShouldInterruptOnDamage() => false;
+ }
}
bool Start(ITask task);
+ bool ShouldInterruptOnDamage();
+
bool WasInterrupted();
ETaskResult Update();
}
public abstract ETaskResult Update();
+
+ public abstract bool ShouldInterruptOnDamage();
}
serviceCollection.AddSingleton<GatheringController>();
serviceCollection.AddSingleton<ContextMenuController>();
serviceCollection.AddSingleton<ShopController>();
+ serviceCollection.AddSingleton<InterruptHandler>();
serviceCollection.AddSingleton<CraftworksSupplyController>();
serviceCollection.AddSingleton<CreditsController>();