}
]
},
+ {
+ "Sequence": 2,
+ "Steps": [
+ {
+ "DataId": 2012185,
+ "Position": {
+ "X": -5.416992,
+ "Y": -49.05786,
+ "Z": -269.24548
+ },
+ "TerritoryId": 959,
+ "InteractionType": "WaitForManualProgress",
+ "Comment": "Follow Urianger"
+ }
+ ]
+ },
{
"Sequence": 3,
"Steps": [
"Z": -269.24548
},
"TerritoryId": 959,
- "InteractionType": "SinglePlayerDuty",
+ "InteractionType": "WaitForManualProgress",
"Comment": "Follow Urianger"
}
]
+++ /dev/null
-{
- "$schema": "https://carvel.li/questionable/quest-1.0",
- "Author": "liza",
- "QuestSequence": [
- {
- "Sequence": 0,
- "Steps": [
- {
- "DataId": 1038935,
- "Position": {
- "X": 68.5282,
- "Y": 75.72459,
- "Z": -23.51416
- },
- "StopDistance": 5,
- "TerritoryId": 959,
- "InteractionType": "Interact"
- }
- ]
- },
- {
- "Sequence": 1,
- "Steps": [
- {
- "Position": {
- "X": -126.76068,
- "Y": 61.04055,
- "Z": -76.382324
- },
- "TerritoryId": 959,
- "InteractionType": "WalkTo"
- },
- {
- "DataId": 2012014,
- "Position": {
- "X": -128.008,
- "Y": 66.33093,
- "Z": -68.2536
- },
- "StopDistance": 5,
- "TerritoryId": 959,
- "InteractionType": "AttuneAetherCurrent",
- "AetherCurrentId": 2818363
- },
- {
- "Position": {
- "X": -116.83438,
- "Y": 63.151585,
- "Z": -71.81973
- },
- "TerritoryId": 959,
- "InteractionType": "WalkTo",
- "Mount": true
- },
- {
- "DataId": 2012010,
- "Position": {
- "X": 42.58789,
- "Y": 124.01001,
- "Z": -167.04059
- },
- "TerritoryId": 959,
- "InteractionType": "AttuneAetherCurrent",
- "AetherCurrentId": 2818359
- },
- {
- "DataId": 1038936,
- "Position": {
- "X": 191.48547,
- "Y": 93.90228,
- "Z": -54.306885
- },
- "TerritoryId": 959,
- "InteractionType": "Interact"
- }
- ]
- },
- {
- "Sequence": 2,
- "Steps": [
- {
- "DataId": 2012235,
- "Position": {
- "X": 183.30652,
- "Y": 87.20532,
- "Z": -30.47229
- },
- "TerritoryId": 959,
- "InteractionType": "Combat",
- "EnemySpawnType": "AfterInteraction",
- "KillEnemyDataIds": [
- 13998,
- 14093,
- 13998
- ]
- }
- ]
- },
- {
- "Sequence": 3,
- "Steps": [
- {
- "DataId": 2012433,
- "Position": {
- "X": 183.30652,
- "Y": 87.17468,
- "Z": -30.380737
- },
- "TerritoryId": 959,
- "InteractionType": "Interact"
- }
- ]
- },
- {
- "Sequence": 4,
- "Steps": [
- {
- "DataId": 1038936,
- "Position": {
- "X": 191.48547,
- "Y": 93.90228,
- "Z": -54.306885
- },
- "TerritoryId": 959,
- "InteractionType": "Interact"
- }
- ]
- },
- {
- "Sequence": 5,
- "Steps": [
- {
- "Position": {
- "X": 304.50522,
- "Y": -144,
- "Z": -558.8681
- },
- "TerritoryId": 959,
- "InteractionType": "Jump",
- "AetheryteShortcut": "Mare Lamentorum - Bestways Burrow",
- "JumpDestination": {
- "Position": {
- "X": 307.72073,
- "Y": -143.15913,
- "Z": -563.4788
- },
- "StopDistance": 5
- }
- },
- {
- "DataId": 2012012,
- "Position": {
- "X": 316.39575,
- "Y": -154.98596,
- "Z": -595.5444
- },
- "StopDistance": 3,
- "TerritoryId": 959,
- "InteractionType": "AttuneAetherCurrent",
- "AetherCurrentId": 2818361
- },
- {
- "Position": {
- "X": 351.1467,
- "Y": -167.87698,
- "Z": -605.0467
- },
- "TerritoryId": 959,
- "InteractionType": "WalkTo"
- },
- {
- "DataId": 1038937,
- "Position": {
- "X": 381.7959,
- "Y": -168.00002,
- "Z": -594.62885
- },
- "TerritoryId": 959,
- "InteractionType": "Interact"
- }
- ]
- },
- {
- "Sequence": 6,
- "Steps": [
- {
- "DataId": 2012337,
- "Position": {
- "X": 466.8496,
- "Y": -168.01715,
- "Z": -654.13904
- },
- "TerritoryId": 959,
- "InteractionType": "SinglePlayerDuty",
- "Comment": "Find \"Runninway?\""
- }
- ]
- },
- {
- "Sequence": 255,
- "Steps": [
- {
- "DataId": 1038944,
- "Position": {
- "X": 361.65405,
- "Y": -168.00002,
- "Z": -628.19867
- },
- "TerritoryId": 959,
- "InteractionType": "Interact"
- }
- ]
- }
- ]
-}
--- /dev/null
+{
+ "$schema": "https://carvel.li/questionable/quest-1.0",
+ "Author": "liza",
+ "QuestSequence": [
+ {
+ "Sequence": 0,
+ "Steps": [
+ {
+ "DataId": 1038935,
+ "Position": {
+ "X": 68.5282,
+ "Y": 75.72459,
+ "Z": -23.51416
+ },
+ "StopDistance": 5,
+ "TerritoryId": 959,
+ "InteractionType": "Interact"
+ }
+ ]
+ },
+ {
+ "Sequence": 1,
+ "Steps": [
+ {
+ "Position": {
+ "X": -126.76068,
+ "Y": 61.04055,
+ "Z": -76.382324
+ },
+ "TerritoryId": 959,
+ "InteractionType": "WalkTo"
+ },
+ {
+ "DataId": 2012014,
+ "Position": {
+ "X": -128.008,
+ "Y": 66.33093,
+ "Z": -68.2536
+ },
+ "StopDistance": 5,
+ "TerritoryId": 959,
+ "InteractionType": "AttuneAetherCurrent",
+ "AetherCurrentId": 2818363
+ },
+ {
+ "Position": {
+ "X": -116.83438,
+ "Y": 63.151585,
+ "Z": -71.81973
+ },
+ "TerritoryId": 959,
+ "InteractionType": "WalkTo",
+ "Mount": true
+ },
+ {
+ "DataId": 2012010,
+ "Position": {
+ "X": 42.58789,
+ "Y": 124.01001,
+ "Z": -167.04059
+ },
+ "TerritoryId": 959,
+ "InteractionType": "AttuneAetherCurrent",
+ "AetherCurrentId": 2818359
+ },
+ {
+ "DataId": 1038936,
+ "Position": {
+ "X": 191.48547,
+ "Y": 93.90228,
+ "Z": -54.306885
+ },
+ "TerritoryId": 959,
+ "InteractionType": "Interact"
+ }
+ ]
+ },
+ {
+ "Sequence": 2,
+ "Steps": [
+ {
+ "DataId": 2012235,
+ "Position": {
+ "X": 183.30652,
+ "Y": 87.20532,
+ "Z": -30.47229
+ },
+ "TerritoryId": 959,
+ "InteractionType": "Combat",
+ "EnemySpawnType": "AfterInteraction",
+ "KillEnemyDataIds": [
+ 13998,
+ 14093,
+ 13998
+ ]
+ }
+ ]
+ },
+ {
+ "Sequence": 3,
+ "Steps": [
+ {
+ "DataId": 2012433,
+ "Position": {
+ "X": 183.30652,
+ "Y": 87.17468,
+ "Z": -30.380737
+ },
+ "TerritoryId": 959,
+ "InteractionType": "Interact"
+ }
+ ]
+ },
+ {
+ "Sequence": 4,
+ "Steps": [
+ {
+ "DataId": 1038936,
+ "Position": {
+ "X": 191.48547,
+ "Y": 93.90228,
+ "Z": -54.306885
+ },
+ "TerritoryId": 959,
+ "InteractionType": "Interact"
+ }
+ ]
+ },
+ {
+ "Sequence": 5,
+ "Steps": [
+ {
+ "Position": {
+ "X": 304.50522,
+ "Y": -144,
+ "Z": -558.8681
+ },
+ "TerritoryId": 959,
+ "InteractionType": "Jump",
+ "AetheryteShortcut": "Mare Lamentorum - Bestways Burrow",
+ "JumpDestination": {
+ "Position": {
+ "X": 307.72073,
+ "Y": -143.15913,
+ "Z": -563.4788
+ },
+ "StopDistance": 5
+ }
+ },
+ {
+ "DataId": 2012012,
+ "Position": {
+ "X": 316.39575,
+ "Y": -154.98596,
+ "Z": -595.5444
+ },
+ "StopDistance": 3,
+ "TerritoryId": 959,
+ "InteractionType": "AttuneAetherCurrent",
+ "AetherCurrentId": 2818361
+ },
+ {
+ "Position": {
+ "X": 351.1467,
+ "Y": -167.87698,
+ "Z": -605.0467
+ },
+ "TerritoryId": 959,
+ "InteractionType": "WalkTo"
+ },
+ {
+ "DataId": 1038937,
+ "Position": {
+ "X": 381.7959,
+ "Y": -168.00002,
+ "Z": -594.62885
+ },
+ "TerritoryId": 959,
+ "InteractionType": "Interact"
+ }
+ ]
+ },
+ {
+ "Sequence": 6,
+ "Steps": [
+ {
+ "DataId": 2012337,
+ "Position": {
+ "X": 466.8496,
+ "Y": -168.01715,
+ "Z": -654.13904
+ },
+ "TerritoryId": 959,
+ "InteractionType": "SinglePlayerDuty",
+ "Comment": "Find \"Runninway?\""
+ }
+ ]
+ },
+ {
+ "Sequence": 255,
+ "Steps": [
+ {
+ "DataId": 1038944,
+ "Position": {
+ "X": 361.65405,
+ "Y": -168.00002,
+ "Z": -628.19867
+ },
+ "TerritoryId": 959,
+ "InteractionType": "Interact"
+ }
+ ]
+ }
+ ]
+}
_logger.LogInformation("Using warp {Id}, {Prompt}", entry.RowId, excelPrompt);
addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
- if (increaseStepCount)
- _questController.IncreaseStepCount();
+ //if (increaseStepCount)
+ //_questController.IncreaseStepCount();
return;
}
}
public bool IsPathRunning => _navmeshIpc.IsPathRunning;
public bool IsPathfinding => _pathfindTask is { IsCompleted: false };
public DestinationData? Destination { get; private set; }
+ public DateTime MovementStartedAt { get; private set; } = DateTime.MaxValue;
public void Update()
{
if (_pathfindTask.IsCompletedSuccessfully)
{
_logger.LogInformation("Pathfinding complete, route: [{Route}]",
- string.Join(" → ", _pathfindTask.Result.Select(x => x.ToString("G", CultureInfo.InvariantCulture))));
+ string.Join(" → ",
+ _pathfindTask.Result.Select(x => x.ToString("G", CultureInfo.InvariantCulture))));
+
+ if (_pathfindTask.Result.Count == 0)
+ {
+ ResetPathfinding();
+ throw new PathfindingFailedException();
+ }
var navPoints = _pathfindTask.Result.Skip(1).ToList();
Vector3 start = _clientState.LocalPlayer?.Position ?? navPoints[0];
}
_navmeshIpc.MoveTo(navPoints, Destination.IsFlying);
+ MovementStartedAt = DateTime.Now;
+
ResetPathfinding();
}
else if (_pathfindTask.IsCompleted)
{
_logger.LogWarning("Unable to complete pathfinding task");
ResetPathfinding();
+ throw new PathfindingFailedException();
}
}
return pointOnFloor != null && Math.Abs(pointOnFloor.Value.Y - p.Y) > 0.5f;
}
- private void PrepareNavigation(EMovementType type, uint? dataId, Vector3 to, bool fly, bool sprint, float? stopDistance)
+ private void PrepareNavigation(EMovementType type, uint? dataId, Vector3 to, bool fly, bool sprint,
+ float? stopDistance)
{
ResetPathfinding();
_gameFunctions.ExecuteCommand("/automove off");
Destination = new DestinationData(dataId, to, stopDistance ?? (DefaultStopDistance - 0.2f), fly, sprint);
+ MovementStartedAt = DateTime.MaxValue;
}
- public void NavigateTo(EMovementType type, uint? dataId, Vector3 to, bool fly, bool sprint, float? stopDistance = null)
+ public void NavigateTo(EMovementType type, uint? dataId, Vector3 to, bool fly, bool sprint,
+ float? stopDistance = null)
{
fly |= _condition[ConditionFlag.Diving];
PrepareNavigation(type, dataId, to, fly, sprint, stopDistance);
_navmeshIpc.Pathfind(_clientState.LocalPlayer!.Position, to, fly, _cancellationTokenSource.Token);
}
- public void NavigateTo(EMovementType type, uint? dataId, List<Vector3> to, bool fly, bool sprint, float? stopDistance)
+ public void NavigateTo(EMovementType type, uint? dataId, List<Vector3> to, bool fly, bool sprint,
+ float? stopDistance)
{
fly |= _condition[ConditionFlag.Diving];
PrepareNavigation(type, dataId, to.Last(), fly, sprint, stopDistance);
_logger.LogInformation("Moving to {Destination}", Destination);
_navmeshIpc.MoveTo(to, fly);
+ MovementStartedAt = DateTime.Now;
}
public void ResetPathfinding()
Stop();
}
- public sealed record DestinationData(uint? DataId, Vector3 Position, float StopDistance, bool IsFlying, bool CanSprint);
+ public sealed record DestinationData(
+ uint? DataId,
+ Vector3 Position,
+ float StopDistance,
+ bool IsFlying,
+ bool CanSprint);
+
+ public sealed class PathfindingFailedException : Exception
+ {
+ public PathfindingFailedException()
+ {
+ }
+
+ public PathfindingFailedException(string message)
+ : base(message)
+ {
+ }
+
+ public PathfindingFailedException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
}
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Numerics;
+using System.Threading;
+using System.Threading.Tasks;
using Dalamud.Game.ClientState.Conditions;
+using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.Logging;
+using Questionable.Controller.Steps;
using Questionable.Data;
using Questionable.External;
using Questionable.Model;
private readonly GameFunctions _gameFunctions;
private readonly MovementController _movementController;
private readonly ILogger<QuestController> _logger;
- private readonly ICondition _condition;
- private readonly IChatGui _chatGui;
- private readonly IFramework _framework;
- private readonly AetheryteData _aetheryteData;
- private readonly LifestreamIpc _lifestreamIpc;
- private readonly TerritoryData _territoryData;
private readonly QuestRegistry _questRegistry;
-
- public QuestController(IClientState clientState, GameFunctions gameFunctions, MovementController movementController,
- ILogger<QuestController> logger, ICondition condition, IChatGui chatGui, IFramework framework,
- AetheryteData aetheryteData, LifestreamIpc lifestreamIpc, TerritoryData territoryData,
- QuestRegistry questRegistry)
+ private readonly IKeyState _keyState;
+ private readonly IReadOnlyList<ITaskFactory> _taskFactories;
+
+ private readonly Queue<ITask> _taskQueue = new();
+ private ITask? _currentTask;
+ private bool _automatic;
+
+ public QuestController(
+ IClientState clientState,
+ GameFunctions gameFunctions,
+ MovementController movementController,
+ ILogger<QuestController> logger,
+ QuestRegistry questRegistry,
+ IKeyState keyState,
+ IEnumerable<ITaskFactory> taskFactories)
{
_clientState = clientState;
_gameFunctions = gameFunctions;
_movementController = movementController;
_logger = logger;
- _condition = condition;
- _chatGui = chatGui;
- _framework = framework;
- _aetheryteData = aetheryteData;
- _lifestreamIpc = lifestreamIpc;
- _territoryData = territoryData;
_questRegistry = questRegistry;
+ _keyState = keyState;
+ _taskFactories = taskFactories.ToList().AsReadOnly();
}
}
public void Update()
+ {
+ UpdateCurrentQuest();
+
+ if (_keyState[VirtualKey.ESCAPE])
+ {
+ Stop();
+ _movementController.Stop();
+ }
+
+ if (CurrentQuest != null && CurrentQuest.Quest.Data.TerritoryBlacklist.Contains(_clientState.TerritoryType))
+ return;
+
+ UpdateCurrentTask();
+ }
+
+ private void UpdateCurrentQuest()
{
DebugState = null;
if (currentQuestId == 0)
{
if (CurrentQuest != null)
+ {
+ _logger.LogInformation("No current quest, resetting data");
CurrentQuest = null;
+ Stop();
+ }
}
else if (CurrentQuest == null || CurrentQuest.Quest.QuestId != currentQuestId)
{
if (_questRegistry.TryGetQuest(currentQuestId, out var quest))
+ {
+ _logger.LogInformation("New quest: {QuestName}", quest.Name);
CurrentQuest = new QuestProgress(quest, currentSequence, 0);
+ }
else if (CurrentQuest != null)
+ {
+ _logger.LogInformation("No active quest anymore? Not sure what happened...");
CurrentQuest = null;
+ }
+
+ Stop();
+ return;
}
if (CurrentQuest == null)
{
DebugState = "No quest active";
Comment = null;
+ Stop();
return;
}
- if (_condition[ConditionFlag.Occupied] || _condition[ConditionFlag.Occupied30] ||
- _condition[ConditionFlag.Occupied33] || _condition[ConditionFlag.Occupied38] ||
- _condition[ConditionFlag.Occupied39] || _condition[ConditionFlag.OccupiedInEvent] ||
- _condition[ConditionFlag.OccupiedInQuestEvent] || _condition[ConditionFlag.OccupiedInCutSceneEvent] ||
- _condition[ConditionFlag.Casting] || _condition[ConditionFlag.Unknown57])
+ if (_gameFunctions.IsOccupied())
{
DebugState = "Occupied";
return;
}
if (CurrentQuest.Sequence != currentSequence)
+ {
CurrentQuest = CurrentQuest with { Sequence = currentSequence, Step = 0 };
+ bool automatic = _automatic;
+ Stop();
+ if (automatic)
+ ExecuteNextStep(true);
+ }
+
var q = CurrentQuest.Quest;
var sequence = q.FindSequence(CurrentQuest.Sequence);
if (sequence == null)
{
DebugState = "Sequence not found";
Comment = null;
+ Stop();
return;
}
{
DebugState = "Step completed";
Comment = null;
+ Stop();
return;
}
{
DebugState = "Step not found";
Comment = null;
+ Stop();
return;
}
return (seq, seq.Steps[CurrentQuest.Step]);
}
- public void IncreaseStepCount()
+ public void IncreaseStepCount(bool shouldContinue = false)
{
(QuestSequence? seq, QuestStep? step) = GetNextStep();
if (CurrentQuest == null || seq == null || step == null)
return;
}
+ _logger.LogInformation("Increasing step count from {CurrentValue}", CurrentQuest.Step);
if (CurrentQuest.Step + 1 < seq.Steps.Count)
{
CurrentQuest = CurrentQuest with
StepProgress = new()
};
}
+
+
+ if (shouldContinue && _automatic)
+ ExecuteNextStep(true);
}
public void IncreaseDialogueChoicesSelected()
}
};
+ /* TODO Is this required?
if (CurrentQuest.StepProgress.DialogueChoicesSelected >= step.DialogueChoices.Count)
IncreaseStepCount();
+ */
}
- public unsafe void ExecuteNextStep()
+ public void Stop()
{
- (QuestSequence? seq, QuestStep? step) = GetNextStep();
- if (CurrentQuest == null || seq == null || step == null)
- {
- _logger.LogWarning("Could not retrieve next quest step, not doing anything");
- return;
- }
-
- if (step.Disabled)
- {
- _logger.LogInformation("Skipping step, as it is disabled");
- IncreaseStepCount();
- return;
- }
-
- if (!CurrentQuest.StepProgress.AetheryteShortcutUsed && step.AetheryteShortcut != null)
- {
- bool skipTeleport = false;
- ushort territoryType = _clientState.TerritoryType;
- if (step.TerritoryId == territoryType)
- {
- Vector3 pos = _clientState.LocalPlayer!.Position;
- if (_aetheryteData.CalculateDistance(pos, territoryType, step.AetheryteShortcut.Value) < 11 ||
- (step.AethernetShortcut != null &&
- (_aetheryteData.CalculateDistance(pos, territoryType, step.AethernetShortcut.From) < 20 ||
- _aetheryteData.CalculateDistance(pos, territoryType, step.AethernetShortcut.To) < 20)))
- {
- _logger.LogInformation("Skipping aetheryte teleport");
- skipTeleport = true;
- }
- }
-
- if (skipTeleport)
- {
- _logger.LogInformation("Marking aetheryte shortcut as used");
- CurrentQuest = CurrentQuest with
- {
- StepProgress = CurrentQuest.StepProgress with { AetheryteShortcutUsed = true }
- };
- }
- else
- {
- if (!_gameFunctions.IsAetheryteUnlocked(step.AetheryteShortcut.Value))
- {
- _logger.LogError("Aetheryte {Aetheryte} is not unlocked.", step.AetheryteShortcut.Value);
- _chatGui.Print($"[Questionable] Aetheryte {step.AetheryteShortcut.Value} is not unlocked.");
- }
- else if (_gameFunctions.TeleportAetheryte(step.AetheryteShortcut.Value))
- {
- _logger.LogInformation("Travelling via aetheryte...");
- CurrentQuest = CurrentQuest with
- {
- StepProgress = CurrentQuest.StepProgress with { AetheryteShortcutUsed = true }
- };
- }
- else
- {
- _logger.LogWarning("Unable to teleport to aetheryte");
- _chatGui.Print("[Questionable] Unable to teleport to aetheryte.");
- }
-
- return;
- }
- }
-
- if (!step.SkipIf.Contains(ESkipCondition.Never))
- {
- _logger.LogInformation("Checking skip conditions; {ConfiguredConditions}", string.Join(",", step.SkipIf));
-
- if (step.SkipIf.Contains(ESkipCondition.FlyingUnlocked) &&
- _gameFunctions.IsFlyingUnlocked(step.TerritoryId))
- {
- _logger.LogInformation("Skipping step, as flying is unlocked");
- IncreaseStepCount();
- return;
- }
-
- if (step.SkipIf.Contains(ESkipCondition.FlyingLocked) &&
- !_gameFunctions.IsFlyingUnlocked(step.TerritoryId))
- {
- _logger.LogInformation("Skipping step, as flying is locked");
- IncreaseStepCount();
- return;
- }
+ _currentTask = null;
- if (step is
- {
- DataId: not null,
- InteractionType: EInteractionType.AttuneAetheryte or EInteractionType.AttuneAethernetShard
- } &&
- _gameFunctions.IsAetheryteUnlocked((EAetheryteLocation)step.DataId.Value))
- {
- _logger.LogInformation("Skipping step, as aetheryte/aethernet shard is unlocked");
- IncreaseStepCount();
- return;
- }
+ if (_taskQueue.Count > 0)
+ _taskQueue.Clear();
- if (step is { DataId: not null, InteractionType: EInteractionType.AttuneAetherCurrent } &&
- _gameFunctions.IsAetherCurrentUnlocked(step.DataId.Value))
- {
- _logger.LogInformation("Skipping step, as current is unlocked");
- IncreaseStepCount();
- return;
- }
+ // reset task queue
+ _automatic = false;
+ }
- QuestWork? questWork = _gameFunctions.GetQuestEx(CurrentQuest.Quest.QuestId);
- if (questWork != null && step.MatchesQuestVariables(questWork.Value))
- {
- _logger.LogInformation("Skipping step, as quest variables match");
- IncreaseStepCount();
- return;
- }
- }
+ private void UpdateCurrentTask()
+ {
+ if (_gameFunctions.IsOccupied())
+ return;
- if (!CurrentQuest.StepProgress.AethernetShortcutUsed && step.AethernetShortcut != null)
+ if (_currentTask == null)
{
- if (_gameFunctions.IsAetheryteUnlocked(step.AethernetShortcut.From) &&
- _gameFunctions.IsAetheryteUnlocked(step.AethernetShortcut.To))
+ if (_taskQueue.TryDequeue(out ITask? upcomingTask))
{
- EAetheryteLocation from = step.AethernetShortcut.From;
- EAetheryteLocation to = step.AethernetShortcut.To;
- ushort territoryType = _clientState.TerritoryType;
- Vector3 playerPosition = _clientState.LocalPlayer!.Position;
-
- // closer to the source
- if (_aetheryteData.CalculateDistance(playerPosition, territoryType, from) <
- _aetheryteData.CalculateDistance(playerPosition, territoryType, to))
+ try
{
- if (_aetheryteData.CalculateDistance(playerPosition, territoryType, from) < 11)
+ _logger.LogInformation("Starting task {TaskName}", upcomingTask.ToString());
+ if (upcomingTask.Start())
{
- _logger.LogInformation("Using lifestream to teleport to {Destination}", to);
- _lifestreamIpc.Teleport(to);
- CurrentQuest = CurrentQuest with
- {
- StepProgress = CurrentQuest.StepProgress with { AethernetShortcutUsed = true }
- };
+ _currentTask = upcomingTask;
+ return;
}
else
{
- _logger.LogInformation("Moving to aethernet shortcut");
- _movementController.NavigateTo(EMovementType.Quest, (uint)from, _aetheryteData.Locations[from],
- false, true,
- AetheryteConverter.IsLargeAetheryte(from) ? 10.9f : 6.9f);
+ _logger.LogTrace("Task {TaskName} was skipped", upcomingTask.ToString());
+ return;
}
-
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Failed to start task {TaskName}", upcomingTask.ToString());
+ Stop();
return;
}
}
else
- _logger.LogWarning(
- "Aethernet shortcut not unlocked (from: {FromAetheryte}, to: {ToAetheryte}), walking manually",
- step.AethernetShortcut.From, step.AethernetShortcut.To);
+ return;
}
- if (step.TargetTerritoryId.HasValue && step.TerritoryId != step.TargetTerritoryId &&
- step.TargetTerritoryId == _clientState.TerritoryType)
+ ETaskResult result;
+ try
{
- // we assume whatever e.g. interaction, walkto etc. we have will trigger the zone transition
- _logger.LogInformation("Zone transition, skipping rest of step");
- IncreaseStepCount();
- return;
+ result = _currentTask.Update();
}
-
- if (step.InteractionType == EInteractionType.Jump && step.JumpDestination != null &&
- (_clientState.LocalPlayer!.Position - step.JumpDestination.Position).Length() <=
- (step.JumpDestination.StopDistance ?? 1f))
+ catch (Exception e)
{
- _logger.LogInformation("We're at the jump destination, skipping movement");
+ _logger.LogError(e, "Failed to update task {TaskName}", _currentTask.ToString());
+ Stop();
+ return;
}
- else if (step.Position != null)
- {
- float distance;
- if (step.InteractionType == EInteractionType.WalkTo)
- distance = step.StopDistance ?? 0.25f;
- else
- distance = step.StopDistance ?? MovementController.DefaultStopDistance;
-
- var position = _clientState.LocalPlayer?.Position ?? new Vector3();
- float actualDistance = (position - step.Position.Value).Length();
-
- if (step.Mount == true && !_gameFunctions.HasStatusPreventingSprintOrMount())
- {
- _logger.LogInformation("Step explicitly wants a mount, trying to mount...");
- if (!_condition[ConditionFlag.Mounted] && !_condition[ConditionFlag.InCombat] &&
- _territoryData.CanUseMount(_clientState.TerritoryType))
- {
- _gameFunctions.Mount();
- return;
- }
- }
- else if (step.Mount == false)
- {
- _logger.LogInformation("Step explicitly wants no mount, trying to unmount...");
- if (_condition[ConditionFlag.Mounted])
- {
- _gameFunctions.Unmount();
- return;
- }
- }
- if (!step.DisableNavmesh)
- {
- if (step.Mount != false && actualDistance > 30f && !_condition[ConditionFlag.Mounted] &&
- !_condition[ConditionFlag.InCombat] && _territoryData.CanUseMount(_clientState.TerritoryType) &&
- !_gameFunctions.HasStatusPreventingSprintOrMount())
- {
- _gameFunctions.Mount();
- return;
- }
-
- if (actualDistance > distance)
- {
- _movementController.NavigateTo(EMovementType.Quest, step.DataId, step.Position.Value,
- fly: step.Fly == true && _gameFunctions.IsFlyingUnlockedInCurrentZone(),
- sprint: step.Sprint != false,
- stopDistance: distance);
- return;
- }
- }
- else
- {
- // navmesh won't move close enough
- if (actualDistance > distance)
- {
- _movementController.NavigateTo(EMovementType.Quest, step.DataId, [step.Position.Value],
- fly: step.Fly == true && _gameFunctions.IsFlyingUnlockedInCurrentZone(),
- sprint: step.Sprint != false,
- stopDistance: distance);
- return;
- }
- }
- }
- else if (step.DataId != null && step.StopDistance != null)
+ switch (result)
{
- GameObject? gameObject = _gameFunctions.FindObjectByDataId(step.DataId.Value);
- if (gameObject == null ||
- (gameObject.Position - _clientState.LocalPlayer!.Position).Length() > step.StopDistance)
- {
- _logger.LogWarning("Object not found or too far away, no position so we can't move");
+ case ETaskResult.StillRunning:
return;
- }
- }
- _logger.LogInformation("Running logic for {InteractionType}", step.InteractionType);
- switch (step.InteractionType)
- {
- case EInteractionType.Interact:
- if (step.DataId != null)
- {
- GameObject? gameObject = _gameFunctions.FindObjectByDataId(step.DataId.Value);
- if (gameObject == null)
- {
- _logger.LogWarning("No game object with dataId {DataId}", step.DataId);
- return;
- }
+ case ETaskResult.SkipRemainingTasksForStep:
+ _logger.LogInformation("Result: {Result}, skipping remaining tasks for step", result);
+ _currentTask = null;
- if (!gameObject.IsTargetable && _condition[ConditionFlag.Mounted])
+ while (_taskQueue.TryDequeue(out ITask? nextTask))
+ {
+ if (nextTask is ILastTask)
{
- _gameFunctions.Unmount();
+ _currentTask = nextTask;
return;
}
-
- _gameFunctions.InteractWith(step.DataId.Value);
-
- // if we have any dialogue, that is handled in GameUiController
- if (step.DialogueChoices.Count == 0)
- IncreaseStepCount();
- }
- else
- _logger.LogWarning("Not interacting on current step, DataId is null");
-
- break;
-
- case EInteractionType.AttuneAethernetShard:
- if (step.DataId != null)
- {
- if (!_gameFunctions.IsAetheryteUnlocked((EAetheryteLocation)step.DataId.Value))
- _gameFunctions.InteractWith(step.DataId.Value);
-
- IncreaseStepCount();
- }
-
- break;
-
- case EInteractionType.AttuneAetheryte:
- if (step.DataId != null)
- {
- if (!_gameFunctions.IsAetheryteUnlocked((EAetheryteLocation)step.DataId.Value))
- _gameFunctions.InteractWith(step.DataId.Value);
-
- IncreaseStepCount();
- }
-
- break;
-
- case EInteractionType.AttuneAetherCurrent:
- if (step.DataId != null)
- {
- _logger.LogInformation(
- "{AetherCurrentId} is unlocked = {Unlocked}",
- step.AetherCurrentId,
- _gameFunctions.IsAetherCurrentUnlocked(step.AetherCurrentId.GetValueOrDefault()));
- if (step.AetherCurrentId == null ||
- !_gameFunctions.IsAetherCurrentUnlocked(step.AetherCurrentId.Value))
- _gameFunctions.InteractWith(step.DataId.Value);
-
- IncreaseStepCount();
- }
-
- break;
-
- case EInteractionType.WalkTo:
- IncreaseStepCount();
- break;
-
- case EInteractionType.UseItem:
- if (_gameFunctions.Unmount())
- return;
-
- if (step is { DataId: not null, ItemId: not null, GroundTarget: true })
- {
- _gameFunctions.UseItemOnGround(step.DataId.Value, step.ItemId.Value);
- IncreaseStepCount();
- }
- else if (step is { DataId: not null, ItemId: not null })
- {
- _gameFunctions.UseItem(step.DataId.Value, step.ItemId.Value);
- IncreaseStepCount();
- }
- else if (step.ItemId != null)
- {
- _gameFunctions.UseItem(step.ItemId.Value);
- IncreaseStepCount();
}
- break;
+ return;
- case EInteractionType.Combat:
- if (_gameFunctions.Unmount())
- return;
+ case ETaskResult.TaskComplete:
+ _logger.LogInformation("Result: {Result}, remaining tasks: {RemainingTaskCount}", result,
+ _taskQueue.Count);
+ _currentTask = null;
- if (step.EnemySpawnType != null)
- {
- if (step is { DataId: not null, EnemySpawnType: EEnemySpawnType.AfterInteraction })
- _gameFunctions.InteractWith(step.DataId.Value);
-
- if (step is { DataId: not null, ItemId: not null, EnemySpawnType: EEnemySpawnType.AfterItemUse })
- _gameFunctions.UseItem(step.DataId.Value, step.ItemId.Value);
+ // handled in next update
+ return;
- // next sequence should trigger automatically
- IncreaseStepCount();
- }
+ case ETaskResult.NextStep:
+ _logger.LogInformation("Result: {Result}", result);
+ IncreaseStepCount(true);
+ return;
- break;
+ case ETaskResult.End:
+ _logger.LogInformation("Result: {Result}", result);
+ Stop();
+ return;
+ }
+ }
- case EInteractionType.Emote:
- if (step is { DataId: not null, Emote: not null })
- {
- _gameFunctions.UseEmote(step.DataId.Value, step.Emote.Value);
- IncreaseStepCount();
- }
- else if (step.Emote != null)
- {
- _gameFunctions.UseEmote(step.Emote.Value);
- IncreaseStepCount();
- }
+ public void ExecuteNextStep(bool automatic)
+ {
+ Stop();
+ _automatic = automatic;
- break;
+ (QuestSequence? seq, QuestStep? step) = GetNextStep();
+ if (CurrentQuest == null || seq == null || step == null)
+ {
+ _logger.LogWarning("Could not retrieve next quest step, not doing anything");
+ return;
+ }
- case EInteractionType.Say:
- if (_condition[ConditionFlag.Mounted])
- {
- _gameFunctions.Unmount();
- return;
- }
+ var newTasks = _taskFactories
+ .SelectMany(x =>
+ {
+ IList<ITask> tasks = x.CreateAllTasks(CurrentQuest.Quest, seq, step).ToList();
- if (step.ChatMessage != null)
+ if (_logger.IsEnabled(LogLevel.Trace))
{
- string? excelString = _gameFunctions.GetDialogueText(CurrentQuest.Quest,
- step.ChatMessage.ExcelSheet,
- step.ChatMessage.Key);
- if (excelString == null)
- return;
+ string factoryName = x.GetType().FullName ?? x.GetType().Name;
+ if (factoryName.Contains('.', StringComparison.Ordinal))
+ factoryName = factoryName[(factoryName.LastIndexOf('.') + 1)..];
- _gameFunctions.ExecuteCommand($"/say {excelString}");
- IncreaseStepCount();
+ _logger.LogTrace("Factory {FactoryName} created Task {TaskNames}",
+ factoryName, string.Join(", ", tasks.Select(y => y.ToString())));
}
- break;
-
- case EInteractionType.WaitForObjectAtPosition:
- if (step is { DataId: not null, Position: not null } &&
- !_gameFunctions.IsObjectAtPosition(step.DataId.Value, step.Position.Value))
- {
- return;
- }
-
- IncreaseStepCount();
- break;
-
- case EInteractionType.WaitForManualProgress:
- // something needs to be done manually, the next sequence will be picked up automatically
- break;
-
- case EInteractionType.Duty:
- if (step.ContentFinderConditionId != null)
- _gameFunctions.OpenDutyFinder(step.ContentFinderConditionId.Value);
-
- break;
-
- case EInteractionType.SinglePlayerDuty:
- // TODO: Disable YesAlready, interact with NPC to open dialog, restore YesAlready
- // TODO: also implement check for territory blacklist
- break;
-
- case EInteractionType.Jump:
- if (step.JumpDestination != null && !_condition[ConditionFlag.Jumping])
- {
- float stopDistance = step.JumpDestination.StopDistance ?? 1f;
- if ((_clientState.LocalPlayer!.Position - step.JumpDestination.Position).Length() <= stopDistance)
- IncreaseStepCount();
- else
- {
- _movementController.NavigateTo(EMovementType.Quest, step.DataId,
- [step.JumpDestination.Position], false, false,
- step.JumpDestination.StopDistance ?? stopDistance);
- _framework.RunOnTick(() => ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2),
- TimeSpan.FromSeconds(step.JumpDestination.DelaySeconds ?? 0.5f));
- }
- }
+ return tasks;
+ })
+ .ToList();
+ if (newTasks.Count == 0)
+ {
+ _logger.LogInformation("Nothing to execute for step?");
+ return;
+ }
- break;
+ foreach (var task in newTasks)
+ _taskQueue.Enqueue(task);
+ }
- case EInteractionType.ShouldBeAJump:
- case EInteractionType.Instruction:
- // Need to manually forward
- break;
+ public IList<string> GetRemainingTaskNames() =>
+ _taskQueue.Select(x => x.ToString() ?? "?").ToList();
- default:
- _logger.LogWarning("Action '{InteractionType}' is not implemented", step.InteractionType);
- break;
- }
+ public string ToStatString()
+ {
+ return _currentTask == null ? $"- (+{_taskQueue.Count})" : $"{_currentTask} (+{_taskQueue.Count})";
}
public sealed record QuestProgress(
}
}
+ // TODO is this still required?
public sealed record StepProgress(
- bool AetheryteShortcutUsed = false,
- bool AethernetShortcutUsed = false,
int DialogueChoicesSelected = 0);
}
--- /dev/null
+using System;
+using System.Numerics;
+using Dalamud.Plugin.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Questionable.Data;
+using Questionable.External;
+using Questionable.Model;
+using Questionable.Model.V1;
+using Questionable.Model.V1.Converter;
+
+namespace Questionable.Controller.Steps.BaseFactory;
+
+internal static class AethernetShortcut
+{
+ internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
+ {
+ public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ if (step.AethernetShortcut == null)
+ return null;
+
+ return serviceProvider.GetRequiredService<UseAethernetShortcut>()
+ .With(step.AethernetShortcut.From, step.AethernetShortcut.To);
+ }
+ }
+
+ internal sealed class UseAethernetShortcut(
+ ILogger<UseAethernetShortcut> logger,
+ GameFunctions gameFunctions,
+ IClientState clientState,
+ AetheryteData aetheryteData,
+ LifestreamIpc lifestreamIpc,
+ MovementController movementController) : ITask
+ {
+ private bool _moving;
+ private bool _teleported;
+
+ public EAetheryteLocation From { get; set; }
+ public EAetheryteLocation To { get; set; }
+
+ public ITask With(EAetheryteLocation from, EAetheryteLocation to)
+ {
+ From = from;
+ To = to;
+ return this;
+ }
+
+ public bool Start()
+ {
+ if (gameFunctions.IsAetheryteUnlocked(From) &&
+ gameFunctions.IsAetheryteUnlocked(To))
+ {
+ ushort territoryType = clientState.TerritoryType;
+ Vector3 playerPosition = clientState.LocalPlayer!.Position;
+
+ // closer to the source
+ if (aetheryteData.CalculateDistance(playerPosition, territoryType, From) <
+ aetheryteData.CalculateDistance(playerPosition, territoryType, To))
+ {
+ if (aetheryteData.CalculateDistance(playerPosition, territoryType, From) < 11)
+ {
+ logger.LogInformation("Using lifestream to teleport to {Destination}", To);
+ lifestreamIpc.Teleport(To);
+
+ _teleported = true;
+ return true;
+ }
+ else
+ {
+ logger.LogInformation("Moving to aethernet shortcut");
+ _moving = true;
+ movementController.NavigateTo(EMovementType.Quest, (uint)From, aetheryteData.Locations[From],
+ false, true,
+ AetheryteConverter.IsLargeAetheryte(From) ? 10.9f : 6.9f);
+ return true;
+ }
+ }
+ }
+ else
+ logger.LogWarning(
+ "Aethernet shortcut not unlocked (from: {FromAetheryte}, to: {ToAetheryte}), walking manually",
+ From, To);
+
+ return false;
+ }
+
+ public ETaskResult Update()
+ {
+ if (_moving)
+ {
+ var movementStartedAt = movementController.MovementStartedAt;
+ if (movementStartedAt == DateTime.MaxValue || movementStartedAt.AddSeconds(2) >= DateTime.Now)
+ return ETaskResult.StillRunning;
+
+ if (!movementController.IsPathfinding && !movementController.IsPathRunning)
+ _moving = false;
+
+ return ETaskResult.StillRunning;
+ }
+
+ if (!_teleported)
+ {
+ logger.LogInformation("Using lifestream to teleport to {Destination}", To);
+ lifestreamIpc.Teleport(To);
+
+ _teleported = true;
+ return ETaskResult.StillRunning;
+ }
+
+ if (aetheryteData.CalculateDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero,
+ clientState.TerritoryType, To) > 11)
+ return ETaskResult.StillRunning;
+
+ return ETaskResult.TaskComplete;
+ }
+
+ public override string ToString() => $"UseAethernet({From} -> {To})";
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using Dalamud.Plugin.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Questionable.Controller.Steps.BaseTasks;
+using Questionable.Data;
+using Questionable.Model;
+using Questionable.Model.V1;
+
+namespace Questionable.Controller.Steps.BaseFactory;
+
+internal static class AetheryteShortcut
+{
+ internal sealed class Factory(IServiceProvider serviceProvider, GameFunctions gameFunctions) : ITaskFactory
+ {
+ public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ if (step.AetheryteShortcut == null)
+ return [];
+
+ var task = serviceProvider.GetRequiredService<UseAetheryteShortcut>()
+ .With(step, step.AetheryteShortcut.Value);
+ return [new WaitConditionTask(gameFunctions.CanTeleport, "CanTeleport"), task];
+ }
+
+ public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ => throw new InvalidOperationException();
+ }
+
+ internal sealed class UseAetheryteShortcut(
+ ILogger<UseAetheryteShortcut> logger,
+ GameFunctions gameFunctions,
+ IClientState clientState,
+ IChatGui chatGui,
+ AetheryteData aetheryteData) : ITask
+ {
+ private DateTime _continueAt;
+
+ public QuestStep Step { get; set; } = null!;
+ public EAetheryteLocation TargetAetheryte { get; set; }
+
+ public ITask With(QuestStep step, EAetheryteLocation targetAetheryte)
+ {
+ Step = step;
+ TargetAetheryte = targetAetheryte;
+ return this;
+ }
+
+ public bool Start()
+ {
+ _continueAt = DateTime.Now.AddSeconds(8);
+ ushort territoryType = clientState.TerritoryType;
+ if (Step.TerritoryId == territoryType)
+ {
+ Vector3 pos = clientState.LocalPlayer!.Position;
+ if (aetheryteData.CalculateDistance(pos, territoryType, TargetAetheryte) < 11 ||
+ (Step.AethernetShortcut != null &&
+ (aetheryteData.CalculateDistance(pos, territoryType, Step.AethernetShortcut.From) < 20 ||
+ aetheryteData.CalculateDistance(pos, territoryType, Step.AethernetShortcut.To) < 20)))
+ {
+ logger.LogInformation("Skipping aetheryte teleport");
+ return false;
+ }
+ }
+
+ if (!gameFunctions.IsAetheryteUnlocked(TargetAetheryte))
+ {
+ chatGui.Print($"[Questionable] Aetheryte {TargetAetheryte} is not unlocked.");
+ throw new TaskException("Aetheryte is not unlocked");
+ }
+ else if (gameFunctions.TeleportAetheryte(TargetAetheryte))
+ {
+ logger.LogInformation("Travelling via aetheryte...");
+ return true;
+ }
+ else
+ {
+ chatGui.Print("[Questionable] Unable to teleport to aetheryte.");
+ throw new TaskException("Unable to teleport to aetheryte");
+ }
+ }
+
+ public ETaskResult Update()
+ {
+
+ if (DateTime.Now >= _continueAt && clientState.TerritoryType == Step.TerritoryId)
+ return ETaskResult.TaskComplete;
+
+ return ETaskResult.StillRunning;
+ }
+
+ public override string ToString() => $"UseAetheryte({TargetAetheryte})";
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Numerics;
+using Dalamud.Game.ClientState.Objects.Types;
+using Dalamud.Plugin.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Questionable.Controller.Steps.BaseTasks;
+using Questionable.Model;
+using Questionable.Model.V1;
+
+namespace Questionable.Controller.Steps.BaseFactory;
+
+internal static class Move
+{
+ internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
+ {
+ public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ if (step.Position != null)
+ {
+ var builder = serviceProvider.GetRequiredService<MoveBuilder>();
+ builder.Step = step;
+ builder.Destination = step.Position.Value;
+ return builder.Build();
+ }
+ else if (step is { DataId: not null, StopDistance: not null })
+ {
+ var task = serviceProvider.GetRequiredService<ExpectToBeNearDataId>();
+ task.DataId = step.DataId.Value;
+ task.StopDistance = step.StopDistance.Value;
+ return [task];
+ }
+
+ return [];
+ }
+
+ public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ => throw new InvalidOperationException();
+ }
+
+ internal sealed class MoveBuilder(
+ IServiceProvider serviceProvider,
+ ILogger<MoveBuilder> logger,
+ GameFunctions gameFunctions,
+ IClientState clientState,
+ MovementController movementController)
+ {
+ public QuestStep Step { get; set; } = null!;
+ public Vector3 Destination { get; set; }
+
+ public IEnumerable<ITask> Build()
+ {
+ 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");
+ yield break;
+ }
+
+ yield return new WaitConditionTask(() => clientState.TerritoryType == Step.TerritoryId, $"Wait(territory: {Step.TerritoryId}");
+ yield return new WaitConditionTask(() => movementController.IsNavmeshReady, "Wait(navmesh ready)");
+
+ float distance;
+ if (Step.InteractionType == EInteractionType.WalkTo)
+ distance = Step.StopDistance ?? 0.25f;
+ else
+ distance = Step.StopDistance ?? MovementController.DefaultStopDistance;
+
+ var position = clientState.LocalPlayer?.Position ?? new Vector3();
+ float actualDistance = (position - Destination).Length();
+
+ if (Step.Mount == true)
+ yield return serviceProvider.GetRequiredService<MountTask>().With(Step.TerritoryId);
+ else if (Step.Mount == false)
+ yield return serviceProvider.GetRequiredService<UnmountTask>();
+
+ if (!Step.DisableNavmesh)
+ {
+ if (Step.Mount == null && actualDistance > 30f)
+ yield return serviceProvider.GetRequiredService<MountTask>().With(Step.TerritoryId);
+
+ if (actualDistance > distance)
+ {
+ yield return serviceProvider.GetRequiredService<MoveInternal>()
+ .With(Destination, m =>
+ {
+ m.NavigateTo(EMovementType.Quest, Step.DataId, Destination,
+ fly: Step.Fly == true && gameFunctions.IsFlyingUnlockedInCurrentZone(),
+ sprint: Step.Sprint != false,
+ stopDistance: distance);
+ });
+ }
+ }
+ else
+ {
+ // navmesh won't move close enough
+ if (actualDistance > distance)
+ {
+ yield return serviceProvider.GetRequiredService<MoveInternal>()
+ .With(Destination, m =>
+ {
+ m.NavigateTo(EMovementType.Quest, Step.DataId, [Destination],
+ fly: Step.Fly == true && gameFunctions.IsFlyingUnlockedInCurrentZone(),
+ sprint: Step.Sprint != false,
+ stopDistance: distance);
+ });
+ }
+ }
+ }
+ }
+
+ internal sealed class MoveInternal(MovementController movementController, ILogger<MoveInternal> logger) : ITask
+ {
+ public Action<MovementController> StartAction { get; set; } = null!;
+ public Vector3 Destination { get; set; }
+
+ public ITask With(Vector3 destination, Action<MovementController> startAction)
+ {
+ Destination = destination;
+ StartAction = startAction;
+ return this;
+ }
+
+ public bool Start()
+ {
+ logger.LogInformation("Moving to {Destination}", Destination.ToString("G", CultureInfo.InvariantCulture));
+ StartAction(movementController);
+ return true;
+ }
+
+ public ETaskResult Update()
+ {
+ if (movementController.IsPathfinding || movementController.IsPathRunning)
+ return ETaskResult.StillRunning;
+
+ DateTime movementStartedAt = movementController.MovementStartedAt;
+ if (movementStartedAt == DateTime.MaxValue || movementStartedAt.AddSeconds(2) >= DateTime.Now)
+ return ETaskResult.StillRunning;
+
+ return ETaskResult.TaskComplete;
+ }
+
+ public override string ToString() => $"MoveTo({Destination.ToString("G", CultureInfo.InvariantCulture)})";
+ }
+
+ internal sealed class ExpectToBeNearDataId(GameFunctions gameFunctions, IClientState clientState) : ITask
+ {
+ public uint DataId { get; set; }
+ public float StopDistance { get; set; }
+
+ public bool Start() => true;
+
+ public ETaskResult Update()
+ {
+ GameObject? gameObject = gameFunctions.FindObjectByDataId(DataId);
+ if (gameObject == null ||
+ (gameObject.Position - clientState.LocalPlayer!.Position).Length() > StopDistance)
+ {
+ throw new TaskException("Object not found or too far away, no position so we can't move");
+ }
+
+ return ETaskResult.TaskComplete;
+ }
+ }
+}
--- /dev/null
+using System;
+using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
+using FFXIVClientStructs.FFXIV.Client.System.Framework;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Questionable.Model;
+using Questionable.Model.V1;
+
+namespace Questionable.Controller.Steps.BaseFactory;
+
+internal static class SkipCondition
+{
+ internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
+ {
+ public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ if (step.SkipIf.Count == 0)
+ return null;
+
+ if (step.SkipIf.Contains(ESkipCondition.Never))
+ return null;
+
+ return serviceProvider.GetRequiredService<CheckTask>()
+ .With(step, quest.QuestId);
+ }
+ }
+
+ internal sealed class CheckTask(
+ ILogger<CheckTask> logger,
+ GameFunctions gameFunctions) : ITask
+ {
+ public QuestStep Step { get; set; } = null!;
+ public ushort QuestId { get; set; }
+
+ public ITask With(QuestStep step, ushort questId)
+ {
+ Step = step;
+ QuestId = questId;
+ return this;
+ }
+
+ public bool Start()
+ {
+ logger.LogInformation("Checking skip conditions; {ConfiguredConditions}", string.Join(",", Step.SkipIf));
+
+ if (Step.SkipIf.Contains(ESkipCondition.FlyingUnlocked) &&
+ gameFunctions.IsFlyingUnlocked(Step.TerritoryId))
+ {
+ logger.LogInformation("Skipping step, as flying is unlocked");
+ return true;
+ }
+
+ if (Step.SkipIf.Contains(ESkipCondition.FlyingLocked) &&
+ !gameFunctions.IsFlyingUnlocked(Step.TerritoryId))
+ {
+ logger.LogInformation("Skipping step, as flying is locked");
+ return true;
+ }
+
+ if (Step is
+ {
+ DataId: not null,
+ InteractionType: EInteractionType.AttuneAetheryte or EInteractionType.AttuneAethernetShard
+ } &&
+ gameFunctions.IsAetheryteUnlocked((EAetheryteLocation)Step.DataId.Value))
+ {
+ logger.LogInformation("Skipping step, as aetheryte/aethernet shard is unlocked");
+ return true;
+ }
+
+ if (Step is { DataId: not null, InteractionType: EInteractionType.AttuneAetherCurrent } &&
+ gameFunctions.IsAetherCurrentUnlocked(Step.DataId.Value))
+ {
+ logger.LogInformation("Skipping step, as current is unlocked");
+ return true;
+ }
+
+ QuestWork? questWork = gameFunctions.GetQuestEx(QuestId);
+ if (questWork != null && Step.MatchesQuestVariables(questWork.Value))
+ {
+ logger.LogInformation("Skipping step, as quest variables match");
+ return true;
+ }
+
+ return false;
+ }
+
+ public ETaskResult Update() => ETaskResult.SkipRemainingTasksForStep;
+
+ public override string ToString() => $"CheckSkip({string.Join(", ", Step.SkipIf)})";
+ }
+}
--- /dev/null
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Questionable.Model;
+using Questionable.Model.V1;
+
+namespace Questionable.Controller.Steps.BaseFactory;
+
+internal static class StepDisabled
+{
+ internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
+ {
+ public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ if (!step.Disabled)
+ return null;
+
+ return serviceProvider.GetRequiredService<Task>();
+ }
+ }
+
+ internal sealed class Task(ILogger<Task> logger) : ITask
+ {
+ public bool Start() => true;
+
+ public ETaskResult Update()
+ {
+ logger.LogInformation("Skipping step, as it is disabled");
+ return ETaskResult.SkipRemainingTasksForStep;
+ }
+
+ public override string ToString() => "StepDisabled";
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Numerics;
+using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
+using Microsoft.Extensions.DependencyInjection;
+using Questionable.Controller.Steps.BaseTasks;
+using Questionable.Model;
+using Questionable.Model.V1;
+
+namespace Questionable.Controller.Steps.BaseFactory;
+
+internal static class WaitAtEnd
+{
+ internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
+ {
+ public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ if (step.CompletionQuestVariablesFlags.Count == 6)
+ {
+ var task = serviceProvider.GetRequiredService<WaitForCompletionFlags>()
+ .With(quest, step);
+ var delay = serviceProvider.GetRequiredService<WaitDelay>();
+ return [task, delay, new NextStep()];
+ }
+
+ switch (step.InteractionType)
+ {
+ case EInteractionType.Combat:
+ case EInteractionType.WaitForManualProgress:
+ case EInteractionType.ShouldBeAJump:
+ case EInteractionType.Instruction:
+ return [serviceProvider.GetRequiredService<WaitNextStepOrSequence>()];
+
+ case EInteractionType.Duty:
+ case EInteractionType.SinglePlayerDuty:
+ return [new EndAutomation()];
+
+ case EInteractionType.WalkTo:
+ case EInteractionType.Jump:
+ // no need to wait if we're just moving around
+ return [new NextStep()];
+
+ case EInteractionType.WaitForObjectAtPosition:
+ return [serviceProvider.GetRequiredService<WaitObjectAtPosition>(), new NextStep()];
+
+ default:
+ return [serviceProvider.GetRequiredService<WaitDelay>(), new NextStep()];
+ }
+ }
+
+ public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ => throw new InvalidOperationException();
+ }
+
+ internal sealed class WaitDelay() : AbstractDelayedTask(TimeSpan.FromSeconds(1))
+ {
+ protected override bool StartInternal() => true;
+
+ public override string ToString() => $"Wait(seconds: {Delay.TotalSeconds})";
+ }
+
+ internal sealed class WaitNextStepOrSequence : ITask
+ {
+ public bool Start() => true;
+
+ public ETaskResult Update() => ETaskResult.StillRunning;
+
+ public override string ToString() => "Wait(next step or sequence)";
+ }
+
+ internal sealed class WaitForCompletionFlags(GameFunctions gameFunctions) : ITask
+ {
+ public Quest Quest { get; set; } = null!;
+ public QuestStep Step { get; set; } = null!;
+ public IList<short?> Flags { get; set; } = null!;
+
+ public ITask With(Quest quest, QuestStep step)
+ {
+ Quest = quest;
+ Step = step;
+ Flags = step.CompletionQuestVariablesFlags;
+ return this;
+ }
+
+ public bool Start() => true;
+
+ public ETaskResult Update()
+ {
+ QuestWork? questWork = gameFunctions.GetQuestEx(Quest.QuestId);
+ return questWork != null && Step.MatchesQuestVariables(questWork.Value)
+ ? ETaskResult.TaskComplete
+ : ETaskResult.StillRunning;
+ }
+
+ public override string ToString() =>
+ $"WaitCF({string.Join(", ", Flags.Select(x => x?.ToString(CultureInfo.InvariantCulture) ?? "-"))})";
+ }
+
+ internal sealed class WaitObjectAtPosition(GameFunctions gameFunctions) : ITask
+ {
+ public uint DataId { get; set; }
+ public Vector3 Destination { get; set; }
+
+ public bool Start() => true;
+
+ public ETaskResult Update() =>
+ gameFunctions.IsObjectAtPosition(DataId, Destination)
+ ? ETaskResult.TaskComplete
+ : ETaskResult.StillRunning;
+
+ public override string ToString() =>
+ $"WaitObj({DataId} at {Destination.ToString("G", CultureInfo.InvariantCulture)})";
+ }
+
+ internal sealed class NextStep : ILastTask
+ {
+ public bool Start() => true;
+
+ public ETaskResult Update() => ETaskResult.NextStep;
+
+ public override string ToString() => "Next Step";
+ }
+
+ internal sealed class EndAutomation : ILastTask
+ {
+ public bool Start() => true;
+
+ public ETaskResult Update() => ETaskResult.End;
+
+ public override string ToString() => "End automation";
+ }
+}
--- /dev/null
+using System;
+using Questionable.Model;
+using Questionable.Model.V1;
+
+namespace Questionable.Controller.Steps.BaseFactory;
+
+internal static class ZoneChange
+{
+ internal sealed class Factory : ITaskFactory
+ {
+ public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ return null;
+ }
+ }
+
+ internal sealed class WaitForZone : ITask
+ {
+ /* TODO: Unsure when this would evne be needed again, this should probably be moved to AFTER walkTo/interacting
+
+ if (step.TargetTerritoryId.HasValue && step.TerritoryId != step.TargetTerritoryId &&
+ step.TargetTerritoryId == _clientState.TerritoryType)
+ {
+ // we assume whatever e.g. interaction, walkto etc. we have will trigger the zone transition
+ _logger.LogInformation("Zone transition, skipping rest of step");
+ IncreaseStepCount();
+ return;
+ }
+ */
+ public bool Start() => throw new NotImplementedException();
+
+ public ETaskResult Update() => throw new NotImplementedException();
+
+ public override string ToString() => "WaitForZone";
+ }
+}
--- /dev/null
+using System;
+
+namespace Questionable.Controller.Steps.BaseTasks;
+
+internal abstract class AbstractDelayedTask : ITask
+{
+ protected readonly TimeSpan Delay;
+ private DateTime _continueAt;
+
+ protected AbstractDelayedTask(TimeSpan delay)
+ {
+ Delay = delay;
+ }
+
+ protected AbstractDelayedTask()
+ : this(TimeSpan.FromSeconds(5))
+ {
+ }
+
+ public bool Start()
+ {
+ _continueAt = DateTime.Now.Add(Delay);
+ return StartInternal();
+ }
+
+ protected abstract bool StartInternal();
+
+ public ETaskResult Update()
+ {
+ if (_continueAt >= DateTime.Now)
+ return ETaskResult.StillRunning;
+
+ return UpdateInternal();
+ }
+
+ protected virtual ETaskResult UpdateInternal() => ETaskResult.TaskComplete;
+}
--- /dev/null
+using Dalamud.Game.ClientState.Conditions;
+using Dalamud.Plugin.Services;
+using Microsoft.Extensions.Logging;
+using Questionable.Data;
+
+namespace Questionable.Controller.Steps.BaseTasks;
+
+internal sealed class MountTask(
+ GameFunctions gameFunctions,
+ ICondition condition,
+ TerritoryData territoryData,
+ ILogger<MountTask> logger) : ITask
+{
+ private ushort _territoryId;
+ private bool _mountTriggered;
+
+ public ITask With(ushort territoryId)
+ {
+ _territoryId = territoryId;
+ 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.HasStatusPreventingSprintOrMount())
+ return false;
+
+ logger.LogInformation("Step wants a mount, trying to mount in territory {Id}...", _territoryId);
+ if (!condition[ConditionFlag.InCombat])
+ {
+ _mountTriggered = gameFunctions.Mount();
+ return true;
+ }
+
+ return false;
+ }
+
+ public ETaskResult Update()
+ {
+ if (!_mountTriggered)
+ {
+ _mountTriggered = gameFunctions.Mount();
+ return ETaskResult.StillRunning;
+ }
+
+ return condition[ConditionFlag.Mounted]
+ ? ETaskResult.TaskComplete
+ : ETaskResult.StillRunning;
+ }
+
+ public override string ToString() => "Mount";
+}
--- /dev/null
+using Dalamud.Game.ClientState.Conditions;
+using Dalamud.Plugin.Services;
+using Microsoft.Extensions.Logging;
+
+namespace Questionable.Controller.Steps.BaseTasks;
+
+internal sealed class UnmountTask(ICondition condition, ILogger<UnmountTask> logger, GameFunctions gameFunctions)
+ : ITask
+{
+ private bool _unmountTriggered;
+
+ public bool Start()
+ {
+ if (!condition[ConditionFlag.Mounted])
+ return false;
+
+ logger.LogInformation("Step explicitly wants no mount, trying to unmount...");
+ _unmountTriggered = gameFunctions.Unmount();
+ return true;
+ }
+
+ public ETaskResult Update()
+ {
+ if (!_unmountTriggered)
+ {
+ _unmountTriggered = gameFunctions.Unmount();
+ return ETaskResult.StillRunning;
+ }
+
+ return condition[ConditionFlag.Mounted]
+ ? ETaskResult.StillRunning
+ : ETaskResult.TaskComplete;
+ }
+
+ public override string ToString() => "Unmount";
+}
--- /dev/null
+using System;
+
+namespace Questionable.Controller.Steps.BaseTasks;
+
+internal sealed class WaitConditionTask(Func<bool> predicate, string description) : ITask
+{
+ public bool Start() => predicate();
+
+ public ETaskResult Update() => predicate() ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
+
+ public override string ToString() => description;
+}
--- /dev/null
+namespace Questionable.Controller.Steps;
+
+internal enum ETaskResult
+{
+ StillRunning,
+
+ TaskComplete,
+
+ /// <summary>
+ /// This step is complete, regardless of what any other following tasks would do.
+ /// </summary>
+ SkipRemainingTasksForStep,
+
+ NextStep,
+ End,
+}
--- /dev/null
+namespace Questionable.Controller.Steps;
+
+internal interface ILastTask : ITask
+{
+
+}
--- /dev/null
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Questionable.Controller.Steps;
+
+internal interface ITask
+{
+ bool Start();
+
+ ETaskResult Update();
+}
--- /dev/null
+using System.Collections.Generic;
+using Questionable.Model;
+using Questionable.Model.V1;
+
+namespace Questionable.Controller.Steps;
+
+internal interface ITaskFactory
+{
+ ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step);
+
+ IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ var task = CreateTask(quest, sequence, step);
+ if (task != null)
+ yield return task;
+ }
+}
--- /dev/null
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Questionable.Model;
+using Questionable.Model.V1;
+
+namespace Questionable.Controller.Steps.InteractionFactory;
+
+internal static class AetherCurrent
+{
+ internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
+ {
+ public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ if (step.InteractionType != EInteractionType.AttuneAetherCurrent)
+ return null;
+
+ ArgumentNullException.ThrowIfNull(step.DataId);
+ ArgumentNullException.ThrowIfNull(step.AetherCurrentId);
+
+ return serviceProvider.GetRequiredService<DoAttune>()
+ .With(step.DataId.Value, step.AetherCurrentId.Value);
+ }
+ }
+
+ internal sealed class DoAttune(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()
+ {
+ if (!gameFunctions.IsAetherCurrentUnlocked(AetherCurrentId))
+ {
+ logger.LogInformation("Attuning to aether current {AetherCurrentId} / {DataId}", AetherCurrentId,
+ DataId);
+ gameFunctions.InteractWith(DataId);
+ return true;
+ }
+
+ logger.LogInformation("Already attuned to aether current {AetherCurrentId} / {DataId}", AetherCurrentId, DataId);
+ return false;
+ }
+
+ public ETaskResult Update() =>
+ gameFunctions.IsAetherCurrentUnlocked(AetherCurrentId)
+ ? ETaskResult.TaskComplete
+ : ETaskResult.StillRunning;
+
+ public override string ToString() => $"AttuneAetherCurrent({AetherCurrentId})";
+ }
+}
--- /dev/null
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Questionable.Model;
+using Questionable.Model.V1;
+
+namespace Questionable.Controller.Steps.InteractionFactory;
+
+internal static class AethernetShard
+{
+ internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
+ {
+ public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ if (step.InteractionType != EInteractionType.AttuneAethernetShard)
+ return null;
+
+ ArgumentNullException.ThrowIfNull(step.DataId);
+
+ return serviceProvider.GetRequiredService<DoAttune>()
+ .With((EAetheryteLocation)step.DataId);
+ }
+ }
+
+ internal sealed class DoAttune(GameFunctions gameFunctions, ILogger<DoAttune> logger) : ITask
+ {
+ public EAetheryteLocation AetheryteLocation { get; set; }
+
+ public ITask? With(EAetheryteLocation aetheryteLocation)
+ {
+ AetheryteLocation = aetheryteLocation;
+ return this;
+ }
+
+ public bool Start()
+ {
+ if (!gameFunctions.IsAetheryteUnlocked(AetheryteLocation))
+ {
+ logger.LogInformation("Attuning to aethernet shard {AethernetShard}", AetheryteLocation);
+ gameFunctions.InteractWith((uint)AetheryteLocation);
+ return true;
+ }
+
+ logger.LogInformation("Already attuned to aethernet shard {AethernetShard}", AetheryteLocation);
+ return false;
+ }
+
+ public ETaskResult Update() =>
+ gameFunctions.IsAetheryteUnlocked(AetheryteLocation)
+ ? ETaskResult.TaskComplete
+ : ETaskResult.StillRunning;
+
+ public override string ToString() => $"AttuneAethernetShard({AetheryteLocation})";
+ }
+}
--- /dev/null
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Questionable.Model;
+using Questionable.Model.V1;
+
+namespace Questionable.Controller.Steps.InteractionFactory;
+
+internal static class Aetheryte
+{
+ internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
+ {
+ public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ if (step.InteractionType != EInteractionType.AttuneAetheryte)
+ return null;
+
+ ArgumentNullException.ThrowIfNull(step.DataId);
+
+ return serviceProvider.GetRequiredService<DoAttune>()
+ .With((EAetheryteLocation)step.DataId.Value);
+ }
+ }
+
+ internal sealed class DoAttune(GameFunctions gameFunctions, ILogger<DoAttune> logger) : ITask
+ {
+ public EAetheryteLocation AetheryteLocation { get; set; }
+
+ public ITask With(EAetheryteLocation aetheryteLocation)
+ {
+ AetheryteLocation = aetheryteLocation;
+ return this;
+ }
+
+ public bool Start()
+ {
+ if (!gameFunctions.IsAetheryteUnlocked(AetheryteLocation))
+ {
+ logger.LogInformation("Attuning to aetheryte {Aetheryte}", AetheryteLocation);
+ gameFunctions.InteractWith((uint)AetheryteLocation);
+ return true;
+ }
+
+ logger.LogInformation("Already attuned to aetheryte {Aetheryte}", AetheryteLocation);
+ return false;
+ }
+
+ public ETaskResult Update() =>
+ gameFunctions.IsAetheryteUnlocked(AetheryteLocation)
+ ? ETaskResult.TaskComplete
+ : ETaskResult.StillRunning;
+
+ public override string ToString() => $"AttuneAetheryte({AetheryteLocation})";
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.DependencyInjection;
+using Questionable.Controller.Steps.BaseTasks;
+using Questionable.Model;
+using Questionable.Model.V1;
+
+namespace Questionable.Controller.Steps.InteractionFactory;
+
+internal static class Combat
+{
+ internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
+ {
+ public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ if (step.InteractionType != EInteractionType.Combat)
+ return [];
+
+ ArgumentNullException.ThrowIfNull(step.EnemySpawnType);
+
+ var unmount = serviceProvider.GetRequiredService<UnmountTask>();
+ if (step.EnemySpawnType == EEnemySpawnType.AfterInteraction)
+ {
+ ArgumentNullException.ThrowIfNull(step.DataId);
+
+ var task = serviceProvider.GetRequiredService<Interact.DoInteract>()
+ .With(step.DataId.Value);
+ return [unmount, task];
+ }
+ else if (step.EnemySpawnType == EEnemySpawnType.AfterItemUse)
+ {
+ ArgumentNullException.ThrowIfNull(step.DataId);
+ ArgumentNullException.ThrowIfNull(step.ItemId);
+
+ var task = serviceProvider.GetRequiredService<UseItem.UseOnObject>()
+ .With(step.DataId.Value, step.ItemId.Value);
+ return [unmount, task];
+ }
+ else
+ // automatically triggered when entering area, i.e. only unmount
+ return [unmount];
+ }
+
+ public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ => throw new InvalidOperationException();
+ }
+}
--- /dev/null
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Questionable.Model;
+using Questionable.Model.V1;
+
+namespace Questionable.Controller.Steps.InteractionFactory;
+
+internal static class Duty
+{
+ internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
+ {
+ public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ if (step.InteractionType != EInteractionType.Duty)
+ return null;
+
+ ArgumentNullException.ThrowIfNull(step.ContentFinderConditionId);
+
+ return serviceProvider.GetRequiredService<OpenDutyFinder>()
+ .With(step.ContentFinderConditionId.Value);
+ }
+ }
+
+ internal sealed class OpenDutyFinder(GameFunctions gameFunctions) : ITask
+ {
+ public uint ContentFinderConditionId { get; set; }
+
+ public ITask With(uint contentFinderConditionId)
+ {
+ ContentFinderConditionId = contentFinderConditionId;
+ return this;
+ }
+
+ public bool Start()
+ {
+ gameFunctions.OpenDutyFinder(ContentFinderConditionId);
+ return true;
+ }
+
+ public ETaskResult Update() => ETaskResult.TaskComplete;
+
+ public override string ToString() => $"OpenDutyFinder({ContentFinderConditionId})";
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.DependencyInjection;
+using Questionable.Controller.Steps.BaseTasks;
+using Questionable.Model;
+using Questionable.Model.V1;
+
+namespace Questionable.Controller.Steps.InteractionFactory;
+
+internal static class Emote
+{
+ internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
+ {
+ public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ if (step.InteractionType != EInteractionType.Emote)
+ return [];
+
+ ArgumentNullException.ThrowIfNull(step.Emote);
+
+ var unmount = serviceProvider.GetRequiredService<UnmountTask>();
+ if (step.DataId != null)
+ {
+ var task = serviceProvider.GetRequiredService<UseOnObject>().With(step.Emote.Value, step.DataId.Value);
+ return [unmount, task];
+ }
+ else
+ {
+ var task = serviceProvider.GetRequiredService<Use>().With(step.Emote.Value);
+ return [unmount, task];
+ }
+ }
+
+ public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ => throw new InvalidOperationException();
+ }
+
+ internal sealed class UseOnObject(GameFunctions gameFunctions) : 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()
+ {
+ gameFunctions.UseEmote(DataId, Emote);
+ return true;
+ }
+
+ public override string ToString() => $"Emote({Emote} on {DataId})";
+ }
+
+ internal sealed class Use(GameFunctions gameFunctions) : AbstractDelayedTask
+ {
+ public EEmote Emote { get; set; }
+
+ public ITask With(EEmote emote)
+ {
+ Emote = emote;
+ return this;
+ }
+
+ protected override bool StartInternal()
+ {
+ gameFunctions.UseEmote(Emote);
+ return true;
+ }
+
+ public override string ToString() => $"Emote({Emote})";
+ }
+}
--- /dev/null
+using System;
+using Dalamud.Game.ClientState.Conditions;
+using Dalamud.Game.ClientState.Objects.Enums;
+using Dalamud.Game.ClientState.Objects.Types;
+using Dalamud.Plugin.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Questionable.Model;
+using Questionable.Model.V1;
+
+namespace Questionable.Controller.Steps.InteractionFactory;
+
+internal static class Interact
+{
+ internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
+ {
+ public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ if (step.InteractionType != EInteractionType.Interact)
+ return null;
+
+ ArgumentNullException.ThrowIfNull(step.DataId);
+
+ return serviceProvider.GetRequiredService<DoInteract>().With(step.DataId.Value);
+ }
+ }
+
+ internal sealed class DoInteract(GameFunctions gameFunctions, ICondition condition, ILogger<DoInteract> logger)
+ : ITask
+ {
+ private bool _interacted;
+ private DateTime _continueAt = DateTime.MinValue;
+
+ private uint DataId { get; set; }
+
+ public ITask With(uint dataId)
+ {
+ DataId = dataId;
+ return this;
+ }
+
+ public bool Start()
+ {
+ GameObject? gameObject = gameFunctions.FindObjectByDataId(DataId);
+ if (gameObject == null)
+ {
+ logger.LogWarning("No game object with dataId {DataId}", DataId);
+ return false;
+ }
+
+ // this is only relevant for followers on quests
+ if (!gameObject.IsTargetable && condition[ConditionFlag.Mounted])
+ {
+ gameFunctions.Unmount();
+ _continueAt = DateTime.Now.AddSeconds(0.5);
+ return true;
+ }
+
+ if (gameObject.IsTargetable && HasAnyMarker(gameObject))
+ {
+ _interacted = gameFunctions.InteractWith(DataId);
+ _continueAt = DateTime.Now.AddSeconds(0.5);
+ return true;
+ }
+
+ return true;
+ }
+
+ public ETaskResult Update()
+ {
+ if (DateTime.Now <= _continueAt)
+ return ETaskResult.StillRunning;
+
+ if (!_interacted)
+ {
+ GameObject? gameObject = gameFunctions.FindObjectByDataId(DataId);
+ if (gameObject == null || !gameObject.IsTargetable || !HasAnyMarker(gameObject))
+ return ETaskResult.StillRunning;
+
+ _interacted = gameFunctions.InteractWith(DataId);
+ _continueAt = DateTime.Now.AddSeconds(0.5);
+ return ETaskResult.StillRunning;
+ }
+
+ return ETaskResult.TaskComplete;
+ }
+
+ private unsafe bool HasAnyMarker(GameObject gameObject)
+ {
+ if (gameObject.ObjectKind != ObjectKind.EventNpc)
+ return true;
+
+ var gameObjectStruct = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject.Address;
+ return gameObjectStruct->NamePlateIconId != 0;
+ }
+
+ public override string ToString() => $"Interact({DataId})";
+ }
+}
--- /dev/null
+using System;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using Microsoft.Extensions.DependencyInjection;
+using Questionable.Controller.Steps.BaseTasks;
+using Questionable.Model;
+using Questionable.Model.V1;
+
+namespace Questionable.Controller.Steps.InteractionFactory;
+
+internal static class Jump
+{
+ internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
+ {
+ public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ if (step.InteractionType != EInteractionType.Jump)
+ return null;
+
+ ArgumentNullException.ThrowIfNull(step.JumpDestination);
+
+ return serviceProvider.GetRequiredService<DoJump>()
+ .With(step.DataId, step.JumpDestination, step.Comment);
+ }
+ }
+
+ internal sealed class DoJump(
+ MovementController movementController,
+ IClientState clientState,
+ 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 bool Start()
+ {
+ float stopDistance = JumpDestination.StopDistance ?? 1f;
+ if ((clientState.LocalPlayer!.Position - JumpDestination.Position).Length() <= stopDistance)
+ return false;
+
+ movementController.NavigateTo(EMovementType.Quest, DataId, [JumpDestination.Position], false, false,
+ JumpDestination.StopDistance ?? stopDistance);
+ framework.RunOnTick(() =>
+ {
+ unsafe
+ {
+ ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2);
+ }
+ },
+ TimeSpan.FromSeconds(JumpDestination.DelaySeconds ?? 0.5f));
+ return true;
+ }
+
+ public ETaskResult Update()
+ {
+ if (movementController.IsPathfinding || movementController.IsPathRunning)
+ return ETaskResult.StillRunning;
+
+ DateTime movementStartedAt = movementController.MovementStartedAt;
+ if (movementStartedAt == DateTime.MaxValue || movementStartedAt.AddSeconds(2) >= DateTime.Now)
+ return ETaskResult.StillRunning;
+
+ return ETaskResult.TaskComplete;
+ }
+
+ public override string ToString() => $"Jump({Comment})";
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.DependencyInjection;
+using Questionable.Controller.Steps.BaseTasks;
+using Questionable.Model;
+using Questionable.Model.V1;
+
+namespace Questionable.Controller.Steps.InteractionFactory;
+
+internal static class Say
+{
+ internal sealed class Factory(IServiceProvider serviceProvider, GameFunctions gameFunctions) : ITaskFactory
+ {
+ public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ if (step.InteractionType != EInteractionType.Emote)
+ return [];
+
+
+ ArgumentNullException.ThrowIfNull(step.ChatMessage);
+
+ string? excelString = gameFunctions.GetDialogueText(quest, step.ChatMessage.ExcelSheet, step.ChatMessage.Key);
+ ArgumentNullException.ThrowIfNull(excelString);
+
+ var unmount = serviceProvider.GetRequiredService<UnmountTask>();
+ var task = serviceProvider.GetRequiredService<UseChat>().With(excelString);
+ return [unmount, task];
+ }
+
+ public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ => throw new InvalidOperationException();
+ }
+
+ internal sealed class UseChat(GameFunctions gameFunctions) : AbstractDelayedTask
+ {
+ public string ChatMessage { get; set; } = null!;
+
+ public ITask With(string chatMessage)
+ {
+ ChatMessage = chatMessage;
+ return this;
+ }
+
+ protected override bool StartInternal()
+ {
+ gameFunctions.ExecuteCommand($"/say {ChatMessage}");
+ return true;
+ }
+
+ public override string ToString() => $"Say({ChatMessage})";
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.DependencyInjection;
+using Questionable.Controller.Steps.BaseTasks;
+using Questionable.Model;
+using Questionable.Model.V1;
+
+namespace Questionable.Controller.Steps.InteractionFactory;
+
+internal static class UseItem
+{
+ internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
+ {
+ public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
+ {
+ if (step.InteractionType != EInteractionType.UseItem)
+ return [];
+
+ ArgumentNullException.ThrowIfNull(step.ItemId);
+
+ var unmount = serviceProvider.GetRequiredService<UnmountTask>();
+ if (step.GroundTarget == true)
+ {
+ ArgumentNullException.ThrowIfNull(step.DataId);
+
+ var task = serviceProvider.GetRequiredService<UseOnGround>()
+ .With(step.DataId.Value, step.ItemId.Value);
+ return [unmount, task];
+ }
+ else if (step.DataId != null)
+ {
+ var task = serviceProvider.GetRequiredService<UseOnObject>()
+ .With(step.DataId.Value, step.ItemId.Value);
+ return [unmount, task];
+ }
+ else
+ {
+ var task = serviceProvider.GetRequiredService<Use>()
+ .With(step.ItemId.Value);
+ return [unmount, task];
+ }
+ }
+
+ public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
+ => throw new InvalidOperationException();
+ }
+
+
+ internal sealed class UseOnGround(GameFunctions gameFunctions) : AbstractDelayedTask
+ {
+ public uint DataId { get; set; }
+ public uint ItemId { get; set; }
+
+ public ITask With(uint dataId, uint itemId)
+ {
+ DataId = dataId;
+ ItemId = itemId;
+ return this;
+ }
+
+ protected override bool StartInternal()
+ {
+ gameFunctions.UseItemOnGround(DataId, ItemId);
+ return true;
+ }
+
+ public override string ToString() => $"UseItem({ItemId} on ground at {DataId})";
+ }
+
+ internal sealed class UseOnObject(GameFunctions gameFunctions) : AbstractDelayedTask
+ {
+ public uint DataId { get; set; }
+ public uint ItemId { get; set; }
+
+ public ITask With(uint dataId, uint itemId)
+ {
+ DataId = dataId;
+ ItemId = itemId;
+ return this;
+ }
+
+ protected override bool StartInternal()
+ {
+ gameFunctions.UseItem(DataId, ItemId);
+ return true;
+ }
+
+ public override string ToString() => $"UseItem({ItemId} on {DataId})";
+ }
+
+ internal sealed class Use(GameFunctions gameFunctions) : AbstractDelayedTask
+ {
+ public uint ItemId { get; set; }
+
+ public ITask With(uint itemId)
+ {
+ ItemId = itemId;
+ return this;
+ }
+
+ protected override bool StartInternal()
+ {
+ gameFunctions.UseItem(ItemId);
+ return true;
+ }
+
+ public override string ToString() => $"UseItem({ItemId})";
+ }
+}
--- /dev/null
+using System;
+
+namespace Questionable.Controller.Steps;
+
+public class TaskException : Exception
+{
+ public TaskException()
+ {
+ }
+
+ public TaskException(string message)
+ : base(message)
+ {
+ }
+
+ public TaskException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+}
{
_questController.Update();
_navigationShortcutController.HandleNavigationShortcut();
- _movementController.Update();
+
+ try
+ {
+ _movementController.Update();
+ }
+ catch (MovementController.PathfindingFailedException)
+ {
+ _questController.Stop();
+ }
}
private void ProcessCommand(string command, string arguments)
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using LLib.GameUI;
using Lumina.Excel.CustomSheets;
using Lumina.Excel.GeneratedSheets;
using Microsoft.Extensions.Logging;
private readonly ICondition _condition;
private readonly IClientState _clientState;
private readonly QuestRegistry _questRegistry;
+ private readonly IGameGui _gameGui;
private readonly ILogger<GameFunctions> _logger;
public GameFunctions(IDataManager dataManager, IObjectTable objectTable, ISigScanner sigScanner,
ITargetManager targetManager, ICondition condition, IClientState clientState, QuestRegistry questRegistry,
- ILogger<GameFunctions> logger)
+ IGameGui gameGui, ILogger<GameFunctions> logger)
{
_dataManager = dataManager;
_objectTable = objectTable;
_condition = condition;
_clientState = clientState;
_questRegistry = questRegistry;
+ _gameGui = gameGui;
_logger = logger;
_processChatBox =
Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(sigScanner.ScanText(Signatures.SendChat));
public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation)
=> IsAetheryteUnlocked((uint)aetheryteLocation, out _);
+ public bool CanTeleport() => ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0;
+
public bool TeleportAetheryte(uint aetheryteId)
{
var status = ActionManager.Instance()->GetActionStatus(ActionType.Action, 5);
return null;
}
- public void InteractWith(uint dataId)
+ public bool InteractWith(uint dataId)
{
GameObject? gameObject = FindObjectByDataId(dataId);
if (gameObject != null)
_logger.LogInformation("Setting target with {DataId} to {ObjectId}", dataId, gameObject.ObjectId);
_targetManager.Target = gameObject;
- TargetSystem.Instance()->InteractWithObject(
+ ulong result = TargetSystem.Instance()->InteractWithObject(
(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject.Address, false);
+ return result != 0;
}
+
+ return false;
}
public void UseItem(uint itemId)
return false;
}
- public void Mount()
+ public bool Mount()
{
- if (!_condition[ConditionFlag.Mounted])
+ if (_condition[ConditionFlag.Mounted])
+ return true;
+
+ var playerState = PlayerState.Instance();
+ if (playerState != null && playerState->IsMountUnlocked(71))
{
- var playerState = PlayerState.Instance();
- if (playerState != null && playerState->IsMountUnlocked(71))
+ if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, 71) == 0)
{
- if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, 71) == 0)
- {
- _logger.LogInformation("Using SDS Fenrir as mount");
- ActionManager.Instance()->UseAction(ActionType.Mount, 71);
- }
+ _logger.LogInformation("Using SDS Fenrir as mount");
+ return ActionManager.Instance()->UseAction(ActionType.Mount, 71);
}
- else
+ }
+ else
+ {
+ if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 9) == 0)
{
- if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 9) == 0)
- {
- _logger.LogInformation("Using mount roulette");
- ActionManager.Instance()->UseAction(ActionType.GeneralAction, 9);
- }
+ _logger.LogInformation("Using mount roulette");
+ return ActionManager.Instance()->UseAction(ActionType.GeneralAction, 9);
}
}
+
+ return false;
}
public bool Unmount()
{
- if (_condition[ConditionFlag.Mounted])
- {
- if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23) == 0)
- {
- _logger.LogInformation("Unmounting...");
- ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23);
- }
- else
- _logger.LogWarning("Can't unmount right now?");
+ if (!_condition[ConditionFlag.Mounted])
+ return false;
- return true;
+ if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23) == 0)
+ {
+ _logger.LogInformation("Unmounting...");
+ ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23);
}
+ else
+ _logger.LogWarning("Can't unmount right now?");
- return false;
+ return true;
}
public void OpenDutyFinder(uint contentFinderConditionId)
var questRow = _dataManager.GetExcelSheet<ContentTalk>()!.GetRow(rowId);
return questRow?.Text?.ToString();
}
+
+ public bool IsOccupied()
+ {
+ if (_gameGui.TryGetAddonByName("FadeMiddle", out AtkUnitBase* fade) &&
+ LAddon.IsAddonReady(fade) &&
+ fade->IsVisible)
+ return true;
+
+ return _condition[ConditionFlag.Occupied] || _condition[ConditionFlag.Occupied30] ||
+ _condition[ConditionFlag.Occupied33] || _condition[ConditionFlag.Occupied38] ||
+ _condition[ConditionFlag.Occupied39] || _condition[ConditionFlag.OccupiedInEvent] ||
+ _condition[ConditionFlag.OccupiedInQuestEvent] || _condition[ConditionFlag.OccupiedInCutSceneEvent] ||
+ _condition[ConditionFlag.Casting] || _condition[ConditionFlag.Unknown57] ||
+ _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
+ }
}
+++ /dev/null
-using System.Diagnostics.CodeAnalysis;
-
-[assembly: SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global",
- Justification = "Properties are used for serialization",
- Scope = "namespaceanddescendants",
- Target = "Questionable.Model.V1")]
-[assembly: SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global",
- Justification = "Properties are used for serialization",
- Scope = "namespaceanddescendants",
- Target = "Questionable.Model.V1")]
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
- <Version>0.5</Version>
+ <Version>0.6</Version>
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Dalamud.Extensions.MicrosoftLogging" Version="3.0.0" />
+ <PackageReference Include="Dalamud.Extensions.MicrosoftLogging" Version="4.0.1" />
<PackageReference Include="DalamudPackager" Version="2.1.12"/>
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" ExcludeAssets="runtime"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<ItemGroup>
<ProjectReference Include="..\LLib\LLib.csproj" />
- <ProjectReference Include="..\QuestPaths\QuestPaths.csproj" Condition="'$(Configuration)' == 'Release'" />
+ <ProjectReference Include="..\QuestPaths\QuestPaths.csproj" />
</ItemGroup>
</Project>
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Questionable.Controller;
+using Questionable.Controller.Steps;
+using Questionable.Controller.Steps.BaseFactory;
+using Questionable.Controller.Steps.BaseTasks;
+using Questionable.Controller.Steps.InteractionFactory;
using Questionable.Data;
using Questionable.External;
using Questionable.Windows;
ICondition condition,
IChatGui chatGui,
ICommandManager commandManager,
- IAddonLifecycle addonLifecycle)
+ IAddonLifecycle addonLifecycle,
+ IKeyState keyState)
{
ArgumentNullException.ThrowIfNull(pluginInterface);
ServiceCollection serviceCollection = new();
serviceCollection.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace)
.ClearProviders()
- .AddDalamudLogger(pluginLog));
+ .AddDalamudLogger(pluginLog, t => t[(t.LastIndexOf('.') + 1)..]));
serviceCollection.AddSingleton<IDalamudPlugin>(this);
serviceCollection.AddSingleton(pluginInterface);
serviceCollection.AddSingleton(clientState);
serviceCollection.AddSingleton(chatGui);
serviceCollection.AddSingleton(commandManager);
serviceCollection.AddSingleton(addonLifecycle);
+ serviceCollection.AddSingleton(keyState);
serviceCollection.AddSingleton(new WindowSystem(nameof(Questionable)));
serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration());
serviceCollection.AddSingleton<NavmeshIpc>();
serviceCollection.AddSingleton<LifestreamIpc>();
+ // individual tasks
+ serviceCollection.AddTransient<MountTask>();
+ serviceCollection.AddTransient<UnmountTask>();
+
+ // tasks with factories
+ serviceCollection.AddTaskWithFactory<StepDisabled.Factory, StepDisabled.Task>();
+ serviceCollection.AddTaskWithFactory<AetheryteShortcut.Factory, AetheryteShortcut.UseAetheryteShortcut>();
+ serviceCollection.AddTaskWithFactory<SkipCondition.Factory, SkipCondition.CheckTask>();
+ serviceCollection.AddTaskWithFactory<AethernetShortcut.Factory, AethernetShortcut.UseAethernetShortcut>();
+ serviceCollection.AddTaskWithFactory<Move.Factory, Move.MoveInternal, Move.ExpectToBeNearDataId>();
+ serviceCollection.AddTransient<Move.MoveBuilder>();
+
+ serviceCollection.AddTaskWithFactory<AetherCurrent.Factory, AetherCurrent.DoAttune>();
+ serviceCollection.AddTaskWithFactory<AethernetShard.Factory, AethernetShard.DoAttune>();
+ serviceCollection.AddTaskWithFactory<Aetheryte.Factory, Aetheryte.DoAttune>();
+ serviceCollection.AddSingleton<ITaskFactory, Combat.Factory>();
+ serviceCollection.AddTaskWithFactory<Duty.Factory, Duty.OpenDutyFinder>();
+ serviceCollection.AddTaskWithFactory<Emote.Factory, Emote.UseOnObject, Emote.Use>();
+ serviceCollection.AddTaskWithFactory<Interact.Factory, Interact.DoInteract>();
+ serviceCollection.AddTaskWithFactory<Jump.Factory, Jump.DoJump>();
+ serviceCollection.AddTaskWithFactory<Say.Factory, Say.UseChat>();
+ serviceCollection.AddTaskWithFactory<UseItem.Factory, UseItem.UseOnGround, UseItem.UseOnObject, UseItem.Use>();
+
+ // TODO sort this in properly
+ serviceCollection.AddTaskWithFactory<ZoneChange.Factory, ZoneChange.WaitForZone>();
+
+ serviceCollection
+ .AddTaskWithFactory<WaitAtEnd.Factory,
+ WaitAtEnd.WaitDelay,
+ WaitAtEnd.WaitNextStepOrSequence,
+ WaitAtEnd.WaitForCompletionFlags,
+ WaitAtEnd.WaitObjectAtPosition>();
+
serviceCollection.AddSingleton<MovementController>();
serviceCollection.AddSingleton<QuestRegistry>();
serviceCollection.AddSingleton<QuestController>();
--- /dev/null
+using JetBrains.Annotations;
+using Microsoft.Extensions.DependencyInjection;
+using Questionable.Controller.Steps;
+
+namespace Questionable;
+
+internal static class ServiceCollectionExtensions
+{
+ public static void AddTaskWithFactory<
+ [MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
+ TFactory,
+ [MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
+ TTask>(
+ this IServiceCollection serviceCollection)
+ where TFactory : class, ITaskFactory
+ where TTask : class, ITask
+ {
+ serviceCollection.AddSingleton<ITaskFactory, TFactory>();
+ serviceCollection.AddTransient<TTask>();
+ }
+
+ 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>();
+ }
+}
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
+using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using ImGuiNET;
using LLib.ImGui;
+using Microsoft.Extensions.Logging;
using Questionable.Controller;
using Questionable.Model;
using Questionable.Model.V1;
private readonly ITargetManager _targetManager;
private readonly GameUiController _gameUiController;
private readonly Configuration _configuration;
+ private readonly ILogger<DebugWindow> _logger;
public DebugWindow(DalamudPluginInterface pluginInterface, WindowSystem windowSystem,
MovementController movementController, QuestController questController, GameFunctions gameFunctions,
IClientState clientState, IFramework framework, ITargetManager targetManager, GameUiController gameUiController,
- Configuration configuration)
+ Configuration configuration, ILogger<DebugWindow> logger)
: base("Questionable", ImGuiWindowFlags.AlwaysAutoResize)
{
_pluginInterface = pluginInterface;
_targetManager = targetManager;
_gameUiController = gameUiController;
_configuration = configuration;
+ _logger = logger;
IsOpen = true;
SizeConstraints = new WindowSizeConstraints
ImGui.EndDisabled();
ImGui.TextUnformatted(_questController.Comment ?? "--");
- var nextStep = _questController.GetNextStep();
- ImGui.BeginDisabled(nextStep.Step == null);
- ImGui.Text(string.Create(CultureInfo.InvariantCulture,
- $"{nextStep.Step?.InteractionType} @ {nextStep.Step?.Position}"));
+ //var nextStep = _questController.GetNextStep();
+ //ImGui.BeginDisabled(nextStep.Step == null);
+ ImGui.Text(_questController.ToStatString());
+ //ImGui.EndDisabled();
+
if (ImGuiComponents.IconButton(FontAwesomeIcon.Play))
{
- _questController.ExecuteNextStep();
+ _questController.ExecuteNextStep(true);
}
ImGui.SameLine();
- if (ImGuiComponents.IconButton(FontAwesomeIcon.StepForward))
+ if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.StepForward, "Step"))
{
- _questController.IncreaseStepCount();
+ _questController.ExecuteNextStep(false);
}
- ImGui.EndDisabled();
+ ImGui.SameLine();
+
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.Stop))
+ {
+ _movementController.Stop();
+ _questController.Stop();
+ }
+
+ if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.ArrowCircleRight, "Skip"))
+ {
+ _questController.Stop();
+ _questController.IncreaseStepCount();
+ }
}
else
ImGui.Text("No active quest");
ImGui.Separator();
ImGui.Text(string.Create(CultureInfo.InvariantCulture,
$"Target: {_targetManager.Target.Name} ({_targetManager.Target.ObjectKind}; {_targetManager.Target.DataId})"));
+
+ GameObject* gameObject = (GameObject*)_targetManager.Target.Address;
ImGui.Text(string.Create(CultureInfo.InvariantCulture,
- $"Distance: {(_targetManager.Target.Position - _clientState.LocalPlayer.Position).Length():F2}, Y: {_targetManager.Target.Position.Y - _clientState.LocalPlayer.Position.Y:F2}"));
+ $"Distance: {(_targetManager.Target.Position - _clientState.LocalPlayer.Position).Length():F2}, Y: {_targetManager.Target.Position.Y - _clientState.LocalPlayer.Position.Y:F2} | QM: {gameObject->NamePlateIconId}"));
ImGui.BeginDisabled(!_movementController.IsNavmeshReady);
if (!_movementController.IsPathfinding)
ImGui.SameLine();
if (ImGui.Button("Interact"))
{
- TargetSystem.Instance()->InteractWithObject(
+ ulong result = TargetSystem.Instance()->InteractWithObject(
(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_targetManager.Target.Address, false);
+ _logger.LogInformation("XXXXX Interaction Result: {Result}", result);
}
ImGui.SameLine();
ImGui.BeginDisabled(!_movementController.IsPathRunning);
if (ImGui.Button("Stop Nav"))
+ {
_movementController.Stop();
+ _questController.Stop();
+ }
+
ImGui.EndDisabled();
if (ImGui.Button("Reload Data"))
_framework.RunOnTick(() => _gameUiController.HandleCurrentDialogueChoices(),
TimeSpan.FromMilliseconds(200));
}
+
+ var remainingTasks = _questController.GetRemainingTaskNames();
+ if (remainingTasks.Count > 0)
+ {
+ ImGui.Separator();
+ ImGui.BeginDisabled();
+ foreach (var task in remainingTasks)
+ ImGui.TextUnformatted(task);
+ ImGui.EndDisabled();
+ }
}
public void Dispose()
"net8.0-windows7.0": {
"Dalamud.Extensions.MicrosoftLogging": {
"type": "Direct",
- "requested": "[3.0.0, )",
- "resolved": "3.0.0",
- "contentHash": "jWK3r/cZUXN8H9vHf78gEzeRmMk4YAbCUYzLcTqUAcega8unUiFGwYy+iOjVYJ9urnr9r+hk+vBi1y9wyv+e7Q==",
+ "requested": "[4.0.1, )",
+ "resolved": "4.0.1",
+ "contentHash": "fMEL2ajtF/30SBBku7vMyG0yye5eHN/A9fgT//1CEjUth/Wz2CYco5Ehye21T8KN1IuAPwoqJuu49rB71j+8ug==",
"dependencies": {
"Microsoft.Extensions.Logging": "8.0.0"
}