diff --git a/Directory.Build.targets b/Directory.Build.targets index aa6b3b2..7a37148 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,5 +1,5 @@ - 3.3 + 3.4 diff --git a/Questionable.Model/Questing/EEnemySpawnType.cs b/Questionable.Model/Questing/EEnemySpawnType.cs index aaf9d78..b7e5332 100644 --- a/Questionable.Model/Questing/EEnemySpawnType.cs +++ b/Questionable.Model/Questing/EEnemySpawnType.cs @@ -13,4 +13,5 @@ public enum EEnemySpawnType AutoOnEnterArea, OverworldEnemies, FateEnemies, + QuestInterruption, } diff --git a/Questionable/Controller/CombatController.cs b/Questionable/Controller/CombatController.cs index 26c4fa5..d357278 100644 --- a/Questionable/Controller/CombatController.cs +++ b/Questionable/Controller/CombatController.cs @@ -75,6 +75,7 @@ internal sealed class CombatController : IDisposable Module = combatModule, Data = combatData, }; + _wasInCombat = combatData.SpawnType == EEnemySpawnType.QuestInterruption; return true; } else @@ -86,7 +87,9 @@ internal sealed class CombatController : IDisposable if (_currentFight == null) return EStatus.Complete; - if (_movementController.IsPathfinding || _movementController.IsPathRunning || _movementController.MovementStartedAt > DateTime.Now.AddSeconds(-1)) + if (_movementController.IsPathfinding || + _movementController.IsPathRunning || + _movementController.MovementStartedAt > DateTime.Now.AddSeconds(-1)) return EStatus.Moving; var target = _targetManager.Target; @@ -111,6 +114,8 @@ internal sealed class CombatController : IDisposable else { var nextTarget = FindNextTarget(); + _logger.LogInformation("NT → {NT}", nextTarget); + if (nextTarget is { IsDead: false }) SetTarget(nextTarget); } @@ -335,7 +340,7 @@ internal sealed class CombatController : IDisposable public sealed class CombatData { - public required ElementId ElementId { get; init; } + public required ElementId? ElementId { get; init; } public required EEnemySpawnType SpawnType { get; init; } public required List KillEnemyDataIds { get; init; } public required List ComplexCombatDatas { get; init; } @@ -345,6 +350,7 @@ internal sealed class CombatController : IDisposable public enum EStatus { + NotStarted, InCombat, Moving, Complete, diff --git a/Questionable/Controller/CommandHandler.cs b/Questionable/Controller/CommandHandler.cs index 776f615..d532b0e 100644 --- a/Questionable/Controller/CommandHandler.cs +++ b/Questionable/Controller/CommandHandler.cs @@ -122,6 +122,10 @@ internal sealed class CommandHandler : IDisposable PrintMountId(); break; + case "handle-interrupt": + _questController.InterruptQueueWithCombat(); + break; + case "": _questWindow.Toggle(); break; diff --git a/Questionable/Controller/GatheringController.cs b/Questionable/Controller/GatheringController.cs index 77860f7..bbad03d 100644 --- a/Questionable/Controller/GatheringController.cs +++ b/Questionable/Controller/GatheringController.cs @@ -129,7 +129,7 @@ internal sealed unsafe class GatheringController : MiniTaskController 0) + if (!_taskQueue.AllTasksComplete) return; var director = UIState.Instance()->DirectorTodo.Director; @@ -267,8 +266,8 @@ internal sealed unsafe class GatheringController : MiniTaskController GetRemainingTaskNames() { - if (_currentTask != null) - return [_currentTask.ToString() ?? "?", .. base.GetRemainingTaskNames()]; + if (_taskQueue.CurrentTask is {} currentTask) + return [currentTask.ToString() ?? "?", .. base.GetRemainingTaskNames()]; else return base.GetRemainingTaskNames(); } @@ -277,10 +276,10 @@ internal sealed unsafe class GatheringController : MiniTaskController { protected readonly IChatGui _chatGui; protected readonly ILogger _logger; + protected readonly TaskQueue _taskQueue = new(); - protected readonly Queue _taskQueue = new(); - protected ITask? _currentTask; - - public MiniTaskController(IChatGui chatGui, ILogger logger) + protected MiniTaskController(IChatGui chatGui, ILogger logger) { _chatGui = chatGui; _logger = logger; @@ -24,7 +22,7 @@ internal abstract class MiniTaskController protected virtual void UpdateCurrentTask() { - if (_currentTask == null) + if (_taskQueue.CurrentTask == null) { if (_taskQueue.TryDequeue(out ITask? upcomingTask)) { @@ -33,7 +31,7 @@ internal abstract class MiniTaskController _logger.LogInformation("Starting task {TaskName}", upcomingTask.ToString()); if (upcomingTask.Start()) { - _currentTask = upcomingTask; + _taskQueue.CurrentTask = upcomingTask; return; } else @@ -58,13 +56,13 @@ internal abstract class MiniTaskController ETaskResult result; try { - result = _currentTask.Update(); + result = _taskQueue.CurrentTask.Update(); } catch (Exception e) { - _logger.LogError(e, "Failed to update task {TaskName}", _currentTask.ToString()); + _logger.LogError(e, "Failed to update task {TaskName}", _taskQueue.CurrentTask.ToString()); _chatGui.PrintError( - $"[Questionable] Failed to update task '{_currentTask}', please check /xllog for details."); + $"[Questionable] Failed to update task '{_taskQueue.CurrentTask}', please check /xllog for details."); Stop("Task failed to update"); return; } @@ -76,14 +74,14 @@ internal abstract class MiniTaskController case ETaskResult.SkipRemainingTasksForStep: _logger.LogInformation("{Task} → {Result}, skipping remaining tasks for step", - _currentTask, result); - _currentTask = null; + _taskQueue.CurrentTask, result); + _taskQueue.CurrentTask = null; while (_taskQueue.TryDequeue(out ITask? nextTask)) { if (nextTask is ILastTask or Gather.SkipMarker) { - _currentTask = nextTask; + _taskQueue.CurrentTask = nextTask; return; } } @@ -92,27 +90,27 @@ internal abstract class MiniTaskController case ETaskResult.TaskComplete: _logger.LogInformation("{Task} → {Result}, remaining tasks: {RemainingTaskCount}", - _currentTask, result, _taskQueue.Count); + _taskQueue.CurrentTask, result, _taskQueue.RemainingTasks.Count()); - OnTaskComplete(_currentTask); + OnTaskComplete(_taskQueue.CurrentTask); - _currentTask = null; + _taskQueue.CurrentTask = null; // handled in next update return; case ETaskResult.NextStep: - _logger.LogInformation("{Task} → {Result}", _currentTask, result); + _logger.LogInformation("{Task} → {Result}", _taskQueue.CurrentTask, result); - var lastTask = (ILastTask)_currentTask; - _currentTask = null; + var lastTask = (ILastTask)_taskQueue.CurrentTask; + _taskQueue.CurrentTask = null; OnNextStep(lastTask); return; case ETaskResult.End: - _logger.LogInformation("{Task} → {Result}", _currentTask, result); - _currentTask = null; + _logger.LogInformation("{Task} → {Result}", _taskQueue.CurrentTask, result); + _taskQueue.CurrentTask = null; Stop("Task end"); return; } @@ -130,5 +128,5 @@ internal abstract class MiniTaskController public abstract void Stop(string label); public virtual IList GetRemainingTaskNames() => - _taskQueue.Select(x => x.ToString() ?? "?").ToList(); + _taskQueue.RemainingTasks.Select(x => x.ToString() ?? "?").ToList(); } diff --git a/Questionable/Controller/MovementController.cs b/Questionable/Controller/MovementController.cs index 95950e1..bf10142 100644 --- a/Questionable/Controller/MovementController.cs +++ b/Questionable/Controller/MovementController.cs @@ -85,7 +85,7 @@ internal sealed class MovementController : IDisposable public bool IsPathfinding => _pathfindTask is { IsCompleted: false }; public DestinationData? Destination { get; set; } - public DateTime MovementStartedAt { get; private set; } = DateTime.MaxValue; + public DateTime MovementStartedAt { get; private set; } = DateTime.Now; public void Update() { diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs index 6a0934a..1b0988b 100644 --- a/Questionable/Controller/QuestController.cs +++ b/Questionable/Controller/QuestController.cs @@ -8,7 +8,9 @@ using Dalamud.Game.Gui.Toast; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; +using LLib; using LLib.GameData; +using Lumina.Excel.GeneratedSheets; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps; using Questionable.Controller.Steps.Interactions; @@ -18,6 +20,8 @@ using Questionable.External; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; +using Quest = Questionable.Model.Quest; +using Mount = Questionable.Controller.Steps.Common.Mount; namespace Questionable.Controller; @@ -36,6 +40,10 @@ internal sealed class QuestController : MiniTaskController, IDi private readonly Configuration _configuration; private readonly YesAlreadyIpc _yesAlreadyIpc; private readonly TaskCreator _taskCreator; + private readonly Mount.Factory _mountFactory; + private readonly Combat.Factory _combatFactory; + + private readonly string _actionCanceledText; private readonly object _progressLock = new(); @@ -73,7 +81,10 @@ internal sealed class QuestController : MiniTaskController, IDi IToastGui toastGui, Configuration configuration, YesAlreadyIpc yesAlreadyIpc, - TaskCreator taskCreator) + TaskCreator taskCreator, + Mount.Factory mountFactory, + Combat.Factory combatFactory, + IDataManager dataManager) : base(chatGui, logger) { _clientState = clientState; @@ -89,10 +100,14 @@ internal sealed class QuestController : MiniTaskController, IDi _configuration = configuration; _yesAlreadyIpc = yesAlreadyIpc; _taskCreator = taskCreator; + _mountFactory = mountFactory; + _combatFactory = combatFactory; _condition.ConditionChange += OnConditionChange; _toastGui.Toast += OnNormalToast; _toastGui.ErrorToast += OnErrorToast; + + _actionCanceledText = dataManager.GetString(1314, x => x.Text)!; } public EAutomationType AutomationType @@ -181,7 +196,7 @@ internal sealed class QuestController : MiniTaskController, IDi if (!_clientState.IsLoggedIn || _condition[ConditionFlag.Unconscious]) { - if (_currentTask != null || _taskQueue.Count > 0) + if (!_taskQueue.AllTasksComplete) { Stop("HP = 0"); _movementController.Stop(); @@ -191,7 +206,7 @@ internal sealed class QuestController : MiniTaskController, IDi } else if (_configuration.General.UseEscToCancelQuesting && _keyState[VirtualKey.ESCAPE]) { - if (_currentTask != null || _taskQueue.Count > 0) + if (!_taskQueue.AllTasksComplete) { Stop("ESC pressed"); _movementController.Stop(); @@ -204,8 +219,7 @@ internal sealed class QuestController : MiniTaskController, IDi return; if (AutomationType == EAutomationType.Automatic && - ((_currentTask == null && _taskQueue.Count == 0) || - _currentTask is WaitAtEnd.WaitQuestAccepted) + (_taskQueue.AllTasksComplete || _taskQueue.CurrentTask is WaitAtEnd.WaitQuestAccepted) && CurrentQuest is { Sequence: 0, Step: 0 } or { Sequence: 0, Step: 255 } && DateTime.Now >= CurrentQuest.StepProgress.StartedAt.AddSeconds(15)) { @@ -276,8 +290,7 @@ internal sealed class QuestController : MiniTaskController, IDi questToRun = _nextQuest; currentSequence = _nextQuest.Sequence; // by definition, this should always be 0 if (_nextQuest.Step == 0 && - _currentTask == null && - _taskQueue.Count == 0 && + _taskQueue.AllTasksComplete && AutomationType == EAutomationType.Automatic) ExecuteNextStep(); } @@ -286,8 +299,7 @@ internal sealed class QuestController : MiniTaskController, IDi questToRun = _gatheringQuest; currentSequence = _gatheringQuest.Sequence; if (_gatheringQuest.Step == 0 && - _currentTask == null && - _taskQueue.Count == 0 && + _taskQueue.AllTasksComplete && AutomationType == EAutomationType.Automatic) ExecuteNextStep(); } @@ -392,7 +404,7 @@ internal sealed class QuestController : MiniTaskController, IDi if (questToRun.Step == 255) { DebugState = "Step completed"; - if (_currentTask != null || _taskQueue.Count > 0) + if (!_taskQueue.AllTasksComplete) CheckNextTasks("Step complete"); return; } @@ -465,10 +477,7 @@ internal sealed class QuestController : MiniTaskController, IDi private void ClearTasksInternal() { //_logger.LogDebug("Clearing task (internally)"); - _currentTask = null; - - if (_taskQueue.Count > 0) - _taskQueue.Clear(); + _taskQueue.Reset(); _yesAlreadyIpc.RestoreYesAlready(); _combatController.Stop("ClearTasksInternal"); @@ -629,13 +638,15 @@ internal sealed class QuestController : MiniTaskController, IDi public string ToStatString() { - return _currentTask == null ? $"- (+{_taskQueue.Count})" : $"{_currentTask} (+{_taskQueue.Count})"; + return _taskQueue.CurrentTask is { } currentTask + ? $"{currentTask} (+{_taskQueue.RemainingTasks.Count()})" + : $"- (+{_taskQueue.RemainingTasks.Count()})"; } public bool HasCurrentTaskMatching([NotNullWhen(true)] out T? task) where T : class, ITask { - if (_currentTask is T t) + if (_taskQueue.CurrentTask is T t) { task = t; return true; @@ -647,7 +658,7 @@ internal sealed class QuestController : MiniTaskController, IDi } } - public bool IsRunning => _currentTask != null || _taskQueue.Count > 0; + public bool IsRunning => !_taskQueue.AllTasksComplete; public sealed class QuestProgress { @@ -687,19 +698,19 @@ internal sealed class QuestController : MiniTaskController, IDi { lock (_progressLock) { - if (_currentTask is ISkippableTask) - _currentTask = null; - else if (_currentTask != null) + if (_taskQueue.CurrentTask is ISkippableTask) + _taskQueue.CurrentTask = null; + else if (_taskQueue.CurrentTask != null) { - _currentTask = null; - while (_taskQueue.Count > 0) + _taskQueue.CurrentTask = null; + while (_taskQueue.TryPeek(out ITask? task)) { - var task = _taskQueue.Dequeue(); + _taskQueue.TryDequeue(out _); if (task is ISkippableTask) return; } - if (_taskQueue.Count == 0) + if (_taskQueue.AllTasksComplete) { Stop("Skip"); IncreaseStepCount(elementId, currentQuestSequence); @@ -715,7 +726,7 @@ internal sealed class QuestController : MiniTaskController, IDi public void SkipSimulatedTask() { - _currentTask = null; + _taskQueue.CurrentTask = null; } public bool IsInterruptible() @@ -774,7 +785,7 @@ internal sealed class QuestController : MiniTaskController, IDi private void OnConditionChange(ConditionFlag flag, bool value) { - if (_currentTask is IConditionChangeAware conditionChangeAware) + if (_taskQueue.CurrentTask is IConditionChangeAware conditionChangeAware) conditionChangeAware.OnConditionChange(flag, value); } @@ -785,13 +796,33 @@ internal sealed class QuestController : MiniTaskController, IDi private void OnErrorToast(ref SeString message, ref bool isHandled) { - if (_currentTask is IToastAware toastAware) + _logger.LogWarning("XXX {A} → {B} XXX", _actionCanceledText, message.TextValue); + if (_taskQueue.CurrentTask is IToastAware toastAware) { if (toastAware.OnErrorToast(message)) { isHandled = true; } } + + if (!isHandled) + { + if (GameFunctions.GameStringEquals(_actionCanceledText, message.TextValue) && + !_condition[ConditionFlag.InFlight]) + InterruptQueueWithCombat(); + } + } + + 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() diff --git a/Questionable/Controller/Steps/Common/Mount.cs b/Questionable/Controller/Steps/Common/Mount.cs index 9502455..958bf2d 100644 --- a/Questionable/Controller/Steps/Common/Mount.cs +++ b/Questionable/Controller/Steps/Common/Mount.cs @@ -46,6 +46,8 @@ internal static class Mount private bool _mountTriggered; private DateTime _retryAt = DateTime.MinValue; + public bool ShouldRedoOnInterrupt() => true; + public bool Start() { if (condition[ConditionFlag.Mounted]) @@ -129,6 +131,8 @@ internal static class Mount private bool _unmountTriggered; private DateTime _continueAt = DateTime.MinValue; + public bool ShouldRedoOnInterrupt() => true; + public bool Start() { if (!condition[ConditionFlag.Mounted]) diff --git a/Questionable/Controller/Steps/ITask.cs b/Questionable/Controller/Steps/ITask.cs index 8354406..0ddc962 100644 --- a/Questionable/Controller/Steps/ITask.cs +++ b/Questionable/Controller/Steps/ITask.cs @@ -5,6 +5,8 @@ namespace Questionable.Controller.Steps; internal interface ITask { + bool ShouldRedoOnInterrupt() => false; + bool Start(); ETaskResult Update(); diff --git a/Questionable/Controller/Steps/Interactions/Combat.cs b/Questionable/Controller/Steps/Interactions/Combat.cs index 38b6228..170883a 100644 --- a/Questionable/Controller/Steps/Interactions/Combat.cs +++ b/Questionable/Controller/Steps/Interactions/Combat.cs @@ -101,7 +101,7 @@ internal static class Combat step.CompletionQuestVariablesFlags, step.ComplexCombatData); } - private HandleCombat CreateTask(ElementId elementId, bool isLastStep, EEnemySpawnType enemySpawnType, + internal HandleCombat CreateTask(ElementId? elementId, bool isLastStep, EEnemySpawnType enemySpawnType, IList killEnemyDataIds, IList completionQuestVariablesFlags, IList complexCombatData) { @@ -115,18 +115,21 @@ internal static class Combat } } - private sealed class HandleCombat( + internal sealed class HandleCombat( bool isLastStep, CombatController.CombatData combatData, IList completionQuestVariableFlags, CombatController combatController, QuestFunctions questFunctions) : ITask { + private CombatController.EStatus _status = CombatController.EStatus.NotStarted; + public bool Start() => combatController.Start(combatData); public ETaskResult Update() { - if (combatController.Update() != CombatController.EStatus.Complete) + _status = combatController.Update(); + if (_status != CombatController.EStatus.Complete) return ETaskResult.StillRunning; // if our quest step has any completion flags, we need to check if they are set @@ -157,11 +160,11 @@ internal static class Combat public override string ToString() { if (QuestWorkUtils.HasCompletionFlags(completionQuestVariableFlags)) - return "HandleCombat(wait: QW flags)"; + return $"HandleCombat(wait: QW flags, s: {_status})"; else if (isLastStep) - return "HandleCombat(wait: next sequence)"; + return $"HandleCombat(wait: next sequence, s: {_status})"; else - return "HandleCombat(wait: not in combat)"; + return $"HandleCombat(wait: not in combat, s: {_status})"; } } } diff --git a/Questionable/Controller/Steps/Shared/MoveTo.cs b/Questionable/Controller/Steps/Shared/MoveTo.cs index cf905a6..f3318e0 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 bool ShouldRedoOnInterrupt() => true; + public bool Start() { float stopDistance = _moveParams.StopDistance ?? QuestStep.DefaultStopDistance; @@ -313,6 +315,8 @@ internal static class MoveTo GameFunctions gameFunctions, IClientState clientState) : ITask { + public bool ShouldRedoOnInterrupt() => true; + public bool Start() => true; public ETaskResult Update() @@ -333,6 +337,8 @@ internal static class MoveTo private bool _landing; private DateTime _continueAt; + public bool ShouldRedoOnInterrupt() => true; + public bool Start() { if (!condition[ConditionFlag.InFlight]) diff --git a/Questionable/Controller/Steps/TaskQueue.cs b/Questionable/Controller/Steps/TaskQueue.cs new file mode 100644 index 0000000..142413c --- /dev/null +++ b/Questionable/Controller/Steps/TaskQueue.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Questionable.Controller.Steps; + +internal sealed class TaskQueue +{ + 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 void Enqueue(ITask task) + { + _tasks.Add(task); + } + + public bool TryDequeue([NotNullWhen(true)] out ITask? task) + { + if (_currentTaskIndex >= _tasks.Count) + { + task = null; + return false; + } + + task = _tasks[_currentTaskIndex]; + if (task.ShouldRedoOnInterrupt()) + _currentTaskIndex++; + else + _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; + } + + public void Reset() + { + _tasks.Clear(); + _currentTaskIndex = 0; + CurrentTask = null; + } + + public void InterruptWith(List interruptionTasks) + { + if (CurrentTask != null) + { + _tasks.Insert(0, CurrentTask); + CurrentTask = null; + _currentTaskIndex = 0; + } + + _tasks.InsertRange(0, interruptionTasks); + } +}