Updates for everything
authorLiza Carvelli <liza@carvel.li>
Sun, 26 May 2024 19:45:26 +0000 (21:45 +0200)
committerLiza Carvelli <liza@carvel.li>
Sun, 26 May 2024 19:45:26 +0000 (21:45 +0200)
45 files changed:
Questionable/Controller/MovementController.cs
Questionable/Controller/QuestController.cs
Questionable/Data/AetheryteData.cs [new file with mode: 0644]
Questionable/Data/TerritoryData.cs [new file with mode: 0644]
Questionable/External/NavmeshIpc.cs
Questionable/GameFunctions.cs
Questionable/Model/Quest.cs
Questionable/Model/V1/Converter/AethernetShortcutConverter.cs
Questionable/Model/V1/Converter/AetheryteConverter.cs [new file with mode: 0644]
Questionable/Model/V1/Converter/InteractionTypeConverter.cs
Questionable/Model/V1/Converter/VectorConverter.cs [new file with mode: 0644]
Questionable/Model/V1/EAetheryteLocation.cs
Questionable/Model/V1/EInteractionType.cs
Questionable/Model/V1/QuestData.cs
Questionable/Model/V1/QuestSequence.cs
Questionable/Model/V1/QuestStep.cs
Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4357_The Next Ship to Sail.json
Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4358_Old Sharlayan New to You.json
Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4359_Hitting the Books.json
Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4360_A Seat at the Last Stand.json
Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4362_Glorified Ratcatcher.json
Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4363_Deeper into the Maze.json
Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4364_The Medial Circuit.json
Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4365_The Full Reports Warts and All.json
Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4366_A Guide of Sorts.json
Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4369_On Low Tide.json
Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4370_A Fishermans Friend.json
Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4371_House of Divinities.json
Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4372_The Great Work.json
Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4373_Shadowed Footsteps.json
Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4374_A Boys Errand.json
Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4375_Tipping the Scale.json
Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4376_The Satrap of Radz at Han.json
Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4377_In the Dark of the Tower.json
Questionable/QuestPaths/Endwalker-B-Garlemald/4383_A Frosty Reception.json
Questionable/QuestPaths/Endwalker-E-Elpis/4419_Return to the Crystarium.json
Questionable/QuestPaths/Endwalker-E-Elpis/4420_Hope Upon a Flower.json
Questionable/QuestPaths/Endwalker-G-UltimaThule/4456_.Roads Paved of Sacrifice.json [deleted file]
Questionable/QuestPaths/Endwalker-G-UltimaThule/4456_Roads Paved of Sacrifice.json [new file with mode: 0644]
Questionable/QuestPaths/Endwalker-L-6.5/4744_Seeking the Light.json
Questionable/QuestPaths/Endwalker-L-6.5/4747_Back to Action.json
Questionable/QuestPaths/Endwalker-L-6.5/4750_Growing Light.json
Questionable/QuestSchema/schema_v1.json
Questionable/Questionable.cs
Questionable/Windows/DebugWindow.cs

index c8183dc604fac7fffb27767f2bdddfd9fc730c5f..334d6ace8e79eac57a7a3d3b15b5a9c66b3181b5 100644 (file)
@@ -11,6 +11,7 @@ namespace Questionable.Controller;
 
 internal sealed class MovementController : IDisposable
 {
+    public const float DefaultStopDistance = 3f;
     private readonly NavmeshIpc _navmeshIpc;
     private readonly IClientState _clientState;
     private readonly GameFunctions _gameFunctions;
@@ -30,6 +31,8 @@ internal sealed class MovementController : IDisposable
     public bool IsNavmeshReady => _navmeshIpc.IsReady;
     public bool IsPathRunning => _navmeshIpc.IsPathRunning;
     public bool IsPathfinding => _pathfindTask is { IsCompleted: false };
+    public Vector3? Destination { get; private set; }
+    public float StopDistance { get; private set; }
 
     public void Update()
     {
@@ -48,14 +51,24 @@ internal sealed class MovementController : IDisposable
                 ResetPathfinding();
             }
         }
+
+        if (IsPathRunning && Destination != null)
+        {
+            Vector3 localPlayerPosition = _clientState.LocalPlayer?.Position ?? Vector3.Zero;
+            if ((localPlayerPosition - Destination.Value).Length() < StopDistance)
+                Stop();
+        }
     }
 
-    public void NavigateTo(EMovementType type, Vector3 to, bool fly)
+    public void NavigateTo(EMovementType type, Vector3 to, bool fly, float? stopDistance = null)
     {
         ResetPathfinding();
 
+
         _gameFunctions.ExecuteCommand("/automove off");
 
+        Destination = to;
+        StopDistance = stopDistance ?? (DefaultStopDistance - 0.2f);
         _cancellationTokenSource = new();
         _cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(10));
         _pathfindTask =
index 095163a4b09fc89758e83660277d872f17446002..d3836af09e2da457124356aee654208ff9e97d69 100644 (file)
@@ -1,22 +1,61 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
 using System.IO;
+using System.Numerics;
 using System.Text.Json;
+using Dalamud.Game.ClientState.Conditions;
 using Dalamud.Plugin;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.Game.Object;
+using Questionable.Data;
 using Questionable.Model.V1;
 
 namespace Questionable.Controller;
 
 internal sealed class QuestController
 {
+    private readonly IClientState _clientState;
+    private readonly GameFunctions _gameFunctions;
+    private readonly MovementController _movementController;
+    private readonly IPluginLog _pluginLog;
+    private readonly ICondition _condition;
+    private readonly IChatGui _chatGui;
+    private readonly ICommandManager _commandManager;
+    private readonly AetheryteData _aetheryteData;
+    private readonly TerritoryData _territoryData;
     private readonly Dictionary<ushort, Quest> _quests = new();
 
-    public QuestController(DalamudPluginInterface pluginInterface)
+    public QuestController(DalamudPluginInterface pluginInterface, IDataManager dataManager, IClientState clientState,
+        GameFunctions gameFunctions, MovementController movementController, IPluginLog pluginLog, ICondition condition,
+        IChatGui chatGui, ICommandManager commandManager)
     {
+        _clientState = clientState;
+        _gameFunctions = gameFunctions;
+        _movementController = movementController;
+        _pluginLog = pluginLog;
+        _condition = condition;
+        _chatGui = chatGui;
+        _commandManager = commandManager;
+        _aetheryteData = new AetheryteData(dataManager);
+        _territoryData = new TerritoryData(dataManager);
 #if false
         LoadFromEmbeddedResources();
 #endif
         LoadFromDirectory(new DirectoryInfo(@"E:\ffxiv\Questionable\Questionable\QuestPaths"));
         LoadFromDirectory(pluginInterface.ConfigDirectory);
+
+        foreach (var (questId, quest) in _quests)
+        {
+            var questData =
+                dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets.Quest>()!.GetRow((uint)questId + 0x10000);
+            if (questData == null)
+                continue;
+
+            quest.Name = questData.Name.ToString();
+        }
     }
 
 #if false
@@ -40,20 +79,30 @@ internal sealed class QuestController
     }
 #endif
 
+    public QuestProgress? CurrentQuest { get; set; }
+    public string? DebugState { get; set; }
+
     private void LoadFromDirectory(DirectoryInfo configDirectory)
     {
         foreach (FileInfo fileInfo in configDirectory.GetFiles("*.json"))
         {
-            using FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
-            var (questId, name) = ExtractQuestDataFromName(fileInfo.Name);
-            Quest quest = new Quest
+            try
             {
-                FilePath = fileInfo.FullName,
-                QuestId = questId,
-                Name = name,
-                Data = JsonSerializer.Deserialize<QuestData>(stream)!,
-            };
-            _quests[questId] = quest;
+                using FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
+                var (questId, name) = ExtractQuestDataFromName(fileInfo.Name);
+                Quest quest = new Quest
+                {
+                    FilePath = fileInfo.FullName,
+                    QuestId = questId,
+                    Name = name,
+                    Data = JsonSerializer.Deserialize<QuestData>(stream)!,
+                };
+                _quests[questId] = quest;
+            }
+            catch (Exception e)
+            {
+                throw new InvalidDataException($"Unable to load file {fileInfo.FullName}", e);
+            }
         }
 
         foreach (DirectoryInfo childDirectory in configDirectory.GetDirectories())
@@ -65,7 +114,253 @@ internal sealed class QuestController
         string name = resourceName.Substring(0, resourceName.Length - ".json".Length);
         name = name.Substring(name.LastIndexOf('.') + 1);
 
