Handle certain interaction interruptions

This commit is contained in:
Liza 2024-09-18 00:30:56 +02:00
parent 5288cc6e31
commit 721f9617a3
Signed by: liza
GPG Key ID: 7199F8D727D55F67
16 changed files with 244 additions and 104 deletions

View File

@ -114,8 +114,6 @@ internal sealed class CombatController : IDisposable
else else
{ {
var nextTarget = FindNextTarget(); var nextTarget = FindNextTarget();
_logger.LogInformation("NT → {NT}", nextTarget);
if (nextTarget is { IsDead: false }) if (nextTarget is { IsDead: false })
SetTarget(nextTarget); SetTarget(nextTarget);
} }

View File

@ -43,6 +43,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
private readonly ILoggerFactory _loggerFactory; private readonly ILoggerFactory _loggerFactory;
private readonly IGameGui _gameGui; private readonly IGameGui _gameGui;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly ILogger<GatheringController> _logger;
private readonly Regex _revisitRegex; private readonly Regex _revisitRegex;
private CurrentRequest? _currentRequest; private CurrentRequest? _currentRequest;
@ -51,6 +52,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
MovementController movementController, MovementController movementController,
MoveTo.Factory moveFactory, MoveTo.Factory moveFactory,
Mount.Factory mountFactory, Mount.Factory mountFactory,
Combat.Factory combatFactory,
Interact.Factory interactFactory, Interact.Factory interactFactory,
GatheringPointRegistry gatheringPointRegistry, GatheringPointRegistry gatheringPointRegistry,
GameFunctions gameFunctions, GameFunctions gameFunctions,
@ -64,7 +66,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
IGameGui gameGui, IGameGui gameGui,
IClientState clientState, IClientState clientState,
IPluginLog pluginLog) IPluginLog pluginLog)
: base(chatGui, logger) : base(chatGui, mountFactory, combatFactory, condition, logger)
{ {
_movementController = movementController; _movementController = movementController;
_moveFactory = moveFactory; _moveFactory = moveFactory;
@ -78,6 +80,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
_gameGui = gameGui; _gameGui = gameGui;
_clientState = clientState; _clientState = clientState;
_logger = logger;
_revisitRegex = dataManager.GetRegex<LogMessage>(5574, x => x.Text, pluginLog) _revisitRegex = dataManager.GetRegex<LogMessage>(5574, x => x.Text, pluginLog)
?? throw new InvalidDataException("No regex found for revisit message"); ?? throw new InvalidDataException("No regex found for revisit message");

View File

@ -1,23 +1,35 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps; using Questionable.Controller.Steps;
using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Interactions;
using Questionable.Controller.Steps.Shared; using Questionable.Controller.Steps.Shared;
using Questionable.Model.Questing;
namespace Questionable.Controller; namespace Questionable.Controller;
internal abstract class MiniTaskController<T> internal abstract class MiniTaskController<T>
{ {
protected readonly IChatGui _chatGui;
protected readonly ILogger<T> _logger;
protected readonly TaskQueue _taskQueue = new(); protected readonly TaskQueue _taskQueue = new();
protected MiniTaskController(IChatGui chatGui, ILogger<T> logger) private readonly IChatGui _chatGui;
private readonly Mount.Factory _mountFactory;
private readonly Combat.Factory _combatFactory;
private readonly ICondition _condition;
private readonly ILogger<T> _logger;
protected MiniTaskController(IChatGui chatGui, Mount.Factory mountFactory, Combat.Factory combatFactory,
ICondition condition, ILogger<T> logger)
{ {
_chatGui = chatGui; _chatGui = chatGui;
_logger = logger; _logger = logger;
_mountFactory = mountFactory;
_combatFactory = combatFactory;
_condition = condition;
} }
protected virtual void UpdateCurrentTask() protected virtual void UpdateCurrentTask()
@ -56,6 +68,12 @@ internal abstract class MiniTaskController<T>
ETaskResult result; ETaskResult result;
try try
{ {
if (_taskQueue.CurrentTask.WasInterrupted())
{
InterruptQueueWithCombat();
return;
}
result = _taskQueue.CurrentTask.Update(); result = _taskQueue.CurrentTask.Update();
} }
catch (Exception e) catch (Exception e)
@ -122,11 +140,27 @@ internal abstract class MiniTaskController<T>
protected virtual void OnNextStep(ILastTask task) protected virtual void OnNextStep(ILastTask task)
{ {
} }
public abstract void Stop(string label); public abstract void Stop(string label);
public virtual IList<string> GetRemainingTaskNames() => public virtual IList<string> GetRemainingTaskNames() =>
_taskQueue.RemainingTasks.Select(x => x.ToString() ?? "?").ToList(); _taskQueue.RemainingTasks.Select(x => x.ToString() ?? "?").ToList();
public void InterruptQueueWithCombat()
{
_logger.LogWarning("Interrupted, attempting to resolve (if in combat)");
if (_condition[ConditionFlag.InCombat])
{
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);
}
else
_taskQueue.InterruptWith([new WaitAtEnd.WaitDelay()]);
}
} }

View File

@ -35,13 +35,13 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
private readonly GatheringController _gatheringController; private readonly GatheringController _gatheringController;
private readonly QuestRegistry _questRegistry; private readonly QuestRegistry _questRegistry;
private readonly IKeyState _keyState; private readonly IKeyState _keyState;
private readonly IChatGui _chatGui;
private readonly ICondition _condition; private readonly ICondition _condition;
private readonly IToastGui _toastGui; private readonly IToastGui _toastGui;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly YesAlreadyIpc _yesAlreadyIpc; private readonly YesAlreadyIpc _yesAlreadyIpc;
private readonly TaskCreator _taskCreator; private readonly TaskCreator _taskCreator;
private readonly Mount.Factory _mountFactory; private readonly ILogger<QuestController> _logger;
private readonly Combat.Factory _combatFactory;
private readonly string _actionCanceledText; private readonly string _actionCanceledText;
@ -85,7 +85,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
Mount.Factory mountFactory, Mount.Factory mountFactory,
Combat.Factory combatFactory, Combat.Factory combatFactory,
IDataManager dataManager) IDataManager dataManager)
: base(chatGui, logger) : base(chatGui, mountFactory, combatFactory, condition, logger)
{ {
_clientState = clientState; _clientState = clientState;
_gameFunctions = gameFunctions; _gameFunctions = gameFunctions;
@ -95,13 +95,13 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
_gatheringController = gatheringController; _gatheringController = gatheringController;
_questRegistry = questRegistry; _questRegistry = questRegistry;
_keyState = keyState; _keyState = keyState;
_chatGui = chatGui;
_condition = condition; _condition = condition;
_toastGui = toastGui; _toastGui = toastGui;
_configuration = configuration; _configuration = configuration;
_yesAlreadyIpc = yesAlreadyIpc; _yesAlreadyIpc = yesAlreadyIpc;
_taskCreator = taskCreator; _taskCreator = taskCreator;
_mountFactory = mountFactory; _logger = logger;
_combatFactory = combatFactory;
_condition.ConditionChange += OnConditionChange; _condition.ConditionChange += OnConditionChange;
_toastGui.Toast += OnNormalToast; _toastGui.Toast += OnNormalToast;
@ -659,6 +659,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
} }
public bool IsRunning => !_taskQueue.AllTasksComplete; public bool IsRunning => !_taskQueue.AllTasksComplete;
public TaskQueue TaskQueue => _taskQueue;
public sealed class QuestProgress public sealed class QuestProgress
{ {
@ -813,18 +814,6 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
} }
} }
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() public void Dispose()
{ {
_toastGui.ErrorToast -= OnErrorToast; _toastGui.ErrorToast -= OnErrorToast;

View File

@ -18,6 +18,8 @@ internal abstract class AbstractDelayedTask : ITask
{ {
} }
public virtual InteractionProgressContext? ProgressContext() => null;
public bool Start() public bool Start()
{ {
_continueAt = DateTime.Now.Add(Delay); _continueAt = DateTime.Now.Add(Delay);

View File

@ -44,8 +44,11 @@ internal static class Mount
ILogger<MountTask> logger) : ITask ILogger<MountTask> logger) : ITask
{ {
private bool _mountTriggered; private bool _mountTriggered;
private InteractionProgressContext? _progressContext;
private DateTime _retryAt = DateTime.MinValue; private DateTime _retryAt = DateTime.MinValue;
public InteractionProgressContext? ProgressContext() => _progressContext;
public bool ShouldRedoOnInterrupt() => true; public bool ShouldRedoOnInterrupt() => true;
public bool Start() public bool Start()
@ -108,7 +111,8 @@ internal static class Mount
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
_mountTriggered = gameFunctions.Mount(); _progressContext = InteractionProgressContext.FromActionUse(() => _mountTriggered = gameFunctions.Mount());
_retryAt = DateTime.Now.AddSeconds(5); _retryAt = DateTime.Now.AddSeconds(5);
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }

View File

@ -5,6 +5,20 @@ namespace Questionable.Controller.Steps;
internal interface ITask 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 ShouldRedoOnInterrupt() => false;
bool Start(); bool Start();

View File

@ -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<bool> 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<bool> func)
{
return FromActionUseInternal(func).Item2;
}
public static InteractionProgressContext? FromActionUseOrDefault(Func<bool> 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()})";
}

