Handle certain interaction interruptions
authorLiza Carvelli <liza@carvel.li>
Tue, 17 Sep 2024 22:30:56 +0000 (00:30 +0200)
committerLiza Carvelli <liza@carvel.li>
Tue, 17 Sep 2024 22:31:27 +0000 (00:31 +0200)
16 files changed:
Questionable/Controller/CombatController.cs
Questionable/Controller/GatheringController.cs
Questionable/Controller/MiniTaskController.cs
Questionable/Controller/QuestController.cs
Questionable/Controller/Steps/Common/AbstractDelayedTask.cs
Questionable/Controller/Steps/Common/Mount.cs
Questionable/Controller/Steps/ITask.cs
Questionable/Controller/Steps/InteractionProgressContext.cs [new file with mode: 0644]
Questionable/Controller/Steps/Interactions/AetherCurrent.cs
Questionable/Controller/Steps/Interactions/AethernetShard.cs
Questionable/Controller/Steps/Interactions/Aetheryte.cs
Questionable/Controller/Steps/Interactions/Interact.cs
Questionable/Controller/Steps/Interactions/UseItem.cs
Questionable/Controller/Steps/Shared/AetheryteShortcut.cs
Questionable/Controller/Steps/Shared/MoveTo.cs
Questionable/Controller/Steps/TaskQueue.cs

index d3572780cf055e6ba454ea43ae9199f26ca95efd..eaf311cb0b4b0a8b1d0a2d22d660fe4a71a375af 100644 (file)
@@ -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);
         }
index bbad03d3d48ce5893f914680a9065dcfb74b6adc..48147fc7cfe806e0590438f5b2450d4e725e3d50 100644 (file)
@@ -43,6 +43,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
     private readonly ILoggerFactory _loggerFactory;
     private readonly IGameGui _gameGui;
     private readonly IClientState _clientState;
+    private readonly ILogger<GatheringController> _logger;
     private readonly Regex _revisitRegex;
 
     private CurrentRequest? _currentRequest;
@@ -51,6 +52,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
         MovementController movementController,
         MoveTo.Factory moveFactory,
         Mount.Factory mountFactory,
+        Combat.Factory combatFactory,
         Interact.Factory interactFactory,
         GatheringPointRegistry gatheringPointRegistry,
         GameFunctions gameFunctions,
@@ -64,7 +66,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
         IGameGui gameGui,
         IClientState clientState,
         IPluginLog pluginLog)
-        : base(chatGui, logger)
+        : base(chatGui, mountFactory, combatFactory, condition, logger)
     {
         _movementController = movementController;
         _moveFactory = moveFactory;
@@ -78,6 +80,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
         _loggerFactory = loggerFactory;
         _gameGui = gameGui;
         _clientState = clientState;
+        _logger = logger;
 
         _revisitRegex = dataManager.GetRegex<LogMessage>(5574, x => x.Text, pluginLog)
                         ?? throw new InvalidDataException("No regex found for revisit message");
index 0ec7bd675f4f1014560f8c8a3ee0d479d9746075..b3e6bd5489c79548aa84287b91e074199091fc13 100644 (file)
@@ -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<T>
 {
-    protected readonly IChatGui _chatGui;
-    protected readonly ILogger<T> _logger;
     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;
         _logger = logger;
+        _mountFactory = mountFactory;
+        _combatFactory = combatFactory;
+        _condition = condition;
     }
 
     protected virtual void UpdateCurrentTask()
@@ -56,6 +68,12 @@ internal abstract class MiniTaskController<T>
         ETaskResult result;
         try
         {
+            if (_taskQueue.CurrentTask.WasInterrupted())
+            {
+                InterruptQueueWithCombat();
+                return;
+            }
+
             result = _taskQueue.CurrentTask.Update();
         }
         catch (Exception e)
@@ -122,11 +140,27 @@ internal abstract class MiniTaskController<T>
 
     protected virtual void OnNextStep(ILastTask task)
     {
-
     }
 
     public abstract void Stop(string label);
 
     public virtual IList<string> 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<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()]);
+    }
 }
index 1b0988b99443fc7b38e3eea0b1c68bf0428af002..8ebf576d9741a779aba171ab014d379f8d928739 100644 (file)
@@ -35,13 +35,13 @@ internal sealed class QuestController : MiniTaskController<QuestController>, 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<QuestController> _logger;
 
     private readonly string _actionCanceledText;
 