-        ushort questId = ushort.Parse(name.Substring(0, name.IndexOf('_')));
-        return (questId, name);
+        string[] parts = name.Split('_', 2);
+        return (ushort.Parse(parts[0], CultureInfo.InvariantCulture), parts[1]);
+    }
+
+    public void Update()
+    {
+        (ushort currentQuestId, byte currentSequence) = _gameFunctions.GetCurrentQuest();
+        if (currentQuestId == 0)
+        {
+            if (CurrentQuest != null)
+                CurrentQuest = null;
+        }
+        else if (CurrentQuest == null || CurrentQuest.Quest.QuestId != currentQuestId)
+        {
+            if (_quests.TryGetValue(currentQuestId, out var quest))
+                CurrentQuest = new QuestProgress(quest, currentSequence, 0);
+            else if (CurrentQuest != null)
+                CurrentQuest = null;
+        }
+
+        if (CurrentQuest == null)
+        {
+            DebugState = "No quest active";
+            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])
+        {
+            DebugState = "Occupied";
+            return;
+        }
+
+        if (!_movementController.IsNavmeshReady)
+        {
+            DebugState = "Navmesh not ready";
+            return;
+        }
+        else if (_movementController.IsPathfinding || _movementController.IsPathRunning)
+        {
+            DebugState = "Path is running";
+            return;
+        }
+
+        if (CurrentQuest.Sequence != currentSequence)
+            CurrentQuest = CurrentQuest with { Sequence = currentSequence, Step = 0 };
+
+        var q = CurrentQuest.Quest;
+        var sequence = q.FindSequence(CurrentQuest.Sequence);
+        if (sequence == null)
+        {
+            DebugState = "Sequence not found";
+            return;
+        }
+
+        if (CurrentQuest.Step == 255)
+        {
+            DebugState = "Step completed";
+            return;
+        }
+
+        if (CurrentQuest.Step >= sequence.Steps.Count)
+        {
+            DebugState = "Step not found";
+            return;
+        }
+
+        var step = sequence.Steps[CurrentQuest.Step];
+        DebugState = step.Comment ?? sequence.Comment ?? q.Data.Comment;
+    }
+
+    public (QuestSequence? Sequence, QuestStep? Step) GetNextStep()
+    {
+        if (CurrentQuest == null)
+            return (null, null);
+
+        var q = CurrentQuest.Quest;
+        var seq = q.FindSequence(CurrentQuest.Sequence);
+        if (seq == null)
+            return (null, null);
+
+        if (CurrentQuest.Step >= seq.Steps.Count)
+            return (null, null);
+
+        return (seq, seq.Steps[CurrentQuest.Step]);
+    }
+
+    public void IncreaseStepCount()
+    {
+        (QuestSequence? seq, QuestStep? step) = GetNextStep();
+        if (seq == null || step == null)
+            return;
+
+        Debug.Assert(CurrentQuest != null, nameof(CurrentQuest) + " != null");
+        if (CurrentQuest.Step + 1 < seq.Steps.Count)
+        {
+            CurrentQuest = CurrentQuest with
+            {
+                Step = CurrentQuest.Step + 1,
+                AetheryteShortcutUsed = false,
+                AethernetShortcutUsed = false
+            };
+        }
+        else
+        {
+            CurrentQuest = CurrentQuest with
+            {
+                Step = 255,
+                AetheryteShortcutUsed = false,
+                AethernetShortcutUsed = false
+            };
+        }
+    }
+
+    public void ExecuteNextStep()
+    {
+        (QuestSequence? seq, QuestStep? step) = GetNextStep();
+        if (seq == null || step == null)
+            return;
+
+        Debug.Assert(CurrentQuest != null, nameof(CurrentQuest) + " != null");
+        if (!CurrentQuest.AetheryteShortcutUsed && step.AetheryteShortcut != null)
+        {
+            bool skipTeleport = false;
+            ushort territoryType = _clientState.TerritoryType;
+            if (step.TerritoryId == territoryType)
+            {
+                Vector3 playerPosition = _clientState.LocalPlayer!.Position;
+                if (_aetheryteData.CalculateDistance(playerPosition, territoryType, step.AetheryteShortcut.Value) < 11 ||
+                    (step.AethernetShortcut != null &&
+                     (_aetheryteData.CalculateDistance(playerPosition, territoryType, step.AethernetShortcut.From) < 11 ||
+                      _aetheryteData.CalculateDistance(playerPosition, territoryType, step.AethernetShortcut.To) < 11)))
+                {
+                    skipTeleport = true;
+                }
+            }
+
+            if (skipTeleport)
+            {
+                CurrentQuest = CurrentQuest with { AetheryteShortcutUsed = true };
+            }
+            else
+            {
+                if (step.AetheryteShortcut != null)
+                {
+                    if (!_gameFunctions.IsAetheryteUnlocked(step.AetheryteShortcut.Value))
+                        _chatGui.Print($"[Questionable] Aetheryte {step.AetheryteShortcut.Value} is not unlocked.");
+                    else if (_gameFunctions.TeleportAetheryte(step.AetheryteShortcut.Value))
+                        CurrentQuest = CurrentQuest with { AetheryteShortcutUsed = true };
+                    else
+                        _chatGui.Print("[Questionable] Unable to teleport to aetheryte.");
+                }
+                else
+                    _chatGui.Print("[Questionable] No aetheryte for teleport set.");
+
+                return;
+            }
+        }
+
+        if (!CurrentQuest.AethernetShortcutUsed)
+        {
+            if (step.AethernetShortcut != null)
+            {
+                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)
+                    {
+                        // unsure if this works across languages
+                        _commandManager.ProcessCommand(
+                            $"/li {_aetheryteData.AethernetNames[step.AethernetShortcut.To].Replace("The ", "", StringComparison.Ordinal)}");
+                        CurrentQuest = CurrentQuest with { AethernetShortcutUsed = true };
+                    }
+                    else
+                        _movementController.NavigateTo(EMovementType.Quest, _aetheryteData.Locations[from], false,
+                            6.9f);
+
+                    return;
+                }
+            }
+        }
+
+        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 (actualDistance > 30f && !_condition[ConditionFlag.Mounted] &&
+                _territoryData.CanUseMount(_clientState.TerritoryType))
+            {
+                unsafe
+                {
+                    ActionManager.Instance()->UseAction(ActionType.Mount, 71);
+                }
+
+                return;
+            }
+            else if (actualDistance > distance)
+            {
+                _movementController.NavigateTo(EMovementType.Quest, step.Position.Value,
+                    _gameFunctions.IsFlyingUnlocked(_clientState.TerritoryType), distance);
+                return;
+            }
+        }
+
+        switch (step.InteractionType)
+        {
+            case EInteractionType.Interact:
+            case EInteractionType.AttuneAetheryte:
+            case EInteractionType.AttuneAethernetShard:
+            case EInteractionType.AttuneAetherCurrent:
+                if (step.DataId != null)
+                {
+                    _gameFunctions.InteractWith(step.DataId.Value);
+                    IncreaseStepCount();
+                }
+
+                break;
+
+            case EInteractionType.WalkTo:
+                IncreaseStepCount();
+                break;
+
+            default:
+                _pluginLog.Warning($"Action '{step.InteractionType}' is not implemented");
+                break;
+        }
     }
+
+    public sealed record QuestProgress(
+        Quest Quest,
+        byte Sequence,
+        int Step,
+        bool AetheryteShortcutUsed = false,
+        bool AethernetShortcutUsed = false);
 }
diff --git a/Questionable/Data/AetheryteData.cs b/Questionable/Data/AetheryteData.cs
new file mode 100644 (file)
index 0000000..8bc261e
--- /dev/null
@@ -0,0 +1,130 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Numerics;
+using Dalamud.Plugin.Services;
+using Lumina.Excel.GeneratedSheets2;
+using Questionable.Model.V1;
+
+namespace Questionable.Data;
+
+internal sealed class AetheryteData
+{
+    public ReadOnlyDictionary<EAetheryteLocation, Vector3> Locations { get; } =
+        new Dictionary<EAetheryteLocation, Vector3>
+            {
+                { EAetheryteLocation.Gridania, new(32.913696f, 2.670288f, 30.014404f) },
+                { EAetheryteLocation.GridaniaArcher, new(166.58276f, -1.7243042f, 86.13721f) },
+                { EAetheryteLocation.GridaniaLeatherworker, new(101.27405f, 9.018005f, -111.31464f) },
+                { EAetheryteLocation.GridaniaLancer, new(121.23291f, 12.649658f, -229.63306f) },
+                { EAetheryteLocation.GridaniaConjurer, new(-145.15906f, 4.9591064f, -11.7647705f) },
+                { EAetheryteLocation.GridaniaBotanist, new(-311.0857f, 7.94989f, -177.05048f) },
+                { EAetheryteLocation.GridaniaAmphitheatre, new(-73.92999f, 7.9804688f, -140.15417f) },
+
+                { EAetheryteLocation.Uldah, new(-144.51825f, -1.3580933f, -169.6651f) },
+                { EAetheryteLocation.UldahAdventurers, new(64.22522f, 4.5318604f, -115.31244f) },
+                { EAetheryteLocation.UldahThaumaturge, new(-154.83331f, 14.633362f, 73.07532f) },
+                { EAetheryteLocation.UldahGladiator, new(-53.849182f, 10.696533f, 12.222412f) },
+                { EAetheryteLocation.UldahMiner, new(33.49353f, 13.229492f, 113.206665f) },
+                { EAetheryteLocation.UldahAlchemist, new(-98.25293f, 42.34375f, 88.45642f) },
+                { EAetheryteLocation.UldahWeaver, new(89.64673f, 12.924377f, 58.27417f) },
+                { EAetheryteLocation.UldahGoldsmith, new(-19.333252f, 14.602844f, 72.03784f) },
+                { EAetheryteLocation.UldahSapphireAvenue, new(131.9447f, 4.714966f, -29.800903f) },
+                { EAetheryteLocation.UldahChamberOfRule, new(6.6376343f, 30.655273f, -24.826477f) },
+
+                { EAetheryteLocation.Limsa, new(-84.031494f, 20.767456f, 0.015197754f) },
+                { EAetheryteLocation.LimsaAftcastle, new(16.067688f, 40.787354f, 68.80286f) },
+                { EAetheryteLocation.LimsaCulinarian, new(-56.50421f, 44.47998f, -131.45648f) },
+                { EAetheryteLocation.LimsaArcanist, new(-335.1645f, 12.619202f, 56.381958f) },
+                { EAetheryteLocation.LimsaFisher, new(-179.40033f, 4.8065186f, 182.97095f) },
+                { EAetheryteLocation.LimsaMarauder, new(-5.1728516f, 44.63257f, -218.06671f) },
+                { EAetheryteLocation.LimsaHawkersAlley, new(-213.61108f, 16.739136f, 51.80432f) },
+
+                // ... missing a few
+
+                { EAetheryteLocation.Crystarium, new(-65.0188f, 4.5318604f, 0.015197754f) },
+                { EAetheryteLocation.CrystariumMarkets, new(-6.149414f, -7.736328f, 148.72961f) },
+                { EAetheryteLocation.CrystariumThemenosRookery, new(-107.37775f, -0.015319824f, -58.762512f) },
+                { EAetheryteLocation.CrystariumDossalGate, new(64.86609f, -0.015319824f, -18.173523f) },
+                { EAetheryteLocation.CrystariumPendants, new(35.477173f, -0.015319824f, 222.58337f) },
+                { EAetheryteLocation.CrystariumAmaroLaunch, new(66.60559f, 35.99597f, -131.09033f) },
+                { EAetheryteLocation.CrystariumCrystallineMean, new(-52.506348f, 19.97406f, -173.35773f) },
+                { EAetheryteLocation.CrystariumCabinetOfCuriosity, new(-54.398438f, -37.70508f, -241.07733f) },
+
+                { EAetheryteLocation.Eulmore, new(0.015197754f, 81.986694f, 0.93078613f) },
+                { EAetheryteLocation.EulmoreMainstay, new(10.940674f, 36.087524f, -4.196289f) },
+                { EAetheryteLocation.EulmoreNightsoilPots, new(-54.093323f, -0.83929443f, 52.140015f) },
+                { EAetheryteLocation.EulmoreGloryGate, new(6.9122925f, 6.240906f, -56.351562f) },
+                { EAetheryteLocation.EulmoreSoutheastDerelict, new(71.82422f, -10.391418f, 65.32385f) },
+
+                // ... missing a few
+
+                { EAetheryteLocation.OldSharlayan, new(0.07623291f, 4.8065186f, -0.10687256f) },
+                { EAetheryteLocation.OldSharlayanStudium, new(-291.1574f, 20.004517f, -74.143616f) },
+                { EAetheryteLocation.OldSharlayanBaldesionAnnex, new(-92.21033f, 2.304016f, 29.709229f) },
+                { EAetheryteLocation.OldSharlayanRostra, new(-36.94214f, 41.367188f, -156.6034f) },
+                { EAetheryteLocation.OldSharlayanLeveilleurEstate, new(204.79126f, 21.774597f, -118.73047f) },
+                { EAetheryteLocation.OldSharlayanJourneysEnd, new(206.22559f, 1.8463135f, 13.77887f) },
+                { EAetheryteLocation.OldSharlayanScholarsHarbor, new(16.494995f, -16.250854f, 127.73328f) },
+
+                { EAetheryteLocation.RadzAtHan, new(25.986084f, 3.250122f, -27.023743f) },
+                { EAetheryteLocation.RadzAtHanMeghaduta, new(-365.95715f, 44.99878f, -31.815125f) },
+                { EAetheryteLocation.RadzAtHanRuveydahFibers, new(-156.14563f, 35.99597f, 27.725586f) },
+                { EAetheryteLocation.RadzAtHanAirship, new(-144.33508f, 27.969727f, 202.2583f) },
+                { EAetheryteLocation.RadzAtHanAlzadaalsPeace, new(6.6071167f, -2.02948f, 110.55151f) },
+                { EAetheryteLocation.RadzAtHanHallOfTheRadiantHost, new(-141.37488f, 3.982544f, -98.435974f) },
+                { EAetheryteLocation.RadzAtHanMehrydesMeyhane, new(-42.61847f, -0.015319824f, -197.61963f) },
+                { EAetheryteLocation.RadzAtHanKama, new(129.59485f, 26.993164f, 13.473633f) },
+                { EAetheryteLocation.RadzAtHanHighCrucible, new(57.90796f, -24.704407f, -210.6203f) },
+
+                { EAetheryteLocation.LabyrinthosArcheion, new(443.5338f, 170.6416f, -476.18835f) },
+                { EAetheryteLocation.LabyrinthosSharlayanHamlet, new(8.377136f, -27.542603f, -46.67737f) },
+                { EAetheryteLocation.LabyrinthosAporia, new(-729.18286f, -27.634155f, 302.1438f) },
+                { EAetheryteLocation.ThavnairYedlihmad, new(193.49963f, 6.9733276f, 629.2362f) },
+                { EAetheryteLocation.ThavnairGreatWork, new(-527.48914f, 4.776001f, 36.75891f) },
+                { EAetheryteLocation.ThavnairPalakasStand, new(405.1422f, 5.2643433f, -244.4953f) },
+                { EAetheryteLocation.GarlemaldCampBrokenGlass, new(-408.10254f, 24.15503f, 479.9724f) },
+                { EAetheryteLocation.GarlemaldTertium, new(518.9136f, -35.324707f, -178.36273f) },
+                { EAetheryteLocation.MareLamentorumSinusLacrimarum, new(-566.2471f, 134.66089f, 650.6294f) },
+                { EAetheryteLocation.MareLamentorumBestwaysBurrow, new(-0.015319824f, -128.83197f, -512.0165f) },
+                { EAetheryteLocation.ElpisAnagnorisis, new(159.96033f, 11.703674f, 126.878784f) },
+                { EAetheryteLocation.ElpisTwelveWonders, new(-633.7225f, -19.821533f, 542.56494f) },
+                { EAetheryteLocation.ElpisPoietenOikos, new(-529.9001f, 161.24207f, -222.2782f) },
+                { EAetheryteLocation.UltimaThuleReahTahra, new(-544.152f, 74.32666f, 269.6421f) },
+                { EAetheryteLocation.UltimaThuleAbodeOfTheEa, new(64.286255f, 272.48022f, -657.49603f) },
+                { EAetheryteLocation.UltimaThuleBaseOmicron, new(489.2804f, 437.5829f, 333.63843f) },
+            }
+            .AsReadOnly();
+
+    public ReadOnlyDictionary<EAetheryteLocation, string> AethernetNames { get; }
+    public ReadOnlyDictionary<EAetheryteLocation, ushort> TerritoryIds { get; }
+
+    public AetheryteData(IDataManager dataManager)
+    {
+        Dictionary<EAetheryteLocation, string> aethernetNames = new();
+        Dictionary<EAetheryteLocation, ushort> territoryIds = new();
+        foreach (var aetheryte in dataManager.GetExcelSheet<Aetheryte>()!.Where(x => x.RowId > 0))
+        {
+            string? aethernetName = aetheryte.AethernetName?.Value?.Name.ToString();
+            if (!string.IsNullOrEmpty(aethernetName))
+                aethernetNames[(EAetheryteLocation)aetheryte.RowId] = aethernetName;
+
+            if (aetheryte.Territory != null && aetheryte.Territory.Row > 0)
+                territoryIds[(EAetheryteLocation)aetheryte.RowId] = (ushort)aetheryte.Territory.Row;
+        }
+
+        AethernetNames = aethernetNames.AsReadOnly();
+        TerritoryIds = territoryIds.AsReadOnly();
+    }
+
+    public float CalculateDistance(Vector3 fromPosition, ushort fromTerritoryType, EAetheryteLocation to)
+    {
+        if (!TerritoryIds.TryGetValue(to, out ushort toTerritoryType) || fromTerritoryType != toTerritoryType)
+            return float.MaxValue;
+
+        if (!Locations.TryGetValue(to, out Vector3 toPosition))
+            return float.MaxValue;
+
+        return (fromPosition - toPosition).Length();
+    }
+}
diff --git a/Questionable/Data/TerritoryData.cs b/Questionable/Data/TerritoryData.cs
new file mode 100644 (file)
index 0000000..b37c8c4
--- /dev/null
@@ -0,0 +1,21 @@
+using System.Collections.Immutable;
+using System.Linq;
+using Dalamud.Plugin.Services;
+using Lumina.Excel.GeneratedSheets;
+
+namespace Questionable.Data;
+
+internal sealed class TerritoryData
+{
+    private readonly ImmutableHashSet<uint> _territoriesWithMount;
+
+    public TerritoryData(IDataManager dataManager)
+    {
+        _territoriesWithMount = dataManager.GetExcelSheet<TerritoryType>()!
+            .Where(x => x.RowId > 0 && x.Mount)
+            .Select(x => x.RowId)
+            .ToImmutableHashSet();
+    }
+
+    public bool CanUseMount(ushort territoryId) => _territoriesWithMount.Contains(territoryId);
+}
index d85f29e0e499cb2f35b7b421b72b93201512b867..84bea336200e976e20454f3766a681e4a5b4cc54 100644 (file)
@@ -53,6 +53,7 @@ internal sealed class NavmeshIpc
     public Task<List<Vector3>> Pathfind(Vector3 localPlayerPosition, Vector3 targetPosition, bool fly,
         CancellationToken cancellationToken)
     {
+        _pathSetTolerance.InvokeAction(0.25f);
         return _navPathfind.InvokeFunc(localPlayerPosition, targetPosition, fly, cancellationToken);
     }
 
