diff --git a/Directory.Build.targets b/Directory.Build.targets
index aa6b3b27..7a37148a 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 aaf9d789..b7e5332e 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 26c4fa5c..d3572780 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 776f615a..d532b0ea 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 77860f7e..bbad03d3 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 95950e14..bf10142f 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 6a0934aa..1b0988b9 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 9502455c..958bf2d7 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 8354406d..0ddc9621 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 38b62286..170883a3 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 cf905a65..f3318e0e 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 00000000..142413c4
--- /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);
+ }
+}