diff --git a/Questionable/Controller/CombatController.cs b/Questionable/Controller/CombatController.cs index d3572780c..eaf311cb0 100644 --- a/Questionable/Controller/CombatController.cs +++ b/Questionable/Controller/CombatController.cs @@ -114,8 +114,6 @@ internal sealed class CombatController : IDisposable else { var nextTarget = FindNextTarget(); - _logger.LogInformation("NT → {NT}", nextTarget); - if (nextTarget is { IsDead: false }) SetTarget(nextTarget); } diff --git a/Questionable/Controller/GatheringController.cs b/Questionable/Controller/GatheringController.cs index bbad03d3d..48147fc7c 100644 --- a/Questionable/Controller/GatheringController.cs +++ b/Questionable/Controller/GatheringController.cs @@ -43,6 +43,7 @@ internal sealed unsafe class GatheringController : MiniTaskController _logger; private readonly Regex _revisitRegex; private CurrentRequest? _currentRequest; @@ -51,6 +52,7 @@ internal sealed unsafe class GatheringController : MiniTaskController(5574, x => x.Text, pluginLog) ?? throw new InvalidDataException("No regex found for revisit message"); diff --git a/Questionable/Controller/MiniTaskController.cs b/Questionable/Controller/MiniTaskController.cs index 0ec7bd675..b3e6bd548 100644 --- a/Questionable/Controller/MiniTaskController.cs +++ b/Questionable/Controller/MiniTaskController.cs @@ -1,23 +1,35 @@ using System; using System.Collections.Generic; using System.Linq; +using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.Services; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps; +using Questionable.Controller.Steps.Common; +using Questionable.Controller.Steps.Interactions; using Questionable.Controller.Steps.Shared; +using Questionable.Model.Questing; namespace Questionable.Controller; internal abstract class MiniTaskController { - protected readonly IChatGui _chatGui; - protected readonly ILogger _logger; protected readonly TaskQueue _taskQueue = new(); - protected MiniTaskController(IChatGui chatGui, ILogger logger) + private readonly IChatGui _chatGui; + private readonly Mount.Factory _mountFactory; + private readonly Combat.Factory _combatFactory; + private readonly ICondition _condition; + private readonly ILogger _logger; + + protected MiniTaskController(IChatGui chatGui, Mount.Factory mountFactory, Combat.Factory combatFactory, + ICondition condition, ILogger logger) { _chatGui = chatGui; _logger = logger; + _mountFactory = mountFactory; + _combatFactory = combatFactory; + _condition = condition; } protected virtual void UpdateCurrentTask() @@ -56,6 +68,12 @@ internal abstract class MiniTaskController ETaskResult result; try { + if (_taskQueue.CurrentTask.WasInterrupted()) + { + InterruptQueueWithCombat(); + return; + } + result = _taskQueue.CurrentTask.Update(); } catch (Exception e) @@ -122,11 +140,27 @@ internal abstract class MiniTaskController protected virtual void OnNextStep(ILastTask task) { - } public abstract void Stop(string label); public virtual IList GetRemainingTaskNames() => _taskQueue.RemainingTasks.Select(x => x.ToString() ?? "?").ToList(); + + public void InterruptQueueWithCombat() + { + _logger.LogWarning("Interrupted, attempting to resolve (if in combat)"); + if (_condition[ConditionFlag.InCombat]) + { + List tasks = []; + if (_condition[ConditionFlag.Mounted]) + tasks.Add(_mountFactory.Unmount()); + + tasks.Add(_combatFactory.CreateTask(null, false, EEnemySpawnType.QuestInterruption, [], [], [])); + tasks.Add(new WaitAtEnd.WaitDelay()); + _taskQueue.InterruptWith(tasks); + } + else + _taskQueue.InterruptWith([new WaitAtEnd.WaitDelay()]); + } } diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs index 1b0988b99..8ebf576d9 100644 --- a/Questionable/Controller/QuestController.cs +++ b/Questionable/Controller/QuestController.cs @@ -35,13 +35,13 @@ internal sealed class QuestController : MiniTaskController, IDi private readonly GatheringController _gatheringController; private readonly QuestRegistry _questRegistry; private readonly IKeyState _keyState; + private readonly IChatGui _chatGui; private readonly ICondition _condition; private readonly IToastGui _toastGui; private readonly Configuration _configuration; private readonly YesAlreadyIpc _yesAlreadyIpc; private readonly TaskCreator _taskCreator; - private readonly Mount.Factory _mountFactory; - private readonly Combat.Factory _combatFactory; + private readonly ILogger _logger; private readonly string _actionCanceledText; @@ -85,7 +85,7 @@ internal sealed class QuestController : MiniTaskController, IDi Mount.Factory mountFactory, Combat.Factory combatFactory, IDataManager dataManager) - : base(chatGui, logger) + : base(chatGui, mountFactory, combatFactory, condition, logger) { _clientState = clientState; _gameFunctions = gameFunctions; @@ -95,13 +95,13 @@ internal sealed class QuestController : MiniTaskController, IDi _gatheringController = gatheringController; _questRegistry = questRegistry; _keyState = keyState; + _chatGui = chatGui; _condition = condition; _toastGui = toastGui; _configuration = configuration; _yesAlreadyIpc = yesAlreadyIpc; _taskCreator = taskCreator; - _mountFactory = mountFactory; - _combatFactory = combatFactory; + _logger = logger; _condition.ConditionChange += OnConditionChange; _toastGui.Toast += OnNormalToast; @@ -659,6 +659,7 @@ internal sealed class QuestController : MiniTaskController, IDi } public bool IsRunning => !_taskQueue.AllTasksComplete; + public TaskQueue TaskQueue => _taskQueue; public sealed class QuestProgress { @@ -813,18 +814,6 @@ internal sealed class QuestController : MiniTaskController, IDi } } - public void InterruptQueueWithCombat() - { - _logger.LogWarning("Interrupted with action canceled message, attempting to resolve"); - List tasks = []; - if (_condition[ConditionFlag.Mounted]) - tasks.Add(_mountFactory.Unmount()); - - tasks.Add(_combatFactory.CreateTask(null, false, EEnemySpawnType.QuestInterruption, [], [], [])); - tasks.Add(new WaitAtEnd.WaitDelay()); - _taskQueue.InterruptWith(tasks); - } - public void Dispose() { _toastGui.ErrorToast -= OnErrorToast; diff --git a/Questionable/Controller/Steps/Common/AbstractDelayedTask.cs b/Questionable/Controller/Steps/Common/AbstractDelayedTask.cs index e4586fb86..abd6514f4 100644 --- a/Questionable/Controller/Steps/Common/AbstractDelayedTask.cs +++ b/Questionable/Controller/Steps/Common/AbstractDelayedTask.cs @@ -18,6 +18,8 @@ internal abstract class AbstractDelayedTask : ITask { } + public virtual InteractionProgressContext? ProgressContext() => null; + public bool Start() { _continueAt = DateTime.Now.Add(Delay); diff --git a/Questionable/Controller/Steps/Common/Mount.cs b/Questionable/Controller/Steps/Common/Mount.cs index 958bf2d74..f067066ca 100644 --- a/Questionable/Controller/Steps/Common/Mount.cs +++ b/Questionable/Controller/Steps/Common/Mount.cs @@ -44,8 +44,11 @@ internal static class Mount ILogger logger) : ITask { private bool _mountTriggered; + private InteractionProgressContext? _progressContext; private DateTime _retryAt = DateTime.MinValue; + public InteractionProgressContext? ProgressContext() => _progressContext; + public bool ShouldRedoOnInterrupt() => true; public bool Start() @@ -108,7 +111,8 @@ internal static class Mount return ETaskResult.TaskComplete; } - _mountTriggered = gameFunctions.Mount(); + _progressContext = InteractionProgressContext.FromActionUse(() => _mountTriggered = gameFunctions.Mount()); + _retryAt = DateTime.Now.AddSeconds(5); return ETaskResult.StillRunning; } diff --git a/Questionable/Controller/Steps/ITask.cs b/Questionable/Controller/Steps/ITask.cs index 0ddc96217..a8442c767 100644 --- a/Questionable/Controller/Steps/ITask.cs +++ b/Questionable/Controller/Steps/ITask.cs @@ -5,6 +5,20 @@ namespace Questionable.Controller.Steps; internal interface ITask { + InteractionProgressContext? ProgressContext() => null; + + bool WasInterrupted() + { + var progressContext = ProgressContext(); + if (progressContext != null) + { + progressContext.Update(); + return progressContext.WasInterrupted(); + } + + return false; + } + bool ShouldRedoOnInterrupt() => false; bool Start(); diff --git a/Questionable/Controller/Steps/InteractionProgressContext.cs b/Questionable/Controller/Steps/InteractionProgressContext.cs new file mode 100644 index 000000000..a943ed5f9 --- /dev/null +++ b/Questionable/Controller/Steps/InteractionProgressContext.cs @@ -0,0 +1,97 @@ +using System; +using FFXIVClientStructs.FFXIV.Client.Game; + +namespace Questionable.Controller.Steps; + +internal sealed class InteractionProgressContext +{ + private bool _firstUpdateDone; + public bool CheckSequence { get; private set; } + public int CurrentSequence { get; private set; } + + private InteractionProgressContext(bool checkSequence, int currentSequence) + { + CheckSequence = checkSequence; + CurrentSequence = currentSequence; + } + + public static unsafe InteractionProgressContext Create(bool checkSequence) + { + if (!checkSequence) + { + // this is a silly hack; we assume that the previous cast was successful + // if not for this, we'd instantly be seen as interrupted + ActionManager.Instance()->CastTimeElapsed = ActionManager.Instance()->CastTimeTotal; + } + + return new InteractionProgressContext(checkSequence, ActionManager.Instance()->LastUsedActionSequence); + } + + private static unsafe (bool, InteractionProgressContext?) FromActionUseInternal(Func func) + { + int oldSequence = ActionManager.Instance()->LastUsedActionSequence; + if (!func()) + return (false, null); + int newSequence = ActionManager.Instance()->LastUsedActionSequence; + if (oldSequence == newSequence) + return (true, null); + return (true, Create(true)); + } + + public static InteractionProgressContext? FromActionUse(Func func) + { + return FromActionUseInternal(func).Item2; + } + + public static InteractionProgressContext? FromActionUseOrDefault(Func func) + { + var result = FromActionUseInternal(func); + if (!result.Item1) + return null; + return result.Item2 ?? Create(false); + } + + public unsafe void Update() + { + if (!_firstUpdateDone) + { + int lastSequence = ActionManager.Instance()->LastUsedActionSequence; + if (!CheckSequence && lastSequence > CurrentSequence) + { + CheckSequence = true; + CurrentSequence = lastSequence; + } + + _firstUpdateDone = true; + } + } + + public unsafe bool WasSuccessful() + { + if (CheckSequence) + { + if (CurrentSequence != ActionManager.Instance()->LastUsedActionSequence || + CurrentSequence != ActionManager.Instance()->LastHandledActionSequence) + return false; + } + + return ActionManager.Instance()->CastTimeElapsed > 0 && + Math.Abs(ActionManager.Instance()->CastTimeElapsed - ActionManager.Instance()->CastTimeTotal) < 0.001f; + } + + public unsafe bool WasInterrupted() + { + if (CheckSequence) + { + if (CurrentSequence == ActionManager.Instance()->LastHandledActionSequence && + CurrentSequence == ActionManager.Instance()->LastUsedActionSequence) + return false; + } + + return ActionManager.Instance()->CastTimeElapsed == 0 && + ActionManager.Instance()->CastTimeTotal > 0; + } + + public override string ToString() => + $"IPCtx({(CheckSequence ? CurrentSequence : "-")} - {WasSuccessful()}, {WasInterrupted()})"; +} diff --git a/Questionable/Controller/Steps/Interactions/AetherCurrent.cs b/Questionable/Controller/Steps/Interactions/AetherCurrent.cs index 45507e804..cbbe68686 100644 --- a/Questionable/Controller/Steps/Interactions/AetherCurrent.cs +++ b/Questionable/Controller/Steps/Interactions/AetherCurrent.cs @@ -32,19 +32,29 @@ internal static class AetherCurrent return null; } - return new DoAttune(step.DataId.Value, step.AetherCurrentId.Value, gameFunctions, loggerFactory.CreateLogger()); + return new DoAttune(step.DataId.Value, step.AetherCurrentId.Value, gameFunctions, + loggerFactory.CreateLogger()); } } - private sealed class DoAttune(uint dataId, uint aetherCurrentId, GameFunctions gameFunctions, ILogger logger) : ITask + private sealed class DoAttune( + uint dataId, + uint aetherCurrentId, + GameFunctions gameFunctions, + ILogger logger) : ITask { + private InteractionProgressContext? _progressContext; + + public InteractionProgressContext? ProgressContext() => _progressContext; + public bool Start() { if (!gameFunctions.IsAetherCurrentUnlocked(aetherCurrentId)) { logger.LogInformation("Attuning to aether current {AetherCurrentId} / {DataId}", aetherCurrentId, dataId); - gameFunctions.InteractWith(dataId); + _progressContext = + InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(dataId)); return true; } diff --git a/Questionable/Controller/Steps/Interactions/AethernetShard.cs b/Questionable/Controller/Steps/Interactions/AethernetShard.cs index edcf4720e..ed5580c8c 100644 --- a/Questionable/Controller/Steps/Interactions/AethernetShard.cs +++ b/Questionable/Controller/Steps/Interactions/AethernetShard.cs @@ -34,12 +34,17 @@ internal static class AethernetShard GameFunctions gameFunctions, ILogger logger) : ITask { + private InteractionProgressContext? _progressContext; + + public InteractionProgressContext? ProgressContext() => _progressContext; + public bool Start() { if (!aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation)) { logger.LogInformation("Attuning to aethernet shard {AethernetShard}", aetheryteLocation); - gameFunctions.InteractWith((uint)aetheryteLocation, ObjectKind.Aetheryte); + _progressContext = InteractionProgressContext.FromActionUseOrDefault(() => + gameFunctions.InteractWith((uint)aetheryteLocation, ObjectKind.Aetheryte)); return true; } diff --git a/Questionable/Controller/Steps/Interactions/Aetheryte.cs b/Questionable/Controller/Steps/Interactions/Aetheryte.cs index 5af0a6a35..eace64099 100644 --- a/Questionable/Controller/Steps/Interactions/Aetheryte.cs +++ b/Questionable/Controller/Steps/Interactions/Aetheryte.cs @@ -33,12 +33,18 @@ internal static class Aetheryte GameFunctions gameFunctions, ILogger logger) : ITask { + private InteractionProgressContext? _progressContext; + + public InteractionProgressContext? ProgressContext() => _progressContext; + public bool Start() { if (!aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation)) { logger.LogInformation("Attuning to aetheryte {Aetheryte}", aetheryteLocation); - gameFunctions.InteractWith((uint)aetheryteLocation); + _progressContext = + InteractionProgressContext.FromActionUseOrDefault(() => + gameFunctions.InteractWith((uint)aetheryteLocation)); return true; } diff --git a/Questionable/Controller/Steps/Interactions/Interact.cs b/Questionable/Controller/Steps/Interactions/Interact.cs index a88e6dda5..020fc66e7 100644 --- a/Questionable/Controller/Steps/Interactions/Interact.cs +++ b/Questionable/Controller/Steps/Interactions/Interact.cs @@ -78,10 +78,10 @@ internal static class Interact GameFunctions gameFunctions, ICondition condition, ILogger logger) - : ITask, IConditionChangeAware + : ITask { private bool _needsUnmount; - private EInteractionState _interactionState = EInteractionState.None; + private InteractionProgressContext? _progressContext; private DateTime _continueAt = DateTime.MinValue; public Quest? Quest => quest; @@ -92,6 +92,8 @@ internal static class Interact set => interactionType = value; } + public InteractionProgressContext? ProgressContext() => _progressContext; + public bool Start() { IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId); @@ -121,9 +123,8 @@ internal static class Interact if (gameObject.IsTargetable && HasAnyMarker(gameObject)) { - _interactionState = gameFunctions.InteractWith(gameObject) - ? EInteractionState.InteractionTriggered - : EInteractionState.None; + _progressContext = + InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(gameObject)); _continueAt = DateTime.Now.AddSeconds(0.5); return true; } @@ -159,7 +160,7 @@ internal static class Interact } else { - if (_interactionState == EInteractionState.InteractionConfirmed) + if (_progressContext != null && _progressContext.WasSuccessful()) return ETaskResult.TaskComplete; if (interactionType == EInteractionType.Gather && condition[ConditionFlag.Gathering]) @@ -170,9 +171,8 @@ internal static class Interact if (gameObject == null || !gameObject.IsTargetable || !HasAnyMarker(gameObject)) return ETaskResult.StillRunning; - _interactionState = gameFunctions.InteractWith(gameObject) - ? EInteractionState.InteractionTriggered - : EInteractionState.None; + _progressContext = + InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(gameObject)); _continueAt = DateTime.Now.AddSeconds(0.5); return ETaskResult.StillRunning; } @@ -187,33 +187,5 @@ internal static class Interact } public override string ToString() => $"Interact({dataId})"; - - public void OnConditionChange(ConditionFlag flag, bool value) - { - logger.LogDebug("Condition change: {Flag} = {Value}", flag, value); - if (_interactionState == EInteractionState.InteractionTriggered && - flag is ConditionFlag.OccupiedInQuestEvent or ConditionFlag.OccupiedInEvent && - value) - { - logger.LogInformation("Interaction was most likely triggered"); - _interactionState = EInteractionState.InteractionConfirmed; - } - else if (dataId is >= 1047901 and <= 1047905 && - condition[ConditionFlag.Disguised] && - flag == ConditionFlag - .Mounting71 && // why the fuck is this the flag that's used, instead of OccupiedIn[Quest]Event - value) - { - logger.LogInformation("(A Knight of Alexandria) Interaction was most likely triggered"); - _interactionState = EInteractionState.InteractionConfirmed; - } - } - - private enum EInteractionState - { - None, - InteractionTriggered, - InteractionConfirmed, - } } } diff --git a/Questionable/Controller/Steps/Interactions/UseItem.cs b/Questionable/Controller/Steps/Interactions/UseItem.cs index 8ca5f40af..e4f0de00e 100644 --- a/Questionable/Controller/Steps/Interactions/UseItem.cs +++ b/Questionable/Controller/Steps/Interactions/UseItem.cs @@ -160,6 +160,9 @@ internal static class UseItem private bool _usedItem; private DateTime _continueAt; private int _itemCount; + private InteractionProgressContext? _progressContext; + + public InteractionProgressContext? ProgressContext() => _progressContext; public ElementId? QuestId => questId; public uint ItemId => itemId; @@ -178,7 +181,7 @@ internal static class UseItem if (_itemCount == 0) throw new TaskException($"Don't have any {ItemId} in inventory (checks NQ only)"); - _usedItem = UseItem(); + _progressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem()); _continueAt = DateTime.Now.Add(GetRetryDelay()); return true; } @@ -221,7 +224,7 @@ internal static class UseItem if (!_usedItem) { - _usedItem = UseItem(); + _progressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem()); _continueAt = DateTime.Now.Add(GetRetryDelay()); return ETaskResult.StillRunning; } diff --git a/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs b/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs index 0ac347a24..601c0c9e0 100644 --- a/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs +++ b/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs @@ -60,6 +60,9 @@ internal static class AetheryteShortcut { private bool _teleported; private DateTime _continueAt; + private InteractionProgressContext? _progressContext; + + public InteractionProgressContext? ProgressContext() => _progressContext; public bool Start() => !ShouldSkipTeleport(); @@ -200,15 +203,21 @@ internal static class AetheryteShortcut chatGui.PrintError($"[Questionable] Aetheryte {targetAetheryte} is not unlocked."); throw new TaskException("Aetheryte is not unlocked"); } - else if (aetheryteFunctions.TeleportAetheryte(targetAetheryte)) - { - logger.LogInformation("Travelling via aetheryte..."); - return true; - } else { - chatGui.Print("[Questionable] Unable to teleport to aetheryte."); - throw new TaskException("Unable to teleport to aetheryte"); + _progressContext = + InteractionProgressContext.FromActionUseOrDefault(() => aetheryteFunctions.TeleportAetheryte(targetAetheryte)); + logger.LogInformation("Ctx = {C}", _progressContext); + if (_progressContext != null) + { + logger.LogInformation("Travelling via aetheryte..."); + return true; + } + else + { + chatGui.Print("[Questionable] Unable to teleport to aetheryte."); + throw new TaskException("Unable to teleport to aetheryte"); + } } } diff --git a/Questionable/Controller/Steps/Shared/MoveTo.cs b/Questionable/Controller/Steps/Shared/MoveTo.cs index f3318e0ee..e6d0cc945 100644 --- a/Questionable/Controller/Steps/Shared/MoveTo.cs +++ b/Questionable/Controller/Steps/Shared/MoveTo.cs @@ -173,6 +173,8 @@ internal static class MoveTo _canRestart = moveParams.RestartNavigation; } + public InteractionProgressContext? ProgressContext() => _mountTask?.ProgressContext(); + public bool ShouldRedoOnInterrupt() => true; public bool Start() diff --git a/Questionable/Controller/Steps/TaskQueue.cs b/Questionable/Controller/Steps/TaskQueue.cs index 142413c4c..ff12876ff 100644 --- a/Questionable/Controller/Steps/TaskQueue.cs +++ b/Questionable/Controller/Steps/TaskQueue.cs @@ -6,12 +6,12 @@ namespace Questionable.Controller.Steps; internal sealed class TaskQueue { + private readonly List _completedTasks = []; private readonly List _tasks = []; - private int _currentTaskIndex; public ITask? CurrentTask { get; set; } - public IEnumerable RemainingTasks => _tasks.Skip(_currentTaskIndex); - public bool AllTasksComplete => CurrentTask == null && _currentTaskIndex >= _tasks.Count; + public IEnumerable RemainingTasks => _tasks; + public bool AllTasksComplete => CurrentTask == null && _tasks.Count == 0; public void Enqueue(ITask task) { @@ -20,48 +20,40 @@ internal sealed class TaskQueue public bool TryDequeue([NotNullWhen(true)] out ITask? task) { - if (_currentTaskIndex >= _tasks.Count) - { - task = null; + task = _tasks.FirstOrDefault(); + if (task == null) return false; - } - task = _tasks[_currentTaskIndex]; if (task.ShouldRedoOnInterrupt()) - _currentTaskIndex++; - else - _tasks.RemoveAt(0); + _completedTasks.Add(task); + + _tasks.RemoveAt(0); return true; } public bool TryPeek([NotNullWhen(true)] out ITask? task) { - if (_currentTaskIndex >= _tasks.Count) - { - task = null; - return false; - } - - task = _tasks[_currentTaskIndex]; - return true; + task = _tasks.FirstOrDefault(); + return task != null; } public void Reset() { _tasks.Clear(); - _currentTaskIndex = 0; + _completedTasks.Clear(); CurrentTask = null; } public void InterruptWith(List interruptionTasks) { - if (CurrentTask != null) - { - _tasks.Insert(0, CurrentTask); - CurrentTask = null; - _currentTaskIndex = 0; - } - - _tasks.InsertRange(0, interruptionTasks); + List newTasks = + [ + ..interruptionTasks, + .._completedTasks.Where(x => !ReferenceEquals(x, CurrentTask)).ToList(), + CurrentTask, + .._tasks + ]; + Reset(); + _tasks.AddRange(newTasks.Where(x => x != null).Cast()); } }