Rewrite logic, all quest steps can be executed automatically now

arr-p5
Liza 2024-06-09 16:30:53 +02:00
parent 78357dc288
commit 81e849abc3
Signed by: liza
GPG Key ID: 7199F8D727D55F67
39 changed files with 2074 additions and 522 deletions

View File

@ -34,6 +34,22 @@
} }
] ]
}, },
{
"Sequence": 2,
"Steps": [
{
"DataId": 2012185,
"Position": {
"X": -5.416992,
"Y": -49.05786,
"Z": -269.24548
},
"TerritoryId": 959,
"InteractionType": "WaitForManualProgress",
"Comment": "Follow Urianger"
}
]
},
{ {
"Sequence": 3, "Sequence": 3,
"Steps": [ "Steps": [
@ -45,7 +61,7 @@
"Z": -269.24548 "Z": -269.24548
}, },
"TerritoryId": 959, "TerritoryId": 959,
"InteractionType": "SinglePlayerDuty", "InteractionType": "WaitForManualProgress",
"Comment": "Follow Urianger" "Comment": "Follow Urianger"
} }
] ]

View File

@ -362,8 +362,8 @@ internal sealed class GameUiController : IDisposable
_logger.LogInformation("Using warp {Id}, {Prompt}", entry.RowId, excelPrompt); _logger.LogInformation("Using warp {Id}, {Prompt}", entry.RowId, excelPrompt);
addonSelectYesno->AtkUnitBase.FireCallbackInt(0); addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
if (increaseStepCount) //if (increaseStepCount)
_questController.IncreaseStepCount(); //_questController.IncreaseStepCount();
return; return;
} }
} }

View File

@ -45,6 +45,7 @@ internal sealed class MovementController : IDisposable
public bool IsPathRunning => _navmeshIpc.IsPathRunning; public bool IsPathRunning => _navmeshIpc.IsPathRunning;
public bool IsPathfinding => _pathfindTask is { IsCompleted: false }; public bool IsPathfinding => _pathfindTask is { IsCompleted: false };
public DestinationData? Destination { get; private set; } public DestinationData? Destination { get; private set; }
public DateTime MovementStartedAt { get; private set; } = DateTime.MaxValue;
public void Update() public void Update()
{ {
@ -53,7 +54,14 @@ internal sealed class MovementController : IDisposable
if (_pathfindTask.IsCompletedSuccessfully) if (_pathfindTask.IsCompletedSuccessfully)
{ {
_logger.LogInformation("Pathfinding complete, route: [{Route}]", _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(); var navPoints = _pathfindTask.Result.Skip(1).ToList();
Vector3 start = _clientState.LocalPlayer?.Position ?? navPoints[0]; Vector3 start = _clientState.LocalPlayer?.Position ?? navPoints[0];
@ -90,12 +98,15 @@ internal sealed class MovementController : IDisposable
} }
_navmeshIpc.MoveTo(navPoints, Destination.IsFlying); _navmeshIpc.MoveTo(navPoints, Destination.IsFlying);
MovementStartedAt = DateTime.Now;
ResetPathfinding(); ResetPathfinding();
} }
else if (_pathfindTask.IsCompleted) else if (_pathfindTask.IsCompleted)
{ {
_logger.LogWarning("Unable to complete pathfinding task"); _logger.LogWarning("Unable to complete pathfinding task");
ResetPathfinding(); ResetPathfinding();
throw new PathfindingFailedException();
} }
} }
@ -156,7 +167,8 @@ internal sealed class MovementController : IDisposable
return pointOnFloor != null && Math.Abs(pointOnFloor.Value.Y - p.Y) > 0.5f; 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(); ResetPathfinding();
@ -164,9 +176,11 @@ internal sealed class MovementController : IDisposable
_gameFunctions.ExecuteCommand("/automove off"); _gameFunctions.ExecuteCommand("/automove off");
Destination = new DestinationData(dataId, to, stopDistance ?? (DefaultStopDistance - 0.2f), fly, sprint); 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]; fly |= _condition[ConditionFlag.Diving];
PrepareNavigation(type, dataId, to, fly, sprint, stopDistance); PrepareNavigation(type, dataId, to, fly, sprint, stopDistance);
@ -178,13 +192,15 @@ internal sealed class MovementController : IDisposable
_navmeshIpc.Pathfind(_clientState.LocalPlayer!.Position, to, fly, _cancellationTokenSource.Token); _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]; fly |= _condition[ConditionFlag.Diving];
PrepareNavigation(type, dataId, to.Last(), fly, sprint, stopDistance); PrepareNavigation(type, dataId, to.Last(), fly, sprint, stopDistance);
_logger.LogInformation("Moving to {Destination}", Destination); _logger.LogInformation("Moving to {Destination}", Destination);
_navmeshIpc.MoveTo(to, fly); _navmeshIpc.MoveTo(to, fly);
MovementStartedAt = DateTime.Now;
} }
public void ResetPathfinding() public void ResetPathfinding()
@ -219,5 +235,27 @@ internal sealed class MovementController : IDisposable
Stop(); 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)
{
}
}
} }