index d8f7764bb1db2e8e828d64903996209072416a12..652ff60ac19fe3b43ebc0f2452c7481d0f4ab9f0 100644 (file)
@@ -3,17 +3,21 @@ using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
+using System.Numerics;
 using System.Runtime.InteropServices;
 using System.Text;
 using Dalamud.Game;
+using Dalamud.Game.ClientState.Objects;
 using Dalamud.Plugin.Services;
 using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.Game.Control;
 using FFXIVClientStructs.FFXIV.Client.Game.UI;
 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 Lumina.Excel.GeneratedSheets;
+using Questionable.Model.V1;
 
 namespace Questionable;
 
@@ -31,8 +35,15 @@ internal sealed unsafe class GameFunctions
     private readonly delegate* unmanaged<Utf8String*, int, IntPtr, void> _sanitiseString;
     private readonly ReadOnlyDictionary<ushort, byte> _territoryToAetherCurrentCompFlgSet;
 
-    public GameFunctions(IDataManager dataManager, ISigScanner sigScanner)
+    private readonly IObjectTable _objectTable;
+    private readonly ITargetManager _targetManager;
+    private readonly IPluginLog _pluginLog;
+
+    public GameFunctions(IDataManager dataManager, IObjectTable objectTable, ISigScanner sigScanner, ITargetManager targetManager, IPluginLog pluginLog)
     {
+        _objectTable = objectTable;
+        _targetManager = targetManager;
+        _pluginLog = pluginLog;
         _processChatBox =
             Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(sigScanner.ScanText(Signatures.SendChat));
         _sanitiseString =
@@ -45,7 +56,7 @@ internal sealed unsafe class GameFunctions
             .AsReadOnly();
     }
 
-    public (uint CurrentQuest, byte Sequence) GetCurrentQuest()
+    public (ushort CurrentQuest, byte Sequence) GetCurrentQuest()
     {
         var scenarioTree = AgentScenarioTree.Instance();
         if (scenarioTree == null)
@@ -69,9 +80,52 @@ internal sealed unsafe class GameFunctions
 
         //ImGui.Text($"Current Quest: {currentQuest}");
         //ImGui.Text($"Progress: {QuestManager.GetQuestSequence(currentQuest)}");
-        return (currentQuest, QuestManager.GetQuestSequence(currentQuest));
+        return ((ushort)currentQuest, QuestManager.GetQuestSequence(currentQuest));
+    }
+
+    public bool IsAetheryteUnlocked(uint aetheryteId, out byte subIndex)
+    {
+        var telepo = Telepo.Instance();
+        if (telepo == null || telepo->UpdateAetheryteList() == null)
+        {
+            subIndex = 0;
+            return false;
+        }
+
+        for (ulong i = 0; i < telepo->TeleportList.Size(); ++ i)
+        {
+            var data = telepo->TeleportList.Get(i);
+            if (data.AetheryteId == aetheryteId)
+            {
+                subIndex = data.SubIndex;
+                return true;
+            }
+        }
+
+        subIndex = 0;
+        return false;
+    }
+
+    public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation)
+        => IsAetheryteUnlocked((uint)aetheryteLocation, out _);
+
+    public bool TeleportAetheryte(uint aetheryteId)
+    {
+        var status = ActionManager.Instance()->GetActionStatus(ActionType.Action, 5);
+        if (status != 0)
+            return false;
+
+        if (IsAetheryteUnlocked(aetheryteId, out var subIndex))
+        {
+            return Telepo.Instance()->Teleport(aetheryteId, subIndex);
+        }
+
+        return false;
     }
 
+    public bool TeleportAetheryte(EAetheryteLocation aetheryteLocation)
+        => TeleportAetheryte((uint)aetheryteLocation);
+
     public bool IsFlyingUnlocked(ushort territoryId)
     {
         var playerState = PlayerState.Instance();
@@ -82,7 +136,7 @@ internal sealed unsafe class GameFunctions
 
     public void ExecuteCommand(string command)
     {
-        if (!command.StartsWith("/", StringComparison.Ordinal))
+        if (!command.StartsWith('/'))
             return;
 
         SendMessage(command);
@@ -190,21 +244,37 @@ internal sealed unsafe class GameFunctions
 
         internal ChatPayload(byte[] stringBytes)
         {
-            this.textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30);
-            Marshal.Copy(stringBytes, 0, this.textPtr, stringBytes.Length);
-            Marshal.WriteByte(this.textPtr + stringBytes.Length, 0);
+            textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30);
+            Marshal.Copy(stringBytes, 0, textPtr, stringBytes.Length);
+            Marshal.WriteByte(textPtr + stringBytes.Length, 0);
 
-            this.textLen = (ulong)(stringBytes.Length + 1);
+            textLen = (ulong)(stringBytes.Length + 1);
 
-            this.unk1 = 64;
-            this.unk2 = 0;
+            unk1 = 64;
+            unk2 = 0;
         }
 
         public void Dispose()
         {
-            Marshal.FreeHGlobal(this.textPtr);
+            Marshal.FreeHGlobal(textPtr);
         }
     }
 
     #endregion
+
+    public void InteractWith(uint dataId)
+    {
+        foreach (var gameObject in _objectTable)
+        {
+            if (gameObject.DataId == dataId)
+            {
+                _targetManager.Target = null;
+                _targetManager.Target = gameObject;
+
+                TargetSystem.Instance()->InteractWithObject(
+                    (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject.Address, false);
+                return;
+            }
+        }
+    }
 }
index 015ddda422220bf802b02315640ccf272b52948d..511e5ef18bea75a088ddd905d39f3cda8674f9d5 100644 (file)
@@ -1,4 +1,5 @@
-using Questionable.Model.V1;
+using System.Linq;
+using Questionable.Model.V1;
 
 namespace Questionable;
 
