Make task logic stateless to support rewind

master
Liza 2024-09-18 22:40:12 +02:00
parent 721f9617a3
commit 7e9070950e
Signed by: liza
GPG Key ID: 7199F8D727D55F67
48 changed files with 1559 additions and 1512 deletions

View File

@ -121,7 +121,7 @@ public sealed class RendererPlugin : IDalamudPlugin
if (!directory.Exists) if (!directory.Exists)
return; return;
_pluginLog.Information($"Loading locations from {directory}"); //_pluginLog.Information($"Loading locations from {directory}");
foreach (FileInfo fileInfo in directory.GetFiles("*.json")) foreach (FileInfo fileInfo in directory.GetFiles("*.json"))
{ {
try try

View File

@ -285,7 +285,7 @@ internal sealed class InteractionUiController : IDisposable
List<DialogueChoiceInfo> dialogueChoices = []; List<DialogueChoiceInfo> dialogueChoices = [];
// levequest choices have some vague sort of priority // 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.Quest != null &&
interact.InteractionType is EInteractionType.AcceptLeve or EInteractionType.CompleteLeve) interact.InteractionType is EInteractionType.AcceptLeve or EInteractionType.CompleteLeve)
{ {
@ -799,7 +799,7 @@ internal sealed class InteractionUiController : IDisposable
private void TeleportTownPostSetup(AddonEvent type, AddonArgs args) private void TeleportTownPostSetup(AddonEvent type, AddonArgs args)
{ {
if (ShouldHandleUiInteractions && if (ShouldHandleUiInteractions &&
_questController.HasCurrentTaskMatching(out AethernetShortcut.UseAethernetShortcut? aethernetShortcut) && _questController.HasCurrentTaskMatching(out AethernetShortcut.Task? aethernetShortcut) &&
aethernetShortcut.From.IsFirmamentAetheryte()) aethernetShortcut.From.IsFirmamentAetheryte())
{ {
// this might be better via atkvalues; but this works for now // this might be better via atkvalues; but this works for now

View File

@ -13,16 +13,13 @@ using FFXIVClientStructs.FFXIV.Client.Game.Event;
using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Game.UI;
using LLib; using LLib;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps; using Questionable.Controller.Steps;
using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Gathering; using Questionable.Controller.Steps.Gathering;
using Questionable.Controller.Steps.Interactions; using Questionable.Controller.Steps.Interactions;
using Questionable.Controller.Steps.Shared; using Questionable.Controller.Steps.Shared;
using Questionable.External; using Questionable.External;
using Questionable.Functions; using Questionable.Functions;
using Questionable.GatheringPaths;
using Questionable.Model.Gathering; using Questionable.Model.Gathering;
using Questionable.Model.Questing; using Questionable.Model.Questing;
using Mount = Questionable.Controller.Steps.Common.Mount; using Mount = Questionable.Controller.Steps.Common.Mount;
@ -32,17 +29,11 @@ namespace Questionable.Controller;
internal sealed unsafe class GatheringController : MiniTaskController<GatheringController> internal sealed unsafe class GatheringController : MiniTaskController<GatheringController>
{ {
private readonly MovementController _movementController; 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 GatheringPointRegistry _gatheringPointRegistry;
private readonly GameFunctions _gameFunctions; private readonly GameFunctions _gameFunctions;
private readonly NavmeshIpc _navmeshIpc; private readonly NavmeshIpc _navmeshIpc;
private readonly IObjectTable _objectTable; private readonly IObjectTable _objectTable;
private readonly ICondition _condition; private readonly ICondition _condition;
private readonly ILoggerFactory _loggerFactory;
private readonly IGameGui _gameGui;
private readonly IClientState _clientState;
private readonly ILogger<GatheringController> _logger; private readonly ILogger<GatheringController> _logger;
private readonly Regex _revisitRegex; private readonly Regex _revisitRegex;
@ -50,10 +41,6 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
public GatheringController( public GatheringController(
MovementController movementController, MovementController movementController,
MoveTo.Factory moveFactory,
Mount.Factory mountFactory,
Combat.Factory combatFactory,
Interact.Factory interactFactory,
GatheringPointRegistry gatheringPointRegistry, GatheringPointRegistry gatheringPointRegistry,
GameFunctions gameFunctions, GameFunctions gameFunctions,
NavmeshIpc navmeshIpc, NavmeshIpc navmeshIpc,
@ -61,25 +48,17 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
IChatGui chatGui, IChatGui chatGui,
ILogger<GatheringController> logger, ILogger<GatheringController> logger,
ICondition condition, ICondition condition,
IServiceProvider serviceProvider,
IDataManager dataManager, IDataManager dataManager,
ILoggerFactory loggerFactory,
IGameGui gameGui,
IClientState clientState,
IPluginLog pluginLog) IPluginLog pluginLog)
: base(chatGui, mountFactory, combatFactory, condition, logger) : base(chatGui, condition, serviceProvider, logger)
{ {
_movementController = movementController; _movementController = movementController;
_moveFactory = moveFactory;
_mountFactory = mountFactory;
_interactFactory = interactFactory;
_gatheringPointRegistry = gatheringPointRegistry; _gatheringPointRegistry = gatheringPointRegistry;
_gameFunctions = gameFunctions; _gameFunctions = gameFunctions;
_navmeshIpc = navmeshIpc; _navmeshIpc = navmeshIpc;
_objectTable = objectTable; _objectTable = objectTable;
_condition = condition; _condition = condition;
_loggerFactory = loggerFactory;
_gameGui = gameGui;
_clientState = clientState;
_logger = logger; _logger = logger;
_revisitRegex = dataManager.GetRegex<LogMessage>(5574, x => x.Text, pluginLog) _revisitRegex = dataManager.GetRegex<LogMessage>(5574, x => x.Text, pluginLog)
@ -170,7 +149,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
return; return;
ushort territoryId = _currentRequest.Root.Steps.Last().TerritoryId; 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)) && bool fly = currentNode.Fly.GetValueOrDefault(_currentRequest.Root.FlyBetweenNodes.GetValueOrDefault(true)) &&
_gameFunctions.IsFlyingUnlocked(territoryId); _gameFunctions.IsFlyingUnlocked(territoryId);
@ -187,14 +166,13 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
if (pointOnFloor != null) if (pointOnFloor != null)
pointOnFloor = pointOnFloor.Value with { Y = pointOnFloor.Value.Y + (fly ? 3f : 0f) }; pointOnFloor = pointOnFloor.Value with { Y = pointOnFloor.Value.Y + (fly ? 3f : 0f) };
_taskQueue.Enqueue(_moveFactory.Move(new MoveTo.MoveParams(territoryId, pointOnFloor ?? averagePosition, _taskQueue.Enqueue(new MoveTo.MoveTask(territoryId, pointOnFloor ?? averagePosition,
null, 50f, Fly: fly, IgnoreDistanceToObject: true))); null, 50f, Fly: fly, IgnoreDistanceToObject: true));
} }
_taskQueue.Enqueue(new MoveToLandingLocation(territoryId, fly, currentNode, _moveFactory, _gameFunctions, _taskQueue.Enqueue(new MoveToLandingLocation.Task(territoryId, fly, currentNode));
_objectTable, _loggerFactory.CreateLogger<MoveToLandingLocation>())); _taskQueue.Enqueue(new Mount.UnmountTask());
_taskQueue.Enqueue(_mountFactory.Unmount()); _taskQueue.Enqueue(new Interact.Task(currentNode.DataId, null, EInteractionType.Gather, true));
_taskQueue.Enqueue(_interactFactory.Interact(currentNode.DataId, null, EInteractionType.Gather, true));
QueueGatherNode(currentNode); QueueGatherNode(currentNode);
} }
@ -203,12 +181,10 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
{ {
foreach (bool revisitRequired in new[] { false, true }) foreach (bool revisitRequired in new[] { false, true })
{ {
_taskQueue.Enqueue(new DoGather(_currentRequest!.Data, currentNode, revisitRequired, this, _gameFunctions, _taskQueue.Enqueue(new DoGather.Task(_currentRequest!.Data, currentNode, revisitRequired));
_gameGui, _clientState, _condition, _loggerFactory.CreateLogger<DoGather>()));
if (_currentRequest.Data.Collectability > 0) if (_currentRequest.Data.Collectability > 0)
{ {
_taskQueue.Enqueue(new DoGatherCollectable(_currentRequest.Data, currentNode, revisitRequired, this, _taskQueue.Enqueue(new DoGatherCollectable.Task(_currentRequest.Data, currentNode, revisitRequired));
_gameFunctions, _clientState, _gameGui, _loggerFactory.CreateLogger<DoGatherCollectable>()));
} }
} }
} }
@ -269,7 +245,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
public override IList<string> GetRemainingTaskNames() public override IList<string> GetRemainingTaskNames()
{ {
if (_taskQueue.CurrentTask is {} currentTask) if (_taskQueue.CurrentTaskExecutor?.CurrentTask is {} currentTask)
return [currentTask.ToString() ?? "?", .. base.GetRemainingTaskNames()]; return [currentTask.ToString() ?? "?", .. base.GetRemainingTaskNames()];
else else
return base.GetRemainingTaskNames(); return base.GetRemainingTaskNames();
@ -279,7 +255,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
{ {
if (_revisitRegex.IsMatch(message.TextValue)) if (_revisitRegex.IsMatch(message.TextValue))
{ {
if (_taskQueue.CurrentTask is IRevisitAware currentTaskRevisitAware) if (_taskQueue.CurrentTaskExecutor?.CurrentTask is IRevisitAware currentTaskRevisitAware)
currentTaskRevisitAware.OnRevisit(); currentTaskRevisitAware.OnRevisit();
foreach (ITask task in _taskQueue.RemainingTasks) foreach (ITask task in _taskQueue.RemainingTasks)

View File

@ -94,7 +94,7 @@ internal sealed class GatheringPointRegistry : IDisposable
private void LoadGatheringPointFromStream(string fileName, Stream stream) 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); GatheringPointId? gatheringPointId = ExtractGatheringPointIdFromName(fileName);
if (gatheringPointId == null) if (gatheringPointId == null)
return; return;
@ -110,7 +110,7 @@ internal sealed class GatheringPointRegistry : IDisposable
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")) foreach (FileInfo fileInfo in directory.GetFiles("*.json"))
{ {
try try

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps; using Questionable.Controller.Steps;
using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Common;
@ -17,33 +18,33 @@ internal abstract class MiniTaskController<T>
protected readonly TaskQueue _taskQueue = new(); protected readonly TaskQueue _taskQueue = new();
private readonly IChatGui _chatGui; private readonly IChatGui _chatGui;
private readonly Mount.Factory _mountFactory;
private readonly Combat.Factory _combatFactory;
private readonly ICondition _condition; private readonly ICondition _condition;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<T> _logger; private readonly ILogger<T> _logger;
protected MiniTaskController(IChatGui chatGui, Mount.Factory mountFactory, Combat.Factory combatFactory, protected MiniTaskController(IChatGui chatGui, ICondition condition, IServiceProvider serviceProvider,
ICondition condition, ILogger<T> logger) ILogger<T> logger)
{ {
_chatGui = chatGui; _chatGui = chatGui;
_logger = logger; _logger = logger;
_mountFactory = mountFactory; _serviceProvider = serviceProvider;
_combatFactory = combatFactory;
_condition = condition; _condition = condition;
} }
protected virtual void UpdateCurrentTask() protected virtual void UpdateCurrentTask()
{ {
if (_taskQueue.CurrentTask == null) if (_taskQueue.CurrentTaskExecutor == null)
{ {
if (_taskQueue.TryDequeue(out ITask? upcomingTask)) if (_taskQueue.TryDequeue(out ITask? upcomingTask))
{ {
try try
{ {
_logger.LogInformation("Starting task {TaskName}", upcomingTask.ToString()); _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; return;
} }
else else
@ -68,19 +69,20 @@ internal abstract class MiniTaskController<T>
ETaskResult result; ETaskResult result;
try try
{ {
if (_taskQueue.CurrentTask.WasInterrupted()) if (_taskQueue.CurrentTaskExecutor.WasInterrupted())
{ {
InterruptQueueWithCombat(); InterruptQueueWithCombat();
return; return;
} }
result = _taskQueue.CurrentTask.Update(); result = _taskQueue.CurrentTaskExecutor.Update();
} }
catch (Exception e) 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( _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"); Stop("Task failed to update");
return; return;
} }
@ -92,14 +94,16 @@ internal abstract class MiniTaskController<T>
case ETaskResult.SkipRemainingTasksForStep: case ETaskResult.SkipRemainingTasksForStep:
_logger.LogInformation("{Task} → {Result}, skipping remaining tasks for step", _logger.LogInformation("{Task} → {Result}, skipping remaining tasks for step",
_taskQueue.CurrentTask, result); _taskQueue.CurrentTaskExecutor.CurrentTask, result);
_taskQueue.CurrentTask = null; _taskQueue.CurrentTaskExecutor = null;
while (_taskQueue.TryDequeue(out ITask? nextTask)) while (_taskQueue.TryDequeue(out ITask? nextTask))
{ {
if (nextTask is ILastTask or Gather.SkipMarker) if (nextTask is ILastTask or Gather.SkipMarker)
{ {
_taskQueue.CurrentTask = nextTask; ITaskExecutor taskExecutor =
_serviceProvider.GetRequiredKeyedService<ITaskExecutor>(nextTask.GetType());
_taskQueue.CurrentTaskExecutor = taskExecutor;
return; return;
} }
} }
@ -108,27 +112,27 @@ internal abstract class MiniTaskController<T>
case ETaskResult.TaskComplete: case ETaskResult.TaskComplete:
_logger.LogInformation("{Task} → {Result}, remaining tasks: {RemainingTaskCount}", _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 // handled in next update
return; return;
case ETaskResult.NextStep: case ETaskResult.NextStep:
_logger.LogInformation("{Task} → {Result}", _taskQueue.CurrentTask, result); _logger.LogInformation("{Task} → {Result}", _taskQueue.CurrentTaskExecutor.CurrentTask, result);
var lastTask = (ILastTask)_taskQueue.CurrentTask; var lastTask = (ILastTask)_taskQueue.CurrentTaskExecutor.CurrentTask;
_taskQueue.CurrentTask = null; _taskQueue.CurrentTaskExecutor = null;
OnNextStep(lastTask); OnNextStep(lastTask);
return; return;
case ETaskResult.End: case ETaskResult.End:
_logger.LogInformation("{Task} → {Result}", _taskQueue.CurrentTask, result); _logger.LogInformation("{Task} → {Result}", _taskQueue.CurrentTaskExecutor.CurrentTask, result);
_taskQueue.CurrentTask = null; _taskQueue.CurrentTaskExecutor = null;
Stop("Task end"); Stop("Task end");
return; return;
} }
@ -154,9 +158,9 @@ internal abstract class MiniTaskController<T>
{ {
List<ITask> tasks = []; List<ITask> tasks = [];
if (_condition[ConditionFlag.Mounted]) 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()); tasks.Add(new WaitAtEnd.WaitDelay());
_taskQueue.InterruptWith(tasks); _taskQueue.InterruptWith(tasks);
} }

View File

@ -82,10 +82,9 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
Configuration configuration, Configuration configuration,
YesAlreadyIpc yesAlreadyIpc, YesAlreadyIpc yesAlreadyIpc,
TaskCreator taskCreator, TaskCreator taskCreator,
Mount.Factory mountFactory, IServiceProvider serviceProvider,
Combat.Factory combatFactory,
IDataManager dataManager) IDataManager dataManager)
: base(chatGui, mountFactory, combatFactory, condition, logger) : base(chatGui, condition, serviceProvider, logger)
{ {
_clientState = clientState; _clientState = clientState;
_gameFunctions = gameFunctions; _gameFunctions = gameFunctions;
@ -219,7 +218,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
return; return;
if (AutomationType == EAutomationType.Automatic && 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 } && CurrentQuest is { Sequence: 0, Step: 0 } or { Sequence: 0, Step: 255 }
&& DateTime.Now >= CurrentQuest.StepProgress.StartedAt.AddSeconds(15)) && DateTime.Now >= CurrentQuest.StepProgress.StartedAt.AddSeconds(15))
{ {
@ -638,15 +637,30 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
public string ToStatString() public string ToStatString()
{ {
return _taskQueue.CurrentTask is { } currentTask return _taskQueue.CurrentTaskExecutor?.CurrentTask is { } currentTask
? $"{currentTask} (+{_taskQueue.RemainingTasks.Count()})" ? $"{currentTask} (+{_taskQueue.RemainingTasks.Count()})"
: $"- (+{_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) public bool HasCurrentTaskMatching<T>([NotNullWhen(true)] out T? task)
where T : class, ITask where T : class, ITask
{ {
if (_taskQueue.CurrentTask is T t) if (_taskQueue.CurrentTaskExecutor?.CurrentTask is T t)
{ {
task = t; task = t;
return true; return true;
@ -699,11 +713,11 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
{ {
lock (_progressLock) lock (_progressLock)
{ {
if (_taskQueue.CurrentTask is ISkippableTask) if (_taskQueue.CurrentTaskExecutor?.CurrentTask is ISkippableTask)
_taskQueue.CurrentTask = null; _taskQueue.CurrentTaskExecutor = null;
else if (_taskQueue.CurrentTask != null) else if (_taskQueue.CurrentTaskExecutor != null)
{ {
_taskQueue.CurrentTask = null; _taskQueue.CurrentTaskExecutor = null;
while (_taskQueue.TryPeek(out ITask? task)) while (_taskQueue.TryPeek(out ITask? task))
{ {
_taskQueue.TryDequeue(out _); _taskQueue.TryDequeue(out _);
@ -727,7 +741,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
public void SkipSimulatedTask() public void SkipSimulatedTask()
{ {
_taskQueue.CurrentTask = null; _taskQueue.CurrentTaskExecutor = null;
} }
public bool IsInterruptible() public bool IsInterruptible()
@ -786,7 +800,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
private void OnConditionChange(ConditionFlag flag, bool value) private void OnConditionChange(ConditionFlag flag, bool value)
{ {
if (_taskQueue.CurrentTask is IConditionChangeAware conditionChangeAware) if (_taskQueue.CurrentTaskExecutor is IConditionChangeAware conditionChangeAware)
conditionChangeAware.OnConditionChange(flag, value); conditionChangeAware.OnConditionChange(flag, value);
} }
@ -798,7 +812,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
private void OnErrorToast(ref SeString message, ref bool isHandled) private void OnErrorToast(ref SeString message, ref bool isHandled)
{ {
_logger.LogWarning("XXX {A} → {B} XXX", _actionCanceledText, message.TextValue); _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)) if (toastAware.OnErrorToast(message))
{ {

View File

@ -142,7 +142,8 @@ internal sealed class QuestRegistry
private void LoadQuestFromStream(string fileName, Stream stream, Quest.ESource source) 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); ElementId? questId = ExtractQuestIdFromName(fileName);
if (questId == null) if (questId == null)
return; return;
@ -173,7 +174,8 @@ internal sealed class QuestRegistry
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")) foreach (FileInfo fileInfo in directory.GetFiles("*.json"))
{ {
try try

View File

@ -2,33 +2,33 @@
namespace Questionable.Controller.Steps.Common; namespace Questionable.Controller.Steps.Common;
internal abstract class AbstractDelayedTask : ITask internal abstract class AbstractDelayedTaskExecutor<T> : TaskExecutor<T>
where T : class, ITask
{ {
private DateTime _continueAt; private DateTime _continueAt;
protected AbstractDelayedTask(TimeSpan delay) protected AbstractDelayedTaskExecutor()
: this(TimeSpan.FromSeconds(5))
{
}
protected AbstractDelayedTaskExecutor(TimeSpan delay)
{ {
Delay = delay; Delay = delay;
} }
protected TimeSpan Delay { get; set; } protected TimeSpan Delay { get; set; }
protected AbstractDelayedTask() protected sealed override bool Start()
: this(TimeSpan.FromSeconds(5))
{
}
public virtual InteractionProgressContext? ProgressContext() => null;
public bool Start()
{ {
bool started = StartInternal();
_continueAt = DateTime.Now.Add(Delay); _continueAt = DateTime.Now.Add(Delay);
return StartInternal(); return started;
} }
protected abstract bool StartInternal(); protected abstract bool StartInternal();
public virtual ETaskResult Update() public override ETaskResult Update()
{ {
if (_continueAt >= DateTime.Now) if (_continueAt >= DateTime.Now)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;

View File

@ -11,54 +11,38 @@ namespace Questionable.Controller.Steps.Common;
internal static class Mount internal static class Mount
{ {
internal sealed class Factory( internal sealed record MountTask(
GameFunctions gameFunctions, ushort TerritoryId,
ICondition condition, EMountIf MountIf,
TerritoryData territoryData, Vector3? Position = null) : ITask
IClientState clientState,
ILoggerFactory loggerFactory)
{ {
public ITask Mount(ushort territoryId, EMountIf mountIf, Vector3? position = null) public Vector3? Position { get; } = MountIf == EMountIf.AwayFromPosition
{ ? Position ?? throw new ArgumentNullException(nameof(Position))
if (mountIf == EMountIf.AwayFromPosition) : null;
ArgumentNullException.ThrowIfNull(position);
return new MountTask(territoryId, mountIf, position, gameFunctions, condition, territoryData, clientState,
loggerFactory.CreateLogger<MountTask>());
}
public ITask Unmount()
{
return new UnmountTask(condition, loggerFactory.CreateLogger<UnmountTask>(), gameFunctions, clientState);
}
}
private sealed class MountTask(
ushort territoryId,
EMountIf mountIf,
Vector3? position,
GameFunctions gameFunctions,
ICondition condition,
TerritoryData territoryData,
IClientState clientState,
ILogger<MountTask> logger) : ITask
{
private bool _mountTriggered;
private InteractionProgressContext? _progressContext;
private DateTime _retryAt = DateTime.MinValue;
public InteractionProgressContext? ProgressContext() => _progressContext;
public bool ShouldRedoOnInterrupt() => true; public bool ShouldRedoOnInterrupt() => true;
public bool Start() public override string ToString() => "Mount";
}
internal sealed class MountExecutor(
GameFunctions gameFunctions,
ICondition condition,
TerritoryData territoryData,
IClientState clientState,
ILogger<MountTask> logger) : TaskExecutor<MountTask>
{
private bool _mountTriggered;
private DateTime _retryAt = DateTime.MinValue;
protected override bool Start()
{ {
if (condition[ConditionFlag.Mounted]) if (condition[ConditionFlag.Mounted])
return false; 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;
} }
@ -68,11 +52,11 @@ internal static class Mount
return false; return false;
} }
if (mountIf == EMountIf.AwayFromPosition) if (Task.MountIf == EMountIf.AwayFromPosition)
{ {
Vector3 playerPosition = clientState.LocalPlayer?.Position ?? Vector3.Zero; Vector3 playerPosition = clientState.LocalPlayer?.Position ?? Vector3.Zero;
float distance = System.Numerics.Vector3.Distance(playerPosition, position.GetValueOrDefault()); float distance = System.Numerics.Vector3.Distance(playerPosition, Task.Position.GetValueOrDefault());
if (territoryId == clientState.TerritoryType && distance < 30f && !Conditions.IsDiving) if (Task.TerritoryId == clientState.TerritoryType && distance < 30f && !Conditions.IsDiving)
{ {
logger.LogInformation("Not using mount, as we're close to the target"); logger.LogInformation("Not using mount, as we're close to the target");
return false; return false;
@ -80,10 +64,10 @@ internal static class Mount
logger.LogInformation( logger.LogInformation(
"Want to use mount if away from destination ({Distance} yalms), trying (in territory {Id})...", "Want to use mount if away from destination ({Distance} yalms), trying (in territory {Id})...",
distance, territoryId); distance, Task.TerritoryId);
} }
else 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]) if (!condition[ConditionFlag.InCombat])
{ {
@ -94,7 +78,7 @@ internal static class Mount
return false; return false;
} }
public ETaskResult Update() public override ETaskResult Update()
{ {
if (_mountTriggered && !condition[ConditionFlag.Mounted] && DateTime.Now > _retryAt) if (_mountTriggered && !condition[ConditionFlag.Mounted] && DateTime.Now > _retryAt)
{ {
@ -111,7 +95,8 @@ internal static class Mount
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
_progressContext = InteractionProgressContext.FromActionUse(() => _mountTriggered = gameFunctions.Mount()); ProgressContext =
InteractionProgressContext.FromActionUse(() => _mountTriggered = gameFunctions.Mount());
_retryAt = DateTime.Now.AddSeconds(5); _retryAt = DateTime.Now.AddSeconds(5);
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -121,23 +106,26 @@ internal static class Mount
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : ETaskResult.StillRunning;
} }
public override string ToString() => "Mount";
} }
private sealed class UnmountTask( internal sealed record UnmountTask : ITask
{
public bool ShouldRedoOnInterrupt() => true;
public override string ToString() => "Unmount";
}
internal sealed class UnmountExecutor(
ICondition condition, ICondition condition,
ILogger<UnmountTask> logger, ILogger<UnmountTask> logger,
GameFunctions gameFunctions, GameFunctions gameFunctions,
IClientState clientState) IClientState clientState)
: ITask : TaskExecutor<UnmountTask>
{ {
private bool _unmountTriggered; private bool _unmountTriggered;
private DateTime _continueAt = DateTime.MinValue; private DateTime _continueAt = DateTime.MinValue;
public bool ShouldRedoOnInterrupt() => true; protected override bool Start()
public bool Start()
{ {
if (!condition[ConditionFlag.Mounted]) if (!condition[ConditionFlag.Mounted])
return false; return false;
@ -155,7 +143,7 @@ internal static class Mount
return true; return true;
} }
public ETaskResult Update() public override ETaskResult Update()
{ {
if (_continueAt >= DateTime.Now) if (_continueAt >= DateTime.Now)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -188,8 +176,6 @@ internal static class Mount
} }
private unsafe bool IsUnmounting() => **(byte**)(clientState.LocalPlayer!.Address + 1432) == 1; private unsafe bool IsUnmounting() => **(byte**)(clientState.LocalPlayer!.Address + 1432) == 1;
public override string ToString() => "Unmount";
} }
public enum EMountIf public enum EMountIf

View File

@ -7,7 +7,7 @@ namespace Questionable.Controller.Steps.Common;
internal static class NextQuest 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) public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -24,34 +24,41 @@ internal static class NextQuest
if (questFunctions.GetPriorityQuests().Contains(step.NextQuestId)) if (questFunctions.GetPriorityQuests().Contains(step.NextQuestId))
return null; 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); questController.SetNextQuest(quest);
} }
else 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); questController.SetNextQuest(null);
} }
return true; return true;
} }
public ETaskResult Update() => ETaskResult.TaskComplete; public override ETaskResult Update() => ETaskResult.TaskComplete;
public override string ToString() => $"SetNextQuest({nextQuestId})";
} }
} }

View File

@ -2,22 +2,28 @@
namespace Questionable.Controller.Steps.Common; namespace Questionable.Controller.Steps.Common;
internal sealed class WaitConditionTask(Func<bool> predicate, string description) : ITask internal static class WaitCondition
{ {
private DateTime _continueAt = DateTime.MaxValue; internal sealed record Task(Func<bool> Predicate, string Description) : ITask
public bool Start() => !predicate();
public ETaskResult Update()
{ {
if (_continueAt == DateTime.MaxValue) public override string ToString() => Description;
{
if (predicate())
_continueAt = DateTime.Now.AddSeconds(0.5);
}
return DateTime.Now >= _continueAt ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
} }
public override string ToString() => description; internal sealed class Executor : TaskExecutor<Task>
{
private DateTime _continueAt = DateTime.MaxValue;
protected override bool Start() => !Task.Predicate();
public override ETaskResult Update()
{
if (_continueAt == DateTime.MaxValue)
{
if (Task.Predicate())
_continueAt = DateTime.Now.AddSeconds(0.5);
}
return DateTime.Now >= _continueAt ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
}
}
} }

View File

@ -15,227 +15,231 @@ using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Gathering; namespace Questionable.Controller.Steps.Gathering;
internal sealed class DoGather( internal static class DoGather
GatheringController.GatheringRequest currentRequest,
GatheringNode currentNode,
bool revisitRequired,
GatheringController gatheringController,
GameFunctions gameFunctions,
IGameGui gameGui,
IClientState clientState,
ICondition condition,
ILogger<DoGather> logger) : ITask, IRevisitAware
{ {
private const uint StatusGatheringRateUp = 218; internal sealed record Task(
GatheringController.GatheringRequest Request,
private bool _revisitTriggered; GatheringNode Node,
private bool _wasGathering; bool RevisitRequired) : ITask, IRevisitAware
private SlotInfo? _slotToGather;
private Queue<EAction>? _actionQueue;
public bool Start() => true;
public unsafe ETaskResult Update()
{ {
if (revisitRequired && !_revisitTriggered) public bool RevisitTriggered { get; private set; }
public void OnRevisit() => RevisitTriggered = true;
public override string ToString() => $"DoGather{(RevisitRequired ? " if revist" : "")}";
}
internal sealed class Executor(
GatheringController gatheringController,
GameFunctions gameFunctions,
IGameGui gameGui,
IClientState clientState,
ICondition condition,
ILogger<Executor> logger) : TaskExecutor<Task>
{
private const uint StatusGatheringRateUp = 218;
private bool _wasGathering;
private SlotInfo? _slotToGather;
private Queue<EAction>? _actionQueue;
protected override bool Start() => true;
public override unsafe ETaskResult Update()
{ {
logger.LogInformation("No revisit"); if (Task is { RevisitRequired: true, RevisitTriggered: false })
return ETaskResult.TaskComplete;
}
if (gatheringController.HasNodeDisappeared(currentNode))
{
logger.LogInformation("Node disappeared");
return ETaskResult.TaskComplete;
}
if (gameFunctions.GetFreeInventorySlots() == 0)
throw new TaskException("Inventory full");
if (condition[ConditionFlag.Gathering])
{
if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* _))
return ETaskResult.TaskComplete;
_wasGathering = true;
if (gameGui.TryGetAddonByName("Gathering", out AddonGathering* addonGathering))
{ {
if (gatheringController.HasRequestedItems()) logger.LogInformation("No revisit");
return ETaskResult.TaskComplete;
}
if (gatheringController.HasNodeDisappeared(Task.Node))
{
logger.LogInformation("Node disappeared");
return ETaskResult.TaskComplete;
}
if (gameFunctions.GetFreeInventorySlots() == 0)
throw new TaskException("Inventory full");
if (condition[ConditionFlag.Gathering])
{
if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* _))
return ETaskResult.TaskComplete;
_wasGathering = true;
if (gameGui.TryGetAddonByName("Gathering", out AddonGathering* addonGathering))
{ {
addonGathering->FireCallbackInt(-1); if (gatheringController.HasRequestedItems())
}
else
{
var slots = ReadSlots(addonGathering);
if (currentRequest.Collectability > 0)
{ {
var slot = slots.Single(x => x.ItemId == currentRequest.ItemId); addonGathering->FireCallbackInt(-1);
addonGathering->FireCallbackInt(slot.Index);
} }
else else
{ {
NodeCondition nodeCondition = new NodeCondition( var slots = ReadSlots(addonGathering);
addonGathering->AtkValues[110].UInt, if (Task.Request.Collectability > 0)
addonGathering->AtkValues[111].UInt);
if (_actionQueue != null && _actionQueue.TryPeek(out EAction nextAction))
{ {
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); if (gameFunctions.UseAction(nextAction))
_actionQueue.Dequeue(); {
logger.LogInformation("Used action {Action} on node", nextAction);
_actionQueue.Dequeue();
}
return ETaskResult.StillRunning;
} }
return ETaskResult.StillRunning; _actionQueue = GetNextActions(nodeCondition, slots);
} if (_actionQueue.Count == 0)
{
_actionQueue = GetNextActions(nodeCondition, slots); var slot = _slotToGather ?? slots.Single(x => x.ItemId == Task.Request.ItemId);
if (_actionQueue.Count == 0) addonGathering->FireCallbackInt(slot.Index);
{ }
var slot = _slotToGather ?? slots.Single(x => x.ItemId == currentRequest.ItemId);
addonGathering->FireCallbackInt(slot.Index);
} }
} }
} }
} }
return _wasGathering && !condition[ConditionFlag.Gathering]
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
} }
return _wasGathering && !condition[ConditionFlag.Gathering] private unsafe List<SlotInfo> ReadSlots(AddonGathering* addonGathering)
? 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)
{ {
// +8 = new item? var atkValues = addonGathering->AtkValues;
uint itemId = atkValues[i * 11 + 7].UInt; List<SlotInfo> slots = new List<SlotInfo>();
if (itemId == 0) for (int i = 0; i < 8; ++i)
continue;
AtkComponentCheckBox* atkCheckbox = addonGathering->GatheredItemComponentCheckbox[i].Value;
AtkTextNode* atkGatheringChance = atkCheckbox->UldManager.SearchNodeById(10)->GetAsAtkTextNode();
if (!int.TryParse(atkGatheringChance->NodeText.ToString(), out int gatheringChance))
gatheringChance = 0;
AtkTextNode* atkBoonChance = atkCheckbox->UldManager.SearchNodeById(16)->GetAsAtkTextNode();
if (!int.TryParse(atkBoonChance->NodeText.ToString(), out int boonChance))
boonChance = 0;
AtkComponentNode* atkImage = atkCheckbox->UldManager.SearchNodeById(31)->GetAsAtkComponentNode();
AtkTextNode* atkQuantity = atkImage->Component->UldManager.SearchNodeById(7)->GetAsAtkTextNode();
if (!atkQuantity->IsVisible() || !int.TryParse(atkQuantity->NodeText.ToString(), out int quantity))
quantity = 1;
var slot = new SlotInfo(i, itemId, gatheringChance, boonChance, quantity);
slots.Add(slot);
}
return slots;
}
[SuppressMessage("ReSharper", "UnusedParameter.Local")]
private Queue<EAction> GetNextActions(NodeCondition nodeCondition, List<SlotInfo> slots)
{
//uint gp = clientState.LocalPlayer!.CurrentGp;
Queue<EAction> actions = new();
if (!gameFunctions.HasStatus(StatusGatheringRateUp))
{
// 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); // +8 = new item?
uint itemId = atkValues[i * 11 + 7].UInt;
if (itemId == 0)
continue;
if (alternativeSlot.GatheringChance == 100) AtkComponentCheckBox* atkCheckbox = addonGathering->GatheredItemComponentCheckbox[i].Value;
{
_slotToGather = alternativeSlot;
return actions;
}
if (alternativeSlot.GatheringChance > 0) AtkTextNode* atkGatheringChance = atkCheckbox->UldManager.SearchNodeById(10)->GetAsAtkTextNode();
if (!int.TryParse(atkGatheringChance->NodeText.ToString(), out int gatheringChance))
gatheringChance = 0;
AtkTextNode* atkBoonChance = atkCheckbox->UldManager.SearchNodeById(16)->GetAsAtkTextNode();
if (!int.TryParse(atkBoonChance->NodeText.ToString(), out int boonChance))
boonChance = 0;
AtkComponentNode* atkImage = atkCheckbox->UldManager.SearchNodeById(31)->GetAsAtkComponentNode();
AtkTextNode* atkQuantity = atkImage->Component->UldManager.SearchNodeById(7)->GetAsAtkTextNode();
if (!atkQuantity->IsVisible() || !int.TryParse(atkQuantity->NodeText.ToString(), out int quantity))
quantity = 1;
var slot = new SlotInfo(i, itemId, gatheringChance, boonChance, quantity);
slots.Add(slot);
}
return slots;
}
[SuppressMessage("ReSharper", "UnusedParameter.Local")]
private Queue<EAction> GetNextActions(NodeCondition nodeCondition, List<SlotInfo> slots)
{
//uint gp = clientState.LocalPlayer!.CurrentGp;
Queue<EAction> actions = new();
if (!gameFunctions.HasStatus(StatusGatheringRateUp))
{
// do we have an alternative item? only happens for 'evaluation' leve quests
if (Task.Request.AlternativeItemId != 0)
{ {
if (alternativeSlot.GatheringChance >= 95 && var alternativeSlot = slots.Single(x => x.ItemId == Task.Request.AlternativeItemId);
CanUseAction(EAction.SharpVision1, EAction.FieldMastery1))
if (alternativeSlot.GatheringChance == 100)
{ {
_slotToGather = alternativeSlot; _slotToGather = alternativeSlot;
return actions;
}
if (alternativeSlot.GatheringChance > 0)
{
if (alternativeSlot.GatheringChance >= 95 &&
CanUseAction(EAction.SharpVision1, EAction.FieldMastery1))
{
_slotToGather = alternativeSlot;
actions.Enqueue(PickAction(EAction.SharpVision1, EAction.FieldMastery1));
return actions;
}
if (alternativeSlot.GatheringChance >= 85 &&
CanUseAction(EAction.SharpVision2, EAction.FieldMastery2))
{
_slotToGather = alternativeSlot;
actions.Enqueue(PickAction(EAction.SharpVision2, EAction.FieldMastery2));
return actions;
}
if (alternativeSlot.GatheringChance >= 50 &&
CanUseAction(EAction.SharpVision3, EAction.FieldMastery3))
{
_slotToGather = alternativeSlot;
actions.Enqueue(PickAction(EAction.SharpVision3, EAction.FieldMastery3));
return actions;
}
}
}
var slot = slots.Single(x => x.ItemId == Task.Request.ItemId);
if (slot.GatheringChance > 0 && slot.GatheringChance < 100)
{
if (slot.GatheringChance >= 95 &&
CanUseAction(EAction.SharpVision1, EAction.FieldMastery1))
{
actions.Enqueue(PickAction(EAction.SharpVision1, EAction.FieldMastery1)); actions.Enqueue(PickAction(EAction.SharpVision1, EAction.FieldMastery1));
return actions; return actions;
} }
if (alternativeSlot.GatheringChance >= 85 && if (slot.GatheringChance >= 85 &&
CanUseAction(EAction.SharpVision2, EAction.FieldMastery2)) CanUseAction(EAction.SharpVision2, EAction.FieldMastery2))
{ {
_slotToGather = alternativeSlot;
actions.Enqueue(PickAction(EAction.SharpVision2, EAction.FieldMastery2)); actions.Enqueue(PickAction(EAction.SharpVision2, EAction.FieldMastery2));
return actions; return actions;
} }
if (alternativeSlot.GatheringChance >= 50 && if (slot.GatheringChance >= 50 &&
CanUseAction(EAction.SharpVision3, EAction.FieldMastery3)) CanUseAction(EAction.SharpVision3, EAction.FieldMastery3))
{ {
_slotToGather = alternativeSlot;
actions.Enqueue(PickAction(EAction.SharpVision3, EAction.FieldMastery3)); actions.Enqueue(PickAction(EAction.SharpVision3, EAction.FieldMastery3));
return actions; return actions;
} }
} }
} }
var slot = slots.Single(x => x.ItemId == currentRequest.ItemId); return actions;
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; private EAction PickAction(EAction minerAction, EAction botanistAction)
} {
if ((EClassJob?)clientState.LocalPlayer?.ClassJob.Id == EClassJob.Miner)
return minerAction;
else
return botanistAction;
}
private EAction PickAction(EAction minerAction, EAction botanistAction) private unsafe bool CanUseAction(EAction minerAction, EAction botanistAction)
{ {
if ((EClassJob?)clientState.LocalPlayer?.ClassJob.Id == EClassJob.Miner) EAction action = PickAction(minerAction, botanistAction);
return minerAction; return ActionManager.Instance()->GetActionStatus(ActionType.Action, (uint)action) == 0;
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;
}
public void OnRevisit()
{
_revisitTriggered = true;
}
public override string ToString() => $"DoGather{(revisitRequired ? " if revist" : "")}";
[SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Local")] [SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Local")]
private sealed record SlotInfo(int Index, uint ItemId, int GatheringChance, int BoonChance, int Quantity); private sealed record SlotInfo(int Index, uint ItemId, int GatheringChance, int BoonChance, int Quantity);

View File

@ -1,5 +1,4 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
@ -13,189 +12,194 @@ using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Gathering; namespace Questionable.Controller.Steps.Gathering;
internal sealed class DoGatherCollectable( internal static class DoGatherCollectable
GatheringController.GatheringRequest currentRequest,
GatheringNode currentNode,
bool revisitRequired,
GatheringController gatheringController,
GameFunctions gameFunctions,
IClientState clientState,
IGameGui gameGui,
ILogger<DoGatherCollectable> logger) : ITask, IRevisitAware
{ {
private bool _revisitTriggered; internal sealed record Task(
private Queue<EAction>? _actionQueue; GatheringController.GatheringRequest Request,
GatheringNode Node,
private bool? _expectedScrutiny; bool RevisitRequired) : ITask, IRevisitAware
public bool Start() => true;
public unsafe ETaskResult Update()
{ {
if (revisitRequired && !_revisitTriggered) public bool RevisitTriggered { get; private set; }
public void OnRevisit() => RevisitTriggered = true;
public override string ToString() =>
$"DoGatherCollectable({SeIconChar.Collectible.ToIconString()}/{Request.Collectability}){(RevisitRequired ? " if revist" : "")}";
}
internal sealed class Executor(
GatheringController gatheringController,
GameFunctions gameFunctions,
IClientState clientState,
IGameGui gameGui,
ILogger<Executor> logger) : TaskExecutor<Task>
{
private Queue<EAction>? _actionQueue;
private bool? _expectedScrutiny;
protected override bool Start() => true;
public override unsafe ETaskResult Update()
{ {
logger.LogInformation("No revisit"); if (Task.RevisitRequired && !Task.RevisitTriggered)
return ETaskResult.TaskComplete; {
logger.LogInformation("No revisit");
return ETaskResult.TaskComplete;
}
if (gatheringController.HasNodeDisappeared(Task.Node))
{
logger.LogInformation("Node disappeared");
return ETaskResult.TaskComplete;
}
if (gatheringController.HasRequestedItems())
{
if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* atkUnitBase))
{
atkUnitBase->FireCallbackInt(1);
return ETaskResult.StillRunning;
}
if (gameGui.TryGetAddonByName("Gathering", out atkUnitBase))
{
atkUnitBase->FireCallbackInt(-1);
return ETaskResult.TaskComplete;
}
}
if (gameFunctions.GetFreeInventorySlots() == 0)
throw new TaskException("Inventory full");
NodeCondition? nodeCondition = GetNodeCondition();
if (nodeCondition == null)
return ETaskResult.TaskComplete;
if (_expectedScrutiny != null)
{
if (nodeCondition.ScrutinyActive != _expectedScrutiny)
return ETaskResult.StillRunning;
// continue on next frame
_expectedScrutiny = null;
return ETaskResult.StillRunning;
}
if (_actionQueue != null && _actionQueue.TryPeek(out EAction nextAction))
{
if (gameFunctions.UseAction(nextAction))
{
_expectedScrutiny = nextAction switch
{
EAction.ScrutinyMiner or EAction.ScrutinyBotanist => true,
EAction.ScourMiner or EAction.ScourBotanist or EAction.MeticulousMiner
or EAction.MeticulousBotanist => false,
_ => null
};
logger.LogInformation("Used action {Action} on node", nextAction);
_actionQueue.Dequeue();
}
return ETaskResult.StillRunning;
}
if (nodeCondition.CollectabilityToGoal(Task.Request.Collectability) > 0)
{
_actionQueue = GetNextActions(nodeCondition);
if (_actionQueue != null)
{
foreach (var action in _actionQueue)
logger.LogInformation("Next Actions {Action}", action);
return ETaskResult.StillRunning;
}
}
_actionQueue = new Queue<EAction>();
_actionQueue.Enqueue(PickAction(EAction.CollectMiner, EAction.CollectBotanist));
return ETaskResult.StillRunning;
} }
if (gatheringController.HasNodeDisappeared(currentNode)) private unsafe NodeCondition? GetNodeCondition()
{
logger.LogInformation("Node disappeared");
return ETaskResult.TaskComplete;
}
if (gatheringController.HasRequestedItems())
{ {
if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* atkUnitBase)) if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* atkUnitBase))
{ {
atkUnitBase->FireCallbackInt(1); var atkValues = atkUnitBase->AtkValues;
return ETaskResult.StillRunning; return new NodeCondition(
CurrentCollectability: atkValues[13].UInt,
MaxCollectability: atkValues[14].UInt,
CurrentIntegrity: atkValues[62].UInt,
MaxIntegrity: atkValues[63].UInt,
ScrutinyActive: atkValues[54].Bool,
CollectabilityFromScour: atkValues[48].UInt,
CollectabilityFromMeticulous: atkValues[51].UInt
);
} }
if (gameGui.TryGetAddonByName("Gathering", out atkUnitBase)) return null;
}
private Queue<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();
uint neededCollectability = nodeCondition.CollectabilityToGoal(Task.Request.Collectability);
if (neededCollectability <= nodeCondition.CollectabilityFromMeticulous)
{ {
atkUnitBase->FireCallbackInt(-1); logger.LogTrace("Can get all needed {NeededCollectability} from {Collectability}~ meticulous",
return ETaskResult.TaskComplete; neededCollectability, nodeCondition.CollectabilityFromMeticulous);
actions.Enqueue(PickAction(EAction.MeticulousMiner, EAction.MeticulousBotanist));
return actions;
} }
}
if (gameFunctions.GetFreeInventorySlots() == 0) if (neededCollectability <= nodeCondition.CollectabilityFromScour)
throw new TaskException("Inventory full");
NodeCondition? nodeCondition = GetNodeCondition();
if (nodeCondition == null)
return ETaskResult.TaskComplete;
if (_expectedScrutiny != null)
{
if (nodeCondition.ScrutinyActive != _expectedScrutiny)
return ETaskResult.StillRunning;
// continue on next frame
_expectedScrutiny = null;
return ETaskResult.StillRunning;
}
if (_actionQueue != null && _actionQueue.TryPeek(out EAction nextAction))
{
if (gameFunctions.UseAction(nextAction))
{ {
_expectedScrutiny = nextAction switch logger.LogTrace("Can get all needed {NeededCollectability} from {Collectability}~ scour",
{ neededCollectability, nodeCondition.CollectabilityFromScour);
EAction.ScrutinyMiner or EAction.ScrutinyBotanist => true, actions.Enqueue(PickAction(EAction.ScourMiner, EAction.ScourBotanist));
EAction.ScourMiner or EAction.ScourBotanist or EAction.MeticulousMiner return actions;
or EAction.MeticulousBotanist => false,
_ => null
};
logger.LogInformation("Used action {Action} on node", nextAction);
_actionQueue.Dequeue();
} }
return ETaskResult.StillRunning; // neither action directly solves our problem
} if (!nodeCondition.ScrutinyActive && gp >= 200)
if (nodeCondition.CollectabilityToGoal(currentRequest.Collectability) > 0)
{
_actionQueue = GetNextActions(nodeCondition);
if (_actionQueue != null)
{ {
foreach (var action in _actionQueue) logger.LogTrace("Still missing {NeededCollectability} collectability, scrutiny inactive",
logger.LogInformation("Next Actions {Action}", action); neededCollectability);
return ETaskResult.StillRunning; actions.Enqueue(PickAction(EAction.ScrutinyMiner, EAction.ScrutinyBotanist));
return actions;
}
if (nodeCondition.ScrutinyActive)
{
logger.LogTrace(
"Scrutiny active, need {NeededCollectability} and we expect {Collectability}~ meticulous",
neededCollectability, nodeCondition.CollectabilityFromMeticulous);
actions.Enqueue(PickAction(EAction.MeticulousMiner, EAction.MeticulousBotanist));
return actions;
}
else
{
logger.LogTrace("Scrutiny active, need {NeededCollectability} and we expect {Collectability}~ scour",
neededCollectability, nodeCondition.CollectabilityFromScour);
actions.Enqueue(PickAction(EAction.ScourMiner, EAction.ScourBotanist));
return actions;
} }
} }
_actionQueue = new Queue<EAction>(); private EAction PickAction(EAction minerAction, EAction botanistAction)
_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; if ((EClassJob?)clientState.LocalPlayer?.ClassJob.Id == EClassJob.Miner)
return new NodeCondition( return minerAction;
CurrentCollectability: atkValues[13].UInt, else
MaxCollectability: atkValues[14].UInt, return botanistAction;
CurrentIntegrity: atkValues[62].UInt,
MaxIntegrity: atkValues[63].UInt,
ScrutinyActive: atkValues[54].Bool,
CollectabilityFromScour: atkValues[48].UInt,
CollectabilityFromMeticulous: atkValues[51].UInt
);
}
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);
Queue<EAction> actions = new();
uint neededCollectability = nodeCondition.CollectabilityToGoal(currentRequest.Collectability);
if (neededCollectability <= nodeCondition.CollectabilityFromMeticulous)
{
logger.LogTrace("Can get all needed {NeededCollectability} from {Collectability}~ meticulous",
neededCollectability, nodeCondition.CollectabilityFromMeticulous);
actions.Enqueue(PickAction(EAction.MeticulousMiner, EAction.MeticulousBotanist));
return actions;
}
if (neededCollectability <= nodeCondition.CollectabilityFromScour)
{
logger.LogTrace("Can get all needed {NeededCollectability} from {Collectability}~ scour",
neededCollectability, nodeCondition.CollectabilityFromScour);
actions.Enqueue(PickAction(EAction.ScourMiner, EAction.ScourBotanist));
return actions;
}
// neither action directly solves our problem
if (!nodeCondition.ScrutinyActive && gp >= 200)
{
logger.LogTrace("Still missing {NeededCollectability} collectability, scrutiny inactive",
neededCollectability);
actions.Enqueue(PickAction(EAction.ScrutinyMiner, EAction.ScrutinyBotanist));
return actions;
}
if (nodeCondition.ScrutinyActive)
{
logger.LogTrace("Scrutiny active, need {NeededCollectability} and we expect {Collectability}~ meticulous",
neededCollectability, nodeCondition.CollectabilityFromMeticulous);
actions.Enqueue(PickAction(EAction.MeticulousMiner, EAction.MeticulousBotanist));
return actions;
}
else
{
logger.LogTrace("Scrutiny active, need {NeededCollectability} and we expect {Collectability}~ scour",
neededCollectability, nodeCondition.CollectabilityFromScour);
actions.Enqueue(PickAction(EAction.ScourMiner, EAction.ScourBotanist));
return actions;
} }
} }
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")] [SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Local")]
private sealed record NodeCondition( private sealed record NodeCondition(
uint CurrentCollectability, uint CurrentCollectability,

View File

@ -3,7 +3,6 @@ using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Shared; using Questionable.Controller.Steps.Shared;
using Questionable.Functions; using Questionable.Functions;
@ -12,41 +11,49 @@ using Questionable.Model.Gathering;
namespace Questionable.Controller.Steps.Gathering; namespace Questionable.Controller.Steps.Gathering;
internal sealed class MoveToLandingLocation( internal static class MoveToLandingLocation
ushort territoryId,
bool flyBetweenNodes,
GatheringNode gatheringNode,
MoveTo.Factory moveFactory,
GameFunctions gameFunctions,
IObjectTable objectTable,
ILogger<MoveToLandingLocation> logger) : ITask
{ {
private ITask _moveTask = null!; internal sealed record Task(
ushort TerritoryId,
public bool Start() bool FlyBetweenNodes,
GatheringNode GatheringNode) : ITask
{ {
var location = gatheringNode.Locations.First(); public override string ToString() => $"Land/{FlyBetweenNodes}";
if (gatheringNode.Locations.Count > 1)
{
var gameObject = objectTable.SingleOrDefault(x =>
x.ObjectKind == ObjectKind.GatheringPoint && x.DataId == gatheringNode.DataId && x.IsTargetable);
if (gameObject == null)
return false;
location = gatheringNode.Locations.Single(x => Vector3.Distance(x.Position, gameObject.Position) < 0.1f);
}
var (target, degrees, range) = GatheringMath.CalculateLandingLocation(location);
logger.LogInformation("Preliminary landing location: {Location}, with degrees = {Degrees}, range = {Range}",
target.ToString("G", CultureInfo.InvariantCulture), degrees, range);
bool fly = flyBetweenNodes && gameFunctions.IsFlyingUnlocked(territoryId);
_moveTask = moveFactory.Move(new MoveTo.MoveParams(territoryId, target, null, 0.25f,
DataId: gatheringNode.DataId, Fly: fly, IgnoreDistanceToObject: true));
return _moveTask.Start();
} }
public ETaskResult Update() => _moveTask.Update(); internal sealed class Executor(
MoveTo.MoveExecutor moveExecutor,
GameFunctions gameFunctions,
IObjectTable objectTable,
ILogger<Executor> logger) : TaskExecutor<Task>
{
private ITask _moveTask = null!;
public override string ToString() => $"Land/{_moveTask}/{flyBetweenNodes}"; protected override bool Start()
{
var location = Task.GatheringNode.Locations.First();
if (Task.GatheringNode.Locations.Count > 1)
{
var gameObject = objectTable.SingleOrDefault(x =>
x.ObjectKind == ObjectKind.GatheringPoint && x.DataId == Task.GatheringNode.DataId &&
x.IsTargetable);
if (gameObject == null)
return false;
location = Task.GatheringNode.Locations.Single(x =>
Vector3.Distance(x.Position, gameObject.Position) < 0.1f);
}
var (target, degrees, range) = GatheringMath.CalculateLandingLocation(location);
logger.LogInformation("Preliminary landing location: {Location}, with degrees = {Degrees}, range = {Range}",
target.ToString("G", CultureInfo.InvariantCulture), degrees, range);
bool fly = Task.FlyBetweenNodes && gameFunctions.IsFlyingUnlocked(Task.TerritoryId);
_moveTask = new MoveTo.MoveTask(Task.TerritoryId, target, null, 0.25f,
DataId: Task.GatheringNode.DataId, Fly: fly, IgnoreDistanceToObject: true);
return moveExecutor.Start(_moveTask);
}
public override ETaskResult Update() => moveExecutor.Update();
}
} }

View File

@ -1,9 +1,7 @@
using System; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI; using LLib.GameUI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
@ -13,24 +11,29 @@ namespace Questionable.Controller.Steps.Gathering;
internal static class TurnInDelivery 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) public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
if (quest.Id is not SatisfactionSupplyNpcId || sequence.Sequence != 1) if (quest.Id is not SatisfactionSupplyNpcId || sequence.Sequence != 1)
return null; 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; 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(); AgentSatisfactionSupply* agentSatisfactionSupply = AgentSatisfactionSupply.Instance();
if (agentSatisfactionSupply == null || !agentSatisfactionSupply->IsAgentActive()) if (agentSatisfactionSupply == null || !agentSatisfactionSupply->IsAgentActive())
@ -77,7 +80,5 @@ internal static class TurnInDelivery
addon->FireCallback(2, pickGatheringItem); addon->FireCallback(2, pickGatheringItem);
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
public override string ToString() => "WeeklyDeliveryTurnIn";
} }
} }

View File

@ -2,7 +2,7 @@
namespace Questionable.Controller.Steps; namespace Questionable.Controller.Steps;
public interface IConditionChangeAware internal interface IConditionChangeAware : ITaskExecutor
{ {
void OnConditionChange(ConditionFlag flag, bool value); void OnConditionChange(ConditionFlag flag, bool value);
} }

View File

@ -1,6 +1,6 @@
namespace Questionable.Controller.Steps; namespace Questionable.Controller.Steps;
public interface IRevisitAware internal interface IRevisitAware : ITask
{ {
void OnRevisit(); void OnRevisit();
} }

View File

@ -1,27 +1,6 @@
using System.Threading; namespace Questionable.Controller.Steps;
using System.Threading.Tasks;
namespace Questionable.Controller.Steps;
internal interface ITask internal interface ITask
{ {
InteractionProgressContext? ProgressContext() => null;
bool WasInterrupted()
{
var progressContext = ProgressContext();
if (progressContext != null)
{
progressContext.Update();
return progressContext.WasInterrupted();
}
return false;
}
bool ShouldRedoOnInterrupt() => false; bool ShouldRedoOnInterrupt() => false;
bool Start();
ETaskResult Update();
} }

View File

@ -2,7 +2,7 @@
namespace Questionable.Controller.Steps; namespace Questionable.Controller.Steps;
public interface IToastAware internal interface IToastAware : ITaskExecutor
{ {
bool OnErrorToast(SeString message); bool OnErrorToast(SeString message);
} }

View File

@ -11,8 +11,7 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Action internal static class Action
{ {
internal sealed class Factory(GameFunctions gameFunctions, Mount.Factory mountFactory, ILoggerFactory loggerFactory) internal sealed class Factory : ITaskFactory
: ITaskFactory
{ {
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -25,39 +24,43 @@ internal static class Action
if (step.Action.Value.RequiresMount()) if (step.Action.Value.RequiresMount())
return [task]; return [task];
else 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, return new UseOnObject(dataId, action);
loggerFactory.CreateLogger<UseOnObject>());
} }
} }
private sealed class UseOnObject( internal sealed record UseOnObject(
uint? dataId, uint? DataId,
EAction action, EAction Action) : ITask
{
public override string ToString() => $"Action({Action})";
}
internal sealed class UseOnObjectExecutor(
GameFunctions gameFunctions, GameFunctions gameFunctions,
ILogger<UseOnObject> logger) : ITask ILogger<UseOnObject> logger) : TaskExecutor<UseOnObject>
{ {
private bool _usedAction; private bool _usedAction;
private DateTime _continueAt = DateTime.MinValue; 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) if (gameObject == null)
{ {
logger.LogWarning("No game object with dataId {DataId}", dataId); logger.LogWarning("No game object with dataId {DataId}", Task.DataId);
return false; return false;
} }
if (gameObject.IsTargetable) if (gameObject.IsTargetable)
{ {
if (action == EAction.Diagnosis) if (Task.Action == EAction.Diagnosis)
{ {
uint eukrasiaAura = 2606; uint eukrasiaAura = 2606;
// If SGE have Eukrasia status, we need to remove it. // If SGE have Eukrasia status, we need to remove it.
@ -72,14 +75,14 @@ internal static class Action
} }
} }
_usedAction = gameFunctions.UseAction(gameObject, action); _usedAction = gameFunctions.UseAction(gameObject, Task.Action);
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(0.5);
return true; return true;
} }
} }
else else
{ {
_usedAction = gameFunctions.UseAction(action); _usedAction = gameFunctions.UseAction(Task.Action);
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(0.5);
return true; return true;
} }
@ -87,25 +90,25 @@ internal static class Action
return true; return true;
} }
public ETaskResult Update() public override ETaskResult Update()
{ {
if (DateTime.Now <= _continueAt) if (DateTime.Now <= _continueAt)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
if (!_usedAction) 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) if (gameObject == null || !gameObject.IsTargetable)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
_usedAction = gameFunctions.UseAction(gameObject, action); _usedAction = gameFunctions.UseAction(gameObject, Task.Action);
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(0.5);
} }
else else
{ {
_usedAction = gameFunctions.UseAction(action); _usedAction = gameFunctions.UseAction(Task.Action);
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(0.5);
} }
@ -114,7 +117,5 @@ internal static class Action
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
public override string ToString() => $"Action({action})";
} }
} }