@@ -85,7 +85,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>, 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<QuestController>, 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<QuestController>, IDi
     }
 
     public bool IsRunning => !_taskQueue.AllTasksComplete;
+    public TaskQueue TaskQueue => _taskQueue;
 
     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()
     {
         _toastGui.ErrorToast -= OnErrorToast;
index e4586fb86e4435e69225db1b2427dc47fa033044..abd6514f4c2c0b5fdb2ec0a5c15351f9d3b12c09 100644 (file)
@@ -18,6 +18,8 @@ internal abstract class AbstractDelayedTask : ITask
     {
     }
 
+    public virtual InteractionProgressContext? ProgressContext() => null;
+
     public bool Start()
     {
         _continueAt = DateTime.Now.Add(Delay);
index 958bf2d74bdeb492eb4992a4e2769ff88d005bef..f067066cac04c9bfc90d265313645e6ac05a8eb1 100644 (file)
@@ -44,8 +44,11 @@ internal static class Mount
         ILogger<MountTask> 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;
             }
index 0ddc962178e57907a8edef6535e598da1da9a54e..a8442c7670bc784adb7fd84e6b85efe55bee4f49 100644 (file)
@@ -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 (file)
index 0000000..a943ed5
--- /dev/null
@@ -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()})";
+}
index 45507e80485389ac83c4709dbd32fb97e6f84892..cbbe68686d793825d8c7683a8d314a89d016bf1c 100644 (file)
@@ -32,19 +32,29 @@ internal static class AetherCurrent
                 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()
         {
             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;
             }
 
index edcf4720ef00c4a8bc88b5ada4feee1250bb767a..ed5580c8c2062daf8daf24dbca081a75976a0ea4 100644 (file)
@@ -34,12 +34,17 @@ internal static class AethernetShard
         GameFunctions gameFunctions,
         ILogger<DoAttune> 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;
             }
 
index 5af0a6a356d89c95ad0ab759da6008e42fdabf23..eace64099b629803c56eb9faf58cc010f03483bc 100644 (file)
@@ -33,12 +33,18 @@ internal static class Aetheryte
         GameFunctions gameFunctions,
         ILogger<DoAttune> 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;
             }
 
index a88e6dda5a79ceeb452eeb7151e382c3cfd566a1..020fc66e7e131f5fea8fb174ea649df0a3f7bf43 100644 (file)
@@ -78,10 +78,10 @@ internal static class Interact
         GameFunctions gameFunctions,
         ICondition condition,
         ILogger<DoInteract> 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,
-        }
     }
 }
index 8ca5f40af5d9133233ca76276ea539bffa07f5ed..e4f0de00e0e9f89d46391f7c2f526b58893aabd1 100644 (file)
@@ -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;
             }
index 0ac347a24dc67b83e4f9240980256dff7db2f4cd..601c0c9e0a7b712d8e6e96317d7fcd7fbff0ee44 100644 (file)
@@ -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");
+                }
             }
         }
 
index f3318e0eeebf41d24f74451ef9c5934e3301c00e..e6d0cc945d16ec5bbbddf891d36d4a866223337c 100644 (file)
@@ -173,6 +173,8 @@ internal static class MoveTo
             _canRestart = moveParams.RestartNavigation;
         }
 
+        public InteractionProgressContext? ProgressContext() => _mountTask?.ProgressContext();
+
         public bool ShouldRedoOnInterrupt() => true;
 
         public bool Start()
index 142413c4cd59be2dd0fbc9e0d96e30e71feb56c5..ff12876ff6e2b1e90c8243f52ddab824cfebc813 100644 (file)
@@ -6,12 +6,12 @@ namespace Questionable.Controller.Steps;
 
 internal sealed class TaskQueue
 {
+    private readonly List<ITask> _completedTasks = [];
     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 IEnumerable<ITask> 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<ITask> interruptionTasks)
     {
-        if (CurrentTask != null)
-        {
-            _tasks.Insert(0, CurrentTask);
-            CurrentTask = null;
-            _currentTaskIndex = 0;
-        }
-
-        _tasks.InsertRange(0, interruptionTasks);
+        List<ITask?> newTasks =
+        [
+            ..interruptionTasks,
+            .._completedTasks.Where(x => !ReferenceEquals(x, CurrentTask)).ToList(),
+            CurrentTask,
+            .._tasks
+        ];
+        Reset();
+        _tasks.AddRange(newTasks.Where(x => x != null).Cast<ITask>());
     }
 }