diff --git a/QuestPaths/Endwalker/MSQ/B-Garlemald/4387_A Way Forward.json b/QuestPaths/Endwalker/MSQ/B-Garlemald/4387_A Way Forward.json index 6e96c3c55..49796fd65 100644 --- a/QuestPaths/Endwalker/MSQ/B-Garlemald/4387_A Way Forward.json +++ b/QuestPaths/Endwalker/MSQ/B-Garlemald/4387_A Way Forward.json @@ -77,7 +77,7 @@ "Z": 365.7129 }, "TerritoryId": 958, - "InteractionType": "WaitForManualProgress", + "InteractionType": "SinglePlayerDuty", "Comment": "Follow Alphinaud and Alisaie", "DialogueChoices": [ { diff --git a/QuestPaths/Endwalker/MSQ/B-Garlemald/4389_Personae non Gratae.json b/QuestPaths/Endwalker/MSQ/B-Garlemald/4389_Personae non Gratae.json index 81827ee08..fb628bc45 100644 --- a/QuestPaths/Endwalker/MSQ/B-Garlemald/4389_Personae non Gratae.json +++ b/QuestPaths/Endwalker/MSQ/B-Garlemald/4389_Personae non Gratae.json @@ -76,7 +76,7 @@ null, null, null, - 128 + -128 ] }, { diff --git a/QuestPaths/Endwalker/MSQ/B-Garlemald/4390_His Park Materials.json b/QuestPaths/Endwalker/MSQ/B-Garlemald/4390_His Park Materials.json index cee2e95c0..fd0028ed5 100644 --- a/QuestPaths/Endwalker/MSQ/B-Garlemald/4390_His Park Materials.json +++ b/QuestPaths/Endwalker/MSQ/B-Garlemald/4390_His Park Materials.json @@ -52,7 +52,15 @@ "Z": -503.2578 }, "TerritoryId": 958, - "InteractionType": "Interact" + "InteractionType": "Interact", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] }, { "DataId": 2012094, @@ -64,7 +72,15 @@ "TerritoryId": 958, "InteractionType": "Interact", "Comment": "Map", - "Mount": true + "Mount": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 + ] }, { "DataId": 2012005, @@ -86,7 +102,15 @@ }, "TerritoryId": 958, "InteractionType": "Interact", - "Comment": "Warmachine Wreckage" + "Comment": "Warmachine Wreckage", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 32 + ] }, { "DataId": 2012096, @@ -97,7 +121,15 @@ }, "TerritoryId": 958, "InteractionType": "Interact", - "Comment": "Children's Slide" + "Comment": "Children's Slide", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 16 + ] } ] }, diff --git a/QuestPaths/Endwalker/MSQ/F-Labyrinthos2/4446_Hither and Yarns.json b/QuestPaths/Endwalker/MSQ/F-Labyrinthos2/4446_Hither and Yarns.json index d954b745a..b00a949d4 100644 --- a/QuestPaths/Endwalker/MSQ/F-Labyrinthos2/4446_Hither and Yarns.json +++ b/QuestPaths/Endwalker/MSQ/F-Labyrinthos2/4446_Hither and Yarns.json @@ -21,6 +21,16 @@ { "Sequence": 1, "Steps": [ + { + "Position": { + "X": -148.48793, + "Y": -10.30035, + "Z": -247.25652 + }, + "TerritoryId": 956, + "InteractionType": "WalkTo", + "Comment": "Avoids Combat" + }, { "DataId": 2011987, "Position": { @@ -43,6 +53,16 @@ "Mount": true, "DisableNavmesh": true }, + { + "Position": { + "X": -480.30975, + "Y": -22.946651, + "Z": -145.08534 + }, + "TerritoryId": 956, + "InteractionType": "WalkTo", + "Comment": "Avoids Combat (typically)" + }, { "DataId": 2011988, "Position": { diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs index 14b076ce1..a47aee57e 100644 --- a/Questionable/Controller/QuestController.cs +++ b/Questionable/Controller/QuestController.cs @@ -71,7 +71,8 @@ internal sealed class QuestController if (_keyState[VirtualKey.ESCAPE]) { - Stop(); + if (_currentTask != null || _taskQueue.Count > 0) + Stop("ESC pressed"); _movementController.Stop(); } @@ -92,7 +93,7 @@ internal sealed class QuestController { _logger.LogInformation("No current quest, resetting data"); CurrentQuest = null; - Stop(); + Stop("Resetting current quest"); } } else if (CurrentQuest == null || CurrentQuest.Quest.QuestId != currentQuestId) @@ -101,14 +102,15 @@ internal sealed class QuestController { _logger.LogInformation("New quest: {QuestName}", quest.Name); CurrentQuest = new QuestProgress(quest, currentSequence, 0); + Stop("Different Quest"); } else if (CurrentQuest != null) { _logger.LogInformation("No active quest anymore? Not sure what happened..."); CurrentQuest = null; + Stop("No active Quest"); } - Stop(); return; } @@ -116,7 +118,7 @@ internal sealed class QuestController { DebugState = "No quest active"; Comment = null; - Stop(); + Stop("No quest active"); return; } @@ -140,11 +142,7 @@ internal sealed class QuestController if (CurrentQuest.Sequence != currentSequence) { CurrentQuest = CurrentQuest with { Sequence = currentSequence, Step = 0 }; - - bool automatic = _automatic; - Stop(); - if (automatic) - ExecuteNextStep(true); + Stop("New sequence", continueIfAutomatic: true); } var q = CurrentQuest.Quest; @@ -153,7 +151,7 @@ internal sealed class QuestController { DebugState = "Sequence not found"; Comment = null; - Stop(); + Stop("Unknown sequence"); return; } @@ -161,7 +159,8 @@ internal sealed class QuestController { DebugState = "Step completed"; Comment = null; - Stop(); + if (_currentTask != null || _taskQueue.Count > 0) + Stop("Step complete", continueIfAutomatic: true); return; } @@ -169,7 +168,7 @@ internal sealed class QuestController { DebugState = "Step not found"; Comment = null; - Stop(); + Stop("Unknown step"); return; } @@ -249,15 +248,31 @@ internal sealed class QuestController */ } - public void Stop() + private void ClearTasksInternal() { _currentTask = null; if (_taskQueue.Count > 0) _taskQueue.Clear(); + } + + public void Stop(string label, bool continueIfAutomatic = false) + { + using var scope = _logger.BeginScope(label); + + ClearTasksInternal(); // reset task queue - _automatic = false; + if (continueIfAutomatic && _automatic) + { + if (CurrentQuest?.Step is >= 0 and < 255) + ExecuteNextStep(true); + } + else + { + _logger.LogInformation("Stopping automatic questing"); + _automatic = false; + } } private void UpdateCurrentTask() @@ -286,7 +301,7 @@ internal sealed class QuestController catch (Exception e) { _logger.LogError(e, "Failed to start task {TaskName}", upcomingTask.ToString()); - Stop(); + Stop("Task failed to start"); return; } } @@ -302,7 +317,7 @@ internal sealed class QuestController catch (Exception e) { _logger.LogError(e, "Failed to update task {TaskName}", _currentTask.ToString()); - Stop(); + Stop("Task failed to update"); return; } @@ -312,7 +327,8 @@ internal sealed class QuestController return; case ETaskResult.SkipRemainingTasksForStep: - _logger.LogInformation("Result: {Result}, skipping remaining tasks for step", result); + _logger.LogInformation("{Task} → {Result}, skipping remaining tasks for step", + _currentTask, result); _currentTask = null; while (_taskQueue.TryDequeue(out ITask? nextTask)) @@ -327,28 +343,28 @@ internal sealed class QuestController return; case ETaskResult.TaskComplete: - _logger.LogInformation("Result: {Result}, remaining tasks: {RemainingTaskCount}", result, - _taskQueue.Count); + _logger.LogInformation("{Task} → {Result}, remaining tasks: {RemainingTaskCount}", + _currentTask, result, _taskQueue.Count); _currentTask = null; // handled in next update return; case ETaskResult.NextStep: - _logger.LogInformation("Result: {Result}", result); + _logger.LogInformation("{Task} → {Result}", _currentTask, result); IncreaseStepCount(true); return; case ETaskResult.End: - _logger.LogInformation("Result: {Result}", result); - Stop(); + _logger.LogInformation("{Task} → {Result}", _currentTask, result); + Stop("Task end"); return; } } public void ExecuteNextStep(bool automatic) { - Stop(); + ClearTasksInternal(); _automatic = automatic; (QuestSequence? seq, QuestStep? step) = GetNextStep(); @@ -382,6 +398,9 @@ internal sealed class QuestController return; } + _logger.LogInformation("Tasks for {QuestId}, {Sequence}, {Step}: {Tasks}", + CurrentQuest.Quest.QuestId, seq.Sequence, seq.Steps.IndexOf(step), + string.Join(", ", newTasks.Select(x => x.ToString()))); foreach (var task in newTasks) _taskQueue.Enqueue(task); } @@ -394,6 +413,9 @@ internal sealed class QuestController return _currentTask == null ? $"- (+{_taskQueue.Count})" : $"{_currentTask} (+{_taskQueue.Count})"; } + public bool HasCurrentTaskMatching() => + _currentTask is T; + public sealed record QuestProgress( Quest Quest, byte Sequence, diff --git a/Questionable/Controller/Steps/BaseFactory/SkipCondition.cs b/Questionable/Controller/Steps/BaseFactory/SkipCondition.cs index 6d288d5fc..f025a622c 100644 --- a/Questionable/Controller/Steps/BaseFactory/SkipCondition.cs +++ b/Questionable/Controller/Steps/BaseFactory/SkipCondition.cs @@ -14,10 +14,10 @@ internal static class SkipCondition { public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step) { - if (step.SkipIf.Count == 0) + if (step.SkipIf.Contains(ESkipCondition.Never)) return null; - if (step.SkipIf.Contains(ESkipCondition.Never)) + if (step.SkipIf.Count == 0 && step.CompletionQuestVariablesFlags.Count == 0) return null; return serviceProvider.GetRequiredService() diff --git a/Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs b/Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs index e0519faf6..91a43e262 100644 --- a/Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs +++ b/Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Numerics; +using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions; using Microsoft.Extensions.DependencyInjection; using Questionable.Controller.Steps.BaseTasks; @@ -13,7 +14,7 @@ namespace Questionable.Controller.Steps.BaseFactory; internal static class WaitAtEnd { - internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory + internal sealed class Factory(IServiceProvider serviceProvider, IClientState clientState) : ITaskFactory { public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) { @@ -54,6 +55,36 @@ internal static class WaitAtEnd new NextStep() ]; + case EInteractionType.Interact when step.TargetTerritoryId != null: + ITask waitInteraction; + if (step.TerritoryId != step.TargetTerritoryId) + { + // interaction moves to a different territory + waitInteraction = new WaitConditionTask(() => clientState.TerritoryType == step.TerritoryId, + $"Wait(tp to territory: {step.TerritoryId})"); + } + else + { + Vector3 lastPosition = step.Position ?? clientState.LocalPlayer?.Position ?? Vector3.Zero; + waitInteraction = new WaitConditionTask(() => + { + Vector3? currentPosition = clientState.LocalPlayer?.Position; + if (currentPosition == null) + return false; + + // interaction moved to elsewhere in the zone + return (lastPosition - currentPosition.Value).Length() > 20; + }, $"Wait(tp away from {lastPosition.ToString("G", CultureInfo.InvariantCulture)})"); + } + + return + [ + waitInteraction, + serviceProvider.GetRequiredService(), + new NextStep() + ]; + + case EInteractionType.Interact: default: return [serviceProvider.GetRequiredService(), new NextStep()]; } @@ -136,7 +167,7 @@ internal static class WaitAtEnd public ETaskResult Update() => ETaskResult.NextStep; - public override string ToString() => "Next Step"; + public override string ToString() => "NextStep"; } internal sealed class EndAutomation : ILastTask @@ -145,6 +176,6 @@ internal static class WaitAtEnd public ETaskResult Update() => ETaskResult.End; - public override string ToString() => "End automation"; + public override string ToString() => "EndAutomation"; } } diff --git a/Questionable/Controller/Steps/BaseTasks/MountTask.cs b/Questionable/Controller/Steps/BaseTasks/MountTask.cs index 1893804fd..65fb649d9 100644 --- a/Questionable/Controller/Steps/BaseTasks/MountTask.cs +++ b/Questionable/Controller/Steps/BaseTasks/MountTask.cs @@ -32,7 +32,10 @@ internal sealed class MountTask( } if (gameFunctions.HasStatusPreventingSprintOrMount()) + { + logger.LogInformation("Can't mount due to status preventing sprint or mount"); return false; + } logger.LogInformation("Step wants a mount, trying to mount in territory {Id}...", _territoryId); if (!condition[ConditionFlag.InCombat]) @@ -48,6 +51,12 @@ internal sealed class MountTask( { if (!_mountTriggered) { + if (gameFunctions.HasStatusPreventingSprintOrMount()) + { + logger.LogInformation("Can't mount due to status preventing sprint or mount"); + return ETaskResult.TaskComplete; + } + _mountTriggered = gameFunctions.Mount(); return ETaskResult.StillRunning; } diff --git a/Questionable/Controller/Steps/InteractionFactory/Combat.cs b/Questionable/Controller/Steps/InteractionFactory/Combat.cs index 969b8055d..b9ebcc751 100644 --- a/Questionable/Controller/Steps/InteractionFactory/Combat.cs +++ b/Questionable/Controller/Steps/InteractionFactory/Combat.cs @@ -24,7 +24,7 @@ internal static class Combat ArgumentNullException.ThrowIfNull(step.DataId); var task = serviceProvider.GetRequiredService() - .With(step.DataId.Value); + .With(step.DataId.Value, true); return [unmount, task]; } else if (step.EnemySpawnType == EEnemySpawnType.AfterItemUse) diff --git a/Questionable/Controller/Steps/InteractionFactory/Interact.cs b/Questionable/Controller/Steps/InteractionFactory/Interact.cs index fb7db657d..8ab55778e 100644 --- a/Questionable/Controller/Steps/InteractionFactory/Interact.cs +++ b/Questionable/Controller/Steps/InteractionFactory/Interact.cs @@ -21,21 +21,25 @@ internal static class Interact ArgumentNullException.ThrowIfNull(step.DataId); - return serviceProvider.GetRequiredService().With(step.DataId.Value); + return serviceProvider.GetRequiredService() + .With(step.DataId.Value, step.TargetTerritoryId != null); } } internal sealed class DoInteract(GameFunctions gameFunctions, ICondition condition, ILogger logger) : ITask { + private bool _needsUnmount; private bool _interacted; private DateTime _continueAt = DateTime.MinValue; private uint DataId { get; set; } + private bool SkipMarkerCheck { get; set; } - public ITask With(uint dataId) + public ITask With(uint dataId, bool skipMarkerCheck) { DataId = dataId; + SkipMarkerCheck = skipMarkerCheck; return this; } @@ -51,6 +55,7 @@ internal static class Interact // this is only relevant for followers on quests if (!gameObject.IsTargetable && condition[ConditionFlag.Mounted]) { + _needsUnmount = true; gameFunctions.Unmount(); _continueAt = DateTime.Now.AddSeconds(0.5); return true; @@ -71,6 +76,18 @@ internal static class Interact if (DateTime.Now <= _continueAt) return ETaskResult.StillRunning; + if (_needsUnmount) + { + if (condition[ConditionFlag.Mounted]) + { + gameFunctions.Unmount(); + _continueAt = DateTime.Now.AddSeconds(0.5); + return ETaskResult.StillRunning; + } + else + _needsUnmount = false; + } + if (!_interacted) { GameObject? gameObject = gameFunctions.FindObjectByDataId(DataId); @@ -87,7 +104,7 @@ internal static class Interact private unsafe bool HasAnyMarker(GameObject gameObject) { - if (gameObject.ObjectKind != ObjectKind.EventNpc) + if (SkipMarkerCheck || gameObject.ObjectKind != ObjectKind.EventNpc) return true; var gameObjectStruct = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject.Address; diff --git a/Questionable/DalamudInitializer.cs b/Questionable/DalamudInitializer.cs index e2d03cf7f..44607f2d2 100644 --- a/Questionable/DalamudInitializer.cs +++ b/Questionable/DalamudInitializer.cs @@ -54,7 +54,7 @@ internal sealed class DalamudInitializer : IDisposable } catch (MovementController.PathfindingFailedException) { - _questController.Stop(); + _questController.Stop("Pathfinding failed"); } } diff --git a/Questionable/GameFunctions.cs b/Questionable/GameFunctions.cs index c868f286b..f9bb1183f 100644 --- a/Questionable/GameFunctions.cs +++ b/Questionable/GameFunctions.cs @@ -470,17 +470,18 @@ internal sealed unsafe class GameFunctions public bool Unmount() { if (!_condition[ConditionFlag.Mounted]) - return false; + return true; if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23) == 0) { _logger.LogInformation("Unmounting..."); - ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23); + return ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23); } else + { _logger.LogWarning("Can't unmount right now?"); - - return true; + return false; + } } public void OpenDutyFinder(uint contentFinderConditionId) diff --git a/Questionable/Windows/DebugWindow.cs b/Questionable/Windows/DebugWindow.cs index 2d9b7837f..7e2d353e3 100644 --- a/Questionable/Windows/DebugWindow.cs +++ b/Questionable/Windows/DebugWindow.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Numerics; using Dalamud.Game.ClientState.Objects; using Dalamud.Interface; +using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Windowing; using Dalamud.Plugin; @@ -16,6 +17,7 @@ using ImGuiNET; using LLib.ImGui; using Microsoft.Extensions.Logging; using Questionable.Controller; +using Questionable.Controller.Steps.BaseFactory; using Questionable.Model; using Questionable.Model.V1; @@ -85,6 +87,7 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab ImGui.Separator(); DrawQuickAccessButtons(); + DrawRemainingTasks(); } private unsafe void DrawQuest() @@ -141,14 +144,25 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab if (ImGuiComponents.IconButton(FontAwesomeIcon.Stop)) { _movementController.Stop(); - _questController.Stop(); + _questController.Stop("Manual"); } + QuestStep? currentStep = currentQuest.Quest + .FindSequence(currentQuest.Sequence) + ?.FindStep(currentQuest.Step); + bool colored = currentStep != null && currentStep.InteractionType == EInteractionType.Instruction + && _questController.HasCurrentTaskMatching(); + + if (colored) + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.HealerGreen); if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.ArrowCircleRight, "Skip")) { - _questController.Stop(); + _movementController.Stop(); + _questController.Stop("Manual"); _questController.IncreaseStepCount(); } + if (colored) + ImGui.PopStyleColor(); } else ImGui.Text("No active quest"); @@ -278,7 +292,7 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab if (ImGui.Button("Stop Nav")) { _movementController.Stop(); - _questController.Stop(); + _questController.Stop("Manual"); } ImGui.EndDisabled(); @@ -289,7 +303,10 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab _framework.RunOnTick(() => _gameUiController.HandleCurrentDialogueChoices(), TimeSpan.FromMilliseconds(200)); } + } + private void DrawRemainingTasks() + { var remainingTasks = _questController.GetRemainingTaskNames(); if (remainingTasks.Count > 0) {