@@ -7,6 +8,9 @@ internal sealed class Quest
     public required string FilePath { get; init; }
 
     public required ushort QuestId { get; init; }
-    public required string Name { get; init; }
+    public required string Name { get; set; }
     public required QuestData Data { get; init; }
+
+    public QuestSequence? FindSequence(byte currentSequence)
+        => Data.QuestSequence.SingleOrDefault(seq => seq.Sequence == currentSequence);
 }
index 73d77b601c7b158f1a3de7b7c8c67dc075ee6194..0fb5affebb387ddd943ee94db26c16a10844ea0a 100644 (file)
@@ -61,14 +61,14 @@ public sealed class AethernetShortcutConverter : JsonConverter<AethernetShortcut
         { EAetheryteLocation.KuganeSekiseigumiBarracks, "[Kugane] Sekiseigumi Barracks" },
         { EAetheryteLocation.KuganeRakuzaDistrict, "[Kugane] Rakuza District" },
         { EAetheryteLocation.KuganeAirship, "[Kugane] Airship Landing" },
-        { EAetheryteLocation.Crystarium, "[The Crystarium] Aetheryte Plaza" },
-        { EAetheryteLocation.CrystariumMarkets, "[The Crystarium] Musica Universalis Markets" },
-        { EAetheryteLocation.CrystariumThemenosRookery, "[The Crystarium] Themenos Rookery" },
-        { EAetheryteLocation.CrystariumDossalGate, "[The Crystarium] The Dossal Gate" },
-        { EAetheryteLocation.CrystariumPendants, "[The Crystarium] The Pendants" },
-        { EAetheryteLocation.CrystariumAmaroLaunch, "[The Crystarium] The Amaro Launch" },
-        { EAetheryteLocation.CrystariumCrystallineMean, "[The Crystarium] The Crystalline Mean" },
-        { EAetheryteLocation.CrystariumCabinetOfCuriosity, "[The Crystarium] The Cabinet of Curiosity" },
+        { EAetheryteLocation.Crystarium, "[Crystarium] Aetheryte Plaza" },
+        { EAetheryteLocation.CrystariumMarkets, "[Crystarium] Musica Universalis Markets" },
+        { EAetheryteLocation.CrystariumThemenosRookery, "[Crystarium] Themenos Rookery" },
+        { EAetheryteLocation.CrystariumDossalGate, "[Crystarium] The Dossal Gate" },
+        { EAetheryteLocation.CrystariumPendants, "[Crystarium] The Pendants" },
+        { EAetheryteLocation.CrystariumAmaroLaunch, "[Crystarium] The Amaro Launch" },
+        { EAetheryteLocation.CrystariumCrystallineMean, "[Crystarium] The Crystalline Mean" },
+        { EAetheryteLocation.CrystariumCabinetOfCuriosity, "[Crystarium] The Cabinet of Curiosity" },
         { EAetheryteLocation.Eulmore, "[Eulmore] Aetheryte Plaza" },
         { EAetheryteLocation.EulmoreSoutheastDerelict, "[Eulmore] Southeast Derelicts" },
         { EAetheryteLocation.EulmoreNightsoilPots, "[Eulmore] Nightsoil Pots" },
@@ -95,7 +95,7 @@ public sealed class AethernetShortcutConverter : JsonConverter<AethernetShortcut
     private static readonly Dictionary<string, EAetheryteLocation> StringToEnum =
         EnumToString.ToDictionary(x => x.Value, x => x.Key);
 