View File

@ -32,19 +32,29 @@ internal static class AetherCurrent
return null; return null;
} }
return new DoAttune(step.DataId.Value, step.AetherCurrentId.Value, gameFunctions, loggerFactory.CreateLogger<DoAttune>()); return new DoAttune(step.DataId.Value, step.AetherCurrentId.Value, gameFunctions,
loggerFactory.CreateLogger<DoAttune>());
} }
} }
private sealed class DoAttune(uint dataId, uint aetherCurrentId, GameFunctions gameFunctions, ILogger<DoAttune> logger) : ITask private sealed class DoAttune(
uint dataId,
uint aetherCurrentId,
GameFunctions gameFunctions,
ILogger<DoAttune> logger) : ITask
{ {
private InteractionProgressContext? _progressContext;
public InteractionProgressContext? ProgressContext() => _progressContext;
public bool Start() public bool Start()
{ {
if (!gameFunctions.IsAetherCurrentUnlocked(aetherCurrentId)) if (!gameFunctions.IsAetherCurrentUnlocked(aetherCurrentId))
{ {
logger.LogInformation("Attuning to aether current {AetherCurrentId} / {DataId}", aetherCurrentId, logger.LogInformation("Attuning to aether current {AetherCurrentId} / {DataId}", aetherCurrentId,
dataId); dataId);
gameFunctions.InteractWith(dataId); _progressContext =
InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(dataId));
return true; return true;
} }

View File

@ -34,12 +34,17 @@ internal static class AethernetShard
GameFunctions gameFunctions, GameFunctions gameFunctions,
ILogger<DoAttune> logger) : ITask ILogger<DoAttune> logger) : ITask
{ {
private InteractionProgressContext? _progressContext;
public InteractionProgressContext? ProgressContext() => _progressContext;
public bool Start() public bool Start()
{ {
if (!aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation)) if (!aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation))
{ {
logger.LogInformation("Attuning to aethernet shard {AethernetShard}", 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; return true;
} }

View File

@ -33,12 +33,18 @@ internal static class Aetheryte
GameFunctions gameFunctions, GameFunctions gameFunctions,
ILogger<DoAttune> logger) : ITask ILogger<DoAttune> logger) : ITask
{ {
private InteractionProgressContext? _progressContext;
public InteractionProgressContext? ProgressContext() => _progressContext;
public bool Start() public bool Start()
{ {
if (!aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation)) if (!aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation))
{ {
logger.LogInformation("Attuning to aetheryte {Aetheryte}", aetheryteLocation); logger.LogInformation("Attuning to aetheryte {Aetheryte}", aetheryteLocation);
gameFunctions.InteractWith((uint)aetheryteLocation); _progressContext =
InteractionProgressContext.FromActionUseOrDefault(() =>
gameFunctions.InteractWith((uint)aetheryteLocation));
return true; return true;
} }

View File

@ -78,10 +78,10 @@ internal static class Interact
GameFunctions gameFunctions, GameFunctions gameFunctions,
ICondition condition, ICondition condition,
ILogger<DoInteract> logger) ILogger<DoInteract> logger)
: ITask, IConditionChangeAware : ITask
{ {
private bool _needsUnmount; private bool _needsUnmount;
private EInteractionState _interactionState = EInteractionState.None; private InteractionProgressContext? _progressContext;
private DateTime _continueAt = DateTime.MinValue; private DateTime _continueAt = DateTime.MinValue;
public Quest? Quest => quest; public Quest? Quest => quest;
@ -92,6 +92,8 @@ internal static class Interact
set => interactionType = value; set => interactionType = value;
} }
public InteractionProgressContext? ProgressContext() => _progressContext;
public bool Start() public bool Start()
{ {
IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId); IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId);
@ -121,9 +123,8 @@ internal static class Interact
if (gameObject.IsTargetable && HasAnyMarker(gameObject)) if (gameObject.IsTargetable && HasAnyMarker(gameObject))
{ {
_interactionState = gameFunctions.InteractWith(gameObject) _progressContext =
? EInteractionState.InteractionTriggered InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(gameObject));
: EInteractionState.None;
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(0.5);
return true; return true;
} }
@ -159,7 +160,7 @@ internal static class Interact
} }
else else
{ {
if (_interactionState == EInteractionState.InteractionConfirmed) if (_progressContext != null && _progressContext.WasSuccessful())
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
if (interactionType == EInteractionType.Gather && condition[ConditionFlag.Gathering]) if (interactionType == EInteractionType.Gather && condition[ConditionFlag.Gathering])
@ -170,9 +171,8 @@ internal static class Interact
if (gameObject == null || !gameObject.IsTargetable || !HasAnyMarker(gameObject)) if (gameObject == null || !gameObject.IsTargetable || !HasAnyMarker(gameObject))
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
_interactionState = gameFunctions.InteractWith(gameObject) _progressContext =
? EInteractionState.InteractionTriggered InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(gameObject));
: EInteractionState.None;
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(0.5);
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
@ -187,33 +187,5 @@ internal static class Interact
} }
public override string ToString() => $"Interact({dataId})"; 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,
}
} }
} }

