From 81e849abc313b20f9f2cee8e776a757243ef70af Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sun, 9 Jun 2024 16:30:53 +0200 Subject: [PATCH] Rewrite logic, all quest steps can be executed automatically now --- .../4405_Back to Old Tricks.json | 18 +- ...json => 4406_Setting Things Straight.json} | 0 Questionable/Controller/GameUiController.cs | 4 +- Questionable/Controller/MovementController.cs | 48 +- Questionable/Controller/QuestController.cs | 654 ++++++------------ .../Steps/BaseFactory/AethernetShortcut.cs | 120 ++++ .../Steps/BaseFactory/AetheryteShortcut.cs | 96 +++ .../Controller/Steps/BaseFactory/Move.cs | 168 +++++ .../Steps/BaseFactory/SkipCondition.cs | 92 +++ .../Steps/BaseFactory/StepDisabled.cs | 34 + .../Controller/Steps/BaseFactory/WaitAtEnd.cs | 134 ++++ .../Steps/BaseFactory/ZoneChange.cs | 36 + .../Steps/BaseTasks/AbstractDelayedTask.cs | 37 + .../Controller/Steps/BaseTasks/MountTask.cs | 61 ++ .../Controller/Steps/BaseTasks/UnmountTask.cs | 36 + .../Steps/BaseTasks/WaitConditionTask.cs | 12 + Questionable/Controller/Steps/ETaskResult.cs | 16 + Questionable/Controller/Steps/ILastTask.cs | 6 + Questionable/Controller/Steps/ITask.cs | 11 + Questionable/Controller/Steps/ITaskFactory.cs | 17 + .../Steps/InteractionFactory/AetherCurrent.cs | 59 ++ .../InteractionFactory/AethernetShard.cs | 55 ++ .../Steps/InteractionFactory/Aetheryte.cs | 55 ++ .../Steps/InteractionFactory/Combat.cs | 47 ++ .../Steps/InteractionFactory/Duty.cs | 44 ++ .../Steps/InteractionFactory/Emote.cs | 77 +++ .../Steps/InteractionFactory/Interact.cs | 99 +++ .../Steps/InteractionFactory/Jump.cs | 77 +++ .../Steps/InteractionFactory/Say.cs | 52 ++ .../Steps/InteractionFactory/UseItem.cs | 109 +++ .../Controller/Steps/TaskException.cs | 20 + Questionable/DalamudInitializer.cs | 10 +- Questionable/GameFunctions.cs | 86 ++- Questionable/GlobalSuppressions.cs | 10 - Questionable/Questionable.csproj | 6 +- Questionable/QuestionablePlugin.cs | 44 +- Questionable/ServiceCollectionExtensions.cs | 84 +++ Questionable/Windows/DebugWindow.cs | 56 +- Questionable/packages.lock.json | 6 +- 39 files changed, 2074 insertions(+), 522 deletions(-) rename QuestPaths/Endwalker/MSQ/C-MareLamentorum/{4406_Settiing Things Straight.json => 4406_Setting Things Straight.json} (100%) create mode 100644 Questionable/Controller/Steps/BaseFactory/AethernetShortcut.cs create mode 100644 Questionable/Controller/Steps/BaseFactory/AetheryteShortcut.cs create mode 100644 Questionable/Controller/Steps/BaseFactory/Move.cs create mode 100644 Questionable/Controller/Steps/BaseFactory/SkipCondition.cs create mode 100644 Questionable/Controller/Steps/BaseFactory/StepDisabled.cs create mode 100644 Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs create mode 100644 Questionable/Controller/Steps/BaseFactory/ZoneChange.cs create mode 100644 Questionable/Controller/Steps/BaseTasks/AbstractDelayedTask.cs create mode 100644 Questionable/Controller/Steps/BaseTasks/MountTask.cs create mode 100644 Questionable/Controller/Steps/BaseTasks/UnmountTask.cs create mode 100644 Questionable/Controller/Steps/BaseTasks/WaitConditionTask.cs create mode 100644 Questionable/Controller/Steps/ETaskResult.cs create mode 100644 Questionable/Controller/Steps/ILastTask.cs create mode 100644 Questionable/Controller/Steps/ITask.cs create mode 100644 Questionable/Controller/Steps/ITaskFactory.cs create mode 100644 Questionable/Controller/Steps/InteractionFactory/AetherCurrent.cs create mode 100644 Questionable/Controller/Steps/InteractionFactory/AethernetShard.cs create mode 100644 Questionable/Controller/Steps/InteractionFactory/Aetheryte.cs create mode 100644 Questionable/Controller/Steps/InteractionFactory/Combat.cs create mode 100644 Questionable/Controller/Steps/InteractionFactory/Duty.cs create mode 100644 Questionable/Controller/Steps/InteractionFactory/Emote.cs create mode 100644 Questionable/Controller/Steps/InteractionFactory/Interact.cs create mode 100644 Questionable/Controller/Steps/InteractionFactory/Jump.cs create mode 100644 Questionable/Controller/Steps/InteractionFactory/Say.cs create mode 100644 Questionable/Controller/Steps/InteractionFactory/UseItem.cs create mode 100644 Questionable/Controller/Steps/TaskException.cs delete mode 100644 Questionable/GlobalSuppressions.cs create mode 100644 Questionable/ServiceCollectionExtensions.cs diff --git a/QuestPaths/Endwalker/MSQ/C-MareLamentorum/4405_Back to Old Tricks.json b/QuestPaths/Endwalker/MSQ/C-MareLamentorum/4405_Back to Old Tricks.json index 4e91502c..419959b5 100644 --- a/QuestPaths/Endwalker/MSQ/C-MareLamentorum/4405_Back to Old Tricks.json +++ b/QuestPaths/Endwalker/MSQ/C-MareLamentorum/4405_Back to Old Tricks.json @@ -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" } ] diff --git a/QuestPaths/Endwalker/MSQ/C-MareLamentorum/4406_Settiing Things Straight.json b/QuestPaths/Endwalker/MSQ/C-MareLamentorum/4406_Setting Things Straight.json similarity index 100% rename from QuestPaths/Endwalker/MSQ/C-MareLamentorum/4406_Settiing Things Straight.json rename to QuestPaths/Endwalker/MSQ/C-MareLamentorum/4406_Setting Things Straight.json diff --git a/Questionable/Controller/GameUiController.cs b/Questionable/Controller/GameUiController.cs index eb74e4f2..3e829334 100644 --- a/Questionable/Controller/GameUiController.cs +++ b/Questionable/Controller/GameUiController.cs @@ -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; } } diff --git a/Questionable/Controller/MovementController.cs b/Questionable/Controller/MovementController.cs index bacea45c..a3d97865 100644 --- a/Questionable/Controller/MovementController.cs +++ b/Questionable/Controller/MovementController.cs @@ -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 to, bool fly, bool sprint, float? stopDistance) + public void NavigateTo(EMovementType type, uint? dataId, List 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) + { + } + } } diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs index 34f7d722..14b076ce 100644 --- a/Questionable/Controller/QuestController.cs +++ b/Questionable/Controller/QuestController.cs @@ -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 _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 _taskFactories; - public QuestController(IClientState clientState, GameFunctions gameFunctions, MovementController movementController, - ILogger logger, ICondition condition, IChatGui chatGui, IFramework framework, - AetheryteData aetheryteData, LifestreamIpc lifestreamIpc, TerritoryData territoryData, - QuestRegistry questRegistry) + private readonly Queue _taskQueue = new(); + private ITask? _currentTask; + private bool _automatic; + + public QuestController( + IClientState clientState, + GameFunctions gameFunctions, + MovementController movementController, + ILogger logger, + QuestRegistry questRegistry, + IKeyState keyState, + IEnumerable 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 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 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); } diff --git a/Questionable/Controller/Steps/BaseFactory/AethernetShortcut.cs b/Questionable/Controller/Steps/BaseFactory/AethernetShortcut.cs new file mode 100644 index 00000000..a1d1fafa --- /dev/null +++ b/Questionable/Controller/Steps/BaseFactory/AethernetShortcut.cs @@ -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() + .With(step.AethernetShortcut.From, step.AethernetShortcut.To); + } + } + + internal sealed class UseAethernetShortcut( + ILogger 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})"; + } +} diff --git a/Questionable/Controller/Steps/BaseFactory/AetheryteShortcut.cs b/Questionable/Controller/Steps/BaseFactory/AetheryteShortcut.cs new file mode 100644 index 00000000..5220876d --- /dev/null +++ b/Questionable/Controller/Steps/BaseFactory/AetheryteShortcut.cs @@ -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 CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) + { + if (step.AetheryteShortcut == null) + return []; + + var task = serviceProvider.GetRequiredService() + .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 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})"; + } +} diff --git a/Questionable/Controller/Steps/BaseFactory/Move.cs b/Questionable/Controller/Steps/BaseFactory/Move.cs new file mode 100644 index 00000000..a3d6e1e0 --- /dev/null +++ b/Questionable/Controller/Steps/BaseFactory/Move.cs @@ -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 CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) + { + if (step.Position != null) + { + var builder = serviceProvider.GetRequiredService(); + 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(); + 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 logger, + GameFunctions gameFunctions, + IClientState clientState, + MovementController movementController) + { + public QuestStep Step { get; set; } = null!; + public Vector3 Destination { get; set; } + + public IEnumerable 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().With(Step.TerritoryId); + else if (Step.Mount == false) + yield return serviceProvider.GetRequiredService(); + + if (!Step.DisableNavmesh) + { + if (Step.Mount == null && actualDistance > 30f) + yield return serviceProvider.GetRequiredService().With(Step.TerritoryId); + + if (actualDistance > distance) + { + yield return serviceProvider.GetRequiredService() + .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() + .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 logger) : ITask + { + public Action StartAction { get; set; } = null!; + public Vector3 Destination { get; set; } + + public ITask With(Vector3 destination, Action 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; + } + } +} diff --git a/Questionable/Controller/Steps/BaseFactory/SkipCondition.cs b/Questionable/Controller/Steps/BaseFactory/SkipCondition.cs new file mode 100644 index 00000000..6d288d5f --- /dev/null +++ b/Questionable/Controller/Steps/BaseFactory/SkipCondition.cs @@ -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() + .With(step, quest.QuestId); + } + } + + internal sealed class CheckTask( + ILogger 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)})"; + } +} diff --git a/Questionable/Controller/Steps/BaseFactory/StepDisabled.cs b/Questionable/Controller/Steps/BaseFactory/StepDisabled.cs new file mode 100644 index 00000000..ecb367b2 --- /dev/null +++ b/Questionable/Controller/Steps/BaseFactory/StepDisabled.cs @@ -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(); + } + } + + internal sealed class Task(ILogger 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"; + } +} diff --git a/Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs b/Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs new file mode 100644 index 00000000..01d5c753 --- /dev/null +++ b/Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs @@ -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 CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) + { + if (step.CompletionQuestVariablesFlags.Count == 6) + { + var task = serviceProvider.GetRequiredService() + .With(quest, step); + var delay = serviceProvider.GetRequiredService(); + return [task, delay, new NextStep()]; + } + + switch (step.InteractionType) + { + case EInteractionType.Combat: + case EInteractionType.WaitForManualProgress: + case EInteractionType.ShouldBeAJump: + case EInteractionType.Instruction: + return [serviceProvider.GetRequiredService()]; + + 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(), new NextStep()]; + + default: + return [serviceProvider.GetRequiredService(), 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 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"; + } +} diff --git a/Questionable/Controller/Steps/BaseFactory/ZoneChange.cs b/Questionable/Controller/Steps/BaseFactory/ZoneChange.cs new file mode 100644 index 00000000..dbb89cac --- /dev/null +++ b/Questionable/Controller/Steps/BaseFactory/ZoneChange.cs @@ -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"; + } +} diff --git a/Questionable/Controller/Steps/BaseTasks/AbstractDelayedTask.cs b/Questionable/Controller/Steps/BaseTasks/AbstractDelayedTask.cs new file mode 100644 index 00000000..b43ee953 --- /dev/null +++ b/Questionable/Controller/Steps/BaseTasks/AbstractDelayedTask.cs @@ -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; +} diff --git a/Questionable/Controller/Steps/BaseTasks/MountTask.cs b/Questionable/Controller/Steps/BaseTasks/MountTask.cs new file mode 100644 index 00000000..1893804f --- /dev/null +++ b/Questionable/Controller/Steps/BaseTasks/MountTask.cs @@ -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 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"; +} diff --git a/Questionable/Controller/Steps/BaseTasks/UnmountTask.cs b/Questionable/Controller/Steps/BaseTasks/UnmountTask.cs new file mode 100644 index 00000000..ede325b5 --- /dev/null +++ b/Questionable/Controller/Steps/BaseTasks/UnmountTask.cs @@ -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 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"; +} diff --git a/Questionable/Controller/Steps/BaseTasks/WaitConditionTask.cs b/Questionable/Controller/Steps/BaseTasks/WaitConditionTask.cs new file mode 100644 index 00000000..93ad10c5 --- /dev/null +++ b/Questionable/Controller/Steps/BaseTasks/WaitConditionTask.cs @@ -0,0 +1,12 @@ +using System; + +namespace Questionable.Controller.Steps.BaseTasks; + +internal sealed class WaitConditionTask(Func predicate, string description) : ITask +{ + public bool Start() => predicate(); + + public ETaskResult Update() => predicate() ? ETaskResult.TaskComplete : ETaskResult.StillRunning; + + public override string ToString() => description; +} diff --git a/Questionable/Controller/Steps/ETaskResult.cs b/Questionable/Controller/Steps/ETaskResult.cs new file mode 100644 index 00000000..647e6ae1 --- /dev/null +++ b/Questionable/Controller/Steps/ETaskResult.cs @@ -0,0 +1,16 @@ +namespace Questionable.Controller.Steps; + +internal enum ETaskResult +{ + StillRunning, + + TaskComplete, + + /// + /// This step is complete, regardless of what any other following tasks would do. + /// + SkipRemainingTasksForStep, + + NextStep, + End, +} diff --git a/Questionable/Controller/Steps/ILastTask.cs b/Questionable/Controller/Steps/ILastTask.cs new file mode 100644 index 00000000..66cf214f --- /dev/null +++ b/Questionable/Controller/Steps/ILastTask.cs @@ -0,0 +1,6 @@ +namespace Questionable.Controller.Steps; + +internal interface ILastTask : ITask +{ + +} diff --git a/Questionable/Controller/Steps/ITask.cs b/Questionable/Controller/Steps/ITask.cs new file mode 100644 index 00000000..8354406d --- /dev/null +++ b/Questionable/Controller/Steps/ITask.cs @@ -0,0 +1,11 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Questionable.Controller.Steps; + +internal interface ITask +{ + bool Start(); + + ETaskResult Update(); +} diff --git a/Questionable/Controller/Steps/ITaskFactory.cs b/Questionable/Controller/Steps/ITaskFactory.cs new file mode 100644 index 00000000..159d5c12 --- /dev/null +++ b/Questionable/Controller/Steps/ITaskFactory.cs @@ -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 CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) + { + var task = CreateTask(quest, sequence, step); + if (task != null) + yield return task; + } +} diff --git a/Questionable/Controller/Steps/InteractionFactory/AetherCurrent.cs b/Questionable/Controller/Steps/InteractionFactory/AetherCurrent.cs new file mode 100644 index 00000000..02e7d5fa --- /dev/null +++ b/Questionable/Controller/Steps/InteractionFactory/AetherCurrent.cs @@ -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() + .With(step.DataId.Value, step.AetherCurrentId.Value); + } + } + + internal sealed class DoAttune(GameFunctions gameFunctions, ILogger 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})"; + } +} diff --git a/Questionable/Controller/Steps/InteractionFactory/AethernetShard.cs b/Questionable/Controller/Steps/InteractionFactory/AethernetShard.cs new file mode 100644 index 00000000..18976151 --- /dev/null +++ b/Questionable/Controller/Steps/InteractionFactory/AethernetShard.cs @@ -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() + .With((EAetheryteLocation)step.DataId); + } + } + + internal sealed class DoAttune(GameFunctions gameFunctions, ILogger 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})"; + } +} diff --git a/Questionable/Controller/Steps/InteractionFactory/Aetheryte.cs b/Questionable/Controller/Steps/InteractionFactory/Aetheryte.cs new file mode 100644 index 00000000..8f59a3d3 --- /dev/null +++ b/Questionable/Controller/Steps/InteractionFactory/Aetheryte.cs @@ -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() + .With((EAetheryteLocation)step.DataId.Value); + } + } + + internal sealed class DoAttune(GameFunctions gameFunctions, ILogger 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})"; + } +} diff --git a/Questionable/Controller/Steps/InteractionFactory/Combat.cs b/Questionable/Controller/Steps/InteractionFactory/Combat.cs new file mode 100644 index 00000000..969b8055 --- /dev/null +++ b/Questionable/Controller/Steps/InteractionFactory/Combat.cs @@ -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 CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) + { + if (step.InteractionType != EInteractionType.Combat) + return []; + + ArgumentNullException.ThrowIfNull(step.EnemySpawnType); + + var unmount = serviceProvider.GetRequiredService(); + if (step.EnemySpawnType == EEnemySpawnType.AfterInteraction) + { + ArgumentNullException.ThrowIfNull(step.DataId); + + var task = serviceProvider.GetRequiredService() + .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() + .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(); + } +} diff --git a/Questionable/Controller/Steps/InteractionFactory/Duty.cs b/Questionable/Controller/Steps/InteractionFactory/Duty.cs new file mode 100644 index 00000000..adb6d590 --- /dev/null +++ b/Questionable/Controller/Steps/InteractionFactory/Duty.cs @@ -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() + .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})"; + } +} diff --git a/Questionable/Controller/Steps/InteractionFactory/Emote.cs b/Questionable/Controller/Steps/InteractionFactory/Emote.cs new file mode 100644 index 00000000..8ecb4f58 --- /dev/null +++ b/Questionable/Controller/Steps/InteractionFactory/Emote.cs @@ -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 CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) + { + if (step.InteractionType != EInteractionType.Emote) + return []; + + ArgumentNullException.ThrowIfNull(step.Emote); + + var unmount = serviceProvider.GetRequiredService(); + if (step.DataId != null) + { + var task = serviceProvider.GetRequiredService().With(step.Emote.Value, step.DataId.Value); + return [unmount, task]; + } + else + { + var task = serviceProvider.GetRequiredService().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})"; + } +} diff --git a/Questionable/Controller/Steps/InteractionFactory/Interact.cs b/Questionable/Controller/Steps/InteractionFactory/Interact.cs new file mode 100644 index 00000000..fb7db657 --- /dev/null +++ b/Questionable/Controller/Steps/InteractionFactory/Interact.cs @@ -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().With(step.DataId.Value); + } + } + + internal sealed class DoInteract(GameFunctions gameFunctions, ICondition condition, ILogger 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})"; + } +} diff --git a/Questionable/Controller/Steps/InteractionFactory/Jump.cs b/Questionable/Controller/Steps/InteractionFactory/Jump.cs new file mode 100644 index 00000000..da029acd --- /dev/null +++ b/Questionable/Controller/Steps/InteractionFactory/Jump.cs @@ -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() + .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})"; + } +} diff --git a/Questionable/Controller/Steps/InteractionFactory/Say.cs b/Questionable/Controller/Steps/InteractionFactory/Say.cs new file mode 100644 index 00000000..9ec51c37 --- /dev/null +++ b/Questionable/Controller/Steps/InteractionFactory/Say.cs @@ -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 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(); + var task = serviceProvider.GetRequiredService().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})"; + } +} diff --git a/Questionable/Controller/Steps/InteractionFactory/UseItem.cs b/Questionable/Controller/Steps/InteractionFactory/UseItem.cs new file mode 100644 index 00000000..b562baf4 --- /dev/null +++ b/Questionable/Controller/Steps/InteractionFactory/UseItem.cs @@ -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 CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) + { + if (step.InteractionType != EInteractionType.UseItem) + return []; + + ArgumentNullException.ThrowIfNull(step.ItemId); + + var unmount = serviceProvider.GetRequiredService(); + if (step.GroundTarget == true) + { + ArgumentNullException.ThrowIfNull(step.DataId); + + var task = serviceProvider.GetRequiredService() + .With(step.DataId.Value, step.ItemId.Value); + return [unmount, task]; + } + else if (step.DataId != null) + { + var task = serviceProvider.GetRequiredService() + .With(step.DataId.Value, step.ItemId.Value); + return [unmount, task]; + } + else + { + var task = serviceProvider.GetRequiredService() + .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})"; + } +} diff --git a/Questionable/Controller/Steps/TaskException.cs b/Questionable/Controller/Steps/TaskException.cs new file mode 100644 index 00000000..41904c6e --- /dev/null +++ b/Questionable/Controller/Steps/TaskException.cs @@ -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) + { + } +} diff --git a/Questionable/DalamudInitializer.cs b/Questionable/DalamudInitializer.cs index c0f9d2cc..e2d03cf7 100644 --- a/Questionable/DalamudInitializer.cs +++ b/Questionable/DalamudInitializer.cs @@ -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) diff --git a/Questionable/GameFunctions.cs b/Questionable/GameFunctions.cs index df230854..3ca4c08d 100644 --- a/Questionable/GameFunctions.cs +++ b/Questionable/GameFunctions.cs @@ -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 _logger; public GameFunctions(IDataManager dataManager, IObjectTable objectTable, ISigScanner sigScanner, ITargetManager targetManager, ICondition condition, IClientState clientState, QuestRegistry questRegistry, - ILogger logger) + IGameGui gameGui, ILogger 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(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()!.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]; + } } diff --git a/Questionable/GlobalSuppressions.cs b/Questionable/GlobalSuppressions.cs deleted file mode 100644 index adfec4f8..00000000 --- a/Questionable/GlobalSuppressions.cs +++ /dev/null @@ -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")] diff --git a/Questionable/Questionable.csproj b/Questionable/Questionable.csproj index 48a10da9..c7874da7 100644 --- a/Questionable/Questionable.csproj +++ b/Questionable/Questionable.csproj @@ -1,7 +1,7 @@  net8.0-windows - 0.5 + 0.6 12 enable true @@ -23,7 +23,7 @@ - + @@ -59,6 +59,6 @@ - + diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index 1a6ea83d..ca86fa00 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -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(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(); serviceCollection.AddSingleton(); + // individual tasks + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + + // tasks with factories + serviceCollection.AddTaskWithFactory(); + serviceCollection.AddTaskWithFactory(); + serviceCollection.AddTaskWithFactory(); + serviceCollection.AddTaskWithFactory(); + serviceCollection.AddTaskWithFactory(); + serviceCollection.AddTransient(); + + serviceCollection.AddTaskWithFactory(); + serviceCollection.AddTaskWithFactory(); + serviceCollection.AddTaskWithFactory(); + serviceCollection.AddSingleton(); + serviceCollection.AddTaskWithFactory(); + serviceCollection.AddTaskWithFactory(); + serviceCollection.AddTaskWithFactory(); + serviceCollection.AddTaskWithFactory(); + serviceCollection.AddTaskWithFactory(); + serviceCollection.AddTaskWithFactory(); + + // TODO sort this in properly + serviceCollection.AddTaskWithFactory(); + + serviceCollection + .AddTaskWithFactory(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Questionable/ServiceCollectionExtensions.cs b/Questionable/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..a97401f2 --- /dev/null +++ b/Questionable/ServiceCollectionExtensions.cs @@ -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(); + serviceCollection.AddTransient(); + } + + 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(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + } + + 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(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + } + + 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(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + } +} diff --git a/Questionable/Windows/DebugWindow.cs b/Questionable/Windows/DebugWindow.cs index 79904f57..99ef7e82 100644 --- a/Questionable/Windows/DebugWindow.cs +++ b/Questionable/Windows/DebugWindow.cs @@ -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 _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 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() diff --git a/Questionable/packages.lock.json b/Questionable/packages.lock.json index 7c5ad370..e4dec3d5 100644 --- a/Questionable/packages.lock.json +++ b/Questionable/packages.lock.json @@ -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" }