Make task logic stateless to support rewind
authorLiza Carvelli <liza@carvel.li>
Wed, 18 Sep 2024 20:40:12 +0000 (22:40 +0200)
committerLiza Carvelli <liza@carvel.li>
Wed, 18 Sep 2024 20:40:12 +0000 (22:40 +0200)
49 files changed:
GatheringPathRenderer/RendererPlugin.cs
Questionable/Controller/GameUi/InteractionUiController.cs
Questionable/Controller/GatheringController.cs
Questionable/Controller/GatheringPointRegistry.cs
Questionable/Controller/MiniTaskController.cs
Questionable/Controller/QuestController.cs
Questionable/Controller/QuestRegistry.cs
Questionable/Controller/Steps/Common/AbstractDelayedTask.cs [deleted file]
Questionable/Controller/Steps/Common/AbstractDelayedTaskExecutor.cs [new file with mode: 0644]
Questionable/Controller/Steps/Common/Mount.cs
Questionable/Controller/Steps/Common/NextQuest.cs
Questionable/Controller/Steps/Common/WaitConditionTask.cs
Questionable/Controller/Steps/Gathering/DoGather.cs
Questionable/Controller/Steps/Gathering/DoGatherCollectable.cs
Questionable/Controller/Steps/Gathering/MoveToLandingLocation.cs
Questionable/Controller/Steps/Gathering/TurnInDelivery.cs
Questionable/Controller/Steps/IConditionChangeAware.cs
Questionable/Controller/Steps/IRevisitAware.cs
Questionable/Controller/Steps/ITask.cs
Questionable/Controller/Steps/IToastAware.cs
Questionable/Controller/Steps/Interactions/Action.cs
Questionable/Controller/Steps/Interactions/AetherCurrent.cs
Questionable/Controller/Steps/Interactions/AethernetShard.cs
Questionable/Controller/Steps/Interactions/Aetheryte.cs
Questionable/Controller/Steps/Interactions/Combat.cs
Questionable/Controller/Steps/Interactions/Dive.cs
Questionable/Controller/Steps/Interactions/Duty.cs
Questionable/Controller/Steps/Interactions/Emote.cs
Questionable/Controller/Steps/Interactions/EquipItem.cs
Questionable/Controller/Steps/Interactions/EquipRecommended.cs
Questionable/Controller/Steps/Interactions/Interact.cs
Questionable/Controller/Steps/Interactions/Jump.cs
Questionable/Controller/Steps/Interactions/Say.cs
Questionable/Controller/Steps/Interactions/UseItem.cs
Questionable/Controller/Steps/Leves/InitiateLeve.cs
Questionable/Controller/Steps/Shared/AethernetShortcut.cs
Questionable/Controller/Steps/Shared/AetheryteShortcut.cs
Questionable/Controller/Steps/Shared/Craft.cs
Questionable/Controller/Steps/Shared/Gather.cs
Questionable/Controller/Steps/Shared/MoveTo.cs
Questionable/Controller/Steps/Shared/SkipCondition.cs
Questionable/Controller/Steps/Shared/StepDisabled.cs
Questionable/Controller/Steps/Shared/SwitchClassJob.cs
Questionable/Controller/Steps/Shared/WaitAtEnd.cs
Questionable/Controller/Steps/Shared/WaitAtStart.cs
Questionable/Controller/Steps/TaskExecutor.cs [new file with mode: 0644]
Questionable/Controller/Steps/TaskQueue.cs
Questionable/QuestionablePlugin.cs
Questionable/ServiceCollectionExtensions.cs

index b1ec75612a96dfb0b3764fd8ac6e9d645375350b..dbad17157c6b85583417544b5062698274c9eec8 100644 (file)
@@ -121,7 +121,7 @@ public sealed class RendererPlugin : IDalamudPlugin
         if (!directory.Exists)
             return;
 