View File

@ -1,11 +1,17 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics; using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions; using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps;
using Questionable.Data; using Questionable.Data;
using Questionable.External; using Questionable.External;
using Questionable.Model; using Questionable.Model;
@ -20,30 +26,30 @@ internal sealed class QuestController
private readonly GameFunctions _gameFunctions; private readonly GameFunctions _gameFunctions;
private readonly MovementController _movementController; private readonly MovementController _movementController;
private readonly ILogger<QuestController> _logger; 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; private readonly QuestRegistry _questRegistry;
private readonly IKeyState _keyState;
private readonly IReadOnlyList<ITaskFactory> _taskFactories;
public QuestController(IClientState clientState, GameFunctions gameFunctions, MovementController movementController, private readonly Queue<ITask> _taskQueue = new();
ILogger<QuestController> logger, ICondition condition, IChatGui chatGui, IFramework framework, private ITask? _currentTask;
AetheryteData aetheryteData, LifestreamIpc lifestreamIpc, TerritoryData territoryData, private bool _automatic;
QuestRegistry questRegistry)
public QuestController(
IClientState clientState,
GameFunctions gameFunctions,
MovementController movementController,
ILogger<QuestController> logger,
QuestRegistry questRegistry,
IKeyState keyState,
IEnumerable<ITaskFactory> taskFactories)
{ {
_clientState = clientState; _clientState = clientState;
_gameFunctions = gameFunctions; _gameFunctions = gameFunctions;
_movementController = movementController; _movementController = movementController;
_logger = logger; _logger = logger;
_condition = condition;
_chatGui = chatGui;
_framework = framework;
_aetheryteData = aetheryteData;
_lifestreamIpc = lifestreamIpc;
_territoryData = territoryData;
_questRegistry = questRegistry; _questRegistry = questRegistry;
_keyState = keyState;
_taskFactories = taskFactories.ToList().AsReadOnly();
} }
@ -60,6 +66,22 @@ internal sealed class QuestController
} }
public void Update() 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; DebugState = null;
@ -67,28 +89,38 @@ internal sealed class QuestController
if (currentQuestId == 0) if (currentQuestId == 0)
{ {
if (CurrentQuest != null) if (CurrentQuest != null)
{
_logger.LogInformation("No current quest, resetting data");
CurrentQuest = null; CurrentQuest = null;
Stop();
}
} }
else if (CurrentQuest == null || CurrentQuest.Quest.QuestId != currentQuestId) else if (CurrentQuest == null || CurrentQuest.Quest.QuestId != currentQuestId)
{ {
if (_questRegistry.TryGetQuest(currentQuestId, out var quest)) if (_questRegistry.TryGetQuest(currentQuestId, out var quest))
{
_logger.LogInformation("New quest: {QuestName}", quest.Name);
CurrentQuest = new QuestProgress(quest, currentSequence, 0); CurrentQuest = new QuestProgress(quest, currentSequence, 0);
}
else if (CurrentQuest != null) else if (CurrentQuest != null)
{
_logger.LogInformation("No active quest anymore? Not sure what happened...");
CurrentQuest = null; CurrentQuest = null;
}
Stop();
return;
} }
if (CurrentQuest == null) if (CurrentQuest == null)
{ {
DebugState = "No quest active"; DebugState = "No quest active";
Comment = null; Comment = null;
Stop();
return; return;
} }
if (_condition[ConditionFlag.Occupied] || _condition[ConditionFlag.Occupied30] || if (_gameFunctions.IsOccupied())
_condition[ConditionFlag.Occupied33] || _condition[ConditionFlag.Occupied38] ||
_condition[ConditionFlag.Occupied39] || _condition[ConditionFlag.OccupiedInEvent] ||
_condition[ConditionFlag.OccupiedInQuestEvent] || _condition[ConditionFlag.OccupiedInCutSceneEvent] ||
_condition[ConditionFlag.Casting] || _condition[ConditionFlag.Unknown57])
{ {
DebugState = "Occupied"; DebugState = "Occupied";
return; return;
@ -106,14 +138,22 @@ internal sealed class QuestController
} }
if (CurrentQuest.Sequence != currentSequence) if (CurrentQuest.Sequence != currentSequence)
{
CurrentQuest = CurrentQuest with { Sequence = currentSequence, Step = 0 }; CurrentQuest = CurrentQuest with { Sequence = currentSequence, Step = 0 };
bool automatic = _automatic;
Stop();
if (automatic)
ExecuteNextStep(true);
}
var q = CurrentQuest.Quest; var q = CurrentQuest.Quest;
var sequence = q.FindSequence(CurrentQuest.Sequence); var sequence = q.FindSequence(CurrentQuest.Sequence);
if (sequence == null) if (sequence == null)
{ {
DebugState = "Sequence not found"; DebugState = "Sequence not found";
Comment = null; Comment = null;
Stop();
return; return;
} }
@ -121,6 +161,7 @@ internal sealed class QuestController
{ {
DebugState = "Step completed"; DebugState = "Step completed";
Comment = null; Comment = null;
Stop();
return; return;
} }
@ -128,6 +169,7 @@ internal sealed class QuestController
{ {
DebugState = "Step not found"; DebugState = "Step not found";
Comment = null; Comment = null;
Stop();
return; return;
} }
@ -152,7 +194,7 @@ internal sealed class QuestController
return (seq, seq.Steps[CurrentQuest.Step]); return (seq, seq.Steps[CurrentQuest.Step]);
} }
public void IncreaseStepCount() public void IncreaseStepCount(bool shouldContinue = false)
{ {
(QuestSequence? seq, QuestStep? step) = GetNextStep(); (QuestSequence? seq, QuestStep? step) = GetNextStep();
if (CurrentQuest == null || seq == null || step == null) if (CurrentQuest == null || seq == null || step == null)
@ -161,6 +203,7 @@ internal sealed class QuestController
return; return;
} }
_logger.LogInformation("Increasing step count from {CurrentValue}", CurrentQuest.Step);
if (CurrentQuest.Step + 1 < seq.Steps.Count) if (CurrentQuest.Step + 1 < seq.Steps.Count)
{ {
CurrentQuest = CurrentQuest with CurrentQuest = CurrentQuest with
@ -177,6 +220,10 @@ internal sealed class QuestController
StepProgress = new() StepProgress = new()
}; };
} }
if (shouldContinue && _automatic)
ExecuteNextStep(true);
} }
public void IncreaseDialogueChoicesSelected() public void IncreaseDialogueChoicesSelected()
@ -196,12 +243,114 @@ internal sealed class QuestController
} }
}; };
/* TODO Is this required?
if (CurrentQuest.StepProgress.DialogueChoicesSelected >= step.DialogueChoices.Count) if (CurrentQuest.StepProgress.DialogueChoicesSelected >= step.DialogueChoices.Count)
IncreaseStepCount(); IncreaseStepCount();
*/
} }
public unsafe void ExecuteNextStep() public void Stop()
{ {
_currentTask = null;
if (_taskQueue.Count > 0)
_taskQueue.Clear();
// reset task queue
_automatic = false;
}
private void UpdateCurrentTask()
{
if (_gameFunctions.IsOccupied())
return;
if (_currentTask == null)
{
if (_taskQueue.TryDequeue(out ITask? upcomingTask))
{
try
{
_logger.LogInformation("Starting task {TaskName}", upcomingTask.ToString());
if (upcomingTask.Start())
{
_currentTask = upcomingTask;
return;
}
else
{
_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
return;
}
ETaskResult result;
try
{
result = _currentTask.Update();
}
catch (Exception e)
{
_logger.LogError(e, "Failed to update task {TaskName}", _currentTask.ToString());
Stop();
return;
}
switch (result)
{
case ETaskResult.StillRunning:
return;
case ETaskResult.SkipRemainingTasksForStep:
_logger.LogInformation("Result: {Result}, skipping remaining tasks for step", result);
_currentTask = null;
while (_taskQueue.TryDequeue(out ITask? nextTask))
{
if (nextTask is ILastTask)
{
_currentTask = nextTask;
return;
}
}
return;
case ETaskResult.TaskComplete:
_logger.LogInformation("Result: {Result}, remaining tasks: {RemainingTaskCount}", result,
_taskQueue.Count);
_currentTask = null;
// handled in next update
return;
case ETaskResult.NextStep:
_logger.LogInformation("Result: {Result}", result);
IncreaseStepCount(true);
return;
case ETaskResult.End:
_logger.LogInformation("Result: {Result}", result);
Stop();
return;
}
}
public void ExecuteNextStep(bool automatic)
{
Stop();
_automatic = automatic;
(QuestSequence? seq, QuestStep? step) = GetNextStep(); (QuestSequence? seq, QuestStep? step) = GetNextStep();
if (CurrentQuest == null || seq == null || step == null) if (CurrentQuest == null || seq == null || step == null)
{ {
@ -209,440 +358,40 @@ internal sealed class QuestController
return; return;
} }
if (step.Disabled) var newTasks = _taskFactories
.SelectMany(x =>
{
IList<ITask> tasks = x.CreateAllTasks(CurrentQuest.Quest, seq, step).ToList();
if (_logger.IsEnabled(LogLevel.Trace))
{
string factoryName = x.GetType().FullName ?? x.GetType().Name;
if (factoryName.Contains('.', StringComparison.Ordinal))
factoryName = factoryName[(factoryName.LastIndexOf('.') + 1)..];
_logger.LogTrace("Factory {FactoryName} created Task {TaskNames}",
factoryName, string.Join(", ", tasks.Select(y => y.ToString())));
}
return tasks;
})
.ToList();
if (newTasks.Count == 0)
{ {
_logger.LogInformation("Skipping step, as it is disabled"); _logger.LogInformation("Nothing to execute for step?");
IncreaseStepCount();
return; return;
} }
if (!CurrentQuest.StepProgress.AetheryteShortcutUsed && step.AetheryteShortcut != null) foreach (var task in newTasks)
{ _taskQueue.Enqueue(task);
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) public IList<string> GetRemainingTaskNames() =>
{ _taskQueue.Select(x => x.ToString() ?? "?").ToList();
_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; public string ToStatString()
} {
} return _currentTask == null ? $"- (+{_taskQueue.Count})" : $"{_currentTask} (+{_taskQueue.Count})";
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;
}
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 (step is { DataId: not null, InteractionType: EInteractionType.AttuneAetherCurrent } &&
_gameFunctions.IsAetherCurrentUnlocked(step.DataId.Value))
{
_logger.LogInformation("Skipping step, as current is unlocked");
IncreaseStepCount();
return;
}
QuestWork? questWork = _gameFunctions.GetQuestEx(CurrentQuest.Quest.QuestId);
if (questWork != null && step.MatchesQuestVariables(questWork.Value))
{
_logger.LogInformation("Skipping step, as quest variables match");
IncreaseStepCount();
return;
}
}
if (!CurrentQuest.StepProgress.AethernetShortcutUsed && step.AethernetShortcut != null)
{
if (_gameFunctions.IsAetheryteUnlocked(step.AethernetShortcut.From) &&
_gameFunctions.IsAetheryteUnlocked(step.AethernetShortcut.To))
{
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))
{
if (_aetheryteData.CalculateDistance(playerPosition, territoryType, from) < 11)
{
_logger.LogInformation("Using lifestream to teleport to {Destination}", to);
_lifestreamIpc.Teleport(to);
CurrentQuest = CurrentQuest with
{
StepProgress = CurrentQuest.StepProgress with { AethernetShortcutUsed = true }
};
}
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);
}
return;
}
}
else
_logger.LogWarning(
"Aethernet shortcut not unlocked (from: {FromAetheryte}, to: {ToAetheryte}), walking manually",
step.AethernetShortcut.From, step.AethernetShortcut.To);
}
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;
}
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");
}
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)
{
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");
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;
}
if (!gameObject.IsTargetable && _condition[ConditionFlag.Mounted])
{
_gameFunctions.Unmount();
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;
case EInteractionType.Combat:
if (_gameFunctions.Unmount())
return;
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);
// next sequence should trigger automatically
IncreaseStepCount();
}
break;
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();
}
break;
case EInteractionType.Say:
if (_condition[ConditionFlag.Mounted])
{
_gameFunctions.Unmount();
return;
}
if (step.ChatMessage != null)
{
string? excelString = _gameFunctions.GetDialogueText(CurrentQuest.Quest,
step.ChatMessage.ExcelSheet,
step.ChatMessage.Key);
if (excelString == null)
return;
_gameFunctions.ExecuteCommand($"/say {excelString}");
IncreaseStepCount();
}
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));
}
}
break;
case EInteractionType.ShouldBeAJump:
case EInteractionType.Instruction:
// Need to manually forward
break;
default:
_logger.LogWarning("Action '{InteractionType}' is not implemented", step.InteractionType);
break;
}
} }
public sealed record QuestProgress( public sealed record QuestProgress(
@ -657,8 +406,7 @@ internal sealed class QuestController
} }
} }
// TODO is this still required?
public sealed record StepProgress( public sealed record StepProgress(
bool AetheryteShortcutUsed = false,
bool AethernetShortcutUsed = false,
int DialogueChoicesSelected = 0); int DialogueChoicesSelected = 0);
} }

View File

@ -0,0 +1,120 @@
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})";
}
}

View File

@ -0,0 +1,96 @@
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})";
}
}

View File

@ -0,0 +1,168 @@
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;
}
}
}

View File

@ -0,0 +1,92 @@
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)})";
}
}

View File

@ -0,0 +1,34 @@
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";
}
}

View File

@ -0,0 +1,134 @@
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";
}
}

View File

@ -0,0 +1,36 @@
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";
}
}

View File

@ -0,0 +1,37 @@
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;
}

View File

@ -0,0 +1,61 @@
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";
}

View File

@ -0,0 +1,36 @@
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";
}

View File

@ -0,0 +1,12 @@
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;
}

View File

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

View File

@ -0,0 +1,6 @@
namespace Questionable.Controller.Steps;
internal interface ILastTask : ITask
{
}

View File

@ -0,0 +1,11 @@
using System.Threading;
using System.Threading.Tasks;
namespace Questionable.Controller.Steps;
internal interface ITask
{
bool Start();
ETaskResult Update();
}

View File

@ -0,0 +1,17 @@
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;
}
}

View File

@ -0,0 +1,59 @@
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})";
}
}

View File

@ -0,0 +1,55 @@
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})";
}
}

View File

@ -0,0 +1,55 @@
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})";
}
}

View File

@ -0,0 +1,47 @@
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();
}
}

View File

@ -0,0 +1,44 @@
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})";
}
}

View File

@ -0,0 +1,77 @@
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})";
}
}

View File

@ -0,0 +1,99 @@
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})";
}
}

View File

@ -0,0 +1,77 @@
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})";
}
}

View File

@ -0,0 +1,52 @@
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})";
}
}

View File

@ -0,0 +1,109 @@
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})";
}
}

View File

@ -0,0 +1,20 @@
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)
{
}
}

View File

@ -47,7 +47,15 @@ internal sealed class DalamudInitializer : IDisposable
{ {
_questController.Update(); _questController.Update();
_navigationShortcutController.HandleNavigationShortcut(); _navigationShortcutController.HandleNavigationShortcut();
_movementController.Update();
try
{
_movementController.Update();
}
catch (MovementController.PathfindingFailedException)
{
_questController.Stop();
}
} }
private void ProcessCommand(string command, string arguments) private void ProcessCommand(string command, string arguments)

View File

@ -20,6 +20,8 @@ using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.System.Memory; using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using Lumina.Excel.CustomSheets; using Lumina.Excel.CustomSheets;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -53,11 +55,12 @@ internal sealed unsafe class GameFunctions
private readonly ICondition _condition; private readonly ICondition _condition;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly QuestRegistry _questRegistry; private readonly QuestRegistry _questRegistry;
private readonly IGameGui _gameGui;
private readonly ILogger<GameFunctions> _logger; private readonly ILogger<GameFunctions> _logger;
public GameFunctions(IDataManager dataManager, IObjectTable objectTable, ISigScanner sigScanner, public GameFunctions(IDataManager dataManager, IObjectTable objectTable, ISigScanner sigScanner,
ITargetManager targetManager, ICondition condition, IClientState clientState, QuestRegistry questRegistry, ITargetManager targetManager, ICondition condition, IClientState clientState, QuestRegistry questRegistry,
ILogger<GameFunctions> logger) IGameGui gameGui, ILogger<GameFunctions> logger)
{ {
_dataManager = dataManager; _dataManager = dataManager;
_objectTable = objectTable; _objectTable = objectTable;
@ -65,6 +68,7 @@ internal sealed unsafe class GameFunctions
_condition = condition; _condition = condition;
_clientState = clientState; _clientState = clientState;
_questRegistry = questRegistry; _questRegistry = questRegistry;
_gameGui = gameGui;
_logger = logger; _logger = logger;
_processChatBox = _processChatBox =
Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(sigScanner.ScanText(Signatures.SendChat)); Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(sigScanner.ScanText(Signatures.SendChat));
@ -166,6 +170,8 @@ internal sealed unsafe class GameFunctions
public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation) public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation)
=> IsAetheryteUnlocked((uint)aetheryteLocation, out _); => IsAetheryteUnlocked((uint)aetheryteLocation, out _);
public bool CanTeleport() => ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0;
public bool TeleportAetheryte(uint aetheryteId) public bool TeleportAetheryte(uint aetheryteId)
{ {
var status = ActionManager.Instance()->GetActionStatus(ActionType.Action, 5); var status = ActionManager.Instance()->GetActionStatus(ActionType.Action, 5);
@ -342,7 +348,7 @@ internal sealed unsafe class GameFunctions
return null; return null;
} }
public void InteractWith(uint dataId) public bool InteractWith(uint dataId)
{ {
GameObject? gameObject = FindObjectByDataId(dataId); GameObject? gameObject = FindObjectByDataId(dataId);
if (gameObject != null) if (gameObject != null)
@ -350,9 +356,12 @@ internal sealed unsafe class GameFunctions
_logger.LogInformation("Setting target with {DataId} to {ObjectId}", dataId, gameObject.ObjectId); _logger.LogInformation("Setting target with {DataId} to {ObjectId}", dataId, gameObject.ObjectId);
_targetManager.Target = gameObject; _targetManager.Target = gameObject;
TargetSystem.Instance()->InteractWithObject( ulong result = TargetSystem.Instance()->InteractWithObject(
(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject.Address, false); (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject.Address, false);
return result != 0;
} }
return false;
} }
public void UseItem(uint itemId) public void UseItem(uint itemId)
@ -422,46 +431,46 @@ internal sealed unsafe class GameFunctions
return false; 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 (ActionManager.Instance()->GetActionStatus(ActionType.Mount, 71) == 0)
if (playerState != null && playerState->IsMountUnlocked(71))
{ {
if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, 71) == 0) _logger.LogInformation("Using SDS Fenrir as mount");
{ return ActionManager.Instance()->UseAction(ActionType.Mount, 71);
_logger.LogInformation("Using SDS Fenrir as mount");
ActionManager.Instance()->UseAction(ActionType.Mount, 71);
}
}
else
{
if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 9) == 0)
{
_logger.LogInformation("Using mount roulette");
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 9);
}
} }
} }
else
{
if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 9) == 0)
{
_logger.LogInformation("Using mount roulette");
return ActionManager.Instance()->UseAction(ActionType.GeneralAction, 9);
}
}
return false;
} }
public bool Unmount() public bool Unmount()
{ {
if (_condition[ConditionFlag.Mounted]) if (!_condition[ConditionFlag.Mounted])
return false;
if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23) == 0)
{ {
if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23) == 0) _logger.LogInformation("Unmounting...");
{ ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23);
_logger.LogInformation("Unmounting...");
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23);
}
else
_logger.LogWarning("Can't unmount right now?");
return true;
} }
else
_logger.LogWarning("Can't unmount right now?");
return false; return true;
} }
public void OpenDutyFinder(uint contentFinderConditionId) public void OpenDutyFinder(uint contentFinderConditionId)
@ -508,4 +517,19 @@ internal sealed unsafe class GameFunctions
var questRow = _dataManager.GetExcelSheet<ContentTalk>()!.GetRow(rowId); var questRow = _dataManager.GetExcelSheet<ContentTalk>()!.GetRow(rowId);
return questRow?.Text?.ToString(); 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];
}
} }

View File

@ -1,10 +0,0 @@
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")]

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<Version>0.5</Version> <Version>0.6</Version>
<LangVersion>12</LangVersion> <LangVersion>12</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -23,7 +23,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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="DalamudPackager" Version="2.1.12"/>
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" ExcludeAssets="runtime"/> <PackageReference Include="JetBrains.Annotations" Version="2023.3.0" ExcludeAssets="runtime"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
@ -59,6 +59,6 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\LLib\LLib.csproj" /> <ProjectReference Include="..\LLib\LLib.csproj" />
<ProjectReference Include="..\QuestPaths\QuestPaths.csproj" Condition="'$(Configuration)' == 'Release'" /> <ProjectReference Include="..\QuestPaths\QuestPaths.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -7,8 +7,13 @@ using Dalamud.Interface.Windowing;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller; 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.Data;
using Questionable.External; using Questionable.External;
using Questionable.Windows; using Questionable.Windows;
@ -32,14 +37,15 @@ public sealed class QuestionablePlugin : IDalamudPlugin
ICondition condition, ICondition condition,
IChatGui chatGui, IChatGui chatGui,
ICommandManager commandManager, ICommandManager commandManager,
IAddonLifecycle addonLifecycle) IAddonLifecycle addonLifecycle,
IKeyState keyState)
{ {
ArgumentNullException.ThrowIfNull(pluginInterface); ArgumentNullException.ThrowIfNull(pluginInterface);
ServiceCollection serviceCollection = new(); ServiceCollection serviceCollection = new();
serviceCollection.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace) serviceCollection.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace)
.ClearProviders() .ClearProviders()
.AddDalamudLogger(pluginLog)); .AddDalamudLogger(pluginLog, t => t[(t.LastIndexOf('.') + 1)..]));
serviceCollection.AddSingleton<IDalamudPlugin>(this); serviceCollection.AddSingleton<IDalamudPlugin>(this);
serviceCollection.AddSingleton(pluginInterface); serviceCollection.AddSingleton(pluginInterface);
serviceCollection.AddSingleton(clientState); serviceCollection.AddSingleton(clientState);
@ -53,6 +59,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton(chatGui); serviceCollection.AddSingleton(chatGui);
serviceCollection.AddSingleton(commandManager); serviceCollection.AddSingleton(commandManager);
serviceCollection.AddSingleton(addonLifecycle); serviceCollection.AddSingleton(addonLifecycle);
serviceCollection.AddSingleton(keyState);
serviceCollection.AddSingleton(new WindowSystem(nameof(Questionable))); serviceCollection.AddSingleton(new WindowSystem(nameof(Questionable)));
serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration()); serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration());
@ -62,6 +69,39 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<NavmeshIpc>(); serviceCollection.AddSingleton<NavmeshIpc>();
serviceCollection.AddSingleton<LifestreamIpc>(); 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<MovementController>();
serviceCollection.AddSingleton<QuestRegistry>(); serviceCollection.AddSingleton<QuestRegistry>();
serviceCollection.AddSingleton<QuestController>(); serviceCollection.AddSingleton<QuestController>();

View File

@ -0,0 +1,84 @@
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>();
}
}

View File

@ -9,9 +9,11 @@ using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Control; using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using ImGuiNET; using ImGuiNET;
using LLib.ImGui; using LLib.ImGui;
using Microsoft.Extensions.Logging;
using Questionable.Controller; using Questionable.Controller;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.V1; using Questionable.Model.V1;
@ -30,11 +32,12 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
private readonly ITargetManager _targetManager; private readonly ITargetManager _targetManager;
private readonly GameUiController _gameUiController; private readonly GameUiController _gameUiController;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly ILogger<DebugWindow> _logger;
public DebugWindow(DalamudPluginInterface pluginInterface, WindowSystem windowSystem, public DebugWindow(DalamudPluginInterface pluginInterface, WindowSystem windowSystem,
MovementController movementController, QuestController questController, GameFunctions gameFunctions, MovementController movementController, QuestController questController, GameFunctions gameFunctions,
IClientState clientState, IFramework framework, ITargetManager targetManager, GameUiController gameUiController, IClientState clientState, IFramework framework, ITargetManager targetManager, GameUiController gameUiController,
Configuration configuration) Configuration configuration, ILogger<DebugWindow> logger)
: base("Questionable", ImGuiWindowFlags.AlwaysAutoResize) : base("Questionable", ImGuiWindowFlags.AlwaysAutoResize)
{ {
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
@ -47,6 +50,7 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
_targetManager = targetManager; _targetManager = targetManager;
_gameUiController = gameUiController; _gameUiController = gameUiController;
_configuration = configuration; _configuration = configuration;
_logger = logger;
IsOpen = true; IsOpen = true;
SizeConstraints = new WindowSizeConstraints SizeConstraints = new WindowSizeConstraints
@ -109,23 +113,36 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
ImGui.EndDisabled(); ImGui.EndDisabled();
ImGui.TextUnformatted(_questController.Comment ?? "--"); ImGui.TextUnformatted(_questController.Comment ?? "--");
var nextStep = _questController.GetNextStep(); //var nextStep = _questController.GetNextStep();
ImGui.BeginDisabled(nextStep.Step == null); //ImGui.BeginDisabled(nextStep.Step == null);
ImGui.Text(string.Create(CultureInfo.InvariantCulture, ImGui.Text(_questController.ToStatString());
$"{nextStep.Step?.InteractionType} @ {nextStep.Step?.Position}")); //ImGui.EndDisabled();
if (ImGuiComponents.IconButton(FontAwesomeIcon.Play)) if (ImGuiComponents.IconButton(FontAwesomeIcon.Play))
{ {
_questController.ExecuteNextStep(); _questController.ExecuteNextStep(true);
} }
ImGui.SameLine(); 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 else
ImGui.Text("No active quest"); ImGui.Text("No active quest");
@ -165,8 +182,10 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
ImGui.Separator(); ImGui.Separator();
ImGui.Text(string.Create(CultureInfo.InvariantCulture, ImGui.Text(string.Create(CultureInfo.InvariantCulture,
$"Target: {_targetManager.Target.Name} ({_targetManager.Target.ObjectKind}; {_targetManager.Target.DataId})")); $"Target: {_targetManager.Target.Name} ({_targetManager.Target.ObjectKind}; {_targetManager.Target.DataId})"));
GameObject* gameObject = (GameObject*)_targetManager.Target.Address;
ImGui.Text(string.Create(CultureInfo.InvariantCulture, 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); ImGui.BeginDisabled(!_movementController.IsNavmeshReady);
if (!_movementController.IsPathfinding) if (!_movementController.IsPathfinding)
@ -189,8 +208,9 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
ImGui.SameLine(); ImGui.SameLine();
if (ImGui.Button("Interact")) if (ImGui.Button("Interact"))
{ {
TargetSystem.Instance()->InteractWithObject( ulong result = TargetSystem.Instance()->InteractWithObject(
(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_targetManager.Target.Address, false); (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_targetManager.Target.Address, false);
_logger.LogInformation("XXXXX Interaction Result: {Result}", result);
} }
ImGui.SameLine(); ImGui.SameLine();
@ -246,7 +266,11 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
ImGui.BeginDisabled(!_movementController.IsPathRunning); ImGui.BeginDisabled(!_movementController.IsPathRunning);
if (ImGui.Button("Stop Nav")) if (ImGui.Button("Stop Nav"))
{
_movementController.Stop(); _movementController.Stop();
_questController.Stop();
}
ImGui.EndDisabled(); ImGui.EndDisabled();
if (ImGui.Button("Reload Data")) if (ImGui.Button("Reload Data"))
@ -255,6 +279,16 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
_framework.RunOnTick(() => _gameUiController.HandleCurrentDialogueChoices(), _framework.RunOnTick(() => _gameUiController.HandleCurrentDialogueChoices(),
TimeSpan.FromMilliseconds(200)); 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() public void Dispose()

View File

@ -4,9 +4,9 @@
"net8.0-windows7.0": { "net8.0-windows7.0": {
"Dalamud.Extensions.MicrosoftLogging": { "Dalamud.Extensions.MicrosoftLogging": {
"type": "Direct", "type": "Direct",
"requested": "[3.0.0, )", "requested": "[4.0.1, )",
"resolved": "3.0.0", "resolved": "4.0.1",
"contentHash": "jWK3r/cZUXN8H9vHf78gEzeRmMk4YAbCUYzLcTqUAcega8unUiFGwYy+iOjVYJ9urnr9r+hk+vBi1y9wyv+e7Q==", "contentHash": "fMEL2ajtF/30SBBku7vMyG0yye5eHN/A9fgT//1CEjUth/Wz2CYco5Ehye21T8KN1IuAPwoqJuu49rB71j+8ug==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Logging": "8.0.0" "Microsoft.Extensions.Logging": "8.0.0"
} }