View File

@ -12,10 +12,8 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class AetherCurrent internal static class AetherCurrent
{ {
internal sealed class Factory( internal sealed class Factory(
GameFunctions gameFunctions,
AetherCurrentData aetherCurrentData, AetherCurrentData aetherCurrentData,
IChatGui chatGui, IChatGui chatGui) : SimpleTaskFactory
ILoggerFactory loggerFactory) : SimpleTaskFactory
{ {
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step) public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -32,42 +30,39 @@ internal static class AetherCurrent
return null; return null;
} }
return new DoAttune(step.DataId.Value, step.AetherCurrentId.Value, gameFunctions, return new Attune(step.DataId.Value, step.AetherCurrentId.Value);
loggerFactory.CreateLogger<DoAttune>());
} }
} }
private sealed class DoAttune( internal sealed record Attune(uint DataId, uint AetherCurrentId) : ITask
uint dataId,
uint aetherCurrentId,
GameFunctions gameFunctions,
ILogger<DoAttune> logger) : ITask
{ {
private InteractionProgressContext? _progressContext; public override string ToString() => $"AttuneAetherCurrent({AetherCurrentId})";
}
public InteractionProgressContext? ProgressContext() => _progressContext; internal sealed class DoAttune(
GameFunctions gameFunctions,
public bool Start() 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, logger.LogInformation("Attuning to aether current {AetherCurrentId} / {DataId}", Task.AetherCurrentId,
dataId); Task.DataId);
_progressContext = ProgressContext =
InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(dataId)); InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(Task.DataId));
return true; return true;
} }
logger.LogInformation("Already attuned to aether current {AetherCurrentId} / {DataId}", aetherCurrentId, logger.LogInformation("Already attuned to aether current {AetherCurrentId} / {DataId}",
dataId); Task.AetherCurrentId,
Task.DataId);
return false; return false;
} }
public ETaskResult Update() => public override ETaskResult Update() =>
gameFunctions.IsAetherCurrentUnlocked(aetherCurrentId) gameFunctions.IsAetherCurrentUnlocked(Task.AetherCurrentId)
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : ETaskResult.StillRunning;
public override string ToString() => $"AttuneAetherCurrent({aetherCurrentId})";
} }
} }

