Handle 'Action canceled, you are under attack' while e.g. talking to an NPC

omicron
Liza 2024-09-17 19:37:28 +02:00
parent 21fde119ba
commit 5288cc6e31
Signed by: liza
GPG Key ID: 7199F8D727D55F67
13 changed files with 187 additions and 66 deletions

View File

@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<Version>3.3</Version>
<Version>3.4</Version>
</PropertyGroup>
</Project>

View File

@ -13,4 +13,5 @@ public enum EEnemySpawnType
AutoOnEnterArea,
OverworldEnemies,
FateEnemies,
QuestInterruption,
}

View File

@ -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<uint> KillEnemyDataIds { get; init; }
public required List<ComplexCombatData> ComplexCombatDatas { get; init; }
@ -345,6 +350,7 @@ internal sealed class CombatController : IDisposable
public enum EStatus
{
NotStarted,
InCombat,
Moving,
Complete,

View File

@ -122,6 +122,10 @@ internal sealed class CommandHandler : IDisposable
PrintMountId();
break;
case "handle-interrupt":
_questController.InterruptQueueWithCombat();
break;
case "":
_questWindow.Toggle();
break;

View File

@ -129,7 +129,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
return EStatus.Complete;
}
if (_currentTask == null && _taskQueue.Count == 0)
if (_taskQueue.AllTasksComplete)
GoToNextNode();
UpdateCurrentTask();
@ -141,8 +141,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
public override void Stop(string label)
{
_currentRequest = null;
_currentTask = null;
_taskQueue.Clear();
_taskQueue.Reset();
}
private void GoToNextNode()
@ -150,7 +149,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
if (_currentRequest == null)
return;
if (_taskQueue.Count > 0)
if (!_taskQueue.AllTasksComplete)
return;
var director = UIState.Instance()->DirectorTodo.Director;
@ -267,8 +266,8 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
public override IList<string> 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<GatheringC
{
if (_revisitRegex.IsMatch(message.TextValue))
{
if (_currentTask is IRevisitAware currentTaskRevisitAware)
if (_taskQueue.CurrentTask is IRevisitAware currentTaskRevisitAware)
currentTaskRevisitAware.OnRevisit();
foreach (ITask task in _taskQueue)
foreach (ITask task in _taskQueue.RemainingTasks)
{
if (task is IRevisitAware taskRevisitAware)
taskRevisitAware.OnRevisit();

View File

@ -12,11 +12,9 @@ internal abstract class MiniTaskController<T>
{
protected readonly IChatGui _chatGui;
protected readonly ILogger<T> _logger;
protected readonly TaskQueue _taskQueue = new();
protected readonly Queue<ITask> _taskQueue = new();
protected ITask? _currentTask;
public MiniTaskController(IChatGui chatGui, ILogger<T> logger)
protected MiniTaskController(IChatGui chatGui, ILogger<T> logger)
{
_chatGui = chatGui;
_logger = logger;
@ -24,7 +22,7 @@ internal abstract class MiniTaskController<T>
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<T>
_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<T>
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<T>
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<T>
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<T>
public abstract void Stop(string label);
public virtual IList<string> GetRemainingTaskNames() =>
_taskQueue.Select(x => x.ToString() ?? "?").ToList();
_taskQueue.RemainingTasks.Select(x => x.ToString() ?? "?").ToList();
}

View File

@ -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()
{

View File

@ -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<QuestController>, 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<QuestController>, 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<QuestController>, IDi
_configuration = configuration;
_yesAlreadyIpc = yesAlreadyIpc;
_taskCreator = taskCreator;
_mountFactory = mountFactory;
_combatFactory = combatFactory;
_condition.ConditionChange += OnConditionChange;
_toastGui.Toast += OnNormalToast;
_toastGui.ErrorToast += OnErrorToast;
_actionCanceledText = dataManager.GetString<LogMessage>(1314, x => x.Text)!;
}
public EAutomationType AutomationType
@ -181,7 +196,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>, 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<QuestController>, 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<QuestController>, 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<QuestController>, 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<QuestController>, 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<QuestController>, 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<QuestController>, 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<QuestController>, 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<T>([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<QuestController>, 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<QuestController>, 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<QuestController>, IDi
public void SkipSimulatedTask()
{
_currentTask = null;
_taskQueue.CurrentTask = null;
}
public bool IsInterruptible()
@ -774,7 +785,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>, 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<QuestController>, 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<ITask> 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()

View File

@ -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])

View File

@ -5,6 +5,8 @@ namespace Questionable.Controller.Steps;
internal interface ITask
{
bool ShouldRedoOnInterrupt() => false;
bool Start();
ETaskResult Update();

View File

@ -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<uint> killEnemyDataIds, IList<QuestWorkValue?> completionQuestVariablesFlags,
IList<ComplexCombatData> complexCombatData)
{
@ -115,18 +115,21 @@ internal static class Combat
}
}
private sealed class HandleCombat(
internal sealed class HandleCombat(
bool isLastStep,
CombatController.CombatData combatData,
IList<QuestWorkValue?> 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})";
}
}
}

View File

@ -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])

View File

@ -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<ITask> _tasks = [];
private int _currentTaskIndex;
public ITask? CurrentTask { get; set; }
public IEnumerable<ITask> 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<ITask> interruptionTasks)
{
if (CurrentTask != null)
{
_tasks.Insert(0, CurrentTask);
CurrentTask = null;
_currentTaskIndex = 0;
}
_tasks.InsertRange(0, interruptionTasks);
}
}