From 7e9070950edc906567c3216f5fc7d701b284ac93 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Wed, 18 Sep 2024 22:40:12 +0200 Subject: [PATCH] Make task logic stateless to support rewind --- GatheringPathRenderer/RendererPlugin.cs | 2 +- .../GameUi/InteractionUiController.cs | 4 +- .../Controller/GatheringController.cs | 48 +-- .../Controller/GatheringPointRegistry.cs | 4 +- Questionable/Controller/MiniTaskController.cs | 56 +-- Questionable/Controller/QuestController.cs | 40 +- Questionable/Controller/QuestRegistry.cs | 6 +- ...Task.cs => AbstractDelayedTaskExecutor.cs} | 24 +- Questionable/Controller/Steps/Common/Mount.cs | 98 +++-- .../Controller/Steps/Common/NextQuest.cs | 31 +- .../Steps/Common/WaitConditionTask.cs | 34 +- .../Controller/Steps/Gathering/DoGather.cs | 348 +++++++++--------- .../Steps/Gathering/DoGatherCollectable.cs | 326 ++++++++-------- .../Steps/Gathering/MoveToLandingLocation.cs | 73 ++-- .../Steps/Gathering/TurnInDelivery.cs | 21 +- .../Controller/Steps/IConditionChangeAware.cs | 2 +- .../Controller/Steps/IRevisitAware.cs | 2 +- Questionable/Controller/Steps/ITask.cs | 23 +- Questionable/Controller/Steps/IToastAware.cs | 2 +- .../Controller/Steps/Interactions/Action.cs | 51 +-- .../Steps/Interactions/AetherCurrent.cs | 45 +-- .../Steps/Interactions/AethernetShard.cs | 40 +- .../Steps/Interactions/Aetheryte.cs | 41 +-- .../Controller/Steps/Interactions/Combat.cs | 71 ++-- .../Controller/Steps/Interactions/Dive.cs | 21 +- .../Controller/Steps/Interactions/Duty.cs | 25 +- .../Controller/Steps/Interactions/Emote.cs | 38 +- .../Steps/Interactions/EquipItem.cs | 78 ++-- .../Steps/Interactions/EquipRecommended.cs | 26 +- .../Controller/Steps/Interactions/Interact.cs | 78 ++-- .../Controller/Steps/Interactions/Jump.cs | 100 ++--- .../Controller/Steps/Interactions/Say.cs | 20 +- .../Controller/Steps/Interactions/UseItem.cs | 196 +++++----- .../Controller/Steps/Leves/InitiateLeve.cs | 78 ++-- .../Steps/Shared/AethernetShortcut.cs | 124 +++---- .../Steps/Shared/AetheryteShortcut.cs | 106 +++--- Questionable/Controller/Steps/Shared/Craft.cs | 51 ++- .../Controller/Steps/Shared/Gather.cs | 67 ++-- .../Controller/Steps/Shared/MoveTo.cs | 208 +++++------ .../Controller/Steps/Shared/SkipCondition.cs | 50 ++- .../Controller/Steps/Shared/StepDisabled.cs | 17 +- .../Controller/Steps/Shared/SwitchClassJob.cs | 50 +-- .../Controller/Steps/Shared/WaitAtEnd.cs | 143 ++++--- .../Controller/Steps/Shared/WaitAtStart.cs | 15 +- Questionable/Controller/Steps/TaskExecutor.cs | 51 +++ Questionable/Controller/Steps/TaskQueue.cs | 10 +- Questionable/QuestionablePlugin.cs | 96 +++-- Questionable/ServiceCollectionExtensions.cs | 31 +- 48 files changed, 1559 insertions(+), 1512 deletions(-) rename Questionable/Controller/Steps/Common/{AbstractDelayedTask.cs => AbstractDelayedTaskExecutor.cs} (61%) create mode 100644 Questionable/Controller/Steps/TaskExecutor.cs diff --git a/GatheringPathRenderer/RendererPlugin.cs b/GatheringPathRenderer/RendererPlugin.cs index b1ec75612..dbad17157 100644 --- a/GatheringPathRenderer/RendererPlugin.cs +++ b/GatheringPathRenderer/RendererPlugin.cs @@ -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 diff --git a/Questionable/Controller/GameUi/InteractionUiController.cs b/Questionable/Controller/GameUi/InteractionUiController.cs index ce083331e..99b2dd51a 100644 --- a/Questionable/Controller/GameUi/InteractionUiController.cs +++ b/Questionable/Controller/GameUi/InteractionUiController.cs @@ -285,7 +285,7 @@ internal sealed class InteractionUiController : IDisposable List dialogueChoices = []; // levequest choices have some vague sort of priority - if (_questController.HasCurrentTaskMatching(out var interact) && + if (_questController.HasCurrentTaskExecutorMatching(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 diff --git a/Questionable/Controller/GatheringController.cs b/Questionable/Controller/GatheringController.cs index 48147fc7c..52541c091 100644 --- a/Questionable/Controller/GatheringController.cs +++ b/Questionable/Controller/GatheringController.cs @@ -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 { 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 _logger; private readonly Regex _revisitRegex; @@ -50,10 +41,6 @@ internal sealed unsafe class GatheringController : MiniTaskController 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(5574, x => x.Text, pluginLog) @@ -170,7 +149,7 @@ internal sealed unsafe class GatheringController : MiniTaskController())); - _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())); + _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())); + _taskQueue.Enqueue(new DoGatherCollectable.Task(_currentRequest.Data, currentNode, revisitRequired)); } } } @@ -269,7 +245,7 @@ internal sealed unsafe class GatheringController : MiniTaskController 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 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 _logger; - protected MiniTaskController(IChatGui chatGui, Mount.Factory mountFactory, Combat.Factory combatFactory, - ICondition condition, ILogger logger) + protected MiniTaskController(IChatGui chatGui, ICondition condition, IServiceProvider serviceProvider, + ILogger 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(upcomingTask.GetType()); + if (taskExecutor.Start(upcomingTask)) { - _taskQueue.CurrentTask = upcomingTask; + _taskQueue.CurrentTaskExecutor = taskExecutor; return; } else @@ -68,19 +69,20 @@ internal abstract class MiniTaskController 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 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(nextTask.GetType()); + _taskQueue.CurrentTaskExecutor = taskExecutor; return; } } @@ -108,27 +112,27 @@ internal abstract class MiniTaskController 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 { List 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); } diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs index 8ebf576d9..8a1ed7737 100644 --- a/Questionable/Controller/QuestController.cs +++ b/Questionable/Controller/QuestController.cs @@ -82,10 +82,9 @@ internal sealed class QuestController : MiniTaskController, 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, 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, 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([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([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, 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, IDi public void SkipSimulatedTask() { - _taskQueue.CurrentTask = null; + _taskQueue.CurrentTaskExecutor = null; } public bool IsInterruptible() @@ -786,7 +800,7 @@ internal sealed class QuestController : MiniTaskController, 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, 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)) { diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs index f1af00501..502630519 100644 --- a/Questionable/Controller/QuestRegistry.cs +++ b/Questionable/Controller/QuestRegistry.cs @@ -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/AbstractDelayedTaskExecutor.cs similarity index 61% rename from Questionable/Controller/Steps/Common/AbstractDelayedTask.cs rename to Questionable/Controller/Steps/Common/AbstractDelayedTaskExecutor.cs index abd6514f4..f5e95ca4d 100644 --- a/Questionable/Controller/Steps/Common/AbstractDelayedTask.cs +++ b/Questionable/Controller/Steps/Common/AbstractDelayedTaskExecutor.cs @@ -2,33 +2,33 @@ namespace Questionable.Controller.Steps.Common; -internal abstract class AbstractDelayedTask : ITask +internal abstract class AbstractDelayedTaskExecutor : TaskExecutor + where T : class, ITask { private DateTime _continueAt; - protected AbstractDelayedTask(TimeSpan delay) + protected AbstractDelayedTaskExecutor() + : this(TimeSpan.FromSeconds(5)) + { + } + + protected AbstractDelayedTaskExecutor(TimeSpan delay) { Delay = delay; } protected TimeSpan Delay { get; set; } - protected AbstractDelayedTask() - : this(TimeSpan.FromSeconds(5)) - { - } - - public virtual InteractionProgressContext? ProgressContext() => null; - - public bool Start() + protected sealed override bool Start() { + bool started = StartInternal(); _continueAt = DateTime.Now.Add(Delay); - return StartInternal(); + return started; } protected abstract bool StartInternal(); - public virtual ETaskResult Update() + public override ETaskResult Update() { if (_continueAt >= DateTime.Now) return ETaskResult.StillRunning; diff --git a/Questionable/Controller/Steps/Common/Mount.cs b/Questionable/Controller/Steps/Common/Mount.cs index f067066ca..d9d38ae26 100644 --- a/Questionable/Controller/Steps/Common/Mount.cs +++ b/Questionable/Controller/Steps/Common/Mount.cs @@ -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); - - return new MountTask(territoryId, mountIf, position, gameFunctions, condition, territoryData, clientState, - loggerFactory.CreateLogger()); - } - - public ITask Unmount() - { - return new UnmountTask(condition, loggerFactory.CreateLogger(), gameFunctions, clientState); - } - } - - private sealed class MountTask( - ushort territoryId, - EMountIf mountIf, - Vector3? position, - GameFunctions gameFunctions, - ICondition condition, - TerritoryData territoryData, - IClientState clientState, - ILogger logger) : ITask - { - private bool _mountTriggered; - private InteractionProgressContext? _progressContext; - private DateTime _retryAt = DateTime.MinValue; - - public InteractionProgressContext? ProgressContext() => _progressContext; + public Vector3? Position { get; } = MountIf == EMountIf.AwayFromPosition + ? Position ?? throw new ArgumentNullException(nameof(Position)) + : null; public bool ShouldRedoOnInterrupt() => true; - public bool Start() + public override string ToString() => "Mount"; + } + + internal sealed class MountExecutor( + GameFunctions gameFunctions, + ICondition condition, + TerritoryData territoryData, + IClientState clientState, + ILogger logger) : TaskExecutor + { + private bool _mountTriggered; + private DateTime _retryAt = DateTime.MinValue; + + 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"; } - private sealed class UnmountTask( + internal sealed record UnmountTask : ITask + { + public bool ShouldRedoOnInterrupt() => true; + + public override string ToString() => "Unmount"; + } + + internal sealed class UnmountExecutor( ICondition condition, ILogger logger, GameFunctions gameFunctions, IClientState clientState) - : ITask + : TaskExecutor { 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 diff --git a/Questionable/Controller/Steps/Common/NextQuest.cs b/Questionable/Controller/Steps/Common/NextQuest.cs index 574c76ae7..32aa7ad90 100644 --- a/Questionable/Controller/Steps/Common/NextQuest.cs +++ b/Questionable/Controller/Steps/Common/NextQuest.cs @@ -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()); + return new SetQuestTask(step.NextQuestId, quest.Id); } } - private sealed class SetQuest(ElementId nextQuestId, ElementId currentQuestId, QuestRegistry questRegistry, QuestController questController, QuestFunctions questFunctions, ILogger 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 logger) : TaskExecutor + { + 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; } } diff --git a/Questionable/Controller/Steps/Common/WaitConditionTask.cs b/Questionable/Controller/Steps/Common/WaitConditionTask.cs index 762904ffe..9389bfb23 100644 --- a/Questionable/Controller/Steps/Common/WaitConditionTask.cs +++ b/Questionable/Controller/Steps/Common/WaitConditionTask.cs @@ -2,22 +2,28 @@ namespace Questionable.Controller.Steps.Common; -internal sealed class WaitConditionTask(Func predicate, string description) : ITask +internal static class WaitCondition { - private DateTime _continueAt = DateTime.MaxValue; - - public bool Start() => !predicate(); - - public ETaskResult Update() + internal sealed record Task(Func Predicate, string Description) : ITask { - if (_continueAt == DateTime.MaxValue) - { - if (predicate()) - _continueAt = DateTime.Now.AddSeconds(0.5); - } - - return DateTime.Now >= _continueAt ? ETaskResult.TaskComplete : ETaskResult.StillRunning; + public override string ToString() => Description; } - public override string ToString() => description; + internal sealed class Executor : TaskExecutor + { + private DateTime _continueAt = DateTime.MaxValue; + + protected override bool Start() => !Task.Predicate(); + + public override ETaskResult Update() + { + if (_continueAt == DateTime.MaxValue) + { + if (Task.Predicate()) + _continueAt = DateTime.Now.AddSeconds(0.5); + } + + return DateTime.Now >= _continueAt ? ETaskResult.TaskComplete : ETaskResult.StillRunning; + } + } } diff --git a/Questionable/Controller/Steps/Gathering/DoGather.cs b/Questionable/Controller/Steps/Gathering/DoGather.cs index bc4057375..e0e05a94a 100644 --- a/Questionable/Controller/Steps/Gathering/DoGather.cs +++ b/Questionable/Controller/Steps/Gathering/DoGather.cs @@ -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 logger) : ITask, IRevisitAware +internal static class DoGather { - private const uint StatusGatheringRateUp = 218; - - private bool _revisitTriggered; - private bool _wasGathering; - private SlotInfo? _slotToGather; - private Queue? _actionQueue; - - public bool Start() => true; - - public unsafe ETaskResult Update() + internal sealed record Task( + GatheringController.GatheringRequest Request, + GatheringNode Node, + bool RevisitRequired) : ITask, IRevisitAware { - if (revisitRequired && !_revisitTriggered) + public bool RevisitTriggered { get; private set; } + + public void OnRevisit() => RevisitTriggered = true; + + public override string ToString() => $"DoGather{(RevisitRequired ? " if revist" : "")}"; + } + + internal sealed class Executor( + GatheringController gatheringController, + GameFunctions gameFunctions, + IGameGui gameGui, + IClientState clientState, + ICondition condition, + ILogger logger) : TaskExecutor + { + private const uint StatusGatheringRateUp = 218; + + private bool _wasGathering; + private SlotInfo? _slotToGather; + private Queue? _actionQueue; + + protected override bool Start() => true; + + public override unsafe ETaskResult Update() { - logger.LogInformation("No revisit"); - return ETaskResult.TaskComplete; - } - - if (gatheringController.HasNodeDisappeared(currentNode)) - { - logger.LogInformation("Node disappeared"); - return ETaskResult.TaskComplete; - } - - if (gameFunctions.GetFreeInventorySlots() == 0) - throw new TaskException("Inventory full"); - - if (condition[ConditionFlag.Gathering]) - { - if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* _)) - return ETaskResult.TaskComplete; - - _wasGathering = true; - - if (gameGui.TryGetAddonByName("Gathering", out AddonGathering* addonGathering)) + if (Task is { RevisitRequired: true, RevisitTriggered: false }) { - if (gatheringController.HasRequestedItems()) + logger.LogInformation("No revisit"); + return ETaskResult.TaskComplete; + } + + if (gatheringController.HasNodeDisappeared(Task.Node)) + { + logger.LogInformation("Node disappeared"); + return ETaskResult.TaskComplete; + } + + if (gameFunctions.GetFreeInventorySlots() == 0) + throw new TaskException("Inventory full"); + + if (condition[ConditionFlag.Gathering]) + { + if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* _)) + return ETaskResult.TaskComplete; + + _wasGathering = true; + + if (gameGui.TryGetAddonByName("Gathering", out AddonGathering* addonGathering)) { - addonGathering->FireCallbackInt(-1); - } - else - { - 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 ReadSlots(AddonGathering* addonGathering) - { - var atkValues = addonGathering->AtkValues; - List slots = new List(); - for (int i = 0; i < 8; ++i) + private unsafe List ReadSlots(AddonGathering* addonGathering) { - // +8 = new item? - uint itemId = atkValues[i * 11 + 7].UInt; - if (itemId == 0) - continue; - - AtkComponentCheckBox* atkCheckbox = addonGathering->GatheredItemComponentCheckbox[i].Value; - - AtkTextNode* atkGatheringChance = atkCheckbox->UldManager.SearchNodeById(10)->GetAsAtkTextNode(); - if (!int.TryParse(atkGatheringChance->NodeText.ToString(), out int gatheringChance)) - gatheringChance = 0; - - AtkTextNode* atkBoonChance = atkCheckbox->UldManager.SearchNodeById(16)->GetAsAtkTextNode(); - if (!int.TryParse(atkBoonChance->NodeText.ToString(), out int boonChance)) - boonChance = 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; - - var slot = new SlotInfo(i, itemId, gatheringChance, boonChance, quantity); - slots.Add(slot); - } - - return slots; - } - - [SuppressMessage("ReSharper", "UnusedParameter.Local")] - private Queue GetNextActions(NodeCondition nodeCondition, List slots) - { - //uint gp = clientState.LocalPlayer!.CurrentGp; - Queue actions = new(); - - if (!gameFunctions.HasStatus(StatusGatheringRateUp)) - { - // do we have an alternative item? only happens for 'evaluation' leve quests - if (currentRequest.AlternativeItemId != 0) + var atkValues = addonGathering->AtkValues; + List slots = new List(); + for (int i = 0; i < 8; ++i) { - var alternativeSlot = slots.Single(x => x.ItemId == currentRequest.AlternativeItemId); + // +8 = new item? + uint itemId = atkValues[i * 11 + 7].UInt; + if (itemId == 0) + continue; - if (alternativeSlot.GatheringChance == 100) - { - _slotToGather = alternativeSlot; - return actions; - } + AtkComponentCheckBox* atkCheckbox = addonGathering->GatheredItemComponentCheckbox[i].Value; - if (alternativeSlot.GatheringChance > 0) + AtkTextNode* atkGatheringChance = atkCheckbox->UldManager.SearchNodeById(10)->GetAsAtkTextNode(); + if (!int.TryParse(atkGatheringChance->NodeText.ToString(), out int gatheringChance)) + gatheringChance = 0; + + AtkTextNode* atkBoonChance = atkCheckbox->UldManager.SearchNodeById(16)->GetAsAtkTextNode(); + if (!int.TryParse(atkBoonChance->NodeText.ToString(), out int boonChance)) + boonChance = 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; + + var slot = new SlotInfo(i, itemId, gatheringChance, boonChance, quantity); + slots.Add(slot); + } + + return slots; + } + + [SuppressMessage("ReSharper", "UnusedParameter.Local")] + private Queue GetNextActions(NodeCondition nodeCondition, List slots) + { + //uint gp = clientState.LocalPlayer!.CurrentGp; + Queue actions = new(); + + if (!gameFunctions.HasStatus(StatusGatheringRateUp)) + { + // do we have an alternative item? only happens for 'evaluation' leve quests + if (Task.Request.AlternativeItemId != 0) { - if (alternativeSlot.GatheringChance >= 95 && - CanUseAction(EAction.SharpVision1, EAction.FieldMastery1)) + 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; + } + } + } + + var slot = slots.Single(x => x.ItemId == Task.Request.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 (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 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 unsafe bool CanUseAction(EAction minerAction, EAction botanistAction) - { - EAction action = PickAction(minerAction, botanistAction); - return ActionManager.Instance()->GetActionStatus(ActionType.Action, (uint)action) == 0; - } - - public void OnRevisit() - { - _revisitTriggered = true; - } - - 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); diff --git a/Questionable/Controller/Steps/Gathering/DoGatherCollectable.cs b/Questionable/Controller/Steps/Gathering/DoGatherCollectable.cs index 99779c140..17376e10a 100644 --- a/Questionable/Controller/Steps/Gathering/DoGatherCollectable.cs +++ b/Questionable/Controller/Steps/Gathering/DoGatherCollectable.cs @@ -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 logger) : ITask, IRevisitAware +internal static class DoGatherCollectable { - private bool _revisitTriggered; - private Queue? _actionQueue; - - private bool? _expectedScrutiny; - - public bool Start() => true; - - public unsafe ETaskResult Update() + internal sealed record Task( + GatheringController.GatheringRequest Request, + GatheringNode Node, + bool RevisitRequired) : ITask, IRevisitAware { - if (revisitRequired && !_revisitTriggered) + public bool RevisitTriggered { get; private set; } + + public void OnRevisit() => RevisitTriggered = true; + + public override string ToString() => + $"DoGatherCollectable({SeIconChar.Collectible.ToIconString()}/{Request.Collectability}){(RevisitRequired ? " if revist" : "")}"; + } + + internal sealed class Executor( + GatheringController gatheringController, + GameFunctions gameFunctions, + IClientState clientState, + IGameGui gameGui, + ILogger logger) : TaskExecutor + { + private Queue? _actionQueue; + + private bool? _expectedScrutiny; + + protected override bool Start() => true; + + public override unsafe ETaskResult Update() { - logger.LogInformation("No revisit"); - return ETaskResult.TaskComplete; + if (Task.RevisitRequired && !Task.RevisitTriggered) + { + logger.LogInformation("No revisit"); + return ETaskResult.TaskComplete; + } + + if (gatheringController.HasNodeDisappeared(Task.Node)) + { + logger.LogInformation("Node disappeared"); + return ETaskResult.TaskComplete; + } + + if (gatheringController.HasRequestedItems()) + { + if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* atkUnitBase)) + { + atkUnitBase->FireCallbackInt(1); + return ETaskResult.StillRunning; + } + + if (gameGui.TryGetAddonByName("Gathering", out atkUnitBase)) + { + atkUnitBase->FireCallbackInt(-1); + return ETaskResult.TaskComplete; + } + } + + 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; + } + + 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(); + } + + return ETaskResult.StillRunning; + } + + if (nodeCondition.CollectabilityToGoal(Task.Request.Collectability) > 0) + { + _actionQueue = GetNextActions(nodeCondition); + if (_actionQueue != null) + { + foreach (var action in _actionQueue) + logger.LogInformation("Next Actions {Action}", action); + return ETaskResult.StillRunning; + } + } + + _actionQueue = new Queue(); + _actionQueue.Enqueue(PickAction(EAction.CollectMiner, EAction.CollectBotanist)); + return ETaskResult.StillRunning; } - if (gatheringController.HasNodeDisappeared(currentNode)) - { - logger.LogInformation("Node disappeared"); - return ETaskResult.TaskComplete; - } - - if (gatheringController.HasRequestedItems()) + private unsafe NodeCondition? GetNodeCondition() { if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* atkUnitBase)) { - atkUnitBase->FireCallbackInt(1); - 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 + ); } - if (gameGui.TryGetAddonByName("Gathering", out atkUnitBase)) + return null; + } + + private Queue 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 actions = new(); + + uint neededCollectability = nodeCondition.CollectabilityToGoal(Task.Request.Collectability); + if (neededCollectability <= nodeCondition.CollectabilityFromMeticulous) { - atkUnitBase->FireCallbackInt(-1); - return ETaskResult.TaskComplete; + logger.LogTrace("Can get all needed {NeededCollectability} from {Collectability}~ meticulous", + neededCollectability, nodeCondition.CollectabilityFromMeticulous); + actions.Enqueue(PickAction(EAction.MeticulousMiner, EAction.MeticulousBotanist)); + return actions; } - } - 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; - } - - if (_actionQueue != null && _actionQueue.TryPeek(out EAction nextAction)) - { - if (gameFunctions.UseAction(nextAction)) + if (neededCollectability <= nodeCondition.CollectabilityFromScour) { - _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(); + logger.LogTrace("Can get all needed {NeededCollectability} from {Collectability}~ scour", + neededCollectability, nodeCondition.CollectabilityFromScour); + actions.Enqueue(PickAction(EAction.ScourMiner, EAction.ScourBotanist)); + return actions; } - return ETaskResult.StillRunning; - } - - if (nodeCondition.CollectabilityToGoal(currentRequest.Collectability) > 0) - { - _actionQueue = GetNextActions(nodeCondition); - if (_actionQueue != null) + // neither action directly solves our problem + if (!nodeCondition.ScrutinyActive && gp >= 200) { - foreach (var action in _actionQueue) - logger.LogInformation("Next Actions {Action}", action); - return ETaskResult.StillRunning; + 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; } } - _actionQueue = new Queue(); - _actionQueue.Enqueue(PickAction(EAction.CollectMiner, EAction.CollectBotanist)); - return ETaskResult.StillRunning; - } - - private unsafe NodeCondition? GetNodeCondition() - { - if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* atkUnitBase)) + private EAction PickAction(EAction minerAction, EAction botanistAction) { - 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; - } - - private Queue 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 actions = new(); - - 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; - } - - // 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 ((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, diff --git a/Questionable/Controller/Steps/Gathering/MoveToLandingLocation.cs b/Questionable/Controller/Steps/Gathering/MoveToLandingLocation.cs index a9ebe4a5d..cc9f4d979 100644 --- a/Questionable/Controller/Steps/Gathering/MoveToLandingLocation.cs +++ b/Questionable/Controller/Steps/Gathering/MoveToLandingLocation.cs @@ -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 logger) : ITask +internal static class MoveToLandingLocation { - private ITask _moveTask = null!; - - public bool Start() + internal sealed record Task( + ushort TerritoryId, + bool FlyBetweenNodes, + GatheringNode GatheringNode) : ITask { - 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; - - location = 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 = 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 string ToString() => $"Land/{FlyBetweenNodes}"; } - public ETaskResult Update() => _moveTask.Update(); + internal sealed class Executor( + MoveTo.MoveExecutor moveExecutor, + GameFunctions gameFunctions, + IObjectTable objectTable, + ILogger logger) : TaskExecutor + { + private ITask _moveTask = null!; - public override string ToString() => $"Land/{_moveTask}/{flyBetweenNodes}"; + 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); + } + + public override ETaskResult Update() => moveExecutor.Update(); + } } diff --git a/Questionable/Controller/Steps/Gathering/TurnInDelivery.cs b/Questionable/Controller/Steps/Gathering/TurnInDelivery.cs index 08da14409..caf2b0f46 100644 --- a/Questionable/Controller/Steps/Gathering/TurnInDelivery.cs +++ b/Questionable/Controller/Steps/Gathering/TurnInDelivery.cs @@ -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()); + return new Task(); } } - private sealed class SatisfactionSupplyTurnIn(ILogger logger) : ITask + internal sealed record Task : ITask + { + public override string ToString() => "WeeklyDeliveryTurnIn"; + } + + internal sealed class SatisfactionSupplyTurnIn(ILogger logger) : TaskExecutor { 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"; } } diff --git a/Questionable/Controller/Steps/IConditionChangeAware.cs b/Questionable/Controller/Steps/IConditionChangeAware.cs index 4e41d6389..215106f6f 100644 --- a/Questionable/Controller/Steps/IConditionChangeAware.cs +++ b/Questionable/Controller/Steps/IConditionChangeAware.cs @@ -2,7 +2,7 @@ namespace Questionable.Controller.Steps; -public interface IConditionChangeAware +internal interface IConditionChangeAware : ITaskExecutor { void OnConditionChange(ConditionFlag flag, bool value); } diff --git a/Questionable/Controller/Steps/IRevisitAware.cs b/Questionable/Controller/Steps/IRevisitAware.cs index 4faf1c0bb..6af5d689f 100644 --- a/Questionable/Controller/Steps/IRevisitAware.cs +++ b/Questionable/Controller/Steps/IRevisitAware.cs @@ -1,6 +1,6 @@ namespace Questionable.Controller.Steps; -public interface IRevisitAware +internal interface IRevisitAware : ITask { void OnRevisit(); } diff --git a/Questionable/Controller/Steps/ITask.cs b/Questionable/Controller/Steps/ITask.cs index a8442c767..60c22cf93 100644 --- a/Questionable/Controller/Steps/ITask.cs +++ b/Questionable/Controller/Steps/ITask.cs @@ -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(); } diff --git a/Questionable/Controller/Steps/IToastAware.cs b/Questionable/Controller/Steps/IToastAware.cs index 67a1c7f62..49e09d504 100644 --- a/Questionable/Controller/Steps/IToastAware.cs +++ b/Questionable/Controller/Steps/IToastAware.cs @@ -2,7 +2,7 @@ namespace Questionable.Controller.Steps; -public interface IToastAware +internal interface IToastAware : ITaskExecutor { bool OnErrorToast(SeString message); } diff --git a/Questionable/Controller/Steps/Interactions/Action.cs b/Questionable/Controller/Steps/Interactions/Action.cs index f50f66970..e6c4f1f48 100644 --- a/Questionable/Controller/Steps/Interactions/Action.cs +++ b/Questionable/Controller/Steps/Interactions/Action.cs @@ -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 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()); + 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 logger) : ITask + ILogger logger) : TaskExecutor { 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 +} diff --git a/Questionable/Controller/Steps/Interactions/AetherCurrent.cs b/Questionable/Controller/Steps/Interactions/AetherCurrent.cs index cbbe68686..e0f65c1a8 100644 --- a/Questionable/Controller/Steps/Interactions/AetherCurrent.cs +++ b/Questionable/Controller/Steps/Interactions/AetherCurrent.cs @@ -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()); + return new Attune(step.DataId.Value, step.AetherCurrentId.Value); } } - private sealed class DoAttune( - uint dataId, - uint aetherCurrentId, - GameFunctions gameFunctions, - ILogger logger) : ITask + internal sealed record Attune(uint DataId, uint AetherCurrentId) : ITask { - private InteractionProgressContext? _progressContext; + public override string ToString() => $"AttuneAetherCurrent({AetherCurrentId})"; + } - public InteractionProgressContext? ProgressContext() => _progressContext; - - public bool Start() + internal sealed class DoAttune( + GameFunctions gameFunctions, + ILogger logger) : TaskExecutor + { + 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})"; } } diff --git a/Questionable/Controller/Steps/Interactions/AethernetShard.cs b/Questionable/Controller/Steps/Interactions/AethernetShard.cs index ed5580c8c..d7d09a0c7 100644 --- a/Questionable/Controller/Steps/Interactions/AethernetShard.cs +++ b/Questionable/Controller/Steps/Interactions/AethernetShard.cs @@ -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()); + 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 logger) : ITask + ILogger logger) : TaskExecutor { - 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})"; } } diff --git a/Questionable/Controller/Steps/Interactions/Aetheryte.cs b/Questionable/Controller/Steps/Interactions/Aetheryte.cs index eace64099..6d3cb54dc 100644 --- a/Questionable/Controller/Steps/Interactions/Aetheryte.cs +++ b/Questionable/Controller/Steps/Interactions/Aetheryte.cs @@ -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()); + 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 logger) : ITask + ILogger logger) : TaskExecutor { - 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})"; } } diff --git a/Questionable/Controller/Steps/Interactions/Combat.cs b/Questionable/Controller/Steps/Interactions/Combat.cs index 170883a3d..514f15752 100644 --- a/Questionable/Controller/Steps/Interactions/Combat.cs +++ b/Questionable/Controller/Steps/Interactions/Combat.cs @@ -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 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 killEnemyDataIds, IList completionQuestVariablesFlags, IList 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 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 completionQuestVariableFlags, + CombatController combatController, - QuestFunctions questFunctions) : ITask + QuestFunctions questFunctions) : TaskExecutor { 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})"; - } } } diff --git a/Questionable/Controller/Steps/Interactions/Dive.cs b/Questionable/Controller/Steps/Interactions/Dive.cs index 0fb2d3a6e..b5389774c 100644 --- a/Questionable/Controller/Steps/Interactions/Dive.cs +++ b/Questionable/Controller/Steps/Interactions/Dive.cs @@ -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(); - } - - public ITask Dive() - { - return new DoDive(condition, loggerFactory.CreateLogger()); + return new Task(); } } - private sealed class DoDive(ICondition condition, ILogger logger) - : AbstractDelayedTask(TimeSpan.FromSeconds(5)) + internal sealed class Task : ITask + { + + public override string ToString() => "Dive"; + } + + internal sealed class DoDive(ICondition condition, ILogger logger) + : AbstractDelayedTaskExecutor(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? GetKeysToPress(SeVirtualKey key, ModifierFlag modifier) diff --git a/Questionable/Controller/Steps/Interactions/Duty.cs b/Questionable/Controller/Steps/Interactions/Duty.cs index 975d107e5..172eb961a 100644 --- a/Questionable/Controller/Steps/Interactions/Duty.cs +++ b/Questionable/Controller/Steps/Interactions/Duty.cs @@ -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, - GameFunctions gameFunctions, - ICondition condition) : ITask + internal sealed record Task(uint ContentFinderConditionId) : ITask { - public bool Start() + public override string ToString() => $"OpenDutyFinder({ContentFinderConditionId})"; + } + + internal sealed class Executor( + GameFunctions gameFunctions, + ICondition condition) : TaskExecutor + { + 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; } } diff --git a/Questionable/Controller/Steps/Interactions/Emote.cs b/Questionable/Controller/Steps/Interactions/Emote.cs index 8a5db80e4..085b0356e 100644 --- a/Questionable/Controller/Steps/Interactions/Emote.cs +++ b/Questionable/Controller/Steps/Interactions/Emote.cs @@ -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 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 { - protected override bool StartInternal() - { - chatFunctions.UseEmote(dataId, emote); - return true; - } - - public override string ToString() => $"Emote({emote} on {dataId})"; + public override string ToString() => $"Emote({Emote} on {DataId})"; } - private sealed class UseOnSelf(EEmote emote, ChatFunctions chatFunctions) : AbstractDelayedTask + internal sealed class UseOnObjectExecutor(ChatFunctions chatFunctions) + : AbstractDelayedTaskExecutor { protected override bool StartInternal() { - chatFunctions.UseEmote(emote); + chatFunctions.UseEmote(Task.DataId, Task.Emote); return true; } + } - public override string ToString() => $"Emote({emote})"; + internal sealed record UseOnSelf(EEmote Emote) : ITask + { + public override string ToString() => $"Emote({Emote})"; + } + + internal sealed class UseOnSelfExecutor(ChatFunctions chatFunctions) : AbstractDelayedTaskExecutor + { + protected override bool StartInternal() + { + chatFunctions.UseEmote(Task.Emote); + return true; + } } } diff --git a/Questionable/Controller/Steps/Interactions/EquipItem.cs b/Questionable/Controller/Steps/Interactions/EquipItem.cs index 4adea0795..d9cd3971c 100644 --- a/Questionable/Controller/Steps/Interactions/EquipItem.cs +++ b/Questionable/Controller/Steps/Interactions/EquipItem.cs @@ -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()!.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()); - } - - private static List? 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 - }; + return new Task(step.ItemId.Value); } } - private sealed class DoEquip( - uint itemId, - Item item, - List targetSlots, + internal sealed record Task(uint ItemId) : ITask + { + public override string ToString() => $"Equip({ItemId})"; + } + + internal sealed class Executor( IDataManager dataManager, - ILogger logger) : ITask, IToastAware + ILogger logger) : TaskExecutor, IToastAware { private const int MaxAttempts = 3; @@ -81,16 +63,22 @@ internal static class EquipItem ]; private int _attempts; + private Item _item = null!; + private List _targetSlots = null!; private DateTime _continueAt = DateTime.MaxValue; - public bool Start() + protected override bool Start() { + _item = dataManager.GetExcelSheet()!.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? 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) { diff --git a/Questionable/Controller/Steps/Interactions/EquipRecommended.cs b/Questionable/Controller/Steps/Interactions/EquipRecommended.cs index 5ff7ab355..cd4f79828 100644 --- a/Questionable/Controller/Steps/Interactions/EquipRecommended.cs +++ b/Questionable/Controller/Steps/Interactions/EquipRecommended.cs @@ -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 { 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"; } } diff --git a/Questionable/Controller/Steps/Interactions/Interact.cs b/Questionable/Controller/Steps/Interactions/Interact.cs index 020fc66e7..286cf8286 100644 --- a/Questionable/Controller/Steps/Interactions/Interact.cs +++ b/Questionable/Controller/Steps/Interactions/Interact.cs @@ -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 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()); - } + 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 logger) - : ITask + : TaskExecutor { 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; - } + InteractionType = Task.InteractionType; - public InteractionProgressContext? ProgressContext() => _progressContext; - - 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})"; } } diff --git a/Questionable/Controller/Steps/Interactions/Jump.cs b/Questionable/Controller/Steps/Interactions/Jump.cs index d9d93db59..f7b9892d5 100644 --- a/Questionable/Controller/Steps/Interactions/Jump.cs +++ b/Questionable/Controller/Steps/Interactions/Jump.cs @@ -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); - } - - public ITask SingleJump(uint? dataId, JumpDestination jumpDestination, string? comment) - { - return new DoSingleJump(dataId, jumpDestination, comment, movementController, clientState, framework); - } - - public ITask RepeatedJumps(uint? dataId, JumpDestination jumpDestination, string? comment) - { - return new DoRepeatedJumps(dataId, jumpDestination, comment, movementController, clientState, framework, - condition, loggerFactory.CreateLogger()); + return new RepeatedJumpTask(step.DataId, step.JumpDestination, step.Comment); } } - private class DoSingleJump( - uint? dataId, - JumpDestination jumpDestination, - string? comment, + internal interface IJumpTask : ITask + { + uint? DataId { get; } + JumpDestination JumpDestination { get; } + string? Comment { get; } + } + + internal sealed record SingleJumpTask( + uint? DataId, + JumpDestination JumpDestination, + string? Comment) : IJumpTask + { + public override string ToString() => $"Jump({Comment})"; + } + + internal abstract class JumpBase( MovementController movementController, IClientState clientState, - IFramework framework) : ITask + IFramework framework) : TaskExecutor + 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})"; } - private sealed class DoRepeatedJumps( - uint? dataId, - JumpDestination jumpDestination, - string? comment, + internal sealed class DoSingleJump( + MovementController movementController, + IClientState clientState, + IFramework framework) : JumpBase(movementController, clientState, framework); + + internal sealed record RepeatedJumpTask( + uint? DataId, + JumpDestination JumpDestination, + string? Comment) : IJumpTask + { + public override string ToString() => $"RepeatedJump({Comment})"; + } + + internal sealed class DoRepeatedJumps( MovementController movementController, IClientState clientState, IFramework framework, ICondition condition, ILogger logger) - : DoSingleJump(dataId, jumpDestination, comment, movementController, clientState, framework) + : JumpBase(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})"; } } diff --git a/Questionable/Controller/Steps/Interactions/Say.cs b/Questionable/Controller/Steps/Interactions/Say.cs index 3d917b333..f13ab4ab9 100644 --- a/Questionable/Controller/Steps/Interactions/Say.cs +++ b/Questionable/Controller/Steps/Interactions/Say.cs @@ -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 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 { protected override bool StartInternal() { - chatFunctions.ExecuteCommand($"/say {chatMessage}"); + chatFunctions.ExecuteCommand($"/say {Task.ChatMessage}"); return true; } - - public override string ToString() => $"Say({chatMessage})"; } } diff --git a/Questionable/Controller/Steps/Interactions/UseItem.cs b/Questionable/Controller/Steps/Interactions/UseItem.cs index e4f0de00e..ccdb998d0 100644 --- a/Questionable/Controller/Steps/Interactions/UseItem.cs +++ b/Questionable/Controller/Steps/Interactions/UseItem.cs @@ -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 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 completionQuestVariablesFlags) - { - return new UseOnGround(questId, dataId, itemId, completionQuestVariablesFlags, gameFunctions, - questFunctions, condition, loggerFactory.CreateLogger()); - } - - public ITask OnPosition(ElementId questId, Vector3 position, uint itemId, - List completionQuestVariablesFlags) - { - return new UseOnPosition(questId, position, itemId, completionQuestVariablesFlags, gameFunctions, - questFunctions, condition, loggerFactory.CreateLogger()); - } - - public ITask OnObject(ElementId questId, uint dataId, uint itemId, - List completionQuestVariablesFlags, bool startingCombat = false) - { - return new UseOnObject(questId, dataId, itemId, completionQuestVariablesFlags, startingCombat, - questFunctions, gameFunctions, condition, loggerFactory.CreateLogger()); - } - - public ITask OnSelf(ElementId questId, uint itemId, List completionQuestVariablesFlags) - { - return new Use(questId, itemId, completionQuestVariablesFlags, gameFunctions, questFunctions, condition, - loggerFactory.CreateLogger()); - } - private IEnumerable 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 completionQuestVariablesFlags, - bool startingCombat, + internal interface IUseItemBase : ITask + { + ElementId? QuestId { get; } + uint ItemId { get; } + IList CompletionQuestVariablesFlags { get; } + bool StartingCombat { get; } + } + + internal abstract class UseItemExecutorBase( QuestFunctions questFunctions, ICondition condition, - ILogger logger) : ITask + ILogger logger) : TaskExecutor + 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 CompletionQuestVariablesFlags => completionQuestVariablesFlags; - public bool StartingCombat => startingCombat; + private ElementId? QuestId => Task.QuestId; + protected uint ItemId => Task.ItemId; + private IList 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 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 completionQuestVariablesFlags, + internal sealed class UseOnGroundExecutor( GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, - ILogger logger) - : UseItemBase(questId, itemId, completionQuestVariablesFlags, false, questFunctions, condition, logger) + ILogger logger) + : UseItemExecutorBase(questFunctions, condition, logger) { - protected override bool UseItem() => gameFunctions.UseItemOnGround(dataId, ItemId); - - public override string ToString() => $"UseItem({ItemId} on ground at {dataId})"; + protected override bool UseItem() => gameFunctions.UseItemOnGround(Task.DataId, ItemId); } - private sealed class UseOnPosition( - ElementId? questId, - Vector3 position, - uint itemId, - IList completionQuestVariablesFlags, + internal sealed record UseOnPosition( + ElementId? QuestId, + Vector3 Position, + uint ItemId, + IList CompletionQuestVariablesFlags) + : IUseItemBase + { + public bool StartingCombat => false; + + public override string ToString() => + $"UseItem({ItemId} on ground at {Position.ToString("G", CultureInfo.InvariantCulture)})"; + } + + internal sealed class UseOnPositionExecutor( GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, ILogger logger) - : UseItemBase(questId, itemId, completionQuestVariablesFlags, false, questFunctions, condition, logger) + : UseItemExecutorBase(questFunctions, condition, logger) { - protected override bool UseItem() => gameFunctions.UseItemOnPosition(position, ItemId); - - public override string ToString() => - $"UseItem({ItemId} on ground at {position.ToString("G", CultureInfo.InvariantCulture)})"; + protected override bool UseItem() => gameFunctions.UseItemOnPosition(Task.Position, ItemId); } - private sealed class UseOnObject( - ElementId? questId, - uint dataId, - uint itemId, - IList completionQuestVariablesFlags, - bool startingCombat, + internal sealed record UseOnObject( + ElementId? QuestId, + uint DataId, + uint ItemId, + IList CompletionQuestVariablesFlags, + bool StartingCombat = false) : IUseItemBase + { + public override string ToString() => $"UseItem({ItemId} on {DataId})"; + } + + internal sealed class UseOnObjectExecutor( QuestFunctions questFunctions, GameFunctions gameFunctions, ICondition condition, ILogger logger) - : UseItemBase(questId, itemId, completionQuestVariablesFlags, startingCombat, questFunctions, condition, logger) + : UseItemExecutorBase(questFunctions, condition, logger) { - protected override bool UseItem() => gameFunctions.UseItem(dataId, ItemId); - - public override string ToString() => $"UseItem({ItemId} on {dataId})"; + protected override bool UseItem() => gameFunctions.UseItem(Task.DataId, ItemId); } - private sealed class Use( - ElementId? questId, - uint itemId, - IList completionQuestVariablesFlags, + internal sealed record UseOnSelf( + ElementId? QuestId, + uint ItemId, + IList CompletionQuestVariablesFlags) : IUseItemBase + { + public bool StartingCombat => false; + public override string ToString() => $"UseItem({ItemId})"; + } + + internal sealed class UseOnSelfExecutor( GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, - ILogger logger) - : UseItemBase(questId, itemId, completionQuestVariablesFlags, false, questFunctions, condition, logger) + ILogger logger) + : UseItemExecutorBase(questFunctions, condition, logger) { protected override bool UseItem() => gameFunctions.UseItem(ItemId); - - public override string ToString() => $"UseItem({ItemId})"; } } diff --git a/Questionable/Controller/Steps/Leves/InitiateLeve.cs b/Questionable/Controller/Steps/Leves/InitiateLeve.cs index 7b12c95e1..4014b2fb3 100644 --- a/Questionable/Controller/Steps/Leves/InitiateLeve.cs +++ b/Questionable/Controller/Steps/Leves/InitiateLeve.cs @@ -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 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})"; + } - public ETaskResult Update() + internal sealed unsafe class SkipInitiateIfActiveExecutor : TaskExecutor + { + protected override bool Start() => true; + + 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 unsafe class OpenJournal(ElementId elementId) : ITask + 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 OpenJournalExecutor : TaskExecutor { - 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 unsafe class Initiate(ElementId elementId, IGameGui gameGui) : ITask + internal sealed record Initiate(ElementId ElementId) : ITask { - public bool Start() => true; + public override string ToString() => $"InitiateLeve({ElementId})"; + } - public ETaskResult Update() + internal sealed unsafe class InitiateExecutor(IGameGui gameGui) : TaskExecutor + { + protected override bool Start() => true; + + 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 unsafe class SelectDifficulty(IGameGui gameGui) : ITask + internal sealed class SelectDifficulty : ITask { - public bool Start() => true; + public override string ToString() => "SelectLeveDifficulty"; + } - public ETaskResult Update() + internal sealed unsafe class SelectDifficultyExecutor(IGameGui gameGui) : TaskExecutor + { + protected override bool Start() => true; + + 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"; } } diff --git a/Questionable/Controller/Steps/Shared/AethernetShortcut.cs b/Questionable/Controller/Steps/Shared/AethernetShortcut.cs index 25110ca85..bb39bd815 100644 --- a/Questionable/Controller/Steps/Shared/AethernetShortcut.cs +++ b/Questionable/Controller/Steps/Shared/AethernetShortcut.cs @@ -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 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); - } - - public ITask Use(EAetheryteLocation from, EAetheryteLocation to, SkipAetheryteCondition? skipConditions = null) - { - return new UseAethernetShortcut(from, to, skipConditions ?? new(), - loggerFactory.CreateLogger(), aetheryteFunctions, gameFunctions, questFunctions, - clientState, aetheryteData, territoryData, lifestreamIpc, movementController, condition); + yield return new Task(step.AethernetShortcut.From, step.AethernetShortcut.To, + step.SkipConditions?.AethernetShortcutIf ?? new()); } } + internal sealed record Task( + EAetheryteLocation From, + EAetheryteLocation To, + SkipAetheryteCondition SkipConditions) : ISkippableTask + { + public Task(EAetheryteLocation from, + EAetheryteLocation to) + : this(from, to, new()) + { + } + + public override string ToString() => $"UseAethernet({From} -> {To})"; + } + internal sealed class UseAethernetShortcut( - EAetheryteLocation from, - EAetheryteLocation to, - SkipAetheryteCondition skipConditions, ILogger 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 { 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 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})"; } } diff --git a/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs b/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs index 601c0c9e0..ba1e0211b 100644 --- a/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs +++ b/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs @@ -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 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(), aetheryteFunctions, questFunctions, clientState, - chatGui, aetheryteData); - } } - /// 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. - private sealed class UseAetheryteShortcut( - QuestStep? step, - ElementId? elementId, - EAetheryteLocation targetAetheryte, - ushort 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. + internal sealed record Task( + QuestStep? Step, + ElementId? ElementId, + EAetheryteLocation TargetAetheryte, + ushort ExpectedTerritoryId) : ISkippableTask + { + } + + internal sealed class UseAetheryteShortcut( ILogger logger, AetheryteFunctions aetheryteFunctions, QuestFunctions questFunctions, IClientState clientState, IChatGui chatGui, - AetheryteData aetheryteData) : ISkippableTask + AetheryteData aetheryteData) : TaskExecutor { 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})"; } } diff --git a/Questionable/Controller/Steps/Shared/Craft.cs b/Questionable/Controller/Steps/Shared/Craft.cs index 5d09f37c5..ad474fe2b 100644 --- a/Questionable/Controller/Steps/Shared/Craft.cs +++ b/Questionable/Controller/Steps/Shared/Craft.cs @@ -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 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()); } - private sealed class DoCraft( - uint itemId, - int itemCount, + internal sealed record CraftTask( + uint ItemId, + int ItemCount) : ITask + { + public override string ToString() => $"Craft {ItemCount}x {ItemId} (with Artisan)"; + } + + internal sealed class DoCraft( IDataManager dataManager, IClientState clientState, ArtisanIpc artisanIpc, - ILogger logger) : ITask + ILogger logger) : TaskExecutor { - 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()!.GetRow(itemId); + RecipeLookup? recipeLookup = dataManager.GetExcelSheet()!.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)"; } } diff --git a/Questionable/Controller/Steps/Shared/Gather.cs b/Questionable/Controller/Steps/Shared/Gather.cs index cc5d9cfac..52229aa18 100644 --- a/Questionable/Controller/Steps/Shared/Gather.cs +++ b/Questionable/Controller/Steps/Shared/Gather.cs @@ -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() .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,38 +108,12 @@ 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() - { - return new SkipMarker(); - } } - private sealed class StartGathering( + internal sealed record GatheringTask( GatheringPointId gatheringPointId, - GatheredItem gatheredItem, - GatheringController gatheringController) : ITask + GatheredItem gatheredItem) : ITask { - public bool Start() - { - return gatheringController.Start(new GatheringController.GatheringRequest(gatheringPointId, - gatheredItem.ItemId, gatheredItem.AlternativeItemId, gatheredItem.ItemCount, - gatheredItem.Collectability)); - } - - public ETaskResult Update() - { - if (gatheringController.Update() == GatheringController.EStatus.Complete) - return ETaskResult.TaskComplete; - - return ETaskResult.StillRunning; - } - public override string ToString() { if (gatheredItem.Collectability == 0) @@ -151,13 +124,35 @@ internal static class Gather } } + internal sealed class StartGathering(GatheringController gatheringController) : TaskExecutor + { + protected override bool Start() + { + return gatheringController.Start(new GatheringController.GatheringRequest(Task.gatheringPointId, + Task.gatheredItem.ItemId, Task.gatheredItem.AlternativeItemId, Task.gatheredItem.ItemCount, + Task.gatheredItem.Collectability)); + } + + public override ETaskResult Update() + { + if (gatheringController.Update() == GatheringController.EStatus.Complete) + return ETaskResult.TaskComplete; + + return ETaskResult.StillRunning; + } + } + /// /// A task that does nothing, but if we're skipping a step, this will be the task next in queue to be executed (instead of progressing to the next step) if gathering. /// 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 + { + protected override bool Start() => true; + public override ETaskResult Update() => ETaskResult.TaskComplete; + } } diff --git a/Questionable/Controller/Steps/Shared/MoveTo.cs b/Questionable/Controller/Steps/Shared/MoveTo.cs index e6d0cc945..2f7a8ea34 100644 --- a/Questionable/Controller/Steps/Shared/MoveTo.cs +++ b/Questionable/Controller/Steps/Shared/MoveTo.cs @@ -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 logger) : ITaskFactory { public IEnumerable 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(), clientState, dataManager); - } - - public ITask Land() - { - return new LandTask(clientState, condition, loggerFactory.CreateLogger()); - } - - public ITask ExpectToBeNearDataId(uint dataId, float stopDistance) - { - return new WaitForNearDataId(dataId, stopDistance, gameFunctions, clientState); - } - public IEnumerable 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, IToastAware { private readonly string _cannotExecuteAtThisTime; private readonly MovementController _movementController; - private readonly Mount.Factory _mountFactory; private readonly GameFunctions _gameFunctions; - private readonly ILogger _logger; + private readonly ILogger _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 logger, + ILogger 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(579, x => x.Text)!; - _destination = moveParams.Destination; + } - if (!gameFunctions.IsFlyingUnlocked(moveParams.TerritoryId)) + private void Initialize() + { + _destination = Task.Destination; + + 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 { - 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; + } - public bool Start() => true; + internal sealed class WaitForNearDataIdExecutor( + GameFunctions gameFunctions, + IClientState clientState) : TaskExecutor + { - public ETaskResult Update() + protected override bool Start() => true; + + 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 logger) : ITask + internal sealed class LandTask : ITask + { + public bool ShouldRedoOnInterrupt() => true; + } + + internal sealed class LandExecutor(IClientState clientState, ICondition condition, ILogger logger) : TaskExecutor { 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; diff --git a/Questionable/Controller/Steps/Shared/SkipCondition.cs b/Questionable/Controller/Steps/Shared/SkipCondition.cs index 1c8a3fa38..eaf1d1675 100644 --- a/Questionable/Controller/Steps/Shared/SkipCondition.cs +++ b/Questionable/Controller/Steps/Shared/SkipCondition.cs @@ -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); - } - - private CheckSkip Check(QuestStep step, SkipStepConditions? skipConditions, ElementId questId) - { - return new CheckSkip(step, skipConditions ?? new(), questId, loggerFactory.CreateLogger(), - aetheryteFunctions, gameFunctions, questFunctions, clientState); + return new SkipTask(step, skipConditions ?? new(), quest.Id); } } - private sealed class CheckSkip( - QuestStep step, - SkipStepConditions skipConditions, - ElementId elementId, + internal sealed record SkipTask( + QuestStep Step, + SkipStepConditions SkipConditions, + ElementId ElementId) : ITask + { + public override string ToString() => "CheckSkip"; + } + + internal sealed class CheckSkip( ILogger logger, AetheryteFunctions aetheryteFunctions, GameFunctions gameFunctions, QuestFunctions questFunctions, - IClientState clientState) : ITask + IClientState clientState) : TaskExecutor { - 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; } } diff --git a/Questionable/Controller/Steps/Shared/StepDisabled.cs b/Questionable/Controller/Steps/Shared/StepDisabled.cs index 864876218..917951f8f 100644 --- a/Questionable/Controller/Steps/Shared/StepDisabled.cs +++ b/Questionable/Controller/Steps/Shared/StepDisabled.cs @@ -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()); + return new SkipRemainingTasks(); } } - internal sealed class Task(ILogger logger) : ITask + internal sealed class SkipRemainingTasks : ITask { - public bool Start() => true; + public override string ToString() => "StepDisabled"; + } - public ETaskResult Update() + internal sealed class Executor(ILogger logger) : TaskExecutor + { + protected override bool Start() => true; + + public override ETaskResult Update() { logger.LogInformation("Skipping step, as it is disabled"); return ETaskResult.SkipRemainingTasksForStep; } - - public override string ToString() => "StepDisabled"; } } diff --git a/Questionable/Controller/Steps/Shared/SwitchClassJob.cs b/Questionable/Controller/Steps/Shared/SwitchClassJob.cs index bacd4b7d3..880aac693 100644 --- a/Questionable/Controller/Steps/Shared/SwitchClassJob.cs +++ b/Questionable/Controller/Steps/Shared/SwitchClassJob.cs @@ -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; - - var gearsetModule = RaptureGearsetModule.Instance(); - if (gearsetModule != null) - { - for (int i = 0; i < 100; ++i) - { - var gearset = gearsetModule->GetGearset(i); - if (gearset->ClassJob == (byte)classJob) - { - gearsetModule->EquipGearset(gearset->Id); - return true; - } - } - } - - throw new TaskException($"No gearset found for {classJob}"); + public override string ToString() => $"SwitchJob({ClassJob})"; } - protected override ETaskResult UpdateInternal() => ETaskResult.TaskComplete; + internal sealed class Executor(IClientState clientState) : AbstractDelayedTaskExecutor + { + protected override unsafe bool StartInternal() + { + if (clientState.LocalPlayer!.ClassJob.Id == (uint)Task.ClassJob) + return false; - public override string ToString() => $"SwitchJob({classJob})"; + var gearsetModule = RaptureGearsetModule.Instance(); + if (gearsetModule != null) + { + for (int i = 0; i < 100; ++i) + { + 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}"); + } + + protected override ETaskResult UpdateInternal() => ETaskResult.TaskComplete; + } } diff --git a/Questionable/Controller/Steps/Shared/WaitAtEnd.cs b/Questionable/Controller/Steps/Shared/WaitAtEnd.cs index e2a0864f5..083fff420 100644 --- a/Questionable/Controller/Steps/Shared/WaitAtEnd.cs +++ b/Questionable/Controller/Steps/Shared/WaitAtEnd.cs @@ -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 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,103 +136,133 @@ 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 + { + protected override bool StartInternal() + { + Delay = Task.Delay; + return true; + } + } + internal sealed class WaitNextStepOrSequence : ITask { - public bool Start() => true; - - public ETaskResult Update() => ETaskResult.StillRunning; - public override string ToString() => "Wait(next step or sequence)"; } - internal sealed class WaitForCompletionFlags(QuestId quest, QuestStep step, QuestFunctions questFunctions) : ITask + internal sealed class WaitNextStepOrSequenceExecutor : TaskExecutor { - public bool Start() => true; + protected override bool Start() => true; - public ETaskResult Update() + public override ETaskResult Update() => ETaskResult.StillRunning; + } + + 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 WaitForCompletionFlagsExecutor(QuestFunctions questFunctions) + : TaskExecutor + { + protected override bool Start() => true; + + 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 { - 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 class WaitQuestAccepted(ElementId elementId, QuestFunctions questFunctions) : ITask + internal sealed record WaitQuestAccepted(ElementId ElementId) : ITask { - public bool Start() => true; + public override string ToString() => $"WaitQuestAccepted({ElementId})"; + } - public ETaskResult Update() + internal sealed class WaitQuestAcceptedExecutor(QuestFunctions questFunctions) : TaskExecutor + { + protected override bool Start() => true; + + 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 class WaitQuestCompleted(ElementId elementId, QuestFunctions questFunctions) : ITask + internal sealed record WaitQuestCompleted(ElementId ElementId) : ITask { - public bool Start() => true; + public override string ToString() => $"WaitQuestComplete({ElementId})"; + } - public ETaskResult Update() + internal sealed class WaitQuestCompletedExecutor(QuestFunctions questFunctions) : TaskExecutor + { + protected override bool Start() => true; + + 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 ETaskResult Update() => ETaskResult.NextStep; - public override string ToString() => "NextStep"; } + internal sealed class NextStepExecutor : TaskExecutor + { + protected override bool Start() => true; + + public override ETaskResult Update() => ETaskResult.NextStep; + } + internal sealed class EndAutomation : ILastTask { public ElementId ElementId => throw new InvalidOperationException(); public int Sequence => throw new InvalidOperationException(); - public bool Start() => true; - - public ETaskResult Update() => ETaskResult.End; - public override string ToString() => "EndAutomation"; } + internal sealed class EndAutomationExecutor : TaskExecutor + { + + protected override bool Start() => true; + + public override ETaskResult Update() => ETaskResult.End; + } } diff --git a/Questionable/Controller/Steps/Shared/WaitAtStart.cs b/Questionable/Controller/Steps/Shared/WaitAtStart.cs index cbe63e7f2..c2c304b45 100644 --- a/Questionable/Controller/Steps/Shared/WaitAtStart.cs +++ b/Questionable/Controller/Steps/Shared/WaitAtStart.cs @@ -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 + { + 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 index 000000000..4f48b56b3 --- /dev/null +++ b/Questionable/Controller/Steps/TaskExecutor.cs @@ -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 : 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(); +} diff --git a/Questionable/Controller/Steps/TaskQueue.cs b/Questionable/Controller/Steps/TaskQueue.cs index ff12876ff..7d30706ac 100644 --- a/Questionable/Controller/Steps/TaskQueue.cs +++ b/Questionable/Controller/Steps/TaskQueue.cs @@ -8,10 +8,10 @@ internal sealed class TaskQueue { private readonly List _completedTasks = []; private readonly List _tasks = []; - public ITask? CurrentTask { get; set; } + public ITaskExecutor? CurrentTaskExecutor { get; set; } public IEnumerable 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 interruptionTasks) @@ -49,8 +49,8 @@ internal sealed class TaskQueue List newTasks = [ ..interruptionTasks, - .._completedTasks.Where(x => !ReferenceEquals(x, CurrentTask)).ToList(), - CurrentTask, + .._completedTasks.Where(x => !ReferenceEquals(x, CurrentTaskExecutor?.CurrentTask)).ToList(), + CurrentTaskExecutor?.CurrentTask, .._tasks ]; Reset(); diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index b58783535..98f357f16 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -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(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddSingleton(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); // task factories - serviceCollection.AddTaskFactory(); + serviceCollection + .AddTaskFactoryAndExecutor(); serviceCollection.AddTaskFactory(); - serviceCollection.AddTaskFactory(); - serviceCollection.AddTaskFactory(); - serviceCollection.AddTaskFactory(); - serviceCollection.AddTaskFactory(); - serviceCollection.AddTaskFactory(); - serviceCollection.AddTaskFactory(); + serviceCollection.AddTaskFactoryAndExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection + .AddTaskFactoryAndExecutor(); + serviceCollection + .AddTaskFactoryAndExecutor(); + serviceCollection + .AddTaskFactoryAndExecutor(); + serviceCollection + .AddTaskFactoryAndExecutor(); + serviceCollection.AddTaskFactoryAndExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); - serviceCollection.AddTaskFactory(); - serviceCollection.AddTaskFactory(); - serviceCollection.AddTaskFactory(); - serviceCollection.AddTaskFactory(); - serviceCollection.AddTaskFactory(); - serviceCollection.AddTaskFactory(); + serviceCollection.AddTaskFactoryAndExecutor(); + serviceCollection + .AddTaskFactoryAndExecutor(); + serviceCollection + .AddTaskFactoryAndExecutor(); + serviceCollection.AddTaskFactoryAndExecutor(); + serviceCollection.AddTaskFactoryAndExecutor(); + serviceCollection.AddTaskFactoryAndExecutor(); serviceCollection.AddTaskFactory(); - serviceCollection.AddTaskFactory(); - serviceCollection.AddTaskFactory(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskFactoryAndExecutor(); + serviceCollection.AddTaskFactoryAndExecutor(); serviceCollection.AddTaskFactory(); - serviceCollection.AddTaskFactory(); - serviceCollection.AddTaskFactory(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskFactoryAndExecutor(); + serviceCollection.AddTaskFactoryAndExecutor(); serviceCollection.AddTaskFactory(); - serviceCollection.AddTaskFactory(); - serviceCollection.AddTaskFactory(); - serviceCollection.AddTaskFactory(); - serviceCollection.AddTaskFactory(); - serviceCollection.AddTaskFactory(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskFactoryAndExecutor(); + serviceCollection + .AddTaskFactoryAndExecutor(); + serviceCollection.AddTaskFactoryAndExecutor(); + serviceCollection + .AddTaskFactoryAndExecutor(); + serviceCollection.AddTaskFactory(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + + serviceCollection.AddTaskExecutor(); serviceCollection.AddTaskFactory(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); serviceCollection.AddSingleton(); } diff --git a/Questionable/ServiceCollectionExtensions.cs b/Questionable/ServiceCollectionExtensions.cs index 0247cefe7..2b17a8315 100644 --- a/Questionable/ServiceCollectionExtensions.cs +++ b/Questionable/ServiceCollectionExtensions.cs @@ -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(); serviceCollection.AddSingleton(); } + + public static void AddTaskExecutor( + this IServiceCollection serviceCollection) + where T : class, ITask + where TExecutor : TaskExecutor + { + serviceCollection.AddKeyedTransient(typeof(T)); + serviceCollection.AddTransient(); + } + + public static void AddTaskFactoryAndExecutor( + this IServiceCollection serviceCollection) + where TFactory : class, ITaskFactory + where T : class, ITask + where TExecutor : TaskExecutor + { + serviceCollection.AddTaskFactory(); + serviceCollection.AddTaskExecutor(); + } }