if (!directory.Exists)
return;
- _pluginLog.Information($"Loading locations from {directory}");
+ //_pluginLog.Information($"Loading locations from {directory}");
foreach (FileInfo fileInfo in directory.GetFiles("*.json"))
{
try
List<DialogueChoiceInfo> dialogueChoices = [];
// levequest choices have some vague sort of priority
- if (_questController.HasCurrentTaskMatching<Interact.DoInteract>(out var interact) &&
+ if (_questController.HasCurrentTaskExecutorMatching<Interact.DoInteract>(out var interact) &&
interact.Quest != null &&
interact.InteractionType is EInteractionType.AcceptLeve or EInteractionType.CompleteLeve)
{
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
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;
internal sealed unsafe class GatheringController : MiniTaskController<GatheringController>
{
private readonly MovementController _movementController;
- private readonly MoveTo.Factory _moveFactory;
- private readonly Mount.Factory _mountFactory;
- private readonly Interact.Factory _interactFactory;
private readonly GatheringPointRegistry _gatheringPointRegistry;
private readonly GameFunctions _gameFunctions;
private readonly NavmeshIpc _navmeshIpc;
private readonly IObjectTable _objectTable;
private readonly ICondition _condition;
- private readonly ILoggerFactory _loggerFactory;
- private readonly IGameGui _gameGui;
- private readonly IClientState _clientState;
private readonly ILogger<GatheringController> _logger;
private readonly Regex _revisitRegex;
public GatheringController(
MovementController movementController,
- MoveTo.Factory moveFactory,
- Mount.Factory mountFactory,
- Combat.Factory combatFactory,
- Interact.Factory interactFactory,
GatheringPointRegistry gatheringPointRegistry,
GameFunctions gameFunctions,
NavmeshIpc navmeshIpc,
IChatGui chatGui,
ILogger<GatheringController> logger,
ICondition condition,
+ IServiceProvider serviceProvider,
IDataManager dataManager,
- ILoggerFactory loggerFactory,
- IGameGui gameGui,
- IClientState clientState,
IPluginLog pluginLog)
- : base(chatGui, mountFactory, combatFactory, condition, logger)
+ : base(chatGui, condition, serviceProvider, logger)
{
_movementController = movementController;
- _moveFactory = moveFactory;
- _mountFactory = mountFactory;
- _interactFactory = interactFactory;
_gatheringPointRegistry = gatheringPointRegistry;
_gameFunctions = gameFunctions;
_navmeshIpc = navmeshIpc;
_objectTable = objectTable;
_condition = condition;
- _loggerFactory = loggerFactory;
- _gameGui = gameGui;
- _clientState = clientState;
_logger = logger;
_revisitRegex = dataManager.GetRegex<LogMessage>(5574, x => x.Text, pluginLog)
return;
ushort territoryId = _currentRequest.Root.Steps.Last().TerritoryId;
- _taskQueue.Enqueue(_mountFactory.Mount(territoryId, Mount.EMountIf.Always));
+ _taskQueue.Enqueue(new Mount.MountTask(territoryId, Mount.EMountIf.Always));
bool fly = currentNode.Fly.GetValueOrDefault(_currentRequest.Root.FlyBetweenNodes.GetValueOrDefault(true)) &&
_gameFunctions.IsFlyingUnlocked(territoryId);
if (pointOnFloor != null)
pointOnFloor = pointOnFloor.Value with { Y = pointOnFloor.Value.Y + (fly ? 3f : 0f) };
- _taskQueue.Enqueue(_moveFactory.Move(new MoveTo.MoveParams(territoryId, pointOnFloor ?? averagePosition,
- null, 50f, Fly: fly, IgnoreDistanceToObject: true)));
+ _taskQueue.Enqueue(new MoveTo.MoveTask(territoryId, pointOnFloor ?? averagePosition,
+ null, 50f, Fly: fly, IgnoreDistanceToObject: true));
}
- _taskQueue.Enqueue(new MoveToLandingLocation(territoryId, fly, currentNode, _moveFactory, _gameFunctions,
- _objectTable, _loggerFactory.CreateLogger<MoveToLandingLocation>()));
- _taskQueue.Enqueue(_mountFactory.Unmount());
- _taskQueue.Enqueue(_interactFactory.Interact(currentNode.DataId, null, EInteractionType.Gather, true));
+ _taskQueue.Enqueue(new MoveToLandingLocation.Task(territoryId, fly, currentNode));
+ _taskQueue.Enqueue(new Mount.UnmountTask());
+ _taskQueue.Enqueue(new Interact.Task(currentNode.DataId, null, EInteractionType.Gather, true));
QueueGatherNode(currentNode);
}
{
foreach (bool revisitRequired in new[] { false, true })
{
- _taskQueue.Enqueue(new DoGather(_currentRequest!.Data, currentNode, revisitRequired, this, _gameFunctions,
- _gameGui, _clientState, _condition, _loggerFactory.CreateLogger<DoGather>()));
+ _taskQueue.Enqueue(new DoGather.Task(_currentRequest!.Data, currentNode, revisitRequired));
if (_currentRequest.Data.Collectability > 0)
{
- _taskQueue.Enqueue(new DoGatherCollectable(_currentRequest.Data, currentNode, revisitRequired, this,
- _gameFunctions, _clientState, _gameGui, _loggerFactory.CreateLogger<DoGatherCollectable>()));
+ _taskQueue.Enqueue(new DoGatherCollectable.Task(_currentRequest.Data, currentNode, revisitRequired));
}
}
}
public override IList<string> GetRemainingTaskNames()
{
- if (_taskQueue.CurrentTask is {} currentTask)
+ if (_taskQueue.CurrentTaskExecutor?.CurrentTask is {} currentTask)
return [currentTask.ToString() ?? "?", .. base.GetRemainingTaskNames()];
else
return base.GetRemainingTaskNames();
{
if (_revisitRegex.IsMatch(message.TextValue))
{
- if (_taskQueue.CurrentTask is IRevisitAware currentTaskRevisitAware)
+ if (_taskQueue.CurrentTaskExecutor?.CurrentTask is IRevisitAware currentTaskRevisitAware)
currentTaskRevisitAware.OnRevisit();
foreach (ITask task in _taskQueue.RemainingTasks)
private void LoadGatheringPointFromStream(string fileName, Stream stream)
{
- _logger.LogTrace("Loading gathering point from '{FileName}'", fileName);
+ //_logger.LogTrace("Loading gathering point from '{FileName}'", fileName);
GatheringPointId? gatheringPointId = ExtractGatheringPointIdFromName(fileName);
if (gatheringPointId == null)
return;
return;
}
- _logger.Log(logLevel, "Loading gathering points from {DirectoryName}", directory);
+ //_logger.Log(logLevel, "Loading gathering points from {DirectoryName}", directory);
foreach (FileInfo fileInfo in directory.GetFiles("*.json"))
{
try
using System.Linq;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps;
using Questionable.Controller.Steps.Common;
protected readonly TaskQueue _taskQueue = new();
private readonly IChatGui _chatGui;
- private readonly Mount.Factory _mountFactory;
- private readonly Combat.Factory _combatFactory;
private readonly ICondition _condition;
+ private readonly IServiceProvider _serviceProvider;
private readonly ILogger<T> _logger;
- protected MiniTaskController(IChatGui chatGui, Mount.Factory mountFactory, Combat.Factory combatFactory,
- ICondition condition, ILogger<T> logger)
+ protected MiniTaskController(IChatGui chatGui, ICondition condition, IServiceProvider serviceProvider,
+ ILogger<T> logger)
{
_chatGui = chatGui;
_logger = logger;
- _mountFactory = mountFactory;
- _combatFactory = combatFactory;
+ _serviceProvider = serviceProvider;
_condition = condition;
}
protected virtual void UpdateCurrentTask()
{
- if (_taskQueue.CurrentTask == null)
+ if (_taskQueue.CurrentTaskExecutor == null)
{
if (_taskQueue.TryDequeue(out ITask? upcomingTask))
{
try
{
_logger.LogInformation("Starting task {TaskName}", upcomingTask.ToString());
- if (upcomingTask.Start())
+ ITaskExecutor taskExecutor =
+ _serviceProvider.GetRequiredKeyedService<ITaskExecutor>(upcomingTask.GetType());
+ if (taskExecutor.Start(upcomingTask))
{
- _taskQueue.CurrentTask = upcomingTask;
+ _taskQueue.CurrentTaskExecutor = taskExecutor;
return;
}
else
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;
}
case ETaskResult.SkipRemainingTasksForStep:
_logger.LogInformation("{Task} → {Result}, skipping remaining tasks for step",
- _taskQueue.CurrentTask, result);
- _taskQueue.CurrentTask = null;
+ _taskQueue.CurrentTaskExecutor.CurrentTask, result);
+ _taskQueue.CurrentTaskExecutor = null;
while (_taskQueue.TryDequeue(out ITask? nextTask))
{
if (nextTask is ILastTask or Gather.SkipMarker)
{
- _taskQueue.CurrentTask = nextTask;
+ ITaskExecutor taskExecutor =
+ _serviceProvider.GetRequiredKeyedService<ITaskExecutor>(nextTask.GetType());
+ _taskQueue.CurrentTaskExecutor = taskExecutor;
return;
}
}
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;
}
{
List<ITask> tasks = [];
if (_condition[ConditionFlag.Mounted])
- tasks.Add(_mountFactory.Unmount());
+ tasks.Add(new Mount.UnmountTask());
- tasks.Add(_combatFactory.CreateTask(null, false, EEnemySpawnType.QuestInterruption, [], [], []));
+ tasks.Add(Combat.Factory.CreateTask(null, false, EEnemySpawnType.QuestInterruption, [], [], []));
tasks.Add(new WaitAtEnd.WaitDelay());
_taskQueue.InterruptWith(tasks);
}
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;
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))
{
public string ToStatString()
{
- return _taskQueue.CurrentTask is { } currentTask
+ return _taskQueue.CurrentTaskExecutor?.CurrentTask is { } currentTask
? $"{currentTask} (+{_taskQueue.RemainingTasks.Count()})"
: $"- (+{_taskQueue.RemainingTasks.Count()})";
}
+ public bool HasCurrentTaskExecutorMatching<T>([NotNullWhen(true)] out T? task)
+ where T : class, ITaskExecutor
+ {
+ if (_taskQueue.CurrentTaskExecutor is T t)
+ {
+ task = t;
+ return true;
+ }
+ else
+ {
+ task = null;
+ return false;
+ }
+ }
+
public bool HasCurrentTaskMatching<T>([NotNullWhen(true)] out T? task)
where T : class, ITask
{
- if (_taskQueue.CurrentTask is T t)
+ if (_taskQueue.CurrentTaskExecutor?.CurrentTask is T t)
{
task = t;
return true;
{
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 _);
public void SkipSimulatedTask()
{
- _taskQueue.CurrentTask = null;
+ _taskQueue.CurrentTaskExecutor = null;
}
public bool IsInterruptible()
private void OnConditionChange(ConditionFlag flag, bool value)
{
- if (_taskQueue.CurrentTask is IConditionChangeAware conditionChangeAware)
+ if (_taskQueue.CurrentTaskExecutor is IConditionChangeAware conditionChangeAware)
conditionChangeAware.OnConditionChange(flag, value);
}
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))
{
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;
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
+++ /dev/null
-using System;
-
-namespace Questionable.Controller.Steps.Common;
-
-internal abstract class AbstractDelayedTask : ITask
-{
- private DateTime _continueAt;
-
- protected AbstractDelayedTask(TimeSpan delay)
- {
- Delay = delay;
- }
-
- protected TimeSpan Delay { get; set; }
-
- protected AbstractDelayedTask()
- : this(TimeSpan.FromSeconds(5))
- {
- }
-
- public virtual InteractionProgressContext? ProgressContext() => null;
-
- public bool Start()
- {
- _continueAt = DateTime.Now.Add(Delay);
- return StartInternal();
- }
-
- protected abstract bool StartInternal();
-
- public virtual ETaskResult Update()
- {
- if (_continueAt >= DateTime.Now)
- return ETaskResult.StillRunning;
-
- return UpdateInternal();
- }
-
- protected virtual ETaskResult UpdateInternal() => ETaskResult.TaskComplete;
-}
--- /dev/null
+using System;
+
+namespace Questionable.Controller.Steps.Common;
+
+internal abstract class AbstractDelayedTaskExecutor<T> : TaskExecutor<T>
+ where T : class, ITask
+{
+ private DateTime _continueAt;
+
+ protected AbstractDelayedTaskExecutor()
+ : this(TimeSpan.FromSeconds(5))
+ {
+ }
+
+ protected AbstractDelayedTaskExecutor(TimeSpan delay)
+ {
+ Delay = delay;
+ }
+
+ protected TimeSpan Delay { get; set; }
+
+ protected sealed override bool Start()
+ {
+ bool started = StartInternal();
+ _continueAt = DateTime.Now.Add(Delay);
+ return started;
+ }
+
+ protected abstract bool StartInternal();
+
+ public override ETaskResult Update()
+ {
+ if (_continueAt >= DateTime.Now)
+ return ETaskResult.StillRunning;
+
+ return UpdateInternal();
+ }
+
+ protected virtual ETaskResult UpdateInternal() => ETaskResult.TaskComplete;
+}
internal static class Mount
{
- internal sealed class Factory(
- GameFunctions gameFunctions,
- ICondition condition,
- TerritoryData territoryData,
- IClientState clientState,
- ILoggerFactory loggerFactory)
+ internal sealed record MountTask(
+ ushort TerritoryId,
+ EMountIf MountIf,
+ Vector3? Position = null) : ITask
{
- public ITask Mount(ushort territoryId, EMountIf mountIf, Vector3? position = null)
- {
- if (mountIf == EMountIf.AwayFromPosition)
- ArgumentNullException.ThrowIfNull(position);
+ public Vector3? Position { get; } = MountIf == EMountIf.AwayFromPosition
+ ? Position ?? throw new ArgumentNullException(nameof(Position))
+ : null;
- return new MountTask(territoryId, mountIf, position, gameFunctions, condition, territoryData, clientState,
- loggerFactory.CreateLogger<MountTask>());
- }
+ public bool ShouldRedoOnInterrupt() => true;
- public ITask Unmount()
- {
- return new UnmountTask(condition, loggerFactory.CreateLogger<UnmountTask>(), gameFunctions, clientState);
- }
+ public override string ToString() => "Mount";
}
- private sealed class MountTask(
- ushort territoryId,
- EMountIf mountIf,
- Vector3? position,
+ internal sealed class MountExecutor(
GameFunctions gameFunctions,
ICondition condition,
TerritoryData territoryData,
IClientState clientState,
- ILogger<MountTask> logger) : ITask
+ ILogger<MountTask> logger) : TaskExecutor<MountTask>
{
private bool _mountTriggered;
- private InteractionProgressContext? _progressContext;
private DateTime _retryAt = DateTime.MinValue;
- public InteractionProgressContext? ProgressContext() => _progressContext;
-
- public bool ShouldRedoOnInterrupt() => true;
-
- public bool Start()
+ protected override bool Start()
{
if (condition[ConditionFlag.Mounted])
return false;
- if (!territoryData.CanUseMount(territoryId))
+ if (!territoryData.CanUseMount(Task.TerritoryId))
{
- logger.LogInformation("Can't use mount in current territory {Id}", territoryId);
+ logger.LogInformation("Can't use mount in current territory {Id}", Task.TerritoryId);
return false;
}
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;
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])
{
return false;
}
- public ETaskResult Update()
+ public override ETaskResult Update()
{
if (_mountTriggered && !condition[ConditionFlag.Mounted] && DateTime.Now > _retryAt)
{
return ETaskResult.TaskComplete;
}
- _progressContext = InteractionProgressContext.FromActionUse(() => _mountTriggered = gameFunctions.Mount());
+ ProgressContext =
+ InteractionProgressContext.FromActionUse(() => _mountTriggered = gameFunctions.Mount());
_retryAt = DateTime.Now.AddSeconds(5);
return ETaskResult.StillRunning;
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
}
+ }
- public override string ToString() => "Mount";
+ internal sealed record UnmountTask : ITask
+ {
+ public bool ShouldRedoOnInterrupt() => true;
+
+ public override string ToString() => "Unmount";
}
- private sealed class UnmountTask(
+ internal sealed class UnmountExecutor(
ICondition condition,
ILogger<UnmountTask> logger,
GameFunctions gameFunctions,
IClientState clientState)
- : ITask
+ : TaskExecutor<UnmountTask>
{
private bool _unmountTriggered;
private DateTime _continueAt = DateTime.MinValue;
- public bool ShouldRedoOnInterrupt() => true;
-
- public bool Start()
+ protected override bool Start()
{
if (!condition[ConditionFlag.Mounted])
return false;
return true;
}
- public ETaskResult Update()
+ public override ETaskResult Update()
{
if (_continueAt >= DateTime.Now)
return ETaskResult.StillRunning;
}
private unsafe bool IsUnmounting() => **(byte**)(clientState.LocalPlayer!.Address + 1432) == 1;
-
- public override string ToString() => "Unmount";
}
public enum EMountIf
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)
{
if (questFunctions.GetPriorityQuests().Contains(step.NextQuestId))
return null;
- return new SetQuest(step.NextQuestId, quest.Id, questRegistry, questController, questFunctions, loggerFactory.CreateLogger<SetQuest>());
+ return new SetQuestTask(step.NextQuestId, quest.Id);
}
}
- private sealed class SetQuest(ElementId nextQuestId, ElementId currentQuestId, QuestRegistry questRegistry, QuestController questController, QuestFunctions questFunctions, ILogger<SetQuest> logger) : ITask
+ internal sealed record SetQuestTask(ElementId NextQuestId, ElementId CurrentQuestId) : ITask
{
- public bool Start()
+ public override string ToString() => $"SetNextQuest({NextQuestId})";
+ }
+
+ internal sealed class Executor(
+ QuestRegistry questRegistry,
+ QuestController questController,
+ QuestFunctions questFunctions,
+ ILogger<Executor> logger) : TaskExecutor<SetQuestTask>
+ {
+ protected override bool Start()
{
- if (questFunctions.IsQuestLocked(nextQuestId, currentQuestId))
+ if (questFunctions.IsQuestLocked(Task.NextQuestId, Task.CurrentQuestId))
{
- logger.LogInformation("Can't set next quest to {QuestId}, quest is locked", nextQuestId);
+ logger.LogInformation("Can't set next quest to {QuestId}, quest is locked", Task.NextQuestId);
}
- else if (questRegistry.TryGetQuest(nextQuestId, out Quest? quest))
+ else if (questRegistry.TryGetQuest(Task.NextQuestId, out Quest? quest))
{
- logger.LogInformation("Setting next quest to {QuestId}: '{QuestName}'", nextQuestId, quest.Info.Name);
+ logger.LogInformation("Setting next quest to {QuestId}: '{QuestName}'", Task.NextQuestId, quest.Info.Name);
questController.SetNextQuest(quest);
}
else
{
- logger.LogInformation("Next quest with id {QuestId} not found", nextQuestId);
+ logger.LogInformation("Next quest with id {QuestId} not found", Task.NextQuestId);
questController.SetNextQuest(null);
}
return true;
}
- public ETaskResult Update() => ETaskResult.TaskComplete;
-
- public override string ToString() => $"SetNextQuest({nextQuestId})";
+ public override ETaskResult Update() => ETaskResult.TaskComplete;
}
}
namespace Questionable.Controller.Steps.Common;
-internal sealed class WaitConditionTask(Func<bool> predicate, string description) : ITask
+internal static class WaitCondition
{
- private DateTime _continueAt = DateTime.MaxValue;
-
- public bool Start() => !predicate();
+ internal sealed record Task(Func<bool> Predicate, string Description) : ITask
+ {
+ public override string ToString() => Description;
+ }
- public ETaskResult Update()
+ internal sealed class Executor : TaskExecutor<Task>
{
- if (_continueAt == DateTime.MaxValue)
+ private DateTime _continueAt = DateTime.MaxValue;
+
+ protected override bool Start() => !Task.Predicate();
+
+ public override ETaskResult Update()
{
- if (predicate())
- _continueAt = DateTime.Now.AddSeconds(0.5);
- }
+ if (_continueAt == DateTime.MaxValue)
+ {
+ if (Task.Predicate())
+ _continueAt = DateTime.Now.AddSeconds(0.5);
+ }
- return DateTime.Now >= _continueAt ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
+ return DateTime.Now >= _continueAt ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
+ }
}
-
- public override string ToString() => description;
}
namespace Questionable.Controller.Steps.Gathering;
-internal sealed class DoGather(
- GatheringController.GatheringRequest currentRequest,
- GatheringNode currentNode,
- bool revisitRequired,
- GatheringController gatheringController,
- GameFunctions gameFunctions,
- IGameGui gameGui,
- IClientState clientState,
- ICondition condition,
- ILogger<DoGather> logger) : ITask, IRevisitAware
+internal static class DoGather
{
- private const uint StatusGatheringRateUp = 218;
+ internal sealed record Task(
+ GatheringController.GatheringRequest Request,
+ GatheringNode Node,
+ bool RevisitRequired) : ITask, IRevisitAware
+ {
+ public bool RevisitTriggered { get; private set; }
- private bool _revisitTriggered;
- private bool _wasGathering;
- private SlotInfo? _slotToGather;
- private Queue<EAction>? _actionQueue;
+ public void OnRevisit() => RevisitTriggered = true;
- public bool Start() => true;
+ public override string ToString() => $"DoGather{(RevisitRequired ? " if revist" : "")}";
+ }
- public unsafe ETaskResult Update()
+ internal sealed class Executor(
+ GatheringController gatheringController,
+ GameFunctions gameFunctions,
+ IGameGui gameGui,
+ IClientState clientState,
+ ICondition condition,
+ ILogger<Executor> logger) : TaskExecutor<Task>
{
- if (revisitRequired && !_revisitTriggered)
- {
- logger.LogInformation("No revisit");
- return ETaskResult.TaskComplete;
- }
+ private const uint StatusGatheringRateUp = 218;
- if (gatheringController.HasNodeDisappeared(currentNode))
- {
- logger.LogInformation("Node disappeared");
- return ETaskResult.TaskComplete;
- }
+ private bool _wasGathering;
+ private SlotInfo? _slotToGather;
+ private Queue<EAction>? _actionQueue;
- if (gameFunctions.GetFreeInventorySlots() == 0)
- throw new TaskException("Inventory full");
+ protected override bool Start() => true;
- if (condition[ConditionFlag.Gathering])
+ public override unsafe ETaskResult Update()
{
- if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* _))
+ if (Task is { RevisitRequired: true, RevisitTriggered: false })
+ {
+ logger.LogInformation("No revisit");
return ETaskResult.TaskComplete;
+ }
- _wasGathering = true;
+ if (gatheringController.HasNodeDisappeared(Task.Node))
+ {
+ logger.LogInformation("Node disappeared");
+ return ETaskResult.TaskComplete;
+ }
- if (gameGui.TryGetAddonByName("Gathering", out AddonGathering* addonGathering))
+ if (gameFunctions.GetFreeInventorySlots() == 0)
+ throw new TaskException("Inventory full");
+
+ if (condition[ConditionFlag.Gathering])
{
- if (gatheringController.HasRequestedItems())
- {
- addonGathering->FireCallbackInt(-1);
- }
- else
+ if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* _))
+ return ETaskResult.TaskComplete;
+
+ _wasGathering = true;
+
+ if (gameGui.TryGetAddonByName("Gathering", out AddonGathering* addonGathering))
{
- var slots = ReadSlots(addonGathering);
- if (currentRequest.Collectability > 0)
+ if (gatheringController.HasRequestedItems())
{
- var slot = slots.Single(x => x.ItemId == currentRequest.ItemId);
- addonGathering->FireCallbackInt(slot.Index);
+ addonGathering->FireCallbackInt(-1);
}
else
{
- NodeCondition nodeCondition = new NodeCondition(
- addonGathering->AtkValues[110].UInt,
- addonGathering->AtkValues[111].UInt);
-
- if (_actionQueue != null && _actionQueue.TryPeek(out EAction nextAction))
+ var slots = ReadSlots(addonGathering);
+ if (Task.Request.Collectability > 0)
{
- if (gameFunctions.UseAction(nextAction))
+ var slot = slots.Single(x => x.ItemId == Task.Request.ItemId);
+ addonGathering->FireCallbackInt(slot.Index);
+ }
+ else
+ {
+ NodeCondition nodeCondition = new NodeCondition(
+ addonGathering->AtkValues[110].UInt,
+ addonGathering->AtkValues[111].UInt);
+
+ if (_actionQueue != null && _actionQueue.TryPeek(out EAction nextAction))
{
- logger.LogInformation("Used action {Action} on node", nextAction);
- _actionQueue.Dequeue();
- }
+ if (gameFunctions.UseAction(nextAction))
+ {
+ logger.LogInformation("Used action {Action} on node", nextAction);
+ _actionQueue.Dequeue();
+ }
- return ETaskResult.StillRunning;
- }
+ return ETaskResult.StillRunning;
+ }
- _actionQueue = GetNextActions(nodeCondition, slots);
- if (_actionQueue.Count == 0)
- {
- var slot = _slotToGather ?? slots.Single(x => x.ItemId == currentRequest.ItemId);
- addonGathering->FireCallbackInt(slot.Index);
+ _actionQueue = GetNextActions(nodeCondition, slots);
+ if (_actionQueue.Count == 0)
+ {
+ var slot = _slotToGather ?? slots.Single(x => x.ItemId == Task.Request.ItemId);
+ addonGathering->FireCallbackInt(slot.Index);
+ }
}
}
}
}
- }
- return _wasGathering && !condition[ConditionFlag.Gathering]
- ? ETaskResult.TaskComplete
- : ETaskResult.StillRunning;
- }
+ return _wasGathering && !condition[ConditionFlag.Gathering]
+ ? ETaskResult.TaskComplete
+ : ETaskResult.StillRunning;
+ }
- private unsafe List<SlotInfo> ReadSlots(AddonGathering* addonGathering)
- {
- var atkValues = addonGathering->AtkValues;
- List<SlotInfo> slots = new List<SlotInfo>();
- for (int i = 0; i < 8; ++i)
+ private unsafe List<SlotInfo> ReadSlots(AddonGathering* addonGathering)
{
- // +8 = new item?
- uint itemId = atkValues[i * 11 + 7].UInt;
- if (itemId == 0)
- continue;
-
- AtkComponentCheckBox* atkCheckbox = addonGathering->GatheredItemComponentCheckbox[i].Value;
+ var atkValues = addonGathering->AtkValues;
+ List<SlotInfo> slots = new List<SlotInfo>();
+ for (int i = 0; i < 8; ++i)
+ {
+ // +8 = new item?
+ uint itemId = atkValues[i * 11 + 7].UInt;
+ if (itemId == 0)
+ continue;
- AtkTextNode* atkGatheringChance = atkCheckbox->UldManager.SearchNodeById(10)->GetAsAtkTextNode();
- if (!int.TryParse(atkGatheringChance->NodeText.ToString(), out int gatheringChance))
- gatheringChance = 0;
+ AtkComponentCheckBox* atkCheckbox = addonGathering->GatheredItemComponentCheckbox[i].Value;
- AtkTextNode* atkBoonChance = atkCheckbox->UldManager.SearchNodeById(16)->GetAsAtkTextNode();
- if (!int.TryParse(atkBoonChance->NodeText.ToString(), out int boonChance))
- boonChance = 0;
+ AtkTextNode* atkGatheringChance = atkCheckbox->UldManager.SearchNodeById(10)->GetAsAtkTextNode();
+ if (!int.TryParse(atkGatheringChance->NodeText.ToString(), out int gatheringChance))
+ gatheringChance = 0;
- AtkComponentNode* atkImage = atkCheckbox->UldManager.SearchNodeById(31)->GetAsAtkComponentNode();
- AtkTextNode* atkQuantity = atkImage->Component->UldManager.SearchNodeById(7)->GetAsAtkTextNode();
- if (!atkQuantity->IsVisible() || !int.TryParse(atkQuantity->NodeText.ToString(), out int quantity))
- quantity = 1;
+ AtkTextNode* atkBoonChance = atkCheckbox->UldManager.SearchNodeById(16)->GetAsAtkTextNode();
+ if (!int.TryParse(atkBoonChance->NodeText.ToString(), out int boonChance))
+ boonChance = 0;
- var slot = new SlotInfo(i, itemId, gatheringChance, boonChance, quantity);
- slots.Add(slot);
- }
+ AtkComponentNode* atkImage = atkCheckbox->UldManager.SearchNodeById(31)->GetAsAtkComponentNode();
+ AtkTextNode* atkQuantity = atkImage->Component->UldManager.SearchNodeById(7)->GetAsAtkTextNode();
+ if (!atkQuantity->IsVisible() || !int.TryParse(atkQuantity->NodeText.ToString(), out int quantity))
+ quantity = 1;
- return slots;
- }
+ var slot = new SlotInfo(i, itemId, gatheringChance, boonChance, quantity);
+ slots.Add(slot);
+ }
- [SuppressMessage("ReSharper", "UnusedParameter.Local")]
- private Queue<EAction> GetNextActions(NodeCondition nodeCondition, List<SlotInfo> slots)
- {
- //uint gp = clientState.LocalPlayer!.CurrentGp;
- Queue<EAction> actions = new();
+ return slots;
+ }
- if (!gameFunctions.HasStatus(StatusGatheringRateUp))
+ [SuppressMessage("ReSharper", "UnusedParameter.Local")]
+ private Queue<EAction> GetNextActions(NodeCondition nodeCondition, List<SlotInfo> slots)
{
- // do we have an alternative item? only happens for 'evaluation' leve quests
- if (currentRequest.AlternativeItemId != 0)
- {
- var alternativeSlot = slots.Single(x => x.ItemId == currentRequest.AlternativeItemId);
+ //uint gp = clientState.LocalPlayer!.CurrentGp;
+ Queue<EAction> actions = new();
- if (alternativeSlot.GatheringChance == 100)
+ if (!gameFunctions.HasStatus(StatusGatheringRateUp))
+ {
+ // do we have an alternative item? only happens for 'evaluation' leve quests
+ if (Task.Request.AlternativeItemId != 0)
{
- _slotToGather = alternativeSlot;
- return actions;
+ var alternativeSlot = slots.Single(x => x.ItemId == Task.Request.AlternativeItemId);
+
+ if (alternativeSlot.GatheringChance == 100)
+ {
+ _slotToGather = alternativeSlot;
+ return actions;
+ }
+
+ if (alternativeSlot.GatheringChance > 0)
+ {
+ if (alternativeSlot.GatheringChance >= 95 &&
+ CanUseAction(EAction.SharpVision1, EAction.FieldMastery1))
+ {
+ _slotToGather = alternativeSlot;
+ actions.Enqueue(PickAction(EAction.SharpVision1, EAction.FieldMastery1));
+ return actions;
+ }
+
+ if (alternativeSlot.GatheringChance >= 85 &&
+ CanUseAction(EAction.SharpVision2, EAction.FieldMastery2))
+ {
+ _slotToGather = alternativeSlot;
+ actions.Enqueue(PickAction(EAction.SharpVision2, EAction.FieldMastery2));
+ return actions;
+ }
+
+ if (alternativeSlot.GatheringChance >= 50 &&
+ CanUseAction(EAction.SharpVision3, EAction.FieldMastery3))
+ {
+ _slotToGather = alternativeSlot;
+ actions.Enqueue(PickAction(EAction.SharpVision3, EAction.FieldMastery3));
+ return actions;
+ }
+ }
}
- if (alternativeSlot.GatheringChance > 0)
+ var slot = slots.Single(x => x.ItemId == Task.Request.ItemId);
+ if (slot.GatheringChance > 0 && slot.GatheringChance < 100)
{
- if (alternativeSlot.GatheringChance >= 95 &&
+ if (slot.GatheringChance >= 95 &&
CanUseAction(EAction.SharpVision1, EAction.FieldMastery1))
{
- _slotToGather = alternativeSlot;
actions.Enqueue(PickAction(EAction.SharpVision1, EAction.FieldMastery1));
return actions;
}
- if (alternativeSlot.GatheringChance >= 85 &&
+ if (slot.GatheringChance >= 85 &&
CanUseAction(EAction.SharpVision2, EAction.FieldMastery2))
{
- _slotToGather = alternativeSlot;
actions.Enqueue(PickAction(EAction.SharpVision2, EAction.FieldMastery2));
return actions;
}
- if (alternativeSlot.GatheringChance >= 50 &&
+ if (slot.GatheringChance >= 50 &&
CanUseAction(EAction.SharpVision3, EAction.FieldMastery3))
{
- _slotToGather = alternativeSlot;
actions.Enqueue(PickAction(EAction.SharpVision3, EAction.FieldMastery3));
return actions;
}
}
}
- var slot = slots.Single(x => x.ItemId == currentRequest.ItemId);
- if (slot.GatheringChance > 0 && slot.GatheringChance < 100)
- {
- if (slot.GatheringChance >= 95 &&
- CanUseAction(EAction.SharpVision1, EAction.FieldMastery1))
- {
- actions.Enqueue(PickAction(EAction.SharpVision1, EAction.FieldMastery1));
- return actions;
- }
-
- if (slot.GatheringChance >= 85 &&
- CanUseAction(EAction.SharpVision2, EAction.FieldMastery2))
- {
- actions.Enqueue(PickAction(EAction.SharpVision2, EAction.FieldMastery2));
- return actions;
- }
-
- if (slot.GatheringChance >= 50 &&
- CanUseAction(EAction.SharpVision3, EAction.FieldMastery3))
- {
- actions.Enqueue(PickAction(EAction.SharpVision3, EAction.FieldMastery3));
- return actions;
- }
- }
+ return actions;
}
- return actions;
- }
-
- private EAction PickAction(EAction minerAction, EAction botanistAction)
- {
- if ((EClassJob?)clientState.LocalPlayer?.ClassJob.Id == EClassJob.Miner)
- return minerAction;
- else
- return botanistAction;
- }
-
- private unsafe bool CanUseAction(EAction minerAction, EAction botanistAction)
- {
- EAction action = PickAction(minerAction, botanistAction);
- return ActionManager.Instance()->GetActionStatus(ActionType.Action, (uint)action) == 0;
- }
+ private EAction PickAction(EAction minerAction, EAction botanistAction)
+ {
+ if ((EClassJob?)clientState.LocalPlayer?.ClassJob.Id == EClassJob.Miner)
+ return minerAction;
+ else
+ return botanistAction;
+ }
- public void OnRevisit()
- {
- _revisitTriggered = true;
+ private unsafe bool CanUseAction(EAction minerAction, EAction botanistAction)
+ {
+ EAction action = PickAction(minerAction, botanistAction);
+ return ActionManager.Instance()->GetActionStatus(ActionType.Action, (uint)action) == 0;
+ }
}
- public override string ToString() => $"DoGather{(revisitRequired ? " if revist" : "")}";
-
[SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Local")]
private sealed record SlotInfo(int Index, uint ItemId, int GatheringChance, int BoonChance, int Quantity);
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Dalamud.Game.Text;
using Dalamud.Plugin.Services;
namespace Questionable.Controller.Steps.Gathering;
-internal sealed class DoGatherCollectable(
- GatheringController.GatheringRequest currentRequest,
- GatheringNode currentNode,
- bool revisitRequired,
- GatheringController gatheringController,
- GameFunctions gameFunctions,
- IClientState clientState,
- IGameGui gameGui,
- ILogger<DoGatherCollectable> logger) : ITask, IRevisitAware
+internal static class DoGatherCollectable
{
- private bool _revisitTriggered;
- private Queue<EAction>? _actionQueue;
+ internal sealed record Task(
+ GatheringController.GatheringRequest Request,
+ GatheringNode Node,
+ bool RevisitRequired) : ITask, IRevisitAware
+ {
+ public bool RevisitTriggered { get; private set; }
- private bool? _expectedScrutiny;
+ public void OnRevisit() => RevisitTriggered = true;
- public bool Start() => true;
+ public override string ToString() =>
+ $"DoGatherCollectable({SeIconChar.Collectible.ToIconString()}/{Request.Collectability}){(RevisitRequired ? " if revist" : "")}";
+ }
- public unsafe ETaskResult Update()
+ internal sealed class Executor(
+ GatheringController gatheringController,
+ GameFunctions gameFunctions,
+ IClientState clientState,
+ IGameGui gameGui,
+ ILogger<Executor> logger) : TaskExecutor<Task>
{
- if (revisitRequired && !_revisitTriggered)
- {
- logger.LogInformation("No revisit");
- return ETaskResult.TaskComplete;
- }
+ private Queue<EAction>? _actionQueue;
- if (gatheringController.HasNodeDisappeared(currentNode))
- {
- logger.LogInformation("Node disappeared");
- return ETaskResult.TaskComplete;
- }
+ private bool? _expectedScrutiny;
- if (gatheringController.HasRequestedItems())
+ protected override bool Start() => true;
+
+ public override unsafe ETaskResult Update()
{
- if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* atkUnitBase))
+ if (Task.RevisitRequired && !Task.RevisitTriggered)
{
- atkUnitBase->FireCallbackInt(1);
- return ETaskResult.StillRunning;
+ logger.LogInformation("No revisit");
+ return ETaskResult.TaskComplete;
}
- if (gameGui.TryGetAddonByName("Gathering", out atkUnitBase))
+ if (gatheringController.HasNodeDisappeared(Task.Node))
{
- atkUnitBase->FireCallbackInt(-1);
+ logger.LogInformation("Node disappeared");
return ETaskResult.TaskComplete;
}
- }
- if (gameFunctions.GetFreeInventorySlots() == 0)
- throw new TaskException("Inventory full");
+ if (gatheringController.HasRequestedItems())
+ {
+ if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* atkUnitBase))
+ {
+ atkUnitBase->FireCallbackInt(1);
+ return ETaskResult.StillRunning;
+ }
- NodeCondition? nodeCondition = GetNodeCondition();
- if (nodeCondition == null)
- return ETaskResult.TaskComplete;
+ if (gameGui.TryGetAddonByName("Gathering", out atkUnitBase))
+ {
+ atkUnitBase->FireCallbackInt(-1);
+ return ETaskResult.TaskComplete;
+ }
+ }
- if (_expectedScrutiny != null)
- {
- if (nodeCondition.ScrutinyActive != _expectedScrutiny)
+ if (gameFunctions.GetFreeInventorySlots() == 0)
+ throw new TaskException("Inventory full");
+
+ NodeCondition? nodeCondition = GetNodeCondition();
+ if (nodeCondition == null)
+ return ETaskResult.TaskComplete;
+
+ if (_expectedScrutiny != null)
+ {
+ if (nodeCondition.ScrutinyActive != _expectedScrutiny)
+ return ETaskResult.StillRunning;
+
+ // continue on next frame
+ _expectedScrutiny = null;
return ETaskResult.StillRunning;
+ }
- // continue on next frame
- _expectedScrutiny = null;
- return ETaskResult.StillRunning;
- }
+ if (_actionQueue != null && _actionQueue.TryPeek(out EAction nextAction))
+ {
+ if (gameFunctions.UseAction(nextAction))
+ {
+ _expectedScrutiny = nextAction switch
+ {
+ EAction.ScrutinyMiner or EAction.ScrutinyBotanist => true,
+ EAction.ScourMiner or EAction.ScourBotanist or EAction.MeticulousMiner
+ or EAction.MeticulousBotanist => false,
+ _ => null
+ };
+ logger.LogInformation("Used action {Action} on node", nextAction);
+ _actionQueue.Dequeue();
+ }
- if (_actionQueue != null && _actionQueue.TryPeek(out EAction nextAction))
- {
- if (gameFunctions.UseAction(nextAction))
+ return ETaskResult.StillRunning;
+ }
+
+ if (nodeCondition.CollectabilityToGoal(Task.Request.Collectability) > 0)
{
- _expectedScrutiny = nextAction switch
+ _actionQueue = GetNextActions(nodeCondition);
+ if (_actionQueue != null)
{
- EAction.ScrutinyMiner or EAction.ScrutinyBotanist => true,
- EAction.ScourMiner or EAction.ScourBotanist or EAction.MeticulousMiner
- or EAction.MeticulousBotanist => false,
- _ => null
- };
- logger.LogInformation("Used action {Action} on node", nextAction);
- _actionQueue.Dequeue();
+ foreach (var action in _actionQueue)
+ logger.LogInformation("Next Actions {Action}", action);
+ return ETaskResult.StillRunning;
+ }
}
+ _actionQueue = new Queue<EAction>();
+ _actionQueue.Enqueue(PickAction(EAction.CollectMiner, EAction.CollectBotanist));
return ETaskResult.StillRunning;
}
- if (nodeCondition.CollectabilityToGoal(currentRequest.Collectability) > 0)
+ private unsafe NodeCondition? GetNodeCondition()
{
- _actionQueue = GetNextActions(nodeCondition);
- if (_actionQueue != null)
+ if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* atkUnitBase))
{
- foreach (var action in _actionQueue)
- logger.LogInformation("Next Actions {Action}", action);
- return ETaskResult.StillRunning;
+ var atkValues = atkUnitBase->AtkValues;
+ return new NodeCondition(
+ CurrentCollectability: atkValues[13].UInt,
+ MaxCollectability: atkValues[14].UInt,
+ CurrentIntegrity: atkValues[62].UInt,
+ MaxIntegrity: atkValues[63].UInt,
+ ScrutinyActive: atkValues[54].Bool,
+ CollectabilityFromScour: atkValues[48].UInt,
+ CollectabilityFromMeticulous: atkValues[51].UInt
+ );
}
- }
-
- _actionQueue = new Queue<EAction>();
- _actionQueue.Enqueue(PickAction(EAction.CollectMiner, EAction.CollectBotanist));
- return ETaskResult.StillRunning;
- }
- private unsafe NodeCondition? GetNodeCondition()
- {
- if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* atkUnitBase))
- {
- var atkValues = atkUnitBase->AtkValues;
- return new NodeCondition(
- CurrentCollectability: atkValues[13].UInt,
- MaxCollectability: atkValues[14].UInt,
- CurrentIntegrity: atkValues[62].UInt,
- MaxIntegrity: atkValues[63].UInt,
- ScrutinyActive: atkValues[54].Bool,
- CollectabilityFromScour: atkValues[48].UInt,
- CollectabilityFromMeticulous: atkValues[51].UInt
- );
+ return null;
}
- return null;
- }
+ private Queue<EAction> GetNextActions(NodeCondition nodeCondition)
+ {
+ uint gp = clientState.LocalPlayer!.CurrentGp;
+ logger.LogTrace(
+ "Getting next actions (with {GP} GP, {MeticulousCollectability}~ meticulous, {ScourCollectability}~ scour)",
+ gp, nodeCondition.CollectabilityFromMeticulous, nodeCondition.CollectabilityFromScour);
- private Queue<EAction> GetNextActions(NodeCondition nodeCondition)
- {
- uint gp = clientState.LocalPlayer!.CurrentGp;
- logger.LogTrace(
- "Getting next actions (with {GP} GP, {MeticulousCollectability}~ meticulous, {ScourCollectability}~ scour)",
- gp, nodeCondition.CollectabilityFromMeticulous, nodeCondition.CollectabilityFromScour);
+ Queue<EAction> actions = new();
- Queue<EAction> actions = new();
+ uint neededCollectability = nodeCondition.CollectabilityToGoal(Task.Request.Collectability);
+ if (neededCollectability <= nodeCondition.CollectabilityFromMeticulous)
+ {
+ logger.LogTrace("Can get all needed {NeededCollectability} from {Collectability}~ meticulous",
+ neededCollectability, nodeCondition.CollectabilityFromMeticulous);
+ actions.Enqueue(PickAction(EAction.MeticulousMiner, EAction.MeticulousBotanist));
+ return actions;
+ }
- uint neededCollectability = nodeCondition.CollectabilityToGoal(currentRequest.Collectability);
- if (neededCollectability <= nodeCondition.CollectabilityFromMeticulous)
- {
- logger.LogTrace("Can get all needed {NeededCollectability} from {Collectability}~ meticulous",
- neededCollectability, nodeCondition.CollectabilityFromMeticulous);
- actions.Enqueue(PickAction(EAction.MeticulousMiner, EAction.MeticulousBotanist));
- return actions;
- }
+ if (neededCollectability <= nodeCondition.CollectabilityFromScour)
+ {
+ logger.LogTrace("Can get all needed {NeededCollectability} from {Collectability}~ scour",
+ neededCollectability, nodeCondition.CollectabilityFromScour);
+ actions.Enqueue(PickAction(EAction.ScourMiner, EAction.ScourBotanist));
+ return actions;
+ }
- if (neededCollectability <= nodeCondition.CollectabilityFromScour)
- {
- logger.LogTrace("Can get all needed {NeededCollectability} from {Collectability}~ scour",
- neededCollectability, nodeCondition.CollectabilityFromScour);
- actions.Enqueue(PickAction(EAction.ScourMiner, EAction.ScourBotanist));
- return actions;
- }
+ // neither action directly solves our problem
+ if (!nodeCondition.ScrutinyActive && gp >= 200)
+ {
+ logger.LogTrace("Still missing {NeededCollectability} collectability, scrutiny inactive",
+ neededCollectability);
+ actions.Enqueue(PickAction(EAction.ScrutinyMiner, EAction.ScrutinyBotanist));
+ return actions;
+ }
- // neither action directly solves our problem
- if (!nodeCondition.ScrutinyActive && gp >= 200)
- {
- logger.LogTrace("Still missing {NeededCollectability} collectability, scrutiny inactive",
- neededCollectability);
- actions.Enqueue(PickAction(EAction.ScrutinyMiner, EAction.ScrutinyBotanist));
- return actions;
+ if (nodeCondition.ScrutinyActive)
+ {
+ logger.LogTrace(
+ "Scrutiny active, need {NeededCollectability} and we expect {Collectability}~ meticulous",
+ neededCollectability, nodeCondition.CollectabilityFromMeticulous);
+ actions.Enqueue(PickAction(EAction.MeticulousMiner, EAction.MeticulousBotanist));
+ return actions;
+ }
+ else
+ {
+ logger.LogTrace("Scrutiny active, need {NeededCollectability} and we expect {Collectability}~ scour",
+ neededCollectability, nodeCondition.CollectabilityFromScour);
+ actions.Enqueue(PickAction(EAction.ScourMiner, EAction.ScourBotanist));
+ return actions;
+ }
}
- if (nodeCondition.ScrutinyActive)
- {
- logger.LogTrace("Scrutiny active, need {NeededCollectability} and we expect {Collectability}~ meticulous",
- neededCollectability, nodeCondition.CollectabilityFromMeticulous);
- actions.Enqueue(PickAction(EAction.MeticulousMiner, EAction.MeticulousBotanist));
- return actions;
- }
- else
+ private EAction PickAction(EAction minerAction, EAction botanistAction)
{
- logger.LogTrace("Scrutiny active, need {NeededCollectability} and we expect {Collectability}~ scour",
- neededCollectability, nodeCondition.CollectabilityFromScour);
- actions.Enqueue(PickAction(EAction.ScourMiner, EAction.ScourBotanist));
- return actions;
+ if ((EClassJob?)clientState.LocalPlayer?.ClassJob.Id == EClassJob.Miner)
+ return minerAction;
+ else
+ return botanistAction;
}
}
- private EAction PickAction(EAction minerAction, EAction botanistAction)
- {
- if ((EClassJob?)clientState.LocalPlayer?.ClassJob.Id == EClassJob.Miner)
- return minerAction;
- else
- return botanistAction;
- }
-
- public void OnRevisit()
- {
- _revisitTriggered = true;
- }
-
- public override string ToString() =>
- $"DoGatherCollectable({SeIconChar.Collectible.ToIconString()}/{_expectedScrutiny} {currentRequest.Collectability}){(revisitRequired ? " if revist" : "")}";
-
[SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Local")]
private sealed record NodeCondition(
uint CurrentCollectability,
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;
namespace Questionable.Controller.Steps.Gathering;
-internal sealed class MoveToLandingLocation(
- ushort territoryId,
- bool flyBetweenNodes,
- GatheringNode gatheringNode,
- MoveTo.Factory moveFactory,
- GameFunctions gameFunctions,
- IObjectTable objectTable,
- ILogger<MoveToLandingLocation> logger) : ITask
+internal static class MoveToLandingLocation
{
- private ITask _moveTask = null!;
+ internal sealed record Task(
+ ushort TerritoryId,
+ bool FlyBetweenNodes,
+ GatheringNode GatheringNode) : ITask
+ {
+ public override string ToString() => $"Land/{FlyBetweenNodes}";
+ }
- public bool Start()
+ internal sealed class Executor(
+ MoveTo.MoveExecutor moveExecutor,
+ GameFunctions gameFunctions,
+ IObjectTable objectTable,
+ ILogger<Executor> logger) : TaskExecutor<Task>
{
- var location = gatheringNode.Locations.First();
- if (gatheringNode.Locations.Count > 1)
- {
- var gameObject = objectTable.SingleOrDefault(x =>
- x.ObjectKind == ObjectKind.GatheringPoint && x.DataId == gatheringNode.DataId && x.IsTargetable);
- if (gameObject == null)
- return false;
+ private ITask _moveTask = null!;
- location = gatheringNode.Locations.Single(x => Vector3.Distance(x.Position, gameObject.Position) < 0.1f);
+ protected override bool Start()
+ {
+ var location = Task.GatheringNode.Locations.First();
+ if (Task.GatheringNode.Locations.Count > 1)
+ {
+ var gameObject = objectTable.SingleOrDefault(x =>
+ x.ObjectKind == ObjectKind.GatheringPoint && x.DataId == Task.GatheringNode.DataId &&
+ x.IsTargetable);
+ if (gameObject == null)
+ return false;
+
+ location = Task.GatheringNode.Locations.Single(x =>
+ Vector3.Distance(x.Position, gameObject.Position) < 0.1f);
+ }
+
+ var (target, degrees, range) = GatheringMath.CalculateLandingLocation(location);
+ logger.LogInformation("Preliminary landing location: {Location}, with degrees = {Degrees}, range = {Range}",
+ target.ToString("G", CultureInfo.InvariantCulture), degrees, range);
+
+ bool fly = Task.FlyBetweenNodes && gameFunctions.IsFlyingUnlocked(Task.TerritoryId);
+ _moveTask = new MoveTo.MoveTask(Task.TerritoryId, target, null, 0.25f,
+ DataId: Task.GatheringNode.DataId, Fly: fly, IgnoreDistanceToObject: true);
+ return moveExecutor.Start(_moveTask);
}
- var (target, degrees, range) = GatheringMath.CalculateLandingLocation(location);
- logger.LogInformation("Preliminary landing location: {Location}, with degrees = {Degrees}, range = {Range}",
- target.ToString("G", CultureInfo.InvariantCulture), degrees, range);
-
- bool fly = flyBetweenNodes && gameFunctions.IsFlyingUnlocked(territoryId);
- _moveTask = moveFactory.Move(new MoveTo.MoveParams(territoryId, target, null, 0.25f,
- DataId: gatheringNode.DataId, Fly: fly, IgnoreDistanceToObject: true));
- return _moveTask.Start();
+ public override ETaskResult Update() => moveExecutor.Update();
}
-
- public ETaskResult Update() => _moveTask.Update();
-
- public override string ToString() => $"Land/{_moveTask}/{flyBetweenNodes}";
}
-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;
internal static class TurnInDelivery
{
- internal sealed class Factory(ILoggerFactory loggerFactory) : SimpleTaskFactory
+ internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (quest.Id is not SatisfactionSupplyNpcId || sequence.Sequence != 1)
return null;
- return new SatisfactionSupplyTurnIn(loggerFactory.CreateLogger<SatisfactionSupplyTurnIn>());
+ return new Task();
}
}
- private sealed class SatisfactionSupplyTurnIn(ILogger<SatisfactionSupplyTurnIn> logger) : ITask
+ internal sealed record Task : ITask
+ {
+ public override string ToString() => "WeeklyDeliveryTurnIn";
+ }
+
+ internal sealed class SatisfactionSupplyTurnIn(ILogger<SatisfactionSupplyTurnIn> logger) : TaskExecutor<Task>
{
private ushort? _remainingAllowances;
- public bool Start() => true;
+ protected override bool Start() => true;
- public unsafe ETaskResult Update()
+ public override unsafe ETaskResult Update()
{
AgentSatisfactionSupply* agentSatisfactionSupply = AgentSatisfactionSupply.Instance();
if (agentSatisfactionSupply == null || !agentSatisfactionSupply->IsAgentActive())
addon->FireCallback(2, pickGatheringItem);
return ETaskResult.StillRunning;
}
-
- public override string ToString() => "WeeklyDeliveryTurnIn";
}
}
namespace Questionable.Controller.Steps;
-public interface IConditionChangeAware
+internal interface IConditionChangeAware : ITaskExecutor
{
void OnConditionChange(ConditionFlag flag, bool value);
}
namespace Questionable.Controller.Steps;
-public interface IRevisitAware
+internal interface IRevisitAware : ITask
{
void OnRevisit();
}
-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();
}
namespace Questionable.Controller.Steps;
-public interface IToastAware
+internal interface IToastAware : ITaskExecutor
{
bool OnErrorToast(SeString message);
}
internal static class Action
{
- internal sealed class Factory(GameFunctions gameFunctions, Mount.Factory mountFactory, ILoggerFactory loggerFactory)
- : ITaskFactory
+ internal sealed class Factory : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.Action.Value.RequiresMount())
return [task];
else
- return [mountFactory.Unmount(), task];
+ return [new Mount.UnmountTask(), task];
}
- public ITask OnObject(uint? dataId, EAction action)
+ public static ITask OnObject(uint? dataId, EAction action)
{
- return new UseOnObject(dataId, action, gameFunctions,
- loggerFactory.CreateLogger<UseOnObject>());
+ return new UseOnObject(dataId, action);
}
}
- private sealed class UseOnObject(
- uint? dataId,
- EAction action,
+ internal sealed record UseOnObject(
+ uint? DataId,
+ EAction Action) : ITask
+ {
+ public override string ToString() => $"Action({Action})";
+ }
+
+ internal sealed class UseOnObjectExecutor(
GameFunctions gameFunctions,
- ILogger<UseOnObject> logger) : ITask
+ ILogger<UseOnObject> logger) : TaskExecutor<UseOnObject>
{
private bool _usedAction;
private DateTime _continueAt = DateTime.MinValue;
- public bool Start()
+ protected override bool Start()
{
- if (dataId != null)
+ if (Task.DataId != null)
{
- IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId.Value);
+ IGameObject? gameObject = gameFunctions.FindObjectByDataId(Task.DataId.Value);
if (gameObject == null)
{
- logger.LogWarning("No game object with dataId {DataId}", dataId);
+ logger.LogWarning("No game object with dataId {DataId}", Task.DataId);
return false;
}
if (gameObject.IsTargetable)
{
- if (action == EAction.Diagnosis)
+ if (Task.Action == EAction.Diagnosis)
{
uint eukrasiaAura = 2606;
// If SGE have Eukrasia status, we need to remove it.
}
}
- _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;
}
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);
}
return ETaskResult.TaskComplete;
}
-
- public override string ToString() => $"Action({action})";
}
-}
\ No newline at end of file
+}
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)
{
return null;
}
- return new DoAttune(step.DataId.Value, step.AetherCurrentId.Value, gameFunctions,
- loggerFactory.CreateLogger<DoAttune>());
+ return new Attune(step.DataId.Value, step.AetherCurrentId.Value);
}
}
- private sealed class DoAttune(
- uint dataId,
- uint aetherCurrentId,
- GameFunctions gameFunctions,
- ILogger<DoAttune> logger) : ITask
+ internal sealed record Attune(uint DataId, uint AetherCurrentId) : ITask
{
- private InteractionProgressContext? _progressContext;
-
- public InteractionProgressContext? ProgressContext() => _progressContext;
+ public override string ToString() => $"AttuneAetherCurrent({AetherCurrentId})";
+ }
- public bool Start()
+ internal sealed class DoAttune(
+ GameFunctions gameFunctions,
+ ILogger<DoAttune> logger) : TaskExecutor<Attune>
+ {
+ protected override bool Start()
{
- if (!gameFunctions.IsAetherCurrentUnlocked(aetherCurrentId))
+ if (!gameFunctions.IsAetherCurrentUnlocked(Task.AetherCurrentId))
{
- logger.LogInformation("Attuning to aether current {AetherCurrentId} / {DataId}", aetherCurrentId,
- dataId);
- _progressContext =
- InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(dataId));
+ logger.LogInformation("Attuning to aether current {AetherCurrentId} / {DataId}", Task.AetherCurrentId,
+ Task.DataId);
+ ProgressContext =
+ InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(Task.DataId));
return true;
}
- logger.LogInformation("Already attuned to aether current {AetherCurrentId} / {DataId}", aetherCurrentId,
- dataId);
+ logger.LogInformation("Already attuned to aether current {AetherCurrentId} / {DataId}",
+ Task.AetherCurrentId,
+ Task.DataId);
return false;
}
- public ETaskResult Update() =>
- gameFunctions.IsAetherCurrentUnlocked(aetherCurrentId)
+ public override ETaskResult Update() =>
+ gameFunctions.IsAetherCurrentUnlocked(Task.AetherCurrentId)
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
-
- public override string ToString() => $"AttuneAetherCurrent({aetherCurrentId})";
}
}
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)
{
ArgumentNullException.ThrowIfNull(step.AethernetShard);
- return new DoAttune(step.AethernetShard.Value, aetheryteFunctions, gameFunctions,
- loggerFactory.CreateLogger<DoAttune>());
+ return new Attune(step.AethernetShard.Value);
}
}
- private sealed class DoAttune(
- EAetheryteLocation aetheryteLocation,
+ internal sealed record Attune(EAetheryteLocation AetheryteLocation) : ITask
+ {
+ public override string ToString() => $"AttuneAethernetShard({AetheryteLocation})";
+ }
+
+ internal sealed class DoAttune(
AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions,
- ILogger<DoAttune> logger) : ITask
+ ILogger<DoAttune> logger) : TaskExecutor<Attune>
{
- private InteractionProgressContext? _progressContext;
-
- public InteractionProgressContext? ProgressContext() => _progressContext;
-
- public bool Start()
+ protected override bool Start()
{
- if (!aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation))
+ if (!aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation))
{
- logger.LogInformation("Attuning to aethernet shard {AethernetShard}", aetheryteLocation);
- _progressContext = InteractionProgressContext.FromActionUseOrDefault(() =>
- gameFunctions.InteractWith((uint)aetheryteLocation, ObjectKind.Aetheryte));
+ logger.LogInformation("Attuning to aethernet shard {AethernetShard}", Task.AetheryteLocation);
+ ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() =>
+ gameFunctions.InteractWith((uint)Task.AetheryteLocation, ObjectKind.Aetheryte));
return true;
}
- logger.LogInformation("Already attuned to aethernet shard {AethernetShard}", aetheryteLocation);
+ logger.LogInformation("Already attuned to aethernet shard {AethernetShard}", Task.AetheryteLocation);
return false;
}
- public ETaskResult Update() =>
- aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation)
+ public override ETaskResult Update() =>
+ aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation)
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
-
- public override string ToString() => $"AttuneAethernetShard({aetheryteLocation})";
}
}
using System;
-using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Questionable.Functions;
using Questionable.Model;
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)
{
ArgumentNullException.ThrowIfNull(step.Aetheryte);
- return new DoAttune(step.Aetheryte.Value, aetheryteFunctions, gameFunctions,
- loggerFactory.CreateLogger<DoAttune>());
+ return new Attune(step.Aetheryte.Value);
}
}
- private sealed class DoAttune(
- EAetheryteLocation aetheryteLocation,
+ internal sealed record Attune(EAetheryteLocation AetheryteLocation) : ITask
+ {
+ public override string ToString() => $"AttuneAetheryte({AetheryteLocation})";
+ }
+
+ internal sealed class DoAttune(
AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions,
- ILogger<DoAttune> logger) : ITask
+ ILogger<DoAttune> logger) : TaskExecutor<Attune>
{
- private InteractionProgressContext? _progressContext;
-
- public InteractionProgressContext? ProgressContext() => _progressContext;
-
- public bool Start()
+ protected override bool Start()
{
- if (!aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation))
+ if (!aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation))
{
- logger.LogInformation("Attuning to aetheryte {Aetheryte}", aetheryteLocation);
- _progressContext =
+ logger.LogInformation("Attuning to aetheryte {Aetheryte}", Task.AetheryteLocation);
+ ProgressContext =
InteractionProgressContext.FromActionUseOrDefault(() =>
- gameFunctions.InteractWith((uint)aetheryteLocation));
+ gameFunctions.InteractWith((uint)Task.AetheryteLocation));
return true;
}
- logger.LogInformation("Already attuned to aetheryte {Aetheryte}", aetheryteLocation);
+ logger.LogInformation("Already attuned to aetheryte {Aetheryte}", Task.AetheryteLocation);
return false;
}
- public ETaskResult Update() =>
- aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation)
+ public override ETaskResult Update() =>
+ aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation)
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
-
- public override string ToString() => $"AttuneAetheryte({aetheryteLocation})";
}
}
internal static class Combat
{
- internal sealed class Factory(
- CombatController combatController,
- Interact.Factory interactFactory,
- Mount.Factory mountFactory,
- UseItem.Factory useItemFactory,
- Action.Factory actionFactory,
- QuestFunctions questFunctions,
- GameFunctions gameFunctions) : ITaskFactory
+ internal sealed class Factory(GameFunctions gameFunctions) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
ArgumentNullException.ThrowIfNull(step.EnemySpawnType);
if (gameFunctions.GetMountId() != Mount128Module.MountId)
- yield return mountFactory.Unmount();
+ yield return new Mount.UnmountTask();
if (step.CombatDelaySecondsAtStart != null)
{
{
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;
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);
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;
}
}
- public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ private static Task CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
ArgumentNullException.ThrowIfNull(step.EnemySpawnType);
step.CompletionQuestVariablesFlags, step.ComplexCombatData);
}
- internal HandleCombat CreateTask(ElementId? elementId, bool isLastStep, EEnemySpawnType enemySpawnType,
+ internal static Task CreateTask(ElementId? elementId, bool isLastStep, EEnemySpawnType enemySpawnType,
IList<uint> killEnemyDataIds, IList<QuestWorkValue?> completionQuestVariablesFlags,
IList<ComplexCombatData> complexCombatData)
{
- return new HandleCombat(isLastStep, new CombatController.CombatData
+ return new Task(new CombatController.CombatData
{
ElementId = elementId,
SpawnType = enemySpawnType,
KillEnemyDataIds = killEnemyDataIds.ToList(),
ComplexCombatDatas = complexCombatData.ToList(),
- }, completionQuestVariablesFlags, combatController, questFunctions);
+ }, completionQuestVariablesFlags, isLastStep);
+ }
+ }
+
+ internal sealed record Task(
+ CombatController.CombatData CombatData,
+ IList<QuestWorkValue?> CompletionQuestVariableFlags,
+ bool IsLastStep) : ITask
+ {
+ public override string ToString()
+ {
+ if (QuestWorkUtils.HasCompletionFlags(CompletionQuestVariableFlags))
+ return $"HandleCombat(wait: QW flags)";
+ else if (IsLastStep)
+ return $"HandleCombat(wait: next sequence)";
+ else
+ return $"HandleCombat(wait: not in combat)";
}
}
internal sealed class HandleCombat(
- bool isLastStep,
- CombatController.CombatData combatData,
- IList<QuestWorkValue?> completionQuestVariableFlags,
+
CombatController combatController,
- QuestFunctions questFunctions) : ITask
+ QuestFunctions questFunctions) : TaskExecutor<Task>
{
private CombatController.EStatus _status = CombatController.EStatus.NotStarted;
- public bool Start() => combatController.Start(combatData);
+ protected override bool Start() => combatController.Start(Task.CombatData);
- public ETaskResult Update()
+ public override ETaskResult Update()
{
_status = combatController.Update();
if (_status != CombatController.EStatus.Complete)
return ETaskResult.StillRunning;
// if our quest step has any completion flags, we need to check if they are set
- if (QuestWorkUtils.HasCompletionFlags(completionQuestVariableFlags) &&
- combatData.ElementId is QuestId questId)
+ if (QuestWorkUtils.HasCompletionFlags(Task.CompletionQuestVariableFlags) &&
+ Task.CombatData.ElementId is QuestId questId)
{
var questWork = questFunctions.GetQuestProgressInfo(questId);
if (questWork == null)
return ETaskResult.StillRunning;
- if (QuestWorkUtils.MatchesQuestWork(completionQuestVariableFlags, questWork))
+ if (QuestWorkUtils.MatchesQuestWork(Task.CompletionQuestVariableFlags, questWork))
return ETaskResult.TaskComplete;
else
return ETaskResult.StillRunning;
// 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
{
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})";
- }
}
}
internal static class Dive
{
- internal sealed class Factory(ICondition condition, ILoggerFactory loggerFactory) : SimpleTaskFactory
+ internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.Dive)
return null;
- return Dive();
+ return new Task();
}
+ }
- public ITask Dive()
- {
- return new DoDive(condition, loggerFactory.CreateLogger<DoDive>());
- }
+ internal sealed class Task : ITask
+ {
+
+ public override string ToString() => "Dive";
}
- private sealed class DoDive(ICondition condition, ILogger<DoDive> logger)
- : AbstractDelayedTask(TimeSpan.FromSeconds(5))
+ internal sealed class DoDive(ICondition condition, ILogger<DoDive> logger)
+ : AbstractDelayedTaskExecutor<Task>(TimeSpan.FromSeconds(5))
{
private readonly Queue<(uint Type, nint Key)> _keysToPress = [];
private int _attempts;
foreach (var key in realKeys)
_keysToPress.Enqueue((NativeMethods.WM_KEYUP, key));
}
-
- public override string ToString() => "Dive";
}
private static List<nint>? GetKeysToPress(SeVirtualKey key, ModifierFlag modifier)
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;
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)
{
return null;
ArgumentNullException.ThrowIfNull(step.ContentFinderConditionId);
- return new OpenDutyFinder(step.ContentFinderConditionId.Value, gameFunctions, condition);
+ return new Task(step.ContentFinderConditionId.Value);
}
}
- private sealed class OpenDutyFinder(
- uint contentFinderConditionId,
+ internal sealed record Task(uint ContentFinderConditionId) : ITask
+ {
+ public override string ToString() => $"OpenDutyFinder({ContentFinderConditionId})";
+ }
+
+ internal sealed class Executor(
GameFunctions gameFunctions,
- ICondition condition) : ITask
+ ICondition condition) : TaskExecutor<Task>
{
- public bool Start()
+ protected override bool Start()
{
if (condition[ConditionFlag.InDutyQueue])
return false;
- gameFunctions.OpenDutyFinder(contentFinderConditionId);
+ gameFunctions.OpenDutyFinder(Task.ContentFinderConditionId);
return true;
}
- public ETaskResult Update() => ETaskResult.TaskComplete;
-
- public override string ToString() => $"OpenDutyFinder({contentFinderConditionId})";
+ public override ETaskResult Update() => ETaskResult.TaskComplete;
}
}
using System;
using System.Collections.Generic;
-using Microsoft.Extensions.DependencyInjection;
using Questionable.Controller.Steps.Common;
using Questionable.Functions;
using Questionable.Model;
internal static class Emote
{
- internal sealed class Factory(ChatFunctions chatFunctions, Mount.Factory mountFactory) : ITaskFactory
+ internal sealed class Factory : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
ArgumentNullException.ThrowIfNull(step.Emote);
- var unmount = mountFactory.Unmount();
+ var unmount = new Mount.UnmountTask();
if (step.DataId != null)
{
- var task = new UseOnObject(step.Emote.Value, step.DataId.Value, chatFunctions);
+ var task = new UseOnObject(step.Emote.Value, step.DataId.Value);
return [unmount, task];
}
else
{
- var task = new UseOnSelf(step.Emote.Value, chatFunctions);
+ var task = new UseOnSelf(step.Emote.Value);
return [unmount, task];
}
}
}
- private sealed class UseOnObject(EEmote emote, uint dataId, ChatFunctions chatFunctions) : AbstractDelayedTask
+ internal sealed record UseOnObject(EEmote Emote, uint DataId) : ITask
+ {
+ public override string ToString() => $"Emote({Emote} on {DataId})";
+ }
+
+ internal sealed class UseOnObjectExecutor(ChatFunctions chatFunctions)
+ : AbstractDelayedTaskExecutor<UseOnObject>
{
protected override bool StartInternal()
{
- chatFunctions.UseEmote(dataId, emote);
+ chatFunctions.UseEmote(Task.DataId, Task.Emote);
return true;
}
+ }
- public override string ToString() => $"Emote({emote} on {dataId})";
+ internal sealed record UseOnSelf(EEmote Emote) : ITask
+ {
+ public override string ToString() => $"Emote({Emote})";
}
- private sealed class UseOnSelf(EEmote emote, ChatFunctions chatFunctions) : AbstractDelayedTask
+ internal sealed class UseOnSelfExecutor(ChatFunctions chatFunctions) : AbstractDelayedTaskExecutor<UseOnSelf>
{
protected override bool StartInternal()
{
- chatFunctions.UseEmote(emote);
+ chatFunctions.UseEmote(Task.Emote);
return true;
}
-
- public override string ToString() => $"Emote({emote})";
}
}
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)
{
return null;
ArgumentNullException.ThrowIfNull(step.ItemId);
- return Equip(step.ItemId.Value);
- }
-
- private DoEquip Equip(uint itemId)
- {
- var item = dataManager.GetExcelSheet<Item>()!.GetRow(itemId) ??
- throw new ArgumentOutOfRangeException(nameof(itemId));
- var targetSlots = GetEquipSlot(item) ?? throw new InvalidOperationException("Not a piece of equipment");
- return new DoEquip(itemId, item, targetSlots, dataManager, loggerFactory.CreateLogger<DoEquip>());
+ return new Task(step.ItemId.Value);
}
+ }
- private static List<ushort>? GetEquipSlot(Item item)
- {
- return item.EquipSlotCategory.Row switch
- {
- >= 1 and <= 11 => [(ushort)(item.EquipSlotCategory.Row - 1)],
- 12 => [11, 12], // rings
- 13 => [0],
- 17 => [13], // soul crystal
- _ => null
- };
- }
+ internal sealed record Task(uint ItemId) : ITask
+ {
+ public override string ToString() => $"Equip({ItemId})";
}
- private sealed class DoEquip(
- uint itemId,
- Item item,
- List<ushort> targetSlots,
+ internal sealed class Executor(
IDataManager dataManager,
- ILogger<DoEquip> logger) : ITask, IToastAware
+ ILogger<Executor> logger) : TaskExecutor<Task>, IToastAware
{
private const int MaxAttempts = 3;
];
private int _attempts;
+ private Item _item = null!;
+ private List<ushort> _targetSlots = null!;
private DateTime _continueAt = DateTime.MaxValue;
- public bool Start()
+ protected override bool Start()
{
+ _item = dataManager.GetExcelSheet<Item>()!.GetRow(Task.ItemId) ??
+ throw new ArgumentOutOfRangeException(nameof(Task.ItemId));
+ _targetSlots = GetEquipSlot(_item) ?? throw new InvalidOperationException("Not a piece of equipment");
+
Equip();
_continueAt = DateTime.Now.AddSeconds(1);
return true;
}
- public unsafe ETaskResult Update()
+ public override unsafe ETaskResult Update()
{
if (DateTime.Now < _continueAt)
return ETaskResult.StillRunning;
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;
}
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;
}
}
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}",
}
}
- public override string ToString() => $"Equip({item.Name})";
+ private static List<ushort>? GetEquipSlot(Item item)
+ {
+ return item.EquipSlotCategory.Row switch
+ {
+ >= 1 and <= 11 => [(ushort)(item.EquipSlotCategory.Row - 1)],
+ 12 => [11, 12], // rings
+ 13 => [0],
+ 17 => [13], // soul crystal
+ _ => null
+ };
+ }
public bool OnErrorToast(SeString message)
{
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)
{
step.InteractionType != EInteractionType.Combat)
return null;
- return new DoEquipRecommended(clientState, chatGui);
+ return new EquipTask();
}
}
- private sealed unsafe class DoEquipRecommended(IClientState clientState, IChatGui chatGui) : ITask
+ internal sealed class EquipTask : ITask
+ {
+ public override string ToString() => "EquipRecommended";
+ }
+
+ internal sealed unsafe class DoEquipRecommended(IClientState clientState, IChatGui chatGui) : TaskExecutor<EquipTask>
{
private bool _equipped;
- public bool Start()
+ protected override bool Start()
{
RecommendEquipModule.Instance()->SetupForClassJob((byte)clientState.LocalPlayer!.ClassJob.Id);
return true;
}
- public ETaskResult Update()
+ public override ETaskResult Update()
{
var recommendedEquipModule = RecommendEquipModule.Instance();
if (recommendedEquipModule->IsUpdating)
return ETaskResult.TaskComplete;
}
-
- public override string ToString() => "EquipRecommended";
}
}
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;
internal static class Interact
{
- internal sealed class Factory(
- GameFunctions gameFunctions,
- Configuration configuration,
- ICondition condition,
- ILoggerFactory loggerFactory)
- : ITaskFactory
+ internal sealed class Factory(Configuration configuration) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (sequence.Sequence == 0 && sequence.Steps.IndexOf(step) == 0)
yield return new WaitAtEnd.WaitDelay();
- yield return Interact(step.DataId.Value, quest, step.InteractionType,
+ yield return new Task(step.DataId.Value, quest, step.InteractionType,
step.TargetTerritoryId != null || quest.Id is SatisfactionSupplyNpcId ||
step.SkipConditions is { StepIf.Never: true }, step.PickUpItemId, step.SkipConditions?.StepIf);
}
+ }
- internal ITask Interact(uint dataId, Quest? quest, EInteractionType interactionType,
- bool skipMarkerCheck = false, uint? pickUpItemId = null, SkipStepConditions? skipConditions = null)
- {
- return new DoInteract(dataId, quest, interactionType, skipMarkerCheck, pickUpItemId, skipConditions,
- gameFunctions, condition, loggerFactory.CreateLogger<DoInteract>());
- }
+ internal sealed record Task(
+ uint DataId,
+ Quest? Quest,
+ EInteractionType InteractionType,
+ bool SkipMarkerCheck = false,
+ uint? PickUpItemId = null,
+ SkipStepConditions? SkipConditions = null) : ITask
+ {
+ public override string ToString() => $"Interact({DataId})";
}
internal sealed class DoInteract(
- uint dataId,
- Quest? quest,
- EInteractionType interactionType,
- bool skipMarkerCheck,
- uint? pickUpItemId,
- SkipStepConditions? skipConditions,
GameFunctions gameFunctions,
ICondition condition,
ILogger<DoInteract> logger)
- : ITask
+ : TaskExecutor<Task>
{
private bool _needsUnmount;
- private InteractionProgressContext? _progressContext;
private DateTime _continueAt = DateTime.MinValue;
- public Quest? Quest => quest;
+ public Quest? Quest => Task.Quest;
+ public EInteractionType InteractionType { get; set; }
- public EInteractionType InteractionType
+ protected override bool Start()
{
- get => interactionType;
- set => interactionType = value;
- }
-
- public InteractionProgressContext? ProgressContext() => _progressContext;
+ InteractionType = Task.InteractionType;
- public bool Start()
- {
- IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId);
+ IGameObject? gameObject = gameFunctions.FindObjectByDataId(Task.DataId);
if (gameObject == null)
{
- logger.LogWarning("No game object with dataId {DataId}", dataId);
+ logger.LogWarning("No game object with dataId {DataId}", Task.DataId);
return false;
}
- if (!gameObject.IsTargetable && skipConditions is { Never: false, NotTargetable: true })
+ if (!gameObject.IsTargetable && Task.SkipConditions is { Never: false, NotTargetable: true })
{
logger.LogInformation("Not interacting with {DataId} because it is not targetable (but skippable)",
- dataId);
+ Task.DataId);
return false;
}
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);
if (gameObject.IsTargetable && HasAnyMarker(gameObject))
{
- _progressContext =
+ ProgressContext =
InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(gameObject));
_continueAt = DateTime.Now.AddSeconds(0.5);
return true;
return true;
}
- public ETaskResult Update()
+ public override ETaskResult Update()
{
if (DateTime.Now <= _continueAt)
return ETaskResult.StillRunning;
_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;
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})";
}
}
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)
{
ArgumentNullException.ThrowIfNull(step.JumpDestination);
if (step.JumpDestination.Type == EJumpType.SingleJump)
- return SingleJump(step.DataId, step.JumpDestination, step.Comment);
+ return new SingleJumpTask(step.DataId, step.JumpDestination, step.Comment);
else
- return RepeatedJumps(step.DataId, step.JumpDestination, step.Comment);
+ return new RepeatedJumpTask(step.DataId, step.JumpDestination, step.Comment);
}
+ }
- public ITask SingleJump(uint? dataId, JumpDestination jumpDestination, string? comment)
- {
- return new DoSingleJump(dataId, jumpDestination, comment, movementController, clientState, framework);
- }
+ internal interface IJumpTask : ITask
+ {
+ uint? DataId { get; }
+ JumpDestination JumpDestination { get; }
+ string? Comment { get; }
+ }
- public ITask RepeatedJumps(uint? dataId, JumpDestination jumpDestination, string? comment)
- {
- return new DoRepeatedJumps(dataId, jumpDestination, comment, movementController, clientState, framework,
- condition, loggerFactory.CreateLogger<DoRepeatedJumps>());
- }
+ internal sealed record SingleJumpTask(
+ uint? DataId,
+ JumpDestination JumpDestination,
+ string? Comment) : IJumpTask
+ {
+ public override string ToString() => $"Jump({Comment})";
}
- private class DoSingleJump(
- uint? dataId,
- JumpDestination jumpDestination,
- string? comment,
+ internal abstract class JumpBase<T>(
MovementController movementController,
IClientState clientState,
- IFramework framework) : ITask
+ IFramework framework) : TaskExecutor<T>
+ where T : class, IJumpTask
{
- public virtual bool Start()
+ protected override bool Start()
{
- float stopDistance = jumpDestination.CalculateStopDistance();
- if ((clientState.LocalPlayer!.Position - jumpDestination.Position).Length() <= stopDistance)
+ float stopDistance = Task.JumpDestination.CalculateStopDistance();
+ if ((clientState.LocalPlayer!.Position - Task.JumpDestination.Position).Length() <= stopDistance)
return false;
- movementController.NavigateTo(EMovementType.Quest, dataId, [jumpDestination.Position], false, false,
- jumpDestination.StopDistance ?? stopDistance);
+ movementController.NavigateTo(EMovementType.Quest, Task.DataId, [Task.JumpDestination.Position], false,
+ false,
+ Task.JumpDestination.StopDistance ?? stopDistance);
framework.RunOnTick(() =>
{
unsafe
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;
return ETaskResult.TaskComplete;
}
+ }
- public override string ToString() => $"Jump({comment})";
+ internal sealed class DoSingleJump(
+ MovementController movementController,
+ IClientState clientState,
+ IFramework framework) : JumpBase<SingleJumpTask>(movementController, clientState, framework);
+
+ internal sealed record RepeatedJumpTask(
+ uint? DataId,
+ JumpDestination JumpDestination,
+ string? Comment) : IJumpTask
+ {
+ public override string ToString() => $"RepeatedJump({Comment})";
}
- private sealed class DoRepeatedJumps(
- uint? dataId,
- JumpDestination jumpDestination,
- string? comment,
+ internal sealed class DoRepeatedJumps(
MovementController movementController,
IClientState clientState,
IFramework framework,
ICondition condition,
ILogger<DoRepeatedJumps> logger)
- : DoSingleJump(dataId, jumpDestination, comment, movementController, clientState, framework)
+ : JumpBase<RepeatedJumpTask>(movementController, clientState, framework)
{
- private readonly JumpDestination _jumpDestination = jumpDestination;
- private readonly string? _comment = comment;
private readonly IClientState _clientState = clientState;
private DateTime _continueAt = DateTime.MinValue;
private int _attempts;
- public override bool Start()
+ protected override bool Start()
{
- _continueAt = DateTime.Now + TimeSpan.FromSeconds(2 * (_jumpDestination.DelaySeconds ?? 0.5f));
+ _continueAt = DateTime.Now + TimeSpan.FromSeconds(2 * (Task.JumpDestination.DelaySeconds ?? 0.5f));
return base.Start();
}
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))
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})";
}
}
internal static class Say
{
- internal sealed class Factory(
- ChatFunctions chatFunctions,
- Mount.Factory mountFactory,
- ExcelFunctions excelFunctions) : ITaskFactory
+ internal sealed class Factory(ExcelFunctions excelFunctions) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
.GetString();
ArgumentNullException.ThrowIfNull(excelString);
- var unmount = mountFactory.Unmount();
- var task = new UseChat(excelString, chatFunctions);
+ var unmount = new Mount.UnmountTask();
+ var task = new Task(excelString);
return [unmount, task];
}
}
- private sealed class UseChat(string chatMessage, ChatFunctions chatFunctions) : AbstractDelayedTask
+ internal sealed record Task(string ChatMessage) : ITask
+ {
+ public override string ToString() => $"Say({ChatMessage})";
+ }
+
+ internal sealed class UseChat(ChatFunctions chatFunctions) : AbstractDelayedTaskExecutor<Task>
{
protected override bool StartInternal()
{
- chatFunctions.ExecuteCommand($"/say {chatMessage}");
+ chatFunctions.ExecuteCommand($"/say {Task.ChatMessage}");
return true;
}
-
- public override string ToString() => $"Say({chatMessage})";
}
}
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;
internal static class UseItem
{
internal sealed class Factory(
- Mount.Factory mountFactory,
- MoveTo.Factory moveFactory,
- Interact.Factory interactFactory,
- AetheryteShortcut.Factory aetheryteShortcutFactory,
- AethernetShortcut.Factory aethernetShortcutFactory,
- GameFunctions gameFunctions,
- QuestFunctions questFunctions,
- ICondition condition,
IClientState clientState,
TerritoryData territoryData,
- ILoggerFactory loggerFactory,
ILogger<Factory> logger)
: ITaskFactory
{
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();
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);
}
}
else if (step.DataId != null)
{
- var task = OnObject(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags);
+ var task = new UseOnObject(quest.Id, step.DataId.Value, step.ItemId.Value,
+ step.CompletionQuestVariablesFlags);
return [unmount, task];
}
else
{
- var task = OnSelf(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags);
+ var task = new UseOnSelf(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags);
return [unmount, task];
}
}
- public ITask OnGroundTarget(ElementId questId, uint dataId, uint itemId,
- List<QuestWorkValue?> completionQuestVariablesFlags)
- {
- return new UseOnGround(questId, dataId, itemId, completionQuestVariablesFlags, gameFunctions,
- questFunctions, condition, loggerFactory.CreateLogger<UseOnGround>());
- }
-
- public ITask OnPosition(ElementId questId, Vector3 position, uint itemId,
- List<QuestWorkValue?> completionQuestVariablesFlags)
- {
- return new UseOnPosition(questId, position, itemId, completionQuestVariablesFlags, gameFunctions,
- questFunctions, condition, loggerFactory.CreateLogger<UseOnPosition>());
- }
-
- public ITask OnObject(ElementId questId, uint dataId, uint itemId,
- List<QuestWorkValue?> completionQuestVariablesFlags, bool startingCombat = false)
- {
- return new UseOnObject(questId, dataId, itemId, completionQuestVariablesFlags, startingCombat,
- questFunctions, gameFunctions, condition, loggerFactory.CreateLogger<UseOnObject>());
- }
-
- public ITask OnSelf(ElementId questId, uint itemId, List<QuestWorkValue?> completionQuestVariablesFlags)
- {
- return new Use(questId, itemId, completionQuestVariablesFlags, gameFunctions, questFunctions, condition,
- loggerFactory.CreateLogger<Use>());
- }
-
private IEnumerable<ITask> CreateVesperBayFallbackTask()
{
logger.LogWarning("No vesper bay aetheryte tickets in inventory, navigating via ferry in Limsa instead");
uint npcId = 1003540;
ushort territoryId = 129;
Vector3 destination = new(-360.9217f, 8f, 38.92566f);
- yield return aetheryteShortcutFactory.Use(null, null, EAetheryteLocation.Limsa, territoryId);
- yield return aethernetShortcutFactory.Use(EAetheryteLocation.Limsa, EAetheryteLocation.LimsaArcanist);
+ yield return new AetheryteShortcut.Task(null, null, EAetheryteLocation.Limsa, territoryId);
+ yield return new AethernetShortcut.Task(EAetheryteLocation.Limsa, EAetheryteLocation.LimsaArcanist);
yield return new WaitAtEnd.WaitDelay();
- yield return
- moveFactory.Move(new MoveTo.MoveParams(territoryId, destination, DataId: npcId, Sprint: false));
- yield return interactFactory.Interact(npcId, null, EInteractionType.None, true);
+ yield return new MoveTo.MoveTask(territoryId, destination, DataId: npcId, Sprint: false);
+ yield return new Interact.Task(npcId, null, EInteractionType.None, true);
}
}
- private abstract class UseItemBase(
- ElementId? questId,
- uint itemId,
- IList<QuestWorkValue?> completionQuestVariablesFlags,
- bool startingCombat,
+ internal interface IUseItemBase : ITask
+ {
+ ElementId? QuestId { get; }
+ uint ItemId { get; }
+ IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; }
+ bool StartingCombat { get; }
+ }
+
+ internal abstract class UseItemExecutorBase<T>(
QuestFunctions questFunctions,
ICondition condition,
- ILogger logger) : ITask
+ ILogger logger) : TaskExecutor<T>
+ where T : class, IUseItemBase
{
private bool _usedItem;
private DateTime _continueAt;
private int _itemCount;
- private InteractionProgressContext? _progressContext;
-
- public InteractionProgressContext? ProgressContext() => _progressContext;
- public ElementId? QuestId => questId;
- public uint ItemId => itemId;
- public IList<QuestWorkValue?> CompletionQuestVariablesFlags => completionQuestVariablesFlags;
- public bool StartingCombat => startingCombat;
+ private ElementId? QuestId => Task.QuestId;
+ protected uint ItemId => Task.ItemId;
+ private IList<QuestWorkValue?> CompletionQuestVariablesFlags => Task.CompletionQuestVariablesFlags;
+ private bool StartingCombat => Task.StartingCombat;
protected abstract bool UseItem();
- public unsafe bool Start()
+ protected override unsafe bool Start()
{
InventoryManager* inventoryManager = InventoryManager.Instance();
if (inventoryManager == null)
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))
{
if (!_usedItem)
{
- _progressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem());
+ ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem());
_continueAt = DateTime.Now.Add(GetRetryDelay());
return ETaskResult.StillRunning;
}
}
}
+ internal sealed record UseOnGround(
+ ElementId? QuestId,
+ uint DataId,
+ uint ItemId,
+ IList<QuestWorkValue?> CompletionQuestVariablesFlags) : IUseItemBase
+ {
+ public bool StartingCombat => false;
+ public override string ToString() => $"UseItem({ItemId} on ground at {DataId})";
+ }
- private sealed class UseOnGround(
- ElementId? questId,
- uint dataId,
- uint itemId,
- IList<QuestWorkValue?> completionQuestVariablesFlags,
+ internal sealed class UseOnGroundExecutor(
GameFunctions gameFunctions,
QuestFunctions questFunctions,
ICondition condition,
- ILogger<UseOnGround> logger)
- : UseItemBase(questId, itemId, completionQuestVariablesFlags, false, questFunctions, condition, logger)
+ ILogger<UseOnGroundExecutor> logger)
+ : UseItemExecutorBase<UseOnGround>(questFunctions, condition, logger)
{
- protected override bool UseItem() => gameFunctions.UseItemOnGround(dataId, ItemId);
+ protected override bool UseItem() => gameFunctions.UseItemOnGround(Task.DataId, ItemId);
+ }
+
+ internal sealed record UseOnPosition(
+ ElementId? QuestId,
+ Vector3 Position,
+ uint ItemId,
+ IList<QuestWorkValue?> CompletionQuestVariablesFlags)
+ : IUseItemBase
+ {
+ public bool StartingCombat => false;
- public override string ToString() => $"UseItem({ItemId} on ground at {dataId})";
+ public override string ToString() =>
+ $"UseItem({ItemId} on ground at {Position.ToString("G", CultureInfo.InvariantCulture)})";
}
- private sealed class UseOnPosition(
- ElementId? questId,
- Vector3 position,
- uint itemId,
- IList<QuestWorkValue?> completionQuestVariablesFlags,
+ internal sealed class UseOnPositionExecutor(
GameFunctions gameFunctions,
QuestFunctions questFunctions,
ICondition condition,
ILogger<UseOnPosition> logger)
- : UseItemBase(questId, itemId, completionQuestVariablesFlags, false, questFunctions, condition, logger)
+ : UseItemExecutorBase<UseOnPosition>(questFunctions, condition, logger)
{
- protected override bool UseItem() => gameFunctions.UseItemOnPosition(position, ItemId);
+ protected override bool UseItem() => gameFunctions.UseItemOnPosition(Task.Position, ItemId);
+ }
- public override string ToString() =>
- $"UseItem({ItemId} on ground at {position.ToString("G", CultureInfo.InvariantCulture)})";
+ internal sealed record UseOnObject(
+ ElementId? QuestId,
+ uint DataId,
+ uint ItemId,
+ IList<QuestWorkValue?> CompletionQuestVariablesFlags,
+ bool StartingCombat = false) : IUseItemBase
+ {
+ public override string ToString() => $"UseItem({ItemId} on {DataId})";
}
- private sealed class UseOnObject(
- ElementId? questId,
- uint dataId,
- uint itemId,
- IList<QuestWorkValue?> completionQuestVariablesFlags,
- bool startingCombat,
+ internal sealed class UseOnObjectExecutor(
QuestFunctions questFunctions,
GameFunctions gameFunctions,
ICondition condition,
ILogger<UseOnObject> logger)
- : UseItemBase(questId, itemId, completionQuestVariablesFlags, startingCombat, questFunctions, condition, logger)
+ : UseItemExecutorBase<UseOnObject>(questFunctions, condition, logger)
{
- protected override bool UseItem() => gameFunctions.UseItem(dataId, ItemId);
+ protected override bool UseItem() => gameFunctions.UseItem(Task.DataId, ItemId);
+ }
- public override string ToString() => $"UseItem({ItemId} on {dataId})";
+ internal sealed record UseOnSelf(
+ ElementId? QuestId,
+ uint ItemId,
+ IList<QuestWorkValue?> CompletionQuestVariablesFlags) : IUseItemBase
+ {
+ public bool StartingCombat => false;
+ public override string ToString() => $"UseItem({ItemId})";
}
- private sealed class Use(
- ElementId? questId,
- uint itemId,
- IList<QuestWorkValue?> completionQuestVariablesFlags,
+ internal sealed class UseOnSelfExecutor(
GameFunctions gameFunctions,
QuestFunctions questFunctions,
ICondition condition,
- ILogger<Use> logger)
- : UseItemBase(questId, itemId, completionQuestVariablesFlags, false, questFunctions, condition, logger)
+ ILogger<UseOnSelf> logger)
+ : UseItemExecutorBase<UseOnSelf>(questFunctions, condition, logger)
{
protected override bool UseItem() => gameFunctions.UseItem(ItemId);
-
- public override string ToString() => $"UseItem({ItemId})";
}
}
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;
internal static class InitiateLeve
{
- internal sealed class Factory(IGameGui gameGui, ICondition condition) : ITaskFactory
+ internal sealed class Factory(ICondition condition) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
yield return new SkipInitiateIfActive(quest.Id);
yield return new OpenJournal(quest.Id);
- yield return new Initiate(quest.Id, gameGui);
- yield return new SelectDifficulty(gameGui);
- yield return new WaitConditionTask(() => condition[ConditionFlag.BoundByDuty], "Wait(BoundByDuty)");
+ yield return new Initiate(quest.Id);
+ yield return new SelectDifficulty();
+ yield return new WaitCondition.Task(() => condition[ConditionFlag.BoundByDuty], "Wait(BoundByDuty)");
}
}
- internal sealed unsafe class SkipInitiateIfActive(ElementId elementId) : ITask
+ internal sealed record SkipInitiateIfActive(ElementId ElementId) : ITask
{
- public bool Start() => true;
+ public override string ToString() => $"CheckIfAlreadyActive({ElementId})";
+ }
+
+ internal sealed unsafe class SkipInitiateIfActiveExecutor : TaskExecutor<SkipInitiateIfActive>
+ {
+ protected override bool Start() => true;
- public ETaskResult Update()
+ public override ETaskResult Update()
{
var director = UIState.Instance()->DirectorTodo.Director;
if (director != null &&
director->EventHandlerInfo != null &&
director->EventHandlerInfo->EventId.ContentId == EventHandlerType.GatheringLeveDirector &&
- director->ContentId == elementId.Value)
+ director->ContentId == Task.ElementId.Value)
return ETaskResult.SkipRemainingTasksForStep;
return ETaskResult.TaskComplete;
}
+ }
- public override string ToString() => $"CheckIfAlreadyActive({elementId})";
+ internal sealed record OpenJournal(ElementId ElementId) : ITask
+ {
+ public uint QuestType => ElementId is LeveId ? 2u : 1u;
+ public override string ToString() => $"OpenJournal({ElementId})";
}
- internal sealed unsafe class OpenJournal(ElementId elementId) : ITask
+ internal sealed unsafe class OpenJournalExecutor : TaskExecutor<OpenJournal>
{
- private readonly uint _questType = elementId is LeveId ? 2u : 1u;
private DateTime _openedAt = DateTime.MinValue;
- public bool Start()
+ protected override bool Start()
{
- AgentQuestJournal.Instance()->OpenForQuest(elementId.Value, _questType);
+ AgentQuestJournal.Instance()->OpenForQuest(Task.ElementId.Value, Task.QuestType);
_openedAt = DateTime.Now;
return true;
}
- public ETaskResult Update()
+ public override ETaskResult Update()
{
AgentQuestJournal* agentQuestJournal = AgentQuestJournal.Instance();
if (agentQuestJournal->IsAgentActive() &&
- agentQuestJournal->SelectedQuestId == elementId.Value &&
- agentQuestJournal->SelectedQuestType == _questType)
+ agentQuestJournal->SelectedQuestId == Task.ElementId.Value &&
+ agentQuestJournal->SelectedQuestType == Task.QuestType)
return ETaskResult.TaskComplete;
if (DateTime.Now > _openedAt.AddSeconds(3))
{
- AgentQuestJournal.Instance()->OpenForQuest(elementId.Value, _questType);
+ AgentQuestJournal.Instance()->OpenForQuest(Task.ElementId.Value, Task.QuestType);
_openedAt = DateTime.Now;
}
return ETaskResult.StillRunning;
}
+ }
- public override string ToString() => $"OpenJournal({elementId})";
+ internal sealed record Initiate(ElementId ElementId) : ITask
+ {
+ public override string ToString() => $"InitiateLeve({ElementId})";
}
- internal sealed unsafe class Initiate(ElementId elementId, IGameGui gameGui) : ITask
+ internal sealed unsafe class InitiateExecutor(IGameGui gameGui) : TaskExecutor<Initiate>
{
- public bool Start() => true;
+ protected override bool Start() => true;
- public ETaskResult Update()
+ public override ETaskResult Update()
{
if (gameGui.TryGetAddonByName("JournalDetail", out AtkUnitBase* addonJournalDetail))
{
var pickQuest = stackalloc AtkValue[]
{
new() { Type = ValueType.Int, Int = 4 },
- new() { Type = ValueType.UInt, Int = elementId.Value }
+ new() { Type = ValueType.UInt, Int = Task.ElementId.Value }
};
addonJournalDetail->FireCallback(2, pickQuest);
return ETaskResult.TaskComplete;
return ETaskResult.StillRunning;
}
+ }
- public override string ToString() => $"InitiateLeve({elementId})";
+ internal sealed class SelectDifficulty : ITask
+ {
+ public override string ToString() => "SelectLeveDifficulty";
}
- internal sealed unsafe class SelectDifficulty(IGameGui gameGui) : ITask
+ internal sealed unsafe class SelectDifficultyExecutor(IGameGui gameGui) : TaskExecutor<SelectDifficulty>
{
- public bool Start() => true;
+ protected override bool Start() => true;
- public ETaskResult Update()
+ public override ETaskResult Update()
{
if (gameGui.TryGetAddonByName("GuildLeveDifficulty", out AtkUnitBase* addon))
{
// atkvalues: 1 → default difficulty, 2 → min, 3 → max
-
-
var pickDifficulty = stackalloc AtkValue[]
{
new() { Type = ValueType.Int, Int = 0 },
return ETaskResult.StillRunning;
}
-
- public override string ToString() => "SelectLeveDifficulty";
}
}
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;
internal static class AethernetShortcut
{
- internal sealed class Factory(
- MovementController movementController,
- AetheryteFunctions aetheryteFunctions,
- GameFunctions gameFunctions,
- QuestFunctions questFunctions,
- IClientState clientState,
- AetheryteData aetheryteData,
- TerritoryData territoryData,
- LifestreamIpc lifestreamIpc,
- ICondition condition,
- ILoggerFactory loggerFactory)
+ internal sealed class Factory(MovementController movementController)
: ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
if (step.AethernetShortcut == null)
yield break;
- yield return new WaitConditionTask(() => movementController.IsNavmeshReady,
+ yield return new WaitCondition.Task(() => movementController.IsNavmeshReady,
"Wait(navmesh ready)");
- yield return Use(step.AethernetShortcut.From, step.AethernetShortcut.To,
- step.SkipConditions?.AethernetShortcutIf);
+ yield return new Task(step.AethernetShortcut.From, step.AethernetShortcut.To,
+ step.SkipConditions?.AethernetShortcutIf ?? new());
}
+ }
- public ITask Use(EAetheryteLocation from, EAetheryteLocation to, SkipAetheryteCondition? skipConditions = null)
+ internal sealed record Task(
+ EAetheryteLocation From,
+ EAetheryteLocation To,
+ SkipAetheryteCondition SkipConditions) : ISkippableTask
+ {
+ public Task(EAetheryteLocation from,
+ EAetheryteLocation to)
+ : this(from, to, new())
{
- return new UseAethernetShortcut(from, to, skipConditions ?? new(),
- loggerFactory.CreateLogger<UseAethernetShortcut>(), aetheryteFunctions, gameFunctions, questFunctions,
- clientState, aetheryteData, territoryData, lifestreamIpc, movementController, condition);
}
+
+ public override string ToString() => $"UseAethernet({From} -> {To})";
}
internal sealed class UseAethernetShortcut(
- EAetheryteLocation from,
- EAetheryteLocation to,
- SkipAetheryteCondition skipConditions,
ILogger<UseAethernetShortcut> logger,
AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions,
TerritoryData territoryData,
LifestreamIpc lifestreamIpc,
MovementController movementController,
- ICondition condition) : ISkippableTask
+ ICondition condition) : TaskExecutor<Task>
{
private bool _moving;
private bool _teleported;
private bool _triedMounting;
private DateTime _continueAt = DateTime.MinValue;
- public EAetheryteLocation From => from;
- public EAetheryteLocation To => to;
+ public EAetheryteLocation From => Task.From;
+ public EAetheryteLocation To => Task.To;
- public bool Start()
+ protected override bool Start()
{
- if (!skipConditions.Never)
+ if (!Task.SkipConditions.Never)
{
- if (skipConditions.InSameTerritory && clientState.TerritoryType == aetheryteData.TerritoryIds[to])
+ if (Task.SkipConditions.InSameTerritory &&
+ clientState.TerritoryType == aetheryteData.TerritoryIds[Task.To])
{
logger.LogInformation("Skipping aethernet shortcut because the target is in the same territory");
return false;
}
- if (skipConditions.InTerritory.Contains(clientState.TerritoryType))
+ if (Task.SkipConditions.InTerritory.Contains(clientState.TerritoryType))
{
logger.LogInformation(
"Skipping aethernet shortcut because the target is in the specified territory");
return false;
}
- if (skipConditions.QuestsCompleted.Count > 0 &&
- skipConditions.QuestsCompleted.All(questFunctions.IsQuestComplete))
+ if (Task.SkipConditions.QuestsCompleted.Count > 0 &&
+ Task.SkipConditions.QuestsCompleted.All(questFunctions.IsQuestComplete))
{
logger.LogInformation("Skipping aethernet shortcut, all prequisite quests are complete");
return true;
}
- if (skipConditions.QuestsAccepted.Count > 0 &&
- skipConditions.QuestsAccepted.All(questFunctions.IsQuestAccepted))
+ if (Task.SkipConditions.QuestsAccepted.Count > 0 &&
+ Task.SkipConditions.QuestsAccepted.All(questFunctions.IsQuestAccepted))
{
logger.LogInformation("Skipping aethernet shortcut, all prequisite quests are accepted");
return true;
}
- if (skipConditions.AetheryteLocked != null &&
- !aetheryteFunctions.IsAetheryteUnlocked(skipConditions.AetheryteLocked.Value))
+ if (Task.SkipConditions.AetheryteLocked != null &&
+ !aetheryteFunctions.IsAetheryteUnlocked(Task.SkipConditions.AetheryteLocked.Value))
{
logger.LogInformation("Skipping aethernet shortcut because the target aetheryte is locked");
return false;
}
- if (skipConditions.AetheryteUnlocked != null &&
- aetheryteFunctions.IsAetheryteUnlocked(skipConditions.AetheryteUnlocked.Value))
+ if (Task.SkipConditions.AetheryteUnlocked != null &&
+ aetheryteFunctions.IsAetheryteUnlocked(Task.SkipConditions.AetheryteUnlocked.Value))
{
logger.LogInformation("Skipping aethernet shortcut because the target aetheryte is unlocked");
return false;
}
}
- if (aetheryteFunctions.IsAetheryteUnlocked(from) &&
- aetheryteFunctions.IsAetheryteUnlocked(to))
+ if (aetheryteFunctions.IsAetheryteUnlocked(Task.From) &&
+ aetheryteFunctions.IsAetheryteUnlocked(Task.To))
{
ushort territoryType = clientState.TerritoryType;
Vector3 playerPosition = clientState.LocalPlayer!.Position;
// closer to the source
- if (aetheryteData.CalculateDistance(playerPosition, territoryType, from) <
- aetheryteData.CalculateDistance(playerPosition, territoryType, to))
+ if (aetheryteData.CalculateDistance(playerPosition, territoryType, Task.From) <
+ aetheryteData.CalculateDistance(playerPosition, territoryType, Task.To))
{
- if (aetheryteData.CalculateDistance(playerPosition, territoryType, from) <
- (from.IsFirmamentAetheryte() ? 11f : 4f))
+ if (aetheryteData.CalculateDistance(playerPosition, territoryType, Task.From) <
+ (Task.From.IsFirmamentAetheryte() ? 11f : 4f))
{
DoTeleport();
return true;
}
- else if (from == EAetheryteLocation.SolutionNine)
+ else if (Task.From == EAetheryteLocation.SolutionNine)
{
logger.LogInformation("Moving to S9 aetheryte");
List<Vector3> nearbyPoints =
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();
else
logger.LogWarning(
"Aethernet shortcut not unlocked (from: {FromAetheryte}, to: {ToAetheryte}), walking manually",
- from, to);
+ Task.From, Task.To);
return false;
}
{
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;
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})";
}
}
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;
internal static class AetheryteShortcut
{
- internal sealed class Factory(
- AetheryteData aetheryteData,
- AetheryteFunctions aetheryteFunctions,
- QuestFunctions questFunctions,
- IClientState clientState,
- IChatGui chatGui,
- ILoggerFactory loggerFactory) : ITaskFactory
+ internal sealed class Factory(AetheryteData aetheryteData) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.AetheryteShortcut == null)
yield break;
- yield return Use(step, quest.Id, step.AetheryteShortcut.Value,
+ yield return new Task(step, quest.Id, step.AetheryteShortcut.Value,
aetheryteData.TerritoryIds[step.AetheryteShortcut.Value]);
yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(0.5));
}
+ }
- public ITask Use(QuestStep? step, ElementId? elementId, EAetheryteLocation targetAetheryte,
- ushort expectedTerritoryId)
- {
- return new UseAetheryteShortcut(step, elementId, targetAetheryte, expectedTerritoryId,
- loggerFactory.CreateLogger<UseAetheryteShortcut>(), aetheryteFunctions, questFunctions, clientState,
- chatGui, aetheryteData);
- }
+ /// <param name="ExpectedTerritoryId">If using an aethernet shortcut after, the aetheryte's territory-id and the step's territory-id can differ, we always use the aetheryte's territory-id.</param>
+ internal sealed record Task(
+ QuestStep? Step,
+ ElementId? ElementId,
+ EAetheryteLocation TargetAetheryte,
+ ushort ExpectedTerritoryId) : ISkippableTask
+ {
}
- /// <param name="expectedTerritoryId">If using an aethernet shortcut after, the aetheryte's territory-id and the step's territory-id can differ, we always use the aetheryte's territory-id.</param>
- private sealed class UseAetheryteShortcut(
- QuestStep? step,
- ElementId? elementId,
- EAetheryteLocation targetAetheryte,
- ushort expectedTerritoryId,
+ internal sealed class UseAetheryteShortcut(
ILogger<UseAetheryteShortcut> logger,
AetheryteFunctions aetheryteFunctions,
QuestFunctions questFunctions,
IClientState clientState,
IChatGui chatGui,
- AetheryteData aetheryteData) : ISkippableTask
+ AetheryteData aetheryteData) : TaskExecutor<Task>
{
private bool _teleported;
private DateTime _continueAt;
- private InteractionProgressContext? _progressContext;
- public InteractionProgressContext? ProgressContext() => _progressContext;
+ protected override bool Start() => !ShouldSkipTeleport();
- public bool Start() => !ShouldSkipTeleport();
-
- public ETaskResult Update()
+ public override ETaskResult Update()
{
if (DateTime.Now < _continueAt)
return ETaskResult.StillRunning;
return ETaskResult.StillRunning;
}
- if (clientState.TerritoryType == expectedTerritoryId)
+ if (clientState.TerritoryType == Task.ExpectedTerritoryId)
return ETaskResult.TaskComplete;
return ETaskResult.StillRunning;
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))
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");
}
}
- if (expectedTerritoryId == territoryType)
+ if (Task.ExpectedTerritoryId == territoryType)
{
if (!skipConditions.Never)
{
}
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;
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.");
_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})";
}
}
internal static class Craft
{
- internal sealed class Factory(
- IDataManager dataManager,
- IClientState clientState,
- ArtisanIpc artisanIpc,
- Mount.Factory mountFactory,
- ILoggerFactory loggerFactory) : ITaskFactory
+ internal sealed class Factory : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
ArgumentNullException.ThrowIfNull(step.ItemCount);
return
[
- mountFactory.Unmount(),
- Craft(step.ItemId.Value, step.ItemCount.Value)
+ new Mount.UnmountTask(),
+ new CraftTask(step.ItemId.Value, step.ItemCount.Value)
];
}
+ }
- public ITask Craft(uint itemId, int itemCount) =>
- new DoCraft(itemId, itemCount, dataManager, clientState, artisanIpc, loggerFactory.CreateLogger<DoCraft>());
+ internal sealed record CraftTask(
+ uint ItemId,
+ int ItemCount) : ITask
+ {
+ public override string ToString() => $"Craft {ItemCount}x {ItemId} (with Artisan)";
}
- private sealed class DoCraft(
- uint itemId,
- int itemCount,
+ internal sealed class DoCraft(
IDataManager dataManager,
IClientState clientState,
ArtisanIpc artisanIpc,
- ILogger<DoCraft> logger) : ITask
+ ILogger<DoCraft> logger) : TaskExecutor<CraftTask>
{
- public bool Start()
+ protected override bool Start()
{
if (HasRequestedItems())
{
- logger.LogInformation("Already own {ItemCount}x {ItemId}", itemCount, itemId);
+ logger.LogInformation("Already own {ItemCount}x {ItemId}", Task.ItemCount, Task.ItemId);
return false;
}
- RecipeLookup? recipeLookup = dataManager.GetExcelSheet<RecipeLookup>()!.GetRow(itemId);
+ RecipeLookup? recipeLookup = dataManager.GetExcelSheet<RecipeLookup>()!.GetRow(Task.ItemId);
if (recipeLookup == null)
- throw new TaskException($"Item {itemId} is not craftable");
+ throw new TaskException($"Item {Task.ItemId} is not craftable");
uint recipeId = (EClassJob)clientState.LocalPlayer!.ClassJob.Id switch
{
}
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())
{
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)";
}
}
internal sealed class Factory(
IServiceProvider serviceProvider,
MovementController movementController,
- GatheringController gatheringController,
GatheringPointRegistry gatheringPointRegistry,
IClientState clientState,
GatheringData gatheringData,
if (classJob != currentClassJob)
{
- yield return new SwitchClassJob(classJob, clientState);
+ yield return new SwitchClassJob.Task(classJob);
}
if (HasRequiredItems(itemToGather))
foreach (var task in serviceProvider.GetRequiredService<TaskCreator>()
.CreateTasks(quest, gatheringSequence, gatheringStep))
if (task is WaitAtEnd.NextStep)
- yield return CreateSkipMarkerTask();
+ yield return new SkipMarker();
else
yield return task;
}
}
ushort territoryId = gatheringRoot.Steps.Last().TerritoryId;
- yield return new WaitConditionTask(() => clientState.TerritoryType == territoryId,
+ yield return new WaitCondition.Task(() => clientState.TerritoryType == territoryId,
$"Wait(territory: {territoryData.GetNameAndId(territoryId)})");
- yield return new WaitConditionTask(() => movementController.IsNavmeshReady,
+ yield return new WaitCondition.Task(() => movementController.IsNavmeshReady,
"Wait(navmesh ready)");
- yield return CreateStartGatheringTask(gatheringPointId, itemToGather);
+ yield return new GatheringTask(gatheringPointId, itemToGather);
yield return new WaitAtEnd.WaitDelay();
}
}
minCollectability: (short)itemToGather.Collectability) >=
itemToGather.ItemCount;
}
+ }
- private StartGathering CreateStartGatheringTask(GatheringPointId gatheringPointId, GatheredItem gatheredItem)
- {
- return new StartGathering(gatheringPointId, gatheredItem, gatheringController);
- }
-
- private static SkipMarker CreateSkipMarkerTask()
+ internal sealed record GatheringTask(
+ GatheringPointId gatheringPointId,
+ GatheredItem gatheredItem) : ITask
+ {
+ public override string ToString()
{
- return new SkipMarker();
+ if (gatheredItem.Collectability == 0)
+ return $"Gather({gatheredItem.ItemCount}x {gatheredItem.ItemId})";
+ else
+ return
+ $"Gather({gatheredItem.ItemCount}x {gatheredItem.ItemId} {SeIconChar.Collectible.ToIconString()} {gatheredItem.Collectability})";
}
}
- private sealed class StartGathering(
- GatheringPointId gatheringPointId,
- GatheredItem gatheredItem,
- GatheringController gatheringController) : ITask
+ internal sealed class StartGathering(GatheringController gatheringController) : TaskExecutor<GatheringTask>
{
- public bool Start()
+ protected override bool Start()
{
- return gatheringController.Start(new GatheringController.GatheringRequest(gatheringPointId,
- gatheredItem.ItemId, gatheredItem.AlternativeItemId, gatheredItem.ItemCount,
- gatheredItem.Collectability));
+ return gatheringController.Start(new GatheringController.GatheringRequest(Task.gatheringPointId,
+ Task.gatheredItem.ItemId, Task.gatheredItem.AlternativeItemId, Task.gatheredItem.ItemCount,
+ Task.gatheredItem.Collectability));
}
- public ETaskResult Update()
+ public override ETaskResult Update()
{
if (gatheringController.Update() == GatheringController.EStatus.Complete)
return ETaskResult.TaskComplete;
return ETaskResult.StillRunning;
}
-
- public override string ToString()
- {
- if (gatheredItem.Collectability == 0)
- return $"Gather({gatheredItem.ItemCount}x {gatheredItem.ItemId})";
- else
- return
- $"Gather({gatheredItem.ItemCount}x {gatheredItem.ItemId} {SeIconChar.Collectible.ToIconString()} {gatheredItem.Collectability})";
- }
}
/// <summary>
/// </summary>
internal sealed class SkipMarker : ITask
{
- public bool Start() => true;
- public ETaskResult Update() => ETaskResult.TaskComplete;
public override string ToString() => "Gather/SkipMarker";
}
+
+ internal sealed class DoSkip : TaskExecutor<SkipMarker>
+ {
+ protected override bool Start() => true;
+ public override ETaskResult Update() => ETaskResult.TaskComplete;
+ }
}
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;
{
internal sealed class Factory(
MovementController movementController,
- GameFunctions gameFunctions,
- ICondition condition,
- IDataManager dataManager,
IClientState clientState,
AetheryteData aetheryteData,
TerritoryData territoryData,
- ILoggerFactory loggerFactory,
- Mount.Factory mountFactory,
ILogger<Factory> logger) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
}
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 })
{
return [];
}
- public ITask Move(QuestStep step, Vector3 destination)
- {
- return Move(new MoveParams(step, destination));
- }
-
- public ITask Move(MoveParams moveParams)
- {
- return new MoveInternal(moveParams, movementController, mountFactory, gameFunctions,
- loggerFactory.CreateLogger<MoveInternal>(), clientState, dataManager);
- }
-
- public ITask Land()
- {
- return new LandTask(clientState, condition, loggerFactory.CreateLogger<LandTask>());
- }
-
- public ITask ExpectToBeNearDataId(uint dataId, float stopDistance)
- {
- return new WaitForNearDataId(dataId, stopDistance, gameFunctions, clientState);
- }
-
public IEnumerable<ITask> CreateMountTasks(ElementId questId, QuestStep step, Vector3 destination)
{
if (step.InteractionType == EInteractionType.Jump && step.JumpDestination != null &&
yield break;
}
- yield return new WaitConditionTask(() => clientState.TerritoryType == step.TerritoryId,
+ yield return new WaitCondition.Task(() => clientState.TerritoryType == step.TerritoryId,
$"Wait(territory: {territoryData.GetNameAndId(step.TerritoryId)})");
if (!step.DisableNavmesh)
{
- yield return new WaitConditionTask(() => movementController.IsNavmeshReady,
+ yield return new WaitCondition.Task(() => movementController.IsNavmeshReady,
"Wait(navmesh ready)");
- yield return Move(step, destination);
+ yield return new MoveTask(step, destination);
}
else
{
- yield return Move(step, destination);
+ yield return new MoveTask(step, destination);
}
if (step is { Fly: true, Land: true })
- yield return Land();
+ yield return new LandTask();
}
}
- private sealed class MoveInternal : ITask, IToastAware
+ internal sealed class MoveExecutor : TaskExecutor<MoveTask>, IToastAware
{
private readonly string _cannotExecuteAtThisTime;
private readonly MovementController _movementController;
- private readonly Mount.Factory _mountFactory;
private readonly GameFunctions _gameFunctions;
- private readonly ILogger<MoveInternal> _logger;
+ private readonly ILogger<MoveExecutor> _logger;
private readonly IClientState _clientState;
+ private readonly Mount.MountExecutor _mountExecutor;
+ private readonly Mount.UnmountExecutor _unmountExecutor;
- private readonly Action _startAction;
- private readonly Vector3 _destination;
- private readonly MoveParams _moveParams;
+ private Action _startAction = null!;
+ private Vector3 _destination;
private bool _canRestart;
- private ITask? _mountTask;
+ private ITaskExecutor? _nestedExecutor;
- public MoveInternal(MoveParams moveParams,
+ public MoveExecutor(
MovementController movementController,
- Mount.Factory mountFactory,
GameFunctions gameFunctions,
- ILogger<MoveInternal> logger,
+ ILogger<MoveExecutor> logger,
IClientState clientState,
- IDataManager dataManager)
+ IDataManager dataManager,
+ Mount.MountExecutor mountExecutor,
+ Mount.UnmountExecutor unmountExecutor)
{
_movementController = movementController;
- _mountFactory = mountFactory;
_gameFunctions = gameFunctions;
_logger = logger;
_clientState = clientState;
+ _mountExecutor = mountExecutor;
+ _unmountExecutor = unmountExecutor;
_cannotExecuteAtThisTime = dataManager.GetString<LogMessage>(579, x => x.Text)!;
- _destination = moveParams.Destination;
+ }
+
+ private void Initialize()
+ {
+ _destination = Task.Destination;
- if (!gameFunctions.IsFlyingUnlocked(moveParams.TerritoryId))
+ if (!_gameFunctions.IsFlyingUnlocked(Task.TerritoryId))
{
- moveParams = moveParams with { Fly = false, Land = false };
+ Task = Task with { Fly = false, Land = false };
}
- if (!moveParams.DisableNavMesh)
+ if (!Task.DisableNavmesh)
{
_startAction = () =>
- _movementController.NavigateTo(EMovementType.Quest, moveParams.DataId, _destination,
- fly: moveParams.Fly,
- sprint: moveParams.Sprint,
- stopDistance: moveParams.StopDistance,
- ignoreDistanceToObject: moveParams.IgnoreDistanceToObject,
- land: moveParams.Land);
+ _movementController.NavigateTo(EMovementType.Quest, Task.DataId, _destination,
+ fly: Task.Fly,
+ sprint: Task.Sprint,
+ stopDistance: Task.StopDistance,
+ ignoreDistanceToObject: Task.IgnoreDistanceToObject,
+ land: Task.Land);
}
else
{
_startAction = () =>
- _movementController.NavigateTo(EMovementType.Quest, moveParams.DataId, [_destination],
- fly: moveParams.Fly,
- sprint: moveParams.Sprint,
- stopDistance: moveParams.StopDistance,
- ignoreDistanceToObject: moveParams.IgnoreDistanceToObject,
- land: moveParams.Land);
+ _movementController.NavigateTo(EMovementType.Quest, Task.DataId, [_destination],
+ fly: Task.Fly,
+ sprint: Task.Sprint,
+ stopDistance: Task.StopDistance,
+ ignoreDistanceToObject: Task.IgnoreDistanceToObject,
+ land: Task.Land);
}
- _moveParams = moveParams;
- _canRestart = moveParams.RestartNavigation;
+ _canRestart = Task.RestartNavigation;
}
- public InteractionProgressContext? ProgressContext() => _mountTask?.ProgressContext();
-
- public bool ShouldRedoOnInterrupt() => true;
-
- public bool Start()
+ protected override bool Start()
{
- float stopDistance = _moveParams.StopDistance ?? QuestStep.DefaultStopDistance;
+ Initialize();
+
+ float stopDistance = Task.StopDistance ?? QuestStep.DefaultStopDistance;
Vector3? position = _clientState.LocalPlayer?.Position;
float actualDistance = position == null ? float.MaxValue : Vector3.Distance(position.Value, _destination);
- if (_moveParams.Mount == true)
+ if (Task.Mount == true)
{
- var mountTask = _mountFactory.Mount(_moveParams.TerritoryId, Mount.EMountIf.Always);
- if (mountTask.Start())
+ var mountTask = new Mount.MountTask(Task.TerritoryId, Mount.EMountIf.Always);
+ if (_mountExecutor.Start(mountTask))
{
- _mountTask = mountTask;
+ _nestedExecutor = _mountExecutor;
return true;
}
}
- else if (_moveParams.Mount == false)
+ else if (Task.Mount == false)
{
- var mountTask = _mountFactory.Unmount();
- if (mountTask.Start())
+ var mountTask = new Mount.UnmountTask();
+ if (_unmountExecutor.Start(mountTask))
{
- _mountTask = mountTask;
+ _nestedExecutor = _unmountExecutor;
return true;
}
}
- if (!_moveParams.DisableNavMesh)
+ if (!Task.DisableNavmesh)
{
- if (_moveParams.Mount == null)
+ if (Task.Mount == null)
{
Mount.EMountIf mountIf =
- actualDistance > stopDistance && _moveParams.Fly &&
- _gameFunctions.IsFlyingUnlocked(_moveParams.TerritoryId)
+ actualDistance > stopDistance && Task.Fly &&
+ _gameFunctions.IsFlyingUnlocked(Task.TerritoryId)
? Mount.EMountIf.Always
: Mount.EMountIf.AwayFromPosition;
- var mountTask = _mountFactory.Mount(_moveParams.TerritoryId, mountIf, _destination);
- if (mountTask.Start())
+ var mountTask = new Mount.MountTask(Task.TerritoryId, mountIf, _destination);
+ if (_mountExecutor.Start(mountTask))
{
- _mountTask = mountTask;
+ _nestedExecutor = _mountExecutor;
return true;
}
}
}
- _mountTask = new NoOpTask();
+ _nestedExecutor = new NoOpTaskExecutor();
return true;
}
- public ETaskResult Update()
+ public override ETaskResult Update()
{
- if (_mountTask != null)
+ if (_nestedExecutor != null)
{
- if (_mountTask.Update() == ETaskResult.TaskComplete)
+ if (_nestedExecutor.Update() == ETaskResult.TaskComplete)
{
- _mountTask = null;
+ _nestedExecutor = null;
_logger.LogInformation("Moving to {Destination}", _destination.ToString("G", CultureInfo.InvariantCulture));
_startAction();
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();
return ETaskResult.TaskComplete;
}
- public override string ToString() => $"MoveTo({_destination.ToString("G", CultureInfo.InvariantCulture)})";
public bool OnErrorToast(SeString message)
{
}
}
- private sealed class NoOpTask : ITask
+ private sealed class NoOpTaskExecutor : TaskExecutor<ITask>
{
- public bool Start() => true;
+ protected override bool Start() => true;
- public ETaskResult Update() => ETaskResult.TaskComplete;
+ public override ETaskResult Update() => ETaskResult.TaskComplete;
}
- internal sealed record MoveParams(
+ internal sealed record MoveTask(
ushort TerritoryId,
Vector3 Destination,
bool? Mount = null,
float? StopDistance = null,
uint? DataId = null,
- bool DisableNavMesh = false,
+ bool DisableNavmesh = false,
bool Sprint = true,
bool Fly = false,
bool Land = false,
bool IgnoreDistanceToObject = false,
- bool RestartNavigation = true)
+ bool RestartNavigation = true) : ITask
{
- public MoveParams(QuestStep step, Vector3 destination)
+ public MoveTask(QuestStep step, Vector3 destination)
: this(step.TerritoryId,
destination,
step.Mount,
step.RestartNavigationIfCancelled != false)
{
}
+
+ public override string ToString() => $"MoveTo({Destination.ToString("G", CultureInfo.InvariantCulture)})";
}
- private sealed class WaitForNearDataId(
- uint dataId,
- float stopDistance,
- GameFunctions gameFunctions,
- IClientState clientState) : ITask
+ internal sealed record WaitForNearDataId(uint DataId, float StopDistance) : ITask
{
public bool ShouldRedoOnInterrupt() => true;
+ }
+
+ internal sealed class WaitForNearDataIdExecutor(
+ GameFunctions gameFunctions,
+ IClientState clientState) : TaskExecutor<WaitForNearDataId>
+ {
- public bool Start() => true;
+ protected override bool Start() => true;
- public ETaskResult Update()
+ public override ETaskResult Update()
{
- IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId);
+ IGameObject? gameObject = gameFunctions.FindObjectByDataId(Task.DataId);
if (gameObject == null ||
- (gameObject.Position - clientState.LocalPlayer!.Position).Length() > stopDistance)
+ (gameObject.Position - clientState.LocalPlayer!.Position).Length() > Task.StopDistance)
{
throw new TaskException("Object not found or too far away, no position so we can't move");
}
}
}
- private sealed class LandTask(IClientState clientState, ICondition condition, ILogger<LandTask> logger) : ITask
+ internal sealed class LandTask : ITask
+ {
+ public bool ShouldRedoOnInterrupt() => true;
+ }
+
+ internal sealed class LandExecutor(IClientState clientState, ICondition condition, ILogger<LandExecutor> logger) : TaskExecutor<LandTask>
{
private bool _landing;
private DateTime _continueAt;
- public bool ShouldRedoOnInterrupt() => true;
-
- public bool Start()
+ protected override bool Start()
{
if (!condition[ConditionFlag.InFlight])
{
return true;
}
- public ETaskResult Update()
+ public override ETaskResult Update()
{
if (DateTime.Now < _continueAt)
return ETaskResult.StillRunning;
-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;
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)
{
step.NextQuestId == null)
return null;
- return Check(step, skipConditions, quest.Id);
+ return new SkipTask(step, skipConditions ?? new(), quest.Id);
}
+ }
- private CheckSkip Check(QuestStep step, SkipStepConditions? skipConditions, ElementId questId)
- {
- return new CheckSkip(step, skipConditions ?? new(), questId, loggerFactory.CreateLogger<CheckSkip>(),
- aetheryteFunctions, gameFunctions, questFunctions, clientState);
- }
+ internal sealed record SkipTask(
+ QuestStep Step,
+ SkipStepConditions SkipConditions,
+ ElementId ElementId) : ITask
+ {
+ public override string ToString() => "CheckSkip";
}
- private sealed class CheckSkip(
- QuestStep step,
- SkipStepConditions skipConditions,
- ElementId elementId,
+ internal sealed class CheckSkip(
ILogger<CheckSkip> logger,
AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions,
QuestFunctions questFunctions,
- IClientState clientState) : ITask
+ IClientState clientState) : TaskExecutor<SkipTask>
{
- public unsafe bool Start()
+ protected override unsafe bool Start()
{
+ var skipConditions = Task.SkipConditions;
+ var step = Task.Step;
+ var elementId = Task.ElementId;
+
logger.LogInformation("Checking skip conditions; {ConfiguredConditions}", string.Join(",", skipConditions));
if (skipConditions.Flying == ELockedSkipCondition.Unlocked &&
}
}
- 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)
return false;
}
- public ETaskResult Update() => ETaskResult.SkipRemainingTasksForStep;
-
- public override string ToString() => "CheckSkip";
+ public override ETaskResult Update() => ETaskResult.SkipRemainingTasksForStep;
}
}
internal static class StepDisabled
{
- internal sealed class Factory(ILoggerFactory loggerFactory) : SimpleTaskFactory
+ internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (!step.Disabled)
return null;
- return new Task(loggerFactory.CreateLogger<Task>());
+ return new SkipRemainingTasks();
}
}
- internal sealed class Task(ILogger<Task> logger) : ITask
+ internal sealed class SkipRemainingTasks : ITask
{
- public bool Start() => true;
+ public override string ToString() => "StepDisabled";
+ }
+
+ internal sealed class Executor(ILogger<SkipRemainingTasks> logger) : TaskExecutor<SkipRemainingTasks>
+ {
+ protected override bool Start() => true;
- public ETaskResult Update()
+ public override ETaskResult Update()
{
logger.LogInformation("Skipping step, as it is disabled");
return ETaskResult.SkipRemainingTasksForStep;
}
-
- public override string ToString() => "StepDisabled";
}
}
namespace Questionable.Controller.Steps.Shared;
-internal sealed class SwitchClassJob(EClassJob classJob, IClientState clientState) : AbstractDelayedTask
+internal static class SwitchClassJob
{
- protected override unsafe bool StartInternal()
+ internal sealed record Task(EClassJob ClassJob) : ITask
{
- if (clientState.LocalPlayer!.ClassJob.Id == (uint)classJob)
- return false;
+ public override string ToString() => $"SwitchJob({ClassJob})";
+ }
- var gearsetModule = RaptureGearsetModule.Instance();
- if (gearsetModule != null)
+ internal sealed class Executor(IClientState clientState) : AbstractDelayedTaskExecutor<Task>
+ {
+ protected override unsafe bool StartInternal()
{
- for (int i = 0; i < 100; ++i)
+ if (clientState.LocalPlayer!.ClassJob.Id == (uint)Task.ClassJob)
+ return false;
+
+ var gearsetModule = RaptureGearsetModule.Instance();
+ if (gearsetModule != null)
{
- var gearset = gearsetModule->GetGearset(i);
- if (gearset->ClassJob == (byte)classJob)
+ for (int i = 0; i < 100; ++i)
{
- gearsetModule->EquipGearset(gearset->Id);
- return true;
+ var gearset = gearsetModule->GetGearset(i);
+ if (gearset->ClassJob == (byte)Task.ClassJob)
+ {
+ gearsetModule->EquipGearset(gearset->Id);
+ return true;
+ }
}
}
+
+ throw new TaskException($"No gearset found for {Task.ClassJob}");
}
- throw new TaskException($"No gearset found for {classJob}");
+ protected override ETaskResult UpdateInternal() => ETaskResult.TaskComplete;
}
-
- protected override ETaskResult UpdateInternal() => ETaskResult.TaskComplete;
-
- public override string ToString() => $"SwitchJob({classJob})";
}
internal sealed class Factory(
IClientState clientState,
ICondition condition,
- TerritoryData territoryData,
- QuestFunctions questFunctions,
- GameFunctions gameFunctions)
+ TerritoryData territoryData)
: ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
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)];
}
{
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(),
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)
];
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)
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)];
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)];
}
}
- internal sealed class WaitDelay(TimeSpan? delay = null) : AbstractDelayedTask(delay ?? TimeSpan.FromSeconds(1))
+ internal sealed record WaitDelay(TimeSpan Delay) : ITask
{
- protected override bool StartInternal() => true;
+ public WaitDelay()
+ : this(TimeSpan.FromSeconds(1))
+ {
+ }
public override string ToString() => $"Wait(seconds: {Delay.TotalSeconds})";
}
+ internal sealed class WaitDelayExecutor : AbstractDelayedTaskExecutor<WaitDelay>
+ {
+ protected override bool StartInternal()
+ {
+ Delay = Task.Delay;
+ return true;
+ }
+ }
+
internal sealed class WaitNextStepOrSequence : ITask
{
- public bool Start() => true;
+ public override string ToString() => "Wait(next step or sequence)";
+ }
+
+ internal sealed class WaitNextStepOrSequenceExecutor : TaskExecutor<WaitNextStepOrSequence>
+ {
+ protected override bool Start() => true;
- public ETaskResult Update() => ETaskResult.StillRunning;
+ public override ETaskResult Update() => ETaskResult.StillRunning;
+ }
- public override string ToString() => "Wait(next step or sequence)";
+ internal sealed record WaitForCompletionFlags(QuestId Quest, QuestStep Step) : ITask
+ {
+ public override string ToString() =>
+ $"Wait(QW: {string.Join(", ", Step.CompletionQuestVariablesFlags.Select(x => x?.ToString() ?? "-"))})";
}
- internal sealed class WaitForCompletionFlags(QuestId quest, QuestStep step, QuestFunctions questFunctions) : ITask
+ internal sealed class WaitForCompletionFlagsExecutor(QuestFunctions questFunctions)
+ : TaskExecutor<WaitForCompletionFlags>
{
- public bool Start() => true;
+ protected override bool Start() => true;
- public ETaskResult Update()
+ public override ETaskResult Update()
{
- QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(quest);
+ QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(Task.Quest);
return questWork != null &&
- QuestWorkUtils.MatchesQuestWork(step.CompletionQuestVariablesFlags, questWork)
+ QuestWorkUtils.MatchesQuestWork(Task.Step.CompletionQuestVariablesFlags, questWork)
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
}
+ }
+ internal sealed record WaitObjectAtPosition(
+ uint DataId,
+ Vector3 Destination,
+ float Distance) : ITask
+ {
public override string ToString() =>
- $"Wait(QW: {string.Join(", ", step.CompletionQuestVariablesFlags.Select(x => x?.ToString() ?? "-"))})";
+ $"WaitObj({DataId} at {Destination.ToString("G", CultureInfo.InvariantCulture)} < {Distance})";
}
- private sealed class WaitObjectAtPosition(
- uint dataId,
- Vector3 destination,
- float distance,
- GameFunctions gameFunctions) : ITask
+ internal sealed class WaitObjectAtPositionExecutor(GameFunctions gameFunctions) : TaskExecutor<WaitObjectAtPosition>
{
- public bool Start() => true;
+ protected override bool Start() => true;
- public ETaskResult Update() =>
- gameFunctions.IsObjectAtPosition(dataId, destination, distance)
+ public override ETaskResult Update() =>
+ gameFunctions.IsObjectAtPosition(Task.DataId, Task.Destination, Task.Distance)
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
+ }
- public override string ToString() =>
- $"WaitObj({dataId} at {destination.ToString("G", CultureInfo.InvariantCulture)} < {distance})";
+ internal sealed record WaitQuestAccepted(ElementId ElementId) : ITask
+ {
+ public override string ToString() => $"WaitQuestAccepted({ElementId})";
}
- internal sealed class WaitQuestAccepted(ElementId elementId, QuestFunctions questFunctions) : ITask
+ internal sealed class WaitQuestAcceptedExecutor(QuestFunctions questFunctions) : TaskExecutor<WaitQuestAccepted>
{
- public bool Start() => true;
+ protected override bool Start() => true;
- public ETaskResult Update()
+ public override ETaskResult Update()
{
- return questFunctions.IsQuestAccepted(elementId)
+ return questFunctions.IsQuestAccepted(Task.ElementId)
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
}
+ }
- public override string ToString() => $"WaitQuestAccepted({elementId})";
+ internal sealed record WaitQuestCompleted(ElementId ElementId) : ITask
+ {
+ public override string ToString() => $"WaitQuestComplete({ElementId})";
}
- internal sealed class WaitQuestCompleted(ElementId elementId, QuestFunctions questFunctions) : ITask
+ internal sealed class WaitQuestCompletedExecutor(QuestFunctions questFunctions) : TaskExecutor<WaitQuestCompleted>
{
- public bool Start() => true;
+ protected override bool Start() => true;
- public ETaskResult Update()
+ public override ETaskResult Update()
{
- return questFunctions.IsQuestComplete(elementId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
+ return questFunctions.IsQuestComplete(Task.ElementId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
}
-
- public override string ToString() => $"WaitQuestComplete({elementId})";
}
- internal sealed class NextStep(ElementId elementId, int sequence) : ILastTask
+ internal sealed record NextStep(ElementId ElementId, int Sequence) : ILastTask
{
- public ElementId ElementId { get; } = elementId;
- public int Sequence { get; } = sequence;
-
- public bool Start() => true;
+ public override string ToString() => "NextStep";
+ }
- public ETaskResult Update() => ETaskResult.NextStep;
+ internal sealed class NextStepExecutor : TaskExecutor<NextStep>
+ {
+ protected override bool Start() => true;
- public override string ToString() => "NextStep";
+ public override ETaskResult Update() => ETaskResult.NextStep;
}
internal sealed class EndAutomation : ILastTask
public ElementId ElementId => throw new InvalidOperationException();
public int Sequence => throw new InvalidOperationException();
- public bool Start() => true;
+ public override string ToString() => "EndAutomation";
+ }
+ internal sealed class EndAutomationExecutor : TaskExecutor<EndAutomation>
+ {
- public ETaskResult Update() => ETaskResult.End;
+ protected override bool Start() => true;
- public override string ToString() => "EndAutomation";
+ public override ETaskResult Update() => ETaskResult.End;
}
}
}
}
- internal sealed class WaitDelay(TimeSpan delay) : AbstractDelayedTask(delay)
- {
- protected override bool StartInternal() => true;
+ internal sealed record WaitDelay(TimeSpan Delay) : ITask
+ {
public override string ToString() => $"Wait[S](seconds: {Delay.TotalSeconds})";
}
+
+ internal sealed class WaitDelayExecutor : AbstractDelayedTaskExecutor<WaitDelay>
+ {
+ protected override bool StartInternal()
+ {
+ Delay = Task.Delay;
+ return true;
+ }
+ }
+
}
--- /dev/null
+using System;
+
+namespace Questionable.Controller.Steps;
+
+internal interface ITaskExecutor
+{
+ ITask CurrentTask { get; }
+
+ Type GetTaskType();
+
+ bool Start(ITask task);
+
+ bool WasInterrupted();
+
+ ETaskResult Update();
+}
+
+internal abstract class TaskExecutor<T> : ITaskExecutor
+ where T : class, ITask
+{
+ protected T Task { get; set; } = null!;
+ protected InteractionProgressContext? ProgressContext { get; set; }
+ ITask ITaskExecutor.CurrentTask => Task;
+
+ public bool WasInterrupted()
+ {
+ if (ProgressContext is {} progressContext)
+ {
+ progressContext.Update();
+ return progressContext.WasInterrupted();
+ }
+
+ return false;
+ }
+
+ public Type GetTaskType() => typeof(T);
+
+ protected abstract bool Start();
+
+ public bool Start(ITask task)
+ {
+ if (task is T t)
+ {
+ Task = t;
+ return Start();
+ }
+ throw new TaskException($"Unable to cast {task.GetType()} to {typeof(T)}");
+ }
+
+ public abstract ETaskResult Update();
+}
{
private readonly List<ITask> _completedTasks = [];
private readonly List<ITask> _tasks = [];
- public ITask? CurrentTask { get; set; }
+ public ITaskExecutor? CurrentTaskExecutor { get; set; }
public IEnumerable<ITask> RemainingTasks => _tasks;
- public bool AllTasksComplete => CurrentTask == null && _tasks.Count == 0;
+ public bool AllTasksComplete => CurrentTaskExecutor == null && _tasks.Count == 0;
public void Enqueue(ITask task)
{
{
_tasks.Clear();
_completedTasks.Clear();
- CurrentTask = null;
+ CurrentTaskExecutor = null;
}
public void InterruptWith(List<ITask> interruptionTasks)
List<ITask?> newTasks =
[
..interruptionTasks,
- .._completedTasks.Where(x => !ReferenceEquals(x, CurrentTask)).ToList(),
- CurrentTask,
+ .._completedTasks.Where(x => !ReferenceEquals(x, CurrentTaskExecutor?.CurrentTask)).ToList(),
+ CurrentTaskExecutor?.CurrentTask,
.._tasks
];
Reset();
{
ArgumentNullException.ThrowIfNull(pluginInterface);
ArgumentNullException.ThrowIfNull(chatGui);
+
try
{
ServiceCollection serviceCollection = new();
private static void AddTaskFactories(ServiceCollection serviceCollection)
{
// individual tasks
- serviceCollection.AddTransient<MoveToLandingLocation>();
- serviceCollection.AddTransient<DoGather>();
- serviceCollection.AddTransient<DoGatherCollectable>();
- serviceCollection.AddTransient<SwitchClassJob>();
- serviceCollection.AddSingleton<Mount.Factory>();
+ serviceCollection.AddTaskExecutor<MoveToLandingLocation.Task, MoveToLandingLocation.Executor>();
+ serviceCollection.AddTaskExecutor<DoGather.Task, DoGather.Executor>();
+ serviceCollection.AddTaskExecutor<DoGatherCollectable.Task, DoGatherCollectable.Executor>();
+ serviceCollection.AddTaskExecutor<SwitchClassJob.Task, SwitchClassJob.Executor>();
+ serviceCollection.AddTaskExecutor<Mount.MountTask, Mount.MountExecutor>();
+ serviceCollection.AddTaskExecutor<Mount.UnmountTask, Mount.UnmountExecutor>();
// task factories
- serviceCollection.AddTaskFactory<StepDisabled.Factory>();
+ serviceCollection
+ .AddTaskFactoryAndExecutor<StepDisabled.SkipRemainingTasks, StepDisabled.Factory, StepDisabled.Executor>();
serviceCollection.AddTaskFactory<EquipRecommended.BeforeDutyOrInstance>();
- serviceCollection.AddTaskFactory<Gather.Factory>();
- serviceCollection.AddTaskFactory<AetheryteShortcut.Factory>();
- serviceCollection.AddTaskFactory<SkipCondition.Factory>();
- serviceCollection.AddTaskFactory<AethernetShortcut.Factory>();
- serviceCollection.AddTaskFactory<WaitAtStart.Factory>();
- serviceCollection.AddTaskFactory<MoveTo.Factory>();
-
- serviceCollection.AddTaskFactory<NextQuest.Factory>();
- serviceCollection.AddTaskFactory<AetherCurrent.Factory>();
- serviceCollection.AddTaskFactory<AethernetShard.Factory>();
- serviceCollection.AddTaskFactory<Aetheryte.Factory>();
- serviceCollection.AddTaskFactory<Combat.Factory>();
- serviceCollection.AddTaskFactory<Duty.Factory>();
+ serviceCollection.AddTaskFactoryAndExecutor<Gather.GatheringTask, Gather.Factory, Gather.StartGathering>();
+ serviceCollection.AddTaskExecutor<Gather.SkipMarker, Gather.DoSkip>();
+ serviceCollection
+ .AddTaskFactoryAndExecutor<AetheryteShortcut.Task, AetheryteShortcut.Factory,
+ AetheryteShortcut.UseAetheryteShortcut>();
+ serviceCollection
+ .AddTaskFactoryAndExecutor<SkipCondition.SkipTask, SkipCondition.Factory, SkipCondition.CheckSkip>();
+ serviceCollection
+ .AddTaskFactoryAndExecutor<AethernetShortcut.Task, AethernetShortcut.Factory,
+ AethernetShortcut.UseAethernetShortcut>();
+ serviceCollection
+ .AddTaskFactoryAndExecutor<WaitAtStart.WaitDelay, WaitAtStart.Factory, WaitAtStart.WaitDelayExecutor>();
+ serviceCollection.AddTaskFactoryAndExecutor<MoveTo.MoveTask, MoveTo.Factory, MoveTo.MoveExecutor>();
+ serviceCollection.AddTaskExecutor<MoveTo.WaitForNearDataId, MoveTo.WaitForNearDataIdExecutor>();
+ serviceCollection.AddTaskExecutor<MoveTo.LandTask, MoveTo.LandExecutor>();
+
+ serviceCollection.AddTaskFactoryAndExecutor<NextQuest.SetQuestTask, NextQuest.Factory, NextQuest.Executor>();
+ serviceCollection
+ .AddTaskFactoryAndExecutor<AetherCurrent.Attune, AetherCurrent.Factory, AetherCurrent.DoAttune>();
+ serviceCollection
+ .AddTaskFactoryAndExecutor<AethernetShard.Attune, AethernetShard.Factory, AethernetShard.DoAttune>();
+ serviceCollection.AddTaskFactoryAndExecutor<Aetheryte.Attune, Aetheryte.Factory, Aetheryte.DoAttune>();
+ serviceCollection.AddTaskFactoryAndExecutor<Combat.Task, Combat.Factory, Combat.HandleCombat>();
+ serviceCollection.AddTaskFactoryAndExecutor<Duty.Task, Duty.Factory, Duty.Executor>();
serviceCollection.AddTaskFactory<Emote.Factory>();
- serviceCollection.AddTaskFactory<Action.Factory>();
- serviceCollection.AddTaskFactory<Interact.Factory>();
+ serviceCollection.AddTaskExecutor<Emote.UseOnObject, Emote.UseOnObjectExecutor>();
+ serviceCollection.AddTaskExecutor<Emote.UseOnSelf, Emote.UseOnSelfExecutor>();
+ serviceCollection.AddTaskFactoryAndExecutor<Action.UseOnObject, Action.Factory, Action.UseOnObjectExecutor>();
+ serviceCollection.AddTaskFactoryAndExecutor<Interact.Task, Interact.Factory, Interact.DoInteract>();
serviceCollection.AddTaskFactory<Jump.Factory>();
- serviceCollection.AddTaskFactory<Dive.Factory>();
- serviceCollection.AddTaskFactory<Say.Factory>();
+ serviceCollection.AddTaskExecutor<Jump.SingleJumpTask, Jump.DoSingleJump>();
+ serviceCollection.AddTaskExecutor<Jump.RepeatedJumpTask, Jump.DoRepeatedJumps>();
+ serviceCollection.AddTaskFactoryAndExecutor<Dive.Task, Dive.Factory, Dive.DoDive>();
+ serviceCollection.AddTaskFactoryAndExecutor<Say.Task, Say.Factory, Say.UseChat>();
serviceCollection.AddTaskFactory<UseItem.Factory>();
- serviceCollection.AddTaskFactory<EquipItem.Factory>();
- serviceCollection.AddTaskFactory<EquipRecommended.Factory>();
- serviceCollection.AddTaskFactory<Craft.Factory>();
- serviceCollection.AddTaskFactory<TurnInDelivery.Factory>();
+ serviceCollection.AddTaskExecutor<UseItem.UseOnGround, UseItem.UseOnGroundExecutor>();
+ serviceCollection.AddTaskExecutor<UseItem.UseOnPosition, UseItem.UseOnPositionExecutor>();
+ serviceCollection.AddTaskExecutor<UseItem.UseOnObject, UseItem.UseOnObjectExecutor>();
+ serviceCollection.AddTaskExecutor<UseItem.UseOnSelf, UseItem.UseOnSelfExecutor>();
+ serviceCollection.AddTaskFactoryAndExecutor<EquipItem.Task, EquipItem.Factory, EquipItem.Executor>();
+ serviceCollection
+ .AddTaskFactoryAndExecutor<EquipRecommended.EquipTask, EquipRecommended.Factory,
+ EquipRecommended.DoEquipRecommended>();
+ serviceCollection.AddTaskFactoryAndExecutor<Craft.CraftTask, Craft.Factory, Craft.DoCraft>();
+ serviceCollection
+ .AddTaskFactoryAndExecutor<TurnInDelivery.Task, TurnInDelivery.Factory,
+ TurnInDelivery.SatisfactionSupplyTurnIn>();
+
serviceCollection.AddTaskFactory<InitiateLeve.Factory>();
+ serviceCollection.AddTaskExecutor<InitiateLeve.SkipInitiateIfActive, InitiateLeve.SkipInitiateIfActiveExecutor>();
+ serviceCollection.AddTaskExecutor<InitiateLeve.OpenJournal, InitiateLeve.OpenJournalExecutor>();
+ serviceCollection.AddTaskExecutor<InitiateLeve.Initiate, InitiateLeve.InitiateExecutor>();
+ serviceCollection.AddTaskExecutor<InitiateLeve.SelectDifficulty, InitiateLeve.SelectDifficultyExecutor>();
+ serviceCollection.AddTaskExecutor<WaitCondition.Task, WaitCondition.Executor>();
serviceCollection.AddTaskFactory<WaitAtEnd.Factory>();
- serviceCollection.AddTransient<WaitAtEnd.WaitQuestAccepted>();
- serviceCollection.AddTransient<WaitAtEnd.WaitQuestCompleted>();
+ serviceCollection.AddTaskExecutor<WaitAtEnd.WaitDelay, WaitAtEnd.WaitDelayExecutor>();
+ serviceCollection.AddTaskExecutor<WaitAtEnd.WaitNextStepOrSequence, WaitAtEnd.WaitNextStepOrSequenceExecutor>();
+ serviceCollection.AddTaskExecutor<WaitAtEnd.WaitForCompletionFlags, WaitAtEnd.WaitForCompletionFlagsExecutor>();
+ serviceCollection.AddTaskExecutor<WaitAtEnd.WaitObjectAtPosition, WaitAtEnd.WaitObjectAtPositionExecutor>();
+ serviceCollection.AddTaskExecutor<WaitAtEnd.WaitQuestAccepted, WaitAtEnd.WaitQuestAcceptedExecutor>();
+ serviceCollection.AddTaskExecutor<WaitAtEnd.WaitQuestCompleted, WaitAtEnd.WaitQuestCompletedExecutor>();
+ serviceCollection.AddTaskExecutor<WaitAtEnd.NextStep, WaitAtEnd.NextStepExecutor>();
+ serviceCollection.AddTaskExecutor<WaitAtEnd.EndAutomation, WaitAtEnd.EndAutomationExecutor>();
serviceCollection.AddSingleton<TaskCreator>();
}
-using JetBrains.Annotations;
+using Dalamud.Plugin.Services;
+using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using Questionable.Controller.Steps;
internal static class ServiceCollectionExtensions
{
public static void AddTaskFactory<
- [MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] TFactory>(
+ [MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
+ TFactory>(
this IServiceCollection serviceCollection)
where TFactory : class, ITaskFactory
{
serviceCollection.AddSingleton<ITaskFactory, TFactory>();
serviceCollection.AddSingleton<TFactory>();
}
+
+ public static void AddTaskExecutor<T,
+ [MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
+ TExecutor>(
+ this IServiceCollection serviceCollection)
+ where T : class, ITask
+ where TExecutor : TaskExecutor<T>
+ {
+ serviceCollection.AddKeyedTransient<ITaskExecutor, TExecutor>(typeof(T));
+ serviceCollection.AddTransient<TExecutor>();
+ }
+
+ public static void AddTaskFactoryAndExecutor<T,
+ [MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
+ TFactory,
+ [MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
+ TExecutor>(
+ this IServiceCollection serviceCollection)
+ where TFactory : class, ITaskFactory
+ where T : class, ITask
+ where TExecutor : TaskExecutor<T>
+ {
+ serviceCollection.AddTaskFactory<TFactory>();
+ serviceCollection.AddTaskExecutor<T, TExecutor>();
+ }
}