View File

@ -11,10 +11,7 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class AethernetShard internal static class AethernetShard
{ {
internal sealed class Factory( internal sealed class Factory : SimpleTaskFactory
AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions,
ILoggerFactory loggerFactory) : SimpleTaskFactory
{ {
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step) public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -23,40 +20,37 @@ internal static class AethernetShard
ArgumentNullException.ThrowIfNull(step.AethernetShard); ArgumentNullException.ThrowIfNull(step.AethernetShard);
return new DoAttune(step.AethernetShard.Value, aetheryteFunctions, gameFunctions, return new Attune(step.AethernetShard.Value);
loggerFactory.CreateLogger<DoAttune>());
} }
} }
private sealed class DoAttune( internal sealed record Attune(EAetheryteLocation AetheryteLocation) : ITask
EAetheryteLocation aetheryteLocation, {
public override string ToString() => $"AttuneAethernetShard({AetheryteLocation})";
}
internal sealed class DoAttune(
AetheryteFunctions aetheryteFunctions, AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions, GameFunctions gameFunctions,
ILogger<DoAttune> logger) : ITask ILogger<DoAttune> logger) : TaskExecutor<Attune>
{ {
private InteractionProgressContext? _progressContext; protected override bool Start()
public InteractionProgressContext? ProgressContext() => _progressContext;
public bool Start()
{ {
if (!aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation)) if (!aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation))
{ {
logger.LogInformation("Attuning to aethernet shard {AethernetShard}", aetheryteLocation); logger.LogInformation("Attuning to aethernet shard {AethernetShard}", Task.AetheryteLocation);
_progressContext = InteractionProgressContext.FromActionUseOrDefault(() => ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() =>
gameFunctions.InteractWith((uint)aetheryteLocation, ObjectKind.Aetheryte)); gameFunctions.InteractWith((uint)Task.AetheryteLocation, ObjectKind.Aetheryte));
return true; return true;
} }
logger.LogInformation("Already attuned to aethernet shard {AethernetShard}", aetheryteLocation); logger.LogInformation("Already attuned to aethernet shard {AethernetShard}", Task.AetheryteLocation);
return false; return false;
} }
public ETaskResult Update() => public override ETaskResult Update() =>
aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation) aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation)
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : ETaskResult.StillRunning;
public override string ToString() => $"AttuneAethernetShard({aetheryteLocation})";
} }
} }

View File

@ -1,5 +1,4 @@
using System; using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Functions; using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
@ -10,10 +9,7 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Aetheryte internal static class Aetheryte
{ {
internal sealed class Factory( internal sealed class Factory : SimpleTaskFactory
AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions,
ILoggerFactory loggerFactory) : SimpleTaskFactory
{ {
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step) public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -22,41 +18,38 @@ internal static class Aetheryte
ArgumentNullException.ThrowIfNull(step.Aetheryte); ArgumentNullException.ThrowIfNull(step.Aetheryte);
return new DoAttune(step.Aetheryte.Value, aetheryteFunctions, gameFunctions, return new Attune(step.Aetheryte.Value);
loggerFactory.CreateLogger<DoAttune>());
} }
} }
private sealed class DoAttune( internal sealed record Attune(EAetheryteLocation AetheryteLocation) : ITask
EAetheryteLocation aetheryteLocation, {
public override string ToString() => $"AttuneAetheryte({AetheryteLocation})";
}
internal sealed class DoAttune(
AetheryteFunctions aetheryteFunctions, AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions, GameFunctions gameFunctions,
ILogger<DoAttune> logger) : ITask ILogger<DoAttune> logger) : TaskExecutor<Attune>
{ {
private InteractionProgressContext? _progressContext; protected override bool Start()
public InteractionProgressContext? ProgressContext() => _progressContext;
public bool Start()
{ {
if (!aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation)) if (!aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation))
{ {
logger.LogInformation("Attuning to aetheryte {Aetheryte}", aetheryteLocation); logger.LogInformation("Attuning to aetheryte {Aetheryte}", Task.AetheryteLocation);
_progressContext = ProgressContext =
InteractionProgressContext.FromActionUseOrDefault(() => InteractionProgressContext.FromActionUseOrDefault(() =>
gameFunctions.InteractWith((uint)aetheryteLocation)); gameFunctions.InteractWith((uint)Task.AetheryteLocation));
return true; return true;
} }
logger.LogInformation("Already attuned to aetheryte {Aetheryte}", aetheryteLocation); logger.LogInformation("Already attuned to aetheryte {Aetheryte}", Task.AetheryteLocation);
return false; return false;
} }
public ETaskResult Update() => public override ETaskResult Update() =>
aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation) aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation)
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : ETaskResult.StillRunning;
public override string ToString() => $"AttuneAetheryte({aetheryteLocation})";
} }
} }

View File

@ -13,14 +13,7 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Combat internal static class Combat
{ {
internal sealed class Factory( internal sealed class Factory(GameFunctions gameFunctions) : ITaskFactory
CombatController combatController,
Interact.Factory interactFactory,
Mount.Factory mountFactory,
UseItem.Factory useItemFactory,
Action.Factory actionFactory,
QuestFunctions questFunctions,
GameFunctions gameFunctions) : ITaskFactory
{ {
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -30,7 +23,7 @@ internal static class Combat
ArgumentNullException.ThrowIfNull(step.EnemySpawnType); ArgumentNullException.ThrowIfNull(step.EnemySpawnType);
if (gameFunctions.GetMountId() != Mount128Module.MountId) if (gameFunctions.GetMountId() != Mount128Module.MountId)
yield return mountFactory.Unmount(); yield return new Mount.UnmountTask();
if (step.CombatDelaySecondsAtStart != null) if (step.CombatDelaySecondsAtStart != null)
{ {
@ -43,7 +36,7 @@ internal static class Combat
{ {
ArgumentNullException.ThrowIfNull(step.DataId); 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 new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(1));
yield return CreateTask(quest, sequence, step); yield return CreateTask(quest, sequence, step);
break; break;
@ -54,7 +47,7 @@ internal static class Combat
ArgumentNullException.ThrowIfNull(step.DataId); ArgumentNullException.ThrowIfNull(step.DataId);
ArgumentNullException.ThrowIfNull(step.ItemId); 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); step.CompletionQuestVariablesFlags, true);
yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(1)); yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(1));
yield return CreateTask(quest, sequence, step); yield return CreateTask(quest, sequence, step);
@ -67,8 +60,8 @@ internal static class Combat
ArgumentNullException.ThrowIfNull(step.Action); ArgumentNullException.ThrowIfNull(step.Action);
if (!step.Action.Value.RequiresMount()) if (!step.Action.Value.RequiresMount())
yield return mountFactory.Unmount(); yield return new Mount.UnmountTask();
yield return actionFactory.OnObject(step.DataId.Value, step.Action.Value); yield return new Action.UseOnObject(step.DataId.Value, step.Action.Value);
yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(1)); yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(1));
yield return CreateTask(quest, sequence, step); yield return CreateTask(quest, sequence, step);
break; break;
@ -92,7 +85,7 @@ internal static class Combat
} }
} }
public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step) private static Task CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
ArgumentNullException.ThrowIfNull(step.EnemySpawnType); ArgumentNullException.ThrowIfNull(step.EnemySpawnType);
@ -101,46 +94,60 @@ internal static class Combat
step.CompletionQuestVariablesFlags, step.ComplexCombatData); 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<uint> killEnemyDataIds, IList<QuestWorkValue?> completionQuestVariablesFlags,
IList<ComplexCombatData> complexCombatData) IList<ComplexCombatData> complexCombatData)
{ {
return new HandleCombat(isLastStep, new CombatController.CombatData return new Task(new CombatController.CombatData
{ {
ElementId = elementId, ElementId = elementId,
SpawnType = enemySpawnType, SpawnType = enemySpawnType,
KillEnemyDataIds = killEnemyDataIds.ToList(), KillEnemyDataIds = killEnemyDataIds.ToList(),
ComplexCombatDatas = complexCombatData.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( internal sealed class HandleCombat(
bool isLastStep,
CombatController.CombatData combatData,
IList<QuestWorkValue?> completionQuestVariableFlags,
CombatController combatController, CombatController combatController,
QuestFunctions questFunctions) : ITask QuestFunctions questFunctions) : TaskExecutor<Task>
{ {
private CombatController.EStatus _status = CombatController.EStatus.NotStarted; 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(); _status = combatController.Update();
if (_status != CombatController.EStatus.Complete) if (_status != CombatController.EStatus.Complete)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
// if our quest step has any completion flags, we need to check if they are set // if our quest step has any completion flags, we need to check if they are set
if (QuestWorkUtils.HasCompletionFlags(completionQuestVariableFlags) && if (QuestWorkUtils.HasCompletionFlags(Task.CompletionQuestVariableFlags) &&
combatData.ElementId is QuestId questId) Task.CombatData.ElementId is QuestId questId)
{ {
var questWork = questFunctions.GetQuestProgressInfo(questId); var questWork = questFunctions.GetQuestProgressInfo(questId);
if (questWork == null) if (questWork == null)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
if (QuestWorkUtils.MatchesQuestWork(completionQuestVariableFlags, questWork)) if (QuestWorkUtils.MatchesQuestWork(Task.CompletionQuestVariableFlags, questWork))
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
else else
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -148,7 +155,7 @@ internal static class Combat
// the last step, by definition, can only be progressed by the game recognizing we're in a new sequence, // 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 // so this is an indefinite wait
if (isLastStep) if (Task.IsLastStep)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
else else
{ {
@ -156,15 +163,5 @@ internal static class Combat
return ETaskResult.TaskComplete; 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})";
}
} }
} }

View File

@ -18,24 +18,25 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Dive 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) public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
if (step.InteractionType != EInteractionType.Dive) if (step.InteractionType != EInteractionType.Dive)
return null; return null;
return Dive(); return new Task();
}
public ITask Dive()
{
return new DoDive(condition, loggerFactory.CreateLogger<DoDive>());
} }
} }
private sealed class DoDive(ICondition condition, ILogger<DoDive> logger) internal sealed class Task : ITask
: AbstractDelayedTask(TimeSpan.FromSeconds(5)) {
public override string ToString() => "Dive";
}
internal sealed class DoDive(ICondition condition, ILogger<DoDive> logger)
: AbstractDelayedTaskExecutor<Task>(TimeSpan.FromSeconds(5))
{ {
private readonly Queue<(uint Type, nint Key)> _keysToPress = []; private readonly Queue<(uint Type, nint Key)> _keysToPress = [];
private int _attempts; private int _attempts;
@ -114,8 +115,6 @@ internal static class Dive
foreach (var key in realKeys) foreach (var key in realKeys)
_keysToPress.Enqueue((NativeMethods.WM_KEYUP, key)); _keysToPress.Enqueue((NativeMethods.WM_KEYUP, key));
} }
public override string ToString() => "Dive";
} }
private static List<nint>? GetKeysToPress(SeVirtualKey key, ModifierFlag modifier) private static List<nint>? GetKeysToPress(SeVirtualKey key, ModifierFlag modifier)

View File

@ -1,7 +1,6 @@
using System; using System;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Microsoft.Extensions.DependencyInjection;
using Questionable.Functions; using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
@ -10,7 +9,7 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Duty 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) public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -18,26 +17,28 @@ internal static class Duty
return null; return null;
ArgumentNullException.ThrowIfNull(step.ContentFinderConditionId); ArgumentNullException.ThrowIfNull(step.ContentFinderConditionId);
return new OpenDutyFinder(step.ContentFinderConditionId.Value, gameFunctions, condition); return new Task(step.ContentFinderConditionId.Value);
} }
} }
private sealed class OpenDutyFinder( internal sealed record Task(uint ContentFinderConditionId) : ITask
uint contentFinderConditionId,
GameFunctions gameFunctions,
ICondition condition) : ITask
{ {
public bool Start() public override string ToString() => $"OpenDutyFinder({ContentFinderConditionId})";
}
internal sealed class Executor(
GameFunctions gameFunctions,
ICondition condition) : TaskExecutor<Task>
{
protected override bool Start()
{ {
if (condition[ConditionFlag.InDutyQueue]) if (condition[ConditionFlag.InDutyQueue])
return false; return false;
gameFunctions.OpenDutyFinder(contentFinderConditionId); gameFunctions.OpenDutyFinder(Task.ContentFinderConditionId);
return true; return true;
} }
public ETaskResult Update() => ETaskResult.TaskComplete; public override ETaskResult Update() => ETaskResult.TaskComplete;
public override string ToString() => $"OpenDutyFinder({contentFinderConditionId})";
} }
} }

View File

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Common;
using Questionable.Functions; using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
@ -10,7 +9,7 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Emote 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) public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -25,39 +24,46 @@ internal static class Emote
ArgumentNullException.ThrowIfNull(step.Emote); ArgumentNullException.ThrowIfNull(step.Emote);
var unmount = mountFactory.Unmount(); var unmount = new Mount.UnmountTask();
if (step.DataId != null) 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]; return [unmount, task];
} }
else else
{ {
var task = new UseOnSelf(step.Emote.Value, chatFunctions); var task = new UseOnSelf(step.Emote.Value);
return [unmount, task]; return [unmount, task];
} }
} }
} }
private sealed class UseOnObject(EEmote emote, uint dataId, ChatFunctions chatFunctions) : AbstractDelayedTask internal sealed record UseOnObject(EEmote Emote, uint DataId) : ITask
{ {
protected override bool StartInternal() public override string ToString() => $"Emote({Emote} on {DataId})";
{
chatFunctions.UseEmote(dataId, emote);
return true;
}
public override string ToString() => $"Emote({emote} on {dataId})";
} }
private sealed class UseOnSelf(EEmote emote, ChatFunctions chatFunctions) : AbstractDelayedTask internal sealed class UseOnObjectExecutor(ChatFunctions chatFunctions)
: AbstractDelayedTaskExecutor<UseOnObject>
{ {
protected override bool StartInternal() protected override bool StartInternal()
{ {
chatFunctions.UseEmote(emote); chatFunctions.UseEmote(Task.DataId, Task.Emote);
return true; return true;
} }
}
public override string ToString() => $"Emote({emote})"; internal sealed record UseOnSelf(EEmote Emote) : ITask
{
public override string ToString() => $"Emote({Emote})";
}
internal sealed class UseOnSelfExecutor(ChatFunctions chatFunctions) : AbstractDelayedTaskExecutor<UseOnSelf>
{
protected override bool StartInternal()
{
chatFunctions.UseEmote(Task.Emote);
return true;
}
} }
} }

View File

@ -16,7 +16,7 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class EquipItem 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) public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -24,36 +24,18 @@ internal static class EquipItem
return null; return null;
ArgumentNullException.ThrowIfNull(step.ItemId); ArgumentNullException.ThrowIfNull(step.ItemId);
return Equip(step.ItemId.Value); return new Task(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>());
}
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
};
} }
} }
private sealed class DoEquip( internal sealed record Task(uint ItemId) : ITask
uint itemId, {
Item item, public override string ToString() => $"Equip({ItemId})";
List<ushort> targetSlots, }
internal sealed class Executor(
IDataManager dataManager, IDataManager dataManager,
ILogger<DoEquip> logger) : ITask, IToastAware ILogger<Executor> logger) : TaskExecutor<Task>, IToastAware
{ {
private const int MaxAttempts = 3; private const int MaxAttempts = 3;
@ -81,16 +63,22 @@ internal static class EquipItem
]; ];
private int _attempts; private int _attempts;
private Item _item = null!;
private List<ushort> _targetSlots = null!;
private DateTime _continueAt = DateTime.MaxValue; 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(); Equip();
_continueAt = DateTime.Now.AddSeconds(1); _continueAt = DateTime.Now.AddSeconds(1);
return true; return true;
} }
public unsafe ETaskResult Update() public override unsafe ETaskResult Update()
{ {
if (DateTime.Now < _continueAt) if (DateTime.Now < _continueAt)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -99,10 +87,10 @@ internal static class EquipItem
if (inventoryManager == null) if (inventoryManager == null)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
foreach (ushort x in targetSlots) foreach (ushort x in _targetSlots)
{ {
var itemSlot = inventoryManager->GetInventorySlot(InventoryType.EquippedItems, x); var itemSlot = inventoryManager->GetInventorySlot(InventoryType.EquippedItems, x);
if (itemSlot != null && itemSlot->ItemId == itemId) if (itemSlot != null && itemSlot->ItemId == Task.ItemId)
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
@ -125,12 +113,12 @@ internal static class EquipItem
if (equippedContainer == null) if (equippedContainer == null)
return; return;
foreach (ushort slot in targetSlots) foreach (ushort slot in _targetSlots)
{ {
var itemSlot = equippedContainer->GetInventorySlot(slot); 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; return;
} }
} }
@ -141,24 +129,24 @@ internal static class EquipItem
if (sourceContainer == null) if (sourceContainer == null)
continue; continue;
if (inventoryManager->GetItemCountInContainer(itemId, sourceInventoryType, true) == 0 && if (inventoryManager->GetItemCountInContainer(Task.ItemId, sourceInventoryType, true) == 0 &&
inventoryManager->GetItemCountInContainer(itemId, sourceInventoryType) == 0) inventoryManager->GetItemCountInContainer(Task.ItemId, sourceInventoryType) == 0)
continue; continue;
for (ushort sourceSlot = 0; sourceSlot < sourceContainer->Size; sourceSlot++) for (ushort sourceSlot = 0; sourceSlot < sourceContainer->Size; sourceSlot++)
{ {
var sourceItem = sourceContainer->GetInventorySlot(sourceSlot); var sourceItem = sourceContainer->GetInventorySlot(sourceSlot);
if (sourceItem == null || sourceItem->ItemId != itemId) if (sourceItem == null || sourceItem->ItemId != Task.ItemId)
continue; continue;
// Move the item to the first available slot // Move the item to the first available slot
ushort targetSlot = targetSlots ushort targetSlot = _targetSlots
.Where(x => .Where(x =>
{ {
var itemSlot = inventoryManager->GetInventorySlot(InventoryType.EquippedItems, x); var itemSlot = inventoryManager->GetInventorySlot(InventoryType.EquippedItems, x);
return itemSlot == null || itemSlot->ItemId == 0; return itemSlot == null || itemSlot->ItemId == 0;
}) })
.Concat(targetSlots).First(); .Concat(_targetSlots).First();
logger.LogInformation( logger.LogInformation(
"Equipping item from {SourceInventory}, {SourceSlot} to {TargetInventory}, {TargetSlot}", "Equipping item from {SourceInventory}, {SourceSlot} to {TargetInventory}, {TargetSlot}",
@ -172,7 +160,17 @@ internal static class EquipItem
} }
} }
public override string ToString() => $"Equip({item.Name})"; private static List<ushort>? GetEquipSlot(Item item)
{
return item.EquipSlotCategory.Row switch
{
>= 1 and <= 11 => [(ushort)(item.EquipSlotCategory.Row - 1)],
12 => [11, 12], // rings
13 => [0],
17 => [13], // soul crystal
_ => null
};
}
public bool OnErrorToast(SeString message) public bool OnErrorToast(SeString message)
{ {

View File

@ -10,23 +10,18 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class EquipRecommended 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) public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
if (step.InteractionType != EInteractionType.EquipRecommended) if (step.InteractionType != EInteractionType.EquipRecommended)
return null; return null;
return DoEquip(); return new EquipTask();
}
public ITask DoEquip()
{
return new DoEquipRecommended(clientState, chatGui);
} }
} }
internal sealed class BeforeDutyOrInstance(IClientState clientState, IChatGui chatGui) : SimpleTaskFactory internal sealed class BeforeDutyOrInstance : SimpleTaskFactory
{ {
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step) public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -35,21 +30,26 @@ internal static class EquipRecommended
step.InteractionType != EInteractionType.Combat) step.InteractionType != EInteractionType.Combat)
return null; 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; private bool _equipped;
public bool Start() protected override bool Start()
{ {
RecommendEquipModule.Instance()->SetupForClassJob((byte)clientState.LocalPlayer!.ClassJob.Id); RecommendEquipModule.Instance()->SetupForClassJob((byte)clientState.LocalPlayer!.ClassJob.Id);
return true; return true;
} }
public ETaskResult Update() public override ETaskResult Update()
{ {
var recommendedEquipModule = RecommendEquipModule.Instance(); var recommendedEquipModule = RecommendEquipModule.Instance();
if (recommendedEquipModule->IsUpdating) if (recommendedEquipModule->IsUpdating)
@ -94,7 +94,5 @@ internal static class EquipRecommended
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
public override string ToString() => "EquipRecommended";
} }
} }

View File

@ -5,7 +5,6 @@ using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Shared; using Questionable.Controller.Steps.Shared;
using Questionable.Functions; using Questionable.Functions;
@ -16,12 +15,7 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Interact internal static class Interact
{ {
internal sealed class Factory( internal sealed class Factory(Configuration configuration) : ITaskFactory
GameFunctions gameFunctions,
Configuration configuration,
ICondition condition,
ILoggerFactory loggerFactory)
: ITaskFactory
{ {
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -55,58 +49,50 @@ internal static class Interact
if (sequence.Sequence == 0 && sequence.Steps.IndexOf(step) == 0) if (sequence.Sequence == 0 && sequence.Steps.IndexOf(step) == 0)
yield return new WaitAtEnd.WaitDelay(); 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.TargetTerritoryId != null || quest.Id is SatisfactionSupplyNpcId ||
step.SkipConditions is { StepIf.Never: true }, step.PickUpItemId, step.SkipConditions?.StepIf); step.SkipConditions is { StepIf.Never: true }, step.PickUpItemId, step.SkipConditions?.StepIf);
} }
}
internal ITask Interact(uint dataId, Quest? quest, EInteractionType interactionType, internal sealed record Task(
bool skipMarkerCheck = false, uint? pickUpItemId = null, SkipStepConditions? skipConditions = null) uint DataId,
{ Quest? Quest,
return new DoInteract(dataId, quest, interactionType, skipMarkerCheck, pickUpItemId, skipConditions, EInteractionType InteractionType,
gameFunctions, condition, loggerFactory.CreateLogger<DoInteract>()); bool SkipMarkerCheck = false,
} uint? PickUpItemId = null,
SkipStepConditions? SkipConditions = null) : ITask
{
public override string ToString() => $"Interact({DataId})";
} }
internal sealed class DoInteract( internal sealed class DoInteract(
uint dataId,
Quest? quest,
EInteractionType interactionType,
bool skipMarkerCheck,
uint? pickUpItemId,
SkipStepConditions? skipConditions,
GameFunctions gameFunctions, GameFunctions gameFunctions,
ICondition condition, ICondition condition,
ILogger<DoInteract> logger) ILogger<DoInteract> logger)
: ITask : TaskExecutor<Task>
{ {
private bool _needsUnmount; private bool _needsUnmount;
private InteractionProgressContext? _progressContext;
private DateTime _continueAt = DateTime.MinValue; 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; InteractionType = Task.InteractionType;
set => interactionType = value;
}
public InteractionProgressContext? ProgressContext() => _progressContext; IGameObject? gameObject = gameFunctions.FindObjectByDataId(Task.DataId);
public bool Start()
{
IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId);
if (gameObject == null) if (gameObject == null)
{ {
logger.LogWarning("No game object with dataId {DataId}", dataId); logger.LogWarning("No game object with dataId {DataId}", Task.DataId);
return false; 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)", logger.LogInformation("Not interacting with {DataId} because it is not targetable (but skippable)",
dataId); Task.DataId);
return false; return false;
} }
@ -114,7 +100,7 @@ internal static class Interact
if (!gameObject.IsTargetable && condition[ConditionFlag.Mounted] && if (!gameObject.IsTargetable && condition[ConditionFlag.Mounted] &&
gameObject.ObjectKind != ObjectKind.GatheringPoint) 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; _needsUnmount = true;
gameFunctions.Unmount(); gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(1); _continueAt = DateTime.Now.AddSeconds(1);
@ -123,7 +109,7 @@ internal static class Interact
if (gameObject.IsTargetable && HasAnyMarker(gameObject)) if (gameObject.IsTargetable && HasAnyMarker(gameObject))
{ {
_progressContext = ProgressContext =
InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(gameObject)); InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(gameObject));
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(0.5);
return true; return true;
@ -132,7 +118,7 @@ internal static class Interact
return true; return true;
} }
public ETaskResult Update() public override ETaskResult Update()
{ {
if (DateTime.Now <= _continueAt) if (DateTime.Now <= _continueAt)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -149,29 +135,29 @@ internal static class Interact
_needsUnmount = false; _needsUnmount = false;
} }
if (pickUpItemId != null) if (Task.PickUpItemId != null)
{ {
unsafe unsafe
{ {
InventoryManager* inventoryManager = InventoryManager.Instance(); InventoryManager* inventoryManager = InventoryManager.Instance();
if (inventoryManager->GetInventoryItemCount(pickUpItemId.Value) > 0) if (inventoryManager->GetInventoryItemCount(Task.PickUpItemId.Value) > 0)
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
} }
else else
{ {
if (_progressContext != null && _progressContext.WasSuccessful()) if (ProgressContext != null && ProgressContext.WasSuccessful())
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
if (interactionType == EInteractionType.Gather && condition[ConditionFlag.Gathering]) if (InteractionType == EInteractionType.Gather && condition[ConditionFlag.Gathering])
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId); IGameObject? gameObject = gameFunctions.FindObjectByDataId(Task.DataId);
if (gameObject == null || !gameObject.IsTargetable || !HasAnyMarker(gameObject)) if (gameObject == null || !gameObject.IsTargetable || !HasAnyMarker(gameObject))
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
_progressContext = ProgressContext =
InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(gameObject)); InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(gameObject));
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(0.5);
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -179,13 +165,11 @@ internal static class Interact
private unsafe bool HasAnyMarker(IGameObject gameObject) private unsafe bool HasAnyMarker(IGameObject gameObject)
{ {
if (skipMarkerCheck || gameObject.ObjectKind != ObjectKind.EventNpc) if (Task.SkipMarkerCheck || gameObject.ObjectKind != ObjectKind.EventNpc)
return true; return true;
var gameObjectStruct = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject.Address; var gameObjectStruct = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject.Address;
return gameObjectStruct->NamePlateIconId != 0; return gameObjectStruct->NamePlateIconId != 0;
} }
public override string ToString() => $"Interact({dataId})";
} }
} }

View File

@ -12,12 +12,7 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Jump internal static class Jump
{ {
internal sealed class Factory( internal sealed class Factory : SimpleTaskFactory
MovementController movementController,
IClientState clientState,
IFramework framework,
ICondition condition,
ILoggerFactory loggerFactory) : SimpleTaskFactory
{ {
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step) public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -27,39 +22,42 @@ internal static class Jump
ArgumentNullException.ThrowIfNull(step.JumpDestination); ArgumentNullException.ThrowIfNull(step.JumpDestination);
if (step.JumpDestination.Type == EJumpType.SingleJump) if (step.JumpDestination.Type == EJumpType.SingleJump)
return SingleJump(step.DataId, step.JumpDestination, step.Comment); return new SingleJumpTask(step.DataId, step.JumpDestination, step.Comment);
else 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);
}
public ITask RepeatedJumps(uint? dataId, JumpDestination jumpDestination, string? comment)
{
return new DoRepeatedJumps(dataId, jumpDestination, comment, movementController, clientState, framework,
condition, loggerFactory.CreateLogger<DoRepeatedJumps>());
} }
} }
private class DoSingleJump( internal interface IJumpTask : ITask
uint? dataId, {
JumpDestination jumpDestination, uint? DataId { get; }
string? comment, JumpDestination JumpDestination { get; }
string? Comment { get; }
}
internal sealed record SingleJumpTask(
uint? DataId,
JumpDestination JumpDestination,
string? Comment) : IJumpTask
{
public override string ToString() => $"Jump({Comment})";
}
internal abstract class JumpBase<T>(
MovementController movementController, MovementController movementController,
IClientState clientState, 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(); float stopDistance = Task.JumpDestination.CalculateStopDistance();
if ((clientState.LocalPlayer!.Position - jumpDestination.Position).Length() <= stopDistance) if ((clientState.LocalPlayer!.Position - Task.JumpDestination.Position).Length() <= stopDistance)
return false; return false;
movementController.NavigateTo(EMovementType.Quest, dataId, [jumpDestination.Position], false, false, movementController.NavigateTo(EMovementType.Quest, Task.DataId, [Task.JumpDestination.Position], false,
jumpDestination.StopDistance ?? stopDistance); false,
Task.JumpDestination.StopDistance ?? stopDistance);
framework.RunOnTick(() => framework.RunOnTick(() =>
{ {
unsafe unsafe
@ -67,11 +65,11 @@ internal static class Jump
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2); ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2);
} }
}, },
TimeSpan.FromSeconds(jumpDestination.DelaySeconds ?? 0.5f)); TimeSpan.FromSeconds(Task.JumpDestination.DelaySeconds ?? 0.5f));
return true; return true;
} }
public virtual ETaskResult Update() public override ETaskResult Update()
{ {
if (movementController.IsPathfinding || movementController.IsPathRunning) if (movementController.IsPathfinding || movementController.IsPathRunning)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -82,30 +80,36 @@ internal static class Jump
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
public override string ToString() => $"Jump({comment})";
} }
private sealed class DoRepeatedJumps( internal sealed class DoSingleJump(
uint? dataId, MovementController movementController,
JumpDestination jumpDestination, IClientState clientState,
string? comment, IFramework framework) : JumpBase<SingleJumpTask>(movementController, clientState, framework);
internal sealed record RepeatedJumpTask(
uint? DataId,
JumpDestination JumpDestination,
string? Comment) : IJumpTask
{
public override string ToString() => $"RepeatedJump({Comment})";
}
internal sealed class DoRepeatedJumps(
MovementController movementController, MovementController movementController,
IClientState clientState, IClientState clientState,
IFramework framework, IFramework framework,
ICondition condition, ICondition condition,
ILogger<DoRepeatedJumps> logger) 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 readonly IClientState _clientState = clientState;
private DateTime _continueAt = DateTime.MinValue; private DateTime _continueAt = DateTime.MinValue;
private int _attempts; 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(); return base.Start();
} }
@ -114,13 +118,13 @@ internal static class Jump
if (DateTime.Now < _continueAt || condition[ConditionFlag.Jumping]) if (DateTime.Now < _continueAt || condition[ConditionFlag.Jumping])
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
float stopDistance = _jumpDestination.CalculateStopDistance(); float stopDistance = Task.JumpDestination.CalculateStopDistance();
if ((_clientState.LocalPlayer!.Position - _jumpDestination.Position).Length() <= stopDistance || if ((_clientState.LocalPlayer!.Position - Task.JumpDestination.Position).Length() <= stopDistance ||
_clientState.LocalPlayer.Position.Y >= _jumpDestination.Position.Y - 0.5f) _clientState.LocalPlayer.Position.Y >= Task.JumpDestination.Position.Y - 0.5f)
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
logger.LogTrace("Y-Heights for jumps: player={A}, target={B}", _clientState.LocalPlayer.Position.Y, 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 unsafe
{ {
if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2)) if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2))
@ -130,10 +134,8 @@ internal static class Jump
if (_attempts >= 50) if (_attempts >= 50)
throw new TaskException("Tried to jump too many times, didn't reach the target"); 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; return ETaskResult.StillRunning;
} }
public override string ToString() => $"RepeatedJump({_comment})";
} }
} }

View File

@ -10,10 +10,7 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Say internal static class Say
{ {
internal sealed class Factory( internal sealed class Factory(ExcelFunctions excelFunctions) : ITaskFactory
ChatFunctions chatFunctions,
Mount.Factory mountFactory,
ExcelFunctions excelFunctions) : ITaskFactory
{ {
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -33,20 +30,23 @@ internal static class Say
.GetString(); .GetString();
ArgumentNullException.ThrowIfNull(excelString); ArgumentNullException.ThrowIfNull(excelString);
var unmount = mountFactory.Unmount(); var unmount = new Mount.UnmountTask();
var task = new UseChat(excelString, chatFunctions); var task = new Task(excelString);
return [unmount, task]; 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() protected override bool StartInternal()
{ {
chatFunctions.ExecuteCommand($"/say {chatMessage}"); chatFunctions.ExecuteCommand($"/say {Task.ChatMessage}");
return true; return true;
} }
public override string ToString() => $"Say({chatMessage})";
} }
} }

View File

@ -5,9 +5,7 @@ using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Shared; using Questionable.Controller.Steps.Shared;
@ -24,17 +22,8 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class UseItem internal static class UseItem
{ {
internal sealed class Factory( 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, IClientState clientState,
TerritoryData territoryData, TerritoryData territoryData,
ILoggerFactory loggerFactory,
ILogger<Factory> logger) ILogger<Factory> logger)
: ITaskFactory : ITaskFactory
{ {
@ -59,7 +48,7 @@ internal static class UseItem
return CreateVesperBayFallbackTask(); 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); int currentStepIndex = sequence.Steps.IndexOf(step);
QuestStep? nextStep = sequence.Steps.Skip(currentStepIndex + 1).FirstOrDefault(); QuestStep? nextStep = sequence.Steps.Skip(currentStepIndex + 1).FirstOrDefault();
@ -67,27 +56,27 @@ internal static class UseItem
return return
[ [
task, task,
new WaitConditionTask(() => clientState.TerritoryType == 140, new WaitCondition.Task(() => clientState.TerritoryType == 140,
$"Wait(territory: {territoryData.GetNameAndId(140)})"), $"Wait(territory: {territoryData.GetNameAndId(140)})"),
mountFactory.Mount(140, new Mount.MountTask(140,
nextPosition != null ? Mount.EMountIf.AwayFromPosition : Mount.EMountIf.Always, nextPosition != null ? Mount.EMountIf.AwayFromPosition : Mount.EMountIf.Always,
nextPosition), nextPosition),
moveFactory.Move(new MoveTo.MoveParams(140, new(-408.92343f, 23.167036f, -351.16223f), null, 0.25f, new MoveTo.MoveTask(140, new(-408.92343f, 23.167036f, -351.16223f), null, 0.25f,
DataId: null, DisableNavMesh: true, Sprint: false, Fly: false)) DataId: null, DisableNavmesh: true, Sprint: false, Fly: false)
]; ];
} }
var unmount = mountFactory.Unmount(); var unmount = new Mount.UnmountTask();
if (step.GroundTarget == true) if (step.GroundTarget == true)
{ {
ITask task; ITask task;
if (step.DataId != null) 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); step.CompletionQuestVariablesFlags);
else else
{ {
ArgumentNullException.ThrowIfNull(step.Position); 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); step.CompletionQuestVariablesFlags);
} }
@ -95,43 +84,17 @@ internal static class UseItem
} }
else if (step.DataId != null) 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]; return [unmount, task];
} }
else 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]; 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() private IEnumerable<ITask> CreateVesperBayFallbackTask()
{ {
logger.LogWarning("No vesper bay aetheryte tickets in inventory, navigating via ferry in Limsa instead"); logger.LogWarning("No vesper bay aetheryte tickets in inventory, navigating via ferry in Limsa instead");
@ -139,39 +102,40 @@ internal static class UseItem
uint npcId = 1003540; uint npcId = 1003540;
ushort territoryId = 129; ushort territoryId = 129;
Vector3 destination = new(-360.9217f, 8f, 38.92566f); Vector3 destination = new(-360.9217f, 8f, 38.92566f);
yield return aetheryteShortcutFactory.Use(null, null, EAetheryteLocation.Limsa, territoryId); yield return new AetheryteShortcut.Task(null, null, EAetheryteLocation.Limsa, territoryId);
yield return aethernetShortcutFactory.Use(EAetheryteLocation.Limsa, EAetheryteLocation.LimsaArcanist); yield return new AethernetShortcut.Task(EAetheryteLocation.Limsa, EAetheryteLocation.LimsaArcanist);
yield return new WaitAtEnd.WaitDelay(); yield return new WaitAtEnd.WaitDelay();
yield return yield return new MoveTo.MoveTask(territoryId, destination, DataId: npcId, Sprint: false);
moveFactory.Move(new MoveTo.MoveParams(territoryId, destination, DataId: npcId, Sprint: false)); yield return new Interact.Task(npcId, null, EInteractionType.None, true);
yield return interactFactory.Interact(npcId, null, EInteractionType.None, true);
} }
} }
private abstract class UseItemBase( internal interface IUseItemBase : ITask
ElementId? questId, {
uint itemId, ElementId? QuestId { get; }
IList<QuestWorkValue?> completionQuestVariablesFlags, uint ItemId { get; }
bool startingCombat, IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; }
bool StartingCombat { get; }
}
internal abstract class UseItemExecutorBase<T>(
QuestFunctions questFunctions, QuestFunctions questFunctions,
ICondition condition, ICondition condition,
ILogger logger) : ITask ILogger logger) : TaskExecutor<T>
where T : class, IUseItemBase
{ {
private bool _usedItem; private bool _usedItem;
private DateTime _continueAt; private DateTime _continueAt;
private int _itemCount; private int _itemCount;
private InteractionProgressContext? _progressContext;
public InteractionProgressContext? ProgressContext() => _progressContext; private ElementId? QuestId => Task.QuestId;
protected uint ItemId => Task.ItemId;
public ElementId? QuestId => questId; private IList<QuestWorkValue?> CompletionQuestVariablesFlags => Task.CompletionQuestVariablesFlags;
public uint ItemId => itemId; private bool StartingCombat => Task.StartingCombat;
public IList<QuestWorkValue?> CompletionQuestVariablesFlags => completionQuestVariablesFlags;
public bool StartingCombat => startingCombat;
protected abstract bool UseItem(); protected abstract bool UseItem();
public unsafe bool Start() protected override unsafe bool Start()
{ {
InventoryManager* inventoryManager = InventoryManager.Instance(); InventoryManager* inventoryManager = InventoryManager.Instance();
if (inventoryManager == null) if (inventoryManager == null)
@ -181,12 +145,12 @@ internal static class UseItem
if (_itemCount == 0) if (_itemCount == 0)
throw new TaskException($"Don't have any {ItemId} in inventory (checks NQ only)"); throw new TaskException($"Don't have any {ItemId} in inventory (checks NQ only)");
_progressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem()); ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem());
_continueAt = DateTime.Now.Add(GetRetryDelay()); _continueAt = DateTime.Now.Add(GetRetryDelay());
return true; return true;
} }
public unsafe ETaskResult Update() public override unsafe ETaskResult Update()
{ {
if (QuestId is QuestId realQuestId && QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags)) if (QuestId is QuestId realQuestId && QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags))
{ {
@ -224,7 +188,7 @@ internal static class UseItem
if (!_usedItem) if (!_usedItem)
{ {
_progressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem()); ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem());
_continueAt = DateTime.Now.Add(GetRetryDelay()); _continueAt = DateTime.Now.Add(GetRetryDelay());
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
@ -241,69 +205,85 @@ internal static class UseItem
} }
} }
internal sealed record UseOnGround(
ElementId? QuestId,
uint DataId,
uint ItemId,
IList<QuestWorkValue?> CompletionQuestVariablesFlags) : IUseItemBase
{
public bool StartingCombat => false;
public override string ToString() => $"UseItem({ItemId} on ground at {DataId})";
}
private sealed class UseOnGround( internal sealed class UseOnGroundExecutor(
ElementId? questId,
uint dataId,
uint itemId,
IList<QuestWorkValue?> completionQuestVariablesFlags,
GameFunctions gameFunctions, GameFunctions gameFunctions,
QuestFunctions questFunctions, QuestFunctions questFunctions,
ICondition condition, ICondition condition,
ILogger<UseOnGround> logger) ILogger<UseOnGroundExecutor> logger)
: UseItemBase(questId, itemId, completionQuestVariablesFlags, false, questFunctions, condition, logger) : UseItemExecutorBase<UseOnGround>(questFunctions, condition, logger)
{ {
protected override bool UseItem() => gameFunctions.UseItemOnGround(dataId, ItemId); protected override bool UseItem() => gameFunctions.UseItemOnGround(Task.DataId, ItemId);
public override string ToString() => $"UseItem({ItemId} on ground at {dataId})";
} }
private sealed class UseOnPosition( internal sealed record UseOnPosition(
ElementId? questId, ElementId? QuestId,
Vector3 position, Vector3 Position,
uint itemId, uint ItemId,
IList<QuestWorkValue?> completionQuestVariablesFlags, IList<QuestWorkValue?> CompletionQuestVariablesFlags)
: IUseItemBase
{
public bool StartingCombat => false;
public override string ToString() =>
$"UseItem({ItemId} on ground at {Position.ToString("G", CultureInfo.InvariantCulture)})";
}
internal sealed class UseOnPositionExecutor(
GameFunctions gameFunctions, GameFunctions gameFunctions,
QuestFunctions questFunctions, QuestFunctions questFunctions,
ICondition condition, ICondition condition,
ILogger<UseOnPosition> logger) 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)})";
} }
private sealed class UseOnObject( internal sealed record UseOnObject(
ElementId? questId, ElementId? QuestId,
uint dataId, uint DataId,
uint itemId, uint ItemId,
IList<QuestWorkValue?> completionQuestVariablesFlags, IList<QuestWorkValue?> CompletionQuestVariablesFlags,
bool startingCombat, bool StartingCombat = false) : IUseItemBase
{
public override string ToString() => $"UseItem({ItemId} on {DataId})";
}
internal sealed class UseOnObjectExecutor(
QuestFunctions questFunctions, QuestFunctions questFunctions,
GameFunctions gameFunctions, GameFunctions gameFunctions,
ICondition condition, ICondition condition,
ILogger<UseOnObject> logger) 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})";
} }
private sealed class Use( internal sealed record UseOnSelf(
ElementId? questId, ElementId? QuestId,
uint itemId, uint ItemId,
IList<QuestWorkValue?> completionQuestVariablesFlags, IList<QuestWorkValue?> CompletionQuestVariablesFlags) : IUseItemBase
{
public bool StartingCombat => false;
public override string ToString() => $"UseItem({ItemId})";
}
internal sealed class UseOnSelfExecutor(
GameFunctions gameFunctions, GameFunctions gameFunctions,
QuestFunctions questFunctions, QuestFunctions questFunctions,
ICondition condition, ICondition condition,
ILogger<Use> logger) ILogger<UseOnSelf> logger)
: UseItemBase(questId, itemId, completionQuestVariablesFlags, false, questFunctions, condition, logger) : UseItemExecutorBase<UseOnSelf>(questFunctions, condition, logger)
{ {
protected override bool UseItem() => gameFunctions.UseItem(ItemId); protected override bool UseItem() => gameFunctions.UseItem(ItemId);
public override string ToString() => $"UseItem({ItemId})";
} }
} }

View File

@ -7,9 +7,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI; using LLib.GameUI;
using Microsoft.Extensions.DependencyInjection;
using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Common;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
@ -18,7 +16,7 @@ namespace Questionable.Controller.Steps.Leves;
internal static class InitiateLeve 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) public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -27,75 +25,86 @@ internal static class InitiateLeve
yield return new SkipInitiateIfActive(quest.Id); yield return new SkipInitiateIfActive(quest.Id);
yield return new OpenJournal(quest.Id); yield return new OpenJournal(quest.Id);
yield return new Initiate(quest.Id, gameGui); yield return new Initiate(quest.Id);
yield return new SelectDifficulty(gameGui); yield return new SelectDifficulty();
yield return new WaitConditionTask(() => condition[ConditionFlag.BoundByDuty], "Wait(BoundByDuty)"); yield return new WaitCondition.Task(() => condition[ConditionFlag.BoundByDuty], "Wait(BoundByDuty)");
} }
} }
internal sealed unsafe class SkipInitiateIfActive(ElementId elementId) : ITask internal sealed record SkipInitiateIfActive(ElementId ElementId) : ITask
{ {
public bool Start() => true; public override string ToString() => $"CheckIfAlreadyActive({ElementId})";
}
public ETaskResult Update() internal sealed unsafe class SkipInitiateIfActiveExecutor : TaskExecutor<SkipInitiateIfActive>
{
protected override bool Start() => true;
public override ETaskResult Update()
{ {
var director = UIState.Instance()->DirectorTodo.Director; var director = UIState.Instance()->DirectorTodo.Director;
if (director != null && if (director != null &&
director->EventHandlerInfo != null && director->EventHandlerInfo != null &&
director->EventHandlerInfo->EventId.ContentId == EventHandlerType.GatheringLeveDirector && director->EventHandlerInfo->EventId.ContentId == EventHandlerType.GatheringLeveDirector &&
director->ContentId == elementId.Value) director->ContentId == Task.ElementId.Value)
return ETaskResult.SkipRemainingTasksForStep; return ETaskResult.SkipRemainingTasksForStep;
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
public override string ToString() => $"CheckIfAlreadyActive({elementId})";
} }
internal sealed unsafe class OpenJournal(ElementId elementId) : ITask internal sealed record OpenJournal(ElementId ElementId) : ITask
{
public uint QuestType => ElementId is LeveId ? 2u : 1u;
public override string ToString() => $"OpenJournal({ElementId})";
}
internal sealed unsafe class OpenJournalExecutor : TaskExecutor<OpenJournal>
{ {
private readonly uint _questType = elementId is LeveId ? 2u : 1u;
private DateTime _openedAt = DateTime.MinValue; 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; _openedAt = DateTime.Now;
return true; return true;
} }
public ETaskResult Update() public override ETaskResult Update()
{ {
AgentQuestJournal* agentQuestJournal = AgentQuestJournal.Instance(); AgentQuestJournal* agentQuestJournal = AgentQuestJournal.Instance();
if (agentQuestJournal->IsAgentActive() && if (agentQuestJournal->IsAgentActive() &&
agentQuestJournal->SelectedQuestId == elementId.Value && agentQuestJournal->SelectedQuestId == Task.ElementId.Value &&
agentQuestJournal->SelectedQuestType == _questType) agentQuestJournal->SelectedQuestType == Task.QuestType)
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
if (DateTime.Now > _openedAt.AddSeconds(3)) if (DateTime.Now > _openedAt.AddSeconds(3))
{ {
AgentQuestJournal.Instance()->OpenForQuest(elementId.Value, _questType); AgentQuestJournal.Instance()->OpenForQuest(Task.ElementId.Value, Task.QuestType);
_openedAt = DateTime.Now; _openedAt = DateTime.Now;
} }
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
public override string ToString() => $"OpenJournal({elementId})";
} }
internal sealed unsafe class Initiate(ElementId elementId, IGameGui gameGui) : ITask internal sealed record Initiate(ElementId ElementId) : ITask
{ {
public bool Start() => true; public override string ToString() => $"InitiateLeve({ElementId})";
}
public ETaskResult Update() internal sealed unsafe class InitiateExecutor(IGameGui gameGui) : TaskExecutor<Initiate>
{
protected override bool Start() => true;
public override ETaskResult Update()
{ {
if (gameGui.TryGetAddonByName("JournalDetail", out AtkUnitBase* addonJournalDetail)) if (gameGui.TryGetAddonByName("JournalDetail", out AtkUnitBase* addonJournalDetail))
{ {
var pickQuest = stackalloc AtkValue[] var pickQuest = stackalloc AtkValue[]
{ {
new() { Type = ValueType.Int, Int = 4 }, 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); addonJournalDetail->FireCallback(2, pickQuest);
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
@ -103,21 +112,22 @@ internal static class InitiateLeve
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
public override string ToString() => $"InitiateLeve({elementId})";
} }
internal sealed unsafe class SelectDifficulty(IGameGui gameGui) : ITask internal sealed class SelectDifficulty : ITask
{ {
public bool Start() => true; public override string ToString() => "SelectLeveDifficulty";
}
public ETaskResult Update() internal sealed unsafe class SelectDifficultyExecutor(IGameGui gameGui) : TaskExecutor<SelectDifficulty>
{
protected override bool Start() => true;
public override ETaskResult Update()
{ {
if (gameGui.TryGetAddonByName("GuildLeveDifficulty", out AtkUnitBase* addon)) if (gameGui.TryGetAddonByName("GuildLeveDifficulty", out AtkUnitBase* addon))
{ {
// atkvalues: 1 → default difficulty, 2 → min, 3 → max // atkvalues: 1 → default difficulty, 2 → min, 3 → max
var pickDifficulty = stackalloc AtkValue[] var pickDifficulty = stackalloc AtkValue[]
{ {
new() { Type = ValueType.Int, Int = 0 }, new() { Type = ValueType.Int, Int = 0 },
@ -129,7 +139,5 @@ internal static class InitiateLeve
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
public override string ToString() => "SelectLeveDifficulty";
} }
} }

View File

@ -5,7 +5,6 @@ using System.Numerics;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Common;
using Questionable.Data; using Questionable.Data;
@ -20,17 +19,7 @@ namespace Questionable.Controller.Steps.Shared;
internal static class AethernetShortcut internal static class AethernetShortcut
{ {
internal sealed class Factory( internal sealed class Factory(MovementController movementController)
MovementController movementController,
AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions,
QuestFunctions questFunctions,
IClientState clientState,
AetheryteData aetheryteData,
TerritoryData territoryData,
LifestreamIpc lifestreamIpc,
ICondition condition,
ILoggerFactory loggerFactory)
: ITaskFactory : ITaskFactory
{ {
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
@ -38,24 +27,28 @@ internal static class AethernetShortcut
if (step.AethernetShortcut == null) if (step.AethernetShortcut == null)
yield break; yield break;
yield return new WaitConditionTask(() => movementController.IsNavmeshReady, yield return new WaitCondition.Task(() => movementController.IsNavmeshReady,
"Wait(navmesh ready)"); "Wait(navmesh ready)");
yield return Use(step.AethernetShortcut.From, step.AethernetShortcut.To, yield return new Task(step.AethernetShortcut.From, step.AethernetShortcut.To,
step.SkipConditions?.AethernetShortcutIf); step.SkipConditions?.AethernetShortcutIf ?? new());
}
public ITask Use(EAetheryteLocation from, EAetheryteLocation to, SkipAetheryteCondition? skipConditions = null)
{
return new UseAethernetShortcut(from, to, skipConditions ?? new(),
loggerFactory.CreateLogger<UseAethernetShortcut>(), aetheryteFunctions, gameFunctions, questFunctions,
clientState, aetheryteData, territoryData, lifestreamIpc, movementController, condition);
} }
} }
internal sealed record Task(
EAetheryteLocation From,
EAetheryteLocation To,
SkipAetheryteCondition SkipConditions) : ISkippableTask
{
public Task(EAetheryteLocation from,
EAetheryteLocation to)
: this(from, to, new())
{
}
public override string ToString() => $"UseAethernet({From} -> {To})";
}
internal sealed class UseAethernetShortcut( internal sealed class UseAethernetShortcut(
EAetheryteLocation from,
EAetheryteLocation to,
SkipAetheryteCondition skipConditions,
ILogger<UseAethernetShortcut> logger, ILogger<UseAethernetShortcut> logger,
AetheryteFunctions aetheryteFunctions, AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions, GameFunctions gameFunctions,
@ -65,79 +58,80 @@ internal static class AethernetShortcut
TerritoryData territoryData, TerritoryData territoryData,
LifestreamIpc lifestreamIpc, LifestreamIpc lifestreamIpc,
MovementController movementController, MovementController movementController,
ICondition condition) : ISkippableTask ICondition condition) : TaskExecutor<Task>
{ {
private bool _moving; private bool _moving;
private bool _teleported; private bool _teleported;
private bool _triedMounting; private bool _triedMounting;
private DateTime _continueAt = DateTime.MinValue; private DateTime _continueAt = DateTime.MinValue;
public EAetheryteLocation From => from; public EAetheryteLocation From => Task.From;
public EAetheryteLocation To => to; 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"); logger.LogInformation("Skipping aethernet shortcut because the target is in the same territory");
return false; return false;
} }
if (skipConditions.InTerritory.Contains(clientState.TerritoryType)) if (Task.SkipConditions.InTerritory.Contains(clientState.TerritoryType))
{ {
logger.LogInformation( logger.LogInformation(
"Skipping aethernet shortcut because the target is in the specified territory"); "Skipping aethernet shortcut because the target is in the specified territory");
return false; return false;
} }
if (skipConditions.QuestsCompleted.Count > 0 && if (Task.SkipConditions.QuestsCompleted.Count > 0 &&
skipConditions.QuestsCompleted.All(questFunctions.IsQuestComplete)) Task.SkipConditions.QuestsCompleted.All(questFunctions.IsQuestComplete))
{ {
logger.LogInformation("Skipping aethernet shortcut, all prequisite quests are complete"); logger.LogInformation("Skipping aethernet shortcut, all prequisite quests are complete");
return true; return true;
} }
if (skipConditions.QuestsAccepted.Count > 0 && if (Task.SkipConditions.QuestsAccepted.Count > 0 &&
skipConditions.QuestsAccepted.All(questFunctions.IsQuestAccepted)) Task.SkipConditions.QuestsAccepted.All(questFunctions.IsQuestAccepted))
{ {
logger.LogInformation("Skipping aethernet shortcut, all prequisite quests are accepted"); logger.LogInformation("Skipping aethernet shortcut, all prequisite quests are accepted");
return true; return true;
} }
if (skipConditions.AetheryteLocked != null && if (Task.SkipConditions.AetheryteLocked != null &&
!aetheryteFunctions.IsAetheryteUnlocked(skipConditions.AetheryteLocked.Value)) !aetheryteFunctions.IsAetheryteUnlocked(Task.SkipConditions.AetheryteLocked.Value))
{ {
logger.LogInformation("Skipping aethernet shortcut because the target aetheryte is locked"); logger.LogInformation("Skipping aethernet shortcut because the target aetheryte is locked");
return false; return false;
} }
if (skipConditions.AetheryteUnlocked != null && if (Task.SkipConditions.AetheryteUnlocked != null &&
aetheryteFunctions.IsAetheryteUnlocked(skipConditions.AetheryteUnlocked.Value)) aetheryteFunctions.IsAetheryteUnlocked(Task.SkipConditions.AetheryteUnlocked.Value))
{ {
logger.LogInformation("Skipping aethernet shortcut because the target aetheryte is unlocked"); logger.LogInformation("Skipping aethernet shortcut because the target aetheryte is unlocked");
return false; return false;
} }
} }
if (aetheryteFunctions.IsAetheryteUnlocked(from) && if (aetheryteFunctions.IsAetheryteUnlocked(Task.From) &&
aetheryteFunctions.IsAetheryteUnlocked(to)) aetheryteFunctions.IsAetheryteUnlocked(Task.To))
{ {
ushort territoryType = clientState.TerritoryType; ushort territoryType = clientState.TerritoryType;
Vector3 playerPosition = clientState.LocalPlayer!.Position; Vector3 playerPosition = clientState.LocalPlayer!.Position;
// closer to the source // closer to the source
if (aetheryteData.CalculateDistance(playerPosition, territoryType, from) < if (aetheryteData.CalculateDistance(playerPosition, territoryType, Task.From) <
aetheryteData.CalculateDistance(playerPosition, territoryType, to)) aetheryteData.CalculateDistance(playerPosition, territoryType, Task.To))
{ {
if (aetheryteData.CalculateDistance(playerPosition, territoryType, from) < if (aetheryteData.CalculateDistance(playerPosition, territoryType, Task.From) <
(from.IsFirmamentAetheryte() ? 11f : 4f)) (Task.From.IsFirmamentAetheryte() ? 11f : 4f))
{ {
DoTeleport(); DoTeleport();
return true; return true;
} }
else if (from == EAetheryteLocation.SolutionNine) else if (Task.From == EAetheryteLocation.SolutionNine)
{ {
logger.LogInformation("Moving to S9 aetheryte"); logger.LogInformation("Moving to S9 aetheryte");
List<Vector3> nearbyPoints = List<Vector3> nearbyPoints =
@ -150,14 +144,14 @@ internal static class AethernetShortcut
Vector3 closestPoint = nearbyPoints.MinBy(x => (playerPosition - x).Length()); Vector3 closestPoint = nearbyPoints.MinBy(x => (playerPosition - x).Length());
_moving = true; _moving = true;
movementController.NavigateTo(EMovementType.Quest, (uint)from, closestPoint, false, true, movementController.NavigateTo(EMovementType.Quest, (uint)Task.From, closestPoint, false, true,
0.25f); 0.25f);
return true; return true;
} }
else else
{ {
if (territoryData.CanUseMount(territoryType) && if (territoryData.CanUseMount(territoryType) &&
aetheryteData.CalculateDistance(playerPosition, territoryType, from) > 30 && aetheryteData.CalculateDistance(playerPosition, territoryType, Task.From) > 30 &&
!gameFunctions.HasStatusPreventingMount()) !gameFunctions.HasStatusPreventingMount())
{ {
_triedMounting = gameFunctions.Mount(); _triedMounting = gameFunctions.Mount();
@ -176,7 +170,7 @@ internal static class AethernetShortcut
else else
logger.LogWarning( logger.LogWarning(
"Aethernet shortcut not unlocked (from: {FromAetheryte}, to: {ToAetheryte}), walking manually", "Aethernet shortcut not unlocked (from: {FromAetheryte}, to: {ToAetheryte}), walking manually",
from, to); Task.From, Task.To);
return false; return false;
} }
@ -185,34 +179,34 @@ internal static class AethernetShortcut
{ {
logger.LogInformation("Moving to aethernet shortcut"); logger.LogInformation("Moving to aethernet shortcut");
_moving = true; _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, EAetheryteLocation.UldahChamberOfRule => 5f,
_ when AetheryteConverter.IsLargeAetheryte(from) => 10.9f, _ when AetheryteConverter.IsLargeAetheryte(Task.From) => 10.9f,
_ => 6.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, false, true,
distance); distance);
} }
private void DoTeleport() private void DoTeleport()
{ {
if (from.IsFirmamentAetheryte()) if (Task.From.IsFirmamentAetheryte())
{ {
logger.LogInformation("Using manual teleport interaction"); logger.LogInformation("Using manual teleport interaction");
_teleported = gameFunctions.InteractWith((uint)from, ObjectKind.EventObj); _teleported = gameFunctions.InteractWith((uint)Task.From, ObjectKind.EventObj);
} }
else else
{ {
logger.LogInformation("Using lifestream to teleport to {Destination}", to); logger.LogInformation("Using lifestream to teleport to {Destination}", Task.To);
lifestreamIpc.Teleport(to); lifestreamIpc.Teleport(Task.To);
_teleported = true; _teleported = true;
} }
} }
public ETaskResult Update() public override ETaskResult Update()
{ {
if (DateTime.Now < _continueAt) if (DateTime.Now < _continueAt)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -247,29 +241,27 @@ internal static class AethernetShortcut
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
if (aetheryteData.IsAirshipLanding(to)) if (aetheryteData.IsAirshipLanding(Task.To))
{ {
if (aetheryteData.CalculateAirshipLandingDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero, if (aetheryteData.CalculateAirshipLandingDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero,
clientState.TerritoryType, to) > 5) clientState.TerritoryType, Task.To) > 5)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
else if (aetheryteData.IsCityAetheryte(to)) else if (aetheryteData.IsCityAetheryte(Task.To))
{ {
if (aetheryteData.CalculateDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero, if (aetheryteData.CalculateDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero,
clientState.TerritoryType, to) > 20) clientState.TerritoryType, Task.To) > 20)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
else else
{ {
// some overworld location (e.g. 'Tesselation (Lakeland)' would end up here // 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.StillRunning;
} }
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
public override string ToString() => $"UseAethernet({from} -> {to})";
} }
} }

View File

@ -3,10 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Common;
using Questionable.Controller.Utils; using Questionable.Controller.Utils;
using Questionable.Data; using Questionable.Data;
using Questionable.Functions; using Questionable.Functions;
@ -18,55 +15,42 @@ namespace Questionable.Controller.Steps.Shared;
internal static class AetheryteShortcut internal static class AetheryteShortcut
{ {
internal sealed class Factory( internal sealed class Factory(AetheryteData aetheryteData) : ITaskFactory
AetheryteData aetheryteData,
AetheryteFunctions aetheryteFunctions,
QuestFunctions questFunctions,
IClientState clientState,
IChatGui chatGui,
ILoggerFactory loggerFactory) : ITaskFactory
{ {
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{ {
if (step.AetheryteShortcut == null) if (step.AetheryteShortcut == null)
yield break; 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]); aetheryteData.TerritoryIds[step.AetheryteShortcut.Value]);
yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(0.5)); 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> /// <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( internal sealed record Task(
QuestStep? step, QuestStep? Step,
ElementId? elementId, ElementId? ElementId,
EAetheryteLocation targetAetheryte, EAetheryteLocation TargetAetheryte,
ushort expectedTerritoryId, ushort ExpectedTerritoryId) : ISkippableTask
{
}
internal sealed class UseAetheryteShortcut(
ILogger<UseAetheryteShortcut> logger, ILogger<UseAetheryteShortcut> logger,
AetheryteFunctions aetheryteFunctions, AetheryteFunctions aetheryteFunctions,
QuestFunctions questFunctions, QuestFunctions questFunctions,
IClientState clientState, IClientState clientState,
IChatGui chatGui, IChatGui chatGui,
AetheryteData aetheryteData) : ISkippableTask AetheryteData aetheryteData) : TaskExecutor<Task>
{ {
private bool _teleported; private bool _teleported;
private DateTime _continueAt; private DateTime _continueAt;
private InteractionProgressContext? _progressContext;
public InteractionProgressContext? ProgressContext() => _progressContext; protected override bool Start() => !ShouldSkipTeleport();
public bool Start() => !ShouldSkipTeleport(); public override ETaskResult Update()
public ETaskResult Update()
{ {
if (DateTime.Now < _continueAt) if (DateTime.Now < _continueAt)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -77,7 +61,7 @@ internal static class AetheryteShortcut
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
if (clientState.TerritoryType == expectedTerritoryId) if (clientState.TerritoryType == Task.ExpectedTerritoryId)
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -86,9 +70,9 @@ internal static class AetheryteShortcut
private bool ShouldSkipTeleport() private bool ShouldSkipTeleport()
{ {
ushort territoryType = clientState.TerritoryType; 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 is { Never: false })
{ {
if (skipConditions.InTerritory.Contains(territoryType)) if (skipConditions.InTerritory.Contains(territoryType))
@ -125,12 +109,12 @@ internal static class AetheryteShortcut
return true; return true;
} }
if (elementId != null) if (Task.ElementId != null)
{ {
QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(elementId); QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(Task.ElementId);
if (skipConditions.RequiredQuestVariablesNotMet && if (skipConditions.RequiredQuestVariablesNotMet &&
questWork != null && questWork != null &&
!QuestWorkUtils.MatchesRequiredQuestWorkConfig(step.RequiredQuestVariables, questWork, !QuestWorkUtils.MatchesRequiredQuestWorkConfig(Task.Step.RequiredQuestVariables, questWork,
logger)) logger))
{ {
logger.LogInformation("Skipping aetheryte teleport, as required variables do not match"); logger.LogInformation("Skipping aetheryte teleport, as required variables do not match");
@ -151,7 +135,7 @@ internal static class AetheryteShortcut
} }
} }
if (expectedTerritoryId == territoryType) if (Task.ExpectedTerritoryId == territoryType)
{ {
if (!skipConditions.Never) if (!skipConditions.Never)
{ {
@ -162,17 +146,19 @@ internal static class AetheryteShortcut
} }
Vector3 pos = clientState.LocalPlayer!.Position; Vector3 pos = clientState.LocalPlayer!.Position;
if (step.Position != null && if (Task.Step.Position != null &&
(pos - step.Position.Value).Length() < step.CalculateActualStopDistance()) (pos - Task.Step.Position.Value).Length() < Task.Step.CalculateActualStopDistance())
{ {
logger.LogInformation("Skipping aetheryte teleport, we're near the target"); logger.LogInformation("Skipping aetheryte teleport, we're near the target");
return true; return true;
} }
if (aetheryteData.CalculateDistance(pos, territoryType, targetAetheryte) < 20 || if (aetheryteData.CalculateDistance(pos, territoryType, Task.TargetAetheryte) < 20 ||
(step.AethernetShortcut != null && (Task.Step.AethernetShortcut != null &&
(aetheryteData.CalculateDistance(pos, territoryType, step.AethernetShortcut.From) < 20 || (aetheryteData.CalculateDistance(pos, territoryType, Task.Step.AethernetShortcut.From) <
aetheryteData.CalculateDistance(pos, territoryType, step.AethernetShortcut.To) < 20))) 20 ||
aetheryteData.CalculateDistance(pos, territoryType, Task.Step.AethernetShortcut.To) <
20)))
{ {
logger.LogInformation("Skipping aetheryte teleport"); logger.LogInformation("Skipping aetheryte teleport");
return true; return true;
@ -186,7 +172,7 @@ internal static class AetheryteShortcut
private bool DoTeleport() private bool DoTeleport()
{ {
if (!aetheryteFunctions.CanTeleport(targetAetheryte)) if (!aetheryteFunctions.CanTeleport(Task.TargetAetheryte))
{ {
if (!aetheryteFunctions.IsTeleportUnlocked()) if (!aetheryteFunctions.IsTeleportUnlocked())
throw new TaskException("Teleport is not unlocked, attune to any aetheryte first."); throw new TaskException("Teleport is not unlocked, attune to any aetheryte first.");
@ -198,29 +184,27 @@ internal static class AetheryteShortcut
_continueAt = DateTime.Now.AddSeconds(8); _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"); 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 else
{ {
_progressContext = chatGui.Print("[Questionable] Unable to teleport to aetheryte.");
InteractionProgressContext.FromActionUseOrDefault(() => aetheryteFunctions.TeleportAetheryte(targetAetheryte)); throw new TaskException("Unable to teleport to aetheryte");
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");
}
} }
} }
public override string ToString() => $"UseAetheryte({targetAetheryte})"; public override string ToString() => $"UseAetheryte({Task.TargetAetheryte})";
} }
} }

View File

@ -17,12 +17,7 @@ namespace Questionable.Controller.Steps.Shared;
internal static class Craft internal static class Craft
{ {
internal sealed class Factory( internal sealed class Factory : ITaskFactory
IDataManager dataManager,
IClientState clientState,
ArtisanIpc artisanIpc,
Mount.Factory mountFactory,
ILoggerFactory loggerFactory) : ITaskFactory
{ {
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -33,34 +28,36 @@ internal static class Craft
ArgumentNullException.ThrowIfNull(step.ItemCount); ArgumentNullException.ThrowIfNull(step.ItemCount);
return return
[ [
mountFactory.Unmount(), new Mount.UnmountTask(),
Craft(step.ItemId.Value, step.ItemCount.Value) 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>());
} }
private sealed class DoCraft( internal sealed record CraftTask(
uint itemId, uint ItemId,
int itemCount, int ItemCount) : ITask
{
public override string ToString() => $"Craft {ItemCount}x {ItemId} (with Artisan)";
}
internal sealed class DoCraft(
IDataManager dataManager, IDataManager dataManager,
IClientState clientState, IClientState clientState,
ArtisanIpc artisanIpc, ArtisanIpc artisanIpc,
ILogger<DoCraft> logger) : ITask ILogger<DoCraft> logger) : TaskExecutor<CraftTask>
{ {
public bool Start() protected override bool Start()
{ {
if (HasRequestedItems()) if (HasRequestedItems())
{ {
logger.LogInformation("Already own {ItemCount}x {ItemId}", itemCount, itemId); logger.LogInformation("Already own {ItemCount}x {ItemId}", Task.ItemCount, Task.ItemId);
return false; return false;
} }
RecipeLookup? recipeLookup = dataManager.GetExcelSheet<RecipeLookup>()!.GetRow(itemId); RecipeLookup? recipeLookup = dataManager.GetExcelSheet<RecipeLookup>()!.GetRow(Task.ItemId);
if (recipeLookup == null) 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 uint recipeId = (EClassJob)clientState.LocalPlayer!.ClassJob.Id switch
{ {
@ -92,19 +89,19 @@ internal static class Craft
} }
if (recipeId == 0) 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( logger.LogInformation(
"Starting craft for item {ItemId} with recipe {RecipeId} for {RemainingItemCount} items", "Starting craft for item {ItemId} with recipe {RecipeId} for {RemainingItemCount} items",
itemId, recipeId, remainingItemCount); Task.ItemId, recipeId, remainingItemCount);
if (!artisanIpc.CraftItem((ushort)recipeId, remainingItemCount)) if (!artisanIpc.CraftItem((ushort)recipeId, remainingItemCount))
throw new TaskException($"Failed to start Artisan craft for recipe {recipeId}"); throw new TaskException($"Failed to start Artisan craft for recipe {recipeId}");
return true; return true;
} }
public unsafe ETaskResult Update() public override unsafe ETaskResult Update()
{ {
if (HasRequestedItems() && !artisanIpc.IsCrafting()) if (HasRequestedItems() && !artisanIpc.IsCrafting())
{ {
@ -128,15 +125,13 @@ internal static class Craft
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
private bool HasRequestedItems() => GetOwnedItemCount() >= itemCount; private bool HasRequestedItems() => GetOwnedItemCount() >= Task.ItemCount;
private unsafe int GetOwnedItemCount() private unsafe int GetOwnedItemCount()
{ {
InventoryManager* inventoryManager = InventoryManager.Instance(); InventoryManager* inventoryManager = InventoryManager.Instance();
return inventoryManager->GetInventoryItemCount(itemId, isHq: false, checkEquipped: false) return inventoryManager->GetInventoryItemCount(Task.ItemId, isHq: false, checkEquipped: false)
+ inventoryManager->GetInventoryItemCount(itemId, isHq: true, checkEquipped: false); + inventoryManager->GetInventoryItemCount(Task.ItemId, isHq: true, checkEquipped: false);
} }
public override string ToString() => $"Craft {itemCount}x {itemId} (with Artisan)";
} }
} }

View File

@ -21,7 +21,6 @@ internal static class Gather
internal sealed class Factory( internal sealed class Factory(
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
MovementController movementController, MovementController movementController,
GatheringController gatheringController,
GatheringPointRegistry gatheringPointRegistry, GatheringPointRegistry gatheringPointRegistry,
IClientState clientState, IClientState clientState,
GatheringData gatheringData, GatheringData gatheringData,
@ -53,7 +52,7 @@ internal static class Gather
if (classJob != currentClassJob) if (classJob != currentClassJob)
{ {
yield return new SwitchClassJob(classJob, clientState); yield return new SwitchClassJob.Task(classJob);
} }
if (HasRequiredItems(itemToGather)) if (HasRequiredItems(itemToGather))
@ -71,20 +70,20 @@ internal static class Gather
foreach (var task in serviceProvider.GetRequiredService<TaskCreator>() foreach (var task in serviceProvider.GetRequiredService<TaskCreator>()
.CreateTasks(quest, gatheringSequence, gatheringStep)) .CreateTasks(quest, gatheringSequence, gatheringStep))
if (task is WaitAtEnd.NextStep) if (task is WaitAtEnd.NextStep)
yield return CreateSkipMarkerTask(); yield return new SkipMarker();
else else
yield return task; yield return task;
} }
} }
ushort territoryId = gatheringRoot.Steps.Last().TerritoryId; 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)})"); $"Wait(territory: {territoryData.GetNameAndId(territoryId)})");
yield return new WaitConditionTask(() => movementController.IsNavmeshReady, yield return new WaitCondition.Task(() => movementController.IsNavmeshReady,
"Wait(navmesh ready)"); "Wait(navmesh ready)");
yield return CreateStartGatheringTask(gatheringPointId, itemToGather); yield return new GatheringTask(gatheringPointId, itemToGather);
yield return new WaitAtEnd.WaitDelay(); yield return new WaitAtEnd.WaitDelay();
} }
} }
@ -109,38 +108,12 @@ internal static class Gather
minCollectability: (short)itemToGather.Collectability) >= minCollectability: (short)itemToGather.Collectability) >=
itemToGather.ItemCount; itemToGather.ItemCount;
} }
private StartGathering CreateStartGatheringTask(GatheringPointId gatheringPointId, GatheredItem gatheredItem)
{
return new StartGathering(gatheringPointId, gatheredItem, gatheringController);
}
private static SkipMarker CreateSkipMarkerTask()
{
return new SkipMarker();
}
} }
private sealed class StartGathering( internal sealed record GatheringTask(
GatheringPointId gatheringPointId, GatheringPointId gatheringPointId,
GatheredItem gatheredItem, GatheredItem gatheredItem) : ITask
GatheringController gatheringController) : ITask
{ {
public bool Start()
{
return gatheringController.Start(new GatheringController.GatheringRequest(gatheringPointId,
gatheredItem.ItemId, gatheredItem.AlternativeItemId, gatheredItem.ItemCount,
gatheredItem.Collectability));
}
public ETaskResult Update()
{
if (gatheringController.Update() == GatheringController.EStatus.Complete)
return ETaskResult.TaskComplete;
return ETaskResult.StillRunning;
}
public override string ToString() public override string ToString()
{ {
if (gatheredItem.Collectability == 0) if (gatheredItem.Collectability == 0)
@ -151,13 +124,35 @@ internal static class Gather
} }
} }
internal sealed class StartGathering(GatheringController gatheringController) : TaskExecutor<GatheringTask>
{
protected override bool Start()
{
return gatheringController.Start(new GatheringController.GatheringRequest(Task.gatheringPointId,
Task.gatheredItem.ItemId, Task.gatheredItem.AlternativeItemId, Task.gatheredItem.ItemCount,
Task.gatheredItem.Collectability));
}
public override ETaskResult Update()
{
if (gatheringController.Update() == GatheringController.EStatus.Complete)
return ETaskResult.TaskComplete;
return ETaskResult.StillRunning;
}
}
/// <summary> /// <summary>
/// A task that does nothing, but if we're skipping a step, this will be the task next in queue to be executed (instead of progressing to the next step) if gathering. /// A task that does nothing, but if we're skipping a step, this will be the task next in queue to be executed (instead of progressing to the next step) if gathering.
/// </summary> /// </summary>
internal sealed class SkipMarker : ITask internal sealed class SkipMarker : ITask
{ {
public bool Start() => true;
public ETaskResult Update() => ETaskResult.TaskComplete;
public override string ToString() => "Gather/SkipMarker"; public override string ToString() => "Gather/SkipMarker";
} }
internal sealed class DoSkip : TaskExecutor<SkipMarker>
{
protected override bool Start() => true;
public override ETaskResult Update() => ETaskResult.TaskComplete;
}
} }

View File

@ -11,7 +11,6 @@ using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using LLib; using LLib;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Common;
using Questionable.Data; using Questionable.Data;
@ -28,14 +27,9 @@ internal static class MoveTo
{ {
internal sealed class Factory( internal sealed class Factory(
MovementController movementController, MovementController movementController,
GameFunctions gameFunctions,
ICondition condition,
IDataManager dataManager,
IClientState clientState, IClientState clientState,
AetheryteData aetheryteData, AetheryteData aetheryteData,
TerritoryData territoryData, TerritoryData territoryData,
ILoggerFactory loggerFactory,
Mount.Factory mountFactory,
ILogger<Factory> logger) : ITaskFactory ILogger<Factory> logger) : ITaskFactory
{ {
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
@ -46,7 +40,7 @@ internal static class MoveTo
} }
else if (step is { DataId: not null, StopDistance: not null }) 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 }) else if (step is { InteractionType: EInteractionType.AttuneAetheryte, Aetheryte: not null })
{ {
@ -60,27 +54,6 @@ internal static class MoveTo
return []; 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) public IEnumerable<ITask> CreateMountTasks(ElementId questId, QuestStep step, Vector3 destination)
{ {
if (step.InteractionType == EInteractionType.Jump && step.JumpDestination != null && if (step.InteractionType == EInteractionType.Jump && step.JumpDestination != null &&
@ -91,146 +64,149 @@ internal static class MoveTo
yield break; 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)})"); $"Wait(territory: {territoryData.GetNameAndId(step.TerritoryId)})");
if (!step.DisableNavmesh) if (!step.DisableNavmesh)
{ {
yield return new WaitConditionTask(() => movementController.IsNavmeshReady, yield return new WaitCondition.Task(() => movementController.IsNavmeshReady,
"Wait(navmesh ready)"); "Wait(navmesh ready)");
yield return Move(step, destination); yield return new MoveTask(step, destination);
} }
else else
{ {
yield return Move(step, destination); yield return new MoveTask(step, destination);
} }
if (step is { Fly: true, Land: true }) 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 string _cannotExecuteAtThisTime;
private readonly MovementController _movementController; private readonly MovementController _movementController;
private readonly Mount.Factory _mountFactory;
private readonly GameFunctions _gameFunctions; private readonly GameFunctions _gameFunctions;
private readonly ILogger<MoveInternal> _logger; private readonly ILogger<MoveExecutor> _logger;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly Mount.MountExecutor _mountExecutor;
private readonly Mount.UnmountExecutor _unmountExecutor;
private readonly Action _startAction; private Action _startAction = null!;
private readonly Vector3 _destination; private Vector3 _destination;
private readonly MoveParams _moveParams;
private bool _canRestart; private bool _canRestart;
private ITask? _mountTask; private ITaskExecutor? _nestedExecutor;
public MoveInternal(MoveParams moveParams, public MoveExecutor(
MovementController movementController, MovementController movementController,
Mount.Factory mountFactory,
GameFunctions gameFunctions, GameFunctions gameFunctions,
ILogger<MoveInternal> logger, ILogger<MoveExecutor> logger,
IClientState clientState, IClientState clientState,
IDataManager dataManager) IDataManager dataManager,
Mount.MountExecutor mountExecutor,
Mount.UnmountExecutor unmountExecutor)
{ {
_movementController = movementController; _movementController = movementController;
_mountFactory = mountFactory;
_gameFunctions = gameFunctions; _gameFunctions = gameFunctions;
_logger = logger; _logger = logger;
_clientState = clientState; _clientState = clientState;
_mountExecutor = mountExecutor;
_unmountExecutor = unmountExecutor;
_cannotExecuteAtThisTime = dataManager.GetString<LogMessage>(579, x => x.Text)!; _cannotExecuteAtThisTime = dataManager.GetString<LogMessage>(579, x => x.Text)!;
_destination = moveParams.Destination; }
if (!gameFunctions.IsFlyingUnlocked(moveParams.TerritoryId)) private void Initialize()
{
_destination = Task.Destination;
if (!_gameFunctions.IsFlyingUnlocked(Task.TerritoryId))
{ {
moveParams = moveParams with { Fly = false, Land = false }; Task = Task with { Fly = false, Land = false };
} }
if (!moveParams.DisableNavMesh) if (!Task.DisableNavmesh)
{ {
_startAction = () => _startAction = () =>
_movementController.NavigateTo(EMovementType.Quest, moveParams.DataId, _destination, _movementController.NavigateTo(EMovementType.Quest, Task.DataId, _destination,
fly: moveParams.Fly, fly: Task.Fly,
sprint: moveParams.Sprint, sprint: Task.Sprint,
stopDistance: moveParams.StopDistance, stopDistance: Task.StopDistance,
ignoreDistanceToObject: moveParams.IgnoreDistanceToObject, ignoreDistanceToObject: Task.IgnoreDistanceToObject,
land: moveParams.Land); land: Task.Land);
} }
else else
{ {
_startAction = () => _startAction = () =>
_movementController.NavigateTo(EMovementType.Quest, moveParams.DataId, [_destination], _movementController.NavigateTo(EMovementType.Quest, Task.DataId, [_destination],
fly: moveParams.Fly, fly: Task.Fly,
sprint: moveParams.Sprint, sprint: Task.Sprint,
stopDistance: moveParams.StopDistance, stopDistance: Task.StopDistance,
ignoreDistanceToObject: moveParams.IgnoreDistanceToObject, ignoreDistanceToObject: Task.IgnoreDistanceToObject,
land: moveParams.Land); land: Task.Land);
} }
_moveParams = moveParams; _canRestart = Task.RestartNavigation;
_canRestart = moveParams.RestartNavigation;
} }
public InteractionProgressContext? ProgressContext() => _mountTask?.ProgressContext(); protected override bool Start()
public bool ShouldRedoOnInterrupt() => true;
public bool Start()
{ {
float stopDistance = _moveParams.StopDistance ?? QuestStep.DefaultStopDistance; Initialize();
float stopDistance = Task.StopDistance ?? QuestStep.DefaultStopDistance;
Vector3? position = _clientState.LocalPlayer?.Position; Vector3? position = _clientState.LocalPlayer?.Position;
float actualDistance = position == null ? float.MaxValue : Vector3.Distance(position.Value, _destination); 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); var mountTask = new Mount.MountTask(Task.TerritoryId, Mount.EMountIf.Always);
if (mountTask.Start()) if (_mountExecutor.Start(mountTask))
{ {
_mountTask = mountTask; _nestedExecutor = _mountExecutor;
return true; return true;
} }
} }
else if (_moveParams.Mount == false) else if (Task.Mount == false)
{ {
var mountTask = _mountFactory.Unmount(); var mountTask = new Mount.UnmountTask();
if (mountTask.Start()) if (_unmountExecutor.Start(mountTask))
{ {
_mountTask = mountTask; _nestedExecutor = _unmountExecutor;
return true; return true;
} }
} }
if (!_moveParams.DisableNavMesh) if (!Task.DisableNavmesh)
{ {
if (_moveParams.Mount == null) if (Task.Mount == null)
{ {
Mount.EMountIf mountIf = Mount.EMountIf mountIf =
actualDistance > stopDistance && _moveParams.Fly && actualDistance > stopDistance && Task.Fly &&
_gameFunctions.IsFlyingUnlocked(_moveParams.TerritoryId) _gameFunctions.IsFlyingUnlocked(Task.TerritoryId)
? Mount.EMountIf.Always ? Mount.EMountIf.Always
: Mount.EMountIf.AwayFromPosition; : Mount.EMountIf.AwayFromPosition;
var mountTask = _mountFactory.Mount(_moveParams.TerritoryId, mountIf, _destination); var mountTask = new Mount.MountTask(Task.TerritoryId, mountIf, _destination);
if (mountTask.Start()) if (_mountExecutor.Start(mountTask))
{ {
_mountTask = mountTask; _nestedExecutor = _mountExecutor;
return true; return true;
} }
} }
} }
_mountTask = new NoOpTask(); _nestedExecutor = new NoOpTaskExecutor();
return true; 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)); _logger.LogInformation("Moving to {Destination}", _destination.ToString("G", CultureInfo.InvariantCulture));
_startAction(); _startAction();
@ -247,10 +223,10 @@ internal static class MoveTo
if (_canRestart && if (_canRestart &&
Vector3.Distance(_clientState.LocalPlayer!.Position, _destination) > Vector3.Distance(_clientState.LocalPlayer!.Position, _destination) >
(_moveParams.StopDistance ?? QuestStep.DefaultStopDistance) + 5f) (Task.StopDistance ?? QuestStep.DefaultStopDistance) + 5f)
{ {
_canRestart = false; _canRestart = false;
if (_clientState.TerritoryType == _moveParams.TerritoryId) if (_clientState.TerritoryType == Task.TerritoryId)
{ {
_logger.LogInformation("Looks like movement was interrupted, re-attempting to move"); _logger.LogInformation("Looks like movement was interrupted, re-attempting to move");
_startAction(); _startAction();
@ -264,7 +240,6 @@ internal static class MoveTo
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
public override string ToString() => $"MoveTo({_destination.ToString("G", CultureInfo.InvariantCulture)})";
public bool OnErrorToast(SeString message) public bool OnErrorToast(SeString message)
{ {
@ -275,27 +250,27 @@ internal static class MoveTo
} }
} }
private sealed class NoOpTask : ITask private sealed class NoOpTaskExecutor : TaskExecutor<ITask>
{ {
public bool Start() => true; protected override bool Start() => true;
public ETaskResult Update() => ETaskResult.TaskComplete; public override ETaskResult Update() => ETaskResult.TaskComplete;
} }
internal sealed record MoveParams( internal sealed record MoveTask(
ushort TerritoryId, ushort TerritoryId,
Vector3 Destination, Vector3 Destination,
bool? Mount = null, bool? Mount = null,
float? StopDistance = null, float? StopDistance = null,
uint? DataId = null, uint? DataId = null,
bool DisableNavMesh = false, bool DisableNavmesh = false,
bool Sprint = true, bool Sprint = true,
bool Fly = false, bool Fly = false,
bool Land = false, bool Land = false,
bool IgnoreDistanceToObject = 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, : this(step.TerritoryId,
destination, destination,
step.Mount, step.Mount,
@ -309,23 +284,27 @@ internal static class MoveTo
step.RestartNavigationIfCancelled != false) step.RestartNavigationIfCancelled != false)
{ {
} }
public override string ToString() => $"MoveTo({Destination.ToString("G", CultureInfo.InvariantCulture)})";
} }
private sealed class WaitForNearDataId( internal sealed record WaitForNearDataId(uint DataId, float StopDistance) : ITask
uint dataId,
float stopDistance,
GameFunctions gameFunctions,
IClientState clientState) : ITask
{ {
public bool ShouldRedoOnInterrupt() => true; public bool ShouldRedoOnInterrupt() => true;
}
public bool Start() => true; internal sealed class WaitForNearDataIdExecutor(
GameFunctions gameFunctions,
IClientState clientState) : TaskExecutor<WaitForNearDataId>
{
public ETaskResult Update() protected override bool Start() => true;
public override ETaskResult Update()
{ {
IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId); IGameObject? gameObject = gameFunctions.FindObjectByDataId(Task.DataId);
if (gameObject == null || 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"); throw new TaskException("Object not found or too far away, no position so we can't move");
} }
@ -334,14 +313,17 @@ internal static class MoveTo
} }
} }
private sealed class LandTask(IClientState clientState, ICondition condition, ILogger<LandTask> logger) : ITask internal sealed class LandTask : ITask
{
public bool ShouldRedoOnInterrupt() => true;
}
internal sealed class LandExecutor(IClientState clientState, ICondition condition, ILogger<LandExecutor> logger) : TaskExecutor<LandTask>
{ {
private bool _landing; private bool _landing;
private DateTime _continueAt; private DateTime _continueAt;
public bool ShouldRedoOnInterrupt() => true; protected override bool Start()
public bool Start()
{ {
if (!condition[ConditionFlag.InFlight]) if (!condition[ConditionFlag.InFlight])
{ {
@ -354,7 +336,7 @@ internal static class MoveTo
return true; return true;
} }
public ETaskResult Update() public override ETaskResult Update()
{ {
if (DateTime.Now < _continueAt) if (DateTime.Now < _continueAt)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;

View File

@ -1,14 +1,9 @@
using System; using System.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Utils; using Questionable.Controller.Utils;
using Questionable.Functions; using Questionable.Functions;
@ -20,12 +15,7 @@ namespace Questionable.Controller.Steps.Shared;
internal static class SkipCondition internal static class SkipCondition
{ {
internal sealed class Factory( internal sealed class Factory : SimpleTaskFactory
ILoggerFactory loggerFactory,
AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions,
QuestFunctions questFunctions,
IClientState clientState) : SimpleTaskFactory
{ {
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step) public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -40,28 +30,31 @@ internal static class SkipCondition
step.NextQuestId == null) step.NextQuestId == null)
return 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);
} }
} }
private sealed class CheckSkip( internal sealed record SkipTask(
QuestStep step, QuestStep Step,
SkipStepConditions skipConditions, SkipStepConditions SkipConditions,
ElementId elementId, ElementId ElementId) : ITask
{
public override string ToString() => "CheckSkip";
}
internal sealed class CheckSkip(
ILogger<CheckSkip> logger, ILogger<CheckSkip> logger,
AetheryteFunctions aetheryteFunctions, AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions, GameFunctions gameFunctions,
QuestFunctions questFunctions, 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)); logger.LogInformation("Checking skip conditions; {ConfiguredConditions}", string.Join(",", skipConditions));
if (skipConditions.Flying == ELockedSkipCondition.Unlocked && if (skipConditions.Flying == ELockedSkipCondition.Unlocked &&
@ -204,7 +197,8 @@ internal static class SkipCondition
} }
} }
if (skipConditions.NearPosition is { } nearPosition && clientState.TerritoryType == nearPosition.TerritoryId) if (skipConditions.NearPosition is { } nearPosition &&
clientState.TerritoryType == nearPosition.TerritoryId)
{ {
if (Vector3.Distance(nearPosition.Position, clientState.LocalPlayer!.Position) <= if (Vector3.Distance(nearPosition.Position, clientState.LocalPlayer!.Position) <=
nearPosition.MaximumDistance) nearPosition.MaximumDistance)
@ -251,8 +245,6 @@ internal static class SkipCondition
return false; return false;
} }
public ETaskResult Update() => ETaskResult.SkipRemainingTasksForStep; public override ETaskResult Update() => ETaskResult.SkipRemainingTasksForStep;
public override string ToString() => "CheckSkip";
} }
} }

View File

@ -6,27 +6,30 @@ namespace Questionable.Controller.Steps.Shared;
internal static class StepDisabled 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) public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
if (!step.Disabled) if (!step.Disabled)
return null; 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";
}
public ETaskResult Update() internal sealed class Executor(ILogger<SkipRemainingTasks> logger) : TaskExecutor<SkipRemainingTasks>
{
protected override bool Start() => true;
public override ETaskResult Update()
{ {
logger.LogInformation("Skipping step, as it is disabled"); logger.LogInformation("Skipping step, as it is disabled");
return ETaskResult.SkipRemainingTasksForStep; return ETaskResult.SkipRemainingTasksForStep;
} }
public override string ToString() => "StepDisabled";
} }
} }

View File

@ -6,31 +6,37 @@ using Questionable.Controller.Steps.Common;
namespace Questionable.Controller.Steps.Shared; 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) public override string ToString() => $"SwitchJob({ClassJob})";
return false;
var gearsetModule = RaptureGearsetModule.Instance();
if (gearsetModule != null)
{
for (int i = 0; i < 100; ++i)
{
var gearset = gearsetModule->GetGearset(i);
if (gearset->ClassJob == (byte)classJob)
{
gearsetModule->EquipGearset(gearset->Id);
return true;
}
}
}
throw new TaskException($"No gearset found for {classJob}");
} }
protected override ETaskResult UpdateInternal() => ETaskResult.TaskComplete; internal sealed class Executor(IClientState clientState) : AbstractDelayedTaskExecutor<Task>
{
protected override unsafe bool StartInternal()
{
if (clientState.LocalPlayer!.ClassJob.Id == (uint)Task.ClassJob)
return false;
public override string ToString() => $"SwitchJob({classJob})"; var gearsetModule = RaptureGearsetModule.Instance();
if (gearsetModule != null)
{
for (int i = 0; i < 100; ++i)
{
var gearset = gearsetModule->GetGearset(i);
if (gearset->ClassJob == (byte)Task.ClassJob)
{
gearsetModule->EquipGearset(gearset->Id);
return true;
}
}
}
throw new TaskException($"No gearset found for {Task.ClassJob}");
}
protected override ETaskResult UpdateInternal() => ETaskResult.TaskComplete;
}
} }

View File

@ -19,9 +19,7 @@ internal static class WaitAtEnd
internal sealed class Factory( internal sealed class Factory(
IClientState clientState, IClientState clientState,
ICondition condition, ICondition condition,
TerritoryData territoryData, TerritoryData territoryData)
QuestFunctions questFunctions,
GameFunctions gameFunctions)
: ITaskFactory : ITaskFactory
{ {
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
@ -29,7 +27,7 @@ internal static class WaitAtEnd
if (step.CompletionQuestVariablesFlags.Count == 6 && if (step.CompletionQuestVariablesFlags.Count == 6 &&
QuestWorkUtils.HasCompletionFlags(step.CompletionQuestVariablesFlags)) 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(); var delay = new WaitDelay();
return [task, delay, Next(quest, sequence)]; return [task, delay, Next(quest, sequence)];
} }
@ -38,7 +36,7 @@ internal static class WaitAtEnd
{ {
case EInteractionType.Combat: case EInteractionType.Combat:
var notInCombat = var notInCombat =
new WaitConditionTask(() => !condition[ConditionFlag.InCombat], "Wait(not in combat)"); new WaitCondition.Task(() => !condition[ConditionFlag.InCombat], "Wait(not in combat)");
return return
[ [
new WaitDelay(), new WaitDelay(),
@ -67,8 +65,7 @@ internal static class WaitAtEnd
return return
[ [
new WaitObjectAtPosition(step.DataId.Value, step.Position.Value, step.NpcWaitDistance ?? 0.5f, new WaitObjectAtPosition(step.DataId.Value, step.Position.Value, step.NpcWaitDistance ?? 0.5f),
gameFunctions),
new WaitDelay(), new WaitDelay(),
Next(quest, sequence) Next(quest, sequence)
]; ];
@ -79,14 +76,14 @@ internal static class WaitAtEnd
if (step.TerritoryId != step.TargetTerritoryId) if (step.TerritoryId != step.TargetTerritoryId)
{ {
// interaction moves to a different territory // interaction moves to a different territory
waitInteraction = new WaitConditionTask( waitInteraction = new WaitCondition.Task(
() => clientState.TerritoryType == step.TargetTerritoryId, () => clientState.TerritoryType == step.TargetTerritoryId,
$"Wait(tp to territory: {territoryData.GetNameAndId(step.TargetTerritoryId.Value)})"); $"Wait(tp to territory: {territoryData.GetNameAndId(step.TargetTerritoryId.Value)})");
} }
else else
{ {
Vector3 lastPosition = step.Position ?? clientState.LocalPlayer?.Position ?? Vector3.Zero; Vector3 lastPosition = step.Position ?? clientState.LocalPlayer?.Position ?? Vector3.Zero;
waitInteraction = new WaitConditionTask(() => waitInteraction = new WaitCondition.Task(() =>
{ {
Vector3? currentPosition = clientState.LocalPlayer?.Position; Vector3? currentPosition = clientState.LocalPlayer?.Position;
if (currentPosition == null) if (currentPosition == null)
@ -109,7 +106,7 @@ internal static class WaitAtEnd
case EInteractionType.AcceptQuest: case EInteractionType.AcceptQuest:
{ {
var accept = new WaitQuestAccepted(step.PickUpQuestId ?? quest.Id, questFunctions); var accept = new WaitQuestAccepted(step.PickUpQuestId ?? quest.Id);
var delay = new WaitDelay(); var delay = new WaitDelay();
if (step.PickUpQuestId != null) if (step.PickUpQuestId != null)
return [accept, delay, Next(quest, sequence)]; return [accept, delay, Next(quest, sequence)];
@ -119,7 +116,7 @@ internal static class WaitAtEnd
case EInteractionType.CompleteQuest: case EInteractionType.CompleteQuest:
{ {
var complete = new WaitQuestCompleted(step.TurnInQuestId ?? quest.Id, questFunctions); var complete = new WaitQuestCompleted(step.TurnInQuestId ?? quest.Id);
var delay = new WaitDelay(); var delay = new WaitDelay();
if (step.TurnInQuestId != null) if (step.TurnInQuestId != null)
return [complete, delay, Next(quest, sequence)]; return [complete, delay, Next(quest, sequence)];
@ -139,103 +136,133 @@ internal static class WaitAtEnd
} }
} }
internal sealed class WaitDelay(TimeSpan? delay = null) : AbstractDelayedTask(delay ?? TimeSpan.FromSeconds(1)) internal sealed record WaitDelay(TimeSpan Delay) : ITask
{ {
protected override bool StartInternal() => true; public WaitDelay()
: this(TimeSpan.FromSeconds(1))
{
}
public override string ToString() => $"Wait(seconds: {Delay.TotalSeconds})"; 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 internal sealed class WaitNextStepOrSequence : ITask
{ {
public bool Start() => true;
public ETaskResult Update() => ETaskResult.StillRunning;
public override string ToString() => "Wait(next step or sequence)"; public override string ToString() => "Wait(next step or sequence)";
} }
internal sealed class WaitForCompletionFlags(QuestId quest, QuestStep step, QuestFunctions questFunctions) : ITask internal sealed class WaitNextStepOrSequenceExecutor : TaskExecutor<WaitNextStepOrSequence>
{ {
public bool Start() => true; protected override bool Start() => true;
public ETaskResult Update() public override ETaskResult Update() => ETaskResult.StillRunning;
}
internal sealed record WaitForCompletionFlags(QuestId Quest, QuestStep Step) : ITask
{
public override string ToString() =>
$"Wait(QW: {string.Join(", ", Step.CompletionQuestVariablesFlags.Select(x => x?.ToString() ?? "-"))})";
}
internal sealed class WaitForCompletionFlagsExecutor(QuestFunctions questFunctions)
: TaskExecutor<WaitForCompletionFlags>
{
protected override bool Start() => true;
public override ETaskResult Update()
{ {
QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(quest); QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(Task.Quest);
return questWork != null && return questWork != null &&
QuestWorkUtils.MatchesQuestWork(step.CompletionQuestVariablesFlags, questWork) QuestWorkUtils.MatchesQuestWork(Task.Step.CompletionQuestVariablesFlags, questWork)
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : ETaskResult.StillRunning;
} }
}
internal sealed record WaitObjectAtPosition(
uint DataId,
Vector3 Destination,
float Distance) : ITask
{
public override string ToString() => 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( internal sealed class WaitObjectAtPositionExecutor(GameFunctions gameFunctions) : TaskExecutor<WaitObjectAtPosition>
uint dataId,
Vector3 destination,
float distance,
GameFunctions gameFunctions) : ITask
{ {
public bool Start() => true; protected override bool Start() => true;
public ETaskResult Update() => public override ETaskResult Update() =>
gameFunctions.IsObjectAtPosition(dataId, destination, distance) gameFunctions.IsObjectAtPosition(Task.DataId, Task.Destination, Task.Distance)
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : ETaskResult.StillRunning;
public override string ToString() =>
$"WaitObj({dataId} at {destination.ToString("G", CultureInfo.InvariantCulture)} < {distance})";
} }
internal sealed class WaitQuestAccepted(ElementId elementId, QuestFunctions questFunctions) : ITask internal sealed record WaitQuestAccepted(ElementId ElementId) : ITask
{ {
public bool Start() => true; public override string ToString() => $"WaitQuestAccepted({ElementId})";
}
public ETaskResult Update() internal sealed class WaitQuestAcceptedExecutor(QuestFunctions questFunctions) : TaskExecutor<WaitQuestAccepted>
{
protected override bool Start() => true;
public override ETaskResult Update()
{ {
return questFunctions.IsQuestAccepted(elementId) return questFunctions.IsQuestAccepted(Task.ElementId)
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : ETaskResult.StillRunning;
} }
public override string ToString() => $"WaitQuestAccepted({elementId})";
} }
internal sealed class WaitQuestCompleted(ElementId elementId, QuestFunctions questFunctions) : ITask internal sealed record WaitQuestCompleted(ElementId ElementId) : ITask
{ {
public bool Start() => true; public override string ToString() => $"WaitQuestComplete({ElementId})";
}
public ETaskResult Update() internal sealed class WaitQuestCompletedExecutor(QuestFunctions questFunctions) : TaskExecutor<WaitQuestCompleted>
{
protected override bool Start() => true;
public override ETaskResult Update()
{ {
return questFunctions.IsQuestComplete(elementId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning; return questFunctions.IsQuestComplete(Task.ElementId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
} }
public override string ToString() => $"WaitQuestComplete({elementId})";
} }
internal sealed class NextStep(ElementId elementId, int sequence) : ILastTask internal sealed record NextStep(ElementId ElementId, int Sequence) : ILastTask
{ {
public ElementId ElementId { get; } = elementId;
public int Sequence { get; } = sequence;
public bool Start() => true;
public ETaskResult Update() => ETaskResult.NextStep;
public override string ToString() => "NextStep"; public override string ToString() => "NextStep";
} }
internal sealed class NextStepExecutor : TaskExecutor<NextStep>
{
protected override bool Start() => true;
public override ETaskResult Update() => ETaskResult.NextStep;
}
internal sealed class EndAutomation : ILastTask internal sealed class EndAutomation : ILastTask
{ {
public ElementId ElementId => throw new InvalidOperationException(); public ElementId ElementId => throw new InvalidOperationException();
public int Sequence => throw new InvalidOperationException(); public int Sequence => throw new InvalidOperationException();
public bool Start() => true;
public ETaskResult Update() => ETaskResult.End;
public override string ToString() => "EndAutomation"; public override string ToString() => "EndAutomation";
} }
internal sealed class EndAutomationExecutor : TaskExecutor<EndAutomation>
{
protected override bool Start() => true;
public override ETaskResult Update() => ETaskResult.End;
}
} }

View File

@ -18,10 +18,19 @@ internal static class WaitAtStart
} }
} }
internal sealed class WaitDelay(TimeSpan delay) : AbstractDelayedTask(delay)
{
protected override bool StartInternal() => true;
internal sealed record WaitDelay(TimeSpan Delay) : ITask
{
public override string ToString() => $"Wait[S](seconds: {Delay.TotalSeconds})"; public override string ToString() => $"Wait[S](seconds: {Delay.TotalSeconds})";
} }
internal sealed class WaitDelayExecutor : AbstractDelayedTaskExecutor<WaitDelay>
{
protected override bool StartInternal()
{
Delay = Task.Delay;
return true;
}
}
} }

View File

@ -0,0 +1,51 @@
using System;
namespace Questionable.Controller.Steps;
internal interface ITaskExecutor
{
ITask CurrentTask { get; }
Type GetTaskType();
bool Start(ITask task);
bool WasInterrupted();
ETaskResult Update();
}
internal abstract class TaskExecutor<T> : ITaskExecutor
where T : class, ITask
{
protected T Task { get; set; } = null!;
protected InteractionProgressContext? ProgressContext { get; set; }
ITask ITaskExecutor.CurrentTask => Task;
public bool WasInterrupted()
{
if (ProgressContext is {} progressContext)
{
progressContext.Update();
return progressContext.WasInterrupted();
}
return false;
}
public Type GetTaskType() => typeof(T);
protected abstract bool Start();
public bool Start(ITask task)
{
if (task is T t)
{
Task = t;
return Start();
}
throw new TaskException($"Unable to cast {task.GetType()} to {typeof(T)}");
}
public abstract ETaskResult Update();
}

View File

@ -8,10 +8,10 @@ internal sealed class TaskQueue
{ {
private readonly List<ITask> _completedTasks = []; private readonly List<ITask> _completedTasks = [];
private readonly List<ITask> _tasks = []; private readonly List<ITask> _tasks = [];
public ITask? CurrentTask { get; set; } public ITaskExecutor? CurrentTaskExecutor { get; set; }
public IEnumerable<ITask> RemainingTasks => _tasks; 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) public void Enqueue(ITask task)
{ {
@ -41,7 +41,7 @@ internal sealed class TaskQueue
{ {
_tasks.Clear(); _tasks.Clear();
_completedTasks.Clear(); _completedTasks.Clear();
CurrentTask = null; CurrentTaskExecutor = null;
} }
public void InterruptWith(List<ITask> interruptionTasks) public void InterruptWith(List<ITask> interruptionTasks)
@ -49,8 +49,8 @@ internal sealed class TaskQueue
List<ITask?> newTasks = List<ITask?> newTasks =
[ [
..interruptionTasks, ..interruptionTasks,
.._completedTasks.Where(x => !ReferenceEquals(x, CurrentTask)).ToList(), .._completedTasks.Where(x => !ReferenceEquals(x, CurrentTaskExecutor?.CurrentTask)).ToList(),
CurrentTask, CurrentTaskExecutor?.CurrentTask,
.._tasks .._tasks
]; ];
Reset(); Reset();

View File

@ -56,6 +56,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
{ {
ArgumentNullException.ThrowIfNull(pluginInterface); ArgumentNullException.ThrowIfNull(pluginInterface);
ArgumentNullException.ThrowIfNull(chatGui); ArgumentNullException.ThrowIfNull(chatGui);
try try
{ {
ServiceCollection serviceCollection = new(); ServiceCollection serviceCollection = new();
@ -128,44 +129,81 @@ public sealed class QuestionablePlugin : IDalamudPlugin
private static void AddTaskFactories(ServiceCollection serviceCollection) private static void AddTaskFactories(ServiceCollection serviceCollection)
{ {
// individual tasks // individual tasks
serviceCollection.AddTransient<MoveToLandingLocation>(); serviceCollection.AddTaskExecutor<MoveToLandingLocation.Task, MoveToLandingLocation.Executor>();
serviceCollection.AddTransient<DoGather>(); serviceCollection.AddTaskExecutor<DoGather.Task, DoGather.Executor>();
serviceCollection.AddTransient<DoGatherCollectable>(); serviceCollection.AddTaskExecutor<DoGatherCollectable.Task, DoGatherCollectable.Executor>();
serviceCollection.AddTransient<SwitchClassJob>(); serviceCollection.AddTaskExecutor<SwitchClassJob.Task, SwitchClassJob.Executor>();
serviceCollection.AddSingleton<Mount.Factory>(); serviceCollection.AddTaskExecutor<Mount.MountTask, Mount.MountExecutor>();
serviceCollection.AddTaskExecutor<Mount.UnmountTask, Mount.UnmountExecutor>();
// task factories // task factories
serviceCollection.AddTaskFactory<StepDisabled.Factory>(); serviceCollection
.AddTaskFactoryAndExecutor<StepDisabled.SkipRemainingTasks, StepDisabled.Factory, StepDisabled.Executor>();
serviceCollection.AddTaskFactory<EquipRecommended.BeforeDutyOrInstance>(); serviceCollection.AddTaskFactory<EquipRecommended.BeforeDutyOrInstance>();
serviceCollection.AddTaskFactory<Gather.Factory>(); serviceCollection.AddTaskFactoryAndExecutor<Gather.GatheringTask, Gather.Factory, Gather.StartGathering>();
serviceCollection.AddTaskFactory<AetheryteShortcut.Factory>(); serviceCollection.AddTaskExecutor<Gather.SkipMarker, Gather.DoSkip>();
serviceCollection.AddTaskFactory<SkipCondition.Factory>(); serviceCollection
serviceCollection.AddTaskFactory<AethernetShortcut.Factory>(); .AddTaskFactoryAndExecutor<AetheryteShortcut.Task, AetheryteShortcut.Factory,
serviceCollection.AddTaskFactory<WaitAtStart.Factory>(); AetheryteShortcut.UseAetheryteShortcut>();
serviceCollection.AddTaskFactory<MoveTo.Factory>(); 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.AddTaskFactory<NextQuest.Factory>(); serviceCollection.AddTaskFactoryAndExecutor<NextQuest.SetQuestTask, NextQuest.Factory, NextQuest.Executor>();
serviceCollection.AddTaskFactory<AetherCurrent.Factory>(); serviceCollection
serviceCollection.AddTaskFactory<AethernetShard.Factory>(); .AddTaskFactoryAndExecutor<AetherCurrent.Attune, AetherCurrent.Factory, AetherCurrent.DoAttune>();
serviceCollection.AddTaskFactory<Aetheryte.Factory>(); serviceCollection
serviceCollection.AddTaskFactory<Combat.Factory>(); .AddTaskFactoryAndExecutor<AethernetShard.Attune, AethernetShard.Factory, AethernetShard.DoAttune>();
serviceCollection.AddTaskFactory<Duty.Factory>(); 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<Emote.Factory>();
serviceCollection.AddTaskFactory<Action.Factory>(); serviceCollection.AddTaskExecutor<Emote.UseOnObject, Emote.UseOnObjectExecutor>();
serviceCollection.AddTaskFactory<Interact.Factory>(); 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<Jump.Factory>();
serviceCollection.AddTaskFactory<Dive.Factory>(); serviceCollection.AddTaskExecutor<Jump.SingleJumpTask, Jump.DoSingleJump>();
serviceCollection.AddTaskFactory<Say.Factory>(); 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<UseItem.Factory>();
serviceCollection.AddTaskFactory<EquipItem.Factory>(); serviceCollection.AddTaskExecutor<UseItem.UseOnGround, UseItem.UseOnGroundExecutor>();
serviceCollection.AddTaskFactory<EquipRecommended.Factory>(); serviceCollection.AddTaskExecutor<UseItem.UseOnPosition, UseItem.UseOnPositionExecutor>();
serviceCollection.AddTaskFactory<Craft.Factory>(); serviceCollection.AddTaskExecutor<UseItem.UseOnObject, UseItem.UseOnObjectExecutor>();
serviceCollection.AddTaskFactory<TurnInDelivery.Factory>(); serviceCollection.AddTaskExecutor<UseItem.UseOnSelf, UseItem.UseOnSelfExecutor>();
serviceCollection.AddTaskFactory<InitiateLeve.Factory>(); 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.AddTaskFactory<WaitAtEnd.Factory>();
serviceCollection.AddTransient<WaitAtEnd.WaitQuestAccepted>(); serviceCollection.AddTaskExecutor<WaitAtEnd.WaitDelay, WaitAtEnd.WaitDelayExecutor>();
serviceCollection.AddTransient<WaitAtEnd.WaitQuestCompleted>(); 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>(); serviceCollection.AddSingleton<TaskCreator>();
} }

View File

@ -1,4 +1,5 @@
using JetBrains.Annotations; using Dalamud.Plugin.Services;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Questionable.Controller.Steps; using Questionable.Controller.Steps;
@ -7,11 +8,37 @@ namespace Questionable;
internal static class ServiceCollectionExtensions internal static class ServiceCollectionExtensions
{ {
public static void AddTaskFactory< public static void AddTaskFactory<
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] TFactory>( [MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
TFactory>(
this IServiceCollection serviceCollection) this IServiceCollection serviceCollection)
where TFactory : class, ITaskFactory where TFactory : class, ITaskFactory
{ {
serviceCollection.AddSingleton<ITaskFactory, TFactory>(); serviceCollection.AddSingleton<ITaskFactory, TFactory>();
serviceCollection.AddSingleton<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>();
}
} }