Compare commits

..

No commits in common. "master" and "v3.4" have entirely different histories.
master ... v3.4

59 changed files with 1523 additions and 1833 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

@ -35,7 +35,7 @@ internal sealed class EditorWindow : Window
public EditorWindow(RendererPlugin plugin, EditorCommands editorCommands, IDataManager dataManager, public EditorWindow(RendererPlugin plugin, EditorCommands editorCommands, IDataManager dataManager,
ITargetManager targetManager, IClientState clientState, IObjectTable objectTable) ITargetManager targetManager, IClientState clientState, IObjectTable objectTable)
: base("Gathering Path Editor###QuestionableGatheringPathEditor", : base("Gathering Path Editor###QuestionableGatheringPathEditor",
ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus | ImGuiWindowFlags.AlwaysAutoResize) ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus)
{ {
_plugin = plugin; _plugin = plugin;
_editorCommands = editorCommands; _editorCommands = editorCommands;
@ -46,7 +46,7 @@ internal sealed class EditorWindow : Window
SizeConstraints = new WindowSizeConstraints SizeConstraints = new WindowSizeConstraints
{ {
MinimumSize = new Vector2(300, 100), MinimumSize = new Vector2(300, 300),
}; };
RespectCloseHotkey = false; RespectCloseHotkey = false;
@ -66,7 +66,7 @@ internal sealed class EditorWindow : Window
_target = _targetManager.Target; _target = _targetManager.Target;
var gatheringLocations = _plugin.GetLocationsInTerritory(_clientState.TerritoryType); var gatheringLocations = _plugin.GetLocationsInTerritory(_clientState.TerritoryType);
var location = gatheringLocations.ToList().SelectMany(context => var location = gatheringLocations.SelectMany(context =>
context.Root.Groups.SelectMany(group => context.Root.Groups.SelectMany(group =>
group.Nodes.SelectMany(node => node.Locations group.Nodes.SelectMany(node => node.Locations
.Select(location => .Select(location =>

View File

@ -5,11 +5,6 @@
{ {
"Sequence": 0, "Sequence": 0,
"Steps": [ "Steps": [
{
"TerritoryId": 886,
"InteractionType": "Gather",
"ItemsToGather": []
},
{ {
"TerritoryId": 886, "TerritoryId": 886,
"InteractionType": "None", "InteractionType": "None",

View File

@ -5,11 +5,6 @@
{ {
"Sequence": 0, "Sequence": 0,
"Steps": [ "Steps": [
{
"TerritoryId": 886,
"InteractionType": "Gather",
"ItemsToGather": []
},
{ {
"TerritoryId": 886, "TerritoryId": 886,
"InteractionType": "None", "InteractionType": "None",

View File

@ -252,7 +252,7 @@
"Y": -151.26128, "Y": -151.26128,
"Z": -657.2519 "Z": -657.2519
}, },
"StopDistance": 6, "StopDistance": 7,
"TerritoryId": 959, "TerritoryId": 959,
"InteractionType": "Say", "InteractionType": "Say",
"DisableNavmesh": true, "DisableNavmesh": true,

View File

@ -27,22 +27,6 @@
{ {
"Sequence": 1, "Sequence": 1,
"Steps": [ "Steps": [
{
"Position": {
"X": 588.5607,
"Y": 437.99976,
"Z": 299.7425
},
"TerritoryId": 960,
"InteractionType": "Jump",
"JumpDestination": {
"Position": {
"X": 602.4677,
"Y": 438.6276,
"Z": 297.1612
}
}
},
{ {
"Position": { "Position": {
"X": 656.94653, "X": 656.94653,

View File

@ -41,7 +41,7 @@
"InteractionType": "Interact", "InteractionType": "Interact",
"AethernetShortcut": [ "AethernetShortcut": [
"[Radz-at-Han] Mehryde's Meyhane", "[Radz-at-Han] Mehryde's Meyhane",
"[Radz-at-Han] The High Crucible of Al-Kimiya" "[Radz-at-Han] Aetheryte Plaza"
] ]
} }
] ]

View File

@ -114,6 +114,8 @@ internal sealed class CombatController : IDisposable
else else
{ {
var nextTarget = FindNextTarget(); var nextTarget = FindNextTarget();
_logger.LogInformation("NT → {NT}", nextTarget);
if (nextTarget is { IsDead: false }) if (nextTarget is { IsDead: false })
SetTarget(nextTarget); SetTarget(nextTarget);
} }

View File

@ -14,8 +14,8 @@ namespace Questionable.Controller;
internal sealed class CommandHandler : IDisposable internal sealed class CommandHandler : IDisposable
{ {
public const string MessageTag = "Questionable"; private const string MessageTag = "Questionable";
public const ushort TagColor = 576; private const ushort TagColor = 576;
private readonly ICommandManager _commandManager; private readonly ICommandManager _commandManager;
private readonly IChatGui _chatGui; private readonly IChatGui _chatGui;

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.HasCurrentTaskExecutorMatching<Interact.DoInteract>(out var interact) && if (_questController.HasCurrentTaskMatching<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.Task? aethernetShortcut) && _questController.HasCurrentTaskMatching(out AethernetShortcut.UseAethernetShortcut? 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,13 +13,16 @@ 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;
@ -29,18 +32,26 @@ 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 ILogger<GatheringController> _logger; private readonly ILoggerFactory _loggerFactory;
private readonly IGameGui _gameGui;
private readonly IClientState _clientState;
private readonly Regex _revisitRegex; private readonly Regex _revisitRegex;
private CurrentRequest? _currentRequest; private CurrentRequest? _currentRequest;
public GatheringController( public GatheringController(
MovementController movementController, MovementController movementController,
MoveTo.Factory moveFactory,
Mount.Factory mountFactory,
Interact.Factory interactFactory,
GatheringPointRegistry gatheringPointRegistry, GatheringPointRegistry gatheringPointRegistry,
GameFunctions gameFunctions, GameFunctions gameFunctions,
NavmeshIpc navmeshIpc, NavmeshIpc navmeshIpc,
@ -48,18 +59,25 @@ 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, condition, serviceProvider, logger) : base(chatGui, 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;
_logger = logger; _loggerFactory = loggerFactory;
_gameGui = gameGui;
_clientState = clientState;
_revisitRegex = dataManager.GetRegex<LogMessage>(5574, x => x.Text, pluginLog) _revisitRegex = dataManager.GetRegex<LogMessage>(5574, x => x.Text, pluginLog)
?? throw new InvalidDataException("No regex found for revisit message"); ?? throw new InvalidDataException("No regex found for revisit message");
@ -149,7 +167,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(new Mount.MountTask(territoryId, Mount.EMountIf.Always)); _taskQueue.Enqueue(_mountFactory.Mount(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);
@ -166,13 +184,14 @@ 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(new MoveTo.MoveTask(territoryId, pointOnFloor ?? averagePosition, _taskQueue.Enqueue(_moveFactory.Move(new MoveTo.MoveParams(territoryId, pointOnFloor ?? averagePosition,
null, 50f, Fly: fly, IgnoreDistanceToObject: true, InteractionType: EInteractionType.WalkTo)); null, 50f, Fly: fly, IgnoreDistanceToObject: true)));
} }
_taskQueue.Enqueue(new MoveToLandingLocation.Task(territoryId, fly, currentNode)); _taskQueue.Enqueue(new MoveToLandingLocation(territoryId, fly, currentNode, _moveFactory, _gameFunctions,
_taskQueue.Enqueue(new Mount.UnmountTask()); _objectTable, _loggerFactory.CreateLogger<MoveToLandingLocation>()));
_taskQueue.Enqueue(new Interact.Task(currentNode.DataId, null, EInteractionType.Gather, true)); _taskQueue.Enqueue(_mountFactory.Unmount());
_taskQueue.Enqueue(_interactFactory.Interact(currentNode.DataId, null, EInteractionType.Gather, true));
QueueGatherNode(currentNode); QueueGatherNode(currentNode);
} }
@ -181,10 +200,12 @@ 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.Task(_currentRequest!.Data, currentNode, revisitRequired)); _taskQueue.Enqueue(new DoGather(_currentRequest!.Data, currentNode, revisitRequired, this, _gameFunctions,
_gameGui, _clientState, _condition, _loggerFactory.CreateLogger<DoGather>()));
if (_currentRequest.Data.Collectability > 0) if (_currentRequest.Data.Collectability > 0)
{ {
_taskQueue.Enqueue(new DoGatherCollectable.Task(_currentRequest.Data, currentNode, revisitRequired)); _taskQueue.Enqueue(new DoGatherCollectable(_currentRequest.Data, currentNode, revisitRequired, this,
_gameFunctions, _clientState, _gameGui, _loggerFactory.CreateLogger<DoGatherCollectable>()));
} }
} }
} }
@ -245,7 +266,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
public override IList<string> GetRemainingTaskNames() public override IList<string> GetRemainingTaskNames()
{ {
if (_taskQueue.CurrentTaskExecutor?.CurrentTask is { } currentTask) if (_taskQueue.CurrentTask is {} currentTask)
return [currentTask.ToString() ?? "?", .. base.GetRemainingTaskNames()]; return [currentTask.ToString() ?? "?", .. base.GetRemainingTaskNames()];
else else
return base.GetRemainingTaskNames(); return base.GetRemainingTaskNames();
@ -255,7 +276,7 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
{ {
if (_revisitRegex.IsMatch(message.TextValue)) if (_revisitRegex.IsMatch(message.TextValue))
{ {
if (_taskQueue.CurrentTaskExecutor?.CurrentTask is IRevisitAware currentTaskRevisitAware) if (_taskQueue.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

@ -1,50 +1,37 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
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.Interactions;
using Questionable.Controller.Steps.Shared; using Questionable.Controller.Steps.Shared;
using Questionable.Model.Questing;
namespace Questionable.Controller; namespace Questionable.Controller;
internal abstract class MiniTaskController<T> internal abstract class MiniTaskController<T>
{ {
protected readonly IChatGui _chatGui;
protected readonly ILogger<T> _logger;
protected readonly TaskQueue _taskQueue = new(); protected readonly TaskQueue _taskQueue = new();
private readonly IChatGui _chatGui; protected MiniTaskController(IChatGui chatGui, ILogger<T> logger)
private readonly ICondition _condition;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<T> _logger;
protected MiniTaskController(IChatGui chatGui, ICondition condition, IServiceProvider serviceProvider,
ILogger<T> logger)
{ {
_chatGui = chatGui; _chatGui = chatGui;
_logger = logger; _logger = logger;
_serviceProvider = serviceProvider;
_condition = condition;
} }
protected virtual void UpdateCurrentTask() protected virtual void UpdateCurrentTask()
{ {
if (_taskQueue.CurrentTaskExecutor == null) if (_taskQueue.CurrentTask == 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());
ITaskExecutor taskExecutor = if (upcomingTask.Start())
_serviceProvider.GetRequiredKeyedService<ITaskExecutor>(upcomingTask.GetType());
if (taskExecutor.Start(upcomingTask))
{ {
_taskQueue.CurrentTaskExecutor = taskExecutor; _taskQueue.CurrentTask = upcomingTask;
return; return;
} }
else else
@ -69,20 +56,13 @@ internal abstract class MiniTaskController<T>
ETaskResult result; ETaskResult result;
try try
{ {
if (_taskQueue.CurrentTaskExecutor.WasInterrupted()) result = _taskQueue.CurrentTask.Update();
{
InterruptQueueWithCombat();
return;
}
result = _taskQueue.CurrentTaskExecutor.Update();
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "Failed to update task {TaskName}", _logger.LogError(e, "Failed to update task {TaskName}", _taskQueue.CurrentTask.ToString());
_taskQueue.CurrentTaskExecutor.CurrentTask.ToString());
_chatGui.PrintError( _chatGui.PrintError(
$"[Questionable] Failed to update task '{_taskQueue.CurrentTaskExecutor.CurrentTask}', please check /xllog for details."); $"[Questionable] Failed to update task '{_taskQueue.CurrentTask}', please check /xllog for details.");
Stop("Task failed to update"); Stop("Task failed to update");
return; return;
} }
@ -94,17 +74,14 @@ 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.CurrentTaskExecutor.CurrentTask, result); _taskQueue.CurrentTask, result);
_taskQueue.CurrentTaskExecutor = null; _taskQueue.CurrentTask = 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)
{ {
ITaskExecutor taskExecutor = _taskQueue.CurrentTask = nextTask;
_serviceProvider.GetRequiredKeyedService<ITaskExecutor>(nextTask.GetType());
taskExecutor.Start(nextTask);
_taskQueue.CurrentTaskExecutor = taskExecutor;
return; return;
} }
} }
@ -113,27 +90,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.CurrentTaskExecutor.CurrentTask, result, _taskQueue.RemainingTasks.Count()); _taskQueue.CurrentTask, result, _taskQueue.RemainingTasks.Count());
OnTaskComplete(_taskQueue.CurrentTaskExecutor.CurrentTask); OnTaskComplete(_taskQueue.CurrentTask);
_taskQueue.CurrentTaskExecutor = null; _taskQueue.CurrentTask = null;
// handled in next update // handled in next update
return; return;
case ETaskResult.NextStep: case ETaskResult.NextStep:
_logger.LogInformation("{Task} → {Result}", _taskQueue.CurrentTaskExecutor.CurrentTask, result); _logger.LogInformation("{Task} → {Result}", _taskQueue.CurrentTask, result);
var lastTask = (ILastTask)_taskQueue.CurrentTaskExecutor.CurrentTask; var lastTask = (ILastTask)_taskQueue.CurrentTask;
_taskQueue.CurrentTaskExecutor = null; _taskQueue.CurrentTask = null;
OnNextStep(lastTask); OnNextStep(lastTask);
return; return;
case ETaskResult.End: case ETaskResult.End:
_logger.LogInformation("{Task} → {Result}", _taskQueue.CurrentTaskExecutor.CurrentTask, result); _logger.LogInformation("{Task} → {Result}", _taskQueue.CurrentTask, result);
_taskQueue.CurrentTaskExecutor = null; _taskQueue.CurrentTask = null;
Stop("Task end"); Stop("Task end");
return; return;
} }
@ -145,27 +122,11 @@ internal abstract class MiniTaskController<T>
protected virtual void OnNextStep(ILastTask task) protected virtual void OnNextStep(ILastTask task)
{ {
} }
public abstract void Stop(string label); public abstract void Stop(string label);
public virtual IList<string> GetRemainingTaskNames() => public virtual IList<string> GetRemainingTaskNames() =>
_taskQueue.RemainingTasks.Select(x => x.ToString() ?? "?").ToList(); _taskQueue.RemainingTasks.Select(x => x.ToString() ?? "?").ToList();
public void InterruptQueueWithCombat()
{
_logger.LogWarning("Interrupted, attempting to resolve (if in combat)");
if (_condition[ConditionFlag.InCombat])
{
List<ITask> tasks = [];
if (_condition[ConditionFlag.Mounted])
tasks.Add(new Mount.UnmountTask());
tasks.Add(Combat.Factory.CreateTask(null, false, EEnemySpawnType.QuestInterruption, [], [], []));
tasks.Add(new WaitAtEnd.WaitDelay());
_taskQueue.InterruptWith(tasks);
}
else
_taskQueue.InterruptWith([new WaitAtEnd.WaitDelay()]);
}
} }

View File

@ -35,13 +35,13 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
private readonly GatheringController _gatheringController; private readonly GatheringController _gatheringController;
private readonly QuestRegistry _questRegistry; private readonly QuestRegistry _questRegistry;
private readonly IKeyState _keyState; private readonly IKeyState _keyState;
private readonly IChatGui _chatGui;
private readonly ICondition _condition; private readonly ICondition _condition;
private readonly IToastGui _toastGui; private readonly IToastGui _toastGui;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly YesAlreadyIpc _yesAlreadyIpc; private readonly YesAlreadyIpc _yesAlreadyIpc;
private readonly TaskCreator _taskCreator; private readonly TaskCreator _taskCreator;
private readonly ILogger<QuestController> _logger; private readonly Mount.Factory _mountFactory;
private readonly Combat.Factory _combatFactory;
private readonly string _actionCanceledText; private readonly string _actionCanceledText;
@ -82,9 +82,10 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
Configuration configuration, Configuration configuration,
YesAlreadyIpc yesAlreadyIpc, YesAlreadyIpc yesAlreadyIpc,
TaskCreator taskCreator, TaskCreator taskCreator,
IServiceProvider serviceProvider, Mount.Factory mountFactory,
Combat.Factory combatFactory,
IDataManager dataManager) IDataManager dataManager)
: base(chatGui, condition, serviceProvider, logger) : base(chatGui, logger)
{ {
_clientState = clientState; _clientState = clientState;
_gameFunctions = gameFunctions; _gameFunctions = gameFunctions;
@ -94,13 +95,13 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
_gatheringController = gatheringController; _gatheringController = gatheringController;
_questRegistry = questRegistry; _questRegistry = questRegistry;
_keyState = keyState; _keyState = keyState;
_chatGui = chatGui;
_condition = condition; _condition = condition;
_toastGui = toastGui; _toastGui = toastGui;
_configuration = configuration; _configuration = configuration;
_yesAlreadyIpc = yesAlreadyIpc; _yesAlreadyIpc = yesAlreadyIpc;
_taskCreator = taskCreator; _taskCreator = taskCreator;
_logger = logger; _mountFactory = mountFactory;
_combatFactory = combatFactory;
_condition.ConditionChange += OnConditionChange; _condition.ConditionChange += OnConditionChange;
_toastGui.Toast += OnNormalToast; _toastGui.Toast += OnNormalToast;
@ -218,7 +219,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
return; return;
if (AutomationType == EAutomationType.Automatic && if (AutomationType == EAutomationType.Automatic &&
(_taskQueue.AllTasksComplete || _taskQueue.CurrentTaskExecutor?.CurrentTask is WaitAtEnd.WaitQuestAccepted) (_taskQueue.AllTasksComplete || _taskQueue.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))
{ {
@ -637,30 +638,15 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
public string ToStatString() public string ToStatString()
{ {
return _taskQueue.CurrentTaskExecutor?.CurrentTask is { } currentTask return _taskQueue.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.CurrentTaskExecutor?.CurrentTask is T t) if (_taskQueue.CurrentTask is T t)
{ {
task = t; task = t;
return true; return true;
@ -673,7 +659,6 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
} }
public bool IsRunning => !_taskQueue.AllTasksComplete; public bool IsRunning => !_taskQueue.AllTasksComplete;
public TaskQueue TaskQueue => _taskQueue;
public sealed class QuestProgress public sealed class QuestProgress
{ {
@ -713,11 +698,11 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
{ {
lock (_progressLock) lock (_progressLock)
{ {
if (_taskQueue.CurrentTaskExecutor?.CurrentTask is ISkippableTask) if (_taskQueue.CurrentTask is ISkippableTask)
_taskQueue.CurrentTaskExecutor = null; _taskQueue.CurrentTask = null;
else if (_taskQueue.CurrentTaskExecutor != null) else if (_taskQueue.CurrentTask != null)
{ {
_taskQueue.CurrentTaskExecutor = null; _taskQueue.CurrentTask = null;
while (_taskQueue.TryPeek(out ITask? task)) while (_taskQueue.TryPeek(out ITask? task))
{ {
_taskQueue.TryDequeue(out _); _taskQueue.TryDequeue(out _);
@ -741,7 +726,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
public void SkipSimulatedTask() public void SkipSimulatedTask()
{ {
_taskQueue.CurrentTaskExecutor = null; _taskQueue.CurrentTask = null;
} }
public bool IsInterruptible() public bool IsInterruptible()
@ -800,7 +785,7 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
private void OnConditionChange(ConditionFlag flag, bool value) private void OnConditionChange(ConditionFlag flag, bool value)
{ {
if (_taskQueue.CurrentTaskExecutor is IConditionChangeAware conditionChangeAware) if (_taskQueue.CurrentTask is IConditionChangeAware conditionChangeAware)
conditionChangeAware.OnConditionChange(flag, value); conditionChangeAware.OnConditionChange(flag, value);
} }
@ -811,7 +796,8 @@ 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)
{ {
if (_taskQueue.CurrentTaskExecutor is IToastAware toastAware) _logger.LogWarning("XXX {A} → {B} XXX", _actionCanceledText, message.TextValue);
if (_taskQueue.CurrentTask is IToastAware toastAware)
{ {
if (toastAware.OnErrorToast(message)) if (toastAware.OnErrorToast(message))
{ {
@ -827,6 +813,18 @@ internal sealed class QuestController : MiniTaskController<QuestController>, IDi
} }
} }
public void InterruptQueueWithCombat()
{
_logger.LogWarning("Interrupted with action canceled message, attempting to resolve");
List<ITask> tasks = [];
if (_condition[ConditionFlag.Mounted])
tasks.Add(_mountFactory.Unmount());
tasks.Add(_combatFactory.CreateTask(null, false, EEnemySpawnType.QuestInterruption, [], [], []));
tasks.Add(new WaitAtEnd.WaitDelay());
_taskQueue.InterruptWith(tasks);
}
public void Dispose() public void Dispose()
{ {
_toastGui.ErrorToast -= OnErrorToast; _toastGui.ErrorToast -= OnErrorToast;

View File

@ -142,8 +142,7 @@ internal sealed class QuestRegistry
private void LoadQuestFromStream(string fileName, Stream stream, Quest.ESource source) private void LoadQuestFromStream(string fileName, Stream stream, Quest.ESource source)
{ {
if (source == Quest.ESource.UserDirectory) _logger.LogTrace("Loading quest from '{FileName}'", fileName);
_logger.LogTrace("Loading quest from '{FileName}'", fileName);
ElementId? questId = ExtractQuestIdFromName(fileName); ElementId? questId = ExtractQuestIdFromName(fileName);
if (questId == null) if (questId == null)
return; return;
@ -174,8 +173,7 @@ internal sealed class QuestRegistry
return; return;
} }
if (source == Quest.ESource.UserDirectory) _logger.Log(logLevel, "Loading quests from {DirectoryName}", directory);
_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,31 @@
namespace Questionable.Controller.Steps.Common; namespace Questionable.Controller.Steps.Common;
internal abstract class AbstractDelayedTaskExecutor<T> : TaskExecutor<T> internal abstract class AbstractDelayedTask : ITask
where T : class, ITask
{ {
private DateTime _continueAt; private DateTime _continueAt;
protected AbstractDelayedTaskExecutor() protected AbstractDelayedTask(TimeSpan delay)
: this(TimeSpan.FromSeconds(5))
{
}
protected AbstractDelayedTaskExecutor(TimeSpan delay)
{ {
Delay = delay; Delay = delay;
} }
protected TimeSpan Delay { get; set; } protected TimeSpan Delay { get; set; }
protected sealed override bool Start() protected AbstractDelayedTask()
: this(TimeSpan.FromSeconds(5))
{
}
public bool Start()
{ {
bool started = StartInternal();
_continueAt = DateTime.Now.Add(Delay); _continueAt = DateTime.Now.Add(Delay);
return started; return StartInternal();
} }
protected abstract bool StartInternal(); protected abstract bool StartInternal();
public override ETaskResult Update() public virtual ETaskResult Update()
{ {
if (_continueAt >= DateTime.Now) if (_continueAt >= DateTime.Now)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;

View File

@ -11,38 +11,51 @@ namespace Questionable.Controller.Steps.Common;
internal static class Mount internal static class Mount
{ {
internal sealed record MountTask( internal sealed class Factory(
ushort TerritoryId,
EMountIf MountIf,
Vector3? Position = null) : ITask
{
public Vector3? Position { get; } = MountIf == EMountIf.AwayFromPosition
? Position ?? throw new ArgumentNullException(nameof(Position))
: null;
public bool ShouldRedoOnInterrupt() => true;
public override string ToString() => "Mount";
}
internal sealed class MountExecutor(
GameFunctions gameFunctions, GameFunctions gameFunctions,
ICondition condition, ICondition condition,
TerritoryData territoryData, TerritoryData territoryData,
IClientState clientState, IClientState clientState,
ILogger<MountTask> logger) : TaskExecutor<MountTask> ILoggerFactory loggerFactory)
{
public ITask Mount(ushort territoryId, EMountIf mountIf, Vector3? position = null)
{
if (mountIf == EMountIf.AwayFromPosition)
ArgumentNullException.ThrowIfNull(position);
return new MountTask(territoryId, mountIf, position, gameFunctions, condition, territoryData, clientState,
loggerFactory.CreateLogger<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 bool _mountTriggered;
private DateTime _retryAt = DateTime.MinValue; private DateTime _retryAt = DateTime.MinValue;
protected override bool Start() public bool ShouldRedoOnInterrupt() => true;
public bool Start()
{ {
if (condition[ConditionFlag.Mounted]) if (condition[ConditionFlag.Mounted])
return false; return false;
if (!territoryData.CanUseMount(Task.TerritoryId)) if (!territoryData.CanUseMount(territoryId))
{ {
logger.LogInformation("Can't use mount in current territory {Id}", Task.TerritoryId); logger.LogInformation("Can't use mount in current territory {Id}", territoryId);
return false; return false;
} }
@ -52,11 +65,11 @@ internal static class Mount
return false; return false;
} }
if (Task.MountIf == EMountIf.AwayFromPosition) if (mountIf == EMountIf.AwayFromPosition)
{ {
Vector3 playerPosition = clientState.LocalPlayer?.Position ?? Vector3.Zero; Vector3 playerPosition = clientState.LocalPlayer?.Position ?? Vector3.Zero;
float distance = System.Numerics.Vector3.Distance(playerPosition, Task.Position.GetValueOrDefault()); float distance = System.Numerics.Vector3.Distance(playerPosition, position.GetValueOrDefault());
if (Task.TerritoryId == clientState.TerritoryType && distance < 30f && !Conditions.IsDiving) if (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;
@ -64,10 +77,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, Task.TerritoryId); distance, territoryId);
} }
else else
logger.LogInformation("Want to use mount, trying (in territory {Id})...", Task.TerritoryId); logger.LogInformation("Want to use mount, trying (in territory {Id})...", territoryId);
if (!condition[ConditionFlag.InCombat]) if (!condition[ConditionFlag.InCombat])
{ {
@ -78,7 +91,7 @@ internal static class Mount
return false; return false;
} }
public override ETaskResult Update() public ETaskResult Update()
{ {
if (_mountTriggered && !condition[ConditionFlag.Mounted] && DateTime.Now > _retryAt) if (_mountTriggered && !condition[ConditionFlag.Mounted] && DateTime.Now > _retryAt)
{ {
@ -95,9 +108,7 @@ internal static class Mount
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
ProgressContext = _mountTriggered = gameFunctions.Mount();
InteractionProgressContext.FromActionUse(() => _mountTriggered = gameFunctions.Mount());
_retryAt = DateTime.Now.AddSeconds(5); _retryAt = DateTime.Now.AddSeconds(5);
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
@ -106,26 +117,23 @@ internal static class Mount
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : ETaskResult.StillRunning;
} }
public override string ToString() => "Mount";
} }
internal sealed record UnmountTask : ITask private sealed class UnmountTask(
{
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)
: TaskExecutor<UnmountTask> : ITask
{ {
private bool _unmountTriggered; private bool _unmountTriggered;
private DateTime _continueAt = DateTime.MinValue; private DateTime _continueAt = DateTime.MinValue;
protected override bool Start() public bool ShouldRedoOnInterrupt() => true;
public bool Start()
{ {
if (!condition[ConditionFlag.Mounted]) if (!condition[ConditionFlag.Mounted])
return false; return false;
@ -143,7 +151,7 @@ internal static class Mount
return true; return true;
} }
public override ETaskResult Update() public ETaskResult Update()
{ {
if (_continueAt >= DateTime.Now) if (_continueAt >= DateTime.Now)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -176,6 +184,8 @@ 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(QuestFunctions questFunctions) : SimpleTaskFactory internal sealed class Factory(QuestRegistry questRegistry, QuestController questController, QuestFunctions questFunctions, ILoggerFactory loggerFactory) : SimpleTaskFactory
{ {
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step) public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -24,42 +24,34 @@ internal static class NextQuest
if (questFunctions.GetPriorityQuests().Contains(step.NextQuestId)) if (questFunctions.GetPriorityQuests().Contains(step.NextQuestId))
return null; return null;
return new SetQuestTask(step.NextQuestId, quest.Id); return new SetQuest(step.NextQuestId, quest.Id, questRegistry, questController, questFunctions, loggerFactory.CreateLogger<SetQuest>());
} }
} }
internal sealed record SetQuestTask(ElementId NextQuestId, ElementId CurrentQuestId) : ITask private sealed class SetQuest(ElementId nextQuestId, ElementId currentQuestId, QuestRegistry questRegistry, QuestController questController, QuestFunctions questFunctions, ILogger<SetQuest> logger) : ITask
{ {
public bool ShouldRedoOnInterrupt() => true; public bool Start()
public override string ToString() => $"SetNextQuest({NextQuestId})";
}
internal sealed class NextQuestExecutor(
QuestRegistry questRegistry,
QuestController questController,
QuestFunctions questFunctions,
ILogger<NextQuestExecutor> logger) : TaskExecutor<SetQuestTask>
{
protected override bool Start()
{ {
if (questFunctions.IsQuestLocked(Task.NextQuestId, Task.CurrentQuestId)) if (questFunctions.IsQuestLocked(nextQuestId, currentQuestId))
{ {
logger.LogInformation("Can't set next quest to {QuestId}, quest is locked", Task.NextQuestId); logger.LogInformation("Can't set next quest to {QuestId}, quest is locked", nextQuestId);
} }
else if (questRegistry.TryGetQuest(Task.NextQuestId, out Quest? quest)) else if (questRegistry.TryGetQuest(nextQuestId, out Quest? quest))
{ {
logger.LogInformation("Setting next quest to {QuestId}: '{QuestName}'", Task.NextQuestId, quest.Info.Name); logger.LogInformation("Setting next quest to {QuestId}: '{QuestName}'", nextQuestId, quest.Info.Name);
questController.SetNextQuest(quest); questController.SetNextQuest(quest);
} }
else else
{ {
logger.LogInformation("Next quest with id {QuestId} not found", Task.NextQuestId); logger.LogInformation("Next quest with id {QuestId} not found", nextQuestId);
questController.SetNextQuest(null); questController.SetNextQuest(null);
} }
return true; return true;
} }
public override ETaskResult Update() => ETaskResult.TaskComplete; public ETaskResult Update() => ETaskResult.TaskComplete;
public override string ToString() => $"SetNextQuest({nextQuestId})";
} }
} }

View File

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

View File

@ -15,231 +15,227 @@ using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Gathering; namespace Questionable.Controller.Steps.Gathering;
internal static class DoGather internal sealed class DoGather(
GatheringController.GatheringRequest currentRequest,
GatheringNode currentNode,
bool revisitRequired,
GatheringController gatheringController,
GameFunctions gameFunctions,
IGameGui gameGui,
IClientState clientState,
ICondition condition,
ILogger<DoGather> logger) : ITask, IRevisitAware
{ {
internal sealed record Task( private const uint StatusGatheringRateUp = 218;
GatheringController.GatheringRequest Request,
GatheringNode Node, private bool _revisitTriggered;
bool RevisitRequired) : ITask, IRevisitAware private bool _wasGathering;
private SlotInfo? _slotToGather;
private Queue<EAction>? _actionQueue;
public bool Start() => true;
public unsafe ETaskResult Update()
{ {
public bool RevisitTriggered { get; private set; } if (revisitRequired && !_revisitTriggered)
public void OnRevisit() => RevisitTriggered = true;
public override string ToString() => $"DoGather{(RevisitRequired ? " if revist" : "")}";
}
internal sealed class GatherExecutor(
GatheringController gatheringController,
GameFunctions gameFunctions,
IGameGui gameGui,
IClientState clientState,
ICondition condition,
ILogger<GatherExecutor> 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()
{ {
if (Task is { RevisitRequired: true, RevisitTriggered: false }) logger.LogInformation("No revisit");
{ return ETaskResult.TaskComplete;
logger.LogInformation("No revisit"); }
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; return ETaskResult.TaskComplete;
}
if (gatheringController.HasNodeDisappeared(Task.Node)) _wasGathering = true;
if (gameGui.TryGetAddonByName("Gathering", out AddonGathering* addonGathering))
{ {
logger.LogInformation("Node disappeared"); if (gatheringController.HasRequestedItems())
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()) addonGathering->FireCallbackInt(-1);
}
else
{
var slots = ReadSlots(addonGathering);
if (currentRequest.Collectability > 0)
{ {
addonGathering->FireCallbackInt(-1); var slot = slots.Single(x => x.ItemId == currentRequest.ItemId);
addonGathering->FireCallbackInt(slot.Index);
} }
else else
{ {
var slots = ReadSlots(addonGathering); NodeCondition nodeCondition = new NodeCondition(
if (Task.Request.Collectability > 0) addonGathering->AtkValues[110].UInt,
addonGathering->AtkValues[111].UInt);
if (_actionQueue != null && _actionQueue.TryPeek(out EAction nextAction))
{ {
var slot = slots.Single(x => x.ItemId == Task.Request.ItemId); if (gameFunctions.UseAction(nextAction))
{
logger.LogInformation("Used action {Action} on node", nextAction);
_actionQueue.Dequeue();
}
return ETaskResult.StillRunning;
}
_actionQueue = GetNextActions(nodeCondition, slots);
if (_actionQueue.Count == 0)
{
var slot = _slotToGather ?? slots.Single(x => x.ItemId == currentRequest.ItemId);
addonGathering->FireCallbackInt(slot.Index); 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))
{
if (gameFunctions.UseAction(nextAction))
{
logger.LogInformation("Used action {Action} on node", nextAction);
_actionQueue.Dequeue();
}
return ETaskResult.StillRunning;
}
_actionQueue = GetNextActions(nodeCondition, slots);
if (_actionQueue.Count == 0)
{
var slot = _slotToGather ?? slots.Single(x => x.ItemId == Task.Request.ItemId);
addonGathering->FireCallbackInt(slot.Index);
}
}
} }
} }
} }
return _wasGathering && !condition[ConditionFlag.Gathering]
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
} }
private unsafe List<SlotInfo> ReadSlots(AddonGathering* addonGathering) return _wasGathering && !condition[ConditionFlag.Gathering]
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
}
private unsafe List<SlotInfo> ReadSlots(AddonGathering* addonGathering)
{
var atkValues = addonGathering->AtkValues;
List<SlotInfo> slots = new List<SlotInfo>();
for (int i = 0; i < 8; ++i)
{ {
var atkValues = addonGathering->AtkValues; // +8 = new item?
List<SlotInfo> slots = new List<SlotInfo>(); uint itemId = atkValues[i * 11 + 7].UInt;
for (int i = 0; i < 8; ++i) if (itemId == 0)
{ continue;
// +8 = new item?
uint itemId = atkValues[i * 11 + 7].UInt;
if (itemId == 0)
continue;
AtkComponentCheckBox* atkCheckbox = addonGathering->GatheredItemComponentCheckbox[i].Value; AtkComponentCheckBox* atkCheckbox = addonGathering->GatheredItemComponentCheckbox[i].Value;
AtkTextNode* atkGatheringChance = atkCheckbox->UldManager.SearchNodeById(10)->GetAsAtkTextNode(); AtkTextNode* atkGatheringChance = atkCheckbox->UldManager.SearchNodeById(10)->GetAsAtkTextNode();
if (!int.TryParse(atkGatheringChance->NodeText.ToString(), out int gatheringChance)) if (!int.TryParse(atkGatheringChance->NodeText.ToString(), out int gatheringChance))
gatheringChance = 0; gatheringChance = 0;
AtkTextNode* atkBoonChance = atkCheckbox->UldManager.SearchNodeById(16)->GetAsAtkTextNode(); AtkTextNode* atkBoonChance = atkCheckbox->UldManager.SearchNodeById(16)->GetAsAtkTextNode();
if (!int.TryParse(atkBoonChance->NodeText.ToString(), out int boonChance)) if (!int.TryParse(atkBoonChance->NodeText.ToString(), out int boonChance))
boonChance = 0; boonChance = 0;
AtkComponentNode* atkImage = atkCheckbox->UldManager.SearchNodeById(31)->GetAsAtkComponentNode(); AtkComponentNode* atkImage = atkCheckbox->UldManager.SearchNodeById(31)->GetAsAtkComponentNode();
AtkTextNode* atkQuantity = atkImage->Component->UldManager.SearchNodeById(7)->GetAsAtkTextNode(); AtkTextNode* atkQuantity = atkImage->Component->UldManager.SearchNodeById(7)->GetAsAtkTextNode();
if (!atkQuantity->IsVisible() || !int.TryParse(atkQuantity->NodeText.ToString(), out int quantity)) if (!atkQuantity->IsVisible() || !int.TryParse(atkQuantity->NodeText.ToString(), out int quantity))
quantity = 1; quantity = 1;
var slot = new SlotInfo(i, itemId, gatheringChance, boonChance, quantity); var slot = new SlotInfo(i, itemId, gatheringChance, boonChance, quantity);
slots.Add(slot); slots.Add(slot);
}
return slots;
} }
[SuppressMessage("ReSharper", "UnusedParameter.Local")] return slots;
private Queue<EAction> GetNextActions(NodeCondition nodeCondition, List<SlotInfo> slots) }
{
//uint gp = clientState.LocalPlayer!.CurrentGp;
Queue<EAction> actions = new();
if (!gameFunctions.HasStatus(StatusGatheringRateUp)) [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)
{ {
// do we have an alternative item? only happens for 'evaluation' leve quests var alternativeSlot = slots.Single(x => x.ItemId == currentRequest.AlternativeItemId);
if (Task.Request.AlternativeItemId != 0)
if (alternativeSlot.GatheringChance == 100)
{ {
var alternativeSlot = slots.Single(x => x.ItemId == Task.Request.AlternativeItemId); _slotToGather = alternativeSlot;
return actions;
if (alternativeSlot.GatheringChance == 100)
{
_slotToGather = alternativeSlot;
return actions;
}
if (alternativeSlot.GatheringChance > 0)
{
if (alternativeSlot.GatheringChance >= 95 &&
CanUseAction(EAction.SharpVision1, EAction.FieldMastery1))
{
_slotToGather = alternativeSlot;
actions.Enqueue(PickAction(EAction.SharpVision1, EAction.FieldMastery1));
return actions;
}
if (alternativeSlot.GatheringChance >= 85 &&
CanUseAction(EAction.SharpVision2, EAction.FieldMastery2))
{
_slotToGather = alternativeSlot;
actions.Enqueue(PickAction(EAction.SharpVision2, EAction.FieldMastery2));
return actions;
}
if (alternativeSlot.GatheringChance >= 50 &&
CanUseAction(EAction.SharpVision3, EAction.FieldMastery3))
{
_slotToGather = alternativeSlot;
actions.Enqueue(PickAction(EAction.SharpVision3, EAction.FieldMastery3));
return actions;
}
}
} }
var slot = slots.Single(x => x.ItemId == Task.Request.ItemId); if (alternativeSlot.GatheringChance > 0)
if (slot.GatheringChance > 0 && slot.GatheringChance < 100)
{ {
if (slot.GatheringChance >= 95 && if (alternativeSlot.GatheringChance >= 95 &&
CanUseAction(EAction.SharpVision1, EAction.FieldMastery1)) CanUseAction(EAction.SharpVision1, EAction.FieldMastery1))
{ {
_slotToGather = alternativeSlot;
actions.Enqueue(PickAction(EAction.SharpVision1, EAction.FieldMastery1)); actions.Enqueue(PickAction(EAction.SharpVision1, EAction.FieldMastery1));
return actions; return actions;
} }
if (slot.GatheringChance >= 85 && if (alternativeSlot.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 (slot.GatheringChance >= 50 && if (alternativeSlot.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;
} }
} }
} }
return actions; var slot = slots.Single(x => x.ItemId == currentRequest.ItemId);
if (slot.GatheringChance > 0 && slot.GatheringChance < 100)
{
if (slot.GatheringChance >= 95 &&
CanUseAction(EAction.SharpVision1, EAction.FieldMastery1))
{
actions.Enqueue(PickAction(EAction.SharpVision1, EAction.FieldMastery1));
return actions;
}
if (slot.GatheringChance >= 85 &&
CanUseAction(EAction.SharpVision2, EAction.FieldMastery2))
{
actions.Enqueue(PickAction(EAction.SharpVision2, EAction.FieldMastery2));
return actions;
}
if (slot.GatheringChance >= 50 &&
CanUseAction(EAction.SharpVision3, EAction.FieldMastery3))
{
actions.Enqueue(PickAction(EAction.SharpVision3, EAction.FieldMastery3));
return actions;
}
}
} }
private EAction PickAction(EAction minerAction, EAction botanistAction) return actions;
{
if ((EClassJob?)clientState.LocalPlayer?.ClassJob.Id == EClassJob.Miner)
return minerAction;
else
return botanistAction;
}
private unsafe bool CanUseAction(EAction minerAction, EAction botanistAction)
{
EAction action = PickAction(minerAction, botanistAction);
return ActionManager.Instance()->GetActionStatus(ActionType.Action, (uint)action) == 0;
}
} }
private EAction PickAction(EAction minerAction, EAction botanistAction)
{
if ((EClassJob?)clientState.LocalPlayer?.ClassJob.Id == EClassJob.Miner)
return minerAction;
else
return botanistAction;
}
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,4 +1,5 @@
using System.Collections.Generic; using System;
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;
@ -12,194 +13,189 @@ using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Gathering; namespace Questionable.Controller.Steps.Gathering;
internal static class DoGatherCollectable internal sealed class DoGatherCollectable(
GatheringController.GatheringRequest currentRequest,
GatheringNode currentNode,
bool revisitRequired,
GatheringController gatheringController,
GameFunctions gameFunctions,
IClientState clientState,
IGameGui gameGui,
ILogger<DoGatherCollectable> logger) : ITask, IRevisitAware
{ {
internal sealed record Task( private bool _revisitTriggered;
GatheringController.GatheringRequest Request, private Queue<EAction>? _actionQueue;
GatheringNode Node,
bool RevisitRequired) : ITask, IRevisitAware private bool? _expectedScrutiny;
public bool Start() => true;
public unsafe ETaskResult Update()
{ {
public bool RevisitTriggered { get; private set; } if (revisitRequired && !_revisitTriggered)
public void OnRevisit() => RevisitTriggered = true;
public override string ToString() =>
$"DoGatherCollectable({SeIconChar.Collectible.ToIconString()}/{Request.Collectability}){(RevisitRequired ? " if revist" : "")}";
}
internal sealed class GatherCollectableExecutor(
GatheringController gatheringController,
GameFunctions gameFunctions,
IClientState clientState,
IGameGui gameGui,
ILogger<GatherCollectableExecutor> logger) : TaskExecutor<Task>
{
private Queue<EAction>? _actionQueue;
private bool? _expectedScrutiny;
protected override bool Start() => true;
public override unsafe ETaskResult Update()
{ {
if (Task.RevisitRequired && !Task.RevisitTriggered) logger.LogInformation("No revisit");
{ 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;
} }
private unsafe NodeCondition? GetNodeCondition() if (gatheringController.HasNodeDisappeared(currentNode))
{
logger.LogInformation("Node disappeared");
return ETaskResult.TaskComplete;
}
if (gatheringController.HasRequestedItems())
{ {
if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* atkUnitBase)) if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* atkUnitBase))
{ {
var atkValues = atkUnitBase->AtkValues; atkUnitBase->FireCallbackInt(1);
return new NodeCondition( return ETaskResult.StillRunning;
CurrentCollectability: atkValues[13].UInt,
MaxCollectability: atkValues[14].UInt,
CurrentIntegrity: atkValues[62].UInt,
MaxIntegrity: atkValues[63].UInt,
ScrutinyActive: atkValues[54].Bool,
CollectabilityFromScour: atkValues[48].UInt,
CollectabilityFromMeticulous: atkValues[51].UInt
);
} }
return null; if (gameGui.TryGetAddonByName("Gathering", out atkUnitBase))
}
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)
{ {
logger.LogTrace("Can get all needed {NeededCollectability} from {Collectability}~ meticulous", atkUnitBase->FireCallbackInt(-1);
neededCollectability, nodeCondition.CollectabilityFromMeticulous); return ETaskResult.TaskComplete;
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 (gameFunctions.GetFreeInventorySlots() == 0)
throw new TaskException("Inventory full");
NodeCondition? nodeCondition = GetNodeCondition();
if (nodeCondition == null)
return ETaskResult.TaskComplete;
if (_expectedScrutiny != null)
{ {
if ((EClassJob?)clientState.LocalPlayer?.ClassJob.Id == EClassJob.Miner) if (nodeCondition.ScrutinyActive != _expectedScrutiny)
return minerAction; return ETaskResult.StillRunning;
else
return botanistAction; // 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(currentRequest.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;
}
private unsafe NodeCondition? GetNodeCondition()
{
if (gameGui.TryGetAddonByName("GatheringMasterpiece", out AtkUnitBase* atkUnitBase))
{
var atkValues = atkUnitBase->AtkValues;
return new NodeCondition(
CurrentCollectability: atkValues[13].UInt,
MaxCollectability: atkValues[14].UInt,
CurrentIntegrity: atkValues[62].UInt,
MaxIntegrity: atkValues[63].UInt,
ScrutinyActive: atkValues[54].Bool,
CollectabilityFromScour: atkValues[48].UInt,
CollectabilityFromMeticulous: atkValues[51].UInt
);
}
return null;
}
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,59 +3,50 @@ 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;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Gathering; using Questionable.Model.Gathering;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Gathering; namespace Questionable.Controller.Steps.Gathering;
internal static class MoveToLandingLocation internal sealed class MoveToLandingLocation(
ushort territoryId,
bool flyBetweenNodes,
GatheringNode gatheringNode,
MoveTo.Factory moveFactory,
GameFunctions gameFunctions,
IObjectTable objectTable,
ILogger<MoveToLandingLocation> logger) : ITask
{ {
internal sealed record Task( private ITask _moveTask = null!;
ushort TerritoryId,
bool FlyBetweenNodes,
GatheringNode GatheringNode) : ITask
{
public override string ToString() => $"Land/{FlyBetweenNodes}";
}
internal sealed class MoveToLandingLocationExecutor( public bool Start()
MoveTo.MoveExecutor moveExecutor,
GameFunctions gameFunctions,
IObjectTable objectTable,
ILogger<MoveToLandingLocationExecutor> logger) : TaskExecutor<Task>
{ {
private ITask _moveTask = null!; var location = gatheringNode.Locations.First();
if (gatheringNode.Locations.Count > 1)
protected override bool Start()
{ {
var location = Task.GatheringNode.Locations.First(); var gameObject = objectTable.SingleOrDefault(x =>
if (Task.GatheringNode.Locations.Count > 1) x.ObjectKind == ObjectKind.GatheringPoint && x.DataId == gatheringNode.DataId && x.IsTargetable);
{ if (gameObject == null)
var gameObject = objectTable.SingleOrDefault(x => return false;
x.ObjectKind == ObjectKind.GatheringPoint && x.DataId == Task.GatheringNode.DataId &&
x.IsTargetable);
if (gameObject == null)
return false;
location = Task.GatheringNode.Locations.Single(x => location = gatheringNode.Locations.Single(x => Vector3.Distance(x.Position, gameObject.Position) < 0.1f);
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,
InteractionType: EInteractionType.Gather);
return moveExecutor.Start(_moveTask);
} }
public override ETaskResult Update() => moveExecutor.Update(); 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();
public override string ToString() => $"Land/{_moveTask}/{flyBetweenNodes}";
} }

View File

@ -1,7 +1,9 @@
using FFXIVClientStructs.FFXIV.Client.Game; using System;
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;
@ -11,29 +13,24 @@ namespace Questionable.Controller.Steps.Gathering;
internal static class TurnInDelivery internal static class TurnInDelivery
{ {
internal sealed class Factory : SimpleTaskFactory internal sealed class Factory(ILoggerFactory loggerFactory) : 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 Task(); return new SatisfactionSupplyTurnIn(loggerFactory.CreateLogger<SatisfactionSupplyTurnIn>());
} }
} }
internal sealed record Task : ITask private sealed class SatisfactionSupplyTurnIn(ILogger<SatisfactionSupplyTurnIn> logger) : ITask
{
public override string ToString() => "WeeklyDeliveryTurnIn";
}
internal sealed class SatisfactionSupplyTurnIn(ILogger<SatisfactionSupplyTurnIn> logger) : TaskExecutor<Task>
{ {
private ushort? _remainingAllowances; private ushort? _remainingAllowances;
protected override bool Start() => true; public bool Start() => true;
public override unsafe ETaskResult Update() public unsafe ETaskResult Update()
{ {
AgentSatisfactionSupply* agentSatisfactionSupply = AgentSatisfactionSupply.Instance(); AgentSatisfactionSupply* agentSatisfactionSupply = AgentSatisfactionSupply.Instance();
if (agentSatisfactionSupply == null || !agentSatisfactionSupply->IsAgentActive()) if (agentSatisfactionSupply == null || !agentSatisfactionSupply->IsAgentActive())
@ -80,5 +77,7 @@ 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;
internal interface IConditionChangeAware : ITaskExecutor public interface IConditionChangeAware
{ {
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;
internal interface IRevisitAware : ITask public interface IRevisitAware
{ {
void OnRevisit(); void OnRevisit();
} }

View File

@ -1,6 +1,13 @@
namespace Questionable.Controller.Steps; using System.Threading;
using System.Threading.Tasks;
namespace Questionable.Controller.Steps;
internal interface ITask internal interface ITask
{ {
bool ShouldRedoOnInterrupt() => false; bool ShouldRedoOnInterrupt() => false;
bool Start();
ETaskResult Update();
} }

View File

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

View File

@ -1,97 +0,0 @@
using System;
using FFXIVClientStructs.FFXIV.Client.Game;
namespace Questionable.Controller.Steps;
internal sealed class InteractionProgressContext
{
private bool _firstUpdateDone;
public bool CheckSequence { get; private set; }
public int CurrentSequence { get; private set; }
private InteractionProgressContext(bool checkSequence, int currentSequence)
{
CheckSequence = checkSequence;
CurrentSequence = currentSequence;
}
public static unsafe InteractionProgressContext Create(bool checkSequence)
{
if (!checkSequence)
{
// this is a silly hack; we assume that the previous cast was successful
// if not for this, we'd instantly be seen as interrupted
ActionManager.Instance()->CastTimeElapsed = ActionManager.Instance()->CastTimeTotal;
}
return new InteractionProgressContext(checkSequence, ActionManager.Instance()->LastUsedActionSequence);
}
private static unsafe (bool, InteractionProgressContext?) FromActionUseInternal(Func<bool> func)
{
int oldSequence = ActionManager.Instance()->LastUsedActionSequence;
if (!func())
return (false, null);
int newSequence = ActionManager.Instance()->LastUsedActionSequence;
if (oldSequence == newSequence)
return (true, null);
return (true, Create(true));
}
public static InteractionProgressContext? FromActionUse(Func<bool> func)
{
return FromActionUseInternal(func).Item2;
}
public static InteractionProgressContext? FromActionUseOrDefault(Func<bool> func)
{
var result = FromActionUseInternal(func);
if (!result.Item1)
return null;
return result.Item2 ?? Create(false);
}
public unsafe void Update()
{
if (!_firstUpdateDone)
{
int lastSequence = ActionManager.Instance()->LastUsedActionSequence;
if (!CheckSequence && lastSequence > CurrentSequence)
{
CheckSequence = true;
CurrentSequence = lastSequence;
}
_firstUpdateDone = true;
}
}
public unsafe bool WasSuccessful()
{
if (CheckSequence)
{
if (CurrentSequence != ActionManager.Instance()->LastUsedActionSequence ||
CurrentSequence != ActionManager.Instance()->LastHandledActionSequence)
return false;
}
return ActionManager.Instance()->CastTimeElapsed > 0 &&
Math.Abs(ActionManager.Instance()->CastTimeElapsed - ActionManager.Instance()->CastTimeTotal) < 0.001f;
}
public unsafe bool WasInterrupted()
{
if (CheckSequence)
{
if (CurrentSequence == ActionManager.Instance()->LastHandledActionSequence &&
CurrentSequence == ActionManager.Instance()->LastUsedActionSequence)
return false;
}
return ActionManager.Instance()->CastTimeElapsed == 0 &&
ActionManager.Instance()->CastTimeTotal > 0;
}
public override string ToString() =>
$"IPCtx({(CheckSequence ? CurrentSequence : "-")} - {WasSuccessful()}, {WasInterrupted()})";
}

View File

@ -11,7 +11,8 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Action internal static class Action
{ {
internal sealed class Factory : ITaskFactory internal sealed class Factory(GameFunctions gameFunctions, 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)
{ {
@ -24,43 +25,39 @@ internal static class Action
if (step.Action.Value.RequiresMount()) if (step.Action.Value.RequiresMount())
return [task]; return [task];
else else
return [new Mount.UnmountTask(), task]; return [mountFactory.Unmount(), task];
} }
public static ITask OnObject(uint? dataId, EAction action) public ITask OnObject(uint? dataId, EAction action)
{ {
return new UseOnObject(dataId, action); return new UseOnObject(dataId, action, gameFunctions,
loggerFactory.CreateLogger<UseOnObject>());
} }
} }
internal sealed record UseOnObject( private sealed class UseOnObject(
uint? DataId, uint? dataId,
EAction Action) : ITask EAction action,
{
public override string ToString() => $"Action({Action})";
}
internal sealed class UseOnObjectExecutor(
GameFunctions gameFunctions, GameFunctions gameFunctions,
ILogger<UseOnObject> logger) : TaskExecutor<UseOnObject> ILogger<UseOnObject> logger) : ITask
{ {
private bool _usedAction; private bool _usedAction;
private DateTime _continueAt = DateTime.MinValue; private DateTime _continueAt = DateTime.MinValue;
protected override bool Start() public bool Start()
{ {
if (Task.DataId != null) if (dataId != null)
{ {
IGameObject? gameObject = gameFunctions.FindObjectByDataId(Task.DataId.Value); IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId.Value);
if (gameObject == null) if (gameObject == null)
{ {
logger.LogWarning("No game object with dataId {DataId}", Task.DataId); logger.LogWarning("No game object with dataId {DataId}", dataId);
return false; return false;
} }
if (gameObject.IsTargetable) if (gameObject.IsTargetable)
{ {
if (Task.Action == EAction.Diagnosis) if (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.
@ -75,14 +72,14 @@ internal static class Action
} }
} }
_usedAction = gameFunctions.UseAction(gameObject, Task.Action); _usedAction = gameFunctions.UseAction(gameObject, action);
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(0.5);
return true; return true;
} }
} }
else else
{ {
_usedAction = gameFunctions.UseAction(Task.Action); _usedAction = gameFunctions.UseAction(action);
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(0.5);
return true; return true;
} }
@ -90,25 +87,25 @@ internal static class Action
return true; return true;
} }
public override ETaskResult Update() public ETaskResult Update()
{ {
if (DateTime.Now <= _continueAt) if (DateTime.Now <= _continueAt)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
if (!_usedAction) if (!_usedAction)
{ {
if (Task.DataId != null) if (dataId != null)
{ {
IGameObject? gameObject = gameFunctions.FindObjectByDataId(Task.DataId.Value); IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId.Value);
if (gameObject == null || !gameObject.IsTargetable) if (gameObject == null || !gameObject.IsTargetable)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
_usedAction = gameFunctions.UseAction(gameObject, Task.Action); _usedAction = gameFunctions.UseAction(gameObject, action);
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(0.5);
} }
else else
{ {
_usedAction = gameFunctions.UseAction(Task.Action); _usedAction = gameFunctions.UseAction(action);
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(0.5);
} }
@ -117,5 +114,7 @@ internal static class Action
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
public override string ToString() => $"Action({action})";
} }
} }

View File

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

View File

@ -11,7 +11,10 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class AethernetShard internal static class AethernetShard
{ {
internal sealed class Factory : SimpleTaskFactory internal sealed class Factory(
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)
{ {
@ -20,38 +23,35 @@ internal static class AethernetShard
ArgumentNullException.ThrowIfNull(step.AethernetShard); ArgumentNullException.ThrowIfNull(step.AethernetShard);
return new Attune(step.AethernetShard.Value); return new DoAttune(step.AethernetShard.Value, aetheryteFunctions, gameFunctions,
loggerFactory.CreateLogger<DoAttune>());
} }
} }
internal sealed record Attune(EAetheryteLocation AetheryteLocation) : ITask private sealed class DoAttune(
{ EAetheryteLocation aetheryteLocation,
public bool ShouldRedoOnInterrupt() => true;
public override string ToString() => $"AttuneAethernetShard({AetheryteLocation})";
}
internal sealed class DoAttune(
AetheryteFunctions aetheryteFunctions, AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions, GameFunctions gameFunctions,
ILogger<DoAttune> logger) : TaskExecutor<Attune> ILogger<DoAttune> logger) : ITask
{ {
protected override bool Start() public bool Start()
{ {
if (!aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation)) if (!aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation))
{ {
logger.LogInformation("Attuning to aethernet shard {AethernetShard}", Task.AetheryteLocation); logger.LogInformation("Attuning to aethernet shard {AethernetShard}", aetheryteLocation);
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}", Task.AetheryteLocation); logger.LogInformation("Already attuned to aethernet shard {AethernetShard}", aetheryteLocation);
return false; return false;
} }
public override ETaskResult Update() => public ETaskResult Update() =>
aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation) aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation)
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : ETaskResult.StillRunning;
public override string ToString() => $"AttuneAethernetShard({aetheryteLocation})";
} }
} }

View File

@ -1,4 +1,5 @@
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;
@ -9,7 +10,10 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Aetheryte internal static class Aetheryte
{ {
internal sealed class Factory : SimpleTaskFactory internal sealed class Factory(
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)
{ {
@ -18,39 +22,35 @@ internal static class Aetheryte
ArgumentNullException.ThrowIfNull(step.Aetheryte); ArgumentNullException.ThrowIfNull(step.Aetheryte);
return new Attune(step.Aetheryte.Value); return new DoAttune(step.Aetheryte.Value, aetheryteFunctions, gameFunctions,
loggerFactory.CreateLogger<DoAttune>());
} }
} }
internal sealed record Attune(EAetheryteLocation AetheryteLocation) : ITask private sealed class DoAttune(
{ EAetheryteLocation aetheryteLocation,
public bool ShouldRedoOnInterrupt() => true;
public override string ToString() => $"AttuneAetheryte({AetheryteLocation})";
}
internal sealed class DoAttune(
AetheryteFunctions aetheryteFunctions, AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions, GameFunctions gameFunctions,
ILogger<DoAttune> logger) : TaskExecutor<Attune> ILogger<DoAttune> logger) : ITask
{ {
protected override bool Start() public bool Start()
{ {
if (!aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation)) if (!aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation))
{ {
logger.LogInformation("Attuning to aetheryte {Aetheryte}", Task.AetheryteLocation); logger.LogInformation("Attuning to aetheryte {Aetheryte}", aetheryteLocation);
ProgressContext = gameFunctions.InteractWith((uint)aetheryteLocation);
InteractionProgressContext.FromActionUseOrDefault(() =>
gameFunctions.InteractWith((uint)Task.AetheryteLocation));
return true; return true;
} }
logger.LogInformation("Already attuned to aetheryte {Aetheryte}", Task.AetheryteLocation); logger.LogInformation("Already attuned to aetheryte {Aetheryte}", aetheryteLocation);
return false; return false;
} }
public override ETaskResult Update() => public ETaskResult Update() =>
aetheryteFunctions.IsAetheryteUnlocked(Task.AetheryteLocation) aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation)
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : ETaskResult.StillRunning;
public override string ToString() => $"AttuneAetheryte({aetheryteLocation})";
} }
} }

View File

@ -13,7 +13,14 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Combat internal static class Combat
{ {
internal sealed class Factory(GameFunctions gameFunctions) : ITaskFactory internal sealed class Factory(
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)
{ {
@ -23,7 +30,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 new Mount.UnmountTask(); yield return mountFactory.Unmount();
if (step.CombatDelaySecondsAtStart != null) if (step.CombatDelaySecondsAtStart != null)
{ {
@ -36,7 +43,7 @@ internal static class Combat
{ {
ArgumentNullException.ThrowIfNull(step.DataId); ArgumentNullException.ThrowIfNull(step.DataId);
yield return new Interact.Task(step.DataId.Value, quest, EInteractionType.None, true); yield return interactFactory.Interact(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;
@ -47,7 +54,7 @@ internal static class Combat
ArgumentNullException.ThrowIfNull(step.DataId); ArgumentNullException.ThrowIfNull(step.DataId);
ArgumentNullException.ThrowIfNull(step.ItemId); ArgumentNullException.ThrowIfNull(step.ItemId);
yield return new UseItem.UseOnObject(quest.Id, step.DataId.Value, step.ItemId.Value, yield return useItemFactory.OnObject(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);
@ -60,8 +67,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 new Mount.UnmountTask(); yield return mountFactory.Unmount();
yield return new Action.UseOnObject(step.DataId.Value, step.Action.Value); yield return actionFactory.OnObject(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;
@ -85,7 +92,7 @@ internal static class Combat
} }
} }
private static Task CreateTask(Quest quest, QuestSequence sequence, QuestStep step) public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
ArgumentNullException.ThrowIfNull(step.EnemySpawnType); ArgumentNullException.ThrowIfNull(step.EnemySpawnType);
@ -94,60 +101,46 @@ internal static class Combat
step.CompletionQuestVariablesFlags, step.ComplexCombatData); step.CompletionQuestVariablesFlags, step.ComplexCombatData);
} }
internal static Task CreateTask(ElementId? elementId, bool isLastStep, EEnemySpawnType enemySpawnType, internal HandleCombat 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 Task(new CombatController.CombatData return new HandleCombat(isLastStep, new CombatController.CombatData
{ {
ElementId = elementId, ElementId = elementId,
SpawnType = enemySpawnType, SpawnType = enemySpawnType,
KillEnemyDataIds = killEnemyDataIds.ToList(), KillEnemyDataIds = killEnemyDataIds.ToList(),
ComplexCombatDatas = complexCombatData.ToList(), ComplexCombatDatas = complexCombatData.ToList(),
}, completionQuestVariablesFlags, isLastStep); }, completionQuestVariablesFlags, combatController, questFunctions);
}
}
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) : TaskExecutor<Task> QuestFunctions questFunctions) : ITask
{ {
private CombatController.EStatus _status = CombatController.EStatus.NotStarted; private CombatController.EStatus _status = CombatController.EStatus.NotStarted;
protected override bool Start() => combatController.Start(Task.CombatData); public bool Start() => combatController.Start(combatData);
public override ETaskResult Update() public 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(Task.CompletionQuestVariableFlags) && if (QuestWorkUtils.HasCompletionFlags(completionQuestVariableFlags) &&
Task.CombatData.ElementId is QuestId questId) 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(Task.CompletionQuestVariableFlags, questWork)) if (QuestWorkUtils.MatchesQuestWork(completionQuestVariableFlags, questWork))
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
else else
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -155,7 +148,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 (Task.IsLastStep) if (isLastStep)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
else else
{ {
@ -163,5 +156,15 @@ 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,25 +18,24 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Dive internal static class Dive
{ {
internal sealed class Factory : SimpleTaskFactory internal sealed class Factory(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)
{ {
if (step.InteractionType != EInteractionType.Dive) if (step.InteractionType != EInteractionType.Dive)
return null; return null;
return new Task(); return Dive();
}
public ITask Dive()
{
return new DoDive(condition, loggerFactory.CreateLogger<DoDive>());
} }
} }
internal sealed class Task : ITask private sealed class DoDive(ICondition condition, ILogger<DoDive> logger)
{ : 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;
@ -115,6 +114,8 @@ 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,6 +1,7 @@
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;
@ -9,7 +10,7 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Duty internal static class Duty
{ {
internal sealed class Factory : SimpleTaskFactory internal sealed class Factory(GameFunctions gameFunctions, ICondition condition) : SimpleTaskFactory
{ {
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step) public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -17,28 +18,26 @@ internal static class Duty
return null; return null;
ArgumentNullException.ThrowIfNull(step.ContentFinderConditionId); ArgumentNullException.ThrowIfNull(step.ContentFinderConditionId);
return new Task(step.ContentFinderConditionId.Value); return new OpenDutyFinder(step.ContentFinderConditionId.Value, gameFunctions, condition);
} }
} }
internal sealed record Task(uint ContentFinderConditionId) : ITask private sealed class OpenDutyFinder(
{ uint contentFinderConditionId,
public override string ToString() => $"OpenDutyFinder({ContentFinderConditionId})";
}
internal sealed class OpenDutyWindowExecutor(
GameFunctions gameFunctions, GameFunctions gameFunctions,
ICondition condition) : TaskExecutor<Task> ICondition condition) : ITask
{ {
protected override bool Start() public bool Start()
{ {
if (condition[ConditionFlag.InDutyQueue]) if (condition[ConditionFlag.InDutyQueue])
return false; return false;
gameFunctions.OpenDutyFinder(Task.ContentFinderConditionId); gameFunctions.OpenDutyFinder(contentFinderConditionId);
return true; return true;
} }
public override ETaskResult Update() => ETaskResult.TaskComplete; public ETaskResult Update() => ETaskResult.TaskComplete;
public override string ToString() => $"OpenDutyFinder({contentFinderConditionId})";
} }
} }

View File

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

View File

@ -16,7 +16,7 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class EquipItem internal static class EquipItem
{ {
internal sealed class Factory : SimpleTaskFactory internal sealed class Factory(IDataManager dataManager, ILoggerFactory loggerFactory) : SimpleTaskFactory
{ {
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step) public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -24,18 +24,36 @@ internal static class EquipItem
return null; return null;
ArgumentNullException.ThrowIfNull(step.ItemId); ArgumentNullException.ThrowIfNull(step.ItemId);
return new Task(step.ItemId.Value); return Equip(step.ItemId.Value);
}
private DoEquip Equip(uint itemId)
{
var item = dataManager.GetExcelSheet<Item>()!.GetRow(itemId) ??
throw new ArgumentOutOfRangeException(nameof(itemId));
var targetSlots = GetEquipSlot(item) ?? throw new InvalidOperationException("Not a piece of equipment");
return new DoEquip(itemId, item, targetSlots, dataManager, loggerFactory.CreateLogger<DoEquip>());
}
private static List<ushort>? GetEquipSlot(Item item)
{
return item.EquipSlotCategory.Row switch
{
>= 1 and <= 11 => [(ushort)(item.EquipSlotCategory.Row - 1)],
12 => [11, 12], // rings
13 => [0],
17 => [13], // soul crystal
_ => null
};
} }
} }
internal sealed record Task(uint ItemId) : ITask private sealed class DoEquip(
{ uint itemId,
public override string ToString() => $"Equip({ItemId})"; Item item,
} List<ushort> targetSlots,
internal sealed class DoEquip(
IDataManager dataManager, IDataManager dataManager,
ILogger<DoEquip> logger) : TaskExecutor<Task>, IToastAware ILogger<DoEquip> logger) : ITask, IToastAware
{ {
private const int MaxAttempts = 3; private const int MaxAttempts = 3;
@ -63,22 +81,16 @@ 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;
protected override bool Start() public 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 override unsafe ETaskResult Update() public unsafe ETaskResult Update()
{ {
if (DateTime.Now < _continueAt) if (DateTime.Now < _continueAt)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -87,10 +99,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 == Task.ItemId) if (itemSlot != null && itemSlot->ItemId == itemId)
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
@ -113,12 +125,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 == Task.ItemId) if (itemSlot != null && itemSlot->ItemId == itemId)
{ {
logger.LogInformation("Already equipped {Item}, skipping step", _item.Name?.ToString()); logger.LogInformation("Already equipped {Item}, skipping step", item.Name?.ToString());
return; return;
} }
} }
@ -129,24 +141,24 @@ internal static class EquipItem
if (sourceContainer == null) if (sourceContainer == null)
continue; continue;
if (inventoryManager->GetItemCountInContainer(Task.ItemId, sourceInventoryType, true) == 0 && if (inventoryManager->GetItemCountInContainer(itemId, sourceInventoryType, true) == 0 &&
inventoryManager->GetItemCountInContainer(Task.ItemId, sourceInventoryType) == 0) inventoryManager->GetItemCountInContainer(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 != Task.ItemId) if (sourceItem == null || sourceItem->ItemId != 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}",
@ -160,17 +172,7 @@ internal static class EquipItem
} }
} }
private static List<ushort>? GetEquipSlot(Item item) public override string ToString() => $"Equip({item.Name})";
{
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,18 +10,23 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class EquipRecommended internal static class EquipRecommended
{ {
internal sealed class Factory : SimpleTaskFactory internal sealed class Factory(IClientState clientState, IChatGui chatGui) : 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 new EquipTask(); return DoEquip();
}
public ITask DoEquip()
{
return new DoEquipRecommended(clientState, chatGui);
} }
} }
internal sealed class BeforeDutyOrInstance : SimpleTaskFactory internal sealed class BeforeDutyOrInstance(IClientState clientState, IChatGui chatGui) : SimpleTaskFactory
{ {
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step) public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -30,26 +35,21 @@ internal static class EquipRecommended
step.InteractionType != EInteractionType.Combat) step.InteractionType != EInteractionType.Combat)
return null; return null;
return new EquipTask(); return new DoEquipRecommended(clientState, chatGui);
} }
} }
internal sealed class EquipTask : ITask private sealed unsafe class DoEquipRecommended(IClientState clientState, IChatGui chatGui) : ITask
{
public override string ToString() => "EquipRecommended";
}
internal sealed unsafe class DoEquipRecommended(IClientState clientState, IChatGui chatGui) : TaskExecutor<EquipTask>
{ {
private bool _equipped; private bool _equipped;
protected override bool Start() public bool Start()
{ {
RecommendEquipModule.Instance()->SetupForClassJob((byte)clientState.LocalPlayer!.ClassJob.Id); RecommendEquipModule.Instance()->SetupForClassJob((byte)clientState.LocalPlayer!.ClassJob.Id);
return true; return true;
} }
public override ETaskResult Update() public ETaskResult Update()
{ {
var recommendedEquipModule = RecommendEquipModule.Instance(); var recommendedEquipModule = RecommendEquipModule.Instance();
if (recommendedEquipModule->IsUpdating) if (recommendedEquipModule->IsUpdating)
@ -94,5 +94,7 @@ internal static class EquipRecommended
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
public override string ToString() => "EquipRecommended";
} }
} }

View File

@ -5,6 +5,7 @@ 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;
@ -15,7 +16,12 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Interact internal static class Interact
{ {
internal sealed class Factory(Configuration configuration) : ITaskFactory internal sealed class Factory(
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)
{ {
@ -49,52 +55,56 @@ 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 new Task(step.DataId.Value, quest, step.InteractionType, yield return Interact(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 sealed record Task( internal ITask Interact(uint dataId, Quest? quest, EInteractionType interactionType,
uint DataId, bool skipMarkerCheck = false, uint? pickUpItemId = null, SkipStepConditions? skipConditions = null)
Quest? Quest, {
EInteractionType InteractionType, return new DoInteract(dataId, quest, interactionType, skipMarkerCheck, pickUpItemId, skipConditions,
bool SkipMarkerCheck = false, gameFunctions, condition, loggerFactory.CreateLogger<DoInteract>());
uint? PickUpItemId = null, }
SkipStepConditions? SkipConditions = null) : ITask
{
public bool ShouldRedoOnInterrupt() => true;
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)
: TaskExecutor<Task> : ITask, IConditionChangeAware
{ {
private bool _needsUnmount; private bool _needsUnmount;
private EInteractionState _interactionState = EInteractionState.None;
private DateTime _continueAt = DateTime.MinValue; private DateTime _continueAt = DateTime.MinValue;
public Quest? Quest => Task.Quest; public Quest? Quest => quest;
public EInteractionType InteractionType { get; set; }
protected override bool Start() public EInteractionType InteractionType
{ {
InteractionType = Task.InteractionType; get => interactionType;
set => interactionType = value;
}
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}", Task.DataId); logger.LogWarning("No game object with dataId {DataId}", dataId);
return false; return false;
} }
if (!gameObject.IsTargetable && Task.SkipConditions is { Never: false, NotTargetable: true }) if (!gameObject.IsTargetable && 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)",
Task.DataId); dataId);
return false; return false;
} }
@ -102,7 +112,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", Task.DataId); logger.LogInformation("Preparing interaction for {DataId} by unmounting", dataId);
_needsUnmount = true; _needsUnmount = true;
gameFunctions.Unmount(); gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(1); _continueAt = DateTime.Now.AddSeconds(1);
@ -111,8 +121,9 @@ internal static class Interact
if (gameObject.IsTargetable && HasAnyMarker(gameObject)) if (gameObject.IsTargetable && HasAnyMarker(gameObject))
{ {
ProgressContext = _interactionState = gameFunctions.InteractWith(gameObject)
InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(gameObject)); ? EInteractionState.InteractionTriggered
: EInteractionState.None;
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(0.5);
return true; return true;
} }
@ -120,7 +131,7 @@ internal static class Interact
return true; return true;
} }
public override ETaskResult Update() public ETaskResult Update()
{ {
if (DateTime.Now <= _continueAt) if (DateTime.Now <= _continueAt)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -137,41 +148,72 @@ internal static class Interact
_needsUnmount = false; _needsUnmount = false;
} }
if (Task.PickUpItemId != null) if (pickUpItemId != null)
{ {
unsafe unsafe
{ {
InventoryManager* inventoryManager = InventoryManager.Instance(); InventoryManager* inventoryManager = InventoryManager.Instance();
if (inventoryManager->GetInventoryItemCount(Task.PickUpItemId.Value) > 0) if (inventoryManager->GetInventoryItemCount(pickUpItemId.Value) > 0)
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
} }
else else
{ {
if (ProgressContext != null && ProgressContext.WasSuccessful()) if (_interactionState == EInteractionState.InteractionConfirmed)
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(Task.DataId); IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId);
if (gameObject == null || !gameObject.IsTargetable || !HasAnyMarker(gameObject)) if (gameObject == null || !gameObject.IsTargetable || !HasAnyMarker(gameObject))
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
ProgressContext = _interactionState = gameFunctions.InteractWith(gameObject)
InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(gameObject)); ? EInteractionState.InteractionTriggered
: EInteractionState.None;
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(0.5);
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
private unsafe bool HasAnyMarker(IGameObject gameObject) private unsafe bool HasAnyMarker(IGameObject gameObject)
{ {
if (Task.SkipMarkerCheck || gameObject.ObjectKind != ObjectKind.EventNpc) if (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})";
public void OnConditionChange(ConditionFlag flag, bool value)
{
logger.LogDebug("Condition change: {Flag} = {Value}", flag, value);
if (_interactionState == EInteractionState.InteractionTriggered &&
flag is ConditionFlag.OccupiedInQuestEvent or ConditionFlag.OccupiedInEvent &&
value)
{
logger.LogInformation("Interaction was most likely triggered");
_interactionState = EInteractionState.InteractionConfirmed;
}
else if (dataId is >= 1047901 and <= 1047905 &&
condition[ConditionFlag.Disguised] &&
flag == ConditionFlag
.Mounting71 && // why the fuck is this the flag that's used, instead of OccupiedIn[Quest]Event
value)
{
logger.LogInformation("(A Knight of Alexandria) Interaction was most likely triggered");
_interactionState = EInteractionState.InteractionConfirmed;
}
}
private enum EInteractionState
{
None,
InteractionTriggered,
InteractionConfirmed,
}
} }
} }

View File

@ -12,7 +12,12 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Jump internal static class Jump
{ {
internal sealed class Factory : SimpleTaskFactory internal sealed class Factory(
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)
{ {
@ -22,42 +27,39 @@ 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 new SingleJumpTask(step.DataId, step.JumpDestination, step.Comment); return SingleJump(step.DataId, step.JumpDestination, step.Comment);
else else
return new RepeatedJumpTask(step.DataId, step.JumpDestination, step.Comment); return RepeatedJumps(step.DataId, step.JumpDestination, step.Comment);
}
public ITask SingleJump(uint? dataId, JumpDestination jumpDestination, string? comment)
{
return new DoSingleJump(dataId, jumpDestination, comment, movementController, clientState, framework);
}
public ITask RepeatedJumps(uint? dataId, JumpDestination jumpDestination, string? comment)
{
return new DoRepeatedJumps(dataId, jumpDestination, comment, movementController, clientState, framework,
condition, loggerFactory.CreateLogger<DoRepeatedJumps>());
} }
} }
internal interface IJumpTask : ITask private class DoSingleJump(
{ uint? dataId,
uint? DataId { get; } JumpDestination jumpDestination,
JumpDestination JumpDestination { get; } string? comment,
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) : TaskExecutor<T> IFramework framework) : ITask
where T : class, IJumpTask
{ {
protected override bool Start() public virtual bool Start()
{ {
float stopDistance = Task.JumpDestination.CalculateStopDistance(); float stopDistance = jumpDestination.CalculateStopDistance();
if ((clientState.LocalPlayer!.Position - Task.JumpDestination.Position).Length() <= stopDistance) if ((clientState.LocalPlayer!.Position - jumpDestination.Position).Length() <= stopDistance)
return false; return false;
movementController.NavigateTo(EMovementType.Quest, Task.DataId, [Task.JumpDestination.Position], false, movementController.NavigateTo(EMovementType.Quest, dataId, [jumpDestination.Position], false, false,
false, jumpDestination.StopDistance ?? stopDistance);
Task.JumpDestination.StopDistance ?? stopDistance);
framework.RunOnTick(() => framework.RunOnTick(() =>
{ {
unsafe unsafe
@ -65,11 +67,11 @@ internal static class Jump
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2); ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2);
} }
}, },
TimeSpan.FromSeconds(Task.JumpDestination.DelaySeconds ?? 0.5f)); TimeSpan.FromSeconds(jumpDestination.DelaySeconds ?? 0.5f));
return true; return true;
} }
public override ETaskResult Update() public virtual ETaskResult Update()
{ {
if (movementController.IsPathfinding || movementController.IsPathRunning) if (movementController.IsPathfinding || movementController.IsPathRunning)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -80,36 +82,30 @@ internal static class Jump
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
public override string ToString() => $"Jump({comment})";
} }
internal sealed class DoSingleJump( private sealed class DoRepeatedJumps(
MovementController movementController, uint? dataId,
IClientState clientState, JumpDestination jumpDestination,
IFramework framework) : JumpBase<SingleJumpTask>(movementController, clientState, framework); string? comment,
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)
: JumpBase<RepeatedJumpTask>(movementController, clientState, framework) : DoSingleJump(dataId, jumpDestination, comment, 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;
protected override bool Start() public override bool Start()
{ {
_continueAt = DateTime.Now + TimeSpan.FromSeconds(2 * (Task.JumpDestination.DelaySeconds ?? 0.5f)); _continueAt = DateTime.Now + TimeSpan.FromSeconds(2 * (_jumpDestination.DelaySeconds ?? 0.5f));
return base.Start(); return base.Start();
} }
@ -118,13 +114,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 = Task.JumpDestination.CalculateStopDistance(); float stopDistance = _jumpDestination.CalculateStopDistance();
if ((_clientState.LocalPlayer!.Position - Task.JumpDestination.Position).Length() <= stopDistance || if ((_clientState.LocalPlayer!.Position - _jumpDestination.Position).Length() <= stopDistance ||
_clientState.LocalPlayer.Position.Y >= Task.JumpDestination.Position.Y - 0.5f) _clientState.LocalPlayer.Position.Y >= _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,
Task.JumpDestination.Position.Y - 0.5f); _jumpDestination.Position.Y - 0.5f);
unsafe unsafe
{ {
if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2)) if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2))
@ -134,8 +130,10 @@ 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(Task.JumpDestination.DelaySeconds ?? 0.5f); _continueAt = DateTime.Now + TimeSpan.FromSeconds(_jumpDestination.DelaySeconds ?? 0.5f);
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
public override string ToString() => $"RepeatedJump({_comment})";
} }
} }

View File

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

View File

@ -5,7 +5,9 @@ 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;
@ -22,8 +24,17 @@ 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
{ {
@ -48,7 +59,7 @@ internal static class UseItem
return CreateVesperBayFallbackTask(); return CreateVesperBayFallbackTask();
} }
var task = new UseOnSelf(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags); var task = OnSelf(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();
@ -56,28 +67,27 @@ internal static class UseItem
return return
[ [
task, task,
new WaitCondition.Task(() => clientState.TerritoryType == 140, new WaitConditionTask(() => clientState.TerritoryType == 140,
$"Wait(territory: {territoryData.GetNameAndId(140)})"), $"Wait(territory: {territoryData.GetNameAndId(140)})"),
new Mount.MountTask(140, mountFactory.Mount(140,
nextPosition != null ? Mount.EMountIf.AwayFromPosition : Mount.EMountIf.Always, nextPosition != null ? Mount.EMountIf.AwayFromPosition : Mount.EMountIf.Always,
nextPosition), nextPosition),
new MoveTo.MoveTask(140, new(-408.92343f, 23.167036f, -351.16223f), null, 0.25f, moveFactory.Move(new MoveTo.MoveParams(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))
InteractionType: EInteractionType.WalkTo)
]; ];
} }
var unmount = new Mount.UnmountTask(); var unmount = mountFactory.Unmount();
if (step.GroundTarget == true) if (step.GroundTarget == true)
{ {
ITask task; ITask task;
if (step.DataId != null) if (step.DataId != null)
task = new UseOnGround(quest.Id, step.DataId.Value, step.ItemId.Value, task = OnGroundTarget(quest.Id, step.DataId.Value, step.ItemId.Value,
step.CompletionQuestVariablesFlags); step.CompletionQuestVariablesFlags);
else else
{ {
ArgumentNullException.ThrowIfNull(step.Position); ArgumentNullException.ThrowIfNull(step.Position);
task = new UseOnPosition(quest.Id, step.Position.Value, step.ItemId.Value, task = OnPosition(quest.Id, step.Position.Value, step.ItemId.Value,
step.CompletionQuestVariablesFlags); step.CompletionQuestVariablesFlags);
} }
@ -85,17 +95,43 @@ internal static class UseItem
} }
else if (step.DataId != null) else if (step.DataId != null)
{ {
var task = new UseOnObject(quest.Id, step.DataId.Value, step.ItemId.Value, var task = OnObject(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags);
step.CompletionQuestVariablesFlags);
return [unmount, task]; return [unmount, task];
} }
else else
{ {
var task = new UseOnSelf(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags); var task = OnSelf(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");
@ -103,41 +139,36 @@ 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 new AetheryteShortcut.Task(null, null, EAetheryteLocation.Limsa, territoryId); yield return aetheryteShortcutFactory.Use(null, null, EAetheryteLocation.Limsa, territoryId);
yield return new AethernetShortcut.Task(EAetheryteLocation.Limsa, EAetheryteLocation.LimsaArcanist); yield return aethernetShortcutFactory.Use(EAetheryteLocation.Limsa, EAetheryteLocation.LimsaArcanist);
yield return new WaitAtEnd.WaitDelay(); yield return new WaitAtEnd.WaitDelay();
yield return new MoveTo.MoveTask(territoryId, destination, DataId: npcId, Sprint: false, yield return
InteractionType: EInteractionType.WalkTo); 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);
} }
} }
internal interface IUseItemBase : ITask private abstract class UseItemBase(
{ ElementId? questId,
ElementId? QuestId { get; } uint itemId,
uint ItemId { get; } IList<QuestWorkValue?> completionQuestVariablesFlags,
IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; } bool startingCombat,
bool StartingCombat { get; }
}
internal abstract class UseItemExecutorBase<T>(
QuestFunctions questFunctions, QuestFunctions questFunctions,
ICondition condition, ICondition condition,
ILogger logger) : TaskExecutor<T> ILogger logger) : ITask
where T : class, IUseItemBase
{ {
private bool _usedItem; private bool _usedItem;
private DateTime _continueAt; private DateTime _continueAt;
private int _itemCount; private int _itemCount;
private ElementId? QuestId => Task.QuestId; public ElementId? QuestId => questId;
protected uint ItemId => Task.ItemId; public uint ItemId => itemId;
private IList<QuestWorkValue?> CompletionQuestVariablesFlags => Task.CompletionQuestVariablesFlags; public IList<QuestWorkValue?> CompletionQuestVariablesFlags => completionQuestVariablesFlags;
private bool StartingCombat => Task.StartingCombat; public bool StartingCombat => startingCombat;
protected abstract bool UseItem(); protected abstract bool UseItem();
protected override unsafe bool Start() public unsafe bool Start()
{ {
InventoryManager* inventoryManager = InventoryManager.Instance(); InventoryManager* inventoryManager = InventoryManager.Instance();
if (inventoryManager == null) if (inventoryManager == null)
@ -147,12 +178,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()); _usedItem = UseItem();
_continueAt = DateTime.Now.Add(GetRetryDelay()); _continueAt = DateTime.Now.Add(GetRetryDelay());
return true; return true;
} }
public override unsafe ETaskResult Update() public unsafe ETaskResult Update()
{ {
if (QuestId is QuestId realQuestId && QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags)) if (QuestId is QuestId realQuestId && QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags))
{ {
@ -190,7 +221,7 @@ internal static class UseItem
if (!_usedItem) if (!_usedItem)
{ {
ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem()); _usedItem = UseItem();
_continueAt = DateTime.Now.Add(GetRetryDelay()); _continueAt = DateTime.Now.Add(GetRetryDelay());
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
@ -207,85 +238,69 @@ 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})";
}
internal sealed class UseOnGroundExecutor( private sealed class UseOnGround(
ElementId? questId,
uint dataId,
uint itemId,
IList<QuestWorkValue?> completionQuestVariablesFlags,
GameFunctions gameFunctions, GameFunctions gameFunctions,
QuestFunctions questFunctions, QuestFunctions questFunctions,
ICondition condition, ICondition condition,
ILogger<UseOnGroundExecutor> logger) ILogger<UseOnGround> logger)
: UseItemExecutorBase<UseOnGround>(questFunctions, condition, logger) : UseItemBase(questId, itemId, completionQuestVariablesFlags, false, questFunctions, condition, logger)
{ {
protected override bool UseItem() => gameFunctions.UseItemOnGround(Task.DataId, ItemId); protected override bool UseItem() => gameFunctions.UseItemOnGround(dataId, ItemId);
public override string ToString() => $"UseItem({ItemId} on ground at {dataId})";
} }
internal sealed record UseOnPosition( private sealed class 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)
: UseItemExecutorBase<UseOnPosition>(questFunctions, condition, logger) : UseItemBase(questId, itemId, completionQuestVariablesFlags, false, questFunctions, condition, logger)
{ {
protected override bool UseItem() => gameFunctions.UseItemOnPosition(Task.Position, ItemId); protected override bool UseItem() => gameFunctions.UseItemOnPosition(position, ItemId);
public override string ToString() =>
$"UseItem({ItemId} on ground at {position.ToString("G", CultureInfo.InvariantCulture)})";
} }
internal sealed record UseOnObject( private sealed class UseOnObject(
ElementId? QuestId, ElementId? questId,
uint DataId, uint dataId,
uint ItemId, uint itemId,
IList<QuestWorkValue?> CompletionQuestVariablesFlags, IList<QuestWorkValue?> completionQuestVariablesFlags,
bool StartingCombat = false) : IUseItemBase bool startingCombat,
{
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)
: UseItemExecutorBase<UseOnObject>(questFunctions, condition, logger) : UseItemBase(questId, itemId, completionQuestVariablesFlags, startingCombat, questFunctions, condition, logger)
{ {
protected override bool UseItem() => gameFunctions.UseItem(Task.DataId, ItemId); protected override bool UseItem() => gameFunctions.UseItem(dataId, ItemId);
public override string ToString() => $"UseItem({ItemId} on {dataId})";
} }
internal sealed record UseOnSelf( private sealed class Use(
ElementId? QuestId, ElementId? questId,
uint ItemId, uint itemId,
IList<QuestWorkValue?> CompletionQuestVariablesFlags) : IUseItemBase IList<QuestWorkValue?> completionQuestVariablesFlags,
{
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<UseOnSelf> logger) ILogger<Use> logger)
: UseItemExecutorBase<UseOnSelf>(questFunctions, condition, logger) : UseItemBase(questId, itemId, completionQuestVariablesFlags, false, 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,7 +7,9 @@ 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;
@ -16,7 +18,7 @@ namespace Questionable.Controller.Steps.Leves;
internal static class InitiateLeve internal static class InitiateLeve
{ {
internal sealed class Factory(ICondition condition) : ITaskFactory internal sealed class Factory(IGameGui gameGui, ICondition condition) : ITaskFactory
{ {
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -25,86 +27,75 @@ 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); yield return new Initiate(quest.Id, gameGui);
yield return new SelectDifficulty(); yield return new SelectDifficulty(gameGui);
yield return new WaitCondition.Task(() => condition[ConditionFlag.BoundByDuty], "Wait(BoundByDuty)"); yield return new WaitConditionTask(() => condition[ConditionFlag.BoundByDuty], "Wait(BoundByDuty)");
} }
} }
internal sealed record SkipInitiateIfActive(ElementId ElementId) : ITask internal sealed unsafe class SkipInitiateIfActive(ElementId elementId) : ITask
{ {
public override string ToString() => $"CheckIfAlreadyActive({ElementId})"; public bool Start() => true;
}
internal sealed unsafe class SkipInitiateIfActiveExecutor : TaskExecutor<SkipInitiateIfActive> public ETaskResult Update()
{
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 == Task.ElementId.Value) director->ContentId == elementId.Value)
return ETaskResult.SkipRemainingTasksForStep; return ETaskResult.SkipRemainingTasksForStep;
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
public override string ToString() => $"CheckIfAlreadyActive({elementId})";
} }
internal sealed record OpenJournal(ElementId ElementId) : ITask internal sealed unsafe class 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;
protected override bool Start() public bool Start()
{ {
AgentQuestJournal.Instance()->OpenForQuest(Task.ElementId.Value, Task.QuestType); AgentQuestJournal.Instance()->OpenForQuest(elementId.Value, _questType);
_openedAt = DateTime.Now; _openedAt = DateTime.Now;
return true; return true;
} }
public override ETaskResult Update() public ETaskResult Update()
{ {
AgentQuestJournal* agentQuestJournal = AgentQuestJournal.Instance(); AgentQuestJournal* agentQuestJournal = AgentQuestJournal.Instance();
if (agentQuestJournal->IsAgentActive() && if (agentQuestJournal->IsAgentActive() &&
agentQuestJournal->SelectedQuestId == Task.ElementId.Value && agentQuestJournal->SelectedQuestId == elementId.Value &&
agentQuestJournal->SelectedQuestType == Task.QuestType) agentQuestJournal->SelectedQuestType == _questType)
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
if (DateTime.Now > _openedAt.AddSeconds(3)) if (DateTime.Now > _openedAt.AddSeconds(3))
{ {
AgentQuestJournal.Instance()->OpenForQuest(Task.ElementId.Value, Task.QuestType); AgentQuestJournal.Instance()->OpenForQuest(elementId.Value, _questType);
_openedAt = DateTime.Now; _openedAt = DateTime.Now;
} }
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
public override string ToString() => $"OpenJournal({elementId})";
} }
internal sealed record Initiate(ElementId ElementId) : ITask internal sealed unsafe class Initiate(ElementId elementId, IGameGui gameGui) : ITask
{ {
public override string ToString() => $"InitiateLeve({ElementId})"; public bool Start() => true;
}
internal sealed unsafe class InitiateExecutor(IGameGui gameGui) : TaskExecutor<Initiate> public ETaskResult Update()
{
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 = Task.ElementId.Value } new() { Type = ValueType.UInt, Int = elementId.Value }
}; };
addonJournalDetail->FireCallback(2, pickQuest); addonJournalDetail->FireCallback(2, pickQuest);
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
@ -112,22 +103,21 @@ internal static class InitiateLeve
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
public override string ToString() => $"InitiateLeve({elementId})";
} }
internal sealed class SelectDifficulty : ITask internal sealed unsafe class SelectDifficulty(IGameGui gameGui) : ITask
{ {
public override string ToString() => "SelectLeveDifficulty"; public bool Start() => true;
}
internal sealed unsafe class SelectDifficultyExecutor(IGameGui gameGui) : TaskExecutor<SelectDifficulty> public ETaskResult Update()
{
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 },
@ -139,5 +129,7 @@ internal static class InitiateLeve
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
public override string ToString() => "SelectLeveDifficulty";
} }
} }

View File

@ -5,6 +5,7 @@ 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;
@ -19,7 +20,17 @@ namespace Questionable.Controller.Steps.Shared;
internal static class AethernetShortcut internal static class AethernetShortcut
{ {
internal sealed class Factory(MovementController movementController) internal sealed class Factory(
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)
@ -27,28 +38,24 @@ internal static class AethernetShortcut
if (step.AethernetShortcut == null) if (step.AethernetShortcut == null)
yield break; yield break;
yield return new WaitCondition.Task(() => movementController.IsNavmeshReady, yield return new WaitConditionTask(() => movementController.IsNavmeshReady,
"Wait(navmesh ready)"); "Wait(navmesh ready)");
yield return new Task(step.AethernetShortcut.From, step.AethernetShortcut.To, yield return Use(step.AethernetShortcut.From, step.AethernetShortcut.To,
step.SkipConditions?.AethernetShortcutIf ?? new()); step.SkipConditions?.AethernetShortcutIf);
} }
}
internal sealed record Task( public ITask Use(EAetheryteLocation from, EAetheryteLocation to, SkipAetheryteCondition? skipConditions = null)
EAetheryteLocation From,
EAetheryteLocation To,
SkipAetheryteCondition SkipConditions) : ISkippableTask
{
public Task(EAetheryteLocation from,
EAetheryteLocation to)
: this(from, to, new())
{ {
return new UseAethernetShortcut(from, to, skipConditions ?? new(),
loggerFactory.CreateLogger<UseAethernetShortcut>(), aetheryteFunctions, gameFunctions, questFunctions,
clientState, aetheryteData, territoryData, lifestreamIpc, movementController, condition);
} }
public override string ToString() => $"UseAethernet({From} -> {To})";
} }
internal sealed class UseAethernetShortcut( internal sealed class UseAethernetShortcut(
EAetheryteLocation from,
EAetheryteLocation to,
SkipAetheryteCondition skipConditions,
ILogger<UseAethernetShortcut> logger, ILogger<UseAethernetShortcut> logger,
AetheryteFunctions aetheryteFunctions, AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions, GameFunctions gameFunctions,
@ -58,80 +65,79 @@ internal static class AethernetShortcut
TerritoryData territoryData, TerritoryData territoryData,
LifestreamIpc lifestreamIpc, LifestreamIpc lifestreamIpc,
MovementController movementController, MovementController movementController,
ICondition condition) : TaskExecutor<Task> ICondition condition) : ISkippableTask
{ {
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 => Task.From; public EAetheryteLocation From => from;
public EAetheryteLocation To => Task.To; public EAetheryteLocation To => to;
protected override bool Start() public bool Start()
{ {
if (!Task.SkipConditions.Never) if (!skipConditions.Never)
{ {
if (Task.SkipConditions.InSameTerritory && if (skipConditions.InSameTerritory && clientState.TerritoryType == aetheryteData.TerritoryIds[to])
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 (Task.SkipConditions.InTerritory.Contains(clientState.TerritoryType)) if (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 (Task.SkipConditions.QuestsCompleted.Count > 0 && if (skipConditions.QuestsCompleted.Count > 0 &&
Task.SkipConditions.QuestsCompleted.All(questFunctions.IsQuestComplete)) 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 (Task.SkipConditions.QuestsAccepted.Count > 0 && if (skipConditions.QuestsAccepted.Count > 0 &&
Task.SkipConditions.QuestsAccepted.All(questFunctions.IsQuestAccepted)) 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 (Task.SkipConditions.AetheryteLocked != null && if (skipConditions.AetheryteLocked != null &&
!aetheryteFunctions.IsAetheryteUnlocked(Task.SkipConditions.AetheryteLocked.Value)) !aetheryteFunctions.IsAetheryteUnlocked(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 (Task.SkipConditions.AetheryteUnlocked != null && if (skipConditions.AetheryteUnlocked != null &&
aetheryteFunctions.IsAetheryteUnlocked(Task.SkipConditions.AetheryteUnlocked.Value)) aetheryteFunctions.IsAetheryteUnlocked(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(Task.From) && if (aetheryteFunctions.IsAetheryteUnlocked(from) &&
aetheryteFunctions.IsAetheryteUnlocked(Task.To)) aetheryteFunctions.IsAetheryteUnlocked(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, Task.From) < if (aetheryteData.CalculateDistance(playerPosition, territoryType, from) <
aetheryteData.CalculateDistance(playerPosition, territoryType, Task.To)) aetheryteData.CalculateDistance(playerPosition, territoryType, to))
{ {
if (aetheryteData.CalculateDistance(playerPosition, territoryType, Task.From) < if (aetheryteData.CalculateDistance(playerPosition, territoryType, from) <
(Task.From.IsFirmamentAetheryte() ? 11f : 4f)) (from.IsFirmamentAetheryte() ? 11f : 4f))
{ {
DoTeleport(); DoTeleport();
return true; return true;
} }
else if (Task.From == EAetheryteLocation.SolutionNine) else if (from == EAetheryteLocation.SolutionNine)
{ {
logger.LogInformation("Moving to S9 aetheryte"); logger.LogInformation("Moving to S9 aetheryte");
List<Vector3> nearbyPoints = List<Vector3> nearbyPoints =
@ -144,14 +150,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)Task.From, closestPoint, false, true, movementController.NavigateTo(EMovementType.Quest, (uint)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, Task.From) > 30 && aetheryteData.CalculateDistance(playerPosition, territoryType, from) > 30 &&
!gameFunctions.HasStatusPreventingMount()) !gameFunctions.HasStatusPreventingMount())
{ {
_triedMounting = gameFunctions.Mount(); _triedMounting = gameFunctions.Mount();
@ -170,7 +176,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",
Task.From, Task.To); from, to);
return false; return false;
} }
@ -179,34 +185,34 @@ internal static class AethernetShortcut
{ {
logger.LogInformation("Moving to aethernet shortcut"); logger.LogInformation("Moving to aethernet shortcut");
_moving = true; _moving = true;
float distance = Task.From switch float distance = from switch
{ {
_ when Task.From.IsFirmamentAetheryte() => 4.4f, _ when from.IsFirmamentAetheryte() => 4.4f,
EAetheryteLocation.UldahChamberOfRule => 5f, EAetheryteLocation.UldahChamberOfRule => 5f,
_ when AetheryteConverter.IsLargeAetheryte(Task.From) => 10.9f, _ when AetheryteConverter.IsLargeAetheryte(from) => 10.9f,
_ => 6.9f, _ => 6.9f,
}; };
movementController.NavigateTo(EMovementType.Quest, (uint)Task.From, aetheryteData.Locations[Task.From], movementController.NavigateTo(EMovementType.Quest, (uint)from, aetheryteData.Locations[from],
false, true, false, true,
distance); distance);
} }
private void DoTeleport() private void DoTeleport()
{ {
if (Task.From.IsFirmamentAetheryte()) if (from.IsFirmamentAetheryte())
{ {
logger.LogInformation("Using manual teleport interaction"); logger.LogInformation("Using manual teleport interaction");
_teleported = gameFunctions.InteractWith((uint)Task.From, ObjectKind.EventObj); _teleported = gameFunctions.InteractWith((uint)from, ObjectKind.EventObj);
} }
else else
{ {
logger.LogInformation("Using lifestream to teleport to {Destination}", Task.To); logger.LogInformation("Using lifestream to teleport to {Destination}", to);
lifestreamIpc.Teleport(Task.To); lifestreamIpc.Teleport(to);
_teleported = true; _teleported = true;
} }
} }
public override ETaskResult Update() public ETaskResult Update()
{ {
if (DateTime.Now < _continueAt) if (DateTime.Now < _continueAt)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -241,27 +247,29 @@ internal static class AethernetShortcut
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
if (aetheryteData.IsAirshipLanding(Task.To)) if (aetheryteData.IsAirshipLanding(to))
{ {
if (aetheryteData.CalculateAirshipLandingDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero, if (aetheryteData.CalculateAirshipLandingDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero,
clientState.TerritoryType, Task.To) > 5) clientState.TerritoryType, to) > 5)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
else if (aetheryteData.IsCityAetheryte(Task.To)) else if (aetheryteData.IsCityAetheryte(to))
{ {
if (aetheryteData.CalculateDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero, if (aetheryteData.CalculateDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero,
clientState.TerritoryType, Task.To) > 20) clientState.TerritoryType, 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[Task.To]) if (clientState.TerritoryType != aetheryteData.TerritoryIds[to])
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
public override string ToString() => $"UseAethernet({from} -> {to})";
} }
} }

View File

@ -3,7 +3,10 @@ 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;
@ -15,43 +18,52 @@ namespace Questionable.Controller.Steps.Shared;
internal static class AetheryteShortcut internal static class AetheryteShortcut
{ {
internal sealed class Factory(AetheryteData aetheryteData) : ITaskFactory internal sealed class Factory(
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 new Task(step, quest.Id, step.AetheryteShortcut.Value, yield return Use(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>
internal sealed record Task( private sealed class UseAetheryteShortcut(
QuestStep? Step, QuestStep? step,
ElementId? ElementId, ElementId? elementId,
EAetheryteLocation TargetAetheryte, EAetheryteLocation targetAetheryte,
ushort ExpectedTerritoryId) : ISkippableTask ushort expectedTerritoryId,
{
public override string ToString() => $"UseAetheryte({TargetAetheryte})";
}
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) : TaskExecutor<Task> AetheryteData aetheryteData) : ISkippableTask
{ {
private bool _teleported; private bool _teleported;
private DateTime _continueAt; private DateTime _continueAt;
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;
@ -62,7 +74,7 @@ internal static class AetheryteShortcut
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
if (clientState.TerritoryType == Task.ExpectedTerritoryId) if (clientState.TerritoryType == expectedTerritoryId)
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -71,9 +83,9 @@ internal static class AetheryteShortcut
private bool ShouldSkipTeleport() private bool ShouldSkipTeleport()
{ {
ushort territoryType = clientState.TerritoryType; ushort territoryType = clientState.TerritoryType;
if (Task.Step != null) if (step != null)
{ {
var skipConditions = Task.Step.SkipConditions?.AetheryteShortcutIf ?? new(); var skipConditions = step.SkipConditions?.AetheryteShortcutIf ?? new();
if (skipConditions is { Never: false }) if (skipConditions is { Never: false })
{ {
if (skipConditions.InTerritory.Contains(territoryType)) if (skipConditions.InTerritory.Contains(territoryType))
@ -110,12 +122,12 @@ internal static class AetheryteShortcut
return true; return true;
} }
if (Task.ElementId != null) if (elementId != null)
{ {
QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(Task.ElementId); QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(elementId);
if (skipConditions.RequiredQuestVariablesNotMet && if (skipConditions.RequiredQuestVariablesNotMet &&
questWork != null && questWork != null &&
!QuestWorkUtils.MatchesRequiredQuestWorkConfig(Task.Step.RequiredQuestVariables, questWork, !QuestWorkUtils.MatchesRequiredQuestWorkConfig(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");
@ -136,7 +148,7 @@ internal static class AetheryteShortcut
} }
} }
if (Task.ExpectedTerritoryId == territoryType) if (expectedTerritoryId == territoryType)
{ {
if (!skipConditions.Never) if (!skipConditions.Never)
{ {
@ -147,19 +159,17 @@ internal static class AetheryteShortcut
} }
Vector3 pos = clientState.LocalPlayer!.Position; Vector3 pos = clientState.LocalPlayer!.Position;
if (Task.Step.Position != null && if (step.Position != null &&
(pos - Task.Step.Position.Value).Length() < Task.Step.CalculateActualStopDistance()) (pos - step.Position.Value).Length() < 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, Task.TargetAetheryte) < 20 || if (aetheryteData.CalculateDistance(pos, territoryType, targetAetheryte) < 20 ||
(Task.Step.AethernetShortcut != null && (step.AethernetShortcut != null &&
(aetheryteData.CalculateDistance(pos, territoryType, Task.Step.AethernetShortcut.From) < (aetheryteData.CalculateDistance(pos, territoryType, step.AethernetShortcut.From) < 20 ||
20 || aetheryteData.CalculateDistance(pos, territoryType, step.AethernetShortcut.To) < 20)))
aetheryteData.CalculateDistance(pos, territoryType, Task.Step.AethernetShortcut.To) <
20)))
{ {
logger.LogInformation("Skipping aetheryte teleport"); logger.LogInformation("Skipping aetheryte teleport");
return true; return true;
@ -173,7 +183,7 @@ internal static class AetheryteShortcut
private bool DoTeleport() private bool DoTeleport()
{ {
if (!aetheryteFunctions.CanTeleport(Task.TargetAetheryte)) if (!aetheryteFunctions.CanTeleport(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.");
@ -185,16 +195,12 @@ internal static class AetheryteShortcut
_continueAt = DateTime.Now.AddSeconds(8); _continueAt = DateTime.Now.AddSeconds(8);
if (!aetheryteFunctions.IsAetheryteUnlocked(Task.TargetAetheryte)) if (!aetheryteFunctions.IsAetheryteUnlocked(targetAetheryte))
{ {
chatGui.PrintError($"[Questionable] Aetheryte {Task.TargetAetheryte} is not unlocked."); chatGui.PrintError($"[Questionable] Aetheryte {targetAetheryte} is not unlocked.");
throw new TaskException("Aetheryte is not unlocked"); throw new TaskException("Aetheryte is not unlocked");
} }
else if (aetheryteFunctions.TeleportAetheryte(targetAetheryte))
ProgressContext =
InteractionProgressContext.FromActionUseOrDefault(() =>
aetheryteFunctions.TeleportAetheryte(Task.TargetAetheryte));
if (ProgressContext != null)
{ {
logger.LogInformation("Travelling via aetheryte..."); logger.LogInformation("Travelling via aetheryte...");
return true; return true;
@ -205,5 +211,7 @@ internal static class AetheryteShortcut
throw new TaskException("Unable to teleport to aetheryte"); throw new TaskException("Unable to teleport to aetheryte");
} }
} }
public override string ToString() => $"UseAetheryte({targetAetheryte})";
} }
} }

View File

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

View File

@ -21,6 +21,7 @@ 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,
@ -52,7 +53,7 @@ internal static class Gather
if (classJob != currentClassJob) if (classJob != currentClassJob)
{ {
yield return new SwitchClassJob.Task(classJob); yield return new SwitchClassJob(classJob, clientState);
} }
if (HasRequiredItems(itemToGather)) if (HasRequiredItems(itemToGather))
@ -70,20 +71,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 new SkipMarker(); yield return CreateSkipMarkerTask();
else else
yield return task; yield return task;
} }
} }
ushort territoryId = gatheringRoot.Steps.Last().TerritoryId; ushort territoryId = gatheringRoot.Steps.Last().TerritoryId;
yield return new WaitCondition.Task(() => clientState.TerritoryType == territoryId, yield return new WaitConditionTask(() => clientState.TerritoryType == territoryId,
$"Wait(territory: {territoryData.GetNameAndId(territoryId)})"); $"Wait(territory: {territoryData.GetNameAndId(territoryId)})");
yield return new WaitCondition.Task(() => movementController.IsNavmeshReady, yield return new WaitConditionTask(() => movementController.IsNavmeshReady,
"Wait(navmesh ready)"); "Wait(navmesh ready)");
yield return new GatheringTask(gatheringPointId, itemToGather); yield return CreateStartGatheringTask(gatheringPointId, itemToGather);
yield return new WaitAtEnd.WaitDelay(); yield return new WaitAtEnd.WaitDelay();
} }
} }
@ -108,12 +109,38 @@ 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();
}
} }
internal sealed record GatheringTask( private sealed class StartGathering(
GatheringPointId gatheringPointId, GatheringPointId gatheringPointId,
GatheredItem gatheredItem) : ITask GatheredItem gatheredItem,
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)
@ -124,35 +151,13 @@ 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,6 +11,7 @@ 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;
@ -27,34 +28,60 @@ 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)
{ {
if (step.Position != null) if (step.Position != null)
{ {
return CreateMoveTasks(step, step.Position.Value); return CreateMountTasks(quest.Id, step, step.Position.Value);
} }
else if (step is { DataId: not null, StopDistance: not null }) else if (step is { DataId: not null, StopDistance: not null })
{ {
return [new WaitForNearDataId(step.DataId.Value, step.StopDistance.Value)]; return [ExpectToBeNearDataId(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 })
{ {
return CreateMoveTasks(step, aetheryteData.Locations[step.Aetheryte.Value]); return CreateMountTasks(quest.Id, step, aetheryteData.Locations[step.Aetheryte.Value]);
} }
else if (step is { InteractionType: EInteractionType.AttuneAethernetShard, AethernetShard: not null }) else if (step is { InteractionType: EInteractionType.AttuneAethernetShard, AethernetShard: not null })
{ {
return CreateMoveTasks(step, aetheryteData.Locations[step.AethernetShard.Value]); return CreateMountTasks(quest.Id, step, aetheryteData.Locations[step.AethernetShard.Value]);
} }
return []; return [];
} }
private IEnumerable<ITask> CreateMoveTasks(QuestStep step, Vector3 destination) public ITask Move(QuestStep step, Vector3 destination)
{
return Move(new MoveParams(step, destination));
}
public ITask Move(MoveParams moveParams)
{
return new MoveInternal(moveParams, movementController, mountFactory, gameFunctions,
loggerFactory.CreateLogger<MoveInternal>(), clientState, dataManager);
}
public ITask Land()
{
return new LandTask(clientState, condition, loggerFactory.CreateLogger<LandTask>());
}
public ITask ExpectToBeNearDataId(uint dataId, float stopDistance)
{
return new WaitForNearDataId(dataId, stopDistance, gameFunctions, clientState);
}
public IEnumerable<ITask> CreateMountTasks(ElementId questId, QuestStep step, Vector3 destination)
{ {
if (step.InteractionType == EInteractionType.Jump && step.JumpDestination != null && if (step.InteractionType == EInteractionType.Jump && step.JumpDestination != null &&
(clientState.LocalPlayer!.Position - step.JumpDestination.Position).Length() <= (clientState.LocalPlayer!.Position - step.JumpDestination.Position).Length() <=
@ -64,161 +91,151 @@ internal static class MoveTo
yield break; yield break;
} }
yield return new WaitCondition.Task(() => clientState.TerritoryType == step.TerritoryId, yield return new WaitConditionTask(() => 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 WaitCondition.Task(() => movementController.IsNavmeshReady, yield return new WaitConditionTask(() => movementController.IsNavmeshReady,
"Wait(navmesh ready)"); "Wait(navmesh ready)");
yield return Move(step, destination);
}
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 new LandTask(); yield return Land();
} }
} }
internal sealed class MoveExecutor : TaskExecutor<MoveTask>, IToastAware private sealed class MoveInternal : ITask, 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<MoveExecutor> _logger; private readonly ILogger<MoveInternal> _logger;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly Mount.MountExecutor _mountExecutor;
private readonly Mount.UnmountExecutor _unmountExecutor;
private Action? _startAction; private readonly Action _startAction;
private Vector3 _destination; private readonly Vector3 _destination;
private readonly MoveParams _moveParams;
private bool _canRestart; private bool _canRestart;
private ITaskExecutor? _nestedExecutor; private ITask? _mountTask;
public MoveExecutor( public MoveInternal(MoveParams moveParams,
MovementController movementController, MovementController movementController,
Mount.Factory mountFactory,
GameFunctions gameFunctions, GameFunctions gameFunctions,
ILogger<MoveExecutor> logger, ILogger<MoveInternal> 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)!;
}
private void PrepareMovementIfNeeded() _destination = moveParams.Destination;
{
if (!_gameFunctions.IsFlyingUnlocked(Task.TerritoryId)) if (!gameFunctions.IsFlyingUnlocked(moveParams.TerritoryId))
{ {
Task = Task with { Fly = false, Land = false }; moveParams = moveParams with { Fly = false, Land = false };
} }
if (!Task.DisableNavmesh) if (!moveParams.DisableNavMesh)
{ {
_startAction = () => _startAction = () =>
_movementController.NavigateTo(EMovementType.Quest, Task.DataId, _destination, _movementController.NavigateTo(EMovementType.Quest, moveParams.DataId, _destination,
fly: Task.Fly, fly: moveParams.Fly,
sprint: Task.Sprint, sprint: moveParams.Sprint,
stopDistance: Task.StopDistance, stopDistance: moveParams.StopDistance,
ignoreDistanceToObject: Task.IgnoreDistanceToObject, ignoreDistanceToObject: moveParams.IgnoreDistanceToObject,
land: Task.Land); land: moveParams.Land);
} }
else else
{ {
_startAction = () => _startAction = () =>
_movementController.NavigateTo(EMovementType.Quest, Task.DataId, [_destination], _movementController.NavigateTo(EMovementType.Quest, moveParams.DataId, [_destination],
fly: Task.Fly, fly: moveParams.Fly,
sprint: Task.Sprint, sprint: moveParams.Sprint,
stopDistance: Task.StopDistance, stopDistance: moveParams.StopDistance,
ignoreDistanceToObject: Task.IgnoreDistanceToObject, ignoreDistanceToObject: moveParams.IgnoreDistanceToObject,
land: Task.Land); land: moveParams.Land);
} }
_moveParams = moveParams;
_canRestart = moveParams.RestartNavigation;
} }
protected override bool Start() public bool ShouldRedoOnInterrupt() => true;
public bool Start()
{ {
_canRestart = Task.RestartNavigation; float stopDistance = _moveParams.StopDistance ?? QuestStep.DefaultStopDistance;
_destination = Task.Destination;
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);
bool requiresMovement = actualDistance > stopDistance;
if (requiresMovement)
PrepareMovementIfNeeded();
// might be able to make this optional if (_moveParams.Mount == true)
if (Task.Mount == true)
{ {
var mountTask = new Mount.MountTask(Task.TerritoryId, Mount.EMountIf.Always); var mountTask = _mountFactory.Mount(_moveParams.TerritoryId, Mount.EMountIf.Always);
if (_mountExecutor.Start(mountTask)) if (mountTask.Start())
{ {
_nestedExecutor = _mountExecutor; _mountTask = mountTask;
return true; return true;
} }
} }
else if (Task.Mount == false) else if (_moveParams.Mount == false)
{ {
var mountTask = new Mount.UnmountTask(); var mountTask = _mountFactory.Unmount();
if (_unmountExecutor.Start(mountTask)) if (mountTask.Start())
{ {
_nestedExecutor = _unmountExecutor; _mountTask = mountTask;
return true; return true;
} }
} }
if (!Task.DisableNavmesh) if (!_moveParams.DisableNavMesh)
{ {
if (Task.Mount == null) if (_moveParams.Mount == null)
{ {
Mount.EMountIf mountIf = Mount.EMountIf mountIf =
actualDistance > stopDistance && Task.Fly && actualDistance > stopDistance && _moveParams.Fly &&
_gameFunctions.IsFlyingUnlocked(Task.TerritoryId) _gameFunctions.IsFlyingUnlocked(_moveParams.TerritoryId)
? Mount.EMountIf.Always ? Mount.EMountIf.Always
: Mount.EMountIf.AwayFromPosition; : Mount.EMountIf.AwayFromPosition;
var mountTask = new Mount.MountTask(Task.TerritoryId, mountIf, _destination); var mountTask = _mountFactory.Mount(_moveParams.TerritoryId, mountIf, _destination);
if (_mountExecutor.Start(mountTask)) if (mountTask.Start())
{ {
_nestedExecutor = _mountExecutor; _mountTask = mountTask;
return true; return true;
} }
} }
} }
_nestedExecutor = new NoOpTaskExecutor(); _mountTask = new NoOpTask();
return true; return true;
} }
public override ETaskResult Update() public ETaskResult Update()
{ {
if (_nestedExecutor != null) if (_mountTask != null)
{ {
if (_nestedExecutor.Update() == ETaskResult.TaskComplete) if (_mountTask.Update() == ETaskResult.TaskComplete)
{ {
_nestedExecutor = null; _mountTask = null;
if (_startAction != null)
{ _logger.LogInformation("Moving to {Destination}", _destination.ToString("G", CultureInfo.InvariantCulture));
_logger.LogInformation("Moving to {Destination}", _startAction();
_destination.ToString("G", CultureInfo.InvariantCulture));
_startAction();
}
else
return ETaskResult.TaskComplete;
} }
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
if (_startAction == null)
return ETaskResult.TaskComplete;
if (_movementController.IsPathfinding || _movementController.IsPathRunning) if (_movementController.IsPathfinding || _movementController.IsPathRunning)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -228,10 +245,10 @@ internal static class MoveTo
if (_canRestart && if (_canRestart &&
Vector3.Distance(_clientState.LocalPlayer!.Position, _destination) > Vector3.Distance(_clientState.LocalPlayer!.Position, _destination) >
(Task.StopDistance ?? QuestStep.DefaultStopDistance) + 5f) (_moveParams.StopDistance ?? QuestStep.DefaultStopDistance) + 5f)
{ {
_canRestart = false; _canRestart = false;
if (_clientState.TerritoryType == Task.TerritoryId) if (_clientState.TerritoryType == _moveParams.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();
@ -245,6 +262,7 @@ 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)
{ {
@ -255,28 +273,27 @@ internal static class MoveTo
} }
} }
private sealed class NoOpTaskExecutor : TaskExecutor<ITask> private sealed class NoOpTask : ITask
{ {
protected override bool Start() => true; public bool Start() => true;
public override ETaskResult Update() => ETaskResult.TaskComplete; public ETaskResult Update() => ETaskResult.TaskComplete;
} }
internal sealed record MoveTask( internal sealed record MoveParams(
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)
EInteractionType InteractionType = EInteractionType.None) : ITask
{ {
public MoveTask(QuestStep step, Vector3 destination) public MoveParams(QuestStep step, Vector3 destination)
: this(step.TerritoryId, : this(step.TerritoryId,
destination, destination,
step.Mount, step.Mount,
@ -287,31 +304,26 @@ internal static class MoveTo
step.Fly == true, step.Fly == true,
step.Land == true, step.Land == true,
step.IgnoreDistanceToObject == true, step.IgnoreDistanceToObject == true,
step.RestartNavigationIfCancelled != false, step.RestartNavigationIfCancelled != false)
step.InteractionType)
{ {
} }
public override string ToString() => $"MoveTo({Destination.ToString("G", CultureInfo.InvariantCulture)})";
} }
internal sealed record WaitForNearDataId(uint DataId, float StopDistance) : ITask private sealed class WaitForNearDataId(
uint dataId,
float stopDistance,
GameFunctions gameFunctions,
IClientState clientState) : ITask
{ {
public bool ShouldRedoOnInterrupt() => true; public bool ShouldRedoOnInterrupt() => true;
}
internal sealed class WaitForNearDataIdExecutor( public bool Start() => true;
GameFunctions gameFunctions,
IClientState clientState) : TaskExecutor<WaitForNearDataId>
{
protected override bool Start() => true; public ETaskResult Update()
public override ETaskResult Update()
{ {
IGameObject? gameObject = gameFunctions.FindObjectByDataId(Task.DataId); IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId);
if (gameObject == null || if (gameObject == null ||
(gameObject.Position - clientState.LocalPlayer!.Position).Length() > Task.StopDistance) (gameObject.Position - clientState.LocalPlayer!.Position).Length() > 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");
} }
@ -320,17 +332,14 @@ internal static class MoveTo
} }
} }
internal sealed class LandTask : ITask private sealed class LandTask(IClientState clientState, ICondition condition, ILogger<LandTask> logger) : 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;
protected override bool Start() public bool ShouldRedoOnInterrupt() => true;
public bool Start()
{ {
if (!condition[ConditionFlag.InFlight]) if (!condition[ConditionFlag.InFlight])
{ {
@ -343,7 +352,7 @@ internal static class MoveTo
return true; return true;
} }
public override ETaskResult Update() public ETaskResult Update()
{ {
if (DateTime.Now < _continueAt) if (DateTime.Now < _continueAt)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;

View File

@ -1,9 +1,14 @@
using System.Linq; using System;
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;
@ -15,7 +20,12 @@ namespace Questionable.Controller.Steps.Shared;
internal static class SkipCondition internal static class SkipCondition
{ {
internal sealed class Factory : SimpleTaskFactory internal sealed class Factory(
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)
{ {
@ -30,31 +40,28 @@ internal static class SkipCondition
step.NextQuestId == null) step.NextQuestId == null)
return null; return null;
return new SkipTask(step, skipConditions ?? new(), quest.Id); return Check(step, skipConditions, quest.Id);
}
private CheckSkip Check(QuestStep step, SkipStepConditions? skipConditions, ElementId questId)
{
return new CheckSkip(step, skipConditions ?? new(), questId, loggerFactory.CreateLogger<CheckSkip>(),
aetheryteFunctions, gameFunctions, questFunctions, clientState);
} }
} }
internal sealed record SkipTask( private sealed class CheckSkip(
QuestStep Step, QuestStep step,
SkipStepConditions SkipConditions, SkipStepConditions skipConditions,
ElementId ElementId) : ITask ElementId elementId,
{
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) : TaskExecutor<SkipTask> IClientState clientState) : ITask
{ {
protected override unsafe bool Start() public 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 &&
@ -197,8 +204,7 @@ internal static class SkipCondition
} }
} }
if (skipConditions.NearPosition is { } nearPosition && if (skipConditions.NearPosition is { } nearPosition && clientState.TerritoryType == nearPosition.TerritoryId)
clientState.TerritoryType == nearPosition.TerritoryId)
{ {
if (Vector3.Distance(nearPosition.Position, clientState.LocalPlayer!.Position) <= if (Vector3.Distance(nearPosition.Position, clientState.LocalPlayer!.Position) <=
nearPosition.MaximumDistance) nearPosition.MaximumDistance)
@ -245,6 +251,8 @@ internal static class SkipCondition
return false; return false;
} }
public override ETaskResult Update() => ETaskResult.SkipRemainingTasksForStep; public ETaskResult Update() => ETaskResult.SkipRemainingTasksForStep;
public override string ToString() => "CheckSkip";
} }
} }

View File

@ -6,30 +6,27 @@ namespace Questionable.Controller.Steps.Shared;
internal static class StepDisabled internal static class StepDisabled
{ {
internal sealed class Factory : SimpleTaskFactory internal sealed class Factory(ILoggerFactory loggerFactory) : 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 SkipRemainingTasks(); return new Task(loggerFactory.CreateLogger<Task>());
} }
} }
internal sealed class SkipRemainingTasks : ITask internal sealed class Task(ILogger<Task> logger) : ITask
{ {
public override string ToString() => "StepDisabled"; public bool Start() => true;
}
internal sealed class SkipDisabledStepsExecutor(ILogger<SkipRemainingTasks> logger) : TaskExecutor<SkipRemainingTasks> public ETaskResult Update()
{
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,37 +6,31 @@ using Questionable.Controller.Steps.Common;
namespace Questionable.Controller.Steps.Shared; namespace Questionable.Controller.Steps.Shared;
internal static class SwitchClassJob internal sealed class SwitchClassJob(EClassJob classJob, IClientState clientState) : AbstractDelayedTask
{ {
internal sealed record Task(EClassJob ClassJob) : ITask protected override unsafe bool StartInternal()
{ {
public override string ToString() => $"SwitchJob({ClassJob})"; if (clientState.LocalPlayer!.ClassJob.Id == (uint)classJob)
} return false;
internal sealed class SwitchClassJobExecutor(IClientState clientState) : AbstractDelayedTaskExecutor<Task> var gearsetModule = RaptureGearsetModule.Instance();
{ if (gearsetModule != null)
protected override unsafe bool StartInternal()
{ {
if (clientState.LocalPlayer!.ClassJob.Id == (uint)Task.ClassJob) for (int i = 0; i < 100; ++i)
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)
{ {
var gearset = gearsetModule->GetGearset(i); gearsetModule->EquipGearset(gearset->Id);
if (gearset->ClassJob == (byte)Task.ClassJob) return true;
{
gearsetModule->EquipGearset(gearset->Id);
return true;
}
} }
} }
throw new TaskException($"No gearset found for {Task.ClassJob}");
} }
protected override ETaskResult UpdateInternal() => ETaskResult.TaskComplete; throw new TaskException($"No gearset found for {classJob}");
} }
protected override ETaskResult UpdateInternal() => ETaskResult.TaskComplete;
public override string ToString() => $"SwitchJob({classJob})";
} }

View File

@ -19,7 +19,9 @@ 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)
@ -27,7 +29,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); var task = new WaitForCompletionFlags((QuestId)quest.Id, step, questFunctions);
var delay = new WaitDelay(); var delay = new WaitDelay();
return [task, delay, Next(quest, sequence)]; return [task, delay, Next(quest, sequence)];
} }
@ -36,7 +38,7 @@ internal static class WaitAtEnd
{ {
case EInteractionType.Combat: case EInteractionType.Combat:
var notInCombat = var notInCombat =
new WaitCondition.Task(() => !condition[ConditionFlag.InCombat], "Wait(not in combat)"); new WaitConditionTask(() => !condition[ConditionFlag.InCombat], "Wait(not in combat)");
return return
[ [
new WaitDelay(), new WaitDelay(),
@ -65,7 +67,8 @@ 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)
]; ];
@ -76,14 +79,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 WaitCondition.Task( waitInteraction = new WaitConditionTask(
() => 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 WaitCondition.Task(() => waitInteraction = new WaitConditionTask(() =>
{ {
Vector3? currentPosition = clientState.LocalPlayer?.Position; Vector3? currentPosition = clientState.LocalPlayer?.Position;
if (currentPosition == null) if (currentPosition == null)
@ -106,7 +109,7 @@ internal static class WaitAtEnd
case EInteractionType.AcceptQuest: case EInteractionType.AcceptQuest:
{ {
var accept = new WaitQuestAccepted(step.PickUpQuestId ?? quest.Id); var accept = new WaitQuestAccepted(step.PickUpQuestId ?? quest.Id, questFunctions);
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)];
@ -116,7 +119,7 @@ internal static class WaitAtEnd
case EInteractionType.CompleteQuest: case EInteractionType.CompleteQuest:
{ {
var complete = new WaitQuestCompleted(step.TurnInQuestId ?? quest.Id); var complete = new WaitQuestCompleted(step.TurnInQuestId ?? quest.Id, questFunctions);
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)];
@ -136,135 +139,103 @@ internal static class WaitAtEnd
} }
} }
internal sealed record WaitDelay(TimeSpan Delay) : ITask internal sealed class WaitDelay(TimeSpan? delay = null) : AbstractDelayedTask(delay ?? TimeSpan.FromSeconds(1))
{ {
public WaitDelay() protected override bool StartInternal() => true;
: this(TimeSpan.FromSeconds(1))
{
}
public bool ShouldRedoOnInterrupt() => true;
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 WaitNextStepOrSequenceExecutor : TaskExecutor<WaitNextStepOrSequence> internal sealed class WaitForCompletionFlags(QuestId quest, QuestStep step, QuestFunctions questFunctions) : ITask
{ {
protected override bool Start() => true; public bool Start() => true;
public override ETaskResult Update() => ETaskResult.StillRunning; public ETaskResult Update()
}
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(Task.Quest); QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(quest);
return questWork != null && return questWork != null &&
QuestWorkUtils.MatchesQuestWork(Task.Step.CompletionQuestVariablesFlags, questWork) QuestWorkUtils.MatchesQuestWork(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() =>
$"WaitObj({DataId} at {Destination.ToString("G", CultureInfo.InvariantCulture)} < {Distance})"; $"Wait(QW: {string.Join(", ", step.CompletionQuestVariablesFlags.Select(x => x?.ToString() ?? "-"))})";
} }
internal sealed class WaitObjectAtPositionExecutor(GameFunctions gameFunctions) : TaskExecutor<WaitObjectAtPosition> private sealed class WaitObjectAtPosition(
uint dataId,
Vector3 destination,
float distance,
GameFunctions gameFunctions) : ITask
{ {
protected override bool Start() => true; public bool Start() => true;
public override ETaskResult Update() => public ETaskResult Update() =>
gameFunctions.IsObjectAtPosition(Task.DataId, Task.Destination, Task.Distance) gameFunctions.IsObjectAtPosition(dataId, destination, distance)
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : ETaskResult.StillRunning;
public override string ToString() =>
$"WaitObj({dataId} at {destination.ToString("G", CultureInfo.InvariantCulture)} < {distance})";
} }
internal sealed record WaitQuestAccepted(ElementId ElementId) : ITask internal sealed class WaitQuestAccepted(ElementId elementId, QuestFunctions questFunctions) : ITask
{ {
public override string ToString() => $"WaitQuestAccepted({ElementId})"; public bool Start() => true;
}
internal sealed class WaitQuestAcceptedExecutor(QuestFunctions questFunctions) : TaskExecutor<WaitQuestAccepted> public ETaskResult Update()
{
protected override bool Start() => true;
public override ETaskResult Update()
{ {
return questFunctions.IsQuestAccepted(Task.ElementId) return questFunctions.IsQuestAccepted(elementId)
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : ETaskResult.StillRunning;
} }
public override string ToString() => $"WaitQuestAccepted({elementId})";
} }
internal sealed record WaitQuestCompleted(ElementId ElementId) : ITask internal sealed class WaitQuestCompleted(ElementId elementId, QuestFunctions questFunctions) : ITask
{ {
public override string ToString() => $"WaitQuestComplete({ElementId})"; public bool Start() => true;
}
internal sealed class WaitQuestCompletedExecutor(QuestFunctions questFunctions) : TaskExecutor<WaitQuestCompleted> public ETaskResult Update()
{
protected override bool Start() => true;
public override ETaskResult Update()
{ {
return questFunctions.IsQuestComplete(Task.ElementId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning; return questFunctions.IsQuestComplete(elementId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
} }
public override string ToString() => $"WaitQuestComplete({elementId})";
} }
internal sealed record NextStep(ElementId ElementId, int Sequence) : ILastTask internal sealed class 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,19 +18,10 @@ internal static class WaitAtStart
} }
} }
internal sealed class WaitDelay(TimeSpan delay) : AbstractDelayedTask(delay)
internal sealed record WaitDelay(TimeSpan Delay) : ITask
{ {
protected override bool StartInternal() => true;
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

@ -1,51 +0,0 @@
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

@ -6,12 +6,12 @@ namespace Questionable.Controller.Steps;
internal sealed class TaskQueue internal sealed class TaskQueue
{ {
private readonly List<ITask> _completedTasks = [];
private readonly List<ITask> _tasks = []; private readonly List<ITask> _tasks = [];
public ITaskExecutor? CurrentTaskExecutor { get; set; } private int _currentTaskIndex;
public ITask? CurrentTask { get; set; }
public IEnumerable<ITask> RemainingTasks => _tasks; public IEnumerable<ITask> RemainingTasks => _tasks.Skip(_currentTaskIndex);
public bool AllTasksComplete => CurrentTaskExecutor == null && _tasks.Count == 0; public bool AllTasksComplete => CurrentTask == null && _currentTaskIndex >= _tasks.Count;
public void Enqueue(ITask task) public void Enqueue(ITask task)
{ {
@ -20,40 +20,48 @@ internal sealed class TaskQueue
public bool TryDequeue([NotNullWhen(true)] out ITask? task) public bool TryDequeue([NotNullWhen(true)] out ITask? task)
{ {
task = _tasks.FirstOrDefault(); if (_currentTaskIndex >= _tasks.Count)
if (task == null) {
task = null;
return false; return false;
}
task = _tasks[_currentTaskIndex];
if (task.ShouldRedoOnInterrupt()) if (task.ShouldRedoOnInterrupt())
_completedTasks.Add(task); _currentTaskIndex++;
else
_tasks.RemoveAt(0); _tasks.RemoveAt(0);
return true; return true;
} }
public bool TryPeek([NotNullWhen(true)] out ITask? task) public bool TryPeek([NotNullWhen(true)] out ITask? task)
{ {
task = _tasks.FirstOrDefault(); if (_currentTaskIndex >= _tasks.Count)
return task != null; {
task = null;
return false;
}
task = _tasks[_currentTaskIndex];
return true;
} }
public void Reset() public void Reset()
{ {
_tasks.Clear(); _tasks.Clear();
_completedTasks.Clear(); _currentTaskIndex = 0;
CurrentTaskExecutor = null; CurrentTask = null;
} }
public void InterruptWith(List<ITask> interruptionTasks) public void InterruptWith(List<ITask> interruptionTasks)
{ {
List<ITask?> newTasks = if (CurrentTask != null)
[ {
..interruptionTasks, _tasks.Insert(0, CurrentTask);
.._completedTasks.Where(x => !ReferenceEquals(x, CurrentTaskExecutor?.CurrentTask)).ToList(), CurrentTask = null;
CurrentTaskExecutor?.CurrentTask, _currentTaskIndex = 0;
.._tasks }
];
Reset(); _tasks.InsertRange(0, interruptionTasks);
_tasks.AddRange(newTasks.Where(x => x != null).Cast<ITask>());
} }
} }

View File

@ -63,7 +63,7 @@ internal static class QuestWorkUtils
{ {
if (requiredQuestVariables.Count != 6 || requiredQuestVariables.All(x => x == null || x.Count == 0)) if (requiredQuestVariables.Count != 6 || requiredQuestVariables.All(x => x == null || x.Count == 0))
{ {
logger.LogDebug("No RequiredQW defined"); logger.LogDebug("No RQW defined");
return true; return true;
} }
@ -71,7 +71,7 @@ internal static class QuestWorkUtils
{ {
if (requiredQuestVariables[i] == null) if (requiredQuestVariables[i] == null)
{ {
logger.LogDebug("No RequiredQW {Index} defined", i); logger.LogInformation("No RQW {Index} defined", i);
continue; continue;
} }

View File

@ -56,7 +56,6 @@ 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();
@ -129,81 +128,44 @@ public sealed class QuestionablePlugin : IDalamudPlugin
private static void AddTaskFactories(ServiceCollection serviceCollection) private static void AddTaskFactories(ServiceCollection serviceCollection)
{ {
// individual tasks // individual tasks
serviceCollection.AddTaskExecutor<MoveToLandingLocation.Task, MoveToLandingLocation.MoveToLandingLocationExecutor>(); serviceCollection.AddTransient<MoveToLandingLocation>();
serviceCollection.AddTaskExecutor<DoGather.Task, DoGather.GatherExecutor>(); serviceCollection.AddTransient<DoGather>();
serviceCollection.AddTaskExecutor<DoGatherCollectable.Task, DoGatherCollectable.GatherCollectableExecutor>(); serviceCollection.AddTransient<DoGatherCollectable>();
serviceCollection.AddTaskExecutor<SwitchClassJob.Task, SwitchClassJob.SwitchClassJobExecutor>(); serviceCollection.AddTransient<SwitchClassJob>();
serviceCollection.AddTaskExecutor<Mount.MountTask, Mount.MountExecutor>(); serviceCollection.AddSingleton<Mount.Factory>();
serviceCollection.AddTaskExecutor<Mount.UnmountTask, Mount.UnmountExecutor>();
// task factories // task factories
serviceCollection serviceCollection.AddTaskFactory<StepDisabled.Factory>();
.AddTaskFactoryAndExecutor<StepDisabled.SkipRemainingTasks, StepDisabled.Factory, StepDisabled.SkipDisabledStepsExecutor>();
serviceCollection.AddTaskFactory<EquipRecommended.BeforeDutyOrInstance>(); serviceCollection.AddTaskFactory<EquipRecommended.BeforeDutyOrInstance>();
serviceCollection.AddTaskFactoryAndExecutor<Gather.GatheringTask, Gather.Factory, Gather.StartGathering>(); serviceCollection.AddTaskFactory<Gather.Factory>();
serviceCollection.AddTaskExecutor<Gather.SkipMarker, Gather.DoSkip>(); serviceCollection.AddTaskFactory<AetheryteShortcut.Factory>();
serviceCollection serviceCollection.AddTaskFactory<SkipCondition.Factory>();
.AddTaskFactoryAndExecutor<AetheryteShortcut.Task, AetheryteShortcut.Factory, serviceCollection.AddTaskFactory<AethernetShortcut.Factory>();
AetheryteShortcut.UseAetheryteShortcut>(); serviceCollection.AddTaskFactory<WaitAtStart.Factory>();
serviceCollection serviceCollection.AddTaskFactory<MoveTo.Factory>();
.AddTaskFactoryAndExecutor<SkipCondition.SkipTask, SkipCondition.Factory, SkipCondition.CheckSkip>();
serviceCollection
.AddTaskFactoryAndExecutor<AethernetShortcut.Task, AethernetShortcut.Factory,
AethernetShortcut.UseAethernetShortcut>();
serviceCollection
.AddTaskFactoryAndExecutor<WaitAtStart.WaitDelay, WaitAtStart.Factory, WaitAtStart.WaitDelayExecutor>();
serviceCollection.AddTaskFactoryAndExecutor<MoveTo.MoveTask, MoveTo.Factory, MoveTo.MoveExecutor>();
serviceCollection.AddTaskExecutor<MoveTo.WaitForNearDataId, MoveTo.WaitForNearDataIdExecutor>();
serviceCollection.AddTaskExecutor<MoveTo.LandTask, MoveTo.LandExecutor>();
serviceCollection.AddTaskFactoryAndExecutor<NextQuest.SetQuestTask, NextQuest.Factory, NextQuest.NextQuestExecutor>(); serviceCollection.AddTaskFactory<NextQuest.Factory>();
serviceCollection serviceCollection.AddTaskFactory<AetherCurrent.Factory>();
.AddTaskFactoryAndExecutor<AetherCurrent.Attune, AetherCurrent.Factory, AetherCurrent.DoAttune>(); serviceCollection.AddTaskFactory<AethernetShard.Factory>();
serviceCollection serviceCollection.AddTaskFactory<Aetheryte.Factory>();
.AddTaskFactoryAndExecutor<AethernetShard.Attune, AethernetShard.Factory, AethernetShard.DoAttune>(); serviceCollection.AddTaskFactory<Combat.Factory>();
serviceCollection.AddTaskFactoryAndExecutor<Aetheryte.Attune, Aetheryte.Factory, Aetheryte.DoAttune>(); serviceCollection.AddTaskFactory<Duty.Factory>();
serviceCollection.AddTaskFactoryAndExecutor<Combat.Task, Combat.Factory, Combat.HandleCombat>();
serviceCollection.AddTaskFactoryAndExecutor<Duty.Task, Duty.Factory, Duty.OpenDutyWindowExecutor>();
serviceCollection.AddTaskFactory<Emote.Factory>(); serviceCollection.AddTaskFactory<Emote.Factory>();
serviceCollection.AddTaskExecutor<Emote.UseOnObject, Emote.UseOnObjectExecutor>(); serviceCollection.AddTaskFactory<Action.Factory>();
serviceCollection.AddTaskExecutor<Emote.UseOnSelf, Emote.UseOnSelfExecutor>(); serviceCollection.AddTaskFactory<Interact.Factory>();
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.AddTaskExecutor<Jump.SingleJumpTask, Jump.DoSingleJump>(); serviceCollection.AddTaskFactory<Dive.Factory>();
serviceCollection.AddTaskExecutor<Jump.RepeatedJumpTask, Jump.DoRepeatedJumps>(); serviceCollection.AddTaskFactory<Say.Factory>();
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.AddTaskExecutor<UseItem.UseOnGround, UseItem.UseOnGroundExecutor>(); serviceCollection.AddTaskFactory<EquipItem.Factory>();
serviceCollection.AddTaskExecutor<UseItem.UseOnPosition, UseItem.UseOnPositionExecutor>(); serviceCollection.AddTaskFactory<EquipRecommended.Factory>();
serviceCollection.AddTaskExecutor<UseItem.UseOnObject, UseItem.UseOnObjectExecutor>(); serviceCollection.AddTaskFactory<Craft.Factory>();
serviceCollection.AddTaskExecutor<UseItem.UseOnSelf, UseItem.UseOnSelfExecutor>(); serviceCollection.AddTaskFactory<TurnInDelivery.Factory>();
serviceCollection.AddTaskFactoryAndExecutor<EquipItem.Task, EquipItem.Factory, EquipItem.DoEquip>();
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.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.WaitConditionExecutor>();
serviceCollection.AddTaskFactory<WaitAtEnd.Factory>(); serviceCollection.AddTaskFactory<WaitAtEnd.Factory>();
serviceCollection.AddTaskExecutor<WaitAtEnd.WaitDelay, WaitAtEnd.WaitDelayExecutor>(); serviceCollection.AddTransient<WaitAtEnd.WaitQuestAccepted>();
serviceCollection.AddTaskExecutor<WaitAtEnd.WaitNextStepOrSequence, WaitAtEnd.WaitNextStepOrSequenceExecutor>(); serviceCollection.AddTransient<WaitAtEnd.WaitQuestCompleted>();
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,5 +1,4 @@
using Dalamud.Plugin.Services; using JetBrains.Annotations;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Questionable.Controller.Steps; using Questionable.Controller.Steps;
@ -8,37 +7,11 @@ namespace Questionable;
internal static class ServiceCollectionExtensions internal static class ServiceCollectionExtensions
{ {
public static void AddTaskFactory< public static void AddTaskFactory<
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] [MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] TFactory>(
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>();
}
} }

View File

@ -1,14 +1,11 @@
using System; using System;
using System.Buffers.Text;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Text;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Components; using Dalamud.Interface.Components;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ImGuiNET; using ImGuiNET;
using LLib.ImGui; using LLib.ImGui;
using Questionable.Controller; using Questionable.Controller;
@ -21,23 +18,18 @@ namespace Questionable.Windows;
internal sealed class PriorityWindow : LWindow internal sealed class PriorityWindow : LWindow
{ {
private const string ClipboardPrefix = "qst:v1:";
private const char ClipboardSeparator = ';';
private readonly QuestController _questController; private readonly QuestController _questController;
private readonly QuestRegistry _questRegistry; private readonly QuestRegistry _questRegistry;
private readonly QuestFunctions _questFunctions; private readonly QuestFunctions _questFunctions;
private readonly QuestTooltipComponent _questTooltipComponent; private readonly QuestTooltipComponent _questTooltipComponent;
private readonly UiUtils _uiUtils; private readonly UiUtils _uiUtils;
private readonly IChatGui _chatGui;
private readonly IDalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
private string _searchString = string.Empty; private string _searchString = string.Empty;
private ElementId? _draggedItem; private ElementId? _draggedItem;
public PriorityWindow(QuestController questController, QuestRegistry questRegistry, QuestFunctions questFunctions, public PriorityWindow(QuestController questController, QuestRegistry questRegistry, QuestFunctions questFunctions,
QuestTooltipComponent questTooltipComponent, UiUtils uiUtils, IChatGui chatGui, QuestTooltipComponent questTooltipComponent, UiUtils uiUtils, IDalamudPluginInterface pluginInterface)
IDalamudPluginInterface pluginInterface)
: base("Quest Priority###QuestionableQuestPriority") : base("Quest Priority###QuestionableQuestPriority")
{ {
_questController = questController; _questController = questController;
@ -45,7 +37,6 @@ internal sealed class PriorityWindow : LWindow
_questFunctions = questFunctions; _questFunctions = questFunctions;
_questTooltipComponent = questTooltipComponent; _questTooltipComponent = questTooltipComponent;
_uiUtils = uiUtils; _uiUtils = uiUtils;
_chatGui = chatGui;
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
Size = new Vector2(400, 400); Size = new Vector2(400, 400);
@ -62,18 +53,6 @@ internal sealed class PriorityWindow : LWindow
ImGui.Text("Quests to do first:"); ImGui.Text("Quests to do first:");
DrawQuestFilter(); DrawQuestFilter();
DrawQuestList(); DrawQuestList();
List<ElementId> clipboardItems = ParseClipboardItems();
ImGui.BeginDisabled(clipboardItems.Count == 0);
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Download, "Import from Clipboard"))
ImportFromClipboard(clipboardItems);
ImGui.EndDisabled();
ImGui.SameLine();
ImGui.BeginDisabled(_questController.ManualPriorityQuests.Count == 0);
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Upload, "Export to Clibpoard"))
ExportToClipboard();
ImGui.EndDisabled();
ImGui.Spacing(); ImGui.Spacing();
ImGui.Separator(); ImGui.Separator();
@ -242,62 +221,4 @@ internal sealed class PriorityWindow : LWindow
priorityQuests.Insert(indexToAdd, itemToAdd); priorityQuests.Insert(indexToAdd, itemToAdd);
} }
} }
private List<ElementId> ParseClipboardItems()
{
List<ElementId> clipboardItems = new List<ElementId>();
try
{
string? clipboardText = GetClipboardText();
if (clipboardText != null && clipboardText.StartsWith(ClipboardPrefix, StringComparison.InvariantCulture))
{
clipboardText = clipboardText.Substring(ClipboardPrefix.Length);
string text = Encoding.UTF8.GetString(Convert.FromBase64String(clipboardText));
foreach (string part in text.Split(ClipboardSeparator))
{
ElementId elementId = ElementId.FromString(part);
clipboardItems.Add(elementId);
}
}
}
catch (Exception)
{
clipboardItems.Clear();
}
return clipboardItems;
}
private void ExportToClipboard()
{
string clipboardText = ClipboardPrefix + Convert.ToBase64String(Encoding.UTF8.GetBytes(
string.Join(ClipboardSeparator, _questController.ManualPriorityQuests.Select(x => x.Id.ToString()))));
ImGui.SetClipboardText(clipboardText);
_chatGui.Print("Copied quests to clipboard.", CommandHandler.MessageTag, CommandHandler.TagColor);
}
private void ImportFromClipboard(List<ElementId> clipboardItems)
{
foreach (ElementId elementId in clipboardItems)
{
if (_questRegistry.TryGetQuest(elementId, out Quest? quest) &&
!_questController.ManualPriorityQuests.Contains(quest))
_questController.ManualPriorityQuests.Add(quest);
}
}
/// <summary>
/// The default implementation for <see cref="ImGui.GetClipboardText"/> throws an NullReferenceException if the clipboard is empty, maybe also if it doesn't contain text.
/// </summary>
private unsafe string? GetClipboardText()
{
byte* ptr = ImGuiNative.igGetClipboardText();
if (ptr == null)
return null;
int byteCount = 0;
while (ptr[byteCount] != 0)
++byteCount;
return Encoding.UTF8.GetString(ptr, byteCount);
}
} }