View File

@ -160,6 +160,9 @@ internal static class UseItem
private bool _usedItem; private bool _usedItem;
private DateTime _continueAt; private DateTime _continueAt;
private int _itemCount; private int _itemCount;
private InteractionProgressContext? _progressContext;
public InteractionProgressContext? ProgressContext() => _progressContext;
public ElementId? QuestId => questId; public ElementId? QuestId => questId;
public uint ItemId => itemId; public uint ItemId => itemId;
@ -178,7 +181,7 @@ internal static class UseItem
if (_itemCount == 0) if (_itemCount == 0)
throw new TaskException($"Don't have any {ItemId} in inventory (checks NQ only)"); 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()); _continueAt = DateTime.Now.Add(GetRetryDelay());
return true; return true;
} }
@ -221,7 +224,7 @@ internal static class UseItem
if (!_usedItem) if (!_usedItem)
{ {
_usedItem = UseItem(); _progressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem());
_continueAt = DateTime.Now.Add(GetRetryDelay()); _continueAt = DateTime.Now.Add(GetRetryDelay());
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }

View File

@ -60,6 +60,9 @@ internal static class AetheryteShortcut
{ {
private bool _teleported; private bool _teleported;
private DateTime _continueAt; private DateTime _continueAt;
private InteractionProgressContext? _progressContext;
public InteractionProgressContext? ProgressContext() => _progressContext;
public bool Start() => !ShouldSkipTeleport(); public bool Start() => !ShouldSkipTeleport();
@ -200,15 +203,21 @@ internal static class AetheryteShortcut
chatGui.PrintError($"[Questionable] Aetheryte {targetAetheryte} is not unlocked."); chatGui.PrintError($"[Questionable] Aetheryte {targetAetheryte} is not unlocked.");
throw new TaskException("Aetheryte is not unlocked"); throw new TaskException("Aetheryte is not unlocked");
} }
else if (aetheryteFunctions.TeleportAetheryte(targetAetheryte))
{
logger.LogInformation("Travelling via aetheryte...");
return true;
}
else else
{ {
chatGui.Print("[Questionable] Unable to teleport to aetheryte."); _progressContext =
throw new TaskException("Unable to teleport to aetheryte"); 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");
}
} }
} }