-    public override AethernetShortcut? Read(ref Utf8JsonReader reader, Type typeToConvert,
+    public override AethernetShortcut Read(ref Utf8JsonReader reader, Type typeToConvert,
         JsonSerializerOptions options)
     {
         if (reader.TokenType != JsonTokenType.StartArray)
diff --git a/Questionable/Model/V1/Converter/AetheryteConverter.cs b/Questionable/Model/V1/Converter/AetheryteConverter.cs
new file mode 100644 (file)
index 0000000..a3fa26e
--- /dev/null
@@ -0,0 +1,96 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Questionable.Model.V1.Converter;
+
+public class AetheryteConverter : JsonConverter<EAetheryteLocation>
+{
+    private static readonly Dictionary<EAetheryteLocation, string> EnumToString = new()
+    {
+        { EAetheryteLocation.Limsa, "Limsa Lominsa" },
+        { EAetheryteLocation.Gridania, "Gridania" },
+        { EAetheryteLocation.Uldah, "Ul'dah" },
+        { EAetheryteLocation.Ishgard, "Ishgard" },
+
+        { EAetheryteLocation.RhalgrsReach, "Rhalgr's Reach" },
+        { EAetheryteLocation.FringesCastrumOriens, "Fringes - Castrum Oriens" },
+        { EAetheryteLocation.FringesPeeringStones, "Fringes - Peering Stones" },
+        { EAetheryteLocation.PeaksAlaGannha, "Peaks - Ala Gannha" },
+        { EAetheryteLocation.PeaksAlaGhiri, "Peaks - Ala Ghiri" },
+        { EAetheryteLocation.LochsPortaPraetoria, "Lochs - Porta Praetoria" },
+        { EAetheryteLocation.LochsAlaMhiganQuarter, "Lochs - Ala Mhigan Quarter" },
+        { EAetheryteLocation.Kugane, "Kugane" },
+        { EAetheryteLocation.RubySeaTamamizu, "Ruby Sea - Tamamizu" },
+        { EAetheryteLocation.RubySeaOnokoro, "Ruby Sea - Onokoro" },
+        { EAetheryteLocation.YanxiaNamai, "Yanxia - Namai" },
+        { EAetheryteLocation.YanxiaHouseOfTheFierce, "Yanxia - House of the Fierce" },
+        { EAetheryteLocation.AzimSteppeReunion, "Azim Steppe - Reunion" },
+        { EAetheryteLocation.AzimSteppeDawnThrone, "Azim Steppe - Dawn Throne" },
+        { EAetheryteLocation.AzimSteppeDhoroIloh, "Azim Steppe - Dhoro Iloh" },
+        { EAetheryteLocation.DomanEnclave, "Doman Enclave" },
+        { EAetheryteLocation.DomamEnclaveNorthern, "Doman Enclave - Northern Enclave" },
+        { EAetheryteLocation.DomamEnclaveSouthern, "Doman Enclave - Southern Enclave" },
+
+        { EAetheryteLocation.Crystarium, "Crystarium" },
+        { EAetheryteLocation.Eulmore, "Eulmore" },
+        { EAetheryteLocation.LakelandFortJobb, "Lakeland - Fort Jobb" },
+        { EAetheryteLocation.LakelandOstallImperative, "Lakeland - Ostall Imperative" },
+        { EAetheryteLocation.KholusiaStilltide, "Kholusia - Stilltide" },
+        { EAetheryteLocation.KholusiaWright, "Kholusia - Wright" },
+        { EAetheryteLocation.KholusiaTomra, "Kholusia - Tomra" },
+        { EAetheryteLocation.AmhAraengMordSouq, "Amh Araeng - Mord Souq" },
+        { EAetheryteLocation.AmhAraengInnAtJourneysHead, "Amh Araeng - Inn at Journey's Head" },
+        { EAetheryteLocation.AmhAraengTwine, "Amh Araeng - Twine" },
+        { EAetheryteLocation.RaktikaSlitherbough, "Rak'tika - Slitherbough" },
+        { EAetheryteLocation.RaktikaFanow, "Rak'tika - Fanow" },
+        { EAetheryteLocation.IlMhegLydhaLran, "Il Mheg - Lydha Lran" },
+        { EAetheryteLocation.IlMhegPiaEnni, "Il Mheg - Pia Enni" },
+        { EAetheryteLocation.IlMhegWolekdorf, "Il Mheg - Wolekdorf" },
+        { EAetheryteLocation.TempestOndoCups, "Tempest - Ondo Cups" },
+        { EAetheryteLocation.TempestMacarensesAngle, "Tempest - Macarenses Angle" },
+
+        { EAetheryteLocation.OldSharlayan, "Old Sharlayan" },
+        { EAetheryteLocation.RadzAtHan, "Radz-at-Han" },
+        { EAetheryteLocation.LabyrinthosArcheion, "Labyrinthos - Archeion" },
+        { EAetheryteLocation.LabyrinthosSharlayanHamlet, "Labyrinthos - Sharlayan Hamlet" },
+        { EAetheryteLocation.LabyrinthosAporia, "Labyrinthos - Aporia" },
+        { EAetheryteLocation.ThavnairYedlihmad, "Thavnair - Yedlihmad" },
+        { EAetheryteLocation.ThavnairGreatWork, "Thavnair - Great Work" },
+        { EAetheryteLocation.ThavnairPalakasStand, "Thavnair - Palaka's Stand" },
+        { EAetheryteLocation.GarlemaldCampBrokenGlass, "Garlemald - Camp Broken Glass" },
+        { EAetheryteLocation.GarlemaldTertium, "Garlemald - Tertium" },
+        { EAetheryteLocation.MareLamentorumSinusLacrimarum, "Mare Lamentorum - Sinus Lacrimarum" },
+        { EAetheryteLocation.MareLamentorumBestwaysBurrow, "Mare Lamentorum - Bestways Burrow" },
+        { EAetheryteLocation.ElpisAnagnorisis, "Elpis - Anagnorisis" },
+        { EAetheryteLocation.ElpisTwelveWonders, "Elpis - Twelve Wonders" },
+        { EAetheryteLocation.ElpisPoietenOikos, "Elpis - Poieten Oikos" },
+        { EAetheryteLocation.UltimaThuleReahTahra, "Ultima Thule - Reah Tahra" },
+        { EAetheryteLocation.UltimaThuleAbodeOfTheEa, "Ultima Thula - Abode of the Ea" },
+        { EAetheryteLocation.UltimaThuleBaseOmicron, "Ultima Thule - Base Omicron" }
+    };
+
+    private static readonly Dictionary<string, EAetheryteLocation> StringToEnum =
+        EnumToString.ToDictionary(x => x.Value, x => x.Key);
+
+    public override EAetheryteLocation Read(ref Utf8JsonReader reader, Type typeToConvert,
+        JsonSerializerOptions options)
+    {
+        if (reader.TokenType != JsonTokenType.String)
+            throw new JsonException();
+
+        string? str = reader.GetString();
+        if (str == null)
+            throw new JsonException();
+
+        return StringToEnum.TryGetValue(str, out EAetheryteLocation value) ? value : throw new JsonException();
+    }
+
+    public override void Write(Utf8JsonWriter writer, EAetheryteLocation value, JsonSerializerOptions options)
+    {
+        ArgumentNullException.ThrowIfNull(writer);
+        writer.WriteStringValue(EnumToString[value]);
+    }
+}
index b85313b50e3fda1c3a9d0fb5d68f30b0d55005af..bf4db121685b71db78c8020c0fc49d4dbe04bfa4 100644 (file)
@@ -12,7 +12,7 @@ public sealed class InteractionTypeConverter : JsonConverter<EInteractionType>
     {
         { EInteractionType.Interact, "Interact" },
         { EInteractionType.WalkTo, "WalkTo" },
-        { EInteractionType.AttuneAethenetShard, "AttuneAethenetShard" },
+        { EInteractionType.AttuneAethernetShard, "AttuneAethenetShard" },
         { EInteractionType.AttuneAetheryte, "AttuneAetheryte" },
         { EInteractionType.AttuneAetherCurrent, "AttuneAetherCurrent" },
         { EInteractionType.Combat, "Combat" },
diff --git a/Questionable/Model/V1/Converter/VectorConverter.cs b/Questionable/Model/V1/Converter/VectorConverter.cs
new file mode 100644 (file)
index 0000000..e7731e0
--- /dev/null
@@ -0,0 +1,66 @@
+using System;
+using System.Numerics;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Questionable.Model.V1.Converter;
+
+public class VectorConverter : JsonConverter<Vector3>
+{
+    public override Vector3 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+    {
+        if (reader.TokenType != JsonTokenType.StartObject)
+            throw new JsonException();
+
+        Vector3 vec = new Vector3();
+        while (reader.Read())
+        {
+            switch (reader.TokenType)
+            {
+                case JsonTokenType.PropertyName:
+                    string? propertyName = reader.GetString();
+                    if (propertyName == null || !reader.Read())
+                        throw new JsonException();
+
+                    switch (propertyName)
+                    {
+                        case nameof(Vector3.X):
+                            vec.X = reader.GetSingle();
+                            break;
+
+                        case nameof(Vector3.Y):
+                            vec.Y = reader.GetSingle();
+                            break;
+
+                        case nameof(Vector3.Z):
+                            vec.Z = reader.GetSingle();
+                            break;
+
+                        default:
+                            throw new JsonException();
+                    }
+
+                    break;
+
+                case JsonTokenType.EndObject:
+                    return vec;
+
+                default:
+                    throw new JsonException();
+            }
+        }
+
+        throw new JsonException();
+    }
+
+    public override void Write(Utf8JsonWriter writer, Vector3 value, JsonSerializerOptions options)
+    {
+        ArgumentNullException.ThrowIfNull(writer);
+
+        writer.WriteStartObject();
+        writer.WriteNumber(nameof(Vector3.X), value.X);
+        writer.WriteNumber(nameof(Vector3.Y), value.X);
+        writer.WriteNumber(nameof(Vector3.Z), value.X);
+        writer.WriteEndObject();
+    }
+}
index 514c52ea062f9f910c78e386679bbb7c3dea7c56..bb62eb1abed7a03b0158d83edd73a10e133c84e6 100644 (file)
@@ -2,6 +2,8 @@
 
 public enum EAetheryteLocation
 {
+    None = 0,
+
     Gridania = 2,
     GridaniaArcher = 25,
     GridaniaLeatherworker = 26,
@@ -60,6 +62,24 @@ public enum EAetheryteLocation
     KuganeRakuzaDistrict = 119,
     KuganeAirship = 120,
 
+    FringesCastrumOriens = 98,
+    FringesPeeringStones = 99,
+    PeaksAlaGannha = 100,
+    PeaksAlaGhiri = 101,
+    LochsPortaPraetoria = 102,
+    LochsAlaMhiganQuarter = 103,
+    RubySeaTamamizu = 105,
+    RubySeaOnokoro = 106,
+    YanxiaNamai = 107,
+    YanxiaHouseOfTheFierce = 108,
+    AzimSteppeReunion = 109,
+    AzimSteppeDawnThrone = 110,
+    AzimSteppeDhoroIloh = 128,
+
+    DomanEnclave = 127,
+    DomamEnclaveNorthern = 129,
+    DomamEnclaveSouthern = 130,
+
     Crystarium = 133,
     CrystariumMarkets = 149,
     CrystariumThemenosRookery = 150,
@@ -75,6 +95,22 @@ public enum EAetheryteLocation
     EulmoreGloryGate = 159,
     EulmoreSoutheastDerelict = 135,
 
+    LakelandFortJobb = 132,
+    LakelandOstallImperative = 136,
+    KholusiaStilltide = 137,
+    KholusiaWright = 138,
+    KholusiaTomra = 139,
+    AmhAraengMordSouq = 140,
+    AmhAraengInnAtJourneysHead = 141,
+    AmhAraengTwine = 142,
+    RaktikaSlitherbough = 143,
+    RaktikaFanow = 144,
+    IlMhegLydhaLran = 145,
+    IlMhegPiaEnni = 146,
+    IlMhegWolekdorf = 147,
+    TempestOndoCups = 148,
+    TempestMacarensesAngle = 156,
+
     OldSharlayan = 182,
     OldSharlayanStudium = 184,
     OldSharlayanBaldesionAnnex = 185,
@@ -92,4 +128,21 @@ public enum EAetheryteLocation
     RadzAtHanMehrydesMeyhane = 196,
     RadzAtHanKama = 198,
     RadzAtHanHighCrucible = 199,
+
+    LabyrinthosArcheion = 166,
+    LabyrinthosSharlayanHamlet = 167,
+    LabyrinthosAporia = 168,
+    ThavnairYedlihmad = 169,
+    ThavnairGreatWork = 170,
+    ThavnairPalakasStand = 171,
+    GarlemaldCampBrokenGlass = 172,
+    GarlemaldTertium = 173,
+    MareLamentorumSinusLacrimarum = 174,
+    MareLamentorumBestwaysBurrow = 175,
+    ElpisAnagnorisis = 176,
+    ElpisTwelveWonders = 177,
+    ElpisPoietenOikos = 178,
+    UltimaThuleReahTahra = 179,
+    UltimaThuleAbodeOfTheEa = 180,
+    UltimaThuleBaseOmicron = 181
 }
index bf29d34fa7bcebce35bb63884c33140c083044b1..2684f6d5a8a0ec8d5dda6cb855f37a8b6bafa4f2 100644 (file)
@@ -4,7 +4,7 @@ public enum EInteractionType
 {
     Interact,
     WalkTo,
-    AttuneAethenetShard,
+    AttuneAethernetShard,
     AttuneAetheryte,
     AttuneAetherCurrent,
     Combat,
index 79bfa1524245d3462c741f70f1732125f56fed23..0787b0f8060c12b8fbb96c369ac6305c9e8271ac 100644 (file)
@@ -7,7 +7,7 @@ public class QuestData
     public required int Version { get; set; }
     public required string Author { get; set; }
     public List<string> Contributors { get; set; } = new();
-    public string Comment { get; set; }
+    public string? Comment { get; set; }
     public List<ushort> TerritoryBlacklist { get; set; } = new();
     public required List<QuestSequence> QuestSequence { get; set; } = new();
 }
index f39d0e9a284b05bbe1dcf47863fcd66edd188fcd..cd691b13f9a3869dd83915596b14545e6b9ff244 100644 (file)
@@ -5,6 +5,6 @@ namespace Questionable.Model.V1;
 public class QuestSequence
 {
     public required int Sequence { get; set; }
-    public string Comment { get; set; }
+    public string? Comment { get; set; }
     public List<QuestStep> Steps { get; set; } = new();
 }
index 3c533322b42379803235bf35dc8c986e31d3edfe..e573c6a330eae58b4c4c3fafd343ea5bc046cd1c 100644 (file)
@@ -10,11 +10,19 @@ public class QuestStep
     [JsonConverter(typeof(InteractionTypeConverter))]
     public EInteractionType InteractionType { get; set; }
 
-    public ulong? DataId { get; set; }
-    public Vector3 Position { get; set; }
+    public uint? DataId { get; set; }
+
+    [JsonConverter(typeof(VectorConverter))]
+    public Vector3? Position { get; set; }
+
+    public float? StopDistance { get; set; }
     public ushort TerritoryId { get; set; }
     public bool Disabled { get; set; }
+    public string? Comment { get; set; }
+
+    [JsonConverter(typeof(AetheryteConverter))]
+    public EAetheryteLocation? AetheryteShortcut { get; set; }
 
     [JsonConverter(typeof(AethernetShortcutConverter))]
-    public AethernetShortcut AethernetShortcut { get; set; }
+    public AethernetShortcut? AethernetShortcut { get; set; }
 }
index 7083ecec2a7c3e5ba6bee05ecdf546e0968d1e7a..04501ad3ce2d20469c2b1f3bb9470b10022745c3 100644 (file)
@@ -15,6 +15,7 @@
           },
           "TerritoryId": 129,
           "InteractionType": "Interact",
+          "AetheryteShortcut": "Limsa Lominsa",
           "AethernetShortcut": [
             "[Limsa Lominsa] Aetheryte Plaza",
             "[Limsa Lominsa] Arcanist's Guild"
index e3fd4aa00d2ead9838e4523421a34cf5f6ac8a3b..118e6d8e7080f377630bab7306f6fe53daf79011 100644 (file)
         {
           "DataId": 1038578,
           "Position": {
-            "X": -53.576973,
+            "X": -56.737106,
             "Y": -15.127001,
-            "Z": 131.09679
+            "Z": 130.76611
           },
+          "StopDistance": 0.25,
           "TerritoryId": 962,
           "InteractionType": "Interact"
         }
         {
           "DataId": 1038578,
           "Position": {
-            "X": 0.86839586,
-            "Y": 3.2250001,
-            "Z": 9.54673
+            "X": -0.03130532,
+            "Y": 3.2249997,
+            "Z": 8.909777
           },
+          "StopDistance": 0.25,
           "TerritoryId": 962,
           "InteractionType": "Interact"
         }
         {
           "DataId": 1038578,
           "Position": {
-            "X": 65.84385,
+            "X": 66.10567,
             "Y": 5.0999994,
-            "Z": -63.417946
+            "Z": -63.37148
           },
+          "StopDistance": 0.25,
           "TerritoryId": 962,
           "InteractionType": "Interact"
         }
     {
       "Sequence": 6,
       "Steps": [
+        {
+          "Position": {
+            "X": 93.30914,
+            "Y": 8.920153,
+            "Z": -89.12467
+          },
+          "TerritoryId": 962,
+          "InteractionType": "WalkTo"
+        },
         {
           "DataId": 1038578,
           "Position": {
             "Y": 41.37599,
             "Z": -142.5033
           },
+          "StopDistance": 0.25,
           "TerritoryId": 962,
           "InteractionType": "Interact"
         }
             "Y": 41.367188,
             "Z": -156.6034
           },
+          "StopDistance": 0.25,
           "TerritoryId": 962,
           "InteractionType": "AttuneAethenetShard"
         },
             "Y": 1.0594833,
             "Z": 30.052902
           },
+          "StopDistance": 0.25,
           "TerritoryId": 962,
           "InteractionType": "Interact",
           "AethernetShortcut": [
index c4e31f40d536dd01d56ae488eabf655c483626c8..94715073d2463c49c6de9ee677e14dcc77ef4b32 100644 (file)
           "InteractionType": "Interact"
         }
       ]
+    },
+    {
+      "Sequence": 255,
+      "Steps": [
+        {
+          "DataId": 1038679,
+          "Position": {
+            "X": -275.5932,
+            "Y": 19.003881,
+            "Z": 13.321045
+          },
+          "TerritoryId": 962,
+          "InteractionType": "Interact"
+        }
+      ]
     }
   ]
 }
index 784be244486d9a0f4e8740da48ac544e9019e7f2..b26408cfd9dc99f9d15ce09f3f84963e36f1357b 100644 (file)
@@ -67,9 +67,9 @@
         {
           "DataId": 1037077,
           "Position": {
-            "X": -38.07129,
+            "X": -38.066784,
             "Y": -14.169313,
-            "Z": 105.30249
+            "Z": 107.68768
           },
           "TerritoryId": 962,
           "InteractionType": "Interact"
       "Steps": [
         {
           "Position": {
-            "X": 15.242085,
+            "X": 19.79008,
             "Y": -16.247002,
-            "Z": 109.177666
+            "Z": 108.36692
           },
           "TerritoryId": 962,
           "InteractionType": "WalkTo"
index 9571e69307b81ee1ae606612e4f47cf84ec326a3..ed2fc020edda2f158d043c345bf2392b47430c71 100644 (file)
@@ -44,6 +44,7 @@
           },
           "TerritoryId": 956,
           "InteractionType": "Combat",
+          "EnemySpawnType": "AutoOnEnterArea",
           "KillEnemyDataIds": [
             14024
           ]
@@ -77,6 +78,7 @@
           },
           "TerritoryId": 956,
           "InteractionType": "Combat",
+          "EnemySpawnType": "AutoOnEnterArea",
           "KillEnemyDataIds": [
             14023,
             14022
index 3b25f9c45e32d38e2d713c87d9295c001fd03e39..5d7676b532170d70703176345293bca15e2e722e 100644 (file)
           },
           "TerritoryId": 956,
           "InteractionType": "ManualAction",
-          "Comment": "Duty - Shoot some bird"
+          "Comment": "Duty - Shoot Large Green Bird"
         }
       ]
     },
index 6cc3d329feddeab9deb2d655dd58c6c4ef07cce4..50932ec44e1b8d6bc4ebd654421a0322a2c40284 100644 (file)
@@ -38,6 +38,7 @@
             "Z": -72.22095
           },
           "TerritoryId": 956,
+          "EnemySpawnType": "AfterInteraction",
           "InteractionType": "Combat",
           "KillEnemyDataIds": [
             14020
@@ -57,6 +58,7 @@
           },
           "TerritoryId": 956,
           "InteractionType": "Combat",
+          "EnemySpawnType": "AfterInteraction",
           "KillEnemyDataIds": [
             14021
           ]
       "Sequence": 5,
       "Steps": [
         {
-          "DataId": 2011984,
-          "Position": {
-            "X": 497.09314,
-            "Y": 73.41101,
-            "Z": -267.23126
-          },
-          "TerritoryId": 956,
-          "InteractionType": "AttuneAetherCurrent"
-        },
-        {
-          "DataId": 1038708,
+          "DataId": 1037985,
           "Position": {
-            "X": 408.31604,
-            "Y": 65.3329,
-            "Z": -130.11371
+            "X": 481.8036,
+            "Y": 66.16195,
+            "Z": -108.537415
           },
           "TerritoryId": 956,
           "InteractionType": "Interact"
           "InteractionType": "Interact"
         },
         {
-          "DataId": 1037985,
+          "DataId": 1038708,
           "Position": {
-            "X": 481.8036,
-            "Y": 66.16195,
-            "Z": -108.537415
+            "X": 408.31604,
+            "Y": 65.3329,
+            "Z": -130.11371
           },
           "TerritoryId": 956,
           "InteractionType": "Interact"
     {
       "Sequence": 6,
       "Steps": [
+        {
+          "DataId": 2011984,
+          "Position": {
+            "X": 497.09314,
+            "Y": 73.41101,
+            "Z": -267.23126
+          },
+          "TerritoryId": 956,
+          "InteractionType": "AttuneAetherCurrent"
+        },
         {
           "DataId": 2011843,
           "Position": {
index c9cfce334c7833251534a163a90e8a1f4e8c5c24..e525de45f151f49190ca549b7272010d73e5fb93 100644 (file)
@@ -56,6 +56,7 @@
             "Y": 81.17488,
             "Z": -534.99316
           },
+          "StopDistance": 0.25,
           "TerritoryId": 956,
           "InteractionType": "WalkTo"
         }
index 9485fef44c8c49b246af0203e654b595193b2b2e..350b87f8ce273a25f6177421442c5cef610f5bb4 100644 (file)
     {
       "Sequence": 1,
       "Steps": [
+        {
+          "Position": {
+            "X": -329.64972,
+            "Y": 77.91884,
+            "Z": -448.5044
+          },
+          "TerritoryId": 956,
+          "InteractionType": "WalkTo"
+        },
+        {
+          "Position": {
+            "X": -327.6718,
+            "Y": 79.535736,
+            "Z": -400.00397
+          },
+          "TerritoryId": 956,
+          "InteractionType": "WalkTo"
+        },
         {
           "DataId": 2011982,
           "Position": {
           "TerritoryId": 956,
           "InteractionType": "AttuneAetherCurrent"
         },
+        {
+          "Position": {
+            "X": -327.6718,
+            "Y": 79.535736,
+            "Z": -400.00397
+          },
+          "TerritoryId": 956,
+          "InteractionType": "WalkTo"
+        },
         {
           "DataId": 2011983,
           "Position": {
index 5e68fd39abf1ba3e9c257c0dad95d24cd4db9d4a..a828bbfaddf41b7b3658687fc5d6d8768c0cc1af 100644 (file)
             "Y": 6.991216,
             "Z": 629.2394
           },
+          "StopDistance": 5,
           "TerritoryId": 957,
           "InteractionType": "AttuneAetheryte"
         },
         {
+          "DataId": 2011948,
           "Position": {
-            "X": 190.45029,
-            "Y": 0.9965663,
-            "Z": 703.01746
-          },
-          "TerritoryId": 957,
-          "InteractionType": "WalkTo"
-        },
-        {
-          "DataId": 1037622,
-          "Position": {
-            "X": 187.9148,
-            "Y": 0.26447815,
-            "Z": 700.12964
+            "X": 204.66919,
+            "Y": 2.243042,
+            "Z": 715.4192
           },
           "TerritoryId": 957,
           "InteractionType": "Interact"
         },
         {
-          "DataId": 2011948,
+          "DataId": 1037622,
           "Position": {
-            "X": 204.66919,
-            "Y": 2.243042,
-            "Z": 715.4192
+            "X": 189.94562,
+            "Y": 0.8560083,
+            "Z": 702.7896
           },
+          "StopDistance": 0.25,
           "TerritoryId": 957,
           "InteractionType": "Interact"
         },
index d9d40c2645acb53eab02b3b3c0e5aa87063dce7e..851409398c00cd8dfe9644b27279a83dcc13203d 100644 (file)
             "Y": 1.769943,
             "Z": 738.9843
           },
+          "StopDistance": 0.25,
           "TerritoryId": 957,
           "InteractionType": "Interact"
         }
index f7ffcbc28b7d468823a0c71df04a813ab967d7a9..f0ec8bbd0e8a7261096b58a1871acafa2b1bd185 100644 (file)
@@ -59,6 +59,7 @@
           },
           "TerritoryId": 957,
           "InteractionType": "Combat",
+          "EnemySpawnType": "AutoOnEnterArea",
           "KillEnemyDataIds": [
             14006
           ]
index 03cc5b2bea9bae04d1d9914c49b4203c0ff7d42b..cf5170b38ce40e48091360ded16625010aef79c3 100644 (file)
@@ -42,6 +42,7 @@
             "Y": 4.785123,
             "Z": 36.76496
           },
+          "StopDistance": 5,
           "TerritoryId": 957,
           "InteractionType": "AttuneAetheryte"
         },
index e49a3073f50be9ef8ecaa1605745944d76122190..f5af5151f20997b04756a4614e3c5e5d78be3593 100644 (file)
@@ -29,6 +29,7 @@
           },
           "TerritoryId": 957,
           "InteractionType": "Combat",
+          "EnemySpawnType": "AfterInteraction",
           "KillEnemyDataIds": [
             14004
           ]
index 6b6603cc8d05caaf7d7bcf37f3c02793d2e5f768..89133562a8955476bf9b87fed298c651f550ab23 100644 (file)
             "Y": 88.84356,
             "Z": -608.6588
           },
+          "StopDistance": 0.25,
           "TerritoryId": 957,
           "InteractionType": "WalkTo"
         }
index 1047bad733f6923fcdbead92a047b9cb8d2823a8..aa5f5bb5fe5741133a5ab4b163972ed3eaf33956 100644 (file)
     {
       "Sequence": 1,
       "Steps": [
+        {
+          "Position": {
+            "X": -80.636894,
+            "Y": 99.974266,
+            "Z": -708.7214
+          },
+          "TerritoryId": 957,
+          "InteractionType": "WalkTo"
+        },
+        {
+          "Position": {
+            "X": -67.775665,
+            "Y": 97.140656,
+            "Z": -710.1025
+          },
+          "TerritoryId": 957,
+          "InteractionType": "WalkTo"
+        },
         {
           "DataId": 2011991,
           "Position": {
index ce1e3f9aebf22980b23c81b0390ee1df44b9baa8..eebb5a89cd4168c85d216eff4f9b40b8c77ee56b 100644 (file)
@@ -69,6 +69,7 @@
           },
           "TerritoryId": 962,
           "InteractionType": "Interact",
+          "AetheryteShortcut": "Old Sharlayan",
           "AethernetShortcut": [
             "[Old Sharlayan] Aetheryte Plaza",
             "[Old Sharlayan] The Baldesion Annex"
index fe2f81f2ecbce2c661a7d0666d5d94fca3c461b6..788e1b7f66b8dd13ce155ce4d032cd93484fb8fe 100644 (file)
@@ -28,6 +28,7 @@
             "Z": -569.8787
           },
           "TerritoryId": 957,
+          "AetheryteShortcut": "Thavnair - Great Work",
           "InteractionType": "Interact"
         }
       ]
index 407e9ed44ccfa6e9ec5c5477975dc6ca09f9cb21..e36f497a74faf1d61791f25d003268ae4b778591 100644 (file)
@@ -31,7 +31,8 @@
             "Z": 539.6046
           },
           "TerritoryId": 621,
-          "InteractionType": "Interact"
+          "InteractionType": "ManualAction",
+          "Comment": "Duty - A Frosty Reception"
         }
       ]
     },
index c7de2233fc0858cf47a651754ca6374bc6f49f22..94de9928faac06e74335949b61928f12631aa871 100644 (file)
@@ -46,8 +46,8 @@
           "InteractionType": "ManualAction",
           "Comment": "Cutscene Interaction needed",
           "AethernetShortcut": [
-            "[The Crystarium] Aetheryte Plaza",
-            "[The Crystarium] The Cabinet of Curiosity"
+            "[Crystarium] Aetheryte Plaza",
+            "[Crystarium] The Cabinet of Curiosity"
           ]
         }
       ]
index 343af2a8796a89d5424323a8d29d217e2db8ce28..2699e71f4a2fcf5892aaa15e4c2a57e7cd5e12cd 100644 (file)
@@ -30,8 +30,8 @@
           "TerritoryId": 819,
           "InteractionType": "Interact",
           "AethernetShortcut": [
-            "[The Crystarium] The Cabinet of Curiosity",
-            "[The Crystarium] The Dossal Gate"
+            "[Crystarium] The Cabinet of Curiosity",
+            "[Crystarium] The Dossal Gate"
           ]
         }
       ]
diff --git a/Questionable/QuestPaths/Endwalker-G-UltimaThule/4456_.Roads Paved of Sacrifice.json b/Questionable/QuestPaths/Endwalker-G-UltimaThule/4456_.Roads Paved of Sacrifice.json
deleted file mode 100644 (file)
index c3e3438..0000000
+++ /dev/null
@@ -1,171 +0,0 @@
-{
-  "Version": 1,
-  "Author": "liza",
-  "QuestSequence": [
-    {
-      "Sequence": 0,
-      "Steps": [
-        {
-          "DataId": 1040307,
-          "Position": {
-            "X": -469.29126,
-            "Y": 232.2548,
-            "Z": -252.8573
-          },
-          "TerritoryId": 960,
-          "InteractionType": "Interact"
-        }
-      ]
-    },
-    {
-      "Sequence": 1,
-      "Steps": [
-        {
-          "DataId": 1040315,
-          "Position": {
-            "X": -345.87628,
-            "Y": 254.66968,
-            "Z": -277.27173
-          },
-          "TerritoryId": 960,
-          "InteractionType": "Interact"
-        }
-      ]
-    },
-    {
-      "Sequence": 2,
-      "Steps": [
-        {
-          "DataId": 1040313,
-          "Position": {
-            "X": -339.10126,
-            "Y": 255.53401,
-            "Z": -281.75793
-          },
-          "TerritoryId": 960,
-          "InteractionType": "Interact"
-        }
-      ]
-    },
-    {
-      "Sequence": 3,
-      "Steps": [
-        {
-          "DataId": 2012030,
-          "Position": {
-            "X": -333.547,
-            "Y": 270.83228,
-            "Z": -361.50153
-          },
-          "TerritoryId": 960,
-          "InteractionType": "AttuneAetherCurrent"
-        },
-        {
-          "DataId": 2012035,
-          "Position": {
-            "X": -238.81903,
-            "Y": 320.36304,
-            "Z": -295.15533
-          },
-          "TerritoryId": 960,
-          "InteractionType": "AttuneAetherCurrent"
-        },
-        {
-          "DataId": 1040317,
-          "Position": {
-            "X": -200.57983,
-            "Y": 268.01642,
-            "Z": -312.58112
-          },
-          "TerritoryId": 960,
-          "InteractionType": "Interact"
-        }
-      ]
-    },
-    {
-      "Sequence": 4,
-      "Steps": [
-        {
-          "DataId": 1040318,
-          "Position": {
-            "X": 29.984009,
-            "Y": 270.45825,
-            "Z": -535.0271
-          },
-          "TerritoryId": 960,
-          "InteractionType": "Interact"
-        }
-      ]
-    },
-    {
-      "Sequence": 5,
-      "Steps": [
-        {
-          "DataId": 2012354,
-          "Position": {
-            "X": 30.777344,
-            "Y": 272.51086,
-            "Z": -600.7019
-          },
-          "TerritoryId": 960,
-          "InteractionType": "Interact"
-        },
-        {
-          "DataId": 2012355,
-          "Position": {
-            "X": 64.10315,
-            "Y": 272.4497,
-            "Z": -616.4492
-          },
-          "TerritoryId": 960,
-          "InteractionType": "Interact"
-        },
-        {
-          "DataId": 180,
-          "Position": {
-            "X": 64.286255,
-            "Y": 272.48022,
-            "Z": -657.49603
-          },
-          "TerritoryId": 960,
-          "InteractionType": "AttuneAetheryte"
-        },
-        {
-          "DataId": 2012356,
-          "Position": {
-            "X": 115.526,
-            "Y": 272.99915,
-            "Z": -617.853
-          },
-          "TerritoryId": 960,
-          "InteractionType": "Interact"
-        },
-        {
-          "DataId": 2012357,
-          "Position": {
-            "X": 151.59839,
-            "Y": 272.9381,
-            "Z": -592.5841
-          },
-          "TerritoryId": 960,
-          "InteractionType": "Interact"
-        }
-      ]
-    },
-    {
-      "Sequence": 255,
-      "Steps": [
-        {
-          "DataId": 1040318,
-          "Position": {
-            "X": 29.984009,
-            "Y": 270.45825,
-            "Z": -535.0271
-          },
-          "TerritoryId": 960,
-          "InteractionType": "Interact"
-        }
-      ]
-    }
-  ]
-}
diff --git a/Questionable/QuestPaths/Endwalker-G-UltimaThule/4456_Roads Paved of Sacrifice.json b/Questionable/QuestPaths/Endwalker-G-UltimaThule/4456_Roads Paved of Sacrifice.json
new file mode 100644 (file)
index 0000000..c3e3438
--- /dev/null
@@ -0,0 +1,171 @@
+{
+  "Version": 1,
+  "Author": "liza",
+  "QuestSequence": [
+    {
+      "Sequence": 0,
+      "Steps": [
+        {
+          "DataId": 1040307,
+          "Position": {
+            "X": -469.29126,
+            "Y": 232.2548,
+            "Z": -252.8573
+          },
+          "TerritoryId": 960,
+          "InteractionType": "Interact"
+        }
+      ]
+    },
+    {
+      "Sequence": 1,
+      "Steps": [
+        {
+          "DataId": 1040315,
+          "Position": {
+            "X": -345.87628,
+            "Y": 254.66968,
+            "Z": -277.27173
+          },
+          "TerritoryId": 960,
+          "InteractionType": "Interact"
+        }
+      ]
+    },
+    {
+      "Sequence": 2,
+      "Steps": [
+        {
+          "DataId": 1040313,
+          "Position": {
+            "X": -339.10126,
+            "Y": 255.53401,
+            "Z": -281.75793
+          },
+          "TerritoryId": 960,
+          "InteractionType": "Interact"
+        }
+      ]
+    },
+    {
+      "Sequence": 3,
+      "Steps": [
+        {
+          "DataId": 2012030,
+          "Position": {
+            "X": -333.547,
+            "Y": 270.83228,
+            "Z": -361.50153
+          },
+          "TerritoryId": 960,
+          "InteractionType": "AttuneAetherCurrent"
+        },
+        {
+          "DataId": 2012035,
+          "Position": {
+            "X": -238.81903,
+            "Y": 320.36304,
+            "Z": -295.15533
+          },
+          "TerritoryId": 960,
+          "InteractionType": "AttuneAetherCurrent"
+        },
+        {
+          "DataId": 1040317,
+          "Position": {
+            "X": -200.57983,
+            "Y": 268.01642,
+            "Z": -312.58112
+          },
+          "TerritoryId": 960,
+          "InteractionType": "Interact"
+        }
+      ]
+    },
+    {
+      "Sequence": 4,
+      "Steps": [
+        {
+          "DataId": 1040318,
+          "Position": {
+            "X": 29.984009,
+            "Y": 270.45825,
+            "Z": -535.0271
+          },
+          "TerritoryId": 960,
+          "InteractionType": "Interact"
+        }
+      ]
+    },
+    {
+      "Sequence": 5,
+      "Steps": [
+        {
+          "DataId": 2012354,
+          "Position": {
+            "X": 30.777344,
+            "Y": 272.51086,
+            "Z": -600.7019
+          },
+          "TerritoryId": 960,
+          "InteractionType": "Interact"
+        },
+        {
+          "DataId": 2012355,
+          "Position": {
+            "X": 64.10315,
+            "Y": 272.4497,
+            "Z": -616.4492
+          },
+          "TerritoryId": 960,
+          "InteractionType": "Interact"
+        },
+        {
+          "DataId": 180,
+          "Position": {
+            "X": 64.286255,
+            "Y": 272.48022,
+            "Z": -657.49603
+          },
+          "TerritoryId": 960,
+          "InteractionType": "AttuneAetheryte"
+        },
+        {
+          "DataId": 2012356,
+          "Position": {
+            "X": 115.526,
+            "Y": 272.99915,
+            "Z": -617.853
+          },
+          "TerritoryId": 960,
+          "InteractionType": "Interact"
+        },
+        {
+          "DataId": 2012357,
+          "Position": {
+            "X": 151.59839,
+            "Y": 272.9381,
+            "Z": -592.5841
+          },
+          "TerritoryId": 960,
+          "InteractionType": "Interact"
+        }
+      ]
+    },
+    {
+      "Sequence": 255,
+      "Steps": [
+        {
+          "DataId": 1040318,
+          "Position": {
+            "X": 29.984009,
+            "Y": 270.45825,
+            "Z": -535.0271
+          },
+          "TerritoryId": 960,
+          "InteractionType": "Interact"
+        }
+      ]
+    }
+  ]
+}
index 99b0da3f583fe7051f549d9a78d429e37d359899..1a2bc813644e986c44ac6339aa582c03b9bbe620 100644 (file)
@@ -45,8 +45,8 @@
           "TerritoryId": 819,
           "InteractionType": "Interact",
           "AethernetShortcut": [
-            "[The Crystarium] Aetheryte Plaza",
-            "[The Crystarium] The Cabinet of Curiosity"
+            "[Crystarium] Aetheryte Plaza",
+            "[Crystarium] The Cabinet of Curiosity"
           ]
         }
       ]
index 8b557f614dbb532a8bcb82a5a34f9bc8d0062d6c..a8cbb0359c99f70f4600131c907d7fe43c65c624 100644 (file)
@@ -1,5 +1,5 @@
 {
-  "Version": 1
+  "Version": 1,
   "Author": "liza",
   "QuestSequence": [
     {
@@ -30,8 +30,8 @@
           "TerritoryId": 819,
           "InteractionType": "Interact",
           "AethernetShortcut": [
-            "[The Crystarium] Aetheryte Plaza",
-            "[The Crystarium] The Dossal Gate"
+            "[Crystarium] Aetheryte Plaza",
+            "[Crystarium] The Dossal Gate"
           ]
         }
       ]
index 68ecbdfa8bf4f80dead902a5a8487c9edb66d35c..8da8fc1842023a8e126cd523584e4fe6e491e547 100644 (file)
@@ -30,8 +30,8 @@
           "TerritoryId": 819,
           "InteractionType": "Interact",
           "AethernetShortcut": [
-            "[The Crystarium] Aetheryte Plaza",
-            "[The Crystarium] The Dossal Gate"
+            "[Crystarium] Aetheryte Plaza",
+            "[Crystarium] The Dossal Gate"
           ]
         }
       ]
index bf35fe5a6c1dab6be9613a3c41eb14d38ca30288..d260a904bcfa535d8ed6b5ac47d82ff6b2994de7 100644 (file)
@@ -1,6 +1,6 @@
 {
   "$schema": "https://json-schema.org/draft/2020-12/schema",
-  "$id": "https://example.com/quest/1.0",
+  "$id": "/quest/1.0",
   "title": "Questionable V1",
   "description": "A series of quest sequences",
   "type": "object",
                     "Z"
                   ]
                 },
+                "StopDistance": {
+                  "type": "number",
+                  "exclusiveMinimum": 0
+                },
                 "TerritoryId": {
                   "type": "integer",
                   "exclusiveMinimum": 0
                 "Disabled": {
                   "type": "boolean"
                 },
+                "AetheryteShortcut": {
+                  "type": "string",
+                  "$comment": "TODO add remaining aetherytes for 2.x/3.x",
+                  "enum": [
+                    "Limsa Lominsa",
+                    "Gridania",
+                    "Ul'dah",
+                    "Ishgard",
+
+                    "Rhalgr's Reach",
+                    "Fringes - Castrum Oriens",
+                    "Fringes - Peering Stones",
+                    "Peaks - Ala Gannha",
+                    "Peaks - Ala Ghiri",
+                    "Lochs - Porta Praetoria",
+                    "Lochs - Ala Mhigan Quarter",
+                    "Kugane",
+                    "Ruby Sea - Tamamizu",
+                    "Ruby Sea - Onokoro",
+                    "Yanxia - Namai",
+                    "Yanxia - House of the Fierce",
+                    "Azim Steppe - Reunion",
+                    "Azim Steppe - Dawn Throne",
+                    "Azim Steppe - Dhoro Iloh",
+                    "Doman Enclave",
+                    "Doman Enclave - Northern Enclave",
+                    "Doman Enclave - Southern Enclave",
+
+                    "Crystarium",
+                    "Eulmore",
+                    "Lakeland - Fort Jobb",
+                    "Lakeland - Ostall Imperative",
+                    "Kholusia - Stilltide",
+                    "Kholusia - Wright",
+                    "Kholusia - Tomra",
+                    "Amh Araeng - Mord Souq",
+                    "Amh Araeng - Inn at Journey's Head",
+                    "Amh Araeng - Twine",
+                    "Rak'tika - Slitherbough",
+                    "Rak'tika - Fanow",
+                    "Il Mheg - Lydha Lran",
+                    "Il Mheg - Pia Enni",
+                    "Il Mheg - Wolekdorf",
+                    "Tempest - Ondo Cups",
+                    "Tempest - Macarenses Angle",
+
+                    "Old Sharlayan",
+                    "Radz-at-Han",
+                    "Labyrinthos - Archeion",
+                    "Labyrinthos - Sharlayan Hamlet",
+                    "Labyrinthos - Aporia",
+                    "Thavnair - Yedlihmad",
+                    "Thavnair - Great Work",
+                    "Thavnair - Palaka's Stand",
+                    "Garlemald - Camp Broken Glass",
+                    "Garlemald - Tertium",
+                    "Mare Lamentorum - Sinus Lacrimarum",
+                    "Mare Lamentorum - Bestways Burrow",
+                    "Elpis - Anagnorisis",
+                    "Elpis - Twelve Wonders",
+                    "Elpis - Poieten Oikos",
+                    "Ultima Thule - Reah Tahra",
+                    "Ultima Thula - Abode of the Ea",
+                    "Ultima Thule - Base Omicron"
+                  ]
+                },
                 "AethernetShortcut": {
                   "type": "array",
                   "minItems": 2,
                       "[Kugane] Sekiseigumi Barracks",
                       "[Kugane] Rakuza District",
                       "[Kugane] Airship Landing",
-                      "[The Crystarium] Aetheryte Plaza",
-                      "[The Crystarium] Musica Universalis Markets",
-                      "[The Crystarium] Themenos Rookery",
-                      "[The Crystarium] The Dossal Gate",
-                      "[The Crystarium] The Pendants",
-                      "[The Crystarium] The Amaro Launch",
-                      "[The Crystarium] The Crystalline Mean",
-                      "[The Crystarium] The Cabinet of Curiosity",
+                      "[Crystarium] Aetheryte Plaza",
+                      "[Crystarium] Musica Universalis Markets",
+                      "[Crystarium] Themenos Rookery",
+                      "[Crystarium] The Dossal Gate",
+                      "[Crystarium] The Pendants",
+                      "[Crystarium] The Amaro Launch",
+                      "[Crystarium] The Crystalline Mean",
+                      "[Crystarium] The Cabinet of Curiosity",
                       "[Eulmore] Aetheryte Plaza",
                       "[Eulmore] Southeast Derelicts",
                       "[Eulmore] Nightsoil Pots",
                     ]
                   }
                 },
+                "EnemySpawnType": {
+                  "type": "string",
+                  "enum": [
+                    "AutoOnEnterArea",
+                    "AfterInteraction"
+                  ]
+                },
                 "KillEnemyDataIds": {
                   "type": "array",
                   "items": {
index 0351d8c72d31871ecb183cfa4e89352744de700b..9a0531395ba2ff9f0485b8f54ac1f0cd10f8b39f 100644 (file)
@@ -1,4 +1,5 @@
-using System.Numerics;
+using System;
+using System.Numerics;
 using Dalamud.Game;
 using Dalamud.Game.ClientState.Objects;
 using Dalamud.Interface.Windowing;
@@ -20,20 +21,32 @@ public sealed class Questionable : IDalamudPlugin
     private readonly IFramework _framework;
     private readonly IGameGui _gameGui;
     private readonly GameFunctions _gameFunctions;
+    private readonly QuestController _questController;
+
     private readonly MovementController _movementController;
 
     public Questionable(DalamudPluginInterface pluginInterface, IClientState clientState, ITargetManager targetManager,
         IFramework framework, IGameGui gameGui, IDataManager dataManager, ISigScanner sigScanner,
-        IPluginLog pluginLog)
+        IObjectTable objectTable, IPluginLog pluginLog, ICondition condition, IChatGui chatGui, ICommandManager commandManager)
     {
+        ArgumentNullException.ThrowIfNull(pluginInterface);
+        ArgumentNullException.ThrowIfNull(sigScanner);
+        ArgumentNullException.ThrowIfNull(dataManager);
+        ArgumentNullException.ThrowIfNull(objectTable);
+
         _pluginInterface = pluginInterface;
         _clientState = clientState;
         _framework = framework;
         _gameGui = gameGui;
-        _gameFunctions = new GameFunctions(dataManager, sigScanner);
+        _gameFunctions = new GameFunctions(dataManager, objectTable, sigScanner, targetManager, pluginLog);
+
+        NavmeshIpc navmeshIpc = new NavmeshIpc(pluginInterface);
         _movementController =
-            new MovementController(new NavmeshIpc(pluginInterface), clientState, _gameFunctions, pluginLog);
-        _windowSystem.AddWindow(new DebugWindow(_movementController, _gameFunctions, clientState, targetManager));
+            new MovementController(navmeshIpc, clientState, _gameFunctions, pluginLog);
+        _questController = new QuestController(pluginInterface, dataManager, _clientState, _gameFunctions,
+            _movementController, pluginLog, condition, chatGui, commandManager);
+        _windowSystem.AddWindow(new DebugWindow(_movementController, _questController, _gameFunctions, clientState,
+            targetManager));
 
         _pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
         _framework.Update += FrameworkUpdate;
@@ -41,8 +54,9 @@ public sealed class Questionable : IDalamudPlugin
 
     private void FrameworkUpdate(IFramework framework)
     {
-        HandleNavigationShortcut();
+        _questController.Update();
 
+        HandleNavigationShortcut();
         _movementController.Update();
     }
 
index ab557a8b69081a8a516688399c428f360c584337..7e789f2a729aef992e36d530365bae4910f65001 100644 (file)
@@ -1,27 +1,33 @@
 using System.Globalization;
 using System.Numerics;
 using Dalamud.Game.ClientState.Objects;
+using Dalamud.Interface;
+using Dalamud.Interface.Components;
 using Dalamud.Interface.Windowing;
 using Dalamud.Plugin.Services;
 using FFXIVClientStructs.FFXIV.Client.Game.Control;
 using FFXIVClientStructs.FFXIV.Client.UI.Agent;
 using ImGuiNET;
 using Questionable.Controller;
+using Questionable.Model.V1;
 
 namespace Questionable.Windows;
 
 internal sealed class DebugWindow : Window
 {
     private readonly MovementController _movementController;
+    private readonly QuestController _questController;
     private readonly GameFunctions _gameFunctions;
     private readonly IClientState _clientState;
     private readonly ITargetManager _targetManager;
 
-    public DebugWindow(MovementController movementController, GameFunctions gameFunctions, IClientState clientState,
+    public DebugWindow(MovementController movementController, QuestController questController,
+        GameFunctions gameFunctions, IClientState clientState,
         ITargetManager targetManager)
         : base("Questionable", ImGuiWindowFlags.AlwaysAutoResize)
     {
         _movementController = movementController;
+        _questController = questController;
         _gameFunctions = gameFunctions;
         _clientState = clientState;
         _targetManager = targetManager;
@@ -39,6 +45,33 @@ internal sealed class DebugWindow : Window
         if (!_clientState.IsLoggedIn || _clientState.LocalPlayer == null)
             return;
 
+        var currentQuest = _questController.CurrentQuest;
+        if (currentQuest != null)
+        {
+            ImGui.TextUnformatted($"Quest: {currentQuest.Quest.Name} / {currentQuest.Sequence} / {currentQuest.Step}");
+            ImGui.TextUnformatted(_questController.DebugState ?? "--");
+
+            ImGui.BeginDisabled(_questController.GetNextStep().Step == null);
+            ImGui.Text($"{_questController.GetNextStep().Step?.Position}");
+            if (ImGuiComponents.IconButton(FontAwesomeIcon.Play))
+            {
+                _questController.ExecuteNextStep();
+            }
+
+            ImGui.SameLine();
+
+            if (ImGuiComponents.IconButton(FontAwesomeIcon.StepForward))
+            {
+                _questController.IncreaseStepCount();
+            }
+
+            ImGui.EndDisabled();
+        }
+        else
+            ImGui.Text("No active quest");
+
+        ImGui.Separator();
+
         ImGui.Text(
             $"Current TerritoryId: {_clientState.TerritoryType}, Flying: {(_gameFunctions.IsFlyingUnlocked(_clientState.TerritoryType) ? "Yes" : "No")}");
 
@@ -48,7 +81,8 @@ internal sealed class DebugWindow : Window
         if (_targetManager.Target != null)
         {
             ImGui.Separator();
-            ImGui.Text($"Target: {_targetManager.Target.Name}");
+            ImGui.Text(string.Create(CultureInfo.InvariantCulture,
+                $"Target: {_targetManager.Target.Name} ({(_targetManager.Target.Position - _clientState.LocalPlayer.Position).Length():F2})"));
 
             ImGui.BeginDisabled(!_movementController.IsNavmeshReady);
             if (!_movementController.IsPathfinding)
@@ -76,7 +110,8 @@ internal sealed class DebugWindow : Window
 
             ImGui.SameLine();
 
-            if (ImGui.Button($"Copy"))
+            ImGui.Button("Copy");
+            if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
             {
                 ImGui.SetClipboardText($$"""
                                          "DataId": {{_targetManager.Target.DataId}},
@@ -89,6 +124,12 @@ internal sealed class DebugWindow : Window
                                          "InteractionType": "Interact"
                                          """);
             }
+            else if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
+            {
+                EAetheryteLocation location = (EAetheryteLocation)_targetManager.Target.DataId;
+                ImGui.SetClipboardText(string.Create(CultureInfo.InvariantCulture,
+                    $"{{EAetheryteLocation.{location}, new({_targetManager.Target.Position.X}f, {_targetManager.Target.Position.Y}f, {_targetManager.Target.Position.Z}f)}},"));
+            }
         }
         else
         {