Update how task factories work

This commit is contained in:
Liza 2024-08-20 02:48:06 +02:00
parent 9be1579f99
commit 2d22657d41
Signed by: liza
GPG Key ID: 7199F8D727D55F67
38 changed files with 1072 additions and 1251 deletions

View File

@ -92,6 +92,14 @@ internal sealed class InteractionUiController : IDisposable
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "PointMenu", PointMenuPostSetup); _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "PointMenu", PointMenuPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "HousingSelectBlock", HousingSelectBlockPostSetup); _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "HousingSelectBlock", HousingSelectBlockPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "TelepotTown", TeleportTownPostSetup); _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "TelepotTown", TeleportTownPostSetup);
unsafe
{
if (_gameGui.TryGetAddonByName("RhythmAction", out AtkUnitBase* addon))
{
addon->Close(true);
}
}
} }
private bool ShouldHandleUiInteractions => _isInitialCheck || _questController.IsRunning; private bool ShouldHandleUiInteractions => _isInitialCheck || _questController.IsRunning;
@ -778,7 +786,7 @@ internal sealed class InteractionUiController : IDisposable
{ {
if (ShouldHandleUiInteractions && if (ShouldHandleUiInteractions &&
_questController.HasCurrentTaskMatching(out AethernetShortcut.UseAethernetShortcut? aethernetShortcut) && _questController.HasCurrentTaskMatching(out AethernetShortcut.UseAethernetShortcut? aethernetShortcut) &&
EAetheryteLocationExtensions.IsFirmamentAetheryte(aethernetShortcut.From)) 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
uint toIndex = aethernetShortcut.To switch uint toIndex = aethernetShortcut.To switch

View File

@ -25,24 +25,34 @@ using Questionable.Functions;
using Questionable.GatheringPaths; 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;
namespace Questionable.Controller; 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 IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly ICondition _condition; private readonly ICondition _condition;
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,
@ -52,16 +62,25 @@ internal sealed unsafe class GatheringController : MiniTaskController<GatheringC
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
ICondition condition, ICondition condition,
IDataManager dataManager, IDataManager dataManager,
ILoggerFactory loggerFactory,
IGameGui gameGui,
IClientState clientState,
IPluginLog pluginLog) IPluginLog pluginLog)
: base(chatGui, 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;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_condition = condition; _condition = condition;
_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");
@ -152,8 +171,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(_serviceProvider.GetRequiredService<MountTask>() _taskQueue.Enqueue(_mountFactory.Mount(territoryId, Mount.EMountIf.Always));
.With(territoryId, MountTask.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);
@ -170,24 +188,28 @@ 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(_serviceProvider.GetRequiredService<Move.MoveInternal>() _taskQueue.Enqueue(_moveFactory.Move(new MoveTo.MoveParams(territoryId, pointOnFloor ?? averagePosition,
.With(territoryId, pointOnFloor ?? averagePosition, 50f, fly: fly, 50f,
ignoreDistanceToObject: true)); Fly: fly, IgnoreDistanceToObject: true)));
} }
_taskQueue.Enqueue(_serviceProvider.GetRequiredService<MoveToLandingLocation>() _taskQueue.Enqueue(new MoveToLandingLocation(territoryId, fly, currentNode, _moveFactory, _gameFunctions,
.With(territoryId, fly, currentNode)); _objectTable, _loggerFactory.CreateLogger<MoveToLandingLocation>()));
_taskQueue.Enqueue(_serviceProvider.GetRequiredService<Interact.DoInteract>() _taskQueue.Enqueue(_interactFactory.Interact(currentNode.DataId, null, EInteractionType.InternalGather, true));
.With(currentNode.DataId, null, EInteractionType.InternalGather, true));
QueueGatherNode(currentNode);
}
private void QueueGatherNode(GatheringNode currentNode)
{
foreach (bool revisitRequired in new[] { false, true }) foreach (bool revisitRequired in new[] { false, true })
{ {
_taskQueue.Enqueue(_serviceProvider.GetRequiredService<DoGather>() _taskQueue.Enqueue(new DoGather(_currentRequest!.Data, currentNode, revisitRequired, this, _gameFunctions,
.With(_currentRequest.Data, currentNode, revisitRequired)); _gameGui, _clientState, _condition, _loggerFactory.CreateLogger<DoGather>()));
if (_currentRequest.Data.Collectability > 0) if (_currentRequest.Data.Collectability > 0)
{ {
_taskQueue.Enqueue(_serviceProvider.GetRequiredService<DoGatherCollectable>() _taskQueue.Enqueue(new DoGatherCollectable(_currentRequest.Data, currentNode, revisitRequired, this,
.With(_currentRequest.Data, currentNode, revisitRequired)); _gameFunctions, _clientState, _gameGui, _loggerFactory.CreateLogger<DoGatherCollectable>()));
} }
} }
} }

View File

@ -0,0 +1,183 @@
using System;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Common.Math;
using Microsoft.Extensions.Logging;
using Questionable.Data;
using Questionable.Functions;
namespace Questionable.Controller.Steps.Common;
internal static class Mount
{
internal sealed class Factory(
GameFunctions gameFunctions,
ICondition condition,
TerritoryData territoryData,
IClientState clientState,
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);
}
}
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 DateTime _retryAt = DateTime.MinValue;
public bool Start()
{
if (condition[ConditionFlag.Mounted])
return false;
if (!territoryData.CanUseMount(territoryId))
{
logger.LogInformation("Can't use mount in current territory {Id}", territoryId);
return false;
}
if (gameFunctions.HasStatusPreventingMount())
{
logger.LogInformation("Can't mount due to status preventing sprint or mount");
return false;
}
if (mountIf == EMountIf.AwayFromPosition)
{
Vector3 playerPosition = clientState.LocalPlayer?.Position ?? Vector3.Zero;
float distance = System.Numerics.Vector3.Distance(playerPosition, position.GetValueOrDefault());
if (territoryId == clientState.TerritoryType && distance < 30f && !Conditions.IsDiving)
{
logger.LogInformation("Not using mount, as we're close to the target");
return false;
}
logger.LogInformation(
"Want to use mount if away from destination ({Distance} yalms), trying (in territory {Id})...",
distance, territoryId);
}
else
logger.LogInformation("Want to use mount, trying (in territory {Id})...", territoryId);
if (!condition[ConditionFlag.InCombat])
{
_retryAt = DateTime.Now.AddSeconds(0.5);
return true;
}
return false;
}
public ETaskResult Update()
{
if (_mountTriggered && !condition[ConditionFlag.Mounted] && DateTime.Now > _retryAt)
{
logger.LogInformation("Not mounted, retrying...");
_mountTriggered = false;
_retryAt = DateTime.MaxValue;
}
if (!_mountTriggered)
{
if (gameFunctions.HasStatusPreventingMount())
{
logger.LogInformation("Can't mount due to status preventing sprint or mount");
return ETaskResult.TaskComplete;
}
_mountTriggered = gameFunctions.Mount();
_retryAt = DateTime.Now.AddSeconds(5);
return ETaskResult.StillRunning;
}
return condition[ConditionFlag.Mounted]
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
}
public override string ToString() => "Mount";
}
private sealed class UnmountTask(ICondition condition, ILogger<UnmountTask> logger, GameFunctions gameFunctions)
: ITask
{
private bool _unmountTriggered;
private DateTime _continueAt = DateTime.MinValue;
public bool Start()
{
if (!condition[ConditionFlag.Mounted])
return false;
logger.LogInformation("Step explicitly wants no mount, trying to unmount...");
if (condition[ConditionFlag.InFlight])
{
gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(1);
return true;
}
_unmountTriggered = gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(1);
return true;
}
public ETaskResult Update()
{
if (_continueAt >= DateTime.Now)
return ETaskResult.StillRunning;
if (!_unmountTriggered)
{
// if still flying, we still need to land
if (condition[ConditionFlag.InFlight])
gameFunctions.Unmount();
else
_unmountTriggered = gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(1);
return ETaskResult.StillRunning;
}
if (condition[ConditionFlag.Mounted] && condition[ConditionFlag.InCombat])
{
_unmountTriggered = gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(1);
return ETaskResult.StillRunning;
}
return condition[ConditionFlag.Mounted]
? ETaskResult.StillRunning
: ETaskResult.TaskComplete;
}
public override string ToString() => "Unmount";
}
public enum EMountIf
{
Always,
AwayFromPosition,
}
}

View File

@ -1,114 +0,0 @@
using System;
using System.Numerics;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.Logging;
using Questionable.Data;
using Questionable.Functions;
namespace Questionable.Controller.Steps.Common;
internal sealed class MountTask(
GameFunctions gameFunctions,
ICondition condition,
TerritoryData territoryData,
IClientState clientState,
ILogger<MountTask> logger) : ITask
{
private ushort _territoryId;
private EMountIf _mountIf;
private Vector3? _position;
private bool _mountTriggered;
private DateTime _retryAt = DateTime.MinValue;
public ITask With(ushort territoryId, EMountIf mountIf, Vector3? position = null)
{
_territoryId = territoryId;
_mountIf = mountIf;
_position = position;
if (_mountIf == EMountIf.AwayFromPosition)
ArgumentNullException.ThrowIfNull(position);
return this;
}
public bool Start()
{
if (condition[ConditionFlag.Mounted])
return false;
if (!territoryData.CanUseMount(_territoryId))
{
logger.LogInformation("Can't use mount in current territory {Id}", _territoryId);
return false;
}
if (gameFunctions.HasStatusPreventingMount())
{
logger.LogInformation("Can't mount due to status preventing sprint or mount");
return false;
}
if (_mountIf == EMountIf.AwayFromPosition)
{
Vector3 playerPosition = clientState.LocalPlayer?.Position ?? Vector3.Zero;
float distance = (playerPosition - _position.GetValueOrDefault()).Length();
if (_territoryId == clientState.TerritoryType && distance < 30f && !Conditions.IsDiving)
{
logger.LogInformation("Not using mount, as we're close to the target");
return false;
}
logger.LogInformation(
"Want to use mount if away from destination ({Distance} yalms), trying (in territory {Id})...",
distance, _territoryId);
}
else
logger.LogInformation("Want to use mount, trying (in territory {Id})...", _territoryId);
if (!condition[ConditionFlag.InCombat])
{
_retryAt = DateTime.Now.AddSeconds(0.5);
return true;
}
return false;
}
public ETaskResult Update()
{
if (_mountTriggered && !condition[ConditionFlag.Mounted] && DateTime.Now > _retryAt)
{
logger.LogInformation("Not mounted, retrying...");
_mountTriggered = false;
_retryAt = DateTime.MaxValue;
}
if (!_mountTriggered)
{
if (gameFunctions.HasStatusPreventingMount())
{
logger.LogInformation("Can't mount due to status preventing sprint or mount");
return ETaskResult.TaskComplete;
}
_mountTriggered = gameFunctions.Mount();
_retryAt = DateTime.Now.AddSeconds(5);
return ETaskResult.StillRunning;
}
return condition[ConditionFlag.Mounted]
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
}
public override string ToString() => "Mount";
public enum EMountIf
{
Always,
AwayFromPosition,
}
}

View File

@ -1,6 +1,4 @@
using System; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Questionable.Functions; using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
@ -9,7 +7,7 @@ namespace Questionable.Controller.Steps.Common;
internal static class NextQuest internal static class NextQuest
{ {
internal sealed class Factory(IServiceProvider serviceProvider) : 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)
{ {
@ -22,37 +20,26 @@ internal static class NextQuest
if (step.NextQuestId == quest.Id) if (step.NextQuestId == quest.Id)
return null; return null;
return serviceProvider.GetRequiredService<SetQuest>() return new SetQuest(step.NextQuestId, quest.Id, questRegistry, questController, questFunctions, loggerFactory.CreateLogger<SetQuest>());
.With(step.NextQuestId, quest.Id);
} }
} }
internal sealed class SetQuest(QuestRegistry questRegistry, QuestController questController, QuestFunctions questFunctions, ILogger<SetQuest> logger) : ITask private sealed class SetQuest(ElementId nextQuestId, ElementId currentQuestId, QuestRegistry questRegistry, QuestController questController, QuestFunctions questFunctions, ILogger<SetQuest> logger) : ITask
{ {
public ElementId NextQuestId { get; set; } = null!;
public ElementId CurrentQuestId { get; set; } = null!;
public ITask With(ElementId nextQuestId, ElementId currentQuestId)
{
NextQuestId = nextQuestId;
CurrentQuestId = currentQuestId;
return this;
}
public bool Start() public bool Start()
{ {
if (questFunctions.IsQuestLocked(NextQuestId, CurrentQuestId)) if (questFunctions.IsQuestLocked(nextQuestId, currentQuestId))
{ {
logger.LogInformation("Can't set next quest to {QuestId}, quest is locked", NextQuestId); logger.LogInformation("Can't set next quest to {QuestId}, quest is locked", nextQuestId);
} }
else if (questRegistry.TryGetQuest(NextQuestId, out Quest? quest)) else if (questRegistry.TryGetQuest(nextQuestId, out Quest? quest))
{ {
logger.LogInformation("Setting next quest to {QuestId}: '{QuestName}'", 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", NextQuestId); logger.LogInformation("Next quest with id {QuestId} not found", nextQuestId);
questController.SetNextQuest(null); questController.SetNextQuest(null);
} }
@ -61,6 +48,6 @@ internal static class NextQuest
public ETaskResult Update() => ETaskResult.TaskComplete; public ETaskResult Update() => ETaskResult.TaskComplete;
public override string ToString() => $"SetNextQuest({NextQuestId})"; public override string ToString() => $"SetNextQuest({nextQuestId})";
} }
} }

View File

@ -1,63 +0,0 @@
using System;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
using Questionable.Functions;
namespace Questionable.Controller.Steps.Common;
internal sealed class UnmountTask(ICondition condition, ILogger<UnmountTask> logger, GameFunctions gameFunctions)
: ITask
{
private bool _unmountTriggered;
private DateTime _continueAt = DateTime.MinValue;
public bool Start()
{
if (!condition[ConditionFlag.Mounted])
return false;
logger.LogInformation("Step explicitly wants no mount, trying to unmount...");
if (condition[ConditionFlag.InFlight])
{
gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(1);
return true;
}
_unmountTriggered = gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(1);
return true;
}
public ETaskResult Update()
{
if (_continueAt >= DateTime.Now)
return ETaskResult.StillRunning;
if (!_unmountTriggered)
{
// if still flying, we still need to land
if (condition[ConditionFlag.InFlight])
gameFunctions.Unmount();
else
_unmountTriggered = gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(1);
return ETaskResult.StillRunning;
}
if (condition[ConditionFlag.Mounted] && condition[ConditionFlag.InCombat])
{
_unmountTriggered = gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(1);
return ETaskResult.StillRunning;
}
return condition[ConditionFlag.Mounted]
? ETaskResult.StillRunning
: ETaskResult.TaskComplete;
}
public override string ToString() => "Unmount";
}

View File

@ -17,6 +17,9 @@ using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Gathering; namespace Questionable.Controller.Steps.Gathering;
internal sealed class DoGather( internal sealed class DoGather(
GatheringController.GatheringRequest currentRequest,
GatheringNode currentNode,
bool revisitRequired,
GatheringController gatheringController, GatheringController gatheringController,
GameFunctions gameFunctions, GameFunctions gameFunctions,
IGameGui gameGui, IGameGui gameGui,
@ -26,34 +29,22 @@ internal sealed class DoGather(
{ {
private const uint StatusGatheringRateUp = 218; private const uint StatusGatheringRateUp = 218;
private GatheringController.GatheringRequest _currentRequest = null!;
private GatheringNode _currentNode = null!;
private bool _revisitRequired;
private bool _revisitTriggered; private bool _revisitTriggered;
private bool _wasGathering; private bool _wasGathering;
private SlotInfo? _slotToGather; private SlotInfo? _slotToGather;
private Queue<EAction>? _actionQueue; private Queue<EAction>? _actionQueue;
public ITask With(GatheringController.GatheringRequest currentRequest, GatheringNode currentNode,
bool revisitRequired)
{
_currentRequest = currentRequest;
_currentNode = currentNode;
_revisitRequired = revisitRequired;
return this;
}
public bool Start() => true; public bool Start() => true;
public unsafe ETaskResult Update() public unsafe ETaskResult Update()
{ {
if (_revisitRequired && !_revisitTriggered) if (revisitRequired && !_revisitTriggered)
{ {
logger.LogInformation("No revisit"); logger.LogInformation("No revisit");
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
if (gatheringController.HasNodeDisappeared(_currentNode)) if (gatheringController.HasNodeDisappeared(currentNode))
{ {
logger.LogInformation("Node disappeared"); logger.LogInformation("Node disappeared");
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
@ -78,9 +69,9 @@ internal sealed class DoGather(
else else
{ {
var slots = ReadSlots(addonGathering); var slots = ReadSlots(addonGathering);
if (_currentRequest.Collectability > 0) if (currentRequest.Collectability > 0)
{ {
var slot = slots.Single(x => x.ItemId == _currentRequest.ItemId); var slot = slots.Single(x => x.ItemId == currentRequest.ItemId);
addonGathering->FireCallbackInt(slot.Index); addonGathering->FireCallbackInt(slot.Index);
} }
else else
@ -103,7 +94,7 @@ internal sealed class DoGather(
_actionQueue = GetNextActions(nodeCondition, slots); _actionQueue = GetNextActions(nodeCondition, slots);
if (_actionQueue.Count == 0) if (_actionQueue.Count == 0)
{ {
var slot = _slotToGather ?? slots.Single(x => x.ItemId == _currentRequest.ItemId); var slot = _slotToGather ?? slots.Single(x => x.ItemId == currentRequest.ItemId);
addonGathering->FireCallbackInt(slot.Index); addonGathering->FireCallbackInt(slot.Index);
} }
} }
@ -157,9 +148,9 @@ internal sealed class DoGather(
if (!gameFunctions.HasStatus(StatusGatheringRateUp)) if (!gameFunctions.HasStatus(StatusGatheringRateUp))
{ {
// do we have an alternative item? only happens for 'evaluation' leve quests // do we have an alternative item? only happens for 'evaluation' leve quests
if (_currentRequest.AlternativeItemId != 0) if (currentRequest.AlternativeItemId != 0)
{ {
var alternativeSlot = slots.Single(x => x.ItemId == _currentRequest.AlternativeItemId); var alternativeSlot = slots.Single(x => x.ItemId == currentRequest.AlternativeItemId);
if (alternativeSlot.GatheringChance == 100) if (alternativeSlot.GatheringChance == 100)
{ {
@ -195,7 +186,7 @@ internal sealed class DoGather(
} }
} }
var slot = slots.Single(x => x.ItemId == _currentRequest.ItemId); var slot = slots.Single(x => x.ItemId == currentRequest.ItemId);
if (slot.GatheringChance > 0 && slot.GatheringChance < 100) if (slot.GatheringChance > 0 && slot.GatheringChance < 100)
{ {
if (slot.GatheringChance >= 95 && if (slot.GatheringChance >= 95 &&
@ -243,7 +234,7 @@ internal sealed class DoGather(
_revisitTriggered = true; _revisitTriggered = true;
} }
public override string ToString() => $"DoGather{(_revisitRequired ? " if revist" : "")}"; public override string ToString() => $"DoGather{(revisitRequired ? " if revist" : "")}";
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

@ -14,40 +14,31 @@ using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Gathering; namespace Questionable.Controller.Steps.Gathering;
internal sealed class DoGatherCollectable( internal sealed class DoGatherCollectable(
GatheringController.GatheringRequest currentRequest,
GatheringNode currentNode,
bool revisitRequired,
GatheringController gatheringController, GatheringController gatheringController,
GameFunctions gameFunctions, GameFunctions gameFunctions,
IClientState clientState, IClientState clientState,
IGameGui gameGui, IGameGui gameGui,
ILogger<DoGatherCollectable> logger) : ITask, IRevisitAware ILogger<DoGatherCollectable> logger) : ITask, IRevisitAware
{ {
private GatheringController.GatheringRequest _currentRequest = null!;
private GatheringNode _currentNode = null!;
private bool _revisitRequired;
private bool _revisitTriggered; private bool _revisitTriggered;
private Queue<EAction>? _actionQueue; private Queue<EAction>? _actionQueue;
private bool? _expectedScrutiny; private bool? _expectedScrutiny;
public ITask With(GatheringController.GatheringRequest currentRequest, GatheringNode currentNode,
bool revisitRequired)
{
_currentRequest = currentRequest;
_currentNode = currentNode;
_revisitRequired = revisitRequired;
return this;
}
public bool Start() => true; public bool Start() => true;
public unsafe ETaskResult Update() public unsafe ETaskResult Update()
{ {
if (_revisitRequired && !_revisitTriggered) if (revisitRequired && !_revisitTriggered)
{ {
logger.LogInformation("No revisit"); logger.LogInformation("No revisit");
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
if (gatheringController.HasNodeDisappeared(_currentNode)) if (gatheringController.HasNodeDisappeared(currentNode))
{ {
logger.LogInformation("Node disappeared"); logger.LogInformation("Node disappeared");
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
@ -103,7 +94,7 @@ internal sealed class DoGatherCollectable(
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
if (nodeCondition.CollectabilityToGoal(_currentRequest.Collectability) > 0) if (nodeCondition.CollectabilityToGoal(currentRequest.Collectability) > 0)
{ {
_actionQueue = GetNextActions(nodeCondition); _actionQueue = GetNextActions(nodeCondition);
if (_actionQueue != null) if (_actionQueue != null)
@ -147,7 +138,7 @@ internal sealed class DoGatherCollectable(
Queue<EAction> actions = new(); Queue<EAction> actions = new();
uint neededCollectability = nodeCondition.CollectabilityToGoal(_currentRequest.Collectability); uint neededCollectability = nodeCondition.CollectabilityToGoal(currentRequest.Collectability);
if (neededCollectability <= nodeCondition.CollectabilityFromMeticulous) if (neededCollectability <= nodeCondition.CollectabilityFromMeticulous)
{ {
logger.LogTrace("Can get all needed {NeededCollectability} from {Collectability}~ meticulous", logger.LogTrace("Can get all needed {NeededCollectability} from {Collectability}~ meticulous",
@ -203,7 +194,7 @@ internal sealed class DoGatherCollectable(
} }
public override string ToString() => public override string ToString() =>
$"DoGatherCollectable({SeIconChar.Collectible.ToIconString()}/{_expectedScrutiny} {_currentRequest.Collectability}){(_revisitRequired ? " if revist" : "")}"; $"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(

View File

@ -1,5 +1,4 @@
using System; using System.Globalization;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Enums;
@ -14,49 +13,40 @@ using Questionable.Model.Gathering;
namespace Questionable.Controller.Steps.Gathering; namespace Questionable.Controller.Steps.Gathering;
internal sealed class MoveToLandingLocation( internal sealed class MoveToLandingLocation(
IServiceProvider serviceProvider, ushort territoryId,
bool flyBetweenNodes,
GatheringNode gatheringNode,
MoveTo.Factory moveFactory,
GameFunctions gameFunctions, GameFunctions gameFunctions,
IObjectTable objectTable, IObjectTable objectTable,
ILogger<MoveToLandingLocation> logger) : ITask ILogger<MoveToLandingLocation> logger) : ITask
{ {
private ushort _territoryId;
private bool _flyBetweenNodes;
private GatheringNode _gatheringNode = null!;
private ITask _moveTask = null!; private ITask _moveTask = null!;
public ITask With(ushort territoryId, bool flyBetweenNodes, GatheringNode gatheringNode)
{
_territoryId = territoryId;
_flyBetweenNodes = flyBetweenNodes;
_gatheringNode = gatheringNode;
return this;
}
public bool Start() public bool Start()
{ {
var location = _gatheringNode.Locations.First(); var location = gatheringNode.Locations.First();
if (_gatheringNode.Locations.Count > 1) if (gatheringNode.Locations.Count > 1)
{ {
var gameObject = objectTable.SingleOrDefault(x => var gameObject = objectTable.SingleOrDefault(x =>
x.ObjectKind == ObjectKind.GatheringPoint && x.DataId == _gatheringNode.DataId && x.IsTargetable); x.ObjectKind == ObjectKind.GatheringPoint && x.DataId == gatheringNode.DataId && x.IsTargetable);
if (gameObject == null) if (gameObject == null)
return false; return false;
location = _gatheringNode.Locations.Single(x => Vector3.Distance(x.Position, gameObject.Position) < 0.1f); location = gatheringNode.Locations.Single(x => Vector3.Distance(x.Position, gameObject.Position) < 0.1f);
} }
var (target, degrees, range) = GatheringMath.CalculateLandingLocation(location); var (target, degrees, range) = GatheringMath.CalculateLandingLocation(location);
logger.LogInformation("Preliminary landing location: {Location}, with degrees = {Degrees}, range = {Range}", logger.LogInformation("Preliminary landing location: {Location}, with degrees = {Degrees}, range = {Range}",
target.ToString("G", CultureInfo.InvariantCulture), degrees, range); target.ToString("G", CultureInfo.InvariantCulture), degrees, range);
bool fly = _flyBetweenNodes && gameFunctions.IsFlyingUnlocked(_territoryId); bool fly = flyBetweenNodes && gameFunctions.IsFlyingUnlocked(territoryId);
_moveTask = serviceProvider.GetRequiredService<Move.MoveInternal>() _moveTask = moveFactory.Move(new MoveTo.MoveParams(territoryId, target, 0.25f, DataId: gatheringNode.DataId,
.With(_territoryId, target, 0.25f, dataId: _gatheringNode.DataId, fly: fly, Fly: fly, IgnoreDistanceToObject: true));
ignoreDistanceToObject: true);
return _moveTask.Start(); return _moveTask.Start();
} }
public ETaskResult Update() => _moveTask.Update(); public ETaskResult Update() => _moveTask.Update();
public override string ToString() => $"Land/{_moveTask}/{_flyBetweenNodes}"; public override string ToString() => $"Land/{_moveTask}/{flyBetweenNodes}";
} }

View File

@ -13,18 +13,18 @@ namespace Questionable.Controller.Steps.Gathering;
internal static class TurnInDelivery internal static class TurnInDelivery
{ {
internal sealed class Factory(IServiceProvider serviceProvider) : 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 serviceProvider.GetRequiredService<SatisfactionSupplyTurnIn>(); return new SatisfactionSupplyTurnIn(loggerFactory.CreateLogger<SatisfactionSupplyTurnIn>());
} }
} }
internal sealed class SatisfactionSupplyTurnIn(ILogger<SatisfactionSupplyTurnIn> logger) : ITask private sealed class SatisfactionSupplyTurnIn(ILogger<SatisfactionSupplyTurnIn> logger) : ITask
{ {
private ushort? _remainingAllowances; private ushort? _remainingAllowances;

View File

@ -14,7 +14,8 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Action internal static class Action
{ {
internal sealed class Factory(IServiceProvider serviceProvider) : 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)
{ {
@ -23,54 +24,45 @@ internal static class Action
ArgumentNullException.ThrowIfNull(step.Action); ArgumentNullException.ThrowIfNull(step.Action);
var task = serviceProvider.GetRequiredService<UseOnObject>() var task = new UseOnObject(step.DataId, step.Action.Value, gameFunctions,
.With(step.DataId, step.Action.Value); loggerFactory.CreateLogger<UseOnObject>());
if (step.Action.Value.RequiresMount()) if (step.Action.Value.RequiresMount())
return [task]; return [task];
else else
{ return [mountFactory.Unmount(), task];
var unmount = serviceProvider.GetRequiredService<UnmountTask>();
return [unmount, task];
}
} }
} }
internal sealed class UseOnObject(GameFunctions gameFunctions, ILogger<UseOnObject> logger) : ITask private sealed class UseOnObject(
uint? dataId,
EAction action,
GameFunctions gameFunctions,
ILogger<UseOnObject> logger) : ITask
{ {
private bool _usedAction; private bool _usedAction;
private DateTime _continueAt = DateTime.MinValue; private DateTime _continueAt = DateTime.MinValue;
public uint? DataId { get; set; }
public EAction Action { get; set; }
public ITask With(uint? dataId, EAction action)
{
DataId = dataId;
Action = action;
return this;
}
public bool Start() public bool Start()
{ {
if (DataId != null) if (dataId != null)
{ {
IGameObject? gameObject = gameFunctions.FindObjectByDataId(DataId.Value); IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId.Value);
if (gameObject == null) if (gameObject == null)
{ {
logger.LogWarning("No game object with dataId {DataId}", DataId); logger.LogWarning("No game object with dataId {DataId}", dataId);
return false; return false;
} }
if (gameObject.IsTargetable) if (gameObject.IsTargetable)
{ {
_usedAction = gameFunctions.UseAction(gameObject, 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(Action); _usedAction = gameFunctions.UseAction(action);
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(0.5);
return true; return true;
} }
@ -85,18 +77,18 @@ internal static class Action
if (!_usedAction) if (!_usedAction)
{ {
if (DataId != null) if (dataId != null)
{ {
IGameObject? gameObject = gameFunctions.FindObjectByDataId(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, Action); _usedAction = gameFunctions.UseAction(gameObject, action);
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(0.5);
} }
else else
{ {
_usedAction = gameFunctions.UseAction(Action); _usedAction = gameFunctions.UseAction(action);
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(0.5);
} }
@ -106,6 +98,6 @@ internal static class Action
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
public override string ToString() => $"Action({Action})"; public override string ToString() => $"Action({action})";
} }
} }

View File

@ -11,7 +11,11 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class AetherCurrent internal static class AetherCurrent
{ {
internal sealed class Factory(IServiceProvider serviceProvider, AetherCurrentData aetherCurrentData, IChatGui chatGui) : SimpleTaskFactory internal sealed class Factory(
GameFunctions gameFunctions,
AetherCurrentData aetherCurrentData,
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)
{ {
@ -23,47 +27,37 @@ internal static class AetherCurrent
if (!aetherCurrentData.IsValidAetherCurrent(step.TerritoryId, step.AetherCurrentId.Value)) if (!aetherCurrentData.IsValidAetherCurrent(step.TerritoryId, step.AetherCurrentId.Value))
{ {
chatGui.PrintError($"[Questionable] Aether current with id {step.AetherCurrentId} is referencing an invalid aether current, will skip attunement"); chatGui.PrintError(
$"[Questionable] Aether current with id {step.AetherCurrentId} is referencing an invalid aether current, will skip attunement");
return null; return null;
} }
return serviceProvider.GetRequiredService<DoAttune>() return new DoAttune(step.DataId.Value, step.AetherCurrentId.Value, gameFunctions, loggerFactory.CreateLogger<DoAttune>());
.With(step.DataId.Value, step.AetherCurrentId.Value);
} }
} }
internal sealed class DoAttune(GameFunctions gameFunctions, ILogger<DoAttune> logger) : ITask private sealed class DoAttune(uint dataId, uint aetherCurrentId, GameFunctions gameFunctions, ILogger<DoAttune> logger) : ITask
{ {
public uint DataId { get; set; }
public uint AetherCurrentId { get; set; }
public ITask With(uint dataId, uint aetherCurrentId)
{
DataId = dataId;
AetherCurrentId = aetherCurrentId;
return this;
}
public bool Start() public bool Start()
{ {
if (!gameFunctions.IsAetherCurrentUnlocked(AetherCurrentId)) if (!gameFunctions.IsAetherCurrentUnlocked(aetherCurrentId))
{ {
logger.LogInformation("Attuning to aether current {AetherCurrentId} / {DataId}", AetherCurrentId, logger.LogInformation("Attuning to aether current {AetherCurrentId} / {DataId}", aetherCurrentId,
DataId); dataId);
gameFunctions.InteractWith(DataId); gameFunctions.InteractWith(dataId);
return true; return true;
} }
logger.LogInformation("Already attuned to aether current {AetherCurrentId} / {DataId}", AetherCurrentId, logger.LogInformation("Already attuned to aether current {AetherCurrentId} / {DataId}", aetherCurrentId,
DataId); dataId);
return false; return false;
} }
public ETaskResult Update() => public ETaskResult Update() =>
gameFunctions.IsAetherCurrentUnlocked(AetherCurrentId) gameFunctions.IsAetherCurrentUnlocked(aetherCurrentId)
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : ETaskResult.StillRunning;
public override string ToString() => $"AttuneAetherCurrent({AetherCurrentId})"; 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(IServiceProvider serviceProvider) : 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,42 +23,35 @@ internal static class AethernetShard
ArgumentNullException.ThrowIfNull(step.AethernetShard); ArgumentNullException.ThrowIfNull(step.AethernetShard);
return serviceProvider.GetRequiredService<DoAttune>() return new DoAttune(step.AethernetShard.Value, aetheryteFunctions, gameFunctions,
.With(step.AethernetShard.Value); loggerFactory.CreateLogger<DoAttune>());
} }
} }
internal sealed class DoAttune( private sealed class DoAttune(
EAetheryteLocation aetheryteLocation,
AetheryteFunctions aetheryteFunctions, AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions, GameFunctions gameFunctions,
ILogger<DoAttune> logger) : ITask ILogger<DoAttune> logger) : ITask
{ {
public EAetheryteLocation AetheryteLocation { get; set; }
public ITask With(EAetheryteLocation aetheryteLocation)
{
AetheryteLocation = aetheryteLocation;
return this;
}
public bool Start() public bool Start()
{ {
if (!aetheryteFunctions.IsAetheryteUnlocked(AetheryteLocation)) if (!aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation))
{ {
logger.LogInformation("Attuning to aethernet shard {AethernetShard}", AetheryteLocation); logger.LogInformation("Attuning to aethernet shard {AethernetShard}", aetheryteLocation);
gameFunctions.InteractWith((uint)AetheryteLocation, ObjectKind.Aetheryte); gameFunctions.InteractWith((uint)aetheryteLocation, ObjectKind.Aetheryte);
return true; return true;
} }
logger.LogInformation("Already attuned to aethernet shard {AethernetShard}", AetheryteLocation); logger.LogInformation("Already attuned to aethernet shard {AethernetShard}", aetheryteLocation);
return false; return false;
} }
public ETaskResult Update() => public ETaskResult Update() =>
aetheryteFunctions.IsAetheryteUnlocked(AetheryteLocation) aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation)
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : ETaskResult.StillRunning;
public override string ToString() => $"AttuneAethernetShard({AetheryteLocation})"; public override string ToString() => $"AttuneAethernetShard({aetheryteLocation})";
} }
} }

View File

@ -10,7 +10,10 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Aetheryte internal static class Aetheryte
{ {
internal sealed class Factory(IServiceProvider serviceProvider) : 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)
{ {
@ -19,42 +22,35 @@ internal static class Aetheryte
ArgumentNullException.ThrowIfNull(step.Aetheryte); ArgumentNullException.ThrowIfNull(step.Aetheryte);
return serviceProvider.GetRequiredService<DoAttune>() return new DoAttune(step.Aetheryte.Value, aetheryteFunctions, gameFunctions,
.With(step.Aetheryte.Value); loggerFactory.CreateLogger<DoAttune>());
} }
} }
internal sealed class DoAttune( private sealed class DoAttune(
EAetheryteLocation aetheryteLocation,
AetheryteFunctions aetheryteFunctions, AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions, GameFunctions gameFunctions,
ILogger<DoAttune> logger) : ITask ILogger<DoAttune> logger) : ITask
{ {
public EAetheryteLocation AetheryteLocation { get; set; }
public ITask With(EAetheryteLocation aetheryteLocation)
{
AetheryteLocation = aetheryteLocation;
return this;
}
public bool Start() public bool Start()
{ {
if (!aetheryteFunctions.IsAetheryteUnlocked(AetheryteLocation)) if (!aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation))
{ {
logger.LogInformation("Attuning to aetheryte {Aetheryte}", AetheryteLocation); logger.LogInformation("Attuning to aetheryte {Aetheryte}", aetheryteLocation);
gameFunctions.InteractWith((uint)AetheryteLocation); gameFunctions.InteractWith((uint)aetheryteLocation);
return true; return true;
} }
logger.LogInformation("Already attuned to aetheryte {Aetheryte}", AetheryteLocation); logger.LogInformation("Already attuned to aetheryte {Aetheryte}", aetheryteLocation);
return false; return false;
} }
public ETaskResult Update() => public ETaskResult Update() =>
aetheryteFunctions.IsAetheryteUnlocked(AetheryteLocation) aetheryteFunctions.IsAetheryteUnlocked(aetheryteLocation)
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : ETaskResult.StillRunning;
public override string ToString() => $"AttuneAetheryte({AetheryteLocation})"; public override string ToString() => $"AttuneAetheryte({aetheryteLocation})";
} }
} }

View File

@ -13,7 +13,12 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Combat internal static class Combat
{ {
internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory internal sealed class Factory(
CombatController combatController,
Interact.Factory interactFactory,
Mount.Factory mountFactory,
UseItem.Factory useItemFactory,
QuestFunctions questFunctions) : ITaskFactory
{ {
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -22,12 +27,11 @@ internal static class Combat
ArgumentNullException.ThrowIfNull(step.EnemySpawnType); ArgumentNullException.ThrowIfNull(step.EnemySpawnType);
yield return serviceProvider.GetRequiredService<UnmountTask>(); yield return mountFactory.Unmount();
if (step.CombatDelaySecondsAtStart != null) if (step.CombatDelaySecondsAtStart != null)
{ {
yield return serviceProvider.GetRequiredService<WaitAtStart.WaitDelay>() yield return new WaitAtStart.WaitDelay(TimeSpan.FromSeconds(step.CombatDelaySecondsAtStart.Value));
.With(TimeSpan.FromSeconds(step.CombatDelaySecondsAtStart.Value));
} }
switch (step.EnemySpawnType) switch (step.EnemySpawnType)
@ -36,8 +40,7 @@ internal static class Combat
{ {
ArgumentNullException.ThrowIfNull(step.DataId); ArgumentNullException.ThrowIfNull(step.DataId);
yield return serviceProvider.GetRequiredService<Interact.DoInteract>() yield return interactFactory.Interact(step.DataId.Value, quest, EInteractionType.None, true);
.With(step.DataId.Value, quest, EInteractionType.None, true);
yield return CreateTask(quest, sequence, step); yield return CreateTask(quest, sequence, step);
break; break;
} }
@ -47,9 +50,8 @@ internal static class Combat
ArgumentNullException.ThrowIfNull(step.DataId); ArgumentNullException.ThrowIfNull(step.DataId);
ArgumentNullException.ThrowIfNull(step.ItemId); ArgumentNullException.ThrowIfNull(step.ItemId);
yield return serviceProvider.GetRequiredService<UseItem.UseOnObject>() yield return useItemFactory.OnObject(quest.Id, step.DataId.Value, step.ItemId.Value,
.With(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags, step.CompletionQuestVariablesFlags, true);
true);
yield return CreateTask(quest, sequence, step); yield return CreateTask(quest, sequence, step);
break; break;
} }
@ -73,34 +75,32 @@ internal static class Combat
ArgumentNullException.ThrowIfNull(step.EnemySpawnType); ArgumentNullException.ThrowIfNull(step.EnemySpawnType);
bool isLastStep = sequence.Steps.Last() == step; bool isLastStep = sequence.Steps.Last() == step;
return serviceProvider.GetRequiredService<HandleCombat>() return CreateTask(quest.Id, isLastStep, step.EnemySpawnType.Value, step.KillEnemyDataIds,
.With(quest.Id, isLastStep, step.EnemySpawnType.Value, step.KillEnemyDataIds, step.CompletionQuestVariablesFlags, step.ComplexCombatData);
step.CompletionQuestVariablesFlags, step.ComplexCombatData);
} }
}
internal sealed class HandleCombat(CombatController combatController, QuestFunctions questFunctions) : ITask private HandleCombat CreateTask(ElementId elementId, bool isLastStep, EEnemySpawnType enemySpawnType,
{ IList<uint> killEnemyDataIds, IList<QuestWorkValue?> completionQuestVariablesFlags,
private bool _isLastStep; IList<ComplexCombatData> complexCombatData)
private CombatController.CombatData _combatData = null!;
private IList<QuestWorkValue?> _completionQuestVariableFlags = null!;
public ITask With(ElementId elementId, bool isLastStep, EEnemySpawnType enemySpawnType, IList<uint> killEnemyDataIds,
IList<QuestWorkValue?> completionQuestVariablesFlags, IList<ComplexCombatData> complexCombatData)
{ {
_isLastStep = isLastStep; return new HandleCombat(isLastStep, new CombatController.CombatData
_combatData = new CombatController.CombatData
{ {
ElementId = elementId, ElementId = elementId,
SpawnType = enemySpawnType, SpawnType = enemySpawnType,
KillEnemyDataIds = killEnemyDataIds.ToList(), KillEnemyDataIds = killEnemyDataIds.ToList(),
ComplexCombatDatas = complexCombatData.ToList(), ComplexCombatDatas = complexCombatData.ToList(),
}; }, completionQuestVariablesFlags, combatController, questFunctions);
_completionQuestVariableFlags = completionQuestVariablesFlags;
return this;
} }
}
public bool Start() => combatController.Start(_combatData); private sealed class HandleCombat(
bool isLastStep,
CombatController.CombatData combatData,
IList<QuestWorkValue?> completionQuestVariableFlags,
CombatController combatController,
QuestFunctions questFunctions) : ITask
{
public bool Start() => combatController.Start(combatData);
public ETaskResult Update() public ETaskResult Update()
{ {
@ -108,13 +108,14 @@ internal static class Combat
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
// if our quest step has any completion flags, we need to check if they are set // if our quest step has any completion flags, we need to check if they are set
if (QuestWorkUtils.HasCompletionFlags(_completionQuestVariableFlags) && _combatData.ElementId is QuestId questId) if (QuestWorkUtils.HasCompletionFlags(completionQuestVariableFlags) &&
combatData.ElementId is QuestId questId)
{ {
var questWork = questFunctions.GetQuestProgressInfo(questId); var questWork = questFunctions.GetQuestProgressInfo(questId);
if (questWork == null) if (questWork == null)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
if (QuestWorkUtils.MatchesQuestWork(_completionQuestVariableFlags, questWork)) if (QuestWorkUtils.MatchesQuestWork(completionQuestVariableFlags, questWork))
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
else else
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -122,7 +123,7 @@ internal static class Combat
// the last step, by definition, can only be progressed by the game recognizing we're in a new sequence, // the last step, by definition, can only be progressed by the game recognizing we're in a new sequence,
// so this is an indefinite wait // so this is an indefinite wait
if (_isLastStep) if (isLastStep)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
else else
{ {
@ -133,9 +134,9 @@ internal static class Combat
public override string ToString() public override string ToString()
{ {
if (QuestWorkUtils.HasCompletionFlags(_completionQuestVariableFlags)) if (QuestWorkUtils.HasCompletionFlags(completionQuestVariableFlags))
return "HandleCombat(wait: QW flags)"; return "HandleCombat(wait: QW flags)";
else if (_isLastStep) else if (isLastStep)
return "HandleCombat(wait: next sequence)"; return "HandleCombat(wait: next sequence)";
else else
return "HandleCombat(wait: not in combat)"; return "HandleCombat(wait: not in combat)";

View File

@ -18,18 +18,23 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Dive internal static class Dive
{ {
internal sealed class Factory(IServiceProvider serviceProvider) : 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 serviceProvider.GetRequiredService<DoDive>(); return Dive();
}
public ITask Dive()
{
return new DoDive(condition, loggerFactory.CreateLogger<DoDive>());
} }
} }
internal sealed class DoDive(ICondition condition, ILogger<DoDive> logger) private sealed class DoDive(ICondition condition, ILogger<DoDive> logger)
: AbstractDelayedTask(TimeSpan.FromSeconds(5)) : AbstractDelayedTask(TimeSpan.FromSeconds(5))
{ {
private readonly Queue<(uint Type, nint Key)> _keysToPress = []; private readonly Queue<(uint Type, nint Key)> _keysToPress = [];

View File

@ -10,7 +10,7 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Duty internal static class Duty
{ {
internal sealed class Factory(IServiceProvider serviceProvider) : 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)
{ {
@ -18,33 +18,26 @@ internal static class Duty
return null; return null;
ArgumentNullException.ThrowIfNull(step.ContentFinderConditionId); ArgumentNullException.ThrowIfNull(step.ContentFinderConditionId);
return new OpenDutyFinder(step.ContentFinderConditionId.Value, gameFunctions, condition);
return serviceProvider.GetRequiredService<OpenDutyFinder>()
.With(step.ContentFinderConditionId.Value);
} }
} }
internal sealed class OpenDutyFinder(GameFunctions gameFunctions, ICondition condition) : ITask private sealed class OpenDutyFinder(
uint contentFinderConditionId,
GameFunctions gameFunctions,
ICondition condition) : ITask
{ {
public uint ContentFinderConditionId { get; set; }
public ITask With(uint contentFinderConditionId)
{
ContentFinderConditionId = contentFinderConditionId;
return this;
}
public bool Start() public bool Start()
{ {
if (condition[ConditionFlag.InDutyQueue]) if (condition[ConditionFlag.InDutyQueue])
return false; return false;
gameFunctions.OpenDutyFinder(ContentFinderConditionId); gameFunctions.OpenDutyFinder(contentFinderConditionId);
return true; return true;
} }
public ETaskResult Update() => ETaskResult.TaskComplete; public ETaskResult Update() => ETaskResult.TaskComplete;
public override string ToString() => $"OpenDutyFinder({ContentFinderConditionId})"; public override string ToString() => $"OpenDutyFinder({contentFinderConditionId})";
} }
} }

View File

@ -10,7 +10,7 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Emote internal static class Emote
{ {
internal sealed class Factory(IServiceProvider serviceProvider) : 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,57 +24,39 @@ internal static class Emote
ArgumentNullException.ThrowIfNull(step.Emote); ArgumentNullException.ThrowIfNull(step.Emote);
var unmount = serviceProvider.GetRequiredService<UnmountTask>(); var unmount = mountFactory.Unmount();
if (step.DataId != null) if (step.DataId != null)
{ {
var task = serviceProvider.GetRequiredService<UseOnObject>().With(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 = serviceProvider.GetRequiredService<Use>().With(step.Emote.Value); var task = new UseOnSelf(step.Emote.Value, chatFunctions);
return [unmount, task]; return [unmount, task];
} }
} }
} }
internal sealed class UseOnObject(ChatFunctions chatFunctions) : AbstractDelayedTask private sealed class UseOnObject(EEmote emote, uint dataId, ChatFunctions chatFunctions) : AbstractDelayedTask
{ {
public EEmote Emote { get; set; }
public uint DataId { get; set; }
public ITask With(EEmote emote, uint dataId)
{
Emote = emote;
DataId = dataId;
return this;
}
protected override bool StartInternal() protected override bool StartInternal()
{ {
chatFunctions.UseEmote(DataId, Emote); chatFunctions.UseEmote(dataId, emote);
return true; return true;
} }
public override string ToString() => $"Emote({Emote} on {DataId})"; public override string ToString() => $"Emote({emote} on {dataId})";
} }
internal sealed class Use(ChatFunctions chatFunctions) : AbstractDelayedTask private sealed class UseOnSelf(EEmote emote, ChatFunctions chatFunctions) : AbstractDelayedTask
{ {
public EEmote Emote { get; set; }
public ITask With(EEmote emote)
{
Emote = emote;
return this;
}
protected override bool StartInternal() protected override bool StartInternal()
{ {
chatFunctions.UseEmote(Emote); chatFunctions.UseEmote(emote);
return true; return true;
} }
public override string ToString() => $"Emote({Emote})"; 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(IServiceProvider serviceProvider) : 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,14 +24,39 @@ internal static class EquipItem
return null; return null;
ArgumentNullException.ThrowIfNull(step.ItemId); ArgumentNullException.ThrowIfNull(step.ItemId);
return serviceProvider.GetRequiredService<DoEquip>() return Equip(step.ItemId.Value);
.With(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 class DoEquip(IDataManager dataManager, ILogger<DoEquip> logger) : ITask, IToastAware private sealed class DoEquip(
uint itemId,
Item item,
List<ushort> targetSlots,
IDataManager dataManager,
ILogger<DoEquip> logger) : ITask, IToastAware
{ {
private const int MaxAttempts = 3; private const int MaxAttempts = 3;
private static readonly IReadOnlyList<InventoryType> SourceInventoryTypes = private static readonly IReadOnlyList<InventoryType> SourceInventoryTypes =
[ [
InventoryType.ArmoryMainHand, InventoryType.ArmoryMainHand,
@ -55,22 +80,9 @@ internal static class EquipItem
InventoryType.Inventory4, InventoryType.Inventory4,
]; ];
private uint _itemId;
private Item _item = null!;
private List<ushort> _targetSlots = [];
private int _attempts; private int _attempts;
private DateTime _continueAt = DateTime.MaxValue; private DateTime _continueAt = DateTime.MaxValue;
public ITask With(uint itemId)
{
_itemId = itemId;
_item = dataManager.GetExcelSheet<Item>()!.GetRow(itemId) ??
throw new ArgumentOutOfRangeException(nameof(itemId));
_targetSlots = GetEquipSlot(_item) ?? throw new InvalidOperationException("Not a piece of equipment");
return this;
}
public bool Start() public bool Start()
{ {
Equip(); Equip();
@ -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 == _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 == _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(_itemId, sourceInventoryType, true) == 0 && if (inventoryManager->GetItemCountInContainer(itemId, sourceInventoryType, true) == 0 &&
inventoryManager->GetItemCountInContainer(_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 != _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,19 +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 override string ToString() => $"Equip({_item.Name})";
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(IServiceProvider serviceProvider) : 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 serviceProvider.GetRequiredService<DoEquipRecommended>(); return DoEquip();
}
public ITask DoEquip()
{
return new DoEquipRecommended(clientState, chatGui);
} }
} }
internal sealed class BeforeDutyOrInstance(IServiceProvider serviceProvider) : 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,11 +35,11 @@ internal static class EquipRecommended
step.InteractionType != EInteractionType.Combat) step.InteractionType != EInteractionType.Combat)
return null; return null;
return serviceProvider.GetRequiredService<DoEquipRecommended>(); return new DoEquipRecommended(clientState, chatGui);
} }
} }
internal sealed unsafe class DoEquipRecommended(IClientState clientState, IChatGui chatGui) : ITask private sealed unsafe class DoEquipRecommended(IClientState clientState, IChatGui chatGui) : ITask
{ {
private bool _equipped; private bool _equipped;

View File

@ -15,7 +15,7 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Interact internal static class Interact
{ {
internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory internal sealed class Factory(GameFunctions gameFunctions, 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)
{ {
@ -36,41 +36,46 @@ internal static class Interact
// if we're fast enough, it is possible to get the smalltalk prompt // if we're fast enough, it is possible to get the smalltalk prompt
if (sequence.Sequence == 0 && sequence.Steps.IndexOf(step) == 0) if (sequence.Sequence == 0 && sequence.Steps.IndexOf(step) == 0)
yield return serviceProvider.GetRequiredService<WaitAtEnd.WaitDelay>(); yield return new WaitAtEnd.WaitDelay();
yield return serviceProvider.GetRequiredService<DoInteract>() yield return Interact(step.DataId.Value, quest, step.InteractionType,
.With(step.DataId.Value, quest, step.InteractionType,
step.TargetTerritoryId != null || quest.Id is SatisfactionSupplyNpcId); step.TargetTerritoryId != null || quest.Id is SatisfactionSupplyNpcId);
} }
internal ITask Interact(uint dataId, Quest? quest, EInteractionType interactionType, bool skipMarkerCheck = false)
{
return new DoInteract(dataId, quest, interactionType, skipMarkerCheck, gameFunctions, condition,
loggerFactory.CreateLogger<DoInteract>());
}
} }
internal sealed class DoInteract(GameFunctions gameFunctions, ICondition condition, ILogger<DoInteract> logger) internal sealed class DoInteract(
uint dataId,
Quest? quest,
EInteractionType interactionType,
bool skipMarkerCheck,
GameFunctions gameFunctions,
ICondition condition,
ILogger<DoInteract> logger)
: ITask, IConditionChangeAware : ITask, IConditionChangeAware
{ {
private bool _needsUnmount; private bool _needsUnmount;
private EInteractionState _interactionState = EInteractionState.None; private EInteractionState _interactionState = EInteractionState.None;
private DateTime _continueAt = DateTime.MinValue; private DateTime _continueAt = DateTime.MinValue;
private uint DataId { get; set; } public Quest? Quest => quest;
public Quest? Quest { get; private set; } public EInteractionType InteractionType
public EInteractionType InteractionType { get; set; }
private bool SkipMarkerCheck { get; set; }
public DoInteract With(uint dataId, Quest? quest, EInteractionType interactionType, bool skipMarkerCheck)
{ {
DataId = dataId; get => interactionType;
Quest = quest; set => interactionType = value;
InteractionType = interactionType;
SkipMarkerCheck = skipMarkerCheck;
return this;
} }
public bool Start() public bool Start()
{ {
IGameObject? gameObject = gameFunctions.FindObjectByDataId(DataId); IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId);
if (gameObject == null) if (gameObject == null)
{ {
logger.LogWarning("No game object with dataId {DataId}", DataId); logger.LogWarning("No game object with dataId {DataId}", dataId);
return false; return false;
} }
@ -78,7 +83,7 @@ internal static class Interact
if (!gameObject.IsTargetable && condition[ConditionFlag.Mounted] && if (!gameObject.IsTargetable && condition[ConditionFlag.Mounted] &&
gameObject.ObjectKind != ObjectKind.GatheringPoint) gameObject.ObjectKind != ObjectKind.GatheringPoint)
{ {
logger.LogInformation("Preparing interaction for {DataId} by unmounting", DataId); logger.LogInformation("Preparing interaction for {DataId} by unmounting", dataId);
_needsUnmount = true; _needsUnmount = true;
gameFunctions.Unmount(); gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(1); _continueAt = DateTime.Now.AddSeconds(1);
@ -117,10 +122,10 @@ internal static class Interact
if (_interactionState == EInteractionState.InteractionConfirmed) if (_interactionState == EInteractionState.InteractionConfirmed)
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
if (InteractionType == EInteractionType.InternalGather && condition[ConditionFlag.Gathering]) if (interactionType == EInteractionType.InternalGather && condition[ConditionFlag.Gathering])
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
IGameObject? gameObject = gameFunctions.FindObjectByDataId(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;
@ -133,14 +138,14 @@ internal static class Interact
private unsafe bool HasAnyMarker(IGameObject gameObject) private unsafe bool HasAnyMarker(IGameObject gameObject)
{ {
if (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 override string ToString() => $"Interact({dataId})";
public void OnConditionChange(ConditionFlag flag, bool value) public void OnConditionChange(ConditionFlag flag, bool value)
{ {

View File

@ -11,7 +11,11 @@ namespace Questionable.Controller.Steps.Interactions;
internal static class Jump internal static class Jump
{ {
internal sealed class Factory(IServiceProvider serviceProvider) : SimpleTaskFactory internal sealed class Factory(
MovementController movementController,
IClientState clientState,
IFramework framework,
ILoggerFactory loggerFactory) : SimpleTaskFactory
{ {
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step) public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -21,43 +25,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 SingleJump(step.DataId, step.JumpDestination, step.Comment);
return serviceProvider.GetRequiredService<SingleJump>()
.With(step.DataId, step.JumpDestination, step.Comment);
}
else else
{ return RepeatedJumps(step.DataId, step.JumpDestination, step.Comment);
return serviceProvider.GetRequiredService<RepeatedJumps>() }
.With(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,
loggerFactory.CreateLogger<DoRepeatedJumps>());
} }
} }
internal class SingleJump( private class DoSingleJump(
uint? dataId,
JumpDestination jumpDestination,
string? comment,
MovementController movementController, MovementController movementController,
IClientState clientState, IClientState clientState,
IFramework framework) : ITask IFramework framework) : ITask
{ {
public uint? DataId { get; set; }
public JumpDestination JumpDestination { get; set; } = null!;
public string? Comment { get; set; }
public ITask With(uint? dataId, JumpDestination jumpDestination, string? comment)
{
DataId = dataId;
JumpDestination = jumpDestination;
Comment = comment ?? string.Empty;
return this;
}
public virtual bool Start() public virtual bool Start()
{ {
float stopDistance = JumpDestination.CalculateStopDistance(); float stopDistance = jumpDestination.CalculateStopDistance();
if ((clientState.LocalPlayer!.Position - JumpDestination.Position).Length() <= stopDistance) if ((clientState.LocalPlayer!.Position - jumpDestination.Position).Length() <= stopDistance)
return false; return false;
movementController.NavigateTo(EMovementType.Quest, DataId, [JumpDestination.Position], false, false, movementController.NavigateTo(EMovementType.Quest, dataId, [jumpDestination.Position], false, false,
JumpDestination.StopDistance ?? stopDistance); jumpDestination.StopDistance ?? stopDistance);
framework.RunOnTick(() => framework.RunOnTick(() =>
{ {
unsafe unsafe
@ -65,7 +65,7 @@ internal static class Jump
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2); ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2);
} }
}, },
TimeSpan.FromSeconds(JumpDestination.DelaySeconds ?? 0.5f)); TimeSpan.FromSeconds(jumpDestination.DelaySeconds ?? 0.5f));
return true; return true;
} }
@ -81,22 +81,28 @@ internal static class Jump
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
public override string ToString() => $"Jump({Comment})"; public override string ToString() => $"Jump({comment})";
} }
internal sealed class RepeatedJumps( private sealed class DoRepeatedJumps(
uint? dataId,
JumpDestination jumpDestination,
string? comment,
MovementController movementController, MovementController movementController,
IClientState clientState, IClientState clientState,
IFramework framework, IFramework framework,
ILogger<RepeatedJumps> logger) : SingleJump(movementController, clientState, framework) ILogger<DoRepeatedJumps> logger)
: 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;
public override bool Start() public override bool Start()
{ {
_continueAt = DateTime.Now + TimeSpan.FromSeconds(2 * (JumpDestination.DelaySeconds ?? 0.5f)); _continueAt = DateTime.Now + TimeSpan.FromSeconds(2 * (_jumpDestination.DelaySeconds ?? 0.5f));
return base.Start(); return base.Start();
} }
@ -105,13 +111,13 @@ internal static class Jump
if (DateTime.Now < _continueAt) if (DateTime.Now < _continueAt)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
float stopDistance = JumpDestination.CalculateStopDistance(); float stopDistance = _jumpDestination.CalculateStopDistance();
if ((_clientState.LocalPlayer!.Position - JumpDestination.Position).Length() <= stopDistance || if ((_clientState.LocalPlayer!.Position - _jumpDestination.Position).Length() <= stopDistance ||
_clientState.LocalPlayer.Position.Y >= 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,
JumpDestination.Position.Y - 0.5f); _jumpDestination.Position.Y - 0.5f);
unsafe unsafe
{ {
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2); ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2);
@ -121,10 +127,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(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})"; 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(IServiceProvider serviceProvider, 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)
{ {
@ -21,31 +24,24 @@ internal static class Say
ArgumentNullException.ThrowIfNull(step.ChatMessage); ArgumentNullException.ThrowIfNull(step.ChatMessage);
string? excelString = string? excelString =
excelFunctions.GetDialogueText(quest, step.ChatMessage.ExcelSheet, step.ChatMessage.Key, false).GetString(); excelFunctions.GetDialogueText(quest, step.ChatMessage.ExcelSheet, step.ChatMessage.Key, false)
.GetString();
ArgumentNullException.ThrowIfNull(excelString); ArgumentNullException.ThrowIfNull(excelString);
var unmount = serviceProvider.GetRequiredService<UnmountTask>(); var unmount = mountFactory.Unmount();
var task = serviceProvider.GetRequiredService<UseChat>().With(excelString); var task = new UseChat(excelString, chatFunctions);
return [unmount, task]; return [unmount, task];
} }
} }
internal sealed class UseChat(ChatFunctions chatFunctions) : AbstractDelayedTask private sealed class UseChat(string chatMessage, ChatFunctions chatFunctions) : AbstractDelayedTask
{ {
public string ChatMessage { get; set; } = null!;
public ITask With(string chatMessage)
{
ChatMessage = chatMessage;
return this;
}
protected override bool StartInternal() protected override bool StartInternal()
{ {
chatFunctions.ExecuteCommand($"/say {ChatMessage}"); chatFunctions.ExecuteCommand($"/say {chatMessage}");
return true; return true;
} }
public override string ToString() => $"Say({ChatMessage})"; public override string ToString() => $"Say({chatMessage})";
} }
} }

View File

@ -26,9 +26,17 @@ internal static class UseItem
public const int VesperBayAetheryteTicket = 30362; public const int VesperBayAetheryteTicket = 30362;
internal sealed class Factory( internal sealed class Factory(
IServiceProvider serviceProvider, 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,8 +56,7 @@ internal static class UseItem
return CreateVesperBayFallbackTask(); return CreateVesperBayFallbackTask();
} }
var task = serviceProvider.GetRequiredService<Use>() var task = OnSelf(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags);
.With(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();
@ -59,47 +66,69 @@ internal static class UseItem
task, task,
new WaitConditionTask(() => clientState.TerritoryType == 140, new WaitConditionTask(() => clientState.TerritoryType == 140,
$"Wait(territory: {territoryData.GetNameAndId(140)})"), $"Wait(territory: {territoryData.GetNameAndId(140)})"),
serviceProvider.GetRequiredService<MountTask>() mountFactory.Mount(140,
.With(140, nextPosition != null ? Mount.EMountIf.AwayFromPosition : Mount.EMountIf.Always,
nextPosition != null ? MountTask.EMountIf.AwayFromPosition : MountTask.EMountIf.Always, nextPosition),
nextPosition), moveFactory.Move(new MoveTo.MoveParams(140, new(-408.92343f, 23.167036f, -351.16223f), 0.25f,
serviceProvider.GetRequiredService<Move.MoveInternal>() DataId: null, DisableNavMesh: true, Sprint: false, Fly: false))
.With(140, new(-408.92343f, 23.167036f, -351.16223f), 0.25f, dataId: null, disableNavMesh: true,
sprint: false, fly: false)
]; ];
} }
var unmount = serviceProvider.GetRequiredService<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 = serviceProvider.GetRequiredService<UseOnGround>() task = OnGroundTarget(quest.Id, step.DataId.Value, step.ItemId.Value,
.With(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags); step.CompletionQuestVariablesFlags);
else else
{ {
ArgumentNullException.ThrowIfNull(step.Position); ArgumentNullException.ThrowIfNull(step.Position);
task = serviceProvider.GetRequiredService<UseOnPosition>() task = OnPosition(quest.Id, step.Position.Value, step.ItemId.Value,
.With(quest.Id, step.Position.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags);
step.CompletionQuestVariablesFlags);
} }
return [unmount, task]; return [unmount, task];
} }
else if (step.DataId != null) else if (step.DataId != null)
{ {
var task = serviceProvider.GetRequiredService<UseOnObject>() var task = OnObject(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags);
.With(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags);
return [unmount, task]; return [unmount, task];
} }
else else
{ {
var task = serviceProvider.GetRequiredService<Use>() var task = OnSelf(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags);
.With(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");
@ -107,28 +136,32 @@ 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 serviceProvider.GetRequiredService<AetheryteShortcut.UseAetheryteShortcut>() yield return aetheryteShortcutFactory.Use(null, null, EAetheryteLocation.Limsa, territoryId);
.With(null, null, EAetheryteLocation.Limsa, territoryId); yield return aethernetShortcutFactory.Use(EAetheryteLocation.Limsa, EAetheryteLocation.LimsaArcanist);
yield return serviceProvider.GetRequiredService<AethernetShortcut.UseAethernetShortcut>() yield return new WaitAtEnd.WaitDelay();
.With(EAetheryteLocation.Limsa, EAetheryteLocation.LimsaArcanist); yield return
yield return serviceProvider.GetRequiredService<WaitAtEnd.WaitDelay>(); moveFactory.Move(new MoveTo.MoveParams(territoryId, destination, DataId: npcId, Sprint: false));
yield return serviceProvider.GetRequiredService<Move.MoveInternal>() yield return interactFactory.Interact(npcId, null, EInteractionType.None, true);
.With(territoryId, destination, dataId: npcId, sprint: false);
yield return serviceProvider.GetRequiredService<Interact.DoInteract>()
.With(npcId, null, EInteractionType.None, true);
} }
} }
internal abstract class UseItemBase(QuestFunctions questFunctions, ICondition condition, ILogger logger) : ITask private abstract class UseItemBase(
ElementId? questId,
uint itemId,
IList<QuestWorkValue?> completionQuestVariablesFlags,
bool startingCombat,
QuestFunctions questFunctions,
ICondition condition,
ILogger logger) : ITask
{ {
private bool _usedItem; private bool _usedItem;
private DateTime _continueAt; private DateTime _continueAt;
private int _itemCount; private int _itemCount;
public ElementId? QuestId { get; set; } public ElementId? QuestId => questId;
public uint ItemId { get; set; } public uint ItemId => itemId;
public IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; set; } = new List<QuestWorkValue?>(); public IList<QuestWorkValue?> CompletionQuestVariablesFlags => completionQuestVariablesFlags;
public bool StartingCombat { get; set; } public bool StartingCombat => startingCombat;
protected abstract bool UseItem(); protected abstract bool UseItem();
@ -149,9 +182,9 @@ internal static class UseItem
public unsafe ETaskResult Update() public unsafe ETaskResult Update()
{ {
if (QuestId is QuestId questId && QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags)) if (QuestId is QuestId realQuestId && QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags))
{ {
QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(questId); QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(realQuestId);
if (questWork != null && if (questWork != null &&
QuestWorkUtils.MatchesQuestWork(CompletionQuestVariablesFlags, questWork)) QuestWorkUtils.MatchesQuestWork(CompletionQuestVariablesFlags, questWork))
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
@ -203,96 +236,66 @@ internal static class UseItem
} }
internal sealed class UseOnGround( 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<UseOnGround> logger) ILogger<UseOnGround> logger)
: UseItemBase(questFunctions, condition, logger) : UseItemBase(questId, itemId, completionQuestVariablesFlags, false, questFunctions, condition, logger)
{ {
public uint DataId { get; set; } protected override bool UseItem() => gameFunctions.UseItemOnGround(dataId, ItemId);
public ITask With(ElementId? questId, uint dataId, uint itemId, public override string ToString() => $"UseItem({ItemId} on ground at {dataId})";
IList<QuestWorkValue?> completionQuestVariablesFlags)
{
QuestId = questId;
DataId = dataId;
ItemId = itemId;
CompletionQuestVariablesFlags = completionQuestVariablesFlags;
return this;
}
protected override bool UseItem() => gameFunctions.UseItemOnGround(DataId, ItemId);
public override string ToString() => $"UseItem({ItemId} on ground at {DataId})";
} }
internal sealed class UseOnPosition( private sealed class UseOnPosition(
ElementId? questId,
Vector3 position,
uint itemId,
IList<QuestWorkValue?> completionQuestVariablesFlags,
GameFunctions gameFunctions, GameFunctions gameFunctions,
QuestFunctions questFunctions, QuestFunctions questFunctions,
ICondition condition, ICondition condition,
ILogger<UseOnPosition> logger) ILogger<UseOnPosition> logger)
: UseItemBase(questFunctions, condition, logger) : UseItemBase(questId, itemId, completionQuestVariablesFlags, false, questFunctions, condition, logger)
{ {
public Vector3 Position { get; set; } protected override bool UseItem() => gameFunctions.UseItemOnPosition(position, ItemId);
public ITask With(ElementId? questId, Vector3 position, uint itemId,
IList<QuestWorkValue?> completionQuestVariablesFlags)
{
QuestId = questId;
Position = position;
ItemId = itemId;
CompletionQuestVariablesFlags = completionQuestVariablesFlags;
return this;
}
protected override bool UseItem() => gameFunctions.UseItemOnPosition(Position, ItemId);
public override string ToString() => public override string ToString() =>
$"UseItem({ItemId} on ground at {Position.ToString("G", CultureInfo.InvariantCulture)})"; $"UseItem({ItemId} on ground at {position.ToString("G", CultureInfo.InvariantCulture)})";
} }
internal sealed class UseOnObject( private sealed class UseOnObject(
ElementId? questId,
uint dataId,
uint itemId,
IList<QuestWorkValue?> completionQuestVariablesFlags,
bool startingCombat,
QuestFunctions questFunctions, QuestFunctions questFunctions,
GameFunctions gameFunctions, GameFunctions gameFunctions,
ICondition condition, ICondition condition,
ILogger<UseOnObject> logger) ILogger<UseOnObject> logger)
: UseItemBase(questFunctions, condition, logger) : UseItemBase(questId, itemId, completionQuestVariablesFlags, startingCombat, questFunctions, condition, logger)
{ {
public uint DataId { get; set; } protected override bool UseItem() => gameFunctions.UseItem(dataId, ItemId);
public ITask With(ElementId? questId, uint dataId, uint itemId, public override string ToString() => $"UseItem({ItemId} on {dataId})";
IList<QuestWorkValue?> completionQuestVariablesFlags,
bool startingCombat = false)
{
QuestId = questId;
DataId = dataId;
ItemId = itemId;
StartingCombat = startingCombat;
CompletionQuestVariablesFlags = completionQuestVariablesFlags;
return this;
}
protected override bool UseItem() => gameFunctions.UseItem(DataId, ItemId);
public override string ToString() => $"UseItem({ItemId} on {DataId})";
} }
internal sealed class Use( private sealed class Use(
ElementId? questId,
uint itemId,
IList<QuestWorkValue?> completionQuestVariablesFlags,
GameFunctions gameFunctions, GameFunctions gameFunctions,
QuestFunctions questFunctions, QuestFunctions questFunctions,
ICondition condition, ICondition condition,
ILogger<Use> logger) ILogger<Use> logger)
: UseItemBase(questFunctions, condition, logger) : UseItemBase(questId, itemId, completionQuestVariablesFlags, false, questFunctions, condition, logger)
{ {
public ITask With(ElementId? questId, uint itemId, IList<QuestWorkValue?> completionQuestVariablesFlags)
{
QuestId = questId;
ItemId = itemId;
CompletionQuestVariablesFlags = completionQuestVariablesFlags;
return this;
}
protected override bool UseItem() => gameFunctions.UseItem(ItemId); protected override bool UseItem() => gameFunctions.UseItem(ItemId);
public override string ToString() => $"UseItem({ItemId})"; public override string ToString() => $"UseItem({ItemId})";

View File

@ -9,6 +9,7 @@ using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI; using LLib.GameUI;
using Microsoft.Extensions.DependencyInjection; 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;
@ -17,31 +18,23 @@ namespace Questionable.Controller.Steps.Leves;
internal static class InitiateLeve internal static class InitiateLeve
{ {
internal sealed class Factory(IServiceProvider serviceProvider, 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)
{ {
if (step.InteractionType != EInteractionType.InitiateLeve) if (step.InteractionType != EInteractionType.InitiateLeve)
yield break; yield break;
yield return serviceProvider.GetRequiredService<SkipInitiateIfActive>().With(quest.Id); yield return new SkipInitiateIfActive(quest.Id);
yield return serviceProvider.GetRequiredService<OpenJournal>().With(quest.Id); yield return new OpenJournal(quest.Id);
yield return serviceProvider.GetRequiredService<Initiate>().With(quest.Id); yield return new Initiate(quest.Id, gameGui);
yield return serviceProvider.GetRequiredService<SelectDifficulty>(); yield return new SelectDifficulty(gameGui);
yield return new WaitConditionTask(() => condition[ConditionFlag.BoundByDuty], "Wait(BoundByDuty)"); yield return new WaitConditionTask(() => condition[ConditionFlag.BoundByDuty], "Wait(BoundByDuty)");
} }
} }
internal sealed unsafe class SkipInitiateIfActive : ITask internal sealed unsafe class SkipInitiateIfActive(ElementId elementId) : ITask
{ {
private ElementId _elementId = null!;
public ITask With(ElementId elementId)
{
_elementId = elementId;
return this;
}
public bool Start() => true; public bool Start() => true;
public ETaskResult Update() public ETaskResult Update()
@ -50,31 +43,23 @@ internal static class InitiateLeve
if (director != null && if (director != null &&
director->EventHandlerInfo != null && director->EventHandlerInfo != null &&
director->EventHandlerInfo->EventId.ContentId == EventHandlerType.GatheringLeveDirector && director->EventHandlerInfo->EventId.ContentId == EventHandlerType.GatheringLeveDirector &&
director->ContentId == _elementId.Value) director->ContentId == elementId.Value)
return ETaskResult.SkipRemainingTasksForStep; return ETaskResult.SkipRemainingTasksForStep;
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
public override string ToString() => $"CheckIfAlreadyActive({_elementId})"; public override string ToString() => $"CheckIfAlreadyActive({elementId})";
} }
internal sealed unsafe class OpenJournal : ITask internal sealed unsafe class OpenJournal(ElementId elementId) : ITask
{ {
private ElementId _elementId = null!; private readonly uint _questType = elementId is LeveId ? 2u : 1u;
private uint _questType;
private DateTime _openedAt = DateTime.MinValue; private DateTime _openedAt = DateTime.MinValue;
public ITask With(ElementId elementId)
{
_elementId = elementId;
_questType = _elementId is LeveId ? 2u : 1u;
return this;
}
public bool Start() public bool Start()
{ {
AgentQuestJournal.Instance()->OpenForQuest(_elementId.Value, _questType); AgentQuestJournal.Instance()->OpenForQuest(elementId.Value, _questType);
_openedAt = DateTime.Now; _openedAt = DateTime.Now;
return true; return true;
} }
@ -83,32 +68,24 @@ internal static class InitiateLeve
{ {
AgentQuestJournal* agentQuestJournal = AgentQuestJournal.Instance(); AgentQuestJournal* agentQuestJournal = AgentQuestJournal.Instance();
if (agentQuestJournal->IsAgentActive() && if (agentQuestJournal->IsAgentActive() &&
agentQuestJournal->SelectedQuestId == _elementId.Value && agentQuestJournal->SelectedQuestId == elementId.Value &&
agentQuestJournal->SelectedQuestType == _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(_elementId.Value, _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})"; public override string ToString() => $"OpenJournal({elementId})";
} }
internal sealed unsafe class Initiate(IGameGui gameGui) : ITask internal sealed unsafe class Initiate(ElementId elementId, IGameGui gameGui) : ITask
{ {
private ElementId _elementId = null!;
public ITask With(ElementId elementId)
{
_elementId = elementId;
return this;
}
public bool Start() => true; public bool Start() => true;
public ETaskResult Update() public ETaskResult Update()
@ -118,7 +95,7 @@ internal static class InitiateLeve
var pickQuest = stackalloc AtkValue[] var pickQuest = stackalloc AtkValue[]
{ {
new() { Type = ValueType.Int, Int = 4 }, new() { Type = ValueType.Int, Int = 4 },
new() { Type = ValueType.UInt, Int = _elementId.Value } new() { Type = ValueType.UInt, Int = elementId.Value }
}; };
addonJournalDetail->FireCallback(2, pickQuest); addonJournalDetail->FireCallback(2, pickQuest);
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
@ -127,7 +104,7 @@ internal static class InitiateLeve
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
public override string ToString() => $"InitiateLeve({_elementId})"; public override string ToString() => $"InitiateLeve({elementId})";
} }
internal sealed unsafe class SelectDifficulty(IGameGui gameGui) : ITask internal sealed unsafe class SelectDifficulty(IGameGui gameGui) : ITask

View File

@ -20,7 +20,16 @@ namespace Questionable.Controller.Steps.Shared;
internal static class AethernetShortcut internal static class AethernetShortcut
{ {
internal sealed class Factory(IServiceProvider serviceProvider, MovementController movementController) internal sealed class Factory(
MovementController movementController,
AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions,
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)
@ -30,12 +39,22 @@ internal static class AethernetShortcut
yield return new WaitConditionTask(() => movementController.IsNavmeshReady, yield return new WaitConditionTask(() => movementController.IsNavmeshReady,
"Wait(navmesh ready)"); "Wait(navmesh ready)");
yield return serviceProvider.GetRequiredService<UseAethernetShortcut>() yield return Use(step.AethernetShortcut.From, step.AethernetShortcut.To,
.With(step.AethernetShortcut.From, step.AethernetShortcut.To, step.SkipConditions?.AethernetShortcutIf); step.SkipConditions?.AethernetShortcutIf);
}
public ITask Use(EAetheryteLocation from, EAetheryteLocation to, SkipAetheryteCondition? skipConditions = null)
{
return new UseAethernetShortcut(from, to, skipConditions ?? new(),
loggerFactory.CreateLogger<UseAethernetShortcut>(), aetheryteFunctions, gameFunctions, clientState,
aetheryteData, territoryData, lifestreamIpc, movementController, condition);
} }
} }
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,
@ -51,68 +70,58 @@ internal static class AethernetShortcut
private bool _triedMounting; private bool _triedMounting;
private DateTime _continueAt = DateTime.MinValue; private DateTime _continueAt = DateTime.MinValue;
public EAetheryteLocation From { get; set; } public EAetheryteLocation From => from;
public EAetheryteLocation To { get; set; } public EAetheryteLocation To => to;
public SkipAetheryteCondition SkipConditions { get; set; } = null!;
public ITask With(EAetheryteLocation from, EAetheryteLocation to,
SkipAetheryteCondition? skipConditions = null)
{
From = from;
To = to;
SkipConditions = skipConditions ?? new();
return this;
}
public bool Start() public bool Start()
{ {
if (!SkipConditions.Never) if (!skipConditions.Never)
{ {
if (SkipConditions.InSameTerritory && clientState.TerritoryType == aetheryteData.TerritoryIds[To]) if (skipConditions.InSameTerritory && clientState.TerritoryType == aetheryteData.TerritoryIds[to])
{ {
logger.LogInformation("Skipping aethernet shortcut because the target is in the same territory"); logger.LogInformation("Skipping aethernet shortcut because the target is in the same territory");
return false; return false;
} }
if (SkipConditions.InTerritory.Contains(clientState.TerritoryType)) if (skipConditions.InTerritory.Contains(clientState.TerritoryType))
{ {
logger.LogInformation( logger.LogInformation(
"Skipping aethernet shortcut because the target is in the specified territory"); "Skipping aethernet shortcut because the target is in the specified territory");
return false; return false;
} }
if (SkipConditions.AetheryteLocked != null && if (skipConditions.AetheryteLocked != null &&
!aetheryteFunctions.IsAetheryteUnlocked(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 (SkipConditions.AetheryteUnlocked != null && if (skipConditions.AetheryteUnlocked != null &&
aetheryteFunctions.IsAetheryteUnlocked(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(From) && if (aetheryteFunctions.IsAetheryteUnlocked(from) &&
aetheryteFunctions.IsAetheryteUnlocked(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, From) < if (aetheryteData.CalculateDistance(playerPosition, territoryType, from) <
aetheryteData.CalculateDistance(playerPosition, territoryType, To)) aetheryteData.CalculateDistance(playerPosition, territoryType, to))
{ {
if (aetheryteData.CalculateDistance(playerPosition, territoryType, From) < if (aetheryteData.CalculateDistance(playerPosition, territoryType, from) <
(From.IsFirmamentAetheryte() ? 11f : 4f)) (from.IsFirmamentAetheryte() ? 11f : 4f))
{ {
DoTeleport(); DoTeleport();
return true; return true;
} }
else if (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 =
@ -125,14 +134,14 @@ internal static class AethernetShortcut
Vector3 closestPoint = nearbyPoints.MinBy(x => (playerPosition - x).Length()); Vector3 closestPoint = nearbyPoints.MinBy(x => (playerPosition - x).Length());
_moving = true; _moving = true;
movementController.NavigateTo(EMovementType.Quest, (uint)From, closestPoint, false, true, movementController.NavigateTo(EMovementType.Quest, (uint)from, closestPoint, false, true,
0.25f); 0.25f);
return true; return true;
} }
else else
{ {
if (territoryData.CanUseMount(territoryType) && if (territoryData.CanUseMount(territoryType) &&
aetheryteData.CalculateDistance(playerPosition, territoryType, From) > 30 && aetheryteData.CalculateDistance(playerPosition, territoryType, from) > 30 &&
!gameFunctions.HasStatusPreventingMount()) !gameFunctions.HasStatusPreventingMount())
{ {
_triedMounting = gameFunctions.Mount(); _triedMounting = gameFunctions.Mount();
@ -151,7 +160,7 @@ internal static class AethernetShortcut
else else
logger.LogWarning( logger.LogWarning(
"Aethernet shortcut not unlocked (from: {FromAetheryte}, to: {ToAetheryte}), walking manually", "Aethernet shortcut not unlocked (from: {FromAetheryte}, to: {ToAetheryte}), walking manually",
From, To); from, to);
return false; return false;
} }
@ -160,26 +169,26 @@ internal static class AethernetShortcut
{ {
logger.LogInformation("Moving to aethernet shortcut"); logger.LogInformation("Moving to aethernet shortcut");
_moving = true; _moving = true;
movementController.NavigateTo(EMovementType.Quest, (uint)From, aetheryteData.Locations[From], movementController.NavigateTo(EMovementType.Quest, (uint)from, aetheryteData.Locations[from],
false, true, false, true,
From.IsFirmamentAetheryte() from.IsFirmamentAetheryte()
? 4.4f ? 4.4f
: AetheryteConverter.IsLargeAetheryte(From) : AetheryteConverter.IsLargeAetheryte(from)
? 10.9f ? 10.9f
: 6.9f); : 6.9f);
} }
private void DoTeleport() private void DoTeleport()
{ {
if (From.IsFirmamentAetheryte()) if (from.IsFirmamentAetheryte())
{ {
logger.LogInformation("Using manual teleport interaction"); logger.LogInformation("Using manual teleport interaction");
_teleported = gameFunctions.InteractWith((uint)From, ObjectKind.EventObj); _teleported = gameFunctions.InteractWith((uint)from, ObjectKind.EventObj);
} }
else else
{ {
logger.LogInformation("Using lifestream to teleport to {Destination}", To); logger.LogInformation("Using lifestream to teleport to {Destination}", to);
lifestreamIpc.Teleport(To); lifestreamIpc.Teleport(to);
_teleported = true; _teleported = true;
} }
} }
@ -219,22 +228,22 @@ internal static class AethernetShortcut
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
if (aetheryteData.IsAirshipLanding(To)) if (aetheryteData.IsAirshipLanding(to))
{ {
if (aetheryteData.CalculateAirshipLandingDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero, if (aetheryteData.CalculateAirshipLandingDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero,
clientState.TerritoryType, To) > 5) clientState.TerritoryType, to) > 5)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
else if (aetheryteData.IsCityAetheryte(To)) else if (aetheryteData.IsCityAetheryte(to))
{ {
if (aetheryteData.CalculateDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero, if (aetheryteData.CalculateDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero,
clientState.TerritoryType, 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[To]) if (clientState.TerritoryType != aetheryteData.TerritoryIds[to])
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
@ -242,6 +251,6 @@ internal static class AethernetShortcut
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
public override string ToString() => $"UseAethernet({From} -> {To})"; public override string ToString() => $"UseAethernet({from} -> {to})";
} }
} }

View File

@ -18,23 +18,38 @@ namespace Questionable.Controller.Steps.Shared;
internal static class AetheryteShortcut internal static class AetheryteShortcut
{ {
internal sealed class Factory( internal sealed class Factory(
IServiceProvider serviceProvider, AetheryteData aetheryteData,
AetheryteData aetheryteData) : ITaskFactory 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 serviceProvider.GetRequiredService<UseAetheryteShortcut>() yield return Use(step, quest.Id, step.AetheryteShortcut.Value,
.With(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 serviceProvider.GetRequiredService<WaitAtEnd.WaitDelay>() }
.With(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);
} }
} }
internal sealed class UseAetheryteShortcut( /// <param name="expectedTerritoryId">If using an aethernet shortcut after, the aetheryte's territory-id and the step's territory-id can differ, we always use the aetheryte's territory-id.</param>
private sealed class UseAetheryteShortcut(
QuestStep? step,
ElementId? elementId,
EAetheryteLocation targetAetheryte,
ushort expectedTerritoryId,
ILogger<UseAetheryteShortcut> logger, ILogger<UseAetheryteShortcut> logger,
AetheryteFunctions aetheryteFunctions, AetheryteFunctions aetheryteFunctions,
QuestFunctions questFunctions, QuestFunctions questFunctions,
@ -45,26 +60,6 @@ internal static class AetheryteShortcut
private bool _teleported; private bool _teleported;
private DateTime _continueAt; private DateTime _continueAt;
public QuestStep? Step { get; set; }
public ElementId? ElementId { get; set; }
public EAetheryteLocation TargetAetheryte { get; set; }
/// <summary>
/// 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.
/// </summary>
public ushort ExpectedTerritoryId { get; set; }
public ITask With(QuestStep? step, ElementId? elementId, EAetheryteLocation targetAetheryte,
ushort expectedTerritoryId)
{
Step = step;
ElementId = elementId;
TargetAetheryte = targetAetheryte;
ExpectedTerritoryId = expectedTerritoryId;
return this;
}
public bool Start() => !ShouldSkipTeleport(); public bool Start() => !ShouldSkipTeleport();
public ETaskResult Update() public ETaskResult Update()
@ -78,7 +73,7 @@ internal static class AetheryteShortcut
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
if (clientState.TerritoryType == ExpectedTerritoryId) if (clientState.TerritoryType == expectedTerritoryId)
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -87,9 +82,9 @@ internal static class AetheryteShortcut
private bool ShouldSkipTeleport() private bool ShouldSkipTeleport()
{ {
ushort territoryType = clientState.TerritoryType; ushort territoryType = clientState.TerritoryType;
if (Step != null) if (step != null)
{ {
var skipConditions = 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))
@ -112,12 +107,12 @@ internal static class AetheryteShortcut
return true; return true;
} }
if (ElementId != null) if (elementId != null)
{ {
QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(ElementId); QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(elementId);
if (skipConditions.RequiredQuestVariablesNotMet && if (skipConditions.RequiredQuestVariablesNotMet &&
questWork != null && questWork != null &&
!QuestWorkUtils.MatchesRequiredQuestWorkConfig(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");
@ -126,10 +121,11 @@ internal static class AetheryteShortcut
} }
if (skipConditions.NearPosition is { } nearPosition &&
if (skipConditions.NearPosition is { } nearPosition && clientState.TerritoryType == Step.TerritoryId) clientState.TerritoryType == step.TerritoryId)
{ {
if (Vector3.Distance(nearPosition.Position, clientState.LocalPlayer!.Position) <= nearPosition.MaximumDistance) if (Vector3.Distance(nearPosition.Position, clientState.LocalPlayer!.Position) <=
nearPosition.MaximumDistance)
{ {
logger.LogInformation("Skipping aetheryte shortcut, as we're near the position"); logger.LogInformation("Skipping aetheryte shortcut, as we're near the position");
return true; return true;
@ -137,7 +133,7 @@ internal static class AetheryteShortcut
} }
} }
if (ExpectedTerritoryId == territoryType) if (expectedTerritoryId == territoryType)
{ {
if (!skipConditions.Never) if (!skipConditions.Never)
{ {
@ -148,17 +144,17 @@ internal static class AetheryteShortcut
} }
Vector3 pos = clientState.LocalPlayer!.Position; Vector3 pos = clientState.LocalPlayer!.Position;
if (Step.Position != null && if (step.Position != null &&
(pos - Step.Position.Value).Length() < 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, TargetAetheryte) < 20 || if (aetheryteData.CalculateDistance(pos, territoryType, targetAetheryte) < 20 ||
(Step.AethernetShortcut != null && (step.AethernetShortcut != null &&
(aetheryteData.CalculateDistance(pos, territoryType, Step.AethernetShortcut.From) < 20 || (aetheryteData.CalculateDistance(pos, territoryType, step.AethernetShortcut.From) < 20 ||
aetheryteData.CalculateDistance(pos, territoryType, Step.AethernetShortcut.To) < 20))) aetheryteData.CalculateDistance(pos, territoryType, step.AethernetShortcut.To) < 20)))
{ {
logger.LogInformation("Skipping aetheryte teleport"); logger.LogInformation("Skipping aetheryte teleport");
return true; return true;
@ -172,7 +168,7 @@ internal static class AetheryteShortcut
private bool DoTeleport() private bool DoTeleport()
{ {
if (!aetheryteFunctions.CanTeleport(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.");
@ -184,12 +180,12 @@ internal static class AetheryteShortcut
_continueAt = DateTime.Now.AddSeconds(8); _continueAt = DateTime.Now.AddSeconds(8);
if (!aetheryteFunctions.IsAetheryteUnlocked(TargetAetheryte)) if (!aetheryteFunctions.IsAetheryteUnlocked(targetAetheryte))
{ {
chatGui.PrintError($"[Questionable] Aetheryte {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)) else if (aetheryteFunctions.TeleportAetheryte(targetAetheryte))
{ {
logger.LogInformation("Travelling via aetheryte..."); logger.LogInformation("Travelling via aetheryte...");
return true; return true;
@ -201,6 +197,6 @@ internal static class AetheryteShortcut
} }
} }
public override string ToString() => $"UseAetheryte({TargetAetheryte})"; public override string ToString() => $"UseAetheryte({targetAetheryte})";
} }
} }

View File

@ -7,18 +7,22 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameData; using LLib.GameData;
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.External; using Questionable.External;
using Questionable.Model.Questing; using Questionable.Model.Questing;
using Mount = Questionable.Controller.Steps.Common.Mount;
using Quest = Questionable.Model.Quest; using Quest = Questionable.Model.Quest;
namespace Questionable.Controller.Steps.Shared; namespace Questionable.Controller.Steps.Shared;
internal static class Craft internal static class Craft
{ {
internal sealed class Factory(IServiceProvider serviceProvider) : 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)
{ {
@ -29,40 +33,34 @@ internal static class Craft
ArgumentNullException.ThrowIfNull(step.ItemCount); ArgumentNullException.ThrowIfNull(step.ItemCount);
return return
[ [
serviceProvider.GetRequiredService<UnmountTask>(), mountFactory.Unmount(),
serviceProvider.GetRequiredService<DoCraft>() Craft(step.ItemId.Value, step.ItemCount.Value)
.With(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 class DoCraft( private sealed class DoCraft(
uint itemId,
int itemCount,
IDataManager dataManager, IDataManager dataManager,
IClientState clientState, IClientState clientState,
ArtisanIpc artisanIpc, ArtisanIpc artisanIpc,
ILogger<DoCraft> logger) : ITask ILogger<DoCraft> logger) : ITask
{ {
private uint _itemId;
private int _itemCount;
public ITask With(uint itemId, int itemCount)
{
_itemId = itemId;
_itemCount = itemCount;
return this;
}
public bool Start() public bool Start()
{ {
if (HasRequestedItems()) if (HasRequestedItems())
{ {
logger.LogInformation("Already own {ItemCount}x {ItemId}", _itemCount, _itemId); logger.LogInformation("Already own {ItemCount}x {ItemId}", itemCount, itemId);
return false; return false;
} }
RecipeLookup? recipeLookup = dataManager.GetExcelSheet<RecipeLookup>()!.GetRow(_itemId); RecipeLookup? recipeLookup = dataManager.GetExcelSheet<RecipeLookup>()!.GetRow(itemId);
if (recipeLookup == null) if (recipeLookup == null)
throw new TaskException($"Item {_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
{ {
@ -94,12 +92,12 @@ internal static class Craft
} }
if (recipeId == 0) if (recipeId == 0)
throw new TaskException($"Unable to determine recipe for item {_itemId}"); throw new TaskException($"Unable to determine recipe for item {itemId}");
int remainingItemCount = _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",
_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}");
@ -130,15 +128,15 @@ internal static class Craft
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
private bool HasRequestedItems() => GetOwnedItemCount() >= _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(_itemId, isHq: false, checkEquipped: false) return inventoryManager->GetInventoryItemCount(itemId, isHq: false, checkEquipped: false)
+ inventoryManager->GetInventoryItemCount(_itemId, isHq: true, checkEquipped: false); + inventoryManager->GetInventoryItemCount(itemId, isHq: true, checkEquipped: false);
} }
public override string ToString() => $"Craft {_itemCount}x {_itemId} (with Artisan)"; public override string ToString() => $"Craft {itemCount}x {itemId} (with Artisan)";
} }
} }

View File

@ -10,7 +10,6 @@ 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;
using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Gathering; using Questionable.Model.Gathering;
using Questionable.Model.Questing; using Questionable.Model.Questing;
@ -22,6 +21,7 @@ internal static class GatheringRequiredItems
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,
@ -50,8 +50,7 @@ internal static class GatheringRequiredItems
if (classJob != currentClassJob) if (classJob != currentClassJob)
{ {
yield return serviceProvider.GetRequiredService<SwitchClassJob>() yield return new SwitchClassJob(classJob, clientState);
.With(classJob);
} }
if (HasRequiredItems(requiredGatheredItems)) if (HasRequiredItems(requiredGatheredItems))
@ -69,7 +68,7 @@ internal static class GatheringRequiredItems
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 serviceProvider.GetRequiredService<SkipMarker>(); yield return CreateSkipMarkerTask();
else else
yield return task; yield return task;
} }
@ -82,8 +81,7 @@ internal static class GatheringRequiredItems
yield return new WaitConditionTask(() => movementController.IsNavmeshReady, yield return new WaitConditionTask(() => movementController.IsNavmeshReady,
"Wait(navmesh ready)"); "Wait(navmesh ready)");
yield return serviceProvider.GetRequiredService<StartGathering>() yield return CreateStartGatheringTask(gatheringPointId, requiredGatheredItems);
.With(gatheringPointId, requiredGatheredItems);
} }
} }
@ -107,25 +105,28 @@ internal static class GatheringRequiredItems
minCollectability: (short)requiredGatheredItems.Collectability) >= minCollectability: (short)requiredGatheredItems.Collectability) >=
requiredGatheredItems.ItemCount; requiredGatheredItems.ItemCount;
} }
}
internal sealed class StartGathering(GatheringController gatheringController) : ITask private StartGathering CreateStartGatheringTask(GatheringPointId gatheringPointId, GatheredItem gatheredItem)
{
private GatheringPointId _gatheringPointId = null!;
private GatheredItem _gatheredItem = null!;
public ITask With(GatheringPointId gatheringPointId, GatheredItem gatheredItem)
{ {
_gatheringPointId = gatheringPointId; return new StartGathering(gatheringPointId, gatheredItem, gatheringController);
_gatheredItem = gatheredItem;
return this;
} }
private static SkipMarker CreateSkipMarkerTask()
{
return new SkipMarker();
}
}
private sealed class StartGathering(
GatheringPointId gatheringPointId,
GatheredItem gatheredItem,
GatheringController gatheringController) : ITask
{
public bool Start() public bool Start()
{ {
return gatheringController.Start(new GatheringController.GatheringRequest(_gatheringPointId, return gatheringController.Start(new GatheringController.GatheringRequest(gatheringPointId,
_gatheredItem.ItemId, _gatheredItem.AlternativeItemId, _gatheredItem.ItemCount, gatheredItem.ItemId, gatheredItem.AlternativeItemId, gatheredItem.ItemCount,
_gatheredItem.Collectability)); gatheredItem.Collectability));
} }
public ETaskResult Update() public ETaskResult Update()
@ -138,11 +139,11 @@ internal static class GatheringRequiredItems
public override string ToString() public override string ToString()
{ {
if (_gatheredItem.Collectability == 0) if (gatheredItem.Collectability == 0)
return $"Gather({_gatheredItem.ItemCount}x {_gatheredItem.ItemId})"; return $"Gather({gatheredItem.ItemCount}x {gatheredItem.ItemId})";
else else
return return
$"Gather({_gatheredItem.ItemCount}x {_gatheredItem.ItemId} {SeIconChar.Collectible.ToIconString()} {_gatheredItem.Collectability})"; $"Gather({gatheredItem.ItemCount}x {gatheredItem.ItemId} {SeIconChar.Collectible.ToIconString()} {gatheredItem.Collectability})";
} }
} }

View File

@ -18,87 +18,88 @@ using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
using Action = System.Action; using Action = System.Action;
using Mount = Questionable.Controller.Steps.Common.Mount;
using Quest = Questionable.Model.Quest; using Quest = Questionable.Model.Quest;
namespace Questionable.Controller.Steps.Shared; namespace Questionable.Controller.Steps.Shared;
internal static class Move internal static class MoveTo
{ {
internal sealed class Factory(IServiceProvider serviceProvider, AetheryteData aetheryteData) : ITaskFactory internal sealed class Factory(
MovementController movementController,
GameFunctions gameFunctions,
ICondition condition,
IDataManager dataManager,
IClientState clientState,
AetheryteData aetheryteData,
TerritoryData territoryData,
ILoggerFactory loggerFactory,
Mount.Factory mountFactory,
ILogger<Factory> logger) : ITaskFactory
{ {
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{ {
if (step.Position != null) if (step.Position != null)
{ {
var builder = serviceProvider.GetRequiredService<MoveBuilder>() return CreateMountTasks(quest.Id, step, step.Position.Value);
.With(quest.Id, step, step.Position.Value);
return builder.Build();
} }
else if (step is { DataId: not null, StopDistance: not null }) else if (step is { DataId: not null, StopDistance: not null })
{ {
var task = serviceProvider.GetRequiredService<ExpectToBeNearDataId>(); return [ExpectToBeNearDataId(step.DataId.Value, step.StopDistance.Value)];
task.DataId = step.DataId.Value;
task.StopDistance = step.StopDistance.Value;
return [task];
} }
else if (step is { InteractionType: EInteractionType.AttuneAetheryte, Aetheryte: not null }) else if (step is { InteractionType: EInteractionType.AttuneAetheryte, Aetheryte: not null })
{ {
var builder = serviceProvider.GetRequiredService<MoveBuilder>() return CreateMountTasks(quest.Id, step, aetheryteData.Locations[step.Aetheryte.Value]);
.With(quest.Id, step, aetheryteData.Locations[step.Aetheryte.Value]);
return builder.Build();
} }
else if (step is { InteractionType: EInteractionType.AttuneAethernetShard, AethernetShard: not null }) else if (step is { InteractionType: EInteractionType.AttuneAethernetShard, AethernetShard: not null })
{ {
var builder = serviceProvider.GetRequiredService<MoveBuilder>().With(quest.Id, step, return CreateMountTasks(quest.Id, step, aetheryteData.Locations[step.AethernetShard.Value]);
aetheryteData.Locations[step.AethernetShard.Value]);
return builder.Build();
} }
return []; return [];
} }
}
internal sealed class MoveBuilder( public ITask Move(QuestStep step, Vector3 destination)
IServiceProvider serviceProvider,
ILogger<MoveBuilder> logger,
GameFunctions gameFunctions,
IClientState clientState,
MovementController movementController,
TerritoryData territoryData,
AetheryteData aetheryteData)
{
private ElementId _questId = null!;
private QuestStep _step = null!;
private Vector3 _destination;
public MoveBuilder With(ElementId questId, QuestStep step, Vector3 destination)
{ {
_questId = questId; return Move(new MoveParams(step, destination));
_step = step;
_destination = destination;
return this;
} }
public IEnumerable<ITask> Build() public ITask Move(MoveParams moveParams)
{ {
if (_step.InteractionType == EInteractionType.Jump && _step.JumpDestination != null && return new MoveInternal(moveParams, movementController, gameFunctions,
(clientState.LocalPlayer!.Position - _step.JumpDestination.Position).Length() <= loggerFactory.CreateLogger<MoveInternal>(), condition, dataManager);
(_step.JumpDestination.StopDistance ?? 1f)) }
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 &&
(clientState.LocalPlayer!.Position - step.JumpDestination.Position).Length() <=
(step.JumpDestination.StopDistance ?? 1f))
{ {
logger.LogInformation("We're at the jump destination, skipping movement"); logger.LogInformation("We're at the jump destination, skipping movement");
yield break; yield break;
} }
yield return new WaitConditionTask(() => 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 WaitConditionTask(() => movementController.IsNavmeshReady, yield return new WaitConditionTask(() => movementController.IsNavmeshReady,
"Wait(navmesh ready)"); "Wait(navmesh ready)");
float stopDistance = _step.CalculateActualStopDistance(); float stopDistance = step.CalculateActualStopDistance();
Vector3? position = clientState.LocalPlayer?.Position; Vector3? position = clientState.LocalPlayer?.Position;
float actualDistance = position == null ? float.MaxValue : Vector3.Distance(position.Value, _destination); float actualDistance = position == null ? float.MaxValue : Vector3.Distance(position.Value, destination);
// if we teleport to a different zone, assume we always need to move; this is primarily relevant for cases // if we teleport to a different zone, assume we always need to move; this is primarily relevant for cases
// where you're e.g. in Lakeland, and the step navigates via Crystarium → Tesselation back into the same // where you're e.g. in Lakeland, and the step navigates via Crystarium → Tesselation back into the same
@ -107,8 +108,8 @@ internal static class Move
// Side effects of this check being broken include: // Side effects of this check being broken include:
// - mounting when near the target npc (if you spawn close enough for the next step) // - mounting when near the target npc (if you spawn close enough for the next step)
// - trying to fly when near the target npc (if close enough where no movement is required) // - trying to fly when near the target npc (if close enough where no movement is required)
if (_step.AetheryteShortcut != null && if (step.AetheryteShortcut != null &&
aetheryteData.TerritoryIds[_step.AetheryteShortcut.Value] != _step.TerritoryId) aetheryteData.TerritoryIds[step.AetheryteShortcut.Value] != step.TerritoryId)
{ {
logger.LogDebug("Aetheryte: Changing distance to max, previous distance: {Distance}", actualDistance); logger.LogDebug("Aetheryte: Changing distance to max, previous distance: {Distance}", actualDistance);
actualDistance = float.MaxValue; actualDistance = float.MaxValue;
@ -116,36 +117,33 @@ internal static class Move
// In particular, MoveBuilder is used so early that it'll have the position when you're starting gathering, // In particular, MoveBuilder is used so early that it'll have the position when you're starting gathering,
// not when you're finished. // not when you're finished.
if (_questId is SatisfactionSupplyNpcId) if (questId is SatisfactionSupplyNpcId)
{ {
logger.LogDebug("SatisfactionSupply: Changing distance to max, previous distance: {Distance}", logger.LogDebug("SatisfactionSupply: Changing distance to max, previous distance: {Distance}",
actualDistance); actualDistance);
actualDistance = float.MaxValue; actualDistance = float.MaxValue;
} }
if (_step.Mount == true) if (step.Mount == true)
yield return serviceProvider.GetRequiredService<MountTask>() yield return mountFactory.Mount(step.TerritoryId, Mount.EMountIf.Always);
.With(_step.TerritoryId, MountTask.EMountIf.Always); else if (step.Mount == false)
else if (_step.Mount == false) yield return mountFactory.Unmount();
yield return serviceProvider.GetRequiredService<UnmountTask>();
if (!_step.DisableNavmesh) if (!step.DisableNavmesh)
{ {
if (_step.Mount == null) if (step.Mount == null)
{ {
MountTask.EMountIf mountIf = Mount.EMountIf mountIf =
actualDistance > stopDistance && _step.Fly == true && actualDistance > stopDistance && step.Fly == true &&
gameFunctions.IsFlyingUnlocked(_step.TerritoryId) gameFunctions.IsFlyingUnlocked(step.TerritoryId)
? MountTask.EMountIf.Always ? Mount.EMountIf.Always
: MountTask.EMountIf.AwayFromPosition; : Mount.EMountIf.AwayFromPosition;
yield return serviceProvider.GetRequiredService<MountTask>() yield return mountFactory.Mount(step.TerritoryId, mountIf, destination);
.With(_step.TerritoryId, mountIf, _destination);
} }
if (actualDistance > stopDistance) if (actualDistance > stopDistance)
{ {
yield return serviceProvider.GetRequiredService<MoveInternal>() yield return Move(step, destination);
.With(_step, _destination);
} }
else else
logger.LogInformation("Skipping move task, distance: {ActualDistance} < {StopDistance}", logger.LogInformation("Skipping move task, distance: {ActualDistance} < {StopDistance}",
@ -156,124 +154,138 @@ internal static class Move
// navmesh won't move close enough // navmesh won't move close enough
if (actualDistance > stopDistance) if (actualDistance > stopDistance)
{ {
yield return serviceProvider.GetRequiredService<MoveInternal>() yield return Move(step, destination);
.With(_step, _destination);
} }
else else
logger.LogInformation("Skipping move task, distance: {ActualDistance} < {StopDistance}", logger.LogInformation("Skipping move task, distance: {ActualDistance} < {StopDistance}",
actualDistance, stopDistance); actualDistance, stopDistance);
} }
if (_step.Fly == true && _step.Land == true) if (step.Fly == true && step.Land == true)
yield return serviceProvider.GetRequiredService<Land>(); yield return Land();
} }
} }
internal sealed class MoveInternal( private sealed class MoveInternal : ITask, IToastAware
MovementController movementController,
GameFunctions gameFunctions,
ILogger<MoveInternal> logger,
ICondition condition,
IDataManager dataManager) : ITask, IToastAware
{ {
private string _cannotExecuteAtThisTime = dataManager.GetString<LogMessage>(579, x => x.Text)!; private readonly string _cannotExecuteAtThisTime;
private readonly MovementController _movementController;
private readonly ILogger<MoveInternal> _logger;
private readonly ICondition _condition;
public Action StartAction { get; set; } = null!; private readonly Action _startAction;
public Vector3 Destination { get; set; } private readonly Vector3 _destination;
public ITask With(QuestStep step, Vector3 destination) public MoveInternal(MoveParams moveParams,
MovementController movementController,
GameFunctions gameFunctions,
ILogger<MoveInternal> logger,
ICondition condition,
IDataManager dataManager)
{ {
return With( _movementController = movementController;
territoryId: step.TerritoryId, _logger = logger;
destination: destination, _condition = condition;
stopDistance: step.CalculateActualStopDistance(), _cannotExecuteAtThisTime = dataManager.GetString<LogMessage>(579, x => x.Text)!;
dataId: step.DataId,
disableNavMesh: step.DisableNavmesh,
sprint: step.Sprint != false,
fly: step.Fly == true,
land: step.Land == true,
ignoreDistanceToObject: step.IgnoreDistanceToObject == true);
}
public ITask With(ushort territoryId, Vector3 destination, float? stopDistance = null, uint? dataId = null, _destination = moveParams.Destination;
bool disableNavMesh = false, bool sprint = true, bool fly = false, bool land = false,
bool ignoreDistanceToObject = false)
{
Destination = destination;
if (!gameFunctions.IsFlyingUnlocked(territoryId)) if (!gameFunctions.IsFlyingUnlocked(moveParams.TerritoryId))
{ {
fly = false; moveParams = moveParams with { Fly = false, Land = false };
land = false;
} }
if (!disableNavMesh) if (!moveParams.DisableNavMesh)
{ {
StartAction = () => _startAction = () =>
movementController.NavigateTo(EMovementType.Quest, dataId, Destination, _movementController.NavigateTo(EMovementType.Quest, moveParams.DataId, _destination,
fly: fly, fly: moveParams.Fly,
sprint: sprint, sprint: moveParams.Sprint,
stopDistance: stopDistance, stopDistance: moveParams.StopDistance,
ignoreDistanceToObject: ignoreDistanceToObject, ignoreDistanceToObject: moveParams.IgnoreDistanceToObject,
land: land); land: moveParams.Land);
} }
else else
{ {
StartAction = () => _startAction = () =>
movementController.NavigateTo(EMovementType.Quest, dataId, [Destination], _movementController.NavigateTo(EMovementType.Quest, moveParams.DataId, [_destination],
fly: fly, fly: moveParams.Fly,
sprint: sprint, sprint: moveParams.Sprint,
stopDistance: stopDistance, stopDistance: moveParams.StopDistance,
ignoreDistanceToObject: ignoreDistanceToObject, ignoreDistanceToObject: moveParams.IgnoreDistanceToObject,
land: land); land: moveParams.Land);
} }
return this;
} }
public bool Start() public bool Start()
{ {
logger.LogInformation("Moving to {Destination}", Destination.ToString("G", CultureInfo.InvariantCulture)); _logger.LogInformation("Moving to {Destination}", _destination.ToString("G", CultureInfo.InvariantCulture));
StartAction(); _startAction();
return true; return true;
} }
public ETaskResult Update() public ETaskResult Update()
{ {
if (movementController.IsPathfinding || movementController.IsPathRunning) if (_movementController.IsPathfinding || _movementController.IsPathRunning)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
DateTime movementStartedAt = movementController.MovementStartedAt; DateTime movementStartedAt = _movementController.MovementStartedAt;
if (movementStartedAt == DateTime.MaxValue || movementStartedAt.AddSeconds(2) >= DateTime.Now) if (movementStartedAt == DateTime.MaxValue || movementStartedAt.AddSeconds(2) >= DateTime.Now)
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
public override string ToString() => $"MoveTo({Destination.ToString("G", CultureInfo.InvariantCulture)})"; public override string ToString() => $"MoveTo({_destination.ToString("G", CultureInfo.InvariantCulture)})";
public bool OnErrorToast(SeString message) public bool OnErrorToast(SeString message)
{ {
if (GameFunctions.GameStringEquals(_cannotExecuteAtThisTime, message.TextValue) && if (GameFunctions.GameStringEquals(_cannotExecuteAtThisTime, message.TextValue) &&
condition[ConditionFlag.Diving]) _condition[ConditionFlag.Diving])
return true; return true;
return false; return false;
} }
} }
internal sealed class ExpectToBeNearDataId(GameFunctions gameFunctions, IClientState clientState) : ITask internal sealed record MoveParams(
ushort TerritoryId,
Vector3 Destination,
float? StopDistance = null,
uint? DataId = null,
bool DisableNavMesh = false,
bool Sprint = true,
bool Fly = false,
bool Land = false,
bool IgnoreDistanceToObject = false)
{ {
public uint DataId { get; set; } public MoveParams(QuestStep step, Vector3 destination)
public float StopDistance { get; set; } : this(step.TerritoryId,
destination,
step.CalculateActualStopDistance(),
step.DataId,
step.DisableNavmesh,
step.Sprint != false,
step.Fly == true,
step.Land == true,
step.IgnoreDistanceToObject == true)
{
}
}
private sealed class WaitForNearDataId(
uint dataId,
float stopDistance,
GameFunctions gameFunctions,
IClientState clientState) : ITask
{
public bool Start() => true; public bool Start() => true;
public ETaskResult Update() public ETaskResult Update()
{ {
IGameObject? gameObject = gameFunctions.FindObjectByDataId(DataId); IGameObject? gameObject = gameFunctions.FindObjectByDataId(dataId);
if (gameObject == null || if (gameObject == null ||
(gameObject.Position - clientState.LocalPlayer!.Position).Length() > 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");
} }
@ -282,7 +294,7 @@ internal static class Move
} }
} }
internal sealed class Land(IClientState clientState, ICondition condition, ILogger<Land> logger) : ITask private sealed class LandTask(IClientState clientState, ICondition condition, ILogger<LandTask> logger) : ITask
{ {
private bool _landing; private bool _landing;
private DateTime _continueAt; private DateTime _continueAt;

View File

@ -20,7 +20,12 @@ namespace Questionable.Controller.Steps.Shared;
internal static class SkipCondition internal static class SkipCondition
{ {
internal sealed class Factory(IServiceProvider serviceProvider) : 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)
{ {
@ -35,90 +40,86 @@ internal static class SkipCondition
step.NextQuestId == null) step.NextQuestId == null)
return null; return null;
return serviceProvider.GetRequiredService<CheckSkip>() return Check(step, skipConditions, quest.Id);
.With(step, skipConditions ?? new(), quest.Id); }
private CheckSkip Check(QuestStep step, SkipStepConditions? skipConditions, ElementId questId)
{
return new CheckSkip(step, skipConditions ?? new(), questId, loggerFactory.CreateLogger<CheckSkip>(),
aetheryteFunctions, gameFunctions, questFunctions, clientState);
} }
} }
internal sealed class CheckSkip( private sealed class CheckSkip(
QuestStep step,
SkipStepConditions skipConditions,
ElementId elementId,
ILogger<CheckSkip> logger, ILogger<CheckSkip> logger,
AetheryteFunctions aetheryteFunctions, AetheryteFunctions aetheryteFunctions,
GameFunctions gameFunctions, GameFunctions gameFunctions,
QuestFunctions questFunctions, QuestFunctions questFunctions,
IClientState clientState) : ITask IClientState clientState) : ITask
{ {
public QuestStep Step { get; set; } = null!;
public SkipStepConditions SkipConditions { get; set; } = null!;
public ElementId ElementId { get; set; } = null!;
public ITask With(QuestStep step, SkipStepConditions skipConditions, ElementId elementId)
{
Step = step;
SkipConditions = skipConditions;
ElementId = elementId;
return this;
}
public unsafe bool Start() public unsafe bool Start()
{ {
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 &&
gameFunctions.IsFlyingUnlocked(Step.TerritoryId)) gameFunctions.IsFlyingUnlocked(step.TerritoryId))
{ {
logger.LogInformation("Skipping step, as flying is unlocked"); logger.LogInformation("Skipping step, as flying is unlocked");
return true; return true;
} }
if (SkipConditions.Flying == ELockedSkipCondition.Locked && if (skipConditions.Flying == ELockedSkipCondition.Locked &&
!gameFunctions.IsFlyingUnlocked(Step.TerritoryId)) !gameFunctions.IsFlyingUnlocked(step.TerritoryId))
{ {
logger.LogInformation("Skipping step, as flying is locked"); logger.LogInformation("Skipping step, as flying is locked");
return true; return true;
} }
if (SkipConditions.Chocobo == ELockedSkipCondition.Unlocked && if (skipConditions.Chocobo == ELockedSkipCondition.Unlocked &&
PlayerState.Instance()->IsMountUnlocked(1)) PlayerState.Instance()->IsMountUnlocked(1))
{ {
logger.LogInformation("Skipping step, as chocobo is unlocked"); logger.LogInformation("Skipping step, as chocobo is unlocked");
return true; return true;
} }
if (SkipConditions.InTerritory.Count > 0 && if (skipConditions.InTerritory.Count > 0 &&
SkipConditions.InTerritory.Contains(clientState.TerritoryType)) skipConditions.InTerritory.Contains(clientState.TerritoryType))
{ {
logger.LogInformation("Skipping step, as in a skip.InTerritory"); logger.LogInformation("Skipping step, as in a skip.InTerritory");
return true; return true;
} }
if (SkipConditions.NotInTerritory.Count > 0 && if (skipConditions.NotInTerritory.Count > 0 &&
!SkipConditions.NotInTerritory.Contains(clientState.TerritoryType)) !skipConditions.NotInTerritory.Contains(clientState.TerritoryType))
{ {
logger.LogInformation("Skipping step, as not in a skip.NotInTerritory"); logger.LogInformation("Skipping step, as not in a skip.NotInTerritory");
return true; return true;
} }
if (SkipConditions.QuestsCompleted.Count > 0 && if (skipConditions.QuestsCompleted.Count > 0 &&
SkipConditions.QuestsCompleted.All(questFunctions.IsQuestComplete)) skipConditions.QuestsCompleted.All(questFunctions.IsQuestComplete))
{ {
logger.LogInformation("Skipping step, all prequisite quests are complete"); logger.LogInformation("Skipping step, all prequisite quests are complete");
return true; return true;
} }
if (SkipConditions.QuestsAccepted.Count > 0 && if (skipConditions.QuestsAccepted.Count > 0 &&
SkipConditions.QuestsAccepted.All(questFunctions.IsQuestAccepted)) skipConditions.QuestsAccepted.All(questFunctions.IsQuestAccepted))
{ {
logger.LogInformation("Skipping step, all prequisite quests are accepted"); logger.LogInformation("Skipping step, all prequisite quests are accepted");
return true; return true;
} }
if (SkipConditions.NotTargetable && if (skipConditions.NotTargetable &&
Step is { DataId: not null }) step is { DataId: not null })
{ {
IGameObject? gameObject = gameFunctions.FindObjectByDataId(Step.DataId.Value); IGameObject? gameObject = gameFunctions.FindObjectByDataId(step.DataId.Value);
if (gameObject == null) if (gameObject == null)
{ {
if ((Step.Position.GetValueOrDefault() - clientState.LocalPlayer!.Position).Length() < 100) if ((step.Position.GetValueOrDefault() - clientState.LocalPlayer!.Position).Length() < 100)
{ {
logger.LogInformation("Skipping step, object is not nearby (but we are)"); logger.LogInformation("Skipping step, object is not nearby (but we are)");
return true; return true;
@ -131,60 +132,60 @@ internal static class SkipCondition
} }
} }
if (SkipConditions.Item is { NotInInventory: true } && Step is { ItemId: not null }) if (skipConditions.Item is { NotInInventory: true } && step is { ItemId: not null })
{ {
InventoryManager* inventoryManager = InventoryManager.Instance(); InventoryManager* inventoryManager = InventoryManager.Instance();
if (inventoryManager->GetInventoryItemCount(Step.ItemId.Value) == 0) if (inventoryManager->GetInventoryItemCount(step.ItemId.Value) == 0)
{ {
logger.LogInformation("Skipping step, no item with itemId {ItemId} in inventory", logger.LogInformation("Skipping step, no item with itemId {ItemId} in inventory",
Step.ItemId.Value); step.ItemId.Value);
return true; return true;
} }
} }
if (Step is if (step is
{ {
DataId: not null, DataId: not null,
InteractionType: EInteractionType.AttuneAetheryte or EInteractionType.AttuneAethernetShard InteractionType: EInteractionType.AttuneAetheryte or EInteractionType.AttuneAethernetShard
} && } &&
aetheryteFunctions.IsAetheryteUnlocked((EAetheryteLocation)Step.DataId.Value)) aetheryteFunctions.IsAetheryteUnlocked((EAetheryteLocation)step.DataId.Value))
{ {
logger.LogInformation("Skipping step, as aetheryte/aethernet shard is unlocked"); logger.LogInformation("Skipping step, as aetheryte/aethernet shard is unlocked");
return true; return true;
} }
if (SkipConditions.AetheryteLocked != null && if (skipConditions.AetheryteLocked != null &&
!aetheryteFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteLocked.Value)) !aetheryteFunctions.IsAetheryteUnlocked(skipConditions.AetheryteLocked.Value))
{ {
logger.LogInformation("Skipping step, as aetheryte is locked"); logger.LogInformation("Skipping step, as aetheryte is locked");
return true; return true;
} }
if (SkipConditions.AetheryteUnlocked != null && if (skipConditions.AetheryteUnlocked != null &&
aetheryteFunctions.IsAetheryteUnlocked(SkipConditions.AetheryteUnlocked.Value)) aetheryteFunctions.IsAetheryteUnlocked(skipConditions.AetheryteUnlocked.Value))
{ {
logger.LogInformation("Skipping step, as aetheryte is unlocked"); logger.LogInformation("Skipping step, as aetheryte is unlocked");
return true; return true;
} }
if (Step is { DataId: not null, InteractionType: EInteractionType.AttuneAetherCurrent } && if (step is { DataId: not null, InteractionType: EInteractionType.AttuneAetherCurrent } &&
gameFunctions.IsAetherCurrentUnlocked(Step.DataId.Value)) gameFunctions.IsAetherCurrentUnlocked(step.DataId.Value))
{ {
logger.LogInformation("Skipping step, as current is unlocked"); logger.LogInformation("Skipping step, as current is unlocked");
return true; return true;
} }
QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(ElementId); QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(elementId);
if (questWork != null) if (questWork != null)
{ {
if (QuestWorkUtils.HasCompletionFlags(Step.CompletionQuestVariablesFlags) && if (QuestWorkUtils.HasCompletionFlags(step.CompletionQuestVariablesFlags) &&
QuestWorkUtils.MatchesQuestWork(Step.CompletionQuestVariablesFlags, questWork)) QuestWorkUtils.MatchesQuestWork(step.CompletionQuestVariablesFlags, questWork))
{ {
logger.LogInformation("Skipping step, as quest variables match (step is complete)"); logger.LogInformation("Skipping step, as quest variables match (step is complete)");
return true; return true;
} }
if (Step is { SkipConditions.StepIf: { } conditions }) if (step is { SkipConditions.StepIf: { } conditions })
{ {
if (QuestWorkUtils.MatchesQuestWork(conditions.CompletionQuestVariablesFlags, questWork)) if (QuestWorkUtils.MatchesQuestWork(conditions.CompletionQuestVariablesFlags, questWork))
{ {
@ -193,7 +194,7 @@ internal static class SkipCondition
} }
} }
if (Step is { RequiredQuestVariables: { } requiredQuestVariables }) if (step is { RequiredQuestVariables: { } requiredQuestVariables })
{ {
if (!QuestWorkUtils.MatchesRequiredQuestWorkConfig(requiredQuestVariables, questWork, logger)) if (!QuestWorkUtils.MatchesRequiredQuestWorkConfig(requiredQuestVariables, questWork, logger))
{ {
@ -203,16 +204,17 @@ internal static class SkipCondition
} }
} }
if (SkipConditions.NearPosition is { } nearPosition && clientState.TerritoryType == Step.TerritoryId) if (skipConditions.NearPosition is { } nearPosition && clientState.TerritoryType == step.TerritoryId)
{ {
if (Vector3.Distance(nearPosition.Position, clientState.LocalPlayer!.Position) <= nearPosition.MaximumDistance) if (Vector3.Distance(nearPosition.Position, clientState.LocalPlayer!.Position) <=
nearPosition.MaximumDistance)
{ {
logger.LogInformation("Skipping step, as we're near the position"); logger.LogInformation("Skipping step, as we're near the position");
return true; return true;
} }
} }
if (SkipConditions.ExtraCondition == EExtraSkipCondition.WakingSandsMainArea && if (skipConditions.ExtraCondition == EExtraSkipCondition.WakingSandsMainArea &&
clientState.TerritoryType == 212) clientState.TerritoryType == 212)
{ {
var position = clientState.LocalPlayer!.Position; var position = clientState.LocalPlayer!.Position;
@ -223,7 +225,7 @@ internal static class SkipCondition
} }
} }
if (SkipConditions.ExtraCondition == EExtraSkipCondition.RisingStonesSolar && if (skipConditions.ExtraCondition == EExtraSkipCondition.RisingStonesSolar &&
clientState.TerritoryType == 351) clientState.TerritoryType == 351)
{ {
var position = clientState.LocalPlayer!.Position; var position = clientState.LocalPlayer!.Position;
@ -234,13 +236,13 @@ internal static class SkipCondition
} }
} }
if (Step.PickUpQuestId != null && questFunctions.IsQuestAcceptedOrComplete(Step.PickUpQuestId)) if (step.PickUpQuestId != null && questFunctions.IsQuestAcceptedOrComplete(step.PickUpQuestId))
{ {
logger.LogInformation("Skipping step, as we have already picked up the relevant quest"); logger.LogInformation("Skipping step, as we have already picked up the relevant quest");
return true; return true;
} }
if (Step.TurnInQuestId != null && questFunctions.IsQuestComplete(Step.TurnInQuestId)) if (step.TurnInQuestId != null && questFunctions.IsQuestComplete(step.TurnInQuestId))
{ {
logger.LogInformation("Skipping step, as we have already completed the relevant quest"); logger.LogInformation("Skipping step, as we have already completed the relevant quest");
return true; return true;

View File

@ -1,6 +1,4 @@
using System; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
@ -8,14 +6,14 @@ namespace Questionable.Controller.Steps.Shared;
internal static class StepDisabled internal static class StepDisabled
{ {
internal sealed class Factory(IServiceProvider serviceProvider) : 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 serviceProvider.GetRequiredService<Task>(); return new Task(loggerFactory.CreateLogger<Task>());
} }
} }

View File

@ -6,19 +6,11 @@ using Questionable.Controller.Steps.Common;
namespace Questionable.Controller.Steps.Shared; namespace Questionable.Controller.Steps.Shared;
internal sealed class SwitchClassJob(IClientState clientState) : AbstractDelayedTask internal sealed class SwitchClassJob(EClassJob classJob, IClientState clientState) : AbstractDelayedTask
{ {
private EClassJob _classJob;
public ITask With(EClassJob classJob)
{
_classJob = classJob;
return this;
}
protected override unsafe bool StartInternal() protected override unsafe bool StartInternal()
{ {
if (clientState.LocalPlayer!.ClassJob.Id == (uint)_classJob) if (clientState.LocalPlayer!.ClassJob.Id == (uint)classJob)
return false; return false;
var gearsetModule = RaptureGearsetModule.Instance(); var gearsetModule = RaptureGearsetModule.Instance();
@ -27,7 +19,7 @@ internal sealed class SwitchClassJob(IClientState clientState) : AbstractDelayed
for (int i = 0; i < 100; ++i) for (int i = 0; i < 100; ++i)
{ {
var gearset = gearsetModule->GetGearset(i); var gearset = gearsetModule->GetGearset(i);
if (gearset->ClassJob == (byte)_classJob) if (gearset->ClassJob == (byte)classJob)
{ {
gearsetModule->EquipGearset(gearset->Id, gearset->BannerIndex); gearsetModule->EquipGearset(gearset->Id, gearset->BannerIndex);
return true; return true;
@ -35,10 +27,10 @@ internal sealed class SwitchClassJob(IClientState clientState) : AbstractDelayed
} }
} }
throw new TaskException($"No gearset found for {_classJob}"); throw new TaskException($"No gearset found for {classJob}");
} }
protected override ETaskResult UpdateInternal() => ETaskResult.TaskComplete; protected override ETaskResult UpdateInternal() => ETaskResult.TaskComplete;
public override string ToString() => $"SwitchJob({_classJob})"; public override string ToString() => $"SwitchJob({classJob})";
} }

View File

@ -5,9 +5,6 @@ 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 Microsoft.Extensions.DependencyInjection;
using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Common;
using Questionable.Controller.Utils; using Questionable.Controller.Utils;
using Questionable.Data; using Questionable.Data;
@ -20,19 +17,20 @@ namespace Questionable.Controller.Steps.Shared;
internal static class WaitAtEnd internal static class WaitAtEnd
{ {
internal sealed class Factory( internal sealed class Factory(
IServiceProvider serviceProvider,
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)
{ {
if (step.CompletionQuestVariablesFlags.Count == 6 && QuestWorkUtils.HasCompletionFlags(step.CompletionQuestVariablesFlags)) if (step.CompletionQuestVariablesFlags.Count == 6 &&
QuestWorkUtils.HasCompletionFlags(step.CompletionQuestVariablesFlags))
{ {
var task = serviceProvider.GetRequiredService<WaitForCompletionFlags>() var task = new WaitForCompletionFlags((QuestId)quest.Id, step, questFunctions);
.With((QuestId)quest.Id, step); var delay = new WaitDelay();
var delay = serviceProvider.GetRequiredService<WaitDelay>();
return [task, delay, Next(quest, sequence)]; return [task, delay, Next(quest, sequence)];
} }
@ -43,15 +41,15 @@ internal static class WaitAtEnd
new WaitConditionTask(() => !condition[ConditionFlag.InCombat], "Wait(not in combat)"); new WaitConditionTask(() => !condition[ConditionFlag.InCombat], "Wait(not in combat)");
return return
[ [
serviceProvider.GetRequiredService<WaitDelay>(), new WaitDelay(),
notInCombat, notInCombat,
serviceProvider.GetRequiredService<WaitDelay>(), new WaitDelay(),
Next(quest, sequence) Next(quest, sequence)
]; ];
case EInteractionType.WaitForManualProgress: case EInteractionType.WaitForManualProgress:
case EInteractionType.Instruction: case EInteractionType.Instruction:
return [serviceProvider.GetRequiredService<WaitNextStepOrSequence>()]; return [new WaitNextStepOrSequence()];
case EInteractionType.Duty: case EInteractionType.Duty:
case EInteractionType.SinglePlayerDuty: case EInteractionType.SinglePlayerDuty:
@ -68,9 +66,9 @@ internal static class WaitAtEnd
return return
[ [
serviceProvider.GetRequiredService<WaitObjectAtPosition>() new WaitObjectAtPosition(step.DataId.Value, step.Position.Value, step.NpcWaitDistance ?? 0.05f,
.With(step.DataId.Value, step.Position.Value, step.NpcWaitDistance ?? 0.05f), gameFunctions),
serviceProvider.GetRequiredService<WaitDelay>(), new WaitDelay(),
Next(quest, sequence) Next(quest, sequence)
]; ];
@ -104,15 +102,14 @@ internal static class WaitAtEnd
return return
[ [
waitInteraction, waitInteraction,
serviceProvider.GetRequiredService<WaitDelay>(), new WaitDelay(),
Next(quest, sequence) Next(quest, sequence)
]; ];
case EInteractionType.AcceptQuest: case EInteractionType.AcceptQuest:
{ {
var accept = serviceProvider.GetRequiredService<WaitQuestAccepted>() var accept = new WaitQuestAccepted(step.PickUpQuestId ?? quest.Id, questFunctions);
.With(step.PickUpQuestId ?? quest.Id); var delay = new WaitDelay();
var delay = serviceProvider.GetRequiredService<WaitDelay>();
if (step.PickUpQuestId != null) if (step.PickUpQuestId != null)
return [accept, delay, Next(quest, sequence)]; return [accept, delay, Next(quest, sequence)];
else else
@ -121,9 +118,8 @@ internal static class WaitAtEnd
case EInteractionType.CompleteQuest: case EInteractionType.CompleteQuest:
{ {
var complete = serviceProvider.GetRequiredService<WaitQuestCompleted>() var complete = new WaitQuestCompleted(step.TurnInQuestId ?? quest.Id, questFunctions);
.With(step.TurnInQuestId ?? quest.Id); var delay = new WaitDelay();
var delay = serviceProvider.GetRequiredService<WaitDelay>();
if (step.TurnInQuestId != null) if (step.TurnInQuestId != null)
return [complete, delay, Next(quest, sequence)]; return [complete, delay, Next(quest, sequence)];
else else
@ -132,7 +128,7 @@ internal static class WaitAtEnd
case EInteractionType.Interact: case EInteractionType.Interact:
default: default:
return [serviceProvider.GetRequiredService<WaitDelay>(), Next(quest, sequence)]; return [new WaitDelay(), Next(quest, sequence)];
} }
} }
@ -142,14 +138,8 @@ internal static class WaitAtEnd
} }
} }
internal sealed class WaitDelay() : AbstractDelayedTask(TimeSpan.FromSeconds(1)) internal sealed class WaitDelay(TimeSpan? delay = null) : AbstractDelayedTask(delay ?? TimeSpan.FromSeconds(1))
{ {
public ITask With(TimeSpan delay)
{
Delay = delay;
return this;
}
protected override bool StartInternal() => true; protected override bool StartInternal() => true;
public override string ToString() => $"Wait(seconds: {Delay.TotalSeconds})"; public override string ToString() => $"Wait(seconds: {Delay.TotalSeconds})";
@ -164,100 +154,64 @@ internal static class WaitAtEnd
public override string ToString() => "Wait(next step or sequence)"; public override string ToString() => "Wait(next step or sequence)";
} }
internal sealed class WaitForCompletionFlags(QuestFunctions questFunctions) : ITask internal sealed class WaitForCompletionFlags(QuestId quest, QuestStep step, QuestFunctions questFunctions) : ITask
{ {
public QuestId Quest { get; set; } = null!;
public QuestStep Step { get; set; } = null!;
public IList<QuestWorkValue?> Flags { get; set; } = null!;
public ITask With(QuestId quest, QuestStep step)
{
Quest = quest;
Step = step;
Flags = step.CompletionQuestVariablesFlags;
return this;
}
public bool Start() => true; public bool Start() => true;
public ETaskResult Update() public ETaskResult Update()
{ {
QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(Quest); QuestProgressInfo? questWork = questFunctions.GetQuestProgressInfo(quest);
return questWork != null && return questWork != null &&
QuestWorkUtils.MatchesQuestWork(Step.CompletionQuestVariablesFlags, questWork) QuestWorkUtils.MatchesQuestWork(step.CompletionQuestVariablesFlags, questWork)
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : ETaskResult.StillRunning;
} }
public override string ToString() => public override string ToString() =>
$"Wait(QW: {string.Join(", ", Flags.Select(x => x?.ToString() ?? "-"))})"; $"Wait(QW: {string.Join(", ", step.CompletionQuestVariablesFlags.Select(x => x?.ToString() ?? "-"))})";
} }
internal sealed class WaitObjectAtPosition(GameFunctions gameFunctions) : ITask private sealed class WaitObjectAtPosition(
uint dataId,
Vector3 destination,
float distance,
GameFunctions gameFunctions) : ITask
{ {
public uint DataId { get; set; }
public Vector3 Destination { get; set; }
public float Distance { get; set; }
public ITask With(uint dataId, Vector3 destination, float distance)
{
DataId = dataId;
Destination = destination;
Distance = distance;
return this;
}
public bool Start() => true; public bool Start() => true;
public ETaskResult Update() => public ETaskResult Update() =>
gameFunctions.IsObjectAtPosition(DataId, Destination, Distance) gameFunctions.IsObjectAtPosition(dataId, destination, distance)
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : ETaskResult.StillRunning;
public override string ToString() => public override string ToString() =>
$"WaitObj({DataId} at {Destination.ToString("G", CultureInfo.InvariantCulture)} < {Distance})"; $"WaitObj({dataId} at {destination.ToString("G", CultureInfo.InvariantCulture)} < {distance})";
} }
internal sealed class WaitQuestAccepted(QuestFunctions questFunctions) : ITask internal sealed class WaitQuestAccepted(ElementId elementId, QuestFunctions questFunctions) : ITask
{ {
public ElementId ElementId { get; set; } = null!;
public ITask With(ElementId elementId)
{
ElementId = elementId;
return this;
}
public bool Start() => true; public bool Start() => true;
public ETaskResult Update() public ETaskResult Update()
{ {
return questFunctions.IsQuestAccepted(ElementId) return questFunctions.IsQuestAccepted(elementId)
? ETaskResult.TaskComplete ? ETaskResult.TaskComplete
: ETaskResult.StillRunning; : ETaskResult.StillRunning;
} }
public override string ToString() => $"WaitQuestAccepted({ElementId})"; public override string ToString() => $"WaitQuestAccepted({elementId})";
} }
internal sealed class WaitQuestCompleted(QuestFunctions questFunctions) : ITask internal sealed class WaitQuestCompleted(ElementId elementId, QuestFunctions questFunctions) : ITask
{ {
public ElementId ElementId { get; set; } = null!;
public ITask With(ElementId elementId)
{
ElementId = elementId;
return this;
}
public bool Start() => true; public bool Start() => true;
public ETaskResult Update() public ETaskResult Update()
{ {
return questFunctions.IsQuestComplete(ElementId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning; return questFunctions.IsQuestComplete(elementId) ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
} }
public override string ToString() => $"WaitQuestComplete({ElementId})"; public override string ToString() => $"WaitQuestComplete({elementId})";
} }
internal sealed class NextStep(ElementId elementId, int sequence) : ILastTask internal sealed class NextStep(ElementId elementId, int sequence) : ILastTask

View File

@ -1,5 +1,4 @@
using System; using System;
using Microsoft.Extensions.DependencyInjection;
using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Common;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
@ -8,26 +7,19 @@ namespace Questionable.Controller.Steps.Shared;
internal static class WaitAtStart internal static class WaitAtStart
{ {
internal sealed class Factory(IServiceProvider serviceProvider) : SimpleTaskFactory internal sealed class Factory : SimpleTaskFactory
{ {
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step) public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{ {
if (step.DelaySecondsAtStart == null) if (step.DelaySecondsAtStart == null)
return null; return null;
return serviceProvider.GetRequiredService<WaitDelay>() return new WaitDelay(TimeSpan.FromSeconds(step.DelaySecondsAtStart.Value));
.With(TimeSpan.FromSeconds(step.DelaySecondsAtStart.Value));
} }
} }
internal sealed class WaitDelay : AbstractDelayedTask internal sealed class WaitDelay(TimeSpan delay) : AbstractDelayedTask(delay)
{ {
public ITask With(TimeSpan delay)
{
Delay = delay;
return this;
}
protected override bool StartInternal() => true; protected override bool StartInternal() => true;
public override string ToString() => $"Wait[S](seconds: {Delay.TotalSeconds})"; public override string ToString() => $"Wait[S](seconds: {Delay.TotalSeconds})";

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
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;
@ -9,18 +10,19 @@ namespace Questionable.Controller.Steps;
internal sealed class TaskCreator internal sealed class TaskCreator
{ {
private readonly IReadOnlyList<ITaskFactory> _taskFactories; private readonly IServiceProvider _serviceProvider;
private readonly ILogger<TaskCreator> _logger; private readonly ILogger<TaskCreator> _logger;
public TaskCreator(IEnumerable<ITaskFactory> taskFactories, ILogger<TaskCreator> logger) public TaskCreator(IServiceProvider serviceProvider, ILogger<TaskCreator> logger)
{ {
_taskFactories = taskFactories.ToList().AsReadOnly(); _serviceProvider = serviceProvider;
_logger = logger; _logger = logger;
} }
public IReadOnlyList<ITask> CreateTasks(Quest quest, QuestSequence sequence, QuestStep step) public IReadOnlyList<ITask> CreateTasks(Quest quest, QuestSequence sequence, QuestStep step)
{ {
var newTasks = _taskFactories using var scope = _serviceProvider.CreateScope();
var newTasks = scope.ServiceProvider.GetRequiredService<IEnumerable<ITaskFactory>>()
.SelectMany(x => .SelectMany(x =>
{ {
IList<ITask> tasks = x.CreateAllTasks(quest, sequence, step).ToList(); IList<ITask> tasks = x.CreateAllTasks(quest, sequence, step).ToList();

View File

@ -125,56 +125,42 @@ public sealed class QuestionablePlugin : IDalamudPlugin
private static void AddTaskFactories(ServiceCollection serviceCollection) private static void AddTaskFactories(ServiceCollection serviceCollection)
{ {
// individual tasks // individual tasks
serviceCollection.AddTransient<MountTask>();
serviceCollection.AddTransient<UnmountTask>();
serviceCollection.AddTransient<MoveToLandingLocation>(); serviceCollection.AddTransient<MoveToLandingLocation>();
serviceCollection.AddTransient<DoGather>(); serviceCollection.AddTransient<DoGather>();
serviceCollection.AddTransient<DoGatherCollectable>(); serviceCollection.AddTransient<DoGatherCollectable>();
serviceCollection.AddTransient<SwitchClassJob>(); serviceCollection.AddTransient<SwitchClassJob>();
serviceCollection.AddSingleton<Mount.Factory>();
// task factories // task factories
serviceCollection.AddTaskWithFactory<StepDisabled.Factory, StepDisabled.Task>(); serviceCollection.AddTaskFactory<StepDisabled.Factory>();
serviceCollection.AddSingleton<ITaskFactory, EquipRecommended.BeforeDutyOrInstance>(); serviceCollection.AddTaskFactory<EquipRecommended.BeforeDutyOrInstance>();
serviceCollection.AddTaskWithFactory<GatheringRequiredItems.Factory, GatheringRequiredItems.StartGathering, GatheringRequiredItems.SkipMarker>(); serviceCollection.AddTaskFactory<GatheringRequiredItems.Factory>();
serviceCollection.AddTaskWithFactory<AetheryteShortcut.Factory, AetheryteShortcut.UseAetheryteShortcut>(); serviceCollection.AddTaskFactory<AetheryteShortcut.Factory>();
serviceCollection.AddTaskWithFactory<SkipCondition.Factory, SkipCondition.CheckSkip>(); serviceCollection.AddTaskFactory<SkipCondition.Factory>();
serviceCollection.AddTaskWithFactory<AethernetShortcut.Factory, AethernetShortcut.UseAethernetShortcut>(); serviceCollection.AddTaskFactory<AethernetShortcut.Factory>();
serviceCollection.AddTaskWithFactory<WaitAtStart.Factory, WaitAtStart.WaitDelay>(); serviceCollection.AddTaskFactory<WaitAtStart.Factory>();
serviceCollection.AddTaskWithFactory<Move.Factory, Move.MoveInternal, Move.ExpectToBeNearDataId, Move.Land>(); serviceCollection.AddTaskFactory<MoveTo.Factory>();
serviceCollection.AddTransient<Move.MoveBuilder>();
serviceCollection.AddTaskWithFactory<NextQuest.Factory, NextQuest.SetQuest>(); serviceCollection.AddTaskFactory<NextQuest.Factory>();
serviceCollection.AddTaskWithFactory<AetherCurrent.Factory, AetherCurrent.DoAttune>(); serviceCollection.AddTaskFactory<AetherCurrent.Factory>();
serviceCollection.AddTaskWithFactory<AethernetShard.Factory, AethernetShard.DoAttune>(); serviceCollection.AddTaskFactory<AethernetShard.Factory>();
serviceCollection.AddTaskWithFactory<Aetheryte.Factory, Aetheryte.DoAttune>(); serviceCollection.AddTaskFactory<Aetheryte.Factory>();
serviceCollection.AddTaskWithFactory<Combat.Factory, Combat.HandleCombat>(); serviceCollection.AddTaskFactory<Combat.Factory>();
serviceCollection.AddTaskWithFactory<Duty.Factory, Duty.OpenDutyFinder>(); serviceCollection.AddTaskFactory<Duty.Factory>();
serviceCollection.AddTaskWithFactory<Emote.Factory, Emote.UseOnObject, Emote.Use>(); serviceCollection.AddTaskFactory<Emote.Factory>();
serviceCollection.AddTaskWithFactory<Action.Factory, Action.UseOnObject>(); serviceCollection.AddTaskFactory<Action.Factory>();
serviceCollection.AddTaskWithFactory<Interact.Factory, Interact.DoInteract>(); serviceCollection.AddTaskFactory<Interact.Factory>();
serviceCollection.AddTaskWithFactory<Jump.Factory, Jump.SingleJump, Jump.RepeatedJumps>(); serviceCollection.AddTaskFactory<Jump.Factory>();
serviceCollection.AddTaskWithFactory<Dive.Factory, Dive.DoDive>(); serviceCollection.AddTaskFactory<Dive.Factory>();
serviceCollection.AddTaskWithFactory<Say.Factory, Say.UseChat>(); serviceCollection.AddTaskFactory<Say.Factory>();
serviceCollection serviceCollection.AddTaskFactory<UseItem.Factory>();
.AddTaskWithFactory<UseItem.Factory, UseItem.UseOnGround, UseItem.UseOnObject, UseItem.Use, serviceCollection.AddTaskFactory<EquipItem.Factory>();
UseItem.UseOnPosition>(); serviceCollection.AddTaskFactory<EquipRecommended.Factory>();
serviceCollection.AddTaskWithFactory<EquipItem.Factory, EquipItem.DoEquip>(); serviceCollection.AddTaskFactory<Craft.Factory>();
serviceCollection.AddTaskWithFactory<EquipRecommended.Factory, EquipRecommended.DoEquipRecommended>(); serviceCollection.AddTaskFactory<TurnInDelivery.Factory>();
serviceCollection.AddTaskWithFactory<Craft.Factory, Craft.DoCraft>(); serviceCollection.AddTaskFactory<InitiateLeve.Factory>();
serviceCollection.AddTaskWithFactory<TurnInDelivery.Factory, TurnInDelivery.SatisfactionSupplyTurnIn>();
serviceCollection
.AddTaskWithFactory<InitiateLeve.Factory,
InitiateLeve.SkipInitiateIfActive,
InitiateLeve.OpenJournal,
InitiateLeve.Initiate,
InitiateLeve.SelectDifficulty>();
serviceCollection serviceCollection.AddTaskFactory<WaitAtEnd.Factory>();
.AddTaskWithFactory<WaitAtEnd.Factory,
WaitAtEnd.WaitDelay,
WaitAtEnd.WaitNextStepOrSequence,
WaitAtEnd.WaitForCompletionFlags,
WaitAtEnd.WaitObjectAtPosition>();
serviceCollection.AddTransient<WaitAtEnd.WaitQuestAccepted>(); serviceCollection.AddTransient<WaitAtEnd.WaitQuestAccepted>();
serviceCollection.AddTransient<WaitAtEnd.WaitQuestCompleted>(); serviceCollection.AddTransient<WaitAtEnd.WaitQuestCompleted>();

View File

@ -6,79 +6,12 @@ namespace Questionable;
internal static class ServiceCollectionExtensions internal static class ServiceCollectionExtensions
{ {
public static void AddTaskWithFactory< public static void AddTaskFactory<
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] [MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] TFactory>(
TFactory,
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
TTask>(
this IServiceCollection serviceCollection) this IServiceCollection serviceCollection)
where TFactory : class, ITaskFactory where TFactory : class, ITaskFactory
where TTask : class, ITask
{ {
serviceCollection.AddSingleton<ITaskFactory, TFactory>(); serviceCollection.AddSingleton<ITaskFactory, TFactory>();
serviceCollection.AddTransient<TTask>(); serviceCollection.AddSingleton<TFactory>();
}
public static void AddTaskWithFactory<
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
TFactory,
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
TTask1,
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
TTask2>(
this IServiceCollection serviceCollection)
where TFactory : class, ITaskFactory
where TTask1 : class, ITask
where TTask2 : class, ITask
{
serviceCollection.AddSingleton<ITaskFactory, TFactory>();
serviceCollection.AddTransient<TTask1>();
serviceCollection.AddTransient<TTask2>();
}
public static void AddTaskWithFactory<
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
TFactory,
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
TTask1,
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
TTask2,
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
TTask3>(
this IServiceCollection serviceCollection)
where TFactory : class, ITaskFactory
where TTask1 : class, ITask
where TTask2 : class, ITask
where TTask3 : class, ITask
{
serviceCollection.AddSingleton<ITaskFactory, TFactory>();
serviceCollection.AddTransient<TTask1>();
serviceCollection.AddTransient<TTask2>();
serviceCollection.AddTransient<TTask3>();
}
public static void AddTaskWithFactory<
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
TFactory,
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
TTask1,
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
TTask2,
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
TTask3,
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
TTask4>(
this IServiceCollection serviceCollection)
where TFactory : class, ITaskFactory
where TTask1 : class, ITask
where TTask2 : class, ITask
where TTask3 : class, ITask
where TTask4 : class, ITask
{
serviceCollection.AddSingleton<ITaskFactory, TFactory>();
serviceCollection.AddTransient<TTask1>();
serviceCollection.AddTransient<TTask2>();
serviceCollection.AddTransient<TTask3>();
serviceCollection.AddTransient<TTask4>();
} }
} }