View File

@ -173,6 +173,8 @@ internal static class MoveTo
_canRestart = moveParams.RestartNavigation; _canRestart = moveParams.RestartNavigation;
} }
public InteractionProgressContext? ProgressContext() => _mountTask?.ProgressContext();
public bool ShouldRedoOnInterrupt() => true; public bool ShouldRedoOnInterrupt() => true;
public bool Start() public bool Start()

View File

@ -6,12 +6,12 @@ namespace Questionable.Controller.Steps;
internal sealed class TaskQueue internal sealed class TaskQueue
{ {
private readonly List<ITask> _completedTasks = [];
private readonly List<ITask> _tasks = []; private readonly List<ITask> _tasks = [];
private int _currentTaskIndex;
public ITask? CurrentTask { get; set; } public ITask? CurrentTask { get; set; }
public IEnumerable<ITask> RemainingTasks => _tasks.Skip(_currentTaskIndex); public IEnumerable<ITask> RemainingTasks => _tasks;
public bool AllTasksComplete => CurrentTask == null && _currentTaskIndex >= _tasks.Count; public bool AllTasksComplete => CurrentTask == null && _tasks.Count == 0;
public void Enqueue(ITask task) public void Enqueue(ITask task)
{ {
@ -20,48 +20,40 @@ internal sealed class TaskQueue
public bool TryDequeue([NotNullWhen(true)] out ITask? task) public bool TryDequeue([NotNullWhen(true)] out ITask? task)
{ {
if (_currentTaskIndex >= _tasks.Count) task = _tasks.FirstOrDefault();
{ if (task == null)
task = null;
return false; return false;
}
task = _tasks[_currentTaskIndex];
if (task.ShouldRedoOnInterrupt()) if (task.ShouldRedoOnInterrupt())
_currentTaskIndex++; _completedTasks.Add(task);
else
_tasks.RemoveAt(0); _tasks.RemoveAt(0);
return true; return true;
} }
public bool TryPeek([NotNullWhen(true)] out ITask? task) public bool TryPeek([NotNullWhen(true)] out ITask? task)
{ {
if (_currentTaskIndex >= _tasks.Count) task = _tasks.FirstOrDefault();
{ return task != null;
task = null;
return false;
}
task = _tasks[_currentTaskIndex];
return true;
} }
public void Reset() public void Reset()
{ {
_tasks.Clear(); _tasks.Clear();
_currentTaskIndex = 0; _completedTasks.Clear();
CurrentTask = null; CurrentTask = null;
} }
public void InterruptWith(List<ITask> interruptionTasks) public void InterruptWith(List<ITask> interruptionTasks)
{ {
if (CurrentTask != null) List<ITask?> newTasks =
{ [
_tasks.Insert(0, CurrentTask); ..interruptionTasks,
CurrentTask = null; .._completedTasks.Where(x => !ReferenceEquals(x, CurrentTask)).ToList(),
_currentTaskIndex = 0; CurrentTask,
} .._tasks
];
_tasks.InsertRange(0, interruptionTasks); Reset();
_tasks.AddRange(newTasks.Where(x => x != null).Cast<ITask>());
} }
} }