Rewrite logic, all quest steps can be executed automatically now

This commit is contained in:
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,
"Steps": [
@ -45,7 +61,7 @@
"Z": -269.24548
},
"TerritoryId": 959,
"InteractionType": "SinglePlayerDuty",
"InteractionType": "WaitForManualProgress",
"Comment": "Follow Urianger"
}
]

View File

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

View File

@ -45,6 +45,7 @@ internal sealed class MovementController : IDisposable
public bool IsPathRunning => _navmeshIpc.IsPathRunning;
public bool IsPathfinding => _pathfindTask is { IsCompleted: false };
public DestinationData? Destination { get; private set; }
public DateTime MovementStartedAt { get; private set; } = DateTime.MaxValue;
public void Update()
{
@ -53,7 +54,14 @@ internal sealed class MovementController : IDisposable
if (_pathfindTask.IsCompletedSuccessfully)
{
_logger.LogInformation("Pathfinding complete, route: [{Route}]",
string.Join(" → ", _pathfindTask.Result.Select(x => x.ToString("G", CultureInfo.InvariantCulture))));
string.Join(" → ",
_pathfindTask.Result.Select(x => x.ToString("G", CultureInfo.InvariantCulture))));
if (_pathfindTask.Result.Count == 0)
{
ResetPathfinding();
throw new PathfindingFailedException();
}
var navPoints = _pathfindTask.Result.Skip(1).ToList();
Vector3 start = _clientState.LocalPlayer?.Position ?? navPoints[0];
@ -90,12 +98,15 @@ internal sealed class MovementController : IDisposable
}
_navmeshIpc.MoveTo(navPoints, Destination.IsFlying);
MovementStartedAt = DateTime.Now;
ResetPathfinding();
}
else if (_pathfindTask.IsCompleted)
{
_logger.LogWarning("Unable to complete pathfinding task");
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;
}
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();
@ -164,9 +176,11 @@ internal sealed class MovementController : IDisposable
_gameFunctions.ExecuteCommand("/automove off");
Destination = new DestinationData(dataId, to, stopDistance ?? (DefaultStopDistance - 0.2f), fly, sprint);
MovementStartedAt = DateTime.MaxValue;
}
public void NavigateTo(EMovementType type, uint? dataId, Vector3 to, bool fly, bool sprint, float? stopDistance = null)
public void NavigateTo(EMovementType type, uint? dataId, Vector3 to, bool fly, bool sprint,
float? stopDistance = null)
{
fly |= _condition[ConditionFlag.Diving];
PrepareNavigation(type, dataId, to, fly, sprint, stopDistance);
@ -178,13 +192,15 @@ internal sealed class MovementController : IDisposable
_navmeshIpc.Pathfind(_clientState.LocalPlayer!.Position, to, fly, _cancellationTokenSource.Token);
}
public void NavigateTo(EMovementType type, uint? dataId, List<Vector3> to, bool fly, bool sprint, float? stopDistance)
public void NavigateTo(EMovementType type, uint? dataId, List<Vector3> to, bool fly, bool sprint,
float? stopDistance)
{
fly |= _condition[ConditionFlag.Diving];
PrepareNavigation(type, dataId, to.Last(), fly, sprint, stopDistance);
_logger.LogInformation("Moving to {Destination}", Destination);
_navmeshIpc.MoveTo(to, fly);
MovementStartedAt = DateTime.Now;
}
public void ResetPathfinding()
@ -219,5 +235,27 @@ internal sealed class MovementController : IDisposable
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.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps;
using Questionable.Data;
using Questionable.External;
using Questionable.Model;
@ -20,30 +26,30 @@ internal sealed class QuestController
private readonly GameFunctions _gameFunctions;
private readonly MovementController _movementController;
private readonly ILogger<QuestController> _logger;
private readonly ICondition _condition;
private readonly IChatGui _chatGui;
private readonly IFramework _framework;
private readonly AetheryteData _aetheryteData;
private readonly LifestreamIpc _lifestreamIpc;
private readonly TerritoryData _territoryData;
private readonly QuestRegistry _questRegistry;
private readonly IKeyState _keyState;
private readonly IReadOnlyList<ITaskFactory> _taskFactories;
public QuestController(IClientState clientState, GameFunctions gameFunctions, MovementController movementController,
ILogger<QuestController> logger, ICondition condition, IChatGui chatGui, IFramework framework,
AetheryteData aetheryteData, LifestreamIpc lifestreamIpc, TerritoryData territoryData,
QuestRegistry questRegistry)
private readonly Queue<ITask> _taskQueue = new();
private ITask? _currentTask;
private bool _automatic;
public QuestController(
IClientState clientState,
GameFunctions gameFunctions,
MovementController movementController,
ILogger<QuestController> logger,
QuestRegistry questRegistry,
IKeyState keyState,
IEnumerable<ITaskFactory> taskFactories)
{
_clientState = clientState;
_gameFunctions = gameFunctions;
_movementController = movementController;
_logger = logger;
_condition = condition;
_chatGui = chatGui;
_framework = framework;
_aetheryteData = aetheryteData;
_lifestreamIpc = lifestreamIpc;
_territoryData = territoryData;
_questRegistry = questRegistry;
_keyState = keyState;
_taskFactories = taskFactories.ToList().AsReadOnly();
}
@ -60,6 +66,22 @@ internal sealed class QuestController
}
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;
@ -67,28 +89,38 @@ internal sealed class QuestController
if (currentQuestId == 0)
{
if (CurrentQuest != null)
{
_logger.LogInformation("No current quest, resetting data");
CurrentQuest = null;
Stop();
}
}
else if (CurrentQuest == null || CurrentQuest.Quest.QuestId != currentQuestId)
{
if (_questRegistry.TryGetQuest(currentQuestId, out var quest))
{
_logger.LogInformation("New quest: {QuestName}", quest.Name);
CurrentQuest = new QuestProgress(quest, currentSequence, 0);
}
else if (CurrentQuest != null)
{
_logger.LogInformation("No active quest anymore? Not sure what happened...");
CurrentQuest = null;
}
Stop();
return;
}
if (CurrentQuest == null)
{
DebugState = "No quest active";
Comment = null;
Stop();
return;
}
if (_condition[ConditionFlag.Occupied] || _condition[ConditionFlag.Occupied30] ||
_condition[ConditionFlag.Occupied33] || _condition[ConditionFlag.Occupied38] ||
_condition[ConditionFlag.Occupied39] || _condition[ConditionFlag.OccupiedInEvent] ||
_condition[ConditionFlag.OccupiedInQuestEvent] || _condition[ConditionFlag.OccupiedInCutSceneEvent] ||
_condition[ConditionFlag.Casting] || _condition[ConditionFlag.Unknown57])
if (_gameFunctions.IsOccupied())
{
DebugState = "Occupied";
return;
@ -106,14 +138,22 @@ internal sealed class QuestController
}
if (CurrentQuest.Sequence != currentSequence)
{
CurrentQuest = CurrentQuest with { Sequence = currentSequence, Step = 0 };
bool automatic = _automatic;
Stop();
if (automatic)
ExecuteNextStep(true);
}
var q = CurrentQuest.Quest;
var sequence = q.FindSequence(CurrentQuest.Sequence);
if (sequence == null)
{
DebugState = "Sequence not found";
Comment = null;
Stop();
return;
}
@ -121,6 +161,7 @@ internal sealed class QuestController
{
DebugState = "Step completed";
Comment = null;
Stop();
return;
}
@ -128,6 +169,7 @@ internal sealed class QuestController
{
DebugState = "Step not found";
Comment = null;
Stop();
return;
}
@ -152,7 +194,7 @@ internal sealed class QuestController
return (seq, seq.Steps[CurrentQuest.Step]);
}
public void IncreaseStepCount()
public void IncreaseStepCount(bool shouldContinue = false)
{
(QuestSequence? seq, QuestStep? step) = GetNextStep();
if (CurrentQuest == null || seq == null || step == null)
@ -161,6 +203,7 @@ internal sealed class QuestController
return;
}
_logger.LogInformation("Increasing step count from {CurrentValue}", CurrentQuest.Step);
if (CurrentQuest.Step + 1 < seq.Steps.Count)
{
CurrentQuest = CurrentQuest with
@ -177,6 +220,10 @@ internal sealed class QuestController
StepProgress = new()
};
}
if (shouldContinue && _automatic)
ExecuteNextStep(true);
}
public void IncreaseDialogueChoicesSelected()
@ -196,12 +243,114 @@ internal sealed class QuestController
}
};
/* TODO Is this required?
if (CurrentQuest.StepProgress.DialogueChoicesSelected >= step.DialogueChoices.Count)
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();
if (CurrentQuest == null || seq == null || step == null)
{
@ -209,440 +358,40 @@ internal sealed class QuestController
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");
IncreaseStepCount();
_logger.LogInformation("Nothing to execute for step?");
return;
}
if (!CurrentQuest.StepProgress.AetheryteShortcutUsed && step.AetheryteShortcut != null)
{
bool skipTeleport = false;
ushort territoryType = _clientState.TerritoryType;
if (step.TerritoryId == territoryType)
{
Vector3 pos = _clientState.LocalPlayer!.Position;
if (_aetheryteData.CalculateDistance(pos, territoryType, step.AetheryteShortcut.Value) < 11 ||
(step.AethernetShortcut != null &&
(_aetheryteData.CalculateDistance(pos, territoryType, step.AethernetShortcut.From) < 20 ||
_aetheryteData.CalculateDistance(pos, territoryType, step.AethernetShortcut.To) < 20)))
{
_logger.LogInformation("Skipping aetheryte teleport");
skipTeleport = true;
}
}
foreach (var task in newTasks)
_taskQueue.Enqueue(task);
}
if (skipTeleport)
{
_logger.LogInformation("Marking aetheryte shortcut as used");
CurrentQuest = CurrentQuest with
{
StepProgress = CurrentQuest.StepProgress with { AetheryteShortcutUsed = true }
};
}
else
{
if (!_gameFunctions.IsAetheryteUnlocked(step.AetheryteShortcut.Value))
{
_logger.LogError("Aetheryte {Aetheryte} is not unlocked.", step.AetheryteShortcut.Value);
_chatGui.Print($"[Questionable] Aetheryte {step.AetheryteShortcut.Value} is not unlocked.");
}
else if (_gameFunctions.TeleportAetheryte(step.AetheryteShortcut.Value))
{
_logger.LogInformation("Travelling via aetheryte...");
CurrentQuest = CurrentQuest with
{
StepProgress = CurrentQuest.StepProgress with { AetheryteShortcutUsed = true }
};
}
else
{
_logger.LogWarning("Unable to teleport to aetheryte");
_chatGui.Print("[Questionable] Unable to teleport to aetheryte.");
}
public IList<string> GetRemainingTaskNames() =>
_taskQueue.Select(x => x.ToString() ?? "?").ToList();
return;
}
}
if (!step.SkipIf.Contains(ESkipCondition.Never))
{
_logger.LogInformation("Checking skip conditions; {ConfiguredConditions}", string.Join(",", step.SkipIf));
if (step.SkipIf.Contains(ESkipCondition.FlyingUnlocked) &&
_gameFunctions.IsFlyingUnlocked(step.TerritoryId))
{
_logger.LogInformation("Skipping step, as flying is unlocked");
IncreaseStepCount();
return;
}
if (step.SkipIf.Contains(ESkipCondition.FlyingLocked) &&
!_gameFunctions.IsFlyingUnlocked(step.TerritoryId))
{
_logger.LogInformation("Skipping step, as flying is locked");
IncreaseStepCount();
return;
}
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 string ToStatString()
{
return _currentTask == null ? $"- (+{_taskQueue.Count})" : $"{_currentTask} (+{_taskQueue.Count})";
}
public sealed record QuestProgress(
@ -657,8 +406,7 @@ internal sealed class QuestController
}
}
// TODO is this still required?
public sealed record StepProgress(
bool AetheryteShortcutUsed = false,
bool AethernetShortcutUsed = false,
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();
_navigationShortcutController.HandleNavigationShortcut();
_movementController.Update();
try
{
_movementController.Update();
}
catch (MovementController.PathfindingFailedException)
{
_questController.Stop();
}
}
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.String;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using Lumina.Excel.CustomSheets;
using Lumina.Excel.GeneratedSheets;
using Microsoft.Extensions.Logging;
@ -53,11 +55,12 @@ internal sealed unsafe class GameFunctions
private readonly ICondition _condition;
private readonly IClientState _clientState;
private readonly QuestRegistry _questRegistry;
private readonly IGameGui _gameGui;
private readonly ILogger<GameFunctions> _logger;
public GameFunctions(IDataManager dataManager, IObjectTable objectTable, ISigScanner sigScanner,
ITargetManager targetManager, ICondition condition, IClientState clientState, QuestRegistry questRegistry,
ILogger<GameFunctions> logger)
IGameGui gameGui, ILogger<GameFunctions> logger)
{
_dataManager = dataManager;
_objectTable = objectTable;
@ -65,6 +68,7 @@ internal sealed unsafe class GameFunctions
_condition = condition;
_clientState = clientState;
_questRegistry = questRegistry;
_gameGui = gameGui;
_logger = logger;
_processChatBox =
Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(sigScanner.ScanText(Signatures.SendChat));
@ -166,6 +170,8 @@ internal sealed unsafe class GameFunctions
public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation)
=> IsAetheryteUnlocked((uint)aetheryteLocation, out _);
public bool CanTeleport() => ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0;
public bool TeleportAetheryte(uint aetheryteId)
{
var status = ActionManager.Instance()->GetActionStatus(ActionType.Action, 5);
@ -342,7 +348,7 @@ internal sealed unsafe class GameFunctions
return null;
}
public void InteractWith(uint dataId)
public bool InteractWith(uint dataId)
{
GameObject? gameObject = FindObjectByDataId(dataId);
if (gameObject != null)
@ -350,9 +356,12 @@ internal sealed unsafe class GameFunctions
_logger.LogInformation("Setting target with {DataId} to {ObjectId}", dataId, gameObject.ObjectId);
_targetManager.Target = gameObject;
TargetSystem.Instance()->InteractWithObject(
ulong result = TargetSystem.Instance()->InteractWithObject(
(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject.Address, false);
return result != 0;
}
return false;
}
public void UseItem(uint itemId)
@ -422,46 +431,46 @@ internal sealed unsafe class GameFunctions
return false;
}
public void Mount()
public bool Mount()
{
if (!_condition[ConditionFlag.Mounted])
if (_condition[ConditionFlag.Mounted])
return true;
var playerState = PlayerState.Instance();
if (playerState != null && playerState->IsMountUnlocked(71))
{
var playerState = PlayerState.Instance();
if (playerState != null && playerState->IsMountUnlocked(71))
if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, 71) == 0)
{
if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, 71) == 0)
{
_logger.LogInformation("Using SDS Fenrir as mount");
ActionManager.Instance()->UseAction(ActionType.Mount, 71);
}
}
else
{
if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 9) == 0)
{
_logger.LogInformation("Using mount roulette");
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 9);
}
_logger.LogInformation("Using SDS Fenrir as mount");
return ActionManager.Instance()->UseAction(ActionType.Mount, 71);
}
}
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()
{
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);
}
else
_logger.LogWarning("Can't unmount right now?");
return true;
_logger.LogInformation("Unmounting...");
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23);
}
else
_logger.LogWarning("Can't unmount right now?");
return false;
return true;
}
public void OpenDutyFinder(uint contentFinderConditionId)
@ -508,4 +517,19 @@ internal sealed unsafe class GameFunctions
var questRow = _dataManager.GetExcelSheet<ContentTalk>()!.GetRow(rowId);
return questRow?.Text?.ToString();
}
public bool IsOccupied()
{
if (_gameGui.TryGetAddonByName("FadeMiddle", out AtkUnitBase* fade) &&
LAddon.IsAddonReady(fade) &&
fade->IsVisible)
return true;
return _condition[ConditionFlag.Occupied] || _condition[ConditionFlag.Occupied30] ||
_condition[ConditionFlag.Occupied33] || _condition[ConditionFlag.Occupied38] ||
_condition[ConditionFlag.Occupied39] || _condition[ConditionFlag.OccupiedInEvent] ||
_condition[ConditionFlag.OccupiedInQuestEvent] || _condition[ConditionFlag.OccupiedInCutSceneEvent] ||
_condition[ConditionFlag.Casting] || _condition[ConditionFlag.Unknown57] ||
_condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
}
}

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

View File

@ -7,8 +7,13 @@ using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Questionable.Controller;
using Questionable.Controller.Steps;
using Questionable.Controller.Steps.BaseFactory;
using Questionable.Controller.Steps.BaseTasks;
using Questionable.Controller.Steps.InteractionFactory;
using Questionable.Data;
using Questionable.External;
using Questionable.Windows;
@ -32,14 +37,15 @@ public sealed class QuestionablePlugin : IDalamudPlugin
ICondition condition,
IChatGui chatGui,
ICommandManager commandManager,
IAddonLifecycle addonLifecycle)
IAddonLifecycle addonLifecycle,
IKeyState keyState)
{
ArgumentNullException.ThrowIfNull(pluginInterface);
ServiceCollection serviceCollection = new();
serviceCollection.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace)
.ClearProviders()
.AddDalamudLogger(pluginLog));
.AddDalamudLogger(pluginLog, t => t[(t.LastIndexOf('.') + 1)..]));
serviceCollection.AddSingleton<IDalamudPlugin>(this);
serviceCollection.AddSingleton(pluginInterface);
serviceCollection.AddSingleton(clientState);
@ -53,6 +59,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton(chatGui);
serviceCollection.AddSingleton(commandManager);
serviceCollection.AddSingleton(addonLifecycle);
serviceCollection.AddSingleton(keyState);
serviceCollection.AddSingleton(new WindowSystem(nameof(Questionable)));
serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration());
@ -62,6 +69,39 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<NavmeshIpc>();
serviceCollection.AddSingleton<LifestreamIpc>();
// individual tasks
serviceCollection.AddTransient<MountTask>();
serviceCollection.AddTransient<UnmountTask>();
// tasks with factories
serviceCollection.AddTaskWithFactory<StepDisabled.Factory, StepDisabled.Task>();
serviceCollection.AddTaskWithFactory<AetheryteShortcut.Factory, AetheryteShortcut.UseAetheryteShortcut>();
serviceCollection.AddTaskWithFactory<SkipCondition.Factory, SkipCondition.CheckTask>();
serviceCollection.AddTaskWithFactory<AethernetShortcut.Factory, AethernetShortcut.UseAethernetShortcut>();
serviceCollection.AddTaskWithFactory<Move.Factory, Move.MoveInternal, Move.ExpectToBeNearDataId>();
serviceCollection.AddTransient<Move.MoveBuilder>();
serviceCollection.AddTaskWithFactory<AetherCurrent.Factory, AetherCurrent.DoAttune>();
serviceCollection.AddTaskWithFactory<AethernetShard.Factory, AethernetShard.DoAttune>();
serviceCollection.AddTaskWithFactory<Aetheryte.Factory, Aetheryte.DoAttune>();
serviceCollection.AddSingleton<ITaskFactory, Combat.Factory>();
serviceCollection.AddTaskWithFactory<Duty.Factory, Duty.OpenDutyFinder>();
serviceCollection.AddTaskWithFactory<Emote.Factory, Emote.UseOnObject, Emote.Use>();
serviceCollection.AddTaskWithFactory<Interact.Factory, Interact.DoInteract>();
serviceCollection.AddTaskWithFactory<Jump.Factory, Jump.DoJump>();
serviceCollection.AddTaskWithFactory<Say.Factory, Say.UseChat>();
serviceCollection.AddTaskWithFactory<UseItem.Factory, UseItem.UseOnGround, UseItem.UseOnObject, UseItem.Use>();
// TODO sort this in properly
serviceCollection.AddTaskWithFactory<ZoneChange.Factory, ZoneChange.WaitForZone>();
serviceCollection
.AddTaskWithFactory<WaitAtEnd.Factory,
WaitAtEnd.WaitDelay,
WaitAtEnd.WaitNextStepOrSequence,
WaitAtEnd.WaitForCompletionFlags,
WaitAtEnd.WaitObjectAtPosition>();
serviceCollection.AddSingleton<MovementController>();
serviceCollection.AddSingleton<QuestRegistry>();
serviceCollection.AddSingleton<QuestController>();

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

View File

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