-        _pluginLog.Information($"Loading locations from {directory}");
+        //_pluginLog.Information($"Loading locations from {directory}");
         foreach (FileInfo fileInfo in directory.GetFiles("*.json"))
         {
             try
index ce083331eb1fdd543a3bb50f989c151d312a3523..99b2dd51aa2b629ef3d33b43816ef5ce89f470c2 100644 (file)
@@ -285,7 +285,7 @@ internal sealed class InteractionUiController : IDisposable
         List<DialogueChoiceInfo> dialogueChoices = [];
 
         // levequest choices have some vague sort of priority
-        if (_questController.HasCurrentTaskMatching<Interact.DoInteract>(out var interact) &&
+        if (_questController.HasCurrentTaskExecutorMatching<Interact.DoInteract>(out var interact) &&
             interact.Quest != null &&
             interact.InteractionType is EInteractionType.AcceptLeve or EInteractionType.CompleteLeve)
         {
@@ -799,7 +799,7 @@ internal sealed class InteractionUiController : IDisposable
     private void TeleportTownPostSetup(AddonEvent type, AddonArgs args)
     {
         if (ShouldHandleUiInteractions &&
-            _questController.HasCurrentTaskMatching(out AethernetShortcut.UseAethernetShortcut? aethernetShortcut) &&
+            _questController.HasCurrentTaskMatching(out AethernetShortcut.Task? aethernetShortcut) &&
             aethernetShortcut.From.IsFirmamentAetheryte())
         {
             // this might be better via atkvalues; but this works for now
index 48147fc7cfe806e0590438f5b2450d4e725e3d50..52541c09117f8583ae9dc128f979434c97a0f8ad 100644 (file)
@@ -13,16 +13,13 @@ using FFXIVClientStructs.FFXIV.Client.Game.Event;
 using FFXIVClientStructs.FFXIV.Client.Game.UI;
 using LLib;
 using Lumina.Excel.GeneratedSheets;
-using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Steps;
-using Questionable.Controller.Steps.Common;
 using Questionable.Controller.Steps.Gathering;
 using Questionable.Controller.Steps.Interactions;
 using Questionable.Controller.Steps.Shared;
 using Questionable.External;
 using Questionable.Functions;
-using Questionable.GatheringPaths;
 using Questionable.Model.Gathering;
 using Questionable.Model.Questing;
 using Mount = Questionable.Controller.Steps.Common.Mount;
@@ -32,17 +29,11 @@ namespace Questionable.Controller;
 internal sealed unsafe class GatheringController : MiniTaskController<GatheringController>
 {
     private readonly MovementController _movementController;
-    private readonly MoveTo.Factory _moveFactory;
-    private readonly Mount.Factory _mountFactory;
-    private readonly Interact.Factory _interactFactory;
     private readonly GatheringPointRegistry _gatheringPointRegistry;
     private readonly GameFunctions _gameFunctions;
     private readonly NavmeshIpc _navmeshIpc;
     private readonly IObjectTable _objectTable;
     private readonly ICondition _condition;
-    private readonly ILoggerFactory _loggerFactory;
-    private readonly IGameGui _gameGui;
-    private readonly IClientState _clientState;
     private readonly ILogger<GatheringController> _logger;
     private readonly Regex _revisitRegex;
 
@@ -50,10 +41,6 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
 
     public GatheringController(
         MovementController movementController,
-        MoveTo.Factory moveFactory,
-        Mount.Factory mountFactory,
-        Combat.Factory combatFactory,
-        Interact.Factory interactFactory,
         GatheringPointRegistry gatheringPointRegistry,
         GameFunctions gameFunctions,
         NavmeshIpc navmeshIpc,
@@ -61,25 +48,17 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
         IChatGui chatGui,
         ILogger<GatheringController> logger,
         ICondition condition,
+        IServiceProvider serviceProvider,
         IDataManager dataManager,
-        ILoggerFactory loggerFactory,
-        IGameGui gameGui,
-        IClientState clientState,
         IPluginLog pluginLog)
-        : base(chatGui, mountFactory, combatFactory, condition, logger)
+        : base(chatGui, condition, serviceProvider, logger)
     {
         _movementController = movementController;
-        _moveFactory = moveFactory;
-        _mountFactory = mountFactory;
-        _interactFactory = interactFactory;
         _gatheringPointRegistry = gatheringPointRegistry;
         _gameFunctions = gameFunctions;
         _navmeshIpc = navmeshIpc;
         _objectTable = objectTable;
         _condition = condition;
-        _loggerFactory = loggerFactory;
-        _gameGui = gameGui;
-        _clientState = clientState;
         _logger = logger;
 
         _revisitRegex = dataManager.GetRegex<LogMessage>(5574, x => x.Text, pluginLog)
@@ -170,7 +149,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
             return;
 
         ushort territoryId = _currentRequest.Root.Steps.Last().TerritoryId;
-        _taskQueue.Enqueue(_mountFactory.Mount(territoryId, Mount.EMountIf.Always));
+        _taskQueue.Enqueue(new Mount.MountTask(territoryId, Mount.EMountIf.Always));
 
         bool fly = currentNode.Fly.GetValueOrDefault(_currentRequest.Root.FlyBetweenNodes.GetValueOrDefault(true)) &&
                    _gameFunctions.IsFlyingUnlocked(territoryId);
@@ -187,14 +166,13 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
             if (pointOnFloor != null)
                 pointOnFloor = pointOnFloor.Value with { Y = pointOnFloor.Value.Y + (fly ? 3f : 0f) };
 
-            _taskQueue.Enqueue(_moveFactory.Move(new MoveTo.MoveParams(territoryId, pointOnFloor ?? averagePosition,
-                null, 50f, Fly: fly, IgnoreDistanceToObject: true)));
+            _taskQueue.Enqueue(new MoveTo.MoveTask(territoryId, pointOnFloor ?? averagePosition,
+                null, 50f, Fly: fly, IgnoreDistanceToObject: true));
         }
 
-        _taskQueue.Enqueue(new MoveToLandingLocation(territoryId, fly, currentNode, _moveFactory, _gameFunctions,
-            _objectTable, _loggerFactory.CreateLogger<MoveToLandingLocation>()));
-        _taskQueue.Enqueue(_mountFactory.Unmount());
-        _taskQueue.Enqueue(_interactFactory.Interact(currentNode.DataId, null, EInteractionType.Gather, true));
+        _taskQueue.Enqueue(new MoveToLandingLocation.Task(territoryId, fly, currentNode));
+        _taskQueue.Enqueue(new Mount.UnmountTask());
+        _taskQueue.Enqueue(new Interact.Task(currentNode.DataId, null, EInteractionType.Gather, true));
 
         QueueGatherNode(currentNode);
     }
@@ -203,12 +181,10 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
     {
         foreach (bool revisitRequired in new[] { false, true })
         {
-            _taskQueue.Enqueue(new DoGather(_currentRequest!.Data, currentNode, revisitRequired, this, _gameFunctions,
-                _gameGui, _clientState, _condition, _loggerFactory.CreateLogger<DoGather>()));
+            _taskQueue.Enqueue(new DoGather.Task(_currentRequest!.Data, currentNode, revisitRequired));
             if (_currentRequest.Data.Collectability > 0)
             {
-                _taskQueue.Enqueue(new DoGatherCollectable(_currentRequest.Data, currentNode, revisitRequired, this,
-                    _gameFunctions, _clientState, _gameGui, _loggerFactory.CreateLogger<DoGatherCollectable>()));
+                _taskQueue.Enqueue(new DoGatherCollectable.Task(_currentRequest.Data, currentNode, revisitRequired));
             }
         }
     }
@@ -269,7 +245,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
 
     public override IList<string> GetRemainingTaskNames()
     {
-        if (_taskQueue.CurrentTask is {} currentTask)
+        if (_taskQueue.CurrentTaskExecutor?.CurrentTask is {} currentTask)
             return [currentTask.ToString() ?? "?", .. base.GetRemainingTaskNames()];
         else
             return base.GetRemainingTaskNames();
@@ -279,7 +255,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
     {
         if (_revisitRegex.IsMatch(message.TextValue))
         {
-            if (_taskQueue.CurrentTask is IRevisitAware currentTaskRevisitAware)
+            if (_taskQueue.CurrentTaskExecutor?.CurrentTask is IRevisitAware currentTaskRevisitAware)
                 currentTaskRevisitAware.OnRevisit();
 
             foreach (ITask task in _taskQueue.RemainingTasks)
index 6b688e5b37c60fc93d1a7d0f3f87a724a5501de6..7e66330a757d21042f963fd1b015aa62b0c23725 100644 (file)
@@ -94,7 +94,7 @@ internal sealed class GatheringPointRegistry : IDisposable
 
     private void LoadGatheringPointFromStream(string fileName, Stream stream)
     {
-        _logger.LogTrace("Loading gathering point from '{FileName}'", fileName);
+        //_logger.LogTrace("Loading gathering point from '{FileName}'", fileName);
         GatheringPointId? gatheringPointId = ExtractGatheringPointIdFromName(fileName);
         if (gatheringPointId == null)
             return;
@@ -110,7 +110,7 @@ internal sealed class GatheringPointRegistry : IDisposable
             return;
         }
 
-        _logger.Log(logLevel, "Loading gathering points from {DirectoryName}", directory);
+        //_logger.Log(logLevel, "Loading gathering points from {DirectoryName}", directory);
         foreach (FileInfo fileInfo in directory.GetFiles("*.json"))
         {
             try
index b3e6bd5489c79548aa84287b91e074199091fc13..b99cd35f94ca78ada53836ebbb13d6fee62c621d 100644 (file)
@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Linq;
 using Dalamud.Game.ClientState.Conditions;
 using Dalamud.Plugin.Services;
+using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Steps;
 using Questionable.Controller.Steps.Common;
@@ -17,33 +18,33 @@ internal abstract class MiniTaskController<T>
     protected readonly TaskQueue _taskQueue = new();
 
     private readonly IChatGui _chatGui;
-    private readonly Mount.Factory _mountFactory;
-    private readonly Combat.Factory _combatFactory;
     private readonly ICondition _condition;
+    private readonly IServiceProvider _serviceProvider;
     private readonly ILogger<T> _logger;
 
-    protected MiniTaskController(IChatGui chatGui, Mount.Factory mountFactory, Combat.Factory combatFactory,
-        ICondition condition, ILogger<T> logger)
+    protected MiniTaskController(IChatGui chatGui, ICondition condition, IServiceProvider serviceProvider,
+        ILogger<T> logger)
     {
         _chatGui = chatGui;
         _logger = logger;
-        _mountFactory = mountFactory;
-        _combatFactory = combatFactory;
+        _serviceProvider = serviceProvider;
         _condition = condition;
     }
 
     protected virtual void UpdateCurrentTask()
     {
-        if (_taskQueue.CurrentTask == null)
+        if (_taskQueue.CurrentTaskExecutor == null)
         {
             if (_taskQueue.TryDequeue(out ITask? upcomingTask))
             {
                 try
                 {
                     _logger.LogInformation("Starting task {TaskName}", upcomingTask.ToString());
-                    if (upcomingTask.Start())
+                    ITaskExecutor taskExecutor =
+                        _serviceProvider.GetRequiredKeyedService<ITaskExecutor>(upcomingTask.GetType());
+                    if (taskExecutor.Start(upcomingTask))
                     {
-                        _taskQueue.CurrentTask = upcomingTask;
+                        _taskQueue.CurrentTaskExecutor = taskExecutor;
                         return;
                     }
                     else
@@ -68,19 +69,20 @@ internal abstract class MiniTaskController<T>
         ETaskResult result;
         try
         {
-            if (_taskQueue.CurrentTask.WasInterrupted())
+            if (_taskQueue.CurrentTaskExecutor.WasInterrupted())
             {
                 InterruptQueueWithCombat();
                 return;
             }
 
-            result = _taskQueue.CurrentTask.Update();
+            result = _taskQueue.CurrentTaskExecutor.Update();
         }
         catch (Exception e)
         {
-            _logger.LogError(e, "Failed to update task {TaskName}", _taskQueue.CurrentTask.ToString());
+            _logger.LogError(e, "Failed to update task {TaskName}",
+                _taskQueue.CurrentTaskExecutor.CurrentTask.ToString());
             _chatGui.PrintError(
-                $"[Questionable] Failed to update task '{_taskQueue.CurrentTask}', please check /xllog for details.");
+                $"[Questionable] Failed to update task '{_taskQueue.CurrentTaskExecutor.CurrentTask}', please check /xllog for details.");
             Stop("Task failed to update");
             return;
         }
@@ -92,14 +94,16 @@ internal abstract class MiniTaskController<T>
 
             case ETaskResult.SkipRemainingTasksForStep:
                 _logger.LogInformation("{Task} → {Result}, skipping remaining tasks for step",
-                    _taskQueue.CurrentTask, result);
-                _taskQueue.CurrentTask = null;
+                    _taskQueue.CurrentTaskExecutor.CurrentTask, result);
+                _taskQueue.CurrentTaskExecutor = null;
 
                 while (_taskQueue.TryDequeue(out ITask? nextTask))
                 {
                     if (nextTask is ILastTask or Gather.SkipMarker)
                     {
-                        _taskQueue.CurrentTask = nextTask;
+                        ITaskExecutor taskExecutor =
+                            _serviceProvider.GetRequiredKeyedService<ITaskExecutor>(nextTask.GetType());
+                        _taskQueue.CurrentTaskExecutor = taskExecutor;
                         return;
                     }
                 }
@@ -108,27 +112,27 @@ internal abstract class MiniTaskController<T>
 
             case ETaskResult.TaskComplete:
                 _logger.LogInformation("{Task} → {Result}, remaining tasks: {RemainingTaskCount}",
-                    _taskQueue.CurrentTask, result, _taskQueue.RemainingTasks.Count());
+                    _taskQueue.CurrentTaskExecutor.CurrentTask, result, _taskQueue.RemainingTasks.Count());
 
-                OnTaskComplete(_taskQueue.CurrentTask);
+                OnTaskComplete(_taskQueue.CurrentTaskExecutor.CurrentTask);
 
-                _taskQueue.CurrentTask = null;
+                _taskQueue.CurrentTaskExecutor = null;
 
                 // handled in next update
                 return;
 
             case ETaskResult.NextStep:
-                _logger.LogInformation("{Task} → {Result}", _taskQueue.CurrentTask, result);
+                _logger.LogInformation("{Task} → {Result}", _taskQueue.CurrentTaskExecutor.CurrentTask, result);
 
-                var lastTask = (ILastTask)_taskQueue.CurrentTask;
-                _taskQueue.CurrentTask = null;
+                var lastTask = (ILastTask)_taskQueue.CurrentTaskExecutor.CurrentTask;
+                _taskQueue.CurrentTaskExecutor = null;
 
                 OnNextStep(lastTask);
                 return;
 
             case ETaskResult.End:
-                _logger.LogInformation("{Task} → {Result}", _taskQueue.CurrentTask, result);
-                _taskQueue.CurrentTask = null;
+                _logger.LogInformation("{Task} → {Result}", _taskQueue.CurrentTaskExecutor.CurrentTask, result);
+                _taskQueue.CurrentTaskExecutor = null;
                 Stop("Task end");
                 return;
         }
@@ -154,9 +158,9 @@ internal abstract class MiniTaskController<T>
         {
             List<ITask> tasks = [];
             if (_condition[ConditionFlag.Mounted])
-                tasks.Add(_mountFactory.Unmount());
+                tasks.Add(new Mount.UnmountTask());
 
-            tasks.Add(_combatFactory.CreateTask(null, false, EEnemySpawnType.QuestInterruption, [], [], []));
+            tasks.Add(Combat.Factory.CreateTask(null, false, EEnemySpawnType.QuestInterruption, [], [], []));
             tasks.Add(new WaitAtEnd.WaitDelay());
             _taskQueue.InterruptWith(tasks);
         }
index 8ebf576d9741a779aba171ab014d379f8d928739..8a1ed7737390210ab0d300c5fb21719f37fe0794 100644 (file)
@@ -82,10 +82,9 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
         Configuration configuration,
         YesAlreadyIpc yesAlreadyIpc,
         TaskCreator taskCreator,
-        Mount.Factory mountFactory,
-        Combat.Factory combatFactory,
+        IServiceProvider serviceProvider,
         IDataManager dataManager)
-        : base(chatGui, mountFactory, combatFactory, condition, logger)
+        : base(chatGui, condition, serviceProvider, logger)
     {
         _clientState = clientState;
         _gameFunctions = gameFunctions;
@@ -219,7 +218,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
             return;
 
         if (AutomationType == EAutomationType.Automatic &&
-            (_taskQueue.AllTasksComplete || _taskQueue.CurrentTask is WaitAtEnd.WaitQuestAccepted)
+            (_taskQueue.AllTasksComplete || _taskQueue.CurrentTaskExecutor?.CurrentTask is WaitAtEnd.WaitQuestAccepted)
             && CurrentQuest is { Sequence: 0, Step: 0 } or { Sequence: 0, Step: 255 }
             && DateTime.Now >= CurrentQuest.StepProgress.StartedAt.AddSeconds(15))
         {
@@ -638,15 +637,30 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
 
     public string ToStatString()
     {
-        return _taskQueue.CurrentTask is { } currentTask
+        return _taskQueue.CurrentTaskExecutor?.CurrentTask is { } currentTask
             ? $"{currentTask} (+{_taskQueue.RemainingTasks.Count()})"
             : $"- (+{_taskQueue.RemainingTasks.Count()})";
     }
 
+    public bool HasCurrentTaskExecutorMatching<T>([NotNullWhen(true)] out T? task)
+        where T : class, ITaskExecutor
+    {
+        if (_taskQueue.CurrentTaskExecutor is T t)
+        {
+            task = t;
+            return true;
+        }
+        else
+        {
+            task = null;
+            return false;
+        }
+    }
+
     public bool HasCurrentTaskMatching<T>([NotNullWhen(true)] out T? task)
         where T : class, ITask
     {
-        if (_taskQueue.CurrentTask is T t)
+        if (_taskQueue.CurrentTaskExecutor?.CurrentTask is T t)
         {
             task = t;
             return true;
@@ -699,11 +713,11 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
     {
         lock (_progressLock)
         {
-            if (_taskQueue.CurrentTask is ISkippableTask)
-                _taskQueue.CurrentTask = null;
-            else if (_taskQueue.CurrentTask != null)
+            if (_taskQueue.CurrentTaskExecutor?.CurrentTask is ISkippableTask)
+                _taskQueue.CurrentTaskExecutor = null;
+            else if (_taskQueue.CurrentTaskExecutor != null)
             {
-                _taskQueue.CurrentTask = null;
+                _taskQueue.CurrentTaskExecutor = null;
                 while (_taskQueue.TryPeek(out ITask? task))
                 {
                     _taskQueue.TryDequeue(out _);
@@ -727,7 +741,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
 
     public void SkipSimulatedTask()
     {
-        _taskQueue.CurrentTask = null;
+        _taskQueue.CurrentTaskExecutor = null;
     }
 
     public bool IsInterruptible()
@@ -786,7 +800,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
 
     private void OnConditionChange(ConditionFlag flag, bool value)
     {
-        if (_taskQueue.CurrentTask is IConditionChangeAware conditionChangeAware)
+        if (_taskQueue.CurrentTaskExecutor is IConditionChangeAware conditionChangeAware)
             conditionChangeAware.OnConditionChange(flag, value);
     }
 
@@ -798,7 +812,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
     private void OnErrorToast(ref SeString message, ref bool isHandled)
     {
         _logger.LogWarning("XXX {A} → {B} XXX", _actionCanceledText, message.TextValue);
-        if (_taskQueue.CurrentTask is IToastAware toastAware)
+        if (_taskQueue.CurrentTaskExecutor is IToastAware toastAware)
         {
             if (toastAware.OnErrorToast(message))
             {
index f1af005012cddcfdfdfad3cbef66611094771e81..5026305195bc669d0d7280adce20fdb9f3c417b8 100644 (file)
@@ -142,7 +142,8 @@ internal sealed class QuestRegistry
 
     private void LoadQuestFromStream(string fileName, Stream stream, Quest.ESource source)
     {
-        _logger.LogTrace("Loading quest from '{FileName}'", fileName);
+        if (source == Quest.ESource.UserDirectory)
+            _logger.LogTrace("Loading quest from '{FileName}'", fileName);
         ElementId? questId = ExtractQuestIdFromName(fileName);
         if (questId == null)
             return;
@@ -173,7 +174,8 @@ internal sealed class QuestRegistry
             return;
         }
 
-        _logger.Log(logLevel, "Loading quests from {DirectoryName}", directory);
+        if (source == Quest.ESource.UserDirectory)
+            _logger.Log(logLevel, "Loading quests from {DirectoryName}", directory);
         foreach (FileInfo fileInfo in directory.GetFiles("*.json"))
         {
             try
diff --git a/Questionable/Controller/Steps/Common/AbstractDelayedTask.cs b/Questionable/Controller/Steps/Common/AbstractDelayedTask.cs
deleted file mode 100644 (file)
index abd6514..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-using System;
-
-namespace Questionable.Controller.Steps.Common;
-
-internal abstract class AbstractDelayedTask : ITask
-{
-    private DateTime _continueAt;
-
-    protected AbstractDelayedTask(TimeSpan delay)
-    {
-        Delay = delay;
-    }
-
-    protected TimeSpan Delay { get; set; }
-
-    protected AbstractDelayedTask()
-        : this(TimeSpan.FromSeconds(5))
-    {
-    }
-
-    public virtual InteractionProgressContext? ProgressContext() => null;
-
-    public bool Start()
-    {
-        _continueAt = DateTime.Now.Add(Delay);
-        return StartInternal();
-    }
-
-    protected abstract bool StartInternal();
-
-    public virtual ETaskResult Update()
-    {
-        if (_continueAt >= DateTime.Now)
-            return ETaskResult.StillRunning;
-
-        return UpdateInternal();
-    }
-
-    protected virtual ETaskResult UpdateInternal() => ETaskResult.TaskComplete;
-}
diff --git a/Questionable/Controller/Steps/Common/AbstractDelayedTaskExecutor.cs b/Questionable/Controller/Steps/Common/AbstractDelayedTaskExecutor.cs
new file mode 100644 (file)
index 0000000..f5e95ca
--- /dev/null
@@ -0,0 +1,40 @@
+using System;
+
+namespace Questionable.Controller.Steps.Common;
+
+internal abstract class AbstractDelayedTaskExecutor<T> : TaskExecutor<T>
+    where T : class, ITask
+{
+    private DateTime _continueAt;
+
+    protected AbstractDelayedTaskExecutor()
+        : this(TimeSpan.FromSeconds(5))
+    {
+    }
+
+    protected AbstractDelayedTaskExecutor(TimeSpan delay)
+    {
+        Delay = delay;
+    }
+
+    protected TimeSpan Delay { get; set; }
+
+    protected sealed override bool Start()
+    {
+        bool started = StartInternal();
+        _continueAt = DateTime.Now.Add(Delay);
+        return started;
+    }
+
+    protected abstract bool StartInternal();
+
+    public override ETaskResult Update()
+    {
+        if (_continueAt >= DateTime.Now)
+            return ETaskResult.StillRunning;
+
+        return UpdateInternal();
+    }
+
+    protected virtual ETaskResult UpdateInternal() => ETaskResult.TaskComplete;
+}
index f067066cac04c9bfc90d265313645e6ac05a8eb1..d9d38ae266ce685d5716882eea6f9645d44300d9 100644 (file)
@@ -11,54 +11,38 @@ namespace Questionable.Controller.Steps.Common;
 
 internal static class Mount
 {
-    internal sealed class Factory(
-        GameFunctions gameFunctions,
-        ICondition condition,
-        TerritoryData territoryData,
-        IClientState clientState,
-        ILoggerFactory loggerFactory)
+    internal sealed record MountTask(
+        ushort TerritoryId,
+        EMountIf MountIf,
+        Vector3? Position = null) : ITask
     {
-        public ITask Mount(ushort territoryId, EMountIf mountIf, Vector3? position = null)
-        {
-            if (mountIf == EMountIf.AwayFromPosition)
-                ArgumentNullException.ThrowIfNull(position);
+        public Vector3? Position { get; } = MountIf == EMountIf.AwayFromPosition
+            ? Position ?? throw new ArgumentNullException(nameof(Position))
+            : null;
 
-            return new MountTask(territoryId, mountIf, position, gameFunctions, condition, territoryData, clientState,
-                loggerFactory.CreateLogger<MountTask>());
-        }
+        public bool ShouldRedoOnInterrupt() => true;
 
-        public ITask Unmount()
-        {
-            return new UnmountTask(condition, loggerFactory.CreateLogger<UnmountTask>(), gameFunctions, clientState);
-        }
+        public override string ToString() => "Mount";
     }
 
-    private sealed class MountTask(
-        ushort territoryId,
-        EMountIf mountIf,
-        Vector3? position,
+    internal sealed class MountExecutor(
         GameFunctions gameFunctions,
         ICondition condition,
         TerritoryData territoryData,
         IClientState clientState,
-        ILogger<MountTask> logger) : ITask
+        ILogger<MountTask> logger) : TaskExecutor<MountTask>
     {
         private bool _mountTriggered;
-        private InteractionProgressContext? _progressContext;
         private DateTime _retryAt = DateTime.MinValue;
 
-        public InteractionProgressContext? ProgressContext() => _progressContext;
-
-        public bool ShouldRedoOnInterrupt() => true;
-
-        public bool Start()
+        protected override bool Start()
         {
             if (condition[ConditionFlag.Mounted])
                 return false;
 
-            if (!territoryData.CanUseMount(territoryId))
+            if (!territoryData.CanUseMount(Task.TerritoryId))
             {
-                logger.LogInformation("Can't use mount in current territory {Id}", territoryId);
+                logger.LogInformation("Can't use mount in current territory {Id}", Task.TerritoryId);
                 return false;
             }
 
@@ -68,11 +52,11 @@ internal static class Mount
                 return false;
             }
 
-            if (mountIf == EMountIf.AwayFromPosition)
+            if (Task.MountIf == EMountIf.AwayFromPosition)
             {
                 Vector3 playerPosition = clientState.LocalPlayer?.Position ?? Vector3.Zero;
-                float distance = System.Numerics.Vector3.Distance(playerPosition, position.GetValueOrDefault());
-                if (territoryId == clientState.TerritoryType && distance < 30f && !Conditions.IsDiving)
+                float distance = System.Numerics.Vector3.Distance(playerPosition, Task.Position.GetValueOrDefault());
+                if (Task.TerritoryId == clientState.TerritoryType && distance < 30f && !Conditions.IsDiving)
                 {
                     logger.LogInformation("Not using mount, as we're close to the target");
                     return false;
@@ -80,10 +64,10 @@ internal static class Mount
 
                 logger.LogInformation(
                     "Want to use mount if away from destination ({Distance} yalms), trying (in territory {Id})...",
-                    distance, territoryId);
+                    distance, Task.TerritoryId);
             }
             else
-                logger.LogInformation("Want to use mount, trying (in territory {Id})...", territoryId);
+                logger.LogInformation("Want to use mount, trying (in territory {Id})...", Task.TerritoryId);
 
             if (!condition[ConditionFlag.InCombat])
             {
@@ -94,7 +78,7 @@ internal static class Mount
             return false;
         }
 
-        public ETaskResult Update()
+        public override ETaskResult Update()
         {
             if (_mountTriggered && !condition[ConditionFlag.Mounted] && DateTime.Now > _retryAt)
             {
@@ -111,7 +95,8 @@ internal static class Mount
                     return ETaskResult.TaskComplete;
                 }
 
-                _progressContext = InteractionProgressContext.FromActionUse(() => _mountTriggered = gameFunctions.Mount());
+                ProgressContext =
+                    InteractionProgressContext.FromActionUse(() => _mountTriggered = gameFunctions.Mount());
 
                 _retryAt = DateTime.Now.AddSeconds(5);
                 return ETaskResult.StillRunning;
@@ -121,23 +106,26 @@ internal static class Mount
                 ? ETaskResult.TaskComplete
                 : ETaskResult.StillRunning;
         }
+    }
 
-        public override string ToString() => "Mount";
+    internal sealed record UnmountTask : ITask
+    {
+        public bool ShouldRedoOnInterrupt() => true;
+
+        public override string ToString() => "Unmount";
     }
 
-    private sealed class UnmountTask(
+    internal sealed class UnmountExecutor(
         ICondition condition,
         ILogger<UnmountTask> logger,
         GameFunctions gameFunctions,
         IClientState clientState)
-        : ITask
+        : TaskExecutor<UnmountTask>
     {
         private bool _unmountTriggered;
         private DateTime _continueAt = DateTime.MinValue;
 
-        public bool ShouldRedoOnInterrupt() => true;
-
-        public bool Start()
+        protected override bool Start()
         {
             if (!condition[ConditionFlag.Mounted])
                 return false;
@@ -155,7 +143,7 @@ internal static class Mount
             return true;
         }
 
-        public ETaskResult Update()
+        public override ETaskResult Update()
         {
             if (_continueAt >= DateTime.Now)
                 return ETaskResult.StillRunning;
@@ -188,8 +176,6 @@ internal static class Mount
         }
 
         private unsafe bool IsUnmounting() => **(byte**)(clientState.LocalPlayer!.Address + 1432) == 1;
-
-        public override string ToString() => "Unmount";
     }
 
     public enum EMountIf
index 574c76ae7d4559e54258ef6031dd23e81f653a33..32aa7ad90a5d47b3c420af496c1b83b2d1f04d91 100644 (file)
@@ -7,7 +7,7 @@ namespace Questionable.Controller.Steps.Common;
 
 internal static class NextQuest
 {
-    internal sealed class Factory(QuestRegistry questRegistry, QuestController questController, QuestFunctions questFunctions, ILoggerFactory loggerFactory) : SimpleTaskFactory
+    internal sealed class Factory(QuestFunctions questFunctions) : SimpleTaskFactory
     {
         public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
         {
@@ -24,34 +24,41 @@ internal static class NextQuest
             if (questFunctions.GetPriorityQuests().Contains(step.NextQuestId))
                 return null;
 
-            return new SetQuest(step.NextQuestId, quest.Id, questRegistry, questController, questFunctions, loggerFactory.CreateLogger<SetQuest>());
+            return new SetQuestTask(step.NextQuestId, quest.Id);
         }
     }
 
-    private sealed class SetQuest(ElementId nextQuestId, ElementId currentQuestId, QuestRegistry questRegistry, QuestController questController, QuestFunctions questFunctions, ILogger<SetQuest> logger) : ITask
+    internal sealed record SetQuestTask(ElementId NextQuestId, ElementId CurrentQuestId) : ITask
     {
-        public bool Start()
+        public override string ToString() => $"SetNextQuest({NextQuestId})";
+    }
+
+    internal sealed class Executor(
+        QuestRegistry questRegistry,
+        QuestController questController,
+        QuestFunctions questFunctions,
+        ILogger<Executor> logger) : TaskExecutor<SetQuestTask>
+    {
+        protected override bool Start()
         {
-            if (questFunctions.IsQuestLocked(nextQuestId, currentQuestId))
+            if (questFunctions.IsQuestLocked(Task.NextQuestId, Task.CurrentQuestId))
             {
-                logger.LogInformation("Can't set next quest to {QuestId}, quest is locked", nextQuestId);
+                logger.LogInformation("Can't set next quest to {QuestId}, quest is locked", Task.NextQuestId);
             }
-            else if (questRegistry.TryGetQuest(nextQuestId, out Quest? quest))
+            else if (questRegistry.TryGetQuest(Task.NextQuestId, out Quest? quest))
             {
-                logger.LogInformation("Setting next quest to {QuestId}: '{QuestName}'", nextQuestId, quest.Info.Name);
+                logger.LogInformation("Setting next quest to {QuestId}: '{QuestName}'", Task.NextQuestId, quest.Info.Name);
                 questController.SetNextQuest(quest);
             }
             else
             {
-                logger.LogInformation("Next quest with id {QuestId} not found", nextQuestId);
+                logger.LogInformation("Next quest with id {QuestId} not found", Task.NextQuestId);
                 questController.SetNextQuest(null);
             }
 
             return true;
         }
 
-        public ETaskResult Update() => ETaskResult.TaskComplete;
-
-        public override string ToString() => $"SetNextQuest({nextQuestId})";
+        public override ETaskResult Update() => ETaskResult.TaskComplete;
     }
 }
index 762904ffe6b37021be7b97e5a98fc3c0efb578f7..9389bfb2323e1ec1f37ee86fe9a4e4671975a490 100644 (file)
@@ -2,22 +2,28 @@
 
 namespace Questionable.Controller.Steps.Common;
 
-internal sealed class WaitConditionTask(Func<bool> predicate, string description) : ITask
+internal static class WaitCondition
 {
-    private DateTime _continueAt = DateTime.MaxValue;
-
-    public bool Start() => !predicate();
+    internal sealed record Task(Func<bool> Predicate, string Description) : ITask
+    {
+        public override string ToString() => Description;
+    }
 
-    public ETaskResult Update()
+    internal sealed class Executor : TaskExecutor<Task>
     {
-        if (_continueAt == DateTime.MaxValue)
+        private DateTime _continueAt = DateTime.MaxValue;
+
+        protected override bool Start() => !Task.Predicate();
+
+        public override ETaskResult Update()
         {
-            if (predicate())
-                _continueAt = DateTime.Now.AddSeconds(0.5);
-        }
+            if (_continueAt == DateTime.MaxValue)
+            {
+                if (Task.Predicate())
+                    _continueAt = DateTime.Now.AddSeconds(0.5);
+            }
 
-        return DateTime.Now >= _continueAt ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
+            return DateTime.Now >= _continueAt ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
+        }
     }
-
-    public override string ToString() => description;
 }
index bc4057375f0681961ca2e108e5e054f70f1687e4..e0e05a94a5320bb884f6cae540ccbad7a613ff63 100644 (file)
@@ -15,227 +15,231 @@ using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps.Gathering;
 
-internal sealed class DoGather(
-    GatheringController.GatheringRequest currentRequest,
-    GatheringNode currentNode,
-    bool revisitRequired,
-    GatheringController gatheringController,
-    GameFunctions gameFunctions,
-    IGameGui gameGui,
-    IClientState clientState,
-    ICondition condition,
-    ILogger<DoGather> logger) : ITask, IRevisitAware
+internal static class DoGather
 {
-    private const uint StatusGatheringRateUp = 218;
+    internal sealed record Task(
+        GatheringController.GatheringRequest Request,
+        GatheringNode Node,
+        bool RevisitRequired) : ITask, IRevisitAware
+    {
+        public bool RevisitTriggered { get; private set; }
 
-    private bool _revisitTriggered;
-    private bool _wasGathering;
-    private SlotInfo? _slotToGather;
-    private Queue<EAction>? _actionQueue;
+        public void OnRevisit() => RevisitTriggered = true;
 
-    public bool Start() => true;
+        public override string ToString() => $"DoGather{(RevisitRequired ? " if revist" : "")}";
+    }
 
-    public unsafe ETaskResult Update()
+    internal sealed class Executor(
+        GatheringController gatheringController,
+        GameFunctions gameFunctions,
+        IGameGui gameGui,
+        IClientState clientState,
+        ICondition condition,
+        ILogger<Executor> logger) : TaskExecutor<Task>
     {
-        if (revisitRequired && !_revisitTriggered)
-        {
-            logger.LogInformation("No revisit");
-            return ETaskResult.TaskComplete;
-        }
+        private const uint StatusGatheringRateUp = 218;
 
-        if (gatheringController.HasNodeDisappeared(currentNode))
-        {
-            logger.LogInformation("Node disappeared");
-            return ETaskResult.TaskComplete;
-        }
+        private bool _wasGathering;
+        private SlotInfo? _slotToGather;
+        private Queue<EAction>? _actionQueue;
 
-        if (gameFunctions.GetFreeInventorySlots() == 0)
-            throw new TaskException("Inventory full");
+        protected override bool Start() => true;
 
-        if (condition[ConditionFlag.Gathering])
+        public override unsafe ETaskResult Update()
         {
-            if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* _))
+            if (Task is { RevisitRequired: true, RevisitTriggered: false })
+            {
+                logger.LogInformation("No revisit");
                 return ETaskResult.TaskComplete;
+            }
 
-            _wasGathering = true;
+            if (gatheringController.HasNodeDisappeared(Task.Node))
+            {
+                logger.LogInformation("Node disappeared");
+                return ETaskResult.TaskComplete;
+            }
 
-            if (gameGui.TryGetAddonByName("Gathering", out AddonGathering* addonGathering))
+            if (gameFunctions.GetFreeInventorySlots() == 0)
+                throw new TaskException("Inventory full");
+
+            if (condition[ConditionFlag.Gathering])
             {
-                if (gatheringController.HasRequestedItems())
-                {
-                    addonGathering->FireCallbackInt(-1);
-                }
-                else
+                if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* _))
+                    return ETaskResult.TaskComplete;
+
+                _wasGathering = true;
+
+                if (gameGui.TryGetAddonByName("Gathering", out AddonGathering* addonGathering))
                 {
-                    var slots = ReadSlots(addonGathering);
-                    if (currentRequest.Collectability > 0)
+                    if (gatheringController.HasRequestedItems())
                     {
-                        var slot = slots.Single(x => x.ItemId == currentRequest.ItemId);
-                        addonGathering->FireCallbackInt(slot.Index);
+                        addonGathering->FireCallbackInt(-1);
                     }
                     else
                     {
-                        NodeCondition nodeCondition = new NodeCondition(
-                            addonGathering->AtkValues[110].UInt,
-                            addonGathering->AtkValues[111].UInt);
-
-                        if (_actionQueue != null && _actionQueue.TryPeek(out EAction nextAction))
+                        var slots = ReadSlots(addonGathering);
+                        if (Task.Request.Collectability > 0)
                         {
-                            if (gameFunctions.UseAction(nextAction))
+                            var slot = slots.Single(x => x.ItemId == Task.Request.ItemId);
+                            addonGathering->FireCallbackInt(slot.Index);
+                        }
+                        else
+                        {
+                            NodeCondition nodeCondition = new NodeCondition(
+                                addonGathering->AtkValues[110].UInt,
+                                addonGathering->AtkValues[111].UInt);
+
+                            if (_actionQueue != null && _actionQueue.TryPeek(out EAction nextAction))
                             {
-                                logger.LogInformation("Used action {Action} on node", nextAction);
-                                _actionQueue.Dequeue();
-                            }
+                                if (gameFunctions.UseAction(nextAction))
+                                {
+                                    logger.LogInformation("Used action {Action} on node", nextAction);
+                                    _actionQueue.Dequeue();
+                                }
 
-                            return ETaskResult.StillRunning;
-                        }
+                                return ETaskResult.StillRunning;
+                            }
 
-                        _actionQueue = GetNextActions(nodeCondition, slots);
-                        if (_actionQueue.Count == 0)
-                        {
-                            var slot = _slotToGather ?? slots.Single(x => x.ItemId == currentRequest.ItemId);
-                            addonGathering->FireCallbackInt(slot.Index);
+                            _actionQueue = GetNextActions(nodeCondition, slots);
+                            if (_actionQueue.Count == 0)
+                            {
+                                var slot = _slotToGather ?? slots.Single(x => x.ItemId == Task.Request.ItemId);
+                                addonGathering->FireCallbackInt(slot.Index);
+                            }
                         }
                     }
                 }
             }
-        }
 
-        return _wasGathering && !condition[ConditionFlag.Gathering]
-            ? ETaskResult.TaskComplete
-            : ETaskResult.StillRunning;
-    }
+            return _wasGathering && !condition[ConditionFlag.Gathering]
+                ? ETaskResult.TaskComplete
+                : ETaskResult.StillRunning;
+        }
 
-    private unsafe List<SlotInfo> ReadSlots(AddonGathering* addonGathering)
-    {
-        var atkValues = addonGathering->AtkValues;
-        List<SlotInfo> slots = new List<SlotInfo>();
-        for (int i = 0; i < 8; ++i)
+        private unsafe List<SlotInfo> ReadSlots(AddonGathering* addonGathering)
         {
-            // +8 = new item?
-            uint itemId = atkValues[i * 11 + 7].UInt;
-            if (itemId == 0)
-                continue;
-
-            AtkComponentCheckBox* atkCheckbox = addonGathering->GatheredItemComponentCheckbox[i].Value;
+            var atkValues = addonGathering->AtkValues;
+            List<SlotInfo> slots = new List<SlotInfo>();
+            for (int i = 0; i < 8; ++i)
+            {
+                // +8 = new item?
+                uint itemId = atkValues[i * 11 + 7].UInt;
+                if (itemId == 0)
+                    continue;
 
-            AtkTextNode* atkGatheringChance = atkCheckbox->UldManager.SearchNodeById(10)->GetAsAtkTextNode();
-            if (!int.TryParse(atkGatheringChance->NodeText.ToString(), out int gatheringChance))
-                gatheringChance = 0;
+                AtkComponentCheckBox* atkCheckbox = addonGathering->GatheredItemComponentCheckbox[i].Value;
 
-            AtkTextNode* atkBoonChance = atkCheckbox->UldManager.SearchNodeById(16)->GetAsAtkTextNode();
-            if (!int.TryParse(atkBoonChance->NodeText.ToString(), out int boonChance))
-                boonChance = 0;
+                AtkTextNode* atkGatheringChance = atkCheckbox->UldManager.SearchNodeById(10)->GetAsAtkTextNode();
+                if (!int.TryParse(atkGatheringChance->NodeText.ToString(), out int gatheringChance))
+                    gatheringChance = 0;
 
-            AtkComponentNode* atkImage = atkCheckbox->UldManager.SearchNodeById(31)->GetAsAtkComponentNode();
-            AtkTextNode* atkQuantity = atkImage->Component->UldManager.SearchNodeById(7)->GetAsAtkTextNode();
-            if (!atkQuantity->IsVisible() || !int.TryParse(atkQuantity->NodeText.ToString(), out int quantity))
-                quantity = 1;
+                AtkTextNode* atkBoonChance = atkCheckbox->UldManager.SearchNodeById(16)->GetAsAtkTextNode();
+                if (!int.TryParse(atkBoonChance->NodeText.ToString(), out int boonChance))
+                    boonChance = 0;
 
-            var slot = new SlotInfo(i, itemId, gatheringChance, boonChance, quantity);
-            slots.Add(slot);
-        }
+                AtkComponentNode* atkImage = atkCheckbox->UldManager.SearchNodeById(31)->GetAsAtkComponentNode();
+                AtkTextNode* atkQuantity = atkImage->Component->UldManager.SearchNodeById(7)->GetAsAtkTextNode();
+                if (!atkQuantity->IsVisible() || !int.TryParse(atkQuantity->NodeText.ToString(), out int quantity))
+                    quantity = 1;
 
-        return slots;
-    }
+                var slot = new SlotInfo(i, itemId, gatheringChance, boonChance, quantity);
+                slots.Add(slot);
+            }
 
-    [SuppressMessage("ReSharper", "UnusedParameter.Local")]
-    private Queue<EAction> GetNextActions(NodeCondition nodeCondition, List<SlotInfo> slots)
-    {
-        //uint gp = clientState.LocalPlayer!.CurrentGp;
-        Queue<EAction> actions = new();
+            return slots;
+        }
 
-        if (!gameFunctions.HasStatus(StatusGatheringRateUp))
+        [SuppressMessage("ReSharper", "UnusedParameter.Local")]
+        private Queue<EAction> GetNextActions(NodeCondition nodeCondition, List<SlotInfo> slots)
         {
-            // do we have an alternative item? only happens for 'evaluation' leve quests
-            if (currentRequest.AlternativeItemId != 0)
-            {
-                var alternativeSlot = slots.Single(x => x.ItemId == currentRequest.AlternativeItemId);
+            //uint gp = clientState.LocalPlayer!.CurrentGp;
+            Queue<EAction> actions = new();
 
-                if (alternativeSlot.GatheringChance == 100)
+            if (!gameFunctions.HasStatus(StatusGatheringRateUp))
+            {
+                // do we have an alternative item? only happens for 'evaluation' leve quests
+                if (Task.Request.AlternativeItemId != 0)
                 {
-                    _slotToGather = alternativeSlot;
-                    return actions;
+                    var alternativeSlot = slots.Single(x => x.ItemId == Task.Request.AlternativeItemId);
+
+                    if (alternativeSlot.GatheringChance == 100)
+                    {
+                        _slotToGather = alternativeSlot;
+                        return actions;
+                    }
+
+                    if (alternativeSlot.GatheringChance > 0)
+                    {
+                        if (alternativeSlot.GatheringChance >= 95 &&
+                            CanUseAction(EAction.SharpVision1, EAction.FieldMastery1))
+                        {
+                            _slotToGather = alternativeSlot;
+                            actions.Enqueue(PickAction(EAction.SharpVision1, EAction.FieldMastery1));
+                            return actions;
+                        }
+
+                        if (alternativeSlot.GatheringChance >= 85 &&
+                            CanUseAction(EAction.SharpVision2, EAction.FieldMastery2))
+                        {
+                            _slotToGather = alternativeSlot;
+                            actions.Enqueue(PickAction(EAction.SharpVision2, EAction.FieldMastery2));
+                            return actions;
+                        }
+
+                        if (alternativeSlot.GatheringChance >= 50 &&
+                            CanUseAction(EAction.SharpVision3, EAction.FieldMastery3))
+                        {
+                            _slotToGather = alternativeSlot;
+                            actions.Enqueue(PickAction(EAction.SharpVision3, EAction.FieldMastery3));
+                            return actions;
+                        }
+                    }
                 }
 
-                if (alternativeSlot.GatheringChance > 0)
+                var slot = slots.Single(x => x.ItemId == Task.Request.ItemId);
+                if (slot.GatheringChance > 0 && slot.GatheringChance < 100)
                 {
-                    if (alternativeSlot.GatheringChance >= 95 &&
+                    if (slot.GatheringChance >= 95 &&
                         CanUseAction(EAction.SharpVision1, EAction.FieldMastery1))
                     {
-                        _slotToGather = alternativeSlot;
                         actions.Enqueue(PickAction(EAction.SharpVision1, EAction.FieldMastery1));
                         return actions;
                     }
 
-                    if (alternativeSlot.GatheringChance >= 85 &&
+                    if (slot.GatheringChance >= 85 &&
                         CanUseAction(EAction.SharpVision2, EAction.FieldMastery2))
                     {
-                        _slotToGather = alternativeSlot;
                         actions.Enqueue(PickAction(EAction.SharpVision2, EAction.FieldMastery2));
                         return actions;
                     }
 
-                    if (alternativeSlot.GatheringChance >= 50 &&
+                    if (slot.GatheringChance >= 50 &&
                         CanUseAction(EAction.SharpVision3, EAction.FieldMastery3))
                     {
-                        _slotToGather = alternativeSlot;
                         actions.Enqueue(PickAction(EAction.SharpVision3, EAction.FieldMastery3));
                         return actions;
                     }
                 }
             }
 
-            var slot = slots.Single(x => x.ItemId == currentRequest.ItemId);
-            if (slot.GatheringChance > 0 && slot.GatheringChance < 100)
-            {
-                if (slot.GatheringChance >= 95 &&
-                    CanUseAction(EAction.SharpVision1, EAction.FieldMastery1))
-                {
-                    actions.Enqueue(PickAction(EAction.SharpVision1, EAction.FieldMastery1));
-                    return actions;
-                }
-
-                if (slot.GatheringChance >= 85 &&
-                    CanUseAction(EAction.SharpVision2, EAction.FieldMastery2))
-                {
-                    actions.Enqueue(PickAction(EAction.SharpVision2, EAction.FieldMastery2));
-                    return actions;
-                }
-
-                if (slot.GatheringChance >= 50 &&
-                    CanUseAction(EAction.SharpVision3, EAction.FieldMastery3))
-                {
-                    actions.Enqueue(PickAction(EAction.SharpVision3, EAction.FieldMastery3));
-                    return actions;
-                }
-            }
+            return actions;
         }
 
-        return actions;
-    }
-
-    private EAction PickAction(EAction minerAction, EAction botanistAction)
-    {
-        if ((EClassJob?)clientState.LocalPlayer?.ClassJob.Id == EClassJob.Miner)
-            return minerAction;
-        else
-            return botanistAction;
-    }
-
-    private unsafe bool CanUseAction(EAction minerAction, EAction botanistAction)
-    {
-        EAction action = PickAction(minerAction, botanistAction);
-        return ActionManager.Instance()->GetActionStatus(ActionType.Action, (uint)action) == 0;
-    }
+        private EAction PickAction(EAction minerAction, EAction botanistAction)
+        {
+            if ((EClassJob?)clientState.LocalPlayer?.ClassJob.Id == EClassJob.Miner)
+                return minerAction;
+            else
+                return botanistAction;
+        }
 
-    public void OnRevisit()
-    {
-        _revisitTriggered = true;
+        private unsafe bool CanUseAction(EAction minerAction, EAction botanistAction)
+        {
+            EAction action = PickAction(minerAction, botanistAction);
+            return ActionManager.Instance()->GetActionStatus(ActionType.Action, (uint)action) == 0;
+        }
     }
 
-    public override string ToString() => $"DoGather{(revisitRequired ? " if revist" : "")}";
-
     [SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Local")]
     private sealed record SlotInfo(int Index, uint ItemId, int GatheringChance, int BoonChance, int Quantity);
 
index 99779c140095ed4bdcf62ba3d752d4d610ae309a..17376e10a03de610308e6b9637c1122c55a7d39a 100644 (file)
@@ -1,5 +1,4 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using Dalamud.Game.Text;
 using Dalamud.Plugin.Services;
@@ -13,189 +12,194 @@ using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps.Gathering;
 
-internal sealed class DoGatherCollectable(
-    GatheringController.GatheringRequest currentRequest,
-    GatheringNode currentNode,
-    bool revisitRequired,
-    GatheringController gatheringController,
-    GameFunctions gameFunctions,
-    IClientState clientState,
-    IGameGui gameGui,
-    ILogger<DoGatherCollectable> logger) : ITask, IRevisitAware
+internal static class DoGatherCollectable
 {
-    private bool _revisitTriggered;
-    private Queue<EAction>? _actionQueue;
+    internal sealed record Task(
+        GatheringController.GatheringRequest Request,
+        GatheringNode Node,
+        bool RevisitRequired) : ITask, IRevisitAware
+    {
+        public bool RevisitTriggered { get; private set; }
 
-    private bool? _expectedScrutiny;
+        public void OnRevisit() => RevisitTriggered = true;
 
-    public bool Start() => true;
+        public override string ToString() =>
+            $"DoGatherCollectable({SeIconChar.Collectible.ToIconString()}/{Request.Collectability}){(RevisitRequired ? " if revist" : "")}";
+    }
 
-    public unsafe ETaskResult Update()
+    internal sealed class Executor(
+        GatheringController gatheringController,
+        GameFunctions gameFunctions,
+        IClientState clientState,
+        IGameGui gameGui,
+        ILogger<Executor> logger) : TaskExecutor<Task>
     {
-        if (revisitRequired && !_revisitTriggered)
-        {
-            logger.LogInformation("No revisit");
-            return ETaskResult.TaskComplete;
-        }
+        private Queue<EAction>? _actionQueue;
 
-        if (gatheringController.HasNodeDisappeared(currentNode))
-        {
-            logger.LogInformation("Node disappeared");
-            return ETaskResult.TaskComplete;
-        }
+        private bool? _expectedScrutiny;
 
-        if (gatheringController.HasRequestedItems())
+        protected override bool Start() => true;
+
+        public override unsafe ETaskResult Update()
         {
-            if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* atkUnitBase))
+            if (Task.RevisitRequired && !Task.RevisitTriggered)
             {
-                atkUnitBase->FireCallbackInt(1);
-                return ETaskResult.StillRunning;
+                logger.LogInformation("No revisit");
+                return ETaskResult.TaskComplete;
             }
 
-            if (gameGui.TryGetAddonByName("Gathering", out atkUnitBase))
+            if (gatheringController.HasNodeDisappeared(Task.Node))
             {
-                atkUnitBase->FireCallbackInt(-1);
+                logger.LogInformation("Node disappeared");
                 return ETaskResult.TaskComplete;
             }
-        }
 
-        if (gameFunctions.GetFreeInventorySlots() == 0)
-            throw new TaskException("Inventory full");
+            if (gatheringController.HasRequestedItems())
+            {
+                if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* atkUnitBase))
+                {
+                    atkUnitBase->FireCallbackInt(1);
+                    return ETaskResult.StillRunning;
+                }
 
-        NodeCondition? nodeCondition = GetNodeCondition();
-        if (nodeCondition == null)
-            return ETaskResult.TaskComplete;
+                if (gameGui.TryGetAddonByName("Gathering", out atkUnitBase))
+                {
+                    atkUnitBase->FireCallbackInt(-1);
+                    return ETaskResult.TaskComplete;
+                }
+            }
 
-        if (_expectedScrutiny != null)
-        {
-            if (nodeCondition.ScrutinyActive != _expectedScrutiny)
+            if (gameFunctions.GetFreeInventorySlots() == 0)
+                throw new TaskException("Inventory full");
+
+            NodeCondition? nodeCondition = GetNodeCondition();
+            if (nodeCondition == null)
+                return ETaskResult.TaskComplete;
+
+            if (_expectedScrutiny != null)
+            {
+                if (nodeCondition.ScrutinyActive != _expectedScrutiny)
+                    return ETaskResult.StillRunning;
+
+                // continue on next frame
+                _expectedScrutiny = null;
                 return ETaskResult.StillRunning;
+            }
 
-            // continue on next frame
-            _expectedScrutiny = null;
-            return ETaskResult.StillRunning;
-        }
+            if (_actionQueue != null && _actionQueue.TryPeek(out EAction nextAction))
+            {
+                if (gameFunctions.UseAction(nextAction))
+                {
+                    _expectedScrutiny = nextAction switch
+                    {
+                        EAction.ScrutinyMiner or EAction.ScrutinyBotanist => true,
+                        EAction.ScourMiner or EAction.ScourBotanist or EAction.MeticulousMiner
+                            or EAction.MeticulousBotanist => false,
+                        _ => null
+                    };
+                    logger.LogInformation("Used action {Action} on node", nextAction);
+                    _actionQueue.Dequeue();
+                }
 
-        if (_actionQueue != null && _actionQueue.TryPeek(out EAction nextAction))
-        {
-            if (gameFunctions.UseAction(nextAction))
+                return ETaskResult.StillRunning;
+            }
+
+            if (nodeCondition.CollectabilityToGoal(Task.Request.Collectability) > 0)
             {
-                _expectedScrutiny = nextAction switch
+                _actionQueue = GetNextActions(nodeCondition);
+                if (_actionQueue != null)
                 {
-                    EAction.ScrutinyMiner or EAction.ScrutinyBotanist => true,
-                    EAction.ScourMiner or EAction.ScourBotanist or EAction.MeticulousMiner
-                        or EAction.MeticulousBotanist => false,
-                    _ => null
-                };
-                logger.LogInformation("Used action {Action} on node", nextAction);
-                _actionQueue.Dequeue();
+                    foreach (var action in _actionQueue)
+                        logger.LogInformation("Next Actions {Action}", action);
+                    return ETaskResult.StillRunning;
+                }
             }
 
+            _actionQueue = new Queue<EAction>();
+            _actionQueue.Enqueue(PickAction(EAction.CollectMiner, EAction.CollectBotanist));
             return ETaskResult.StillRunning;
         }
 
-        if (nodeCondition.CollectabilityToGoal(currentRequest.Collectability) > 0)
+        private unsafe NodeCondition? GetNodeCondition()
         {
-            _actionQueue = GetNextActions(nodeCondition);
-            if (_actionQueue != null)
+            if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* atkUnitBase))
             {
-                foreach (var action in _actionQueue)
-                    logger.LogInformation("Next Actions {Action}", action);
-                return ETaskResult.StillRunning;
+                var atkValues = atkUnitBase->AtkValues;
+                return new NodeCondition(
+                    CurrentCollectability: atkValues[13].UInt,
+                    MaxCollectability: atkValues[14].UInt,
+                    CurrentIntegrity: atkValues[62].UInt,
+                    MaxIntegrity: atkValues[63].UInt,
+                    ScrutinyActive: atkValues[54].Bool,
+                    CollectabilityFromScour: atkValues[48].UInt,
+                    CollectabilityFromMeticulous: atkValues[51].UInt
+                );
             }
-        }
-
-        _actionQueue = new Queue<EAction>();
-        _actionQueue.Enqueue(PickAction(EAction.CollectMiner, EAction.CollectBotanist));
-        return ETaskResult.StillRunning;
-    }
 
-    private unsafe NodeCondition? GetNodeCondition()
-    {
-        if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* atkUnitBase))
-        {
-            var atkValues = atkUnitBase->AtkValues;
-            return new NodeCondition(
-                CurrentCollectability: atkValues[13].UInt,
-                MaxCollectability: atkValues[14].UInt,
-                CurrentIntegrity: atkValues[62].UInt,
-                MaxIntegrity: atkValues[63].UInt,
-                ScrutinyActive: atkValues[54].Bool,
-                CollectabilityFromScour: atkValues[48].UInt,
-                CollectabilityFromMeticulous: atkValues[51].UInt
-            );
+            return null;
         }
 
-        return null;
-    }
+        private Queue<EAction> GetNextActions(NodeCondition nodeCondition)
+        {
+            uint gp = clientState.LocalPlayer!.CurrentGp;
+            logger.LogTrace(
+                "Getting next actions (with {GP} GP, {MeticulousCollectability}~ meticulous, {ScourCollectability}~ scour)",
+                gp, nodeCondition.CollectabilityFromMeticulous, nodeCondition.CollectabilityFromScour);
 
-    private Queue<EAction> GetNextActions(NodeCondition nodeCondition)
-    {
-        uint gp = clientState.LocalPlayer!.CurrentGp;
-        logger.LogTrace(
-            "Getting next actions (with {GP} GP, {MeticulousCollectability}~ meticulous, {ScourCollectability}~ scour)",
-            gp, nodeCondition.CollectabilityFromMeticulous, nodeCondition.CollectabilityFromScour);
+            Queue<EAction> actions = new();
 
-        Queue<EAction> actions = new();
+            uint neededCollectability = nodeCondition.CollectabilityToGoal(Task.Request.Collectability);
+            if (neededCollectability <= nodeCondition.CollectabilityFromMeticulous)
+            {
+                logger.LogTrace("Can get all needed {NeededCollectability} from {Collectability}~ meticulous",
+                    neededCollectability, nodeCondition.CollectabilityFromMeticulous);
+                actions.Enqueue(PickAction(EAction.MeticulousMiner, EAction.MeticulousBotanist));
+                return actions;
+            }
 
-        uint neededCollectability = nodeCondition.CollectabilityToGoal(currentRequest.Collectability);
-        if (neededCollectability <= nodeCondition.CollectabilityFromMeticulous)
-        {
-            logger.LogTrace("Can get all needed {NeededCollectability} from {Collectability}~ meticulous",
-                neededCollectability, nodeCondition.CollectabilityFromMeticulous);
-            actions.Enqueue(PickAction(EAction.MeticulousMiner, EAction.MeticulousBotanist));
-            return actions;
-        }
+            if (neededCollectability <= nodeCondition.CollectabilityFromScour)
+            {
+                logger.LogTrace("Can get all needed {NeededCollectability} from {Collectability}~ scour",
+                    neededCollectability, nodeCondition.CollectabilityFromScour);
+                actions.Enqueue(PickAction(EAction.ScourMiner, EAction.ScourBotanist));
+                return actions;
+            }
 
-        if (neededCollectability <= nodeCondition.CollectabilityFromScour)
-        {
-            logger.LogTrace("Can get all needed {NeededCollectability} from {Collectability}~ scour",
-                neededCollectability, nodeCondition.CollectabilityFromScour);
-            actions.Enqueue(PickAction(EAction.ScourMiner, EAction.ScourBotanist));
-            return actions;
-        }
+            // neither action directly solves our problem
+            if (!nodeCondition.ScrutinyActive && gp >= 200)
+            {
+                logger.LogTrace("Still missing {NeededCollectability} collectability, scrutiny inactive",
+                    neededCollectability);
+                actions.Enqueue(PickAction(EAction.ScrutinyMiner, EAction.ScrutinyBotanist));
+                return actions;
+            }
 
-        // neither action directly solves our problem
-        if (!nodeCondition.ScrutinyActive && gp >= 200)
-        {
-            logger.LogTrace("Still missing {NeededCollectability} collectability, scrutiny inactive",
-                neededCollectability);
-            actions.Enqueue(PickAction(EAction.ScrutinyMiner, EAction.ScrutinyBotanist));
-            return actions;
+            if (nodeCondition.ScrutinyActive)
+            {
+                logger.LogTrace(
+                    "Scrutiny active, need {NeededCollectability} and we expect {Collectability}~ meticulous",
+                    neededCollectability, nodeCondition.CollectabilityFromMeticulous);
+                actions.Enqueue(PickAction(EAction.MeticulousMiner, EAction.MeticulousBotanist));
+                return actions;
+            }
+            else
+            {
+                logger.LogTrace("Scrutiny active, need {NeededCollectability} and we expect {Collectability}~ scour",
+                    neededCollectability, nodeCondition.CollectabilityFromScour);
+                actions.Enqueue(PickAction(EAction.ScourMiner, EAction.ScourBotanist));
+                return actions;
+            }
         }
 
-        if (nodeCondition.ScrutinyActive)
-        {
-            logger.LogTrace("Scrutiny active, need {NeededCollectability} and we expect {Collectability}~ meticulous",
-                neededCollectability, nodeCondition.CollectabilityFromMeticulous);
-            actions.Enqueue(PickAction(EAction.MeticulousMiner, EAction.MeticulousBotanist));
-            return actions;
-        }
-        else
+        private EAction PickAction(EAction minerAction, EAction botanistAction)
         {
-            logger.LogTrace("Scrutiny active, need {NeededCollectability} and we expect {Collectability}~ scour",
-                neededCollectability, nodeCondition.CollectabilityFromScour);
-            actions.Enqueue(PickAction(EAction.ScourMiner, EAction.ScourBotanist));
-            return actions;
+            if ((EClassJob?)clientState.LocalPlayer?.ClassJob.Id == EClassJob.Miner)
+                return minerAction;
+            else
+                return botanistAction;
         }
     }
 
-    private EAction PickAction(EAction minerAction, EAction botanistAction)
-    {
-        if ((EClassJob?)clientState.LocalPlayer?.ClassJob.Id == EClassJob.Miner)
-            return minerAction;
-        else
-            return botanistAction;
-    }
-
-    public void OnRevisit()
-    {
-        _revisitTriggered = true;
-    }
-
-    public override string ToString() =>
-        $"DoGatherCollectable({SeIconChar.Collectible.ToIconString()}/{_expectedScrutiny} {currentRequest.Collectability}){(revisitRequired ? " if revist" : "")}";
-
     [SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Local")]
     private sealed record NodeCondition(
         uint CurrentCollectability,
index a9ebe4a5d9897bf378ee28816638488bfbec3a1e..cc9f4d979fc35c487623826fd4968b54c3d7443a 100644 (file)
@@ -3,7 +3,6 @@ using System.Linq;
 using System.Numerics;
 using Dalamud.Game.ClientState.Objects.Enums;
 using Dalamud.Plugin.Services;
-using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Steps.Shared;
 using Questionable.Functions;
@@ -12,41 +11,49 @@ using Questionable.Model.Gathering;
 
 namespace Questionable.Controller.Steps.Gathering;
 
-internal sealed class MoveToLandingLocation(
-    ushort territoryId,
-    bool flyBetweenNodes,
-    GatheringNode gatheringNode,
-    MoveTo.Factory moveFactory,
-    GameFunctions gameFunctions,
-    IObjectTable objectTable,
-    ILogger<MoveToLandingLocation> logger) : ITask
+internal static class MoveToLandingLocation
 {
-    private ITask _moveTask = null!;
+    internal sealed record Task(
+        ushort TerritoryId,
+        bool FlyBetweenNodes,
+        GatheringNode GatheringNode) : ITask
+    {
+        public override string ToString() => $"Land/{FlyBetweenNodes}";
+    }
 
-    public bool Start()
+    internal sealed class Executor(
+        MoveTo.MoveExecutor moveExecutor,
+        GameFunctions gameFunctions,
+        IObjectTable objectTable,
+        ILogger<Executor> logger) : TaskExecutor<Task>
     {
-        var location = gatheringNode.Locations.First();
-        if (gatheringNode.Locations.Count > 1)
-        {
-            var gameObject = objectTable.SingleOrDefault(x =>
-                x.ObjectKind == ObjectKind.GatheringPoint && x.DataId == gatheringNode.DataId && x.IsTargetable);
-            if (gameObject == null)
-                return false;
+        private ITask _moveTask = null!;
 
-            location = gatheringNode.Locations.Single(x => Vector3.Distance(x.Position, gameObject.Position) < 0.1f);
+        protected override bool Start()
+        {
+            var location = Task.GatheringNode.Locations.First();
+            if (Task.GatheringNode.Locations.Count > 1)
+            {
+                var gameObject = objectTable.SingleOrDefault(x =>
+                    x.ObjectKind == ObjectKind.GatheringPoint && x.DataId == Task.GatheringNode.DataId &&
+                    x.IsTargetable);
+                if (gameObject == null)
+                    return false;
+
+                location = Task.GatheringNode.Locations.Single(x =>
+                    Vector3.Distance(x.Position, gameObject.Position) < 0.1f);
+            }
+
+            var (target, degrees, range) = GatheringMath.CalculateLandingLocation(location);
+            logger.LogInformation("Preliminary landing location: {Location}, with degrees = {Degrees}, range = {Range}",
+                target.ToString("G", CultureInfo.InvariantCulture), degrees, range);
+
+            bool fly = Task.FlyBetweenNodes && gameFunctions.IsFlyingUnlocked(Task.TerritoryId);
+            _moveTask = new MoveTo.MoveTask(Task.TerritoryId, target, null, 0.25f,
+                DataId: Task.GatheringNode.DataId, Fly: fly, IgnoreDistanceToObject: true);
+            return moveExecutor.Start(_moveTask);
         }
 
-        var (target, degrees, range) = GatheringMath.CalculateLandingLocation(location);
-        logger.LogInformation("Preliminary landing location: {Location}, with degrees = {Degrees}, range = {Range}",
-            target.ToString("G", CultureInfo.InvariantCulture), degrees, range);
-
-        bool fly = flyBetweenNodes && gameFunctions.IsFlyingUnlocked(territoryId);
-        _moveTask = moveFactory.Move(new MoveTo.MoveParams(territoryId, target, null, 0.25f,
-            DataId: gatheringNode.DataId, Fly: fly, IgnoreDistanceToObject: true));
-        return _moveTask.Start();
+        public override ETaskResult Update() => moveExecutor.Update();
     }
-
-    public ETaskResult Update() => _moveTask.Update();
-
-    public override string ToString() => $"Land/{_moveTask}/{flyBetweenNodes}";
 }
index 08da14409da2a97b55d8b7c73cab3c41103f566a..caf2b0f46387ad1c662ea13d6ded1d2c68625310 100644 (file)
@@ -1,9 +1,7 @@
-using System;
-using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.Game;
 using FFXIVClientStructs.FFXIV.Client.UI.Agent;
 using FFXIVClientStructs.FFXIV.Component.GUI;
 using LLib.GameUI;
-using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Model;
 using Questionable.Model.Questing;
@@ -13,24 +11,29 @@ namespace Questionable.Controller.Steps.Gathering;
 
 internal static class TurnInDelivery
 {
-    internal sealed class Factory(ILoggerFactory loggerFactory) : SimpleTaskFactory
+    internal sealed class Factory : SimpleTaskFactory
     {
         public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
         {
             if (quest.Id is not SatisfactionSupplyNpcId || sequence.Sequence != 1)
                 return null;
 
-            return new SatisfactionSupplyTurnIn(loggerFactory.CreateLogger<SatisfactionSupplyTurnIn>());
+            return new Task();
         }
     }
 
-    private sealed class SatisfactionSupplyTurnIn(ILogger<SatisfactionSupplyTurnIn> logger) : ITask
+    internal sealed record Task : ITask
+    {
+        public override string ToString() => "WeeklyDeliveryTurnIn";
+    }
+
+    internal sealed class SatisfactionSupplyTurnIn(ILogger<SatisfactionSupplyTurnIn> logger) : TaskExecutor<Task>
     {
         private ushort? _remainingAllowances;
 
-        public bool Start() => true;
+        protected override bool Start() => true;
 
-        public unsafe ETaskResult Update()
+        public override unsafe ETaskResult Update()
         {
             AgentSatisfactionSupply* agentSatisfactionSupply = AgentSatisfactionSupply.Instance();
             if (agentSatisfactionSupply == null || !agentSatisfactionSupply->IsAgentActive())
@@ -77,7 +80,5 @@ internal static class TurnInDelivery
             addon->FireCallback(2, pickGatheringItem);
             return ETaskResult.StillRunning;
         }
-
-        public override string ToString() => "WeeklyDeliveryTurnIn";
     }
 }
index 4e41d6389b7b116fe452eb0c34778477e3d8e847..215106f6f1a78d21f59102fd05835ff0a0173c89 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace Questionable.Controller.Steps;
 
-public interface IConditionChangeAware
+internal interface IConditionChangeAware : ITaskExecutor
 {
     void OnConditionChange(ConditionFlag flag, bool value);
 }
index 4faf1c0bb215cdf9b9be16f1020c3298e171a56c..6af5d689f979cdae5c2b9d78d3c9c9969ec4af49 100644 (file)
@@ -1,6 +1,6 @@
 namespace Questionable.Controller.Steps;
 
-public interface IRevisitAware
+internal interface IRevisitAware : ITask
 {
     void OnRevisit();
 }
index a8442c7670bc784adb7fd84e6b85efe55bee4f49..60c22cf937ceef73e1f88bbbc23e54343c90dff6 100644 (file)
@@ -1,27 +1,6 @@
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Questionable.Controller.Steps;
+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();
-
-    ETaskResult Update();
 }
index 67a1c7f62514a015a7c4757b43388403d5fde24e..49e09d504d4520b4585aa2175ef78f06abfc63e8 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace Questionable.Controller.Steps;
 
-public interface IToastAware
+internal interface IToastAware : ITaskExecutor
 {
     bool OnErrorToast(SeString message);
 }
index f50f669700373806fae5a3ddab433c1430984be9..e6c4f1f484d037446e1905af09da4081e33c4fb0 100644 (file)
@@ -11,8 +11,7 @@ namespace Questionable.Controller.Steps.Interactions;
 
 internal static class Action
 {
-    internal sealed class Factory(GameFunctions gameFunctions, Mount.Factory mountFactory, ILoggerFactory loggerFactory)
-        : ITaskFactory
+    internal sealed class Factory : ITaskFactory
     {
         public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
         {
@@ -25,39 +24,43 @@ internal static class Action
             if (step.Action.Value.RequiresMount())
                 return [task];
             else
-                return [mountFactory.Unmount(), task];
+                return [new Mount.UnmountTask(), task];
         }
 
-        public ITask OnObject(uint? dataId, EAction action)
+        public static ITask OnObject(uint? dataId, EAction action)
         {
-            return new UseOnObject(dataId, action, gameFunctions,
-                loggerFactory.CreateLogger<UseOnObject>());
+            return new UseOnObject(dataId, action);
         }
     }
 
-    private sealed class UseOnObject(
-        uint? dataId,
-        EAction action,
+    internal sealed record UseOnObject(
+        uint? DataId,
+        EAction Action) : ITask
+    {
+        public override string ToString() => $"Action({Action})";
+    }
+
+    internal sealed class UseOnObjectExecutor(
         GameFunctions gameFunctions,
-        ILogger<UseOnObject> logger) : ITask
+        ILogger<UseOnObject> logger) : TaskExecutor<UseOnObject>
     {
         private bool _usedAction;
         private DateTime _continueAt = DateTime.MinValue;
 
-        public bool Start()
+        protected override bool Start()
         {
-            if (dataId != null)
+            if (Task.DataId != null)
             {
-                IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId.Value);
+                IGameObject? gameObject = gameFunctions.FindObjectByDataId(Task.DataId.Value);
                 if (gameObject == null)
                 {
-                    logger.LogWarning("No game object with dataId {DataId}", dataId);
+                    logger.LogWarning("No game object with dataId {DataId}", Task.DataId);
                     return false;
                 }
 
                 if (gameObject.IsTargetable)
                 {
-                    if (action == EAction.Diagnosis)
+                    if (Task.Action == EAction.Diagnosis)
                     {
                         uint eukrasiaAura = 2606;
                         // If SGE have Eukrasia status, we need to remove it.
@@ -72,14 +75,14 @@ internal static class Action
                         }
                     }
                     
-                    _usedAction = gameFunctions.UseAction(gameObject, action);
+                    _usedAction = gameFunctions.UseAction(gameObject, Task.Action);
                     _continueAt = DateTime.Now.AddSeconds(0.5);
                     return true;
                 }
             }
             else
             {
-                _usedAction = gameFunctions.UseAction(action);
+                _usedAction = gameFunctions.UseAction(Task.Action);
                 _continueAt = DateTime.Now.AddSeconds(0.5);
                 return true;
             }
@@ -87,25 +90,25 @@ internal static class Action
             return true;
         }
 
-        public ETaskResult Update()
+        public override ETaskResult Update()
         {
             if (DateTime.Now <= _continueAt)
                 return ETaskResult.StillRunning;
 
             if (!_usedAction)
             {
-                if (dataId != null)
+                if (Task.DataId != null)
                 {
-                    IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId.Value);
+                    IGameObject? gameObject = gameFunctions.FindObjectByDataId(Task.DataId.Value);
                     if (gameObject == null || !gameObject.IsTargetable)
                         return ETaskResult.StillRunning;
 
-                    _usedAction = gameFunctions.UseAction(gameObject, action);
+                    _usedAction = gameFunctions.UseAction(gameObject, Task.Action);
                     _continueAt = DateTime.Now.AddSeconds(0.5);
                 }
                 else
                 {
-                    _usedAction = gameFunctions.UseAction(action);
+                    _usedAction = gameFunctions.UseAction(Task.Action);
                     _continueAt = DateTime.Now.AddSeconds(0.5);
                 }
 
@@ -114,7 +117,5 @@ internal static class Action
 
             return ETaskResult.TaskComplete;
         }
-
-        public override string ToString() => $"Action({action})";
     }
-}
\ No newline at end of file
+}
index cbbe68686d793825d8c7683a8d314a89d016bf1c..e0f65c1a8f8c3dfce9894f8668e528dd6f384032 100644 (file)
@@ -12,10 +12,8 @@ namespace Questionable.Controller.Steps.Interactions;
 internal static class AetherCurrent
 {
     internal sealed class Factory(
-        GameFunctions gameFunctions,
         AetherCurrentData aetherCurrentData,
-        IChatGui chatGui,
-        ILoggerFactory loggerFactory) : SimpleTaskFactory
+        IChatGui chatGui) : SimpleTaskFactory
     {
         public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
         {
@@ -32,42 +30,39 @@ internal static class AetherCurrent
                 return null;
             }
 
-            return new DoAttune(step.DataId.Value, step.AetherCurrentId.Value, gameFunctions,
-                loggerFactory.CreateLogger<DoAttune>());
+            return new Attune(step.DataId.Value, step.AetherCurrentId.Value);
         }
     }
 
-    private sealed class DoAttune(
-        uint dataId,
-        uint aetherCurrentId,
-        GameFunctions gameFunctions,
-        ILogger<DoAttune> logger) : ITask
+    internal sealed record Attune(uint DataId, uint AetherCurrentId) : ITask
     {
-        private InteractionProgressContext? _progressContext;
-
-        public InteractionProgressContext? ProgressContext() => _progressContext;
+        public override string ToString() => $"AttuneAetherCurrent({AetherCurrentId})";
+    }
 
-        public bool Start()
+    internal sealed class DoAttune(
+        GameFunctions gameFunctions,
+        ILogger<DoAttune> logger) : TaskExecutor<Attune>
+    {
+        protected override bool Start()
         {
-            if (!gameFunctions.IsAetherCurrentUnlocked(aetherCurrentId))
+            if (!gameFunctions.IsAetherCurrentUnlocked(Task.AetherCurrentId))
             {
-                logger.LogInformation("Attuning to aether current {AetherCurrentId} / {DataId}", aetherCurrentId,
-                    dataId);
-                _progressContext =
-                    InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(dataId));
+                logger.LogInformation("Attuning to aether current {AetherCurrentId} / {DataId}", Task.AetherCurrentId,
+                    Task.DataId);
+                ProgressContext =
+                    InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(Task.DataId));
                 return true;
             }
 
-            logger.LogInformation("Already attuned to aether current {AetherCurrentId} / {DataId}", aetherCurrentId,
-                dataId);
+            logger.LogInformation("Already attuned to aether current {AetherCurrentId} / {DataId}",
+                Task.AetherCurrentId,
+                Task.DataId);
             return false;
         }
 
-        public ETaskResult Update() =>
-            gameFunctions.IsAetherCurrentUnlocked(aetherCurrentId)
+        public override ETaskResult Update() =>
+            gameFunctions.IsAetherCurrentUnlocked(Task.AetherCurrentId)
                 ? ETaskResult.TaskComplete
                 : ETaskResult.StillRunning;
-
-        public override string ToString() => $"AttuneAetherCurrent({aetherCurrentId})";
     }
 }
index ed5580c8c2062daf8daf24dbca081a75976a0ea4..d7d09a0c746d6382266a2581d202b494af950db9 100644 (file)
@@ -11,10 +11,7 @@ namespace Questionable.Controller.Steps.Interactions;
 
 internal static class AethernetShard
 {
-    internal sealed class Factory(
-        AetheryteFunctions aetheryteFunctions,
-        GameFunctions gameFunctions,
-        ILoggerFactory loggerFactory) : SimpleTaskFactory
+    internal sealed class Factory : SimpleTaskFactory
     {
         public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
         {
@@ -23,40 +20,37 @@ internal static class AethernetShard
 
             ArgumentNullException.ThrowIfNull(step.AethernetShard);
 
-            return new DoAttune(step.AethernetShard.Value, aetheryteFunctions, gameFunctions,
-                loggerFactory.CreateLogger<DoAttune>());
+            return new Attune(step.AethernetShard.Value);
         }
     }
 
-    private sealed class DoAttune(
-        EAetheryteLocation aetheryteLocation,
+    internal sealed record Attune(EAetheryteLocation AetheryteLocation) : ITask
+    {
+        public override string ToString() => $"AttuneAethernetShard({AetheryteLocation})";
+    }
+
+    internal sealed class DoAttune(
         AetheryteFunctions aetheryteFunctions,
         GameFunctions gameFunctions,
-        ILogger<DoAttune> logger) : ITask
+        ILogger<DoAttune> logger) : TaskExecutor<Attune>
     {
-        private InteractionProgressContext? _progressContext;
-
-        public InteractionProgressContext? ProgressContext() => _progressContext;
-
-        public bool Start()
+        protected override bool Start()
         {
-            if (!aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation))
+            if (!aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation))
             {
-                logger.LogInformation("Attuning to aethernet shard {AethernetShard}", aetheryteLocation);
-                _progressContext = InteractionProgressContext.FromActionUseOrDefault(() =>
-                    gameFunctions.InteractWith((uint)aetheryteLocation, ObjectKind.Aetheryte));
+                logger.LogInformation("Attuning to aethernet shard {AethernetShard}", Task.AetheryteLocation);
+                ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() =>
+                    gameFunctions.InteractWith((uint)Task.AetheryteLocation, ObjectKind.Aetheryte));
                 return true;
             }
 
-            logger.LogInformation("Already attuned to aethernet shard {AethernetShard}", aetheryteLocation);
+            logger.LogInformation("Already attuned to aethernet shard {AethernetShard}", Task.AetheryteLocation);
             return false;
         }
 
-        public ETaskResult Update() =>
-            aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation)
+        public override ETaskResult Update() =>
+            aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation)
                 ? ETaskResult.TaskComplete
                 : ETaskResult.StillRunning;
-
-        public override string ToString() => $"AttuneAethernetShard({aetheryteLocation})";
     }
 }
index eace64099b629803c56eb9faf58cc010f03483bc..6d3cb54dc8988bc4412d289215820347072366f9 100644 (file)
@@ -1,5 +1,4 @@
 using System;
-using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Functions;
 using Questionable.Model;
@@ -10,10 +9,7 @@ namespace Questionable.Controller.Steps.Interactions;
 
 internal static class Aetheryte
 {
-    internal sealed class Factory(
-        AetheryteFunctions aetheryteFunctions,
-        GameFunctions gameFunctions,
-        ILoggerFactory loggerFactory) : SimpleTaskFactory
+    internal sealed class Factory : SimpleTaskFactory
     {
         public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
         {
@@ -22,41 +18,38 @@ internal static class Aetheryte
 
             ArgumentNullException.ThrowIfNull(step.Aetheryte);
 
-            return new DoAttune(step.Aetheryte.Value, aetheryteFunctions, gameFunctions,
-                loggerFactory.CreateLogger<DoAttune>());
+            return new Attune(step.Aetheryte.Value);
         }
     }
 
-    private sealed class DoAttune(
-        EAetheryteLocation aetheryteLocation,
+    internal sealed record Attune(EAetheryteLocation AetheryteLocation) : ITask
+    {
+        public override string ToString() => $"AttuneAetheryte({AetheryteLocation})";
+    }
+
+    internal sealed class DoAttune(
         AetheryteFunctions aetheryteFunctions,
         GameFunctions gameFunctions,
-        ILogger<DoAttune> logger) : ITask
+        ILogger<DoAttune> logger) : TaskExecutor<Attune>
     {
-        private InteractionProgressContext? _progressContext;
-
-        public InteractionProgressContext? ProgressContext() => _progressContext;
-
-        public bool Start()
+        protected override bool Start()
         {
-            if (!aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation))
+            if (!aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation))
             {
-                logger.LogInformation("Attuning to aetheryte {Aetheryte}", aetheryteLocation);
-                _progressContext =
+                logger.LogInformation("Attuning to aetheryte {Aetheryte}", Task.AetheryteLocation);
+                ProgressContext =
                     InteractionProgressContext.FromActionUseOrDefault(() =>
-                        gameFunctions.InteractWith((uint)aetheryteLocation));
+                        gameFunctions.InteractWith((uint)Task.AetheryteLocation));
                 return true;
             }
 
-            logger.LogInformation("Already attuned to aetheryte {Aetheryte}", aetheryteLocation);
+            logger.LogInformation("Already attuned to aetheryte {Aetheryte}", Task.AetheryteLocation);
             return false;
         }
 
-        public ETaskResult Update() =>
-            aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation)
+        public override ETaskResult Update() =>
+            aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation)
                 ? ETaskResult.TaskComplete
                 : ETaskResult.StillRunning;
-
-        public override string ToString() => $"AttuneAetheryte({aetheryteLocation})";
     }
 }
index 170883a3d8c76db7dbff36c3ad56dac6a6f89812..514f157529a8ba4fd8e3d9f8a650379b56f3017f 100644 (file)
@@ -13,14 +13,7 @@ namespace Questionable.Controller.Steps.Interactions;
 
 internal static class Combat
 {
-    internal sealed class Factory(
-        CombatController combatController,
-        Interact.Factory interactFactory,
-        Mount.Factory mountFactory,
-        UseItem.Factory useItemFactory,
-        Action.Factory actionFactory,
-        QuestFunctions questFunctions,
-        GameFunctions gameFunctions) : ITaskFactory
+    internal sealed class Factory(GameFunctions gameFunctions) : ITaskFactory
     {
         public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
         {
@@ -30,7 +23,7 @@ internal static class Combat
             ArgumentNullException.ThrowIfNull(step.EnemySpawnType);
 
             if (gameFunctions.GetMountId() != Mount128Module.MountId)
-                yield return mountFactory.Unmount();
+                yield return new Mount.UnmountTask();
 
             if (step.CombatDelaySecondsAtStart != null)
             {
@@ -43,7 +36,7 @@ internal static class Combat
                 {
                     ArgumentNullException.ThrowIfNull(step.DataId);
 
-                    yield return interactFactory.Interact(step.DataId.Value, quest, EInteractionType.None, true);
+                    yield return new Interact.Task(step.DataId.Value, quest, EInteractionType.None, true);
                     yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(1));
                     yield return CreateTask(quest, sequence, step);
                     break;
@@ -54,7 +47,7 @@ internal static class Combat
                     ArgumentNullException.ThrowIfNull(step.DataId);
                     ArgumentNullException.ThrowIfNull(step.ItemId);
 
-                    yield return useItemFactory.OnObject(quest.Id, step.DataId.Value, step.ItemId.Value,
+                    yield return new UseItem.UseOnObject(quest.Id, step.DataId.Value, step.ItemId.Value,
                         step.CompletionQuestVariablesFlags, true);
                     yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(1));
                     yield return CreateTask(quest, sequence, step);
@@ -67,8 +60,8 @@ internal static class Combat
                     ArgumentNullException.ThrowIfNull(step.Action);
 
                     if (!step.Action.Value.RequiresMount())
-                        yield return mountFactory.Unmount();
-                    yield return actionFactory.OnObject(step.DataId.Value, step.Action.Value);
+                        yield return new Mount.UnmountTask();
+                    yield return new Action.UseOnObject(step.DataId.Value, step.Action.Value);
                     yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(1));
                     yield return CreateTask(quest, sequence, step);
                     break;
@@ -92,7 +85,7 @@ internal static class Combat
             }
         }
 
-        public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+        private static Task CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
         {
             ArgumentNullException.ThrowIfNull(step.EnemySpawnType);
 
@@ -101,46 +94,60 @@ internal static class Combat
                 step.CompletionQuestVariablesFlags, step.ComplexCombatData);
         }
 
-        internal HandleCombat CreateTask(ElementId? elementId, bool isLastStep, EEnemySpawnType enemySpawnType,
+        internal static Task CreateTask(ElementId? elementId, bool isLastStep, EEnemySpawnType enemySpawnType,
             IList<uint> killEnemyDataIds, IList<QuestWorkValue?> completionQuestVariablesFlags,
             IList<ComplexCombatData> complexCombatData)
         {
-            return new HandleCombat(isLastStep, new CombatController.CombatData
+            return new Task(new CombatController.CombatData
             {
                 ElementId = elementId,
                 SpawnType = enemySpawnType,
                 KillEnemyDataIds = killEnemyDataIds.ToList(),
                 ComplexCombatDatas = complexCombatData.ToList(),
-            }, completionQuestVariablesFlags, combatController, questFunctions);
+            }, completionQuestVariablesFlags, isLastStep);
+        }
+    }
+
+    internal sealed record Task(
+        CombatController.CombatData CombatData,
+        IList<QuestWorkValue?> CompletionQuestVariableFlags,
+        bool IsLastStep) : ITask
+    {
+        public override string ToString()
+        {
+            if (QuestWorkUtils.HasCompletionFlags(CompletionQuestVariableFlags))
+                return $"HandleCombat(wait: QW flags)";
+            else if (IsLastStep)
+                return $"HandleCombat(wait: next sequence)";
+            else
+                return $"HandleCombat(wait: not in combat)";
         }
     }
 
     internal sealed class HandleCombat(
-        bool isLastStep,
-        CombatController.CombatData combatData,
-        IList<QuestWorkValue?> completionQuestVariableFlags,
+
         CombatController combatController,
-        QuestFunctions questFunctions) : ITask
+        QuestFunctions questFunctions) : TaskExecutor<Task>
     {
         private CombatController.EStatus _status = CombatController.EStatus.NotStarted;
 
-        public bool Start() => combatController.Start(combatData);
+        protected override bool Start() => combatController.Start(Task.CombatData);
 
-        public ETaskResult Update()
+        public override ETaskResult Update()
         {
             _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
-            if (QuestWorkUtils.HasCompletionFlags(completionQuestVariableFlags) &&
-                combatData.ElementId is QuestId questId)
+            if (QuestWorkUtils.HasCompletionFlags(Task.CompletionQuestVariableFlags) &&
+                Task.CombatData.ElementId is QuestId questId)
             {
                 var questWork = questFunctions.GetQuestProgressInfo(questId);
                 if (questWork == null)
                     return ETaskResult.StillRunning;
 
-                if (QuestWorkUtils.MatchesQuestWork(completionQuestVariableFlags, questWork))
+                if (QuestWorkUtils.MatchesQuestWork(Task.CompletionQuestVariableFlags, questWork))
                     return ETaskResult.TaskComplete;
                 else
                     return ETaskResult.StillRunning;
@@ -148,7 +155,7 @@ internal static class Combat
 
             // the last step, by definition, can only be progressed by the game recognizing we're in a new sequence,
             // so this is an indefinite wait
-            if (isLastStep)
+            if (Task.IsLastStep)
                 return ETaskResult.StillRunning;
             else
             {
@@ -156,15 +163,5 @@ internal static class Combat
                 return ETaskResult.TaskComplete;
             }
         }
-
-        public override string ToString()
-        {
-            if (QuestWorkUtils.HasCompletionFlags(completionQuestVariableFlags))
-                return $"HandleCombat(wait: QW flags, s: {_status})";
-            else if (isLastStep)
-                return $"HandleCombat(wait: next sequence, s: {_status})";
-            else
-                return $"HandleCombat(wait: not in combat, s: {_status})";
-        }
     }
 }
index 0fb2d3a6eda080d03e8a24f151297616d28c9f46..b5389774cb921ba341e4676e103fc1eeb85c82d1 100644 (file)
@@ -18,24 +18,25 @@ namespace Questionable.Controller.Steps.Interactions;
 
 internal static class Dive
 {
-    internal sealed class Factory(ICondition condition, ILoggerFactory loggerFactory) : SimpleTaskFactory
+    internal sealed class Factory : SimpleTaskFactory
     {
         public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
         {
             if (step.InteractionType != EInteractionType.Dive)
                 return null;
 
-            return Dive();
+            return new Task();
         }
+    }
 
-        public ITask Dive()
-        {
-            return new DoDive(condition, loggerFactory.CreateLogger<DoDive>());
-        }
+    internal sealed class Task : ITask
+    {
+
+        public override string ToString() => "Dive";
     }
 
-    private sealed class DoDive(ICondition condition, ILogger<DoDive> logger)
-        : AbstractDelayedTask(TimeSpan.FromSeconds(5))
+    internal sealed class DoDive(ICondition condition, ILogger<DoDive> logger)
+        : AbstractDelayedTaskExecutor<Task>(TimeSpan.FromSeconds(5))
     {
         private readonly Queue<(uint Type, nint Key)> _keysToPress = [];
         private int _attempts;
@@ -114,8 +115,6 @@ internal static class Dive
             foreach (var key in realKeys)
                 _keysToPress.Enqueue((NativeMethods.WM_KEYUP, key));
         }
-
-        public override string ToString() => "Dive";
     }
 
     private static List<nint>? GetKeysToPress(SeVirtualKey key, ModifierFlag modifier)
index 975d107e5f532a8a2db80c2fba9a6a73fea33069..172eb961a30058520114e3eca7ff86e71ff55677 100644 (file)
@@ -1,7 +1,6 @@
 using System;
 using Dalamud.Game.ClientState.Conditions;
 using Dalamud.Plugin.Services;
-using Microsoft.Extensions.DependencyInjection;
 using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Questing;
@@ -10,7 +9,7 @@ namespace Questionable.Controller.Steps.Interactions;
 
 internal static class Duty
 {
-    internal sealed class Factory(GameFunctions gameFunctions, ICondition condition) : SimpleTaskFactory
+    internal sealed class Factory : SimpleTaskFactory
     {
         public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
         {
@@ -18,26 +17,28 @@ internal static class Duty
                 return null;
 
             ArgumentNullException.ThrowIfNull(step.ContentFinderConditionId);
-            return new OpenDutyFinder(step.ContentFinderConditionId.Value, gameFunctions, condition);
+            return new Task(step.ContentFinderConditionId.Value);
         }
     }
 
-    private sealed class OpenDutyFinder(
-        uint contentFinderConditionId,
+    internal sealed record Task(uint ContentFinderConditionId) : ITask
+    {
+        public override string ToString() => $"OpenDutyFinder({ContentFinderConditionId})";
+    }
+
+    internal sealed class Executor(
         GameFunctions gameFunctions,
-        ICondition condition) : ITask
+        ICondition condition) : TaskExecutor<Task>
     {
-        public bool Start()
+        protected override bool Start()
         {
             if (condition[ConditionFlag.InDutyQueue])
                 return false;
 
-            gameFunctions.OpenDutyFinder(contentFinderConditionId);
+            gameFunctions.OpenDutyFinder(Task.ContentFinderConditionId);
             return true;
         }
 
-        public ETaskResult Update() => ETaskResult.TaskComplete;
-
-        public override string ToString() => $"OpenDutyFinder({contentFinderConditionId})";
+        public override ETaskResult Update() => ETaskResult.TaskComplete;
     }
 }
index 8a5db80e453a628195c58f509b5cf7ecd5cf27f6..085b0356e714f5bd7c245bb97cbf3312168ae6ea 100644 (file)
@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using Microsoft.Extensions.DependencyInjection;
 using Questionable.Controller.Steps.Common;
 using Questionable.Functions;
 using Questionable.Model;
@@ -10,7 +9,7 @@ namespace Questionable.Controller.Steps.Interactions;
 
 internal static class Emote
 {
-    internal sealed class Factory(ChatFunctions chatFunctions, Mount.Factory mountFactory) : ITaskFactory
+    internal sealed class Factory : ITaskFactory
     {
         public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
         {
@@ -25,39 +24,46 @@ internal static class Emote
 
             ArgumentNullException.ThrowIfNull(step.Emote);
 
-            var unmount = mountFactory.Unmount();
+            var unmount = new Mount.UnmountTask();
             if (step.DataId != null)
             {
-                var task = new UseOnObject(step.Emote.Value, step.DataId.Value, chatFunctions);
+                var task = new UseOnObject(step.Emote.Value, step.DataId.Value);
                 return [unmount, task];
             }
             else
             {
-                var task = new UseOnSelf(step.Emote.Value, chatFunctions);
+                var task = new UseOnSelf(step.Emote.Value);
                 return [unmount, task];
             }
         }
     }
 
-    private sealed class UseOnObject(EEmote emote, uint dataId, ChatFunctions chatFunctions) : AbstractDelayedTask
+    internal sealed record UseOnObject(EEmote Emote, uint DataId) : ITask
+    {
+        public override string ToString() => $"Emote({Emote} on {DataId})";
+    }
+
+    internal sealed class UseOnObjectExecutor(ChatFunctions chatFunctions)
+        : AbstractDelayedTaskExecutor<UseOnObject>
     {
         protected override bool StartInternal()
         {
-            chatFunctions.UseEmote(dataId, emote);
+            chatFunctions.UseEmote(Task.DataId, Task.Emote);
             return true;
         }
+    }
 
-        public override string ToString() => $"Emote({emote} on {dataId})";
+    internal sealed record UseOnSelf(EEmote Emote) : ITask
+    {
+        public override string ToString() => $"Emote({Emote})";
     }
 
-    private sealed class UseOnSelf(EEmote emote, ChatFunctions chatFunctions) : AbstractDelayedTask
+    internal sealed class UseOnSelfExecutor(ChatFunctions chatFunctions) : AbstractDelayedTaskExecutor<UseOnSelf>
     {
         protected override bool StartInternal()
         {
-            chatFunctions.UseEmote(emote);
+            chatFunctions.UseEmote(Task.Emote);
             return true;
         }
-
-        public override string ToString() => $"Emote({emote})";
     }
 }
index 4adea07958a62c73f46ab1a6232998d922a874f9..d9cd3971cf584ab56424b3c4a3cf459c19556939 100644 (file)
@@ -16,7 +16,7 @@ namespace Questionable.Controller.Steps.Interactions;
 
 internal static class EquipItem
 {
-    internal sealed class Factory(IDataManager dataManager, ILoggerFactory loggerFactory) : SimpleTaskFactory
+    internal sealed class Factory : SimpleTaskFactory
     {
         public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
         {
@@ -24,36 +24,18 @@ internal static class EquipItem
                 return null;
 
             ArgumentNullException.ThrowIfNull(step.ItemId);
-            return Equip(step.ItemId.Value);
-        }
-
-        private DoEquip Equip(uint itemId)
-        {
-            var item = dataManager.GetExcelSheet<Item>()!.GetRow(itemId) ??
-                       throw new ArgumentOutOfRangeException(nameof(itemId));
-            var targetSlots = GetEquipSlot(item) ?? throw new InvalidOperationException("Not a piece of equipment");
-            return new DoEquip(itemId, item, targetSlots, dataManager, loggerFactory.CreateLogger<DoEquip>());
+            return new Task(step.ItemId.Value);
         }
+    }
 
-        private static List<ushort>? GetEquipSlot(Item item)
-        {
-            return item.EquipSlotCategory.Row switch
-            {
-                >= 1 and <= 11 => [(ushort)(item.EquipSlotCategory.Row - 1)],
-                12 => [11, 12], // rings
-                13 => [0],
-                17 => [13], // soul crystal
-                _ => null
-            };
-        }
+    internal sealed record Task(uint ItemId) : ITask
+    {
+        public override string ToString() => $"Equip({ItemId})";
     }
 
-    private sealed class DoEquip(
-        uint itemId,
-        Item item,
-        List<ushort> targetSlots,
+    internal sealed class Executor(
         IDataManager dataManager,
-        ILogger<DoEquip> logger) : ITask, IToastAware
+        ILogger<Executor> logger) : TaskExecutor<Task>, IToastAware
     {
         private const int MaxAttempts = 3;
 
@@ -81,16 +63,22 @@ internal static class EquipItem
         ];
 
         private int _attempts;
+        private Item _item = null!;
+        private List<ushort> _targetSlots = null!;
         private DateTime _continueAt = DateTime.MaxValue;
 
-        public bool Start()
+        protected override bool Start()
         {
+            _item = dataManager.GetExcelSheet<Item>()!.GetRow(Task.ItemId) ??
+                    throw new ArgumentOutOfRangeException(nameof(Task.ItemId));
+            _targetSlots = GetEquipSlot(_item) ?? throw new InvalidOperationException("Not a piece of equipment");
+
             Equip();
             _continueAt = DateTime.Now.AddSeconds(1);
             return true;
         }
 
-        public unsafe ETaskResult Update()
+        public override unsafe ETaskResult Update()
         {
             if (DateTime.Now < _continueAt)
                 return ETaskResult.StillRunning;
@@ -99,10 +87,10 @@ internal static class EquipItem
             if (inventoryManager == null)
                 return ETaskResult.StillRunning;
 
-            foreach (ushort x in targetSlots)
+            foreach (ushort x in _targetSlots)
             {
                 var itemSlot = inventoryManager->GetInventorySlot(InventoryType.EquippedItems, x);
-                if (itemSlot != null && itemSlot->ItemId == itemId)
+                if (itemSlot != null && itemSlot->ItemId == Task.ItemId)
                     return ETaskResult.TaskComplete;
             }
 
@@ -125,12 +113,12 @@ internal static class EquipItem
             if (equippedContainer == null)
                 return;
 
-            foreach (ushort slot in targetSlots)
+            foreach (ushort slot in _targetSlots)
             {
                 var itemSlot = equippedContainer->GetInventorySlot(slot);
-                if (itemSlot != null && itemSlot->ItemId == itemId)
+                if (itemSlot != null && itemSlot->ItemId == Task.ItemId)
                 {
-                    logger.LogInformation("Already equipped {Item}, skipping step", item.Name?.ToString());
+                    logger.LogInformation("Already equipped {Item}, skipping step", _item.Name?.ToString());
                     return;
                 }
             }
@@ -141,24 +129,24 @@ internal static class EquipItem
                 if (sourceContainer == null)
                     continue;
 
-                if (inventoryManager->GetItemCountInContainer(itemId, sourceInventoryType, true) == 0 &&
-                    inventoryManager->GetItemCountInContainer(itemId, sourceInventoryType) == 0)
+                if (inventoryManager->GetItemCountInContainer(Task.ItemId, sourceInventoryType, true) == 0 &&
+                    inventoryManager->GetItemCountInContainer(Task.ItemId, sourceInventoryType) == 0)
                     continue;
 
                 for (ushort sourceSlot = 0; sourceSlot < sourceContainer->Size; sourceSlot++)
                 {
                     var sourceItem = sourceContainer->GetInventorySlot(sourceSlot);
-                    if (sourceItem == null || sourceItem->ItemId != itemId)
+                    if (sourceItem == null || sourceItem->ItemId != Task.ItemId)
                         continue;
 
                     // Move the item to the first available slot
-                    ushort targetSlot = targetSlots
+                    ushort targetSlot = _targetSlots
                         .Where(x =>
                         {
                             var itemSlot = inventoryManager->GetInventorySlot(InventoryType.EquippedItems, x);
                             return itemSlot == null || itemSlot->ItemId == 0;
                         })
-                        .Concat(targetSlots).First();
+                        .Concat(_targetSlots).First();
 
                     logger.LogInformation(
                         "Equipping item from {SourceInventory}, {SourceSlot} to {TargetInventory}, {TargetSlot}",
@@ -172,7 +160,17 @@ internal static class EquipItem
             }
         }
 
-        public override string ToString() => $"Equip({item.Name})";
+        private static List<ushort>? GetEquipSlot(Item item)
+        {
+            return item.EquipSlotCategory.Row switch
+            {
+                >= 1 and <= 11 => [(ushort)(item.EquipSlotCategory.Row - 1)],
+                12 => [11, 12], // rings
+                13 => [0],
+                17 => [13], // soul crystal
+                _ => null
+            };
+        }
 
         public bool OnErrorToast(SeString message)
         {
index 5ff7ab355a4488417b66d4f6d16d41cee9584934..cd4f79828f0b9d25b0338d3cca3e7c93d51d06fb 100644 (file)
@@ -10,23 +10,18 @@ namespace Questionable.Controller.Steps.Interactions;
 
 internal static class EquipRecommended
 {
-    internal sealed class Factory(IClientState clientState, IChatGui chatGui) : SimpleTaskFactory
+    internal sealed class Factory : SimpleTaskFactory
     {
         public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
         {
             if (step.InteractionType != EInteractionType.EquipRecommended)
                 return null;
 
-            return DoEquip();
-        }
-
-        public ITask DoEquip()
-        {
-            return new DoEquipRecommended(clientState, chatGui);
+            return new EquipTask();
         }
     }
 
-    internal sealed class BeforeDutyOrInstance(IClientState clientState, IChatGui chatGui) : SimpleTaskFactory
+    internal sealed class BeforeDutyOrInstance : SimpleTaskFactory
     {
         public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
         {
@@ -35,21 +30,26 @@ internal static class EquipRecommended
                 step.InteractionType != EInteractionType.Combat)
                 return null;
 
-            return new DoEquipRecommended(clientState, chatGui);
+            return new EquipTask();
         }
     }
 
-    private sealed unsafe class DoEquipRecommended(IClientState clientState, IChatGui chatGui) : ITask
+    internal sealed class EquipTask : ITask
+    {
+        public override string ToString() => "EquipRecommended";
+    }
+
+    internal sealed unsafe class DoEquipRecommended(IClientState clientState, IChatGui chatGui) : TaskExecutor<EquipTask>
     {
         private bool _equipped;
 
-        public bool Start()
+        protected override bool Start()
         {
             RecommendEquipModule.Instance()->SetupForClassJob((byte)clientState.LocalPlayer!.ClassJob.Id);
             return true;
         }
 
-        public ETaskResult Update()
+        public override ETaskResult Update()
         {
             var recommendedEquipModule = RecommendEquipModule.Instance();
             if (recommendedEquipModule->IsUpdating)
@@ -94,7 +94,5 @@ internal static class EquipRecommended
 
             return ETaskResult.TaskComplete;
         }
-
-        public override string ToString() => "EquipRecommended";
     }
 }
index 020fc66e7e131f5fea8fb174ea649df0a3f7bf43..286cf828690cbdc03055f23e0e247698939f436d 100644 (file)
@@ -5,7 +5,6 @@ using Dalamud.Game.ClientState.Objects.Enums;
 using Dalamud.Game.ClientState.Objects.Types;
 using Dalamud.Plugin.Services;
 using FFXIVClientStructs.FFXIV.Client.Game;
-using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Steps.Shared;
 using Questionable.Functions;
@@ -16,12 +15,7 @@ namespace Questionable.Controller.Steps.Interactions;
 
 internal static class Interact
 {
-    internal sealed class Factory(
-        GameFunctions gameFunctions,
-        Configuration configuration,
-        ICondition condition,
-        ILoggerFactory loggerFactory)
-        : ITaskFactory
+    internal sealed class Factory(Configuration configuration) : ITaskFactory
     {
         public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
         {
@@ -55,58 +49,50 @@ internal static class Interact
             if (sequence.Sequence == 0 && sequence.Steps.IndexOf(step) == 0)
                 yield return new WaitAtEnd.WaitDelay();
 
-            yield return Interact(step.DataId.Value, quest, step.InteractionType,
+            yield return new Task(step.DataId.Value, quest, step.InteractionType,
                 step.TargetTerritoryId != null || quest.Id is SatisfactionSupplyNpcId ||
                 step.SkipConditions is { StepIf.Never: true }, step.PickUpItemId, step.SkipConditions?.StepIf);
         }
+    }
 
-        internal ITask Interact(uint dataId, Quest? quest, EInteractionType interactionType,
-            bool skipMarkerCheck = false, uint? pickUpItemId = null, SkipStepConditions? skipConditions = null)
-        {
-            return new DoInteract(dataId, quest, interactionType, skipMarkerCheck, pickUpItemId, skipConditions,
-                gameFunctions, condition, loggerFactory.CreateLogger<DoInteract>());
-        }
+    internal sealed record Task(
+        uint DataId,
+        Quest? Quest,
+        EInteractionType InteractionType,
+        bool SkipMarkerCheck = false,
+        uint? PickUpItemId = null,
+        SkipStepConditions? SkipConditions = null) : ITask
+    {
+        public override string ToString() => $"Interact({DataId})";
     }
 
     internal sealed class DoInteract(
-        uint dataId,
-        Quest? quest,
-        EInteractionType interactionType,
-        bool skipMarkerCheck,
-        uint? pickUpItemId,
-        SkipStepConditions? skipConditions,
         GameFunctions gameFunctions,
         ICondition condition,
         ILogger<DoInteract> logger)
-        : ITask
+        : TaskExecutor<Task>
     {
         private bool _needsUnmount;
-        private InteractionProgressContext? _progressContext;
         private DateTime _continueAt = DateTime.MinValue;
 
-        public Quest? Quest => quest;
+        public Quest? Quest => Task.Quest;
+        public EInteractionType InteractionType { get; set; }
 
-        public EInteractionType InteractionType
+        protected override bool Start()
         {
-            get => interactionType;
-            set => interactionType = value;
-        }
-
-        public InteractionProgressContext? ProgressContext() => _progressContext;
+            InteractionType = Task.InteractionType;
 
-        public bool Start()
-        {
-            IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId);
+            IGameObject? gameObject = gameFunctions.FindObjectByDataId(Task.DataId);
             if (gameObject == null)
             {
-                logger.LogWarning("No game object with dataId {DataId}", dataId);
+                logger.LogWarning("No game object with dataId {DataId}", Task.DataId);
                 return false;
             }
 
-            if (!gameObject.IsTargetable && skipConditions is { Never: false, NotTargetable: true })
+            if (!gameObject.IsTargetable && Task.SkipConditions is { Never: false, NotTargetable: true })
             {
                 logger.LogInformation("Not interacting with {DataId} because it is not targetable (but skippable)",
-                    dataId);
+                    Task.DataId);
                 return false;
             }
 
@@ -114,7 +100,7 @@ internal static class Interact
             if (!gameObject.IsTargetable && condition[ConditionFlag.Mounted] &&
                 gameObject.ObjectKind != ObjectKind.GatheringPoint)
             {
-                logger.LogInformation("Preparing interaction for {DataId} by unmounting", dataId);
+                logger.LogInformation("Preparing interaction for {DataId} by unmounting", Task.DataId);
                 _needsUnmount = true;
                 gameFunctions.Unmount();
                 _continueAt = DateTime.Now.AddSeconds(1);
@@ -123,7 +109,7 @@ internal static class Interact
 
             if (gameObject.IsTargetable && HasAnyMarker(gameObject))
             {
-                _progressContext =
+                ProgressContext =
                     InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(gameObject));
                 _continueAt = DateTime.Now.AddSeconds(0.5);
                 return true;
@@ -132,7 +118,7 @@ internal static class Interact
             return true;
         }
 
-        public ETaskResult Update()
+        public override ETaskResult Update()
         {
             if (DateTime.Now <= _continueAt)
                 return ETaskResult.StillRunning;
@@ -149,29 +135,29 @@ internal static class Interact
                     _needsUnmount = false;
             }
 
-            if (pickUpItemId != null)
+            if (Task.PickUpItemId != null)
             {
                 unsafe
                 {
                     InventoryManager* inventoryManager = InventoryManager.Instance();
-                    if (inventoryManager->GetInventoryItemCount(pickUpItemId.Value) > 0)
+                    if (inventoryManager->GetInventoryItemCount(Task.PickUpItemId.Value) > 0)
                         return ETaskResult.TaskComplete;
                 }
             }
             else
             {
-                if (_progressContext != null && _progressContext.WasSuccessful())
+                if (ProgressContext != null && ProgressContext.WasSuccessful())
                     return ETaskResult.TaskComplete;
 
-                if (interactionType == EInteractionType.Gather && condition[ConditionFlag.Gathering])
+                if (InteractionType == EInteractionType.Gather && condition[ConditionFlag.Gathering])
                     return ETaskResult.TaskComplete;
             }
 
-            IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId);
+            IGameObject? gameObject = gameFunctions.FindObjectByDataId(Task.DataId);
             if (gameObject == null || !gameObject.IsTargetable || !HasAnyMarker(gameObject))
                 return ETaskResult.StillRunning;
 
-            _progressContext =
+            ProgressContext =
                 InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(gameObject));
             _continueAt = DateTime.Now.AddSeconds(0.5);
             return ETaskResult.StillRunning;
@@ -179,13 +165,11 @@ internal static class Interact
 
         private unsafe bool HasAnyMarker(IGameObject gameObject)
         {
-            if (skipMarkerCheck || gameObject.ObjectKind != ObjectKind.EventNpc)
+            if (Task.SkipMarkerCheck || gameObject.ObjectKind != ObjectKind.EventNpc)
                 return true;
 
             var gameObjectStruct = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject.Address;
             return gameObjectStruct->NamePlateIconId != 0;
         }
-
-        public override string ToString() => $"Interact({dataId})";
     }
 }
index d9d93db59032454d6c5057f3ef4ec9506e006688..f7b9892d53ed67a39f6cfa2b5e88cfa47cb5cc4f 100644 (file)
@@ -12,12 +12,7 @@ namespace Questionable.Controller.Steps.Interactions;
 
 internal static class Jump
 {
-    internal sealed class Factory(
-        MovementController movementController,
-        IClientState clientState,
-        IFramework framework,
-        ICondition condition,
-        ILoggerFactory loggerFactory) : SimpleTaskFactory
+    internal sealed class Factory : SimpleTaskFactory
     {
         public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
         {
@@ -27,39 +22,42 @@ internal static class Jump
             ArgumentNullException.ThrowIfNull(step.JumpDestination);
 
             if (step.JumpDestination.Type == EJumpType.SingleJump)
-                return SingleJump(step.DataId, step.JumpDestination, step.Comment);
+                return new SingleJumpTask(step.DataId, step.JumpDestination, step.Comment);
             else
-                return RepeatedJumps(step.DataId, step.JumpDestination, step.Comment);
+                return new RepeatedJumpTask(step.DataId, step.JumpDestination, step.Comment);
         }
+    }
 
-        public ITask SingleJump(uint? dataId, JumpDestination jumpDestination, string? comment)
-        {
-            return new DoSingleJump(dataId, jumpDestination, comment, movementController, clientState, framework);
-        }
+    internal interface IJumpTask : ITask
+    {
+        uint? DataId { get; }
+        JumpDestination JumpDestination { get; }
+        string? Comment { get; }
+    }
 
-        public ITask RepeatedJumps(uint? dataId, JumpDestination jumpDestination, string? comment)
-        {
-            return new DoRepeatedJumps(dataId, jumpDestination, comment, movementController, clientState, framework,
-                condition, loggerFactory.CreateLogger<DoRepeatedJumps>());
-        }
+    internal sealed record SingleJumpTask(
+        uint? DataId,
+        JumpDestination JumpDestination,
+        string? Comment) : IJumpTask
+    {
+        public override string ToString() => $"Jump({Comment})";
     }
 
-    private class DoSingleJump(
-        uint? dataId,
-        JumpDestination jumpDestination,
-        string? comment,
+    internal abstract class JumpBase<T>(
         MovementController movementController,
         IClientState clientState,
-        IFramework framework) : ITask
+        IFramework framework) : TaskExecutor<T>
+        where T : class, IJumpTask
     {
-        public virtual bool Start()
+        protected override bool Start()
         {
-            float stopDistance = jumpDestination.CalculateStopDistance();
-            if ((clientState.LocalPlayer!.Position - jumpDestination.Position).Length() <= stopDistance)
+            float stopDistance = Task.JumpDestination.CalculateStopDistance();
+            if ((clientState.LocalPlayer!.Position - Task.JumpDestination.Position).Length() <= stopDistance)
                 return false;
 
-            movementController.NavigateTo(EMovementType.Quest, dataId, [jumpDestination.Position], false, false,
-                jumpDestination.StopDistance ?? stopDistance);
+            movementController.NavigateTo(EMovementType.Quest, Task.DataId, [Task.JumpDestination.Position], false,
+                false,
+                Task.JumpDestination.StopDistance ?? stopDistance);
             framework.RunOnTick(() =>
                 {
                     unsafe
@@ -67,11 +65,11 @@ internal static class Jump
                         ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2);
                     }
                 },
-                TimeSpan.FromSeconds(jumpDestination.DelaySeconds ?? 0.5f));
+                TimeSpan.FromSeconds(Task.JumpDestination.DelaySeconds ?? 0.5f));
             return true;
         }
 
-        public virtual ETaskResult Update()
+        public override ETaskResult Update()
         {
             if (movementController.IsPathfinding || movementController.IsPathRunning)
                 return ETaskResult.StillRunning;
@@ -82,30 +80,36 @@ internal static class Jump
 
             return ETaskResult.TaskComplete;
         }
+    }
 
-        public override string ToString() => $"Jump({comment})";
+    internal sealed class DoSingleJump(
+        MovementController movementController,
+        IClientState clientState,
+        IFramework framework) : JumpBase<SingleJumpTask>(movementController, clientState, framework);
+
+    internal sealed record RepeatedJumpTask(
+        uint? DataId,
+        JumpDestination JumpDestination,
+        string? Comment) : IJumpTask
+    {
+        public override string ToString() => $"RepeatedJump({Comment})";
     }
 
-    private sealed class DoRepeatedJumps(
-        uint? dataId,
-        JumpDestination jumpDestination,
-        string? comment,
+    internal sealed class DoRepeatedJumps(
         MovementController movementController,
         IClientState clientState,
         IFramework framework,
         ICondition condition,
         ILogger<DoRepeatedJumps> logger)
-        : DoSingleJump(dataId, jumpDestination, comment, movementController, clientState, framework)
+        : JumpBase<RepeatedJumpTask>(movementController, clientState, framework)
     {
-        private readonly JumpDestination _jumpDestination = jumpDestination;
-        private readonly string? _comment = comment;
         private readonly IClientState _clientState = clientState;
         private DateTime _continueAt = DateTime.MinValue;
         private int _attempts;
 
-        public override bool Start()
+        protected override bool Start()
         {
-            _continueAt = DateTime.Now + TimeSpan.FromSeconds(2 * (_jumpDestination.DelaySeconds ?? 0.5f));
+            _continueAt = DateTime.Now + TimeSpan.FromSeconds(2 * (Task.JumpDestination.DelaySeconds ?? 0.5f));
             return base.Start();
         }
 
@@ -114,13 +118,13 @@ internal static class Jump
             if (DateTime.Now < _continueAt || condition[ConditionFlag.Jumping])
                 return ETaskResult.StillRunning;
 
-            float stopDistance = _jumpDestination.CalculateStopDistance();
-            if ((_clientState.LocalPlayer!.Position - _jumpDestination.Position).Length() <= stopDistance ||
-                _clientState.LocalPlayer.Position.Y >= _jumpDestination.Position.Y - 0.5f)
+            float stopDistance = Task.JumpDestination.CalculateStopDistance();
+            if ((_clientState.LocalPlayer!.Position - Task.JumpDestination.Position).Length() <= stopDistance ||
+                _clientState.LocalPlayer.Position.Y >= Task.JumpDestination.Position.Y - 0.5f)
                 return ETaskResult.TaskComplete;
 
             logger.LogTrace("Y-Heights for jumps: player={A}, target={B}", _clientState.LocalPlayer.Position.Y,
-                _jumpDestination.Position.Y - 0.5f);
+                Task.JumpDestination.Position.Y - 0.5f);
             unsafe
             {
                 if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2))
@@ -130,10 +134,8 @@ internal static class Jump
             if (_attempts >= 50)
                 throw new TaskException("Tried to jump too many times, didn't reach the target");
 
-            _continueAt = DateTime.Now + TimeSpan.FromSeconds(_jumpDestination.DelaySeconds ?? 0.5f);
+            _continueAt = DateTime.Now + TimeSpan.FromSeconds(Task.JumpDestination.DelaySeconds ?? 0.5f);
             return ETaskResult.StillRunning;
         }
-
-        public override string ToString() => $"RepeatedJump({_comment})";
     }
 }
index 3d917b333ea103cd03d8c416608ee13224674179..f13ab4ab97992618997d1bf6fe9c34047e2c8cb6 100644 (file)
@@ -10,10 +10,7 @@ namespace Questionable.Controller.Steps.Interactions;
 
 internal static class Say
 {
-    internal sealed class Factory(
-        ChatFunctions chatFunctions,
-        Mount.Factory mountFactory,
-        ExcelFunctions excelFunctions) : ITaskFactory
+    internal sealed class Factory(ExcelFunctions excelFunctions) : ITaskFactory
     {
         public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
         {
@@ -33,20 +30,23 @@ internal static class Say
                     .GetString();
             ArgumentNullException.ThrowIfNull(excelString);
 
-            var unmount = mountFactory.Unmount();
-            var task = new UseChat(excelString, chatFunctions);
+            var unmount = new Mount.UnmountTask();
+            var task = new Task(excelString);
             return [unmount, task];
         }
     }
 
-    private sealed class UseChat(string chatMessage, ChatFunctions chatFunctions) : AbstractDelayedTask
+    internal sealed record Task(string ChatMessage) : ITask
+    {
+        public override string ToString() => $"Say({ChatMessage})";
+    }
+
+    internal sealed class UseChat(ChatFunctions chatFunctions) : AbstractDelayedTaskExecutor<Task>
     {
         protected override bool StartInternal()
         {
-            chatFunctions.ExecuteCommand($"/say {chatMessage}");
+            chatFunctions.ExecuteCommand($"/say {Task.ChatMessage}");
             return true;
         }
-
-        public override string ToString() => $"Say({chatMessage})";
     }
 }
index e4f0de00e0e9f89d46391f7c2f526b58893aabd1..ccdb998d06c4067960f831a1b5992f29409cfe14 100644 (file)
@@ -5,9 +5,7 @@ using System.Linq;
 using System.Numerics;
 using Dalamud.Game.ClientState.Conditions;
 using Dalamud.Plugin.Services;
-using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
 using FFXIVClientStructs.FFXIV.Client.Game;
-using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Steps.Common;
 using Questionable.Controller.Steps.Shared;
@@ -24,17 +22,8 @@ namespace Questionable.Controller.Steps.Interactions;
 internal static class UseItem
 {
     internal sealed class Factory(
-        Mount.Factory mountFactory,
-        MoveTo.Factory moveFactory,
-        Interact.Factory interactFactory,
-        AetheryteShortcut.Factory aetheryteShortcutFactory,
-        AethernetShortcut.Factory aethernetShortcutFactory,
-        GameFunctions gameFunctions,
-        QuestFunctions questFunctions,
-        ICondition condition,
         IClientState clientState,
         TerritoryData territoryData,
-        ILoggerFactory loggerFactory,
         ILogger<Factory> logger)
         : ITaskFactory
     {
@@ -59,7 +48,7 @@ internal static class UseItem
                         return CreateVesperBayFallbackTask();
                 }
 
-                var task = OnSelf(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags);
+                var task = new UseOnSelf(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags);
 
                 int currentStepIndex = sequence.Steps.IndexOf(step);
                 QuestStep? nextStep = sequence.Steps.Skip(currentStepIndex + 1).FirstOrDefault();
@@ -67,27 +56,27 @@ internal static class UseItem
                 return
                 [
                     task,
-                    new WaitConditionTask(() => clientState.TerritoryType == 140,
+                    new WaitCondition.Task(() => clientState.TerritoryType == 140,
                         $"Wait(territory: {territoryData.GetNameAndId(140)})"),
-                    mountFactory.Mount(140,
+                    new Mount.MountTask(140,
                         nextPosition != null ? Mount.EMountIf.AwayFromPosition : Mount.EMountIf.Always,
                         nextPosition),
-                    moveFactory.Move(new MoveTo.MoveParams(140, new(-408.92343f, 23.167036f, -351.16223f), null, 0.25f,
-                        DataId: null, DisableNavMesh: true, Sprint: false, Fly: false))
+                    new MoveTo.MoveTask(140, new(-408.92343f, 23.167036f, -351.16223f), null, 0.25f,
+                        DataId: null, DisableNavmesh: true, Sprint: false, Fly: false)
                 ];
             }
 
-            var unmount = mountFactory.Unmount();
+            var unmount = new Mount.UnmountTask();
             if (step.GroundTarget == true)
             {
                 ITask task;
                 if (step.DataId != null)
-                    task = OnGroundTarget(quest.Id, step.DataId.Value, step.ItemId.Value,
+                    task = new UseOnGround(quest.Id, step.DataId.Value, step.ItemId.Value,
                         step.CompletionQuestVariablesFlags);
                 else
                 {
                     ArgumentNullException.ThrowIfNull(step.Position);
-                    task = OnPosition(quest.Id, step.Position.Value, step.ItemId.Value,
+                    task = new UseOnPosition(quest.Id, step.Position.Value, step.ItemId.Value,
                         step.CompletionQuestVariablesFlags);
                 }
 
@@ -95,43 +84,17 @@ internal static class UseItem
             }
             else if (step.DataId != null)
             {
-                var task = OnObject(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags);
+                var task = new UseOnObject(quest.Id, step.DataId.Value, step.ItemId.Value,
+                    step.CompletionQuestVariablesFlags);
                 return [unmount, task];
             }
             else
             {
-                var task = OnSelf(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags);
+                var task = new UseOnSelf(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags);
                 return [unmount, task];
             }
         }
 
-        public ITask OnGroundTarget(ElementId questId, uint dataId, uint itemId,
-            List<QuestWorkValue?> completionQuestVariablesFlags)
-        {
-            return new UseOnGround(questId, dataId, itemId, completionQuestVariablesFlags, gameFunctions,
-                questFunctions, condition, loggerFactory.CreateLogger<UseOnGround>());
-        }
-
-        public ITask OnPosition(ElementId questId, Vector3 position, uint itemId,
-            List<QuestWorkValue?> completionQuestVariablesFlags)
-        {
-            return new UseOnPosition(questId, position, itemId, completionQuestVariablesFlags, gameFunctions,
-                questFunctions, condition, loggerFactory.CreateLogger<UseOnPosition>());
-        }
-
-        public ITask OnObject(ElementId questId, uint dataId, uint itemId,
-            List<QuestWorkValue?> completionQuestVariablesFlags, bool startingCombat = false)
-        {
-            return new UseOnObject(questId, dataId, itemId, completionQuestVariablesFlags, startingCombat,
-                questFunctions, gameFunctions, condition, loggerFactory.CreateLogger<UseOnObject>());
-        }
-
-        public ITask OnSelf(ElementId questId, uint itemId, List<QuestWorkValue?> completionQuestVariablesFlags)
-        {
-            return new Use(questId, itemId, completionQuestVariablesFlags, gameFunctions, questFunctions, condition,
-                loggerFactory.CreateLogger<Use>());
-        }
-
         private IEnumerable<ITask> CreateVesperBayFallbackTask()
         {
             logger.LogWarning("No vesper bay aetheryte tickets in inventory, navigating via ferry in Limsa instead");
@@ -139,39 +102,40 @@ internal static class UseItem
             uint npcId = 1003540;
             ushort territoryId = 129;
             Vector3 destination = new(-360.9217f, 8f, 38.92566f);
-            yield return aetheryteShortcutFactory.Use(null, null, EAetheryteLocation.Limsa, territoryId);
-            yield return aethernetShortcutFactory.Use(EAetheryteLocation.Limsa, EAetheryteLocation.LimsaArcanist);
+            yield return new AetheryteShortcut.Task(null, null, EAetheryteLocation.Limsa, territoryId);
+            yield return new AethernetShortcut.Task(EAetheryteLocation.Limsa, EAetheryteLocation.LimsaArcanist);
             yield return new WaitAtEnd.WaitDelay();
-            yield return
-                moveFactory.Move(new MoveTo.MoveParams(territoryId, destination, DataId: npcId, Sprint: false));
-            yield return interactFactory.Interact(npcId, null, EInteractionType.None, true);
+            yield return new MoveTo.MoveTask(territoryId, destination, DataId: npcId, Sprint: false);
+            yield return new Interact.Task(npcId, null, EInteractionType.None, true);
         }
     }
 
-    private abstract class UseItemBase(
-        ElementId? questId,
-        uint itemId,
-        IList<QuestWorkValue?> completionQuestVariablesFlags,
-        bool startingCombat,
+    internal interface IUseItemBase : ITask
+    {
+        ElementId? QuestId { get; }
+        uint ItemId { get; }
+        IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; }
+        bool StartingCombat { get; }
+    }
+
+    internal abstract class UseItemExecutorBase<T>(
         QuestFunctions questFunctions,
         ICondition condition,
-        ILogger logger) : ITask
+        ILogger logger) : TaskExecutor<T>
+        where T : class, IUseItemBase
     {
         private bool _usedItem;
         private DateTime _continueAt;
         private int _itemCount;
-        private InteractionProgressContext? _progressContext;
-
-        public InteractionProgressContext? ProgressContext() => _progressContext;
 
-        public ElementId? QuestId => questId;
-        public uint ItemId => itemId;
-        public IList<QuestWorkValue?> CompletionQuestVariablesFlags => completionQuestVariablesFlags;
-        public bool StartingCombat => startingCombat;
+        private ElementId? QuestId => Task.QuestId;
+        protected uint ItemId => Task.ItemId;
+        private IList<QuestWorkValue?> CompletionQuestVariablesFlags => Task.CompletionQuestVariablesFlags;
+        private bool StartingCombat => Task.StartingCombat;
 
         protected abstract bool UseItem();
 
-        public unsafe bool Start()
+        protected override unsafe bool Start()
         {
             InventoryManager* inventoryManager = InventoryManager.Instance();
             if (inventoryManager == null)
@@ -181,12 +145,12 @@ internal static class UseItem
             if (_itemCount == 0)
                 throw new TaskException($"Don't have any {ItemId} in inventory (checks NQ only)");
 
-            _progressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem());
+            ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem());
             _continueAt = DateTime.Now.Add(GetRetryDelay());
             return true;
         }
 
-        public unsafe ETaskResult Update()
+        public override unsafe ETaskResult Update()
         {
             if (QuestId is QuestId realQuestId && QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags))
             {
@@ -224,7 +188,7 @@ internal static class UseItem
 
             if (!_usedItem)
             {
-                _progressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem());
+                ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem());
                 _continueAt = DateTime.Now.Add(GetRetryDelay());
                 return ETaskResult.StillRunning;
             }
@@ -241,69 +205,85 @@ internal static class UseItem
         }
     }
 
+    internal sealed record UseOnGround(
+        ElementId? QuestId,
+        uint DataId,
+        uint ItemId,
+        IList<QuestWorkValue?> CompletionQuestVariablesFlags) : IUseItemBase
+    {
+        public bool StartingCombat => false;
+        public override string ToString() => $"UseItem({ItemId} on ground at {DataId})";
+    }
 
-    private sealed class UseOnGround(
-        ElementId? questId,
-        uint dataId,
-        uint itemId,
-        IList<QuestWorkValue?> completionQuestVariablesFlags,
+    internal sealed class UseOnGroundExecutor(
         GameFunctions gameFunctions,
         QuestFunctions questFunctions,
         ICondition condition,
-        ILogger<UseOnGround> logger)
-        : UseItemBase(questId, itemId, completionQuestVariablesFlags, false, questFunctions, condition, logger)
+        ILogger<UseOnGroundExecutor> logger)
+        : UseItemExecutorBase<UseOnGround>(questFunctions, condition, logger)
     {
-        protected override bool UseItem() => gameFunctions.UseItemOnGround(dataId, ItemId);
+        protected override bool UseItem() => gameFunctions.UseItemOnGround(Task.DataId, ItemId);
+    }
+
+    internal sealed record UseOnPosition(
+        ElementId? QuestId,
+        Vector3 Position,
+        uint ItemId,
+        IList<QuestWorkValue?> CompletionQuestVariablesFlags)
+        : IUseItemBase
+    {
+        public bool StartingCombat => false;
 
-        public override string ToString() => $"UseItem({ItemId} on ground at {dataId})";
+        public override string ToString() =>
+            $"UseItem({ItemId} on ground at {Position.ToString("G", CultureInfo.InvariantCulture)})";
     }
 
-    private sealed class UseOnPosition(
-        ElementId? questId,
-        Vector3 position,
-        uint itemId,
-        IList<QuestWorkValue?> completionQuestVariablesFlags,
+    internal sealed class UseOnPositionExecutor(
         GameFunctions gameFunctions,
         QuestFunctions questFunctions,
         ICondition condition,
         ILogger<UseOnPosition> logger)
-        : UseItemBase(questId, itemId, completionQuestVariablesFlags, false, questFunctions, condition, logger)
+        : UseItemExecutorBase<UseOnPosition>(questFunctions, condition, logger)
     {
-        protected override bool UseItem() => gameFunctions.UseItemOnPosition(position, ItemId);
+        protected override bool UseItem() => gameFunctions.UseItemOnPosition(Task.Position, ItemId);
+    }
 
-        public override string ToString() =>
-            $"UseItem({ItemId} on ground at {position.ToString("G", CultureInfo.InvariantCulture)})";
+    internal sealed record UseOnObject(
+        ElementId? QuestId,
+        uint DataId,
+        uint ItemId,
+        IList<QuestWorkValue?> CompletionQuestVariablesFlags,
+        bool StartingCombat = false) : IUseItemBase
+    {
+        public override string ToString() => $"UseItem({ItemId} on {DataId})";
     }
 
-    private sealed class UseOnObject(
-        ElementId? questId,
-        uint dataId,
-        uint itemId,
-        IList<QuestWorkValue?> completionQuestVariablesFlags,
-        bool startingCombat,
+    internal sealed class UseOnObjectExecutor(
         QuestFunctions questFunctions,
         GameFunctions gameFunctions,
         ICondition condition,
         ILogger<UseOnObject> logger)
-        : UseItemBase(questId, itemId, completionQuestVariablesFlags, startingCombat, questFunctions, condition, logger)
+        : UseItemExecutorBase<UseOnObject>(questFunctions, condition, logger)
     {
-        protected override bool UseItem() => gameFunctions.UseItem(dataId, ItemId);
+        protected override bool UseItem() => gameFunctions.UseItem(Task.DataId, ItemId);
+    }
 
-        public override string ToString() => $"UseItem({ItemId} on {dataId})";
+    internal sealed record UseOnSelf(
+        ElementId? QuestId,
+        uint ItemId,
+        IList<QuestWorkValue?> CompletionQuestVariablesFlags) : IUseItemBase
+    {
+        public bool StartingCombat => false;
+        public override string ToString() => $"UseItem({ItemId})";
     }
 
-    private sealed class Use(
-        ElementId? questId,
-        uint itemId,
-        IList<QuestWorkValue?> completionQuestVariablesFlags,
+    internal sealed class UseOnSelfExecutor(
         GameFunctions gameFunctions,
         QuestFunctions questFunctions,
         ICondition condition,
-        ILogger<Use> logger)
-        : UseItemBase(questId, itemId, completionQuestVariablesFlags, false, questFunctions, condition, logger)
+        ILogger<UseOnSelf> logger)
+        : UseItemExecutorBase<UseOnSelf>(questFunctions, condition, logger)
     {
         protected override bool UseItem() => gameFunctions.UseItem(ItemId);
-
-        public override string ToString() => $"UseItem({ItemId})";
     }
 }
index 7b12c95e130253a0a13ab89f447bae8934e60142..4014b2fb3152aa4285d1010f51e9d0c000ce925b 100644 (file)
@@ -7,9 +7,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.UI;
 using FFXIVClientStructs.FFXIV.Client.UI.Agent;
 using FFXIVClientStructs.FFXIV.Component.GUI;
 using LLib.GameUI;
-using Microsoft.Extensions.DependencyInjection;
 using Questionable.Controller.Steps.Common;
-using Questionable.Functions;
 using Questionable.Model;
 using Questionable.Model.Questing;
 using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
@@ -18,7 +16,7 @@ namespace Questionable.Controller.Steps.Leves;
 
 internal static class InitiateLeve
 {
-    internal sealed class Factory(IGameGui gameGui, ICondition condition) : ITaskFactory
+    internal sealed class Factory(ICondition condition) : ITaskFactory
     {
         public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
         {
@@ -27,75 +25,86 @@ internal static class InitiateLeve
 
             yield return new SkipInitiateIfActive(quest.Id);
             yield return new OpenJournal(quest.Id);
-            yield return new Initiate(quest.Id, gameGui);
-            yield return new SelectDifficulty(gameGui);
-            yield return new WaitConditionTask(() => condition[ConditionFlag.BoundByDuty], "Wait(BoundByDuty)");
+            yield return new Initiate(quest.Id);
+            yield return new SelectDifficulty();
+            yield return new WaitCondition.Task(() => condition[ConditionFlag.BoundByDuty], "Wait(BoundByDuty)");
         }
     }
 
-    internal sealed unsafe class SkipInitiateIfActive(ElementId elementId) : ITask
+    internal sealed record SkipInitiateIfActive(ElementId ElementId) : ITask
     {
-        public bool Start() => true;
+        public override string ToString() => $"CheckIfAlreadyActive({ElementId})";
+    }
+
+    internal sealed unsafe class SkipInitiateIfActiveExecutor : TaskExecutor<SkipInitiateIfActive>
+    {
+        protected override bool Start() => true;
 
-        public ETaskResult Update()
+        public override ETaskResult Update()
         {
             var director = UIState.Instance()->DirectorTodo.Director;
             if (director != null &&
                 director->EventHandlerInfo != null &&
                 director->EventHandlerInfo->EventId.ContentId == EventHandlerType.GatheringLeveDirector &&
-                director->ContentId == elementId.Value)
+                director->ContentId == Task.ElementId.Value)
                 return ETaskResult.SkipRemainingTasksForStep;
 
             return ETaskResult.TaskComplete;
         }
+    }
 
-        public override string ToString() => $"CheckIfAlreadyActive({elementId})";
+    internal sealed record OpenJournal(ElementId ElementId) : ITask
+    {
+        public uint QuestType => ElementId is LeveId ? 2u : 1u;
+        public override string ToString() => $"OpenJournal({ElementId})";
     }
 
-    internal sealed unsafe class OpenJournal(ElementId elementId) : ITask
+    internal sealed unsafe class OpenJournalExecutor : TaskExecutor<OpenJournal>
     {
-        private readonly uint _questType = elementId is LeveId ? 2u : 1u;
         private DateTime _openedAt = DateTime.MinValue;
 
-        public bool Start()
+        protected override bool Start()
         {
-            AgentQuestJournal.Instance()->OpenForQuest(elementId.Value, _questType);
+            AgentQuestJournal.Instance()->OpenForQuest(Task.ElementId.Value, Task.QuestType);
             _openedAt = DateTime.Now;
             return true;
         }
 
-        public ETaskResult Update()
+        public override ETaskResult Update()
         {
             AgentQuestJournal* agentQuestJournal = AgentQuestJournal.Instance();
             if (agentQuestJournal->IsAgentActive() &&
-                agentQuestJournal->SelectedQuestId == elementId.Value &&
-                agentQuestJournal->SelectedQuestType == _questType)
+                agentQuestJournal->SelectedQuestId == Task.ElementId.Value &&
+                agentQuestJournal->SelectedQuestType == Task.QuestType)
                 return ETaskResult.TaskComplete;
 
             if (DateTime.Now > _openedAt.AddSeconds(3))
             {
-                AgentQuestJournal.Instance()->OpenForQuest(elementId.Value, _questType);
+                AgentQuestJournal.Instance()->OpenForQuest(Task.ElementId.Value, Task.QuestType);
                 _openedAt = DateTime.Now;
             }
 
             return ETaskResult.StillRunning;
         }
+    }
 
-        public override string ToString() => $"OpenJournal({elementId})";
+    internal sealed record Initiate(ElementId ElementId) : ITask
+    {
+        public override string ToString() => $"InitiateLeve({ElementId})";
     }
 
-    internal sealed unsafe class Initiate(ElementId elementId, IGameGui gameGui) : ITask
+    internal sealed unsafe class InitiateExecutor(IGameGui gameGui) : TaskExecutor<Initiate>
     {
-        public bool Start() => true;
+        protected override bool Start() => true;
 
-        public ETaskResult Update()
+        public override ETaskResult Update()
         {
             if (gameGui.TryGetAddonByName("JournalDetail", out AtkUnitBase* addonJournalDetail))
             {
                 var pickQuest = stackalloc AtkValue[]
                 {
                     new() { Type = ValueType.Int, Int = 4 },
-                    new() { Type = ValueType.UInt, Int = elementId.Value }
+                    new() { Type = ValueType.UInt, Int = Task.ElementId.Value }
                 };
                 addonJournalDetail->FireCallback(2, pickQuest);
                 return ETaskResult.TaskComplete;
@@ -103,21 +112,22 @@ internal static class InitiateLeve
 
             return ETaskResult.StillRunning;
         }
+    }
 
-        public override string ToString() => $"InitiateLeve({elementId})";
+    internal sealed class SelectDifficulty : ITask
+    {
+        public override string ToString() => "SelectLeveDifficulty";
     }
 
-    internal sealed unsafe class SelectDifficulty(IGameGui gameGui) : ITask
+    internal sealed unsafe class SelectDifficultyExecutor(IGameGui gameGui) : TaskExecutor<SelectDifficulty>
     {
-        public bool Start() => true;
+        protected override bool Start() => true;
 
-        public ETaskResult Update()
+        public override ETaskResult Update()
         {
             if (gameGui.TryGetAddonByName("GuildLeveDifficulty", out AtkUnitBase* addon))
             {
                 // atkvalues: 1 → default difficulty, 2 → min, 3 → max
-
-
                 var pickDifficulty = stackalloc AtkValue[]
                 {
                     new() { Type = ValueType.Int, Int = 0 },
@@ -129,7 +139,5 @@ internal static class InitiateLeve
 
             return ETaskResult.StillRunning;
         }
-
-        public override string ToString() => "SelectLeveDifficulty";
     }
 }
index 25110ca8555a6b2e865b24ee2c7106813b37b309..bb39bd8154b826e5df7f3958b699d712b41d10f9 100644 (file)
@@ -5,7 +5,6 @@ using System.Numerics;
 using Dalamud.Game.ClientState.Conditions;
 using Dalamud.Game.ClientState.Objects.Enums;
 using Dalamud.Plugin.Services;
-using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Steps.Common;
 using Questionable.Data;
@@ -20,17 +19,7 @@ namespace Questionable.Controller.Steps.Shared;
 
 internal static class AethernetShortcut
 {
-    internal sealed class Factory(
-        MovementController movementController,
-        AetheryteFunctions aetheryteFunctions,
-        GameFunctions gameFunctions,
-        QuestFunctions questFunctions,
-        IClientState clientState,
-        AetheryteData aetheryteData,
-        TerritoryData territoryData,
-        LifestreamIpc lifestreamIpc,
-        ICondition condition,
-        ILoggerFactory loggerFactory)
+    internal sealed class Factory(MovementController movementController)
         : ITaskFactory
     {
         public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
@@ -38,24 +27,28 @@ internal static class AethernetShortcut
             if (step.AethernetShortcut == null)
                 yield break;
 
-            yield return new WaitConditionTask(() => movementController.IsNavmeshReady,
+            yield return new WaitCondition.Task(() => movementController.IsNavmeshReady,
                 "Wait(navmesh ready)");
-            yield return Use(step.AethernetShortcut.From, step.AethernetShortcut.To,
-                step.SkipConditions?.AethernetShortcutIf);
+            yield return new Task(step.AethernetShortcut.From, step.AethernetShortcut.To,
+                step.SkipConditions?.AethernetShortcutIf ?? new());
         }
+    }
 
-        public ITask Use(EAetheryteLocation from, EAetheryteLocation to, SkipAetheryteCondition? skipConditions = null)
+    internal sealed record Task(
+        EAetheryteLocation From,
+        EAetheryteLocation To,
+        SkipAetheryteCondition SkipConditions) : ISkippableTask
+    {
+        public Task(EAetheryteLocation from,
+            EAetheryteLocation to)
+            : this(from, to, new())
         {
-            return new UseAethernetShortcut(from, to, skipConditions ?? new(),
-                loggerFactory.CreateLogger<UseAethernetShortcut>(), aetheryteFunctions, gameFunctions, questFunctions,
-                clientState, aetheryteData, territoryData, lifestreamIpc, movementController, condition);
         }
+
+        public override string ToString() => $"UseAethernet({From} -> {To})";
     }
 
     internal sealed class UseAethernetShortcut(
-        EAetheryteLocation from,
-        EAetheryteLocation to,
-        SkipAetheryteCondition skipConditions,
         ILogger<UseAethernetShortcut> logger,
         AetheryteFunctions aetheryteFunctions,
         GameFunctions gameFunctions,
@@ -65,79 +58,80 @@ internal static class AethernetShortcut
         TerritoryData territoryData,
         LifestreamIpc lifestreamIpc,
         MovementController movementController,
-        ICondition condition) : ISkippableTask
+        ICondition condition) : TaskExecutor<Task>
     {
         private bool _moving;
         private bool _teleported;
         private bool _triedMounting;
         private DateTime _continueAt = DateTime.MinValue;
 
-        public EAetheryteLocation From => from;
-        public EAetheryteLocation To => to;
+        public EAetheryteLocation From => Task.From;
+        public EAetheryteLocation To => Task.To;
 
-        public bool Start()
+        protected override bool Start()
         {
-            if (!skipConditions.Never)
+            if (!Task.SkipConditions.Never)
             {
-                if (skipConditions.InSameTerritory && clientState.TerritoryType == aetheryteData.TerritoryIds[to])
+                if (Task.SkipConditions.InSameTerritory &&
+                    clientState.TerritoryType == aetheryteData.TerritoryIds[Task.To])
                 {
                     logger.LogInformation("Skipping aethernet shortcut because the target is in the same territory");
                     return false;
                 }
 
-                if (skipConditions.InTerritory.Contains(clientState.TerritoryType))
+                if (Task.SkipConditions.InTerritory.Contains(clientState.TerritoryType))
                 {
                     logger.LogInformation(
                         "Skipping aethernet shortcut because the target is in the specified territory");
                     return false;
                 }
 
-                if (skipConditions.QuestsCompleted.Count > 0 &&
-                    skipConditions.QuestsCompleted.All(questFunctions.IsQuestComplete))
+                if (Task.SkipConditions.QuestsCompleted.Count > 0 &&
+                    Task.SkipConditions.QuestsCompleted.All(questFunctions.IsQuestComplete))
                 {
                     logger.LogInformation("Skipping aethernet shortcut, all prequisite quests are complete");
                     return true;
                 }
 
-                if (skipConditions.QuestsAccepted.Count > 0 &&
-                    skipConditions.QuestsAccepted.All(questFunctions.IsQuestAccepted))
+                if (Task.SkipConditions.QuestsAccepted.Count > 0 &&
+                    Task.SkipConditions.QuestsAccepted.All(questFunctions.IsQuestAccepted))
                 {
                     logger.LogInformation("Skipping aethernet shortcut, all prequisite quests are accepted");
                     return true;
                 }
 
-                if (skipConditions.AetheryteLocked != null &&
-                    !aetheryteFunctions.IsAetheryteUnlocked(skipConditions.AetheryteLocked.Value))
+                if (Task.SkipConditions.AetheryteLocked != null &&
+                    !aetheryteFunctions.IsAetheryteUnlocked(Task.SkipConditions.AetheryteLocked.Value))
                 {
                     logger.LogInformation("Skipping aethernet shortcut because the target aetheryte is locked");
                     return false;
                 }
 
-                if (skipConditions.AetheryteUnlocked != null &&
-                    aetheryteFunctions.IsAetheryteUnlocked(skipConditions.AetheryteUnlocked.Value))
+                if (Task.SkipConditions.AetheryteUnlocked != null &&
+                    aetheryteFunctions.IsAetheryteUnlocked(Task.SkipConditions.AetheryteUnlocked.Value))
                 {
                     logger.LogInformation("Skipping aethernet shortcut because the target aetheryte is unlocked");
                     return false;
                 }
             }
 
-            if (aetheryteFunctions.IsAetheryteUnlocked(from) &&
-                aetheryteFunctions.IsAetheryteUnlocked(to))
+            if (aetheryteFunctions.IsAetheryteUnlocked(Task.From) &&
+                aetheryteFunctions.IsAetheryteUnlocked(Task.To))
             {
                 ushort territoryType = clientState.TerritoryType;
                 Vector3 playerPosition = clientState.LocalPlayer!.Position;
 
                 // closer to the source
-                if (aetheryteData.CalculateDistance(playerPosition, territoryType, from) <
-                    aetheryteData.CalculateDistance(playerPosition, territoryType, to))
+                if (aetheryteData.CalculateDistance(playerPosition, territoryType, Task.From) <
+                    aetheryteData.CalculateDistance(playerPosition, territoryType, Task.To))
                 {
-                    if (aetheryteData.CalculateDistance(playerPosition, territoryType, from) <
-                        (from.IsFirmamentAetheryte() ? 11f : 4f))
+                    if (aetheryteData.CalculateDistance(playerPosition, territoryType, Task.From) <
+                        (Task.From.IsFirmamentAetheryte() ? 11f : 4f))
                     {
                         DoTeleport();
                         return true;
                     }
-                    else if (from == EAetheryteLocation.SolutionNine)
+                    else if (Task.From == EAetheryteLocation.SolutionNine)
                     {
                         logger.LogInformation("Moving to S9 aetheryte");
                         List<Vector3> nearbyPoints =
@@ -150,14 +144,14 @@ internal static class AethernetShortcut
 
                         Vector3 closestPoint = nearbyPoints.MinBy(x => (playerPosition - x).Length());
                         _moving = true;
-                        movementController.NavigateTo(EMovementType.Quest, (uint)from, closestPoint, false, true,
+                        movementController.NavigateTo(EMovementType.Quest, (uint)Task.From, closestPoint, false, true,
                             0.25f);
                         return true;
                     }
                     else
                     {
                         if (territoryData.CanUseMount(territoryType) &&
-                            aetheryteData.CalculateDistance(playerPosition, territoryType, from) > 30 &&
+                            aetheryteData.CalculateDistance(playerPosition, territoryType, Task.From) > 30 &&
                             !gameFunctions.HasStatusPreventingMount())
                         {
                             _triedMounting = gameFunctions.Mount();
@@ -176,7 +170,7 @@ internal static class AethernetShortcut
             else
                 logger.LogWarning(
                     "Aethernet shortcut not unlocked (from: {FromAetheryte}, to: {ToAetheryte}), walking manually",
-                    from, to);
+                    Task.From, Task.To);
 
             return false;
         }
@@ -185,34 +179,34 @@ internal static class AethernetShortcut
         {
             logger.LogInformation("Moving to aethernet shortcut");
             _moving = true;
-            float distance = from switch
+            float distance = Task.From switch
             {
-                _ when from.IsFirmamentAetheryte() => 4.4f,
+                _ when Task.From.IsFirmamentAetheryte() => 4.4f,
                 EAetheryteLocation.UldahChamberOfRule => 5f,
-                _ when AetheryteConverter.IsLargeAetheryte(from) => 10.9f,
+                _ when AetheryteConverter.IsLargeAetheryte(Task.From) => 10.9f,
                 _ => 6.9f,
             };
-            movementController.NavigateTo(EMovementType.Quest, (uint)from, aetheryteData.Locations[from],
+            movementController.NavigateTo(EMovementType.Quest, (uint)Task.From, aetheryteData.Locations[Task.From],
                 false, true,
                 distance);
         }
 
         private void DoTeleport()
         {
-            if (from.IsFirmamentAetheryte())
+            if (Task.From.IsFirmamentAetheryte())
             {
                 logger.LogInformation("Using manual teleport interaction");
-                _teleported = gameFunctions.InteractWith((uint)from, ObjectKind.EventObj);
+                _teleported = gameFunctions.InteractWith((uint)Task.From, ObjectKind.EventObj);
             }
             else
             {
-                logger.LogInformation("Using lifestream to teleport to {Destination}", to);
-                lifestreamIpc.Teleport(to);
+                logger.LogInformation("Using lifestream to teleport to {Destination}", Task.To);
+                lifestreamIpc.Teleport(Task.To);
                 _teleported = true;
             }
         }
 
-        public ETaskResult Update()
+        public override ETaskResult Update()
         {
             if (DateTime.Now < _continueAt)
                 return ETaskResult.StillRunning;
@@ -247,29 +241,27 @@ internal static class AethernetShortcut
                 return ETaskResult.StillRunning;
             }
 
-            if (aetheryteData.IsAirshipLanding(to))
+            if (aetheryteData.IsAirshipLanding(Task.To))
             {
                 if (aetheryteData.CalculateAirshipLandingDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero,
-                        clientState.TerritoryType, to) > 5)
+                        clientState.TerritoryType, Task.To) > 5)
                     return ETaskResult.StillRunning;
             }
-            else if (aetheryteData.IsCityAetheryte(to))
+            else if (aetheryteData.IsCityAetheryte(Task.To))
             {
                 if (aetheryteData.CalculateDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero,
-                        clientState.TerritoryType, to) > 20)
+                        clientState.TerritoryType, Task.To) > 20)
                     return ETaskResult.StillRunning;
             }
             else
             {
                 // some overworld location (e.g. 'Tesselation (Lakeland)' would end up here
-                if (clientState.TerritoryType != aetheryteData.TerritoryIds[to])
+                if (clientState.TerritoryType != aetheryteData.TerritoryIds[Task.To])
                     return ETaskResult.StillRunning;
             }
 
 
             return ETaskResult.TaskComplete;
         }
-
-        public override string ToString() => $"UseAethernet({from} -> {to})";
     }
 }
index 601c0c9e0a7b712d8e6e96317d7fcd7fbff0ee44..ba1e0211b1b37ccc3b99b41fc7eec426eb073265 100644 (file)
@@ -3,10 +3,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Numerics;
 using Dalamud.Plugin.Services;
-using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
-using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
-using Questionable.Controller.Steps.Common;
 using Questionable.Controller.Utils;
 using Questionable.Data;
 using Questionable.Functions;
@@ -18,55 +15,42 @@ namespace Questionable.Controller.Steps.Shared;
 
 internal static class AetheryteShortcut
 {
-    internal sealed class Factory(
-        AetheryteData aetheryteData,
-        AetheryteFunctions aetheryteFunctions,
-        QuestFunctions questFunctions,
-        IClientState clientState,
-        IChatGui chatGui,
-        ILoggerFactory loggerFactory) : ITaskFactory
+    internal sealed class Factory(AetheryteData aetheryteData) : ITaskFactory
     {
         public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
         {
             if (step.AetheryteShortcut == null)
                 yield break;
 
-            yield return Use(step, quest.Id, step.AetheryteShortcut.Value,
+            yield return new Task(step, quest.Id, step.AetheryteShortcut.Value,
                 aetheryteData.TerritoryIds[step.AetheryteShortcut.Value]);
             yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(0.5));
         }
+    }
 
-        public ITask Use(QuestStep? step, ElementId? elementId, EAetheryteLocation targetAetheryte,
-            ushort expectedTerritoryId)
-        {
-            return new UseAetheryteShortcut(step, elementId, targetAetheryte, expectedTerritoryId,
-                loggerFactory.CreateLogger<UseAetheryteShortcut>(), aetheryteFunctions, questFunctions, clientState,
-                chatGui, aetheryteData);
-        }
+    /// <param name="ExpectedTerritoryId">If using an aethernet shortcut after, the aetheryte's territory-id and the step's territory-id can differ, we always use the aetheryte's territory-id.</param>
+    internal sealed record Task(
+        QuestStep? Step,
+        ElementId? ElementId,
+        EAetheryteLocation TargetAetheryte,
+        ushort ExpectedTerritoryId) : ISkippableTask
+    {
     }
 
-    /// <param name="expectedTerritoryId">If using an aethernet shortcut after, the aetheryte's territory-id and the step's territory-id can differ, we always use the aetheryte's territory-id.</param>
-    private sealed class UseAetheryteShortcut(
-        QuestStep? step,
-        ElementId? elementId,
-        EAetheryteLocation targetAetheryte,
-        ushort expectedTerritoryId,
+    internal sealed class UseAetheryteShortcut(
         ILogger<UseAetheryteShortcut> logger,
         AetheryteFunctions aetheryteFunctions,
         QuestFunctions questFunctions,
         IClientState clientState,
         IChatGui chatGui,
-        AetheryteData aetheryteData) : ISkippableTask
+        AetheryteData aetheryteData) : TaskExecutor<Task>
     {
         private bool _teleported;
         private DateTime _continueAt;
-        private InteractionProgressContext? _progressContext;
 
-        public InteractionProgressContext? ProgressContext() => _progressContext;
+        protected override bool Start() => !ShouldSkipTeleport();
 
-        public bool Start() => !ShouldSkipTeleport();
-
-        public ETaskResult Update()
+        public override ETaskResult Update()
         {
             if (DateTime.Now < _continueAt)
                 return ETaskResult.StillRunning;
@@ -77,7 +61,7 @@ internal static class AetheryteShortcut
                 return ETaskResult.StillRunning;
             }
 
-            if (clientState.TerritoryType == expectedTerritoryId)
+            if (clientState.TerritoryType == Task.ExpectedTerritoryId)
                 return ETaskResult.TaskComplete;
 
             return ETaskResult.StillRunning;
@@ -86,9 +70,9 @@ internal static class AetheryteShortcut
         private bool ShouldSkipTeleport()
         {
             ushort territoryType = clientState.TerritoryType;
-            if (step != null)
+            if (Task.Step != null)
             {
-                var skipConditions = step.SkipConditions?.AetheryteShortcutIf ?? new();
+                var skipConditions = Task.Step.SkipConditions?.AetheryteShortcutIf ?? new();
                 if (skipConditions is { Never: false })
                 {
                     if (skipConditions.InTerritory.Contains(territoryType))
@@ -125,12 +109,12 @@ internal static class AetheryteShortcut
                         return true;
                     }
 
-                    if (elementId != null)
+                    if (Task.ElementId != null)
                     {
-                        QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(elementId);
+                        QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(Task.ElementId);
                         if (skipConditions.RequiredQuestVariablesNotMet &&
                             questWork != null &&
-                            !QuestWorkUtils.MatchesRequiredQuestWorkConfig(step.RequiredQuestVariables, questWork,
+                            !QuestWorkUtils.MatchesRequiredQuestWorkConfig(Task.Step.RequiredQuestVariables, questWork,
                                 logger))
                         {
                             logger.LogInformation("Skipping aetheryte teleport, as required variables do not match");
@@ -151,7 +135,7 @@ internal static class AetheryteShortcut
                     }
                 }
 
-                if (expectedTerritoryId == territoryType)
+                if (Task.ExpectedTerritoryId == territoryType)
                 {
                     if (!skipConditions.Never)
                     {
@@ -162,17 +146,19 @@ internal static class AetheryteShortcut
                         }
 
                         Vector3 pos = clientState.LocalPlayer!.Position;
-                        if (step.Position != null &&
-                            (pos - step.Position.Value).Length() < step.CalculateActualStopDistance())
+                        if (Task.Step.Position != null &&
+                            (pos - Task.Step.Position.Value).Length() < Task.Step.CalculateActualStopDistance())
                         {
                             logger.LogInformation("Skipping aetheryte teleport, we're near the target");
                             return true;
                         }
 
-                        if (aetheryteData.CalculateDistance(pos, territoryType, targetAetheryte) < 20 ||
-                            (step.AethernetShortcut != null &&
-                             (aetheryteData.CalculateDistance(pos, territoryType, step.AethernetShortcut.From) < 20 ||
-                              aetheryteData.CalculateDistance(pos, territoryType, step.AethernetShortcut.To) < 20)))
+                        if (aetheryteData.CalculateDistance(pos, territoryType, Task.TargetAetheryte) < 20 ||
+                            (Task.Step.AethernetShortcut != null &&
+                             (aetheryteData.CalculateDistance(pos, territoryType, Task.Step.AethernetShortcut.From) <
+                              20 ||
+                              aetheryteData.CalculateDistance(pos, territoryType, Task.Step.AethernetShortcut.To) <
+                              20)))
                         {
                             logger.LogInformation("Skipping aetheryte teleport");
                             return true;
@@ -186,7 +172,7 @@ internal static class AetheryteShortcut
 
         private bool DoTeleport()
         {
-            if (!aetheryteFunctions.CanTeleport(targetAetheryte))
+            if (!aetheryteFunctions.CanTeleport(Task.TargetAetheryte))
             {
                 if (!aetheryteFunctions.IsTeleportUnlocked())
                     throw new TaskException("Teleport is not unlocked, attune to any aetheryte first.");
@@ -198,29 +184,27 @@ internal static class AetheryteShortcut
 
             _continueAt = DateTime.Now.AddSeconds(8);
 
-            if (!aetheryteFunctions.IsAetheryteUnlocked(targetAetheryte))
+            if (!aetheryteFunctions.IsAetheryteUnlocked(Task.TargetAetheryte))
             {
-                chatGui.PrintError($"[Questionable] Aetheryte {targetAetheryte} is not unlocked.");
+                chatGui.PrintError($"[Questionable] Aetheryte {Task.TargetAetheryte} is not unlocked.");
                 throw new TaskException("Aetheryte is not unlocked");
             }
+
+            ProgressContext =
+                InteractionProgressContext.FromActionUseOrDefault(() =>
+                    aetheryteFunctions.TeleportAetheryte(Task.TargetAetheryte));
+            if (ProgressContext != null)
+            {
+                logger.LogInformation("Travelling via aetheryte...");
+                return true;
+            }
             else
             {
-                _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");
-                }
+                chatGui.Print("[Questionable] Unable to teleport to aetheryte.");
+                throw new TaskException("Unable to teleport to aetheryte");
             }
         }
 
-        public override string ToString() => $"UseAetheryte({targetAetheryte})";
+        public override string ToString() => $"UseAetheryte({Task.TargetAetheryte})";
     }
 }
index 5d09f37c522ed49b01be3b908cb471862b2abf4e..ad474fe2bdf5aa31cc8611175882abb6c2e4659b 100644 (file)
@@ -17,12 +17,7 @@ namespace Questionable.Controller.Steps.Shared;
 
 internal static class Craft
 {
-    internal sealed class Factory(
-        IDataManager dataManager,
-        IClientState clientState,
-        ArtisanIpc artisanIpc,
-        Mount.Factory mountFactory,
-        ILoggerFactory loggerFactory) : ITaskFactory
+    internal sealed class Factory : ITaskFactory
     {
         public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
         {
@@ -33,34 +28,36 @@ internal static class Craft
             ArgumentNullException.ThrowIfNull(step.ItemCount);
             return
             [
-                mountFactory.Unmount(),
-                Craft(step.ItemId.Value, step.ItemCount.Value)
+                new Mount.UnmountTask(),
+                new CraftTask(step.ItemId.Value, step.ItemCount.Value)
             ];
         }
+    }
 
-        public ITask Craft(uint itemId, int itemCount) =>
-            new DoCraft(itemId, itemCount, dataManager, clientState, artisanIpc, loggerFactory.CreateLogger<DoCraft>());
+    internal sealed record CraftTask(
+        uint ItemId,
+        int ItemCount) : ITask
+    {
+        public override string ToString() => $"Craft {ItemCount}x {ItemId} (with Artisan)";
     }
 
-    private sealed class DoCraft(
-        uint itemId,
-        int itemCount,
+    internal sealed class DoCraft(
         IDataManager dataManager,
         IClientState clientState,
         ArtisanIpc artisanIpc,
-        ILogger<DoCraft> logger) : ITask
+        ILogger<DoCraft> logger) : TaskExecutor<CraftTask>
     {
-        public bool Start()
+        protected override bool Start()
         {
             if (HasRequestedItems())
             {
-                logger.LogInformation("Already own {ItemCount}x {ItemId}", itemCount, itemId);
+                logger.LogInformation("Already own {ItemCount}x {ItemId}", Task.ItemCount, Task.ItemId);
                 return false;
             }
 
-            RecipeLookup? recipeLookup = dataManager.GetExcelSheet<RecipeLookup>()!.GetRow(itemId);
+            RecipeLookup? recipeLookup = dataManager.GetExcelSheet<RecipeLookup>()!.GetRow(Task.ItemId);
             if (recipeLookup == null)
-                throw new TaskException($"Item {itemId} is not craftable");
+                throw new TaskException($"Item {Task.ItemId} is not craftable");
 
             uint recipeId = (EClassJob)clientState.LocalPlayer!.ClassJob.Id switch
             {
@@ -92,19 +89,19 @@ internal static class Craft
             }
 
             if (recipeId == 0)
-                throw new TaskException($"Unable to determine recipe for item {itemId}");
+                throw new TaskException($"Unable to determine recipe for item {Task.ItemId}");
 
-            int remainingItemCount = itemCount - GetOwnedItemCount();
+            int remainingItemCount = Task.ItemCount - GetOwnedItemCount();
             logger.LogInformation(
                 "Starting craft for item {ItemId} with recipe {RecipeId} for {RemainingItemCount} items",
-                itemId, recipeId, remainingItemCount);
+                Task.ItemId, recipeId, remainingItemCount);
             if (!artisanIpc.CraftItem((ushort)recipeId, remainingItemCount))
                 throw new TaskException($"Failed to start Artisan craft for recipe {recipeId}");
 
             return true;
         }
 
-        public unsafe ETaskResult Update()
+        public override unsafe ETaskResult Update()
         {
             if (HasRequestedItems() && !artisanIpc.IsCrafting())
             {
@@ -128,15 +125,13 @@ internal static class Craft
             return ETaskResult.StillRunning;
         }
 
-        private bool HasRequestedItems() => GetOwnedItemCount() >= itemCount;
+        private bool HasRequestedItems() => GetOwnedItemCount() >= Task.ItemCount;
 
         private unsafe int GetOwnedItemCount()
         {
             InventoryManager* inventoryManager = InventoryManager.Instance();
-            return inventoryManager->GetInventoryItemCount(itemId, isHq: false, checkEquipped: false)
-                   + inventoryManager->GetInventoryItemCount(itemId, isHq: true, checkEquipped: false);
+            return inventoryManager->GetInventoryItemCount(Task.ItemId, isHq: false, checkEquipped: false)
+                   + inventoryManager->GetInventoryItemCount(Task.ItemId, isHq: true, checkEquipped: false);
         }
-
-        public override string ToString() => $"Craft {itemCount}x {itemId} (with Artisan)";
     }
 }
index cc5d9cfacafac5bcc0c008723562f3bcc1008611..52229aa18c011971510f95c1de6f7112af36120f 100644 (file)
@@ -21,7 +21,6 @@ internal static class Gather
     internal sealed class Factory(
         IServiceProvider serviceProvider,
         MovementController movementController,
-        GatheringController gatheringController,
         GatheringPointRegistry gatheringPointRegistry,
         IClientState clientState,
         GatheringData gatheringData,
@@ -53,7 +52,7 @@ internal static class Gather
 
                 if (classJob != currentClassJob)
                 {
-                    yield return new SwitchClassJob(classJob, clientState);
+                    yield return new SwitchClassJob.Task(classJob);
                 }
 
                 if (HasRequiredItems(itemToGather))
@@ -71,20 +70,20 @@ internal static class Gather
                         foreach (var task in serviceProvider.GetRequiredService<TaskCreator>()
                                      .CreateTasks(quest, gatheringSequence, gatheringStep))
                             if (task is WaitAtEnd.NextStep)
-                                yield return CreateSkipMarkerTask();
+                                yield return new SkipMarker();
                             else
                                 yield return task;
                     }
                 }
 
                 ushort territoryId = gatheringRoot.Steps.Last().TerritoryId;
-                yield return new WaitConditionTask(() => clientState.TerritoryType == territoryId,
+                yield return new WaitCondition.Task(() => clientState.TerritoryType == territoryId,
                     $"Wait(territory: {territoryData.GetNameAndId(territoryId)})");
 
-                yield return new WaitConditionTask(() => movementController.IsNavmeshReady,
+                yield return new WaitCondition.Task(() => movementController.IsNavmeshReady,
                     "Wait(navmesh ready)");
 
-                yield return CreateStartGatheringTask(gatheringPointId, itemToGather);
+                yield return new GatheringTask(gatheringPointId, itemToGather);
                 yield return new WaitAtEnd.WaitDelay();
             }
         }
@@ -109,46 +108,38 @@ internal static class Gather
                        minCollectability: (short)itemToGather.Collectability) >=
                    itemToGather.ItemCount;
         }
+    }
 
-        private StartGathering CreateStartGatheringTask(GatheringPointId gatheringPointId, GatheredItem gatheredItem)
-        {
-            return new StartGathering(gatheringPointId, gatheredItem, gatheringController);
-        }
-
-        private static SkipMarker CreateSkipMarkerTask()
+    internal sealed record GatheringTask(
+        GatheringPointId gatheringPointId,
+        GatheredItem gatheredItem) : ITask
+    {
+        public override string ToString()
         {
-            return new SkipMarker();
+            if (gatheredItem.Collectability == 0)
+                return $"Gather({gatheredItem.ItemCount}x {gatheredItem.ItemId})";
+            else
+                return
+                    $"Gather({gatheredItem.ItemCount}x {gatheredItem.ItemId} {SeIconChar.Collectible.ToIconString()} {gatheredItem.Collectability})";
         }
     }
 
-    private sealed class StartGathering(
-        GatheringPointId gatheringPointId,
-        GatheredItem gatheredItem,
-        GatheringController gatheringController) : ITask
+    internal sealed class StartGathering(GatheringController gatheringController) : TaskExecutor<GatheringTask>
     {
-        public bool Start()
+        protected override bool Start()
         {
-            return gatheringController.Start(new GatheringController.GatheringRequest(gatheringPointId,
-                gatheredItem.ItemId, gatheredItem.AlternativeItemId, gatheredItem.ItemCount,
-                gatheredItem.Collectability));
+            return gatheringController.Start(new GatheringController.GatheringRequest(Task.gatheringPointId,
+                Task.gatheredItem.ItemId, Task.gatheredItem.AlternativeItemId, Task.gatheredItem.ItemCount,
+                Task.gatheredItem.Collectability));
         }
 
-        public ETaskResult Update()
+        public override ETaskResult Update()
         {
             if (gatheringController.Update() == GatheringController.EStatus.Complete)
                 return ETaskResult.TaskComplete;
 
             return ETaskResult.StillRunning;
         }
-
-        public override string ToString()
-        {
-            if (gatheredItem.Collectability == 0)
-                return $"Gather({gatheredItem.ItemCount}x {gatheredItem.ItemId})";
-            else
-                return
-                    $"Gather({gatheredItem.ItemCount}x {gatheredItem.ItemId} {SeIconChar.Collectible.ToIconString()} {gatheredItem.Collectability})";
-        }
     }
 
     /// <summary>
@@ -156,8 +147,12 @@ internal static class Gather
     /// </summary>
     internal sealed class SkipMarker : ITask
     {
-        public bool Start() => true;
-        public ETaskResult Update() => ETaskResult.TaskComplete;
         public override string ToString() => "Gather/SkipMarker";
     }
+
+    internal sealed class DoSkip : TaskExecutor<SkipMarker>
+    {
+        protected override bool Start() => true;
+        public override ETaskResult Update() => ETaskResult.TaskComplete;
+    }
 }
index e6d0cc945d16ec5bbbddf891d36d4a866223337c..2f7a8ea34cdc144a58e48196f5eab1e4c4dbee56 100644 (file)
@@ -11,7 +11,6 @@ using FFXIVClientStructs.FFXIV.Client.Game;
 using FFXIVClientStructs.FFXIV.Client.Game.Character;
 using LLib;
 using Lumina.Excel.GeneratedSheets;
-using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Steps.Common;
 using Questionable.Data;
@@ -28,14 +27,9 @@ internal static class MoveTo
 {
     internal sealed class Factory(
         MovementController movementController,
-        GameFunctions gameFunctions,
-        ICondition condition,
-        IDataManager dataManager,
         IClientState clientState,
         AetheryteData aetheryteData,
         TerritoryData territoryData,
-        ILoggerFactory loggerFactory,
-        Mount.Factory mountFactory,
         ILogger<Factory> logger) : ITaskFactory
     {
         public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
@@ -46,7 +40,7 @@ internal static class MoveTo
             }
             else if (step is { DataId: not null, StopDistance: not null })
             {
-                return [ExpectToBeNearDataId(step.DataId.Value, step.StopDistance.Value)];
+                return [new WaitForNearDataId(step.DataId.Value, step.StopDistance.Value)];
             }
             else if (step is { InteractionType: EInteractionType.AttuneAetheryte, Aetheryte: not null })
             {
@@ -60,27 +54,6 @@ internal static class MoveTo
             return [];
         }
 
-        public ITask Move(QuestStep step, Vector3 destination)
-        {
-            return Move(new MoveParams(step, destination));
-        }
-
-        public ITask Move(MoveParams moveParams)
-        {
-            return new MoveInternal(moveParams, movementController, mountFactory, gameFunctions,
-                loggerFactory.CreateLogger<MoveInternal>(), clientState, dataManager);
-        }
-
-        public ITask Land()
-        {
-            return new LandTask(clientState, condition, loggerFactory.CreateLogger<LandTask>());
-        }
-
-        public ITask ExpectToBeNearDataId(uint dataId, float stopDistance)
-        {
-            return new WaitForNearDataId(dataId, stopDistance, gameFunctions, clientState);
-        }
-
         public IEnumerable<ITask> CreateMountTasks(ElementId questId, QuestStep step, Vector3 destination)
         {
             if (step.InteractionType == EInteractionType.Jump && step.JumpDestination != null &&
@@ -91,146 +64,149 @@ internal static class MoveTo
                 yield break;
             }
 
-            yield return new WaitConditionTask(() => clientState.TerritoryType == step.TerritoryId,
+            yield return new WaitCondition.Task(() => clientState.TerritoryType == step.TerritoryId,
                 $"Wait(territory: {territoryData.GetNameAndId(step.TerritoryId)})");
 
             if (!step.DisableNavmesh)
             {
-                yield return new WaitConditionTask(() => movementController.IsNavmeshReady,
+                yield return new WaitCondition.Task(() => movementController.IsNavmeshReady,
                     "Wait(navmesh ready)");
 
-                yield return Move(step, destination);
+                yield return new MoveTask(step, destination);
             }
             else
             {
-                yield return Move(step, destination);
+                yield return new MoveTask(step, destination);
             }
 
             if (step is { Fly: true, Land: true })
-                yield return Land();
+                yield return new LandTask();
         }
     }
 
-    private sealed class MoveInternal : ITask, IToastAware
+    internal sealed class MoveExecutor : TaskExecutor<MoveTask>, IToastAware
     {
         private readonly string _cannotExecuteAtThisTime;
         private readonly MovementController _movementController;
-        private readonly Mount.Factory _mountFactory;
         private readonly GameFunctions _gameFunctions;
-        private readonly ILogger<MoveInternal> _logger;
+        private readonly ILogger<MoveExecutor> _logger;
         private readonly IClientState _clientState;
+        private readonly Mount.MountExecutor _mountExecutor;
+        private readonly Mount.UnmountExecutor _unmountExecutor;
 
-        private readonly Action _startAction;
-        private readonly Vector3 _destination;
-        private readonly MoveParams _moveParams;
+        private Action _startAction = null!;
+        private Vector3 _destination;
         private bool _canRestart;
-        private ITask? _mountTask;
+        private ITaskExecutor? _nestedExecutor;
 
-        public MoveInternal(MoveParams moveParams,
+        public MoveExecutor(
             MovementController movementController,
-            Mount.Factory mountFactory,
             GameFunctions gameFunctions,
-            ILogger<MoveInternal> logger,
+            ILogger<MoveExecutor> logger,
             IClientState clientState,
-            IDataManager dataManager)
+            IDataManager dataManager,
+            Mount.MountExecutor mountExecutor,
+            Mount.UnmountExecutor unmountExecutor)
         {
             _movementController = movementController;
-            _mountFactory = mountFactory;
             _gameFunctions = gameFunctions;
             _logger = logger;
             _clientState = clientState;
+            _mountExecutor = mountExecutor;
+            _unmountExecutor = unmountExecutor;
             _cannotExecuteAtThisTime = dataManager.GetString<LogMessage>(579, x => x.Text)!;
 
-            _destination = moveParams.Destination;
+        }
+
+        private void Initialize()
+        {
+            _destination = Task.Destination;
 
-            if (!gameFunctions.IsFlyingUnlocked(moveParams.TerritoryId))
+            if (!_gameFunctions.IsFlyingUnlocked(Task.TerritoryId))
             {
-                moveParams = moveParams with { Fly = false, Land = false };
+                Task = Task with { Fly = false, Land = false };
             }
 
-            if (!moveParams.DisableNavMesh)
+            if (!Task.DisableNavmesh)
             {
                 _startAction = () =>
-                    _movementController.NavigateTo(EMovementType.Quest, moveParams.DataId, _destination,
-                        fly: moveParams.Fly,
-                        sprint: moveParams.Sprint,
-                        stopDistance: moveParams.StopDistance,
-                        ignoreDistanceToObject: moveParams.IgnoreDistanceToObject,
-                        land: moveParams.Land);
+                    _movementController.NavigateTo(EMovementType.Quest, Task.DataId, _destination,
+                        fly: Task.Fly,
+                        sprint: Task.Sprint,
+                        stopDistance: Task.StopDistance,
+                        ignoreDistanceToObject: Task.IgnoreDistanceToObject,
+                        land: Task.Land);
             }
             else
             {
                 _startAction = () =>
-                    _movementController.NavigateTo(EMovementType.Quest, moveParams.DataId, [_destination],
-                        fly: moveParams.Fly,
-                        sprint: moveParams.Sprint,
-                        stopDistance: moveParams.StopDistance,
-                        ignoreDistanceToObject: moveParams.IgnoreDistanceToObject,
-                        land: moveParams.Land);
+                    _movementController.NavigateTo(EMovementType.Quest, Task.DataId, [_destination],
+                        fly: Task.Fly,
+                        sprint: Task.Sprint,
+                        stopDistance: Task.StopDistance,
+                        ignoreDistanceToObject: Task.IgnoreDistanceToObject,
+                        land: Task.Land);
             }
 
-            _moveParams = moveParams;
-            _canRestart = moveParams.RestartNavigation;
+            _canRestart = Task.RestartNavigation;
         }
 
-        public InteractionProgressContext? ProgressContext() => _mountTask?.ProgressContext();
-
-        public bool ShouldRedoOnInterrupt() => true;
-
-        public bool Start()
+        protected override bool Start()
         {
-            float stopDistance = _moveParams.StopDistance ?? QuestStep.DefaultStopDistance;
+            Initialize();
+
+            float stopDistance = Task.StopDistance ?? QuestStep.DefaultStopDistance;
             Vector3? position = _clientState.LocalPlayer?.Position;
             float actualDistance = position == null ? float.MaxValue : Vector3.Distance(position.Value, _destination);
 
-            if (_moveParams.Mount == true)
+            if (Task.Mount == true)
             {
-                var mountTask = _mountFactory.Mount(_moveParams.TerritoryId, Mount.EMountIf.Always);
-                if (mountTask.Start())
+                var mountTask = new Mount.MountTask(Task.TerritoryId, Mount.EMountIf.Always);
+                if (_mountExecutor.Start(mountTask))
                 {
-                    _mountTask = mountTask;
+                    _nestedExecutor = _mountExecutor;
                     return true;
                 }
             }
-            else if (_moveParams.Mount == false)
+            else if (Task.Mount == false)
             {
-                var mountTask = _mountFactory.Unmount();
-                if (mountTask.Start())
+                var mountTask = new Mount.UnmountTask();
+                if (_unmountExecutor.Start(mountTask))
                 {
-                    _mountTask = mountTask;
+                    _nestedExecutor = _unmountExecutor;
                     return true;
                 }
             }
 
-            if (!_moveParams.DisableNavMesh)
+            if (!Task.DisableNavmesh)
             {
-                if (_moveParams.Mount == null)
+                if (Task.Mount == null)
                 {
                     Mount.EMountIf mountIf =
-                        actualDistance > stopDistance && _moveParams.Fly &&
-                        _gameFunctions.IsFlyingUnlocked(_moveParams.TerritoryId)
+                        actualDistance > stopDistance && Task.Fly &&
+                        _gameFunctions.IsFlyingUnlocked(Task.TerritoryId)
                             ? Mount.EMountIf.Always
                             : Mount.EMountIf.AwayFromPosition;
-                    var mountTask = _mountFactory.Mount(_moveParams.TerritoryId, mountIf, _destination);
-                    if (mountTask.Start())
+                    var mountTask = new Mount.MountTask(Task.TerritoryId, mountIf, _destination);
+                    if (_mountExecutor.Start(mountTask))
                     {
-                        _mountTask = mountTask;
+                        _nestedExecutor = _mountExecutor;
                         return true;
                     }
                 }
             }
 
-            _mountTask = new NoOpTask();
+            _nestedExecutor = new NoOpTaskExecutor();
             return true;
         }
 
-        public ETaskResult Update()
+        public override ETaskResult Update()
         {
-            if (_mountTask != null)
+            if (_nestedExecutor != null)
             {
-                if (_mountTask.Update() == ETaskResult.TaskComplete)
+                if (_nestedExecutor.Update() == ETaskResult.TaskComplete)
                 {
-                    _mountTask = null;
+                    _nestedExecutor = null;
 
                     _logger.LogInformation("Moving to {Destination}", _destination.ToString("G", CultureInfo.InvariantCulture));
                     _startAction();
@@ -247,10 +223,10 @@ internal static class MoveTo
 
             if (_canRestart &&
                 Vector3.Distance(_clientState.LocalPlayer!.Position, _destination) >
-                (_moveParams.StopDistance ?? QuestStep.DefaultStopDistance) + 5f)
+                (Task.StopDistance ?? QuestStep.DefaultStopDistance) + 5f)
             {
                 _canRestart = false;
-                if (_clientState.TerritoryType == _moveParams.TerritoryId)
+                if (_clientState.TerritoryType == Task.TerritoryId)
                 {
                     _logger.LogInformation("Looks like movement was interrupted, re-attempting to move");
                     _startAction();
@@ -264,7 +240,6 @@ internal static class MoveTo
             return ETaskResult.TaskComplete;
         }
 
-        public override string ToString() => $"MoveTo({_destination.ToString("G", CultureInfo.InvariantCulture)})";
 
         public bool OnErrorToast(SeString message)
         {
@@ -275,27 +250,27 @@ internal static class MoveTo
         }
     }
 
-    private sealed class NoOpTask : ITask
+    private sealed class NoOpTaskExecutor : TaskExecutor<ITask>
     {
-        public bool Start() => true;
+        protected override bool Start() => true;
 
-        public ETaskResult Update() => ETaskResult.TaskComplete;
+        public override ETaskResult Update() => ETaskResult.TaskComplete;
     }
 
-    internal sealed record MoveParams(
+    internal sealed record MoveTask(
         ushort TerritoryId,
         Vector3 Destination,
         bool? Mount = null,
         float? StopDistance = null,
         uint? DataId = null,
-        bool DisableNavMesh = false,
+        bool DisableNavmesh = false,
         bool Sprint = true,
         bool Fly = false,
         bool Land = false,
         bool IgnoreDistanceToObject = false,
-        bool RestartNavigation = true)
+        bool RestartNavigation = true) : ITask
     {
-        public MoveParams(QuestStep step, Vector3 destination)
+        public MoveTask(QuestStep step, Vector3 destination)
             : this(step.TerritoryId,
                 destination,
                 step.Mount,
@@ -309,23 +284,27 @@ internal static class MoveTo
                 step.RestartNavigationIfCancelled != false)
         {
         }
+
+        public override string ToString() => $"MoveTo({Destination.ToString("G", CultureInfo.InvariantCulture)})";
     }
 
-    private sealed class WaitForNearDataId(
-        uint dataId,
-        float stopDistance,
-        GameFunctions gameFunctions,
-        IClientState clientState) : ITask
+    internal sealed record WaitForNearDataId(uint DataId, float StopDistance) : ITask
     {
         public bool ShouldRedoOnInterrupt() => true;
+    }
+
+    internal sealed class WaitForNearDataIdExecutor(
+        GameFunctions gameFunctions,
+        IClientState clientState) : TaskExecutor<WaitForNearDataId>
+    {
 
-        public bool Start() => true;
+        protected override bool Start() => true;
 
-        public ETaskResult Update()
+        public override ETaskResult Update()
         {
-            IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId);
+            IGameObject? gameObject = gameFunctions.FindObjectByDataId(Task.DataId);
             if (gameObject == null ||
-                (gameObject.Position - clientState.LocalPlayer!.Position).Length() > stopDistance)
+                (gameObject.Position - clientState.LocalPlayer!.Position).Length() > Task.StopDistance)
             {
                 throw new TaskException("Object not found or too far away, no position so we can't move");
             }
@@ -334,14 +313,17 @@ internal static class MoveTo
         }
     }
 
-    private sealed class LandTask(IClientState clientState, ICondition condition, ILogger<LandTask> logger) : ITask
+    internal sealed class LandTask : ITask
+    {
+        public bool ShouldRedoOnInterrupt() => true;
+    }
+
+    internal sealed class LandExecutor(IClientState clientState, ICondition condition, ILogger<LandExecutor> logger) : TaskExecutor<LandTask>
     {
         private bool _landing;
         private DateTime _continueAt;
 
-        public bool ShouldRedoOnInterrupt() => true;
-
-        public bool Start()
+        protected override bool Start()
         {
             if (!condition[ConditionFlag.InFlight])
             {
@@ -354,7 +336,7 @@ internal static class MoveTo
             return true;
         }
 
-        public ETaskResult Update()
+        public override ETaskResult Update()
         {
             if (DateTime.Now < _continueAt)
                 return ETaskResult.StillRunning;
index 1c8a3fa3804c4fa302c9fc1341467b68aaf9005c..eaf1d167583180e09ec83053964c01e18959b836 100644 (file)
@@ -1,14 +1,9 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
+using System.Linq;
 using System.Numerics;
 using Dalamud.Game.ClientState.Objects.Types;
 using Dalamud.Plugin.Services;
-using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
 using FFXIVClientStructs.FFXIV.Client.Game;
 using FFXIVClientStructs.FFXIV.Client.Game.UI;
-using FFXIVClientStructs.FFXIV.Client.System.Framework;
-using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Utils;
 using Questionable.Functions;
@@ -20,12 +15,7 @@ namespace Questionable.Controller.Steps.Shared;
 
 internal static class SkipCondition
 {
-    internal sealed class Factory(
-        ILoggerFactory loggerFactory,
-        AetheryteFunctions aetheryteFunctions,
-        GameFunctions gameFunctions,
-        QuestFunctions questFunctions,
-        IClientState clientState) : SimpleTaskFactory
+    internal sealed class Factory : SimpleTaskFactory
     {
         public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
         {
@@ -40,28 +30,31 @@ internal static class SkipCondition
                 step.NextQuestId == null)
                 return null;
 
-            return Check(step, skipConditions, quest.Id);
+            return new SkipTask(step, skipConditions ?? new(), quest.Id);
         }
+    }
 
-        private CheckSkip Check(QuestStep step, SkipStepConditions? skipConditions, ElementId questId)
-        {
-            return new CheckSkip(step, skipConditions ?? new(), questId, loggerFactory.CreateLogger<CheckSkip>(),
-                aetheryteFunctions, gameFunctions, questFunctions, clientState);
-        }
+    internal sealed record SkipTask(
+        QuestStep Step,
+        SkipStepConditions SkipConditions,
+        ElementId ElementId) : ITask
+    {
+        public override string ToString() => "CheckSkip";
     }
 
-    private sealed class CheckSkip(
-        QuestStep step,
-        SkipStepConditions skipConditions,
-        ElementId elementId,
+    internal sealed class CheckSkip(
         ILogger<CheckSkip> logger,
         AetheryteFunctions aetheryteFunctions,
         GameFunctions gameFunctions,
         QuestFunctions questFunctions,
-        IClientState clientState) : ITask
+        IClientState clientState) : TaskExecutor<SkipTask>
     {
-        public unsafe bool Start()
+        protected override unsafe bool Start()
         {
+            var skipConditions = Task.SkipConditions;
+            var step = Task.Step;
+            var elementId = Task.ElementId;
+
             logger.LogInformation("Checking skip conditions; {ConfiguredConditions}", string.Join(",", skipConditions));
 
             if (skipConditions.Flying == ELockedSkipCondition.Unlocked &&
@@ -204,7 +197,8 @@ internal static class SkipCondition
                 }
             }
 
-            if (skipConditions.NearPosition is { } nearPosition && clientState.TerritoryType == nearPosition.TerritoryId)
+            if (skipConditions.NearPosition is { } nearPosition &&
+                clientState.TerritoryType == nearPosition.TerritoryId)
             {
                 if (Vector3.Distance(nearPosition.Position, clientState.LocalPlayer!.Position) <=
                     nearPosition.MaximumDistance)
@@ -251,8 +245,6 @@ internal static class SkipCondition
             return false;
         }
 
-        public ETaskResult Update() => ETaskResult.SkipRemainingTasksForStep;
-
-        public override string ToString() => "CheckSkip";
+        public override ETaskResult Update() => ETaskResult.SkipRemainingTasksForStep;
     }
 }
index 86487621834062bb939e59e90672241c5c777258..917951f8fac30979b32234daacc4ebdfa538ac98 100644 (file)
@@ -6,27 +6,30 @@ namespace Questionable.Controller.Steps.Shared;
 
 internal static class StepDisabled
 {
-    internal sealed class Factory(ILoggerFactory loggerFactory) : SimpleTaskFactory
+    internal sealed class Factory : SimpleTaskFactory
     {
         public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
         {
             if (!step.Disabled)
                 return null;
 
-            return new Task(loggerFactory.CreateLogger<Task>());
+            return new SkipRemainingTasks();
         }
     }
 
-    internal sealed class Task(ILogger<Task> logger) : ITask
+    internal sealed class SkipRemainingTasks : ITask
     {
-        public bool Start() => true;
+        public override string ToString() => "StepDisabled";
+    }
+
+    internal sealed class Executor(ILogger<SkipRemainingTasks> logger) : TaskExecutor<SkipRemainingTasks>
+    {
+        protected override bool Start() => true;
 
-        public ETaskResult Update()
+        public override ETaskResult Update()
         {
             logger.LogInformation("Skipping step, as it is disabled");
             return ETaskResult.SkipRemainingTasksForStep;
         }
-
-        public override string ToString() => "StepDisabled";
     }
 }
index bacd4b7d39b7e0e4961c5316f30ae5603c7f7e67..880aac693d5d3febac0a12edc1abba761c1e5452 100644 (file)
@@ -6,31 +6,37 @@ using Questionable.Controller.Steps.Common;
 
 namespace Questionable.Controller.Steps.Shared;
 
-internal sealed class SwitchClassJob(EClassJob classJob, IClientState clientState) : AbstractDelayedTask
+internal static class SwitchClassJob
 {
-    protected override unsafe bool StartInternal()
+    internal sealed record Task(EClassJob ClassJob) : ITask
     {
-        if (clientState.LocalPlayer!.ClassJob.Id == (uint)classJob)
-            return false;
+        public override string ToString() => $"SwitchJob({ClassJob})";
+    }
 
-        var gearsetModule = RaptureGearsetModule.Instance();
-        if (gearsetModule != null)
+    internal sealed class Executor(IClientState clientState) : AbstractDelayedTaskExecutor<Task>
+    {
+        protected override unsafe bool StartInternal()
         {
-            for (int i = 0; i < 100; ++i)
+            if (clientState.LocalPlayer!.ClassJob.Id == (uint)Task.ClassJob)
+                return false;
+
+            var gearsetModule = RaptureGearsetModule.Instance();
+            if (gearsetModule != null)
             {
-                var gearset = gearsetModule->GetGearset(i);
-                if (gearset->ClassJob == (byte)classJob)
+                for (int i = 0; i < 100; ++i)
                 {
-                    gearsetModule->EquipGearset(gearset->Id);
-                    return true;
+                    var gearset = gearsetModule->GetGearset(i);
+                    if (gearset->ClassJob == (byte)Task.ClassJob)
+                    {
+                        gearsetModule->EquipGearset(gearset->Id);
+                        return true;
+                    }
                 }
             }
+
+            throw new TaskException($"No gearset found for {Task.ClassJob}");
         }
 
-        throw new TaskException($"No gearset found for {classJob}");
+        protected override ETaskResult UpdateInternal() => ETaskResult.TaskComplete;
     }
-
-    protected override ETaskResult UpdateInternal() => ETaskResult.TaskComplete;
-
-    public override string ToString() => $"SwitchJob({classJob})";
 }
index e2a0864f510f496ebe204fa178555dbd4ac15ac2..083fff420bcc9da78a679c49b048ca18e075564c 100644 (file)
@@ -19,9 +19,7 @@ internal static class WaitAtEnd
     internal sealed class Factory(
         IClientState clientState,
         ICondition condition,
-        TerritoryData territoryData,
-        QuestFunctions questFunctions,
-        GameFunctions gameFunctions)
+        TerritoryData territoryData)
         : ITaskFactory
     {
         public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
@@ -29,7 +27,7 @@ internal static class WaitAtEnd
             if (step.CompletionQuestVariablesFlags.Count == 6 &&
                 QuestWorkUtils.HasCompletionFlags(step.CompletionQuestVariablesFlags))
             {
-                var task = new WaitForCompletionFlags((QuestId)quest.Id, step, questFunctions);
+                var task = new WaitForCompletionFlags((QuestId)quest.Id, step);
                 var delay = new WaitDelay();
                 return [task, delay, Next(quest, sequence)];
             }
@@ -38,7 +36,7 @@ internal static class WaitAtEnd
             {
                 case EInteractionType.Combat:
                     var notInCombat =
-                        new WaitConditionTask(() => !condition[ConditionFlag.InCombat], "Wait(not in combat)");
+                        new WaitCondition.Task(() => !condition[ConditionFlag.InCombat], "Wait(not in combat)");
                     return
                     [
                         new WaitDelay(),
@@ -67,8 +65,7 @@ internal static class WaitAtEnd
 
                     return
                     [
-                        new WaitObjectAtPosition(step.DataId.Value, step.Position.Value, step.NpcWaitDistance ?? 0.5f,
-                            gameFunctions),
+                        new WaitObjectAtPosition(step.DataId.Value, step.Position.Value, step.NpcWaitDistance ?? 0.5f),
                         new WaitDelay(),
                         Next(quest, sequence)
                     ];
@@ -79,14 +76,14 @@ internal static class WaitAtEnd
                     if (step.TerritoryId != step.TargetTerritoryId)
                     {
                         // interaction moves to a different territory
-                        waitInteraction = new WaitConditionTask(
+                        waitInteraction = new WaitCondition.Task(
                             () => clientState.TerritoryType == step.TargetTerritoryId,
                             $"Wait(tp to territory: {territoryData.GetNameAndId(step.TargetTerritoryId.Value)})");
                     }
                     else
                     {
                         Vector3 lastPosition = step.Position ?? clientState.LocalPlayer?.Position ?? Vector3.Zero;
-                        waitInteraction = new WaitConditionTask(() =>
+                        waitInteraction = new WaitCondition.Task(() =>
                             {
                                 Vector3? currentPosition = clientState.LocalPlayer?.Position;
                                 if (currentPosition == null)
@@ -109,7 +106,7 @@ internal static class WaitAtEnd
 
                 case EInteractionType.AcceptQuest:
                 {
-                    var accept = new WaitQuestAccepted(step.PickUpQuestId ?? quest.Id, questFunctions);
+                    var accept = new WaitQuestAccepted(step.PickUpQuestId ?? quest.Id);
                     var delay = new WaitDelay();
                     if (step.PickUpQuestId != null)
                         return [accept, delay, Next(quest, sequence)];
@@ -119,7 +116,7 @@ internal static class WaitAtEnd
 
                 case EInteractionType.CompleteQuest:
                 {
-                    var complete = new WaitQuestCompleted(step.TurnInQuestId ?? quest.Id, questFunctions);
+                    var complete = new WaitQuestCompleted(step.TurnInQuestId ?? quest.Id);
                     var delay = new WaitDelay();
                     if (step.TurnInQuestId != null)
                         return [complete, delay, Next(quest, sequence)];
@@ -139,92 +136,119 @@ internal static class WaitAtEnd
         }
     }
 
-    internal sealed class WaitDelay(TimeSpan? delay = null) : AbstractDelayedTask(delay ?? TimeSpan.FromSeconds(1))
+    internal sealed record WaitDelay(TimeSpan Delay) : ITask
     {
-        protected override bool StartInternal() => true;
+        public WaitDelay()
+            : this(TimeSpan.FromSeconds(1))
+        {
+        }
 
         public override string ToString() => $"Wait(seconds: {Delay.TotalSeconds})";
     }
 
+    internal sealed class WaitDelayExecutor : AbstractDelayedTaskExecutor<WaitDelay>
+    {
+        protected override bool StartInternal()
+        {
+            Delay = Task.Delay;
+            return true;
+        }
+    }
+
     internal sealed class WaitNextStepOrSequence : ITask
     {
-        public bool Start() => true;
+        public override string ToString() => "Wait(next step or sequence)";
+    }
+
+    internal sealed class WaitNextStepOrSequenceExecutor : TaskExecutor<WaitNextStepOrSequence>
+    {
+        protected override bool Start() => true;
 
-        public ETaskResult Update() => ETaskResult.StillRunning;
+        public override ETaskResult Update() => ETaskResult.StillRunning;
+    }
 
-        public override string ToString() => "Wait(next step or sequence)";
+    internal sealed record WaitForCompletionFlags(QuestId Quest, QuestStep Step) : ITask
+    {
+        public override string ToString() =>
+            $"Wait(QW: {string.Join(", ", Step.CompletionQuestVariablesFlags.Select(x => x?.ToString() ?? "-"))})";
     }
 
-    internal sealed class WaitForCompletionFlags(QuestId quest, QuestStep step, QuestFunctions questFunctions) : ITask
+    internal sealed class WaitForCompletionFlagsExecutor(QuestFunctions questFunctions)
+        : TaskExecutor<WaitForCompletionFlags>
     {
-        public bool Start() => true;
+        protected override bool Start() => true;
 
-        public ETaskResult Update()
+        public override ETaskResult Update()
         {
-            QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(quest);
+            QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(Task.Quest);
             return questWork != null &&
-                   QuestWorkUtils.MatchesQuestWork(step.CompletionQuestVariablesFlags, questWork)
+                   QuestWorkUtils.MatchesQuestWork(Task.Step.CompletionQuestVariablesFlags, questWork)
                 ? ETaskResult.TaskComplete
                 : ETaskResult.StillRunning;
         }
+    }
 
+    internal sealed record WaitObjectAtPosition(
+        uint DataId,
+        Vector3 Destination,
+        float Distance) : ITask
+    {
         public override string ToString() =>
-            $"Wait(QW: {string.Join(", ", step.CompletionQuestVariablesFlags.Select(x => x?.ToString() ?? "-"))})";
+            $"WaitObj({DataId} at {Destination.ToString("G", CultureInfo.InvariantCulture)} < {Distance})";
     }
 
-    private sealed class WaitObjectAtPosition(
-        uint dataId,
-        Vector3 destination,
-        float distance,
-        GameFunctions gameFunctions) : ITask
+    internal sealed class WaitObjectAtPositionExecutor(GameFunctions gameFunctions) : TaskExecutor<WaitObjectAtPosition>
     {
-        public bool Start() => true;
+        protected override bool Start() => true;
 
-        public ETaskResult Update() =>
-            gameFunctions.IsObjectAtPosition(dataId, destination, distance)
+        public override ETaskResult Update() =>
+            gameFunctions.IsObjectAtPosition(Task.DataId, Task.Destination, Task.Distance)
                 ? ETaskResult.TaskComplete
                 : ETaskResult.StillRunning;
+    }
 
-        public override string ToString() =>
-            $"WaitObj({dataId} at {destination.ToString("G", CultureInfo.InvariantCulture)} < {distance})";
+    internal sealed record WaitQuestAccepted(ElementId ElementId) : ITask
+    {
+        public override string ToString() => $"WaitQuestAccepted({ElementId})";
     }
 
-    internal sealed class WaitQuestAccepted(ElementId elementId, QuestFunctions questFunctions) : ITask
+    internal sealed class WaitQuestAcceptedExecutor(QuestFunctions questFunctions) : TaskExecutor<WaitQuestAccepted>
     {
-        public bool Start() => true;
+        protected override bool Start() => true;
 
-        public ETaskResult Update()
+        public override ETaskResult Update()
         {
-            return questFunctions.IsQuestAccepted(elementId)
+            return questFunctions.IsQuestAccepted(Task.ElementId)
                 ? ETaskResult.TaskComplete
                 : ETaskResult.StillRunning;
         }
+    }
 
-        public override string ToString() => $"WaitQuestAccepted({elementId})";
+    internal sealed record WaitQuestCompleted(ElementId ElementId) : ITask
+    {
+        public override string ToString() => $"WaitQuestComplete({ElementId})";
     }
 
-    internal sealed class WaitQuestCompleted(ElementId elementId, QuestFunctions questFunctions) : ITask
+    internal sealed class WaitQuestCompletedExecutor(QuestFunctions questFunctions) : TaskExecutor<WaitQuestCompleted>
     {
-        public bool Start() => true;
+        protected override bool Start() => true;
 
-        public ETaskResult Update()
+        public override ETaskResult Update()
         {
-            return questFunctions.IsQuestComplete(elementId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
+            return questFunctions.IsQuestComplete(Task.ElementId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
         }
-
-        public override string ToString() => $"WaitQuestComplete({elementId})";
     }
 
-    internal sealed class NextStep(ElementId elementId, int sequence) : ILastTask
+    internal sealed record NextStep(ElementId ElementId, int Sequence) : ILastTask
     {
-        public ElementId ElementId { get; } = elementId;
-        public int Sequence { get; } = sequence;
-
-        public bool Start() => true;
+        public override string ToString() => "NextStep";
+    }
 
-        public ETaskResult Update() => ETaskResult.NextStep;
+    internal sealed class NextStepExecutor : TaskExecutor<NextStep>
+    {
+        protected override bool Start() => true;
 
-        public override string ToString() => "NextStep";
+        public override ETaskResult Update() => ETaskResult.NextStep;
     }
 
     internal sealed class EndAutomation : ILastTask
@@ -232,10 +256,13 @@ internal static class WaitAtEnd
         public ElementId ElementId => throw new InvalidOperationException();
         public int Sequence => throw new InvalidOperationException();
 
-        public bool Start() => true;
+        public override string ToString() => "EndAutomation";
+    }
+    internal sealed class EndAutomationExecutor : TaskExecutor<EndAutomation>
+    {
 
-        public ETaskResult Update() => ETaskResult.End;
+        protected override bool Start() => true;
 
-        public override string ToString() => "EndAutomation";
+        public override ETaskResult Update() => ETaskResult.End;
     }
 }
index cbe63e7f219ac545c75713ff142cbf9b246af947..c2c304b459541a9d0eaccb77112eb6e1debeeb7a 100644 (file)
@@ -18,10 +18,19 @@ internal static class WaitAtStart
         }
     }
 
-    internal sealed class WaitDelay(TimeSpan delay) : AbstractDelayedTask(delay)
-    {
-        protected override bool StartInternal() => true;
 
+    internal sealed record WaitDelay(TimeSpan Delay) : ITask
+    {
         public override string ToString() => $"Wait[S](seconds: {Delay.TotalSeconds})";
     }
+
+    internal sealed class WaitDelayExecutor : AbstractDelayedTaskExecutor<WaitDelay>
+    {
+        protected override bool StartInternal()
+        {
+            Delay = Task.Delay;
+            return true;
+        }
+    }
+
 }
diff --git a/Questionable/Controller/Steps/TaskExecutor.cs b/Questionable/Controller/Steps/TaskExecutor.cs
new file mode 100644 (file)
index 0000000..4f48b56
--- /dev/null
@@ -0,0 +1,51 @@
+using System;
+
+namespace Questionable.Controller.Steps;
+
+internal interface ITaskExecutor
+{
+    ITask CurrentTask { get; }
+
+    Type GetTaskType();
+
+    bool Start(ITask task);
+
+    bool WasInterrupted();
+
+    ETaskResult Update();
+}
+
+internal abstract class TaskExecutor<T> : ITaskExecutor
+    where T : class, ITask
+{
+    protected T Task { get; set; } = null!;
+    protected InteractionProgressContext? ProgressContext { get; set; }
+    ITask ITaskExecutor.CurrentTask => Task;
+
+    public bool WasInterrupted()
+    {
+        if (ProgressContext is {} progressContext)
+        {
+            progressContext.Update();
+            return progressContext.WasInterrupted();
+        }
+
+        return false;
+    }
+
+    public Type GetTaskType() => typeof(T);
+
+    protected abstract bool Start();
+
+    public bool Start(ITask task)
+    {
+        if (task is T t)
+        {
+            Task = t;
+            return Start();
+        }
+        throw new TaskException($"Unable to cast {task.GetType()} to {typeof(T)}");
+    }
+
+    public abstract ETaskResult Update();
+}
index ff12876ff6e2b1e90c8243f52ddab824cfebc813..7d30706ac41985062f2e18eab9d96954c18dc36a 100644 (file)
@@ -8,10 +8,10 @@ internal sealed class TaskQueue
 {
     private readonly List<ITask> _completedTasks = [];
     private readonly List<ITask> _tasks = [];
-    public ITask? CurrentTask { get; set; }
+    public ITaskExecutor? CurrentTaskExecutor { get; set; }
 
     public IEnumerable<ITask> RemainingTasks => _tasks;
-    public bool AllTasksComplete => CurrentTask == null && _tasks.Count == 0;
+    public bool AllTasksComplete => CurrentTaskExecutor == null && _tasks.Count == 0;
 
     public void Enqueue(ITask task)
     {
@@ -41,7 +41,7 @@ internal sealed class TaskQueue
     {
         _tasks.Clear();
         _completedTasks.Clear();
-        CurrentTask = null;
+        CurrentTaskExecutor = null;
     }
 
     public void InterruptWith(List<ITask> interruptionTasks)
@@ -49,8 +49,8 @@ internal sealed class TaskQueue
         List<ITask?> newTasks =
         [
             ..interruptionTasks,
-            .._completedTasks.Where(x => !ReferenceEquals(x, CurrentTask)).ToList(),
-            CurrentTask,
+            .._completedTasks.Where(x => !ReferenceEquals(x, CurrentTaskExecutor?.CurrentTask)).ToList(),
+            CurrentTaskExecutor?.CurrentTask,
             .._tasks
         ];
         Reset();
index b587835351912a3c3a649281166b4bdbaa05ad4d..98f357f16d2a69abeffb3eb966ff40b543243a65 100644 (file)
@@ -56,6 +56,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
     {
         ArgumentNullException.ThrowIfNull(pluginInterface);
         ArgumentNullException.ThrowIfNull(chatGui);
+
         try
         {
             ServiceCollection serviceCollection = new();
@@ -128,44 +129,81 @@ public sealed class QuestionablePlugin : IDalamudPlugin
     private static void AddTaskFactories(ServiceCollection serviceCollection)
     {
         // individual tasks
-        serviceCollection.AddTransient<MoveToLandingLocation>();
-        serviceCollection.AddTransient<DoGather>();
-        serviceCollection.AddTransient<DoGatherCollectable>();
-        serviceCollection.AddTransient<SwitchClassJob>();
-        serviceCollection.AddSingleton<Mount.Factory>();
+        serviceCollection.AddTaskExecutor<MoveToLandingLocation.Task, MoveToLandingLocation.Executor>();
+        serviceCollection.AddTaskExecutor<DoGather.Task, DoGather.Executor>();
+        serviceCollection.AddTaskExecutor<DoGatherCollectable.Task, DoGatherCollectable.Executor>();
+        serviceCollection.AddTaskExecutor<SwitchClassJob.Task, SwitchClassJob.Executor>();
+        serviceCollection.AddTaskExecutor<Mount.MountTask, Mount.MountExecutor>();
+        serviceCollection.AddTaskExecutor<Mount.UnmountTask, Mount.UnmountExecutor>();
 
         // task factories
-        serviceCollection.AddTaskFactory<StepDisabled.Factory>();
+        serviceCollection
+            .AddTaskFactoryAndExecutor<StepDisabled.SkipRemainingTasks, StepDisabled.Factory, StepDisabled.Executor>();
         serviceCollection.AddTaskFactory<EquipRecommended.BeforeDutyOrInstance>();
-        serviceCollection.AddTaskFactory<Gather.Factory>();
-        serviceCollection.AddTaskFactory<AetheryteShortcut.Factory>();
-        serviceCollection.AddTaskFactory<SkipCondition.Factory>();
-        serviceCollection.AddTaskFactory<AethernetShortcut.Factory>();
-        serviceCollection.AddTaskFactory<WaitAtStart.Factory>();
-        serviceCollection.AddTaskFactory<MoveTo.Factory>();
-
-        serviceCollection.AddTaskFactory<NextQuest.Factory>();
-        serviceCollection.AddTaskFactory<AetherCurrent.Factory>();
-        serviceCollection.AddTaskFactory<AethernetShard.Factory>();
-        serviceCollection.AddTaskFactory<Aetheryte.Factory>();
-        serviceCollection.AddTaskFactory<Combat.Factory>();
-        serviceCollection.AddTaskFactory<Duty.Factory>();
+        serviceCollection.AddTaskFactoryAndExecutor<Gather.GatheringTask, Gather.Factory, Gather.StartGathering>();
+        serviceCollection.AddTaskExecutor<Gather.SkipMarker, Gather.DoSkip>();
+        serviceCollection
+            .AddTaskFactoryAndExecutor<AetheryteShortcut.Task, AetheryteShortcut.Factory,
+                AetheryteShortcut.UseAetheryteShortcut>();
+        serviceCollection
+            .AddTaskFactoryAndExecutor<SkipCondition.SkipTask, SkipCondition.Factory, SkipCondition.CheckSkip>();
+        serviceCollection
+            .AddTaskFactoryAndExecutor<AethernetShortcut.Task, AethernetShortcut.Factory,
+                AethernetShortcut.UseAethernetShortcut>();
+        serviceCollection
+            .AddTaskFactoryAndExecutor<WaitAtStart.WaitDelay, WaitAtStart.Factory, WaitAtStart.WaitDelayExecutor>();
+        serviceCollection.AddTaskFactoryAndExecutor<MoveTo.MoveTask, MoveTo.Factory, MoveTo.MoveExecutor>();
+        serviceCollection.AddTaskExecutor<MoveTo.WaitForNearDataId, MoveTo.WaitForNearDataIdExecutor>();
+        serviceCollection.AddTaskExecutor<MoveTo.LandTask, MoveTo.LandExecutor>();
+
+        serviceCollection.AddTaskFactoryAndExecutor<NextQuest.SetQuestTask, NextQuest.Factory, NextQuest.Executor>();
+        serviceCollection
+            .AddTaskFactoryAndExecutor<AetherCurrent.Attune, AetherCurrent.Factory, AetherCurrent.DoAttune>();
+        serviceCollection
+            .AddTaskFactoryAndExecutor<AethernetShard.Attune, AethernetShard.Factory, AethernetShard.DoAttune>();
+        serviceCollection.AddTaskFactoryAndExecutor<Aetheryte.Attune, Aetheryte.Factory, Aetheryte.DoAttune>();
+        serviceCollection.AddTaskFactoryAndExecutor<Combat.Task, Combat.Factory, Combat.HandleCombat>();
+        serviceCollection.AddTaskFactoryAndExecutor<Duty.Task, Duty.Factory, Duty.Executor>();
         serviceCollection.AddTaskFactory<Emote.Factory>();
-        serviceCollection.AddTaskFactory<Action.Factory>();
-        serviceCollection.AddTaskFactory<Interact.Factory>();
+        serviceCollection.AddTaskExecutor<Emote.UseOnObject, Emote.UseOnObjectExecutor>();
+        serviceCollection.AddTaskExecutor<Emote.UseOnSelf, Emote.UseOnSelfExecutor>();
+        serviceCollection.AddTaskFactoryAndExecutor<Action.UseOnObject, Action.Factory, Action.UseOnObjectExecutor>();
+        serviceCollection.AddTaskFactoryAndExecutor<Interact.Task, Interact.Factory, Interact.DoInteract>();
         serviceCollection.AddTaskFactory<Jump.Factory>();
-        serviceCollection.AddTaskFactory<Dive.Factory>();
-        serviceCollection.AddTaskFactory<Say.Factory>();
+        serviceCollection.AddTaskExecutor<Jump.SingleJumpTask, Jump.DoSingleJump>();
+        serviceCollection.AddTaskExecutor<Jump.RepeatedJumpTask, Jump.DoRepeatedJumps>();
+        serviceCollection.AddTaskFactoryAndExecutor<Dive.Task, Dive.Factory, Dive.DoDive>();
+        serviceCollection.AddTaskFactoryAndExecutor<Say.Task, Say.Factory, Say.UseChat>();
         serviceCollection.AddTaskFactory<UseItem.Factory>();
-        serviceCollection.AddTaskFactory<EquipItem.Factory>();
-        serviceCollection.AddTaskFactory<EquipRecommended.Factory>();
-        serviceCollection.AddTaskFactory<Craft.Factory>();
-        serviceCollection.AddTaskFactory<TurnInDelivery.Factory>();
+        serviceCollection.AddTaskExecutor<UseItem.UseOnGround, UseItem.UseOnGroundExecutor>();
+        serviceCollection.AddTaskExecutor<UseItem.UseOnPosition, UseItem.UseOnPositionExecutor>();
+        serviceCollection.AddTaskExecutor<UseItem.UseOnObject, UseItem.UseOnObjectExecutor>();
+        serviceCollection.AddTaskExecutor<UseItem.UseOnSelf, UseItem.UseOnSelfExecutor>();
+        serviceCollection.AddTaskFactoryAndExecutor<EquipItem.Task, EquipItem.Factory, EquipItem.Executor>();
+        serviceCollection
+            .AddTaskFactoryAndExecutor<EquipRecommended.EquipTask, EquipRecommended.Factory,
+                EquipRecommended.DoEquipRecommended>();
+        serviceCollection.AddTaskFactoryAndExecutor<Craft.CraftTask, Craft.Factory, Craft.DoCraft>();
+        serviceCollection
+            .AddTaskFactoryAndExecutor<TurnInDelivery.Task, TurnInDelivery.Factory,
+                TurnInDelivery.SatisfactionSupplyTurnIn>();
+
         serviceCollection.AddTaskFactory<InitiateLeve.Factory>();
+        serviceCollection.AddTaskExecutor<InitiateLeve.SkipInitiateIfActive, InitiateLeve.SkipInitiateIfActiveExecutor>();
+        serviceCollection.AddTaskExecutor<InitiateLeve.OpenJournal, InitiateLeve.OpenJournalExecutor>();
+        serviceCollection.AddTaskExecutor<InitiateLeve.Initiate, InitiateLeve.InitiateExecutor>();
+        serviceCollection.AddTaskExecutor<InitiateLeve.SelectDifficulty, InitiateLeve.SelectDifficultyExecutor>();
 
+        serviceCollection.AddTaskExecutor<WaitCondition.Task, WaitCondition.Executor>();
         serviceCollection.AddTaskFactory<WaitAtEnd.Factory>();
-        serviceCollection.AddTransient<WaitAtEnd.WaitQuestAccepted>();
-        serviceCollection.AddTransient<WaitAtEnd.WaitQuestCompleted>();
+        serviceCollection.AddTaskExecutor<WaitAtEnd.WaitDelay, WaitAtEnd.WaitDelayExecutor>();
+        serviceCollection.AddTaskExecutor<WaitAtEnd.WaitNextStepOrSequence, WaitAtEnd.WaitNextStepOrSequenceExecutor>();
+        serviceCollection.AddTaskExecutor<WaitAtEnd.WaitForCompletionFlags, WaitAtEnd.WaitForCompletionFlagsExecutor>();
+        serviceCollection.AddTaskExecutor<WaitAtEnd.WaitObjectAtPosition, WaitAtEnd.WaitObjectAtPositionExecutor>();
+        serviceCollection.AddTaskExecutor<WaitAtEnd.WaitQuestAccepted, WaitAtEnd.WaitQuestAcceptedExecutor>();
+        serviceCollection.AddTaskExecutor<WaitAtEnd.WaitQuestCompleted, WaitAtEnd.WaitQuestCompletedExecutor>();
+        serviceCollection.AddTaskExecutor<WaitAtEnd.NextStep, WaitAtEnd.NextStepExecutor>();
+        serviceCollection.AddTaskExecutor<WaitAtEnd.EndAutomation, WaitAtEnd.EndAutomationExecutor>();
 
         serviceCollection.AddSingleton<TaskCreator>();
     }
index 0247cefe742cbefae5b7ef28b7a64e7a6ed5bed2..2b17a8315acd8a0fcc7c01a396a0aa4bbee7660c 100644 (file)
@@ -1,4 +1,5 @@
-using JetBrains.Annotations;
+using Dalamud.Plugin.Services;
+using JetBrains.Annotations;
 using Microsoft.Extensions.DependencyInjection;
 using Questionable.Controller.Steps;
 
@@ -7,11 +8,37 @@ namespace Questionable;
 internal static class ServiceCollectionExtensions
 {
     public static void AddTaskFactory<
-        [MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] TFactory>(
+        [MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
+        TFactory>(
         this IServiceCollection serviceCollection)
         where TFactory : class, ITaskFactory
     {
         serviceCollection.AddSingleton<ITaskFactory, TFactory>();
         serviceCollection.AddSingleton<TFactory>();
     }
+
+    public static void AddTaskExecutor<T,
+        [MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
+        TExecutor>(
+        this IServiceCollection serviceCollection)
+        where T : class, ITask
+        where TExecutor : TaskExecutor<T>
+    {
+        serviceCollection.AddKeyedTransient<ITaskExecutor, TExecutor>(typeof(T));
+        serviceCollection.AddTransient<TExecutor>();
+    }
+
+    public static void AddTaskFactoryAndExecutor<T,
+        [MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
+        TFactory,
+        [MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
+        TExecutor>(
+        this IServiceCollection serviceCollection)
+        where TFactory : class, ITaskFactory
+        where T : class, ITask
+        where TExecutor : TaskExecutor<T>
+    {
+        serviceCollection.AddTaskFactory<TFactory>();
+        serviceCollection.AddTaskExecutor<T, TExecutor>();
+    }
 }