diff --git a/Questionable/Controller/MovementController.cs b/Questionable/Controller/MovementController.cs index c8183dc6..334d6ace 100644 --- a/Questionable/Controller/MovementController.cs +++ b/Questionable/Controller/MovementController.cs @@ -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 = diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs index 095163a4..d3836af0 100644 --- a/Questionable/Controller/QuestController.cs +++ b/Questionable/Controller/QuestController.cs @@ -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 _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()!.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(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(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 index 00000000..8bc261e5 --- /dev/null +++ b/Questionable/Data/AetheryteData.cs @@ -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 Locations { get; } = + new Dictionary + { + { 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 AethernetNames { get; } + public ReadOnlyDictionary TerritoryIds { get; } + + public AetheryteData(IDataManager dataManager) + { + Dictionary aethernetNames = new(); + Dictionary territoryIds = new(); + foreach (var aetheryte in dataManager.GetExcelSheet()!.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 index 00000000..b37c8c4e --- /dev/null +++ b/Questionable/Data/TerritoryData.cs @@ -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 _territoriesWithMount; + + public TerritoryData(IDataManager dataManager) + { + _territoriesWithMount = dataManager.GetExcelSheet()! + .Where(x => x.RowId > 0 && x.Mount) + .Select(x => x.RowId) + .ToImmutableHashSet(); + } + + public bool CanUseMount(ushort territoryId) => _territoriesWithMount.Contains(territoryId); +} diff --git a/Questionable/External/NavmeshIpc.cs b/Questionable/External/NavmeshIpc.cs index d85f29e0..84bea336 100644 --- a/Questionable/External/NavmeshIpc.cs +++ b/Questionable/External/NavmeshIpc.cs @@ -53,6 +53,7 @@ internal sealed class NavmeshIpc public Task> Pathfind(Vector3 localPlayerPosition, Vector3 targetPosition, bool fly, CancellationToken cancellationToken) { + _pathSetTolerance.InvokeAction(0.25f); return _navPathfind.InvokeFunc(localPlayerPosition, targetPosition, fly, cancellationToken); } diff --git a/Questionable/GameFunctions.cs b/Questionable/GameFunctions.cs index d8f7764b..652ff60a 100644 --- a/Questionable/GameFunctions.cs +++ b/Questionable/GameFunctions.cs @@ -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 _sanitiseString; private readonly ReadOnlyDictionary _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(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; + } + } + } } diff --git a/Questionable/Model/Quest.cs b/Questionable/Model/Quest.cs index 015ddda4..511e5ef1 100644 --- a/Questionable/Model/Quest.cs +++ b/Questionable/Model/Quest.cs @@ -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); } diff --git a/Questionable/Model/V1/Converter/AethernetShortcutConverter.cs b/Questionable/Model/V1/Converter/AethernetShortcutConverter.cs index 73d77b60..0fb5affe 100644 --- a/Questionable/Model/V1/Converter/AethernetShortcutConverter.cs +++ b/Questionable/Model/V1/Converter/AethernetShortcutConverter.cs @@ -61,14 +61,14 @@ public sealed class AethernetShortcutConverter : JsonConverter 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 index 00000000..a3fa26e8 --- /dev/null +++ b/Questionable/Model/V1/Converter/AetheryteConverter.cs @@ -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 +{ + private static readonly Dictionary 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 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]); + } +} diff --git a/Questionable/Model/V1/Converter/InteractionTypeConverter.cs b/Questionable/Model/V1/Converter/InteractionTypeConverter.cs index b85313b5..bf4db121 100644 --- a/Questionable/Model/V1/Converter/InteractionTypeConverter.cs +++ b/Questionable/Model/V1/Converter/InteractionTypeConverter.cs @@ -12,7 +12,7 @@ public sealed class InteractionTypeConverter : JsonConverter { { 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 index 00000000..e7731e02 --- /dev/null +++ b/Questionable/Model/V1/Converter/VectorConverter.cs @@ -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 +{ + 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(); + } +} diff --git a/Questionable/Model/V1/EAetheryteLocation.cs b/Questionable/Model/V1/EAetheryteLocation.cs index 514c52ea..bb62eb1a 100644 --- a/Questionable/Model/V1/EAetheryteLocation.cs +++ b/Questionable/Model/V1/EAetheryteLocation.cs @@ -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 } diff --git a/Questionable/Model/V1/EInteractionType.cs b/Questionable/Model/V1/EInteractionType.cs index bf29d34f..2684f6d5 100644 --- a/Questionable/Model/V1/EInteractionType.cs +++ b/Questionable/Model/V1/EInteractionType.cs @@ -4,7 +4,7 @@ public enum EInteractionType { Interact, WalkTo, - AttuneAethenetShard, + AttuneAethernetShard, AttuneAetheryte, AttuneAetherCurrent, Combat, diff --git a/Questionable/Model/V1/QuestData.cs b/Questionable/Model/V1/QuestData.cs index 79bfa152..0787b0f8 100644 --- a/Questionable/Model/V1/QuestData.cs +++ b/Questionable/Model/V1/QuestData.cs @@ -7,7 +7,7 @@ public class QuestData public required int Version { get; set; } public required string Author { get; set; } public List Contributors { get; set; } = new(); - public string Comment { get; set; } + public string? Comment { get; set; } public List TerritoryBlacklist { get; set; } = new(); public required List QuestSequence { get; set; } = new(); } diff --git a/Questionable/Model/V1/QuestSequence.cs b/Questionable/Model/V1/QuestSequence.cs index f39d0e9a..cd691b13 100644 --- a/Questionable/Model/V1/QuestSequence.cs +++ b/Questionable/Model/V1/QuestSequence.cs @@ -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 Steps { get; set; } = new(); } diff --git a/Questionable/Model/V1/QuestStep.cs b/Questionable/Model/V1/QuestStep.cs index 3c533322..e573c6a3 100644 --- a/Questionable/Model/V1/QuestStep.cs +++ b/Questionable/Model/V1/QuestStep.cs @@ -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; } } diff --git a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4357_The Next Ship to Sail.json b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4357_The Next Ship to Sail.json index 7083ecec..04501ad3 100644 --- a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4357_The Next Ship to Sail.json +++ b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4357_The Next Ship to Sail.json @@ -15,6 +15,7 @@ }, "TerritoryId": 129, "InteractionType": "Interact", + "AetheryteShortcut": "Limsa Lominsa", "AethernetShortcut": [ "[Limsa Lominsa] Aetheryte Plaza", "[Limsa Lominsa] Arcanist's Guild" diff --git a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4358_Old Sharlayan New to You.json b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4358_Old Sharlayan New to You.json index e3fd4aa0..118e6d8e 100644 --- a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4358_Old Sharlayan New to You.json +++ b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4358_Old Sharlayan New to You.json @@ -63,10 +63,11 @@ { "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" } @@ -78,10 +79,11 @@ { "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" } @@ -103,10 +105,11 @@ { "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" } @@ -115,6 +118,15 @@ { "Sequence": 6, "Steps": [ + { + "Position": { + "X": 93.30914, + "Y": 8.920153, + "Z": -89.12467 + }, + "TerritoryId": 962, + "InteractionType": "WalkTo" + }, { "DataId": 1038578, "Position": { @@ -122,6 +134,7 @@ "Y": 41.37599, "Z": -142.5033 }, + "StopDistance": 0.25, "TerritoryId": 962, "InteractionType": "Interact" } @@ -137,6 +150,7 @@ "Y": 41.367188, "Z": -156.6034 }, + "StopDistance": 0.25, "TerritoryId": 962, "InteractionType": "AttuneAethenetShard" }, @@ -182,6 +196,7 @@ "Y": 1.0594833, "Z": 30.052902 }, + "StopDistance": 0.25, "TerritoryId": 962, "InteractionType": "Interact", "AethernetShortcut": [ diff --git a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4359_Hitting the Books.json b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4359_Hitting the Books.json index c4e31f40..94715073 100644 --- a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4359_Hitting the Books.json +++ b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4359_Hitting the Books.json @@ -156,6 +156,21 @@ "InteractionType": "Interact" } ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1038679, + "Position": { + "X": -275.5932, + "Y": 19.003881, + "Z": 13.321045 + }, + "TerritoryId": 962, + "InteractionType": "Interact" + } + ] } ] } diff --git a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4360_A Seat at the Last Stand.json b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4360_A Seat at the Last Stand.json index 784be244..b26408cf 100644 --- a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4360_A Seat at the Last Stand.json +++ b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4360_A Seat at the Last Stand.json @@ -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" @@ -141,9 +141,9 @@ "Steps": [ { "Position": { - "X": 15.242085, + "X": 19.79008, "Y": -16.247002, - "Z": 109.177666 + "Z": 108.36692 }, "TerritoryId": 962, "InteractionType": "WalkTo" diff --git a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4362_Glorified Ratcatcher.json b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4362_Glorified Ratcatcher.json index 9571e693..ed2fc020 100644 --- a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4362_Glorified Ratcatcher.json +++ b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4362_Glorified Ratcatcher.json @@ -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 diff --git a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4363_Deeper into the Maze.json b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4363_Deeper into the Maze.json index 3b25f9c4..5d7676b5 100644 --- a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4363_Deeper into the Maze.json +++ b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4363_Deeper into the Maze.json @@ -109,7 +109,7 @@ }, "TerritoryId": 956, "InteractionType": "ManualAction", - "Comment": "Duty - Shoot some bird" + "Comment": "Duty - Shoot Large Green Bird" } ] }, diff --git a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4364_The Medial Circuit.json b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4364_The Medial Circuit.json index 6cc3d329..50932ec4 100644 --- a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4364_The Medial Circuit.json +++ b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4364_The Medial Circuit.json @@ -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 ] @@ -97,21 +99,11 @@ "Sequence": 5, "Steps": [ { - "DataId": 2011984, + "DataId": 1037985, "Position": { - "X": 497.09314, - "Y": 73.41101, - "Z": -267.23126 - }, - "TerritoryId": 956, - "InteractionType": "AttuneAetherCurrent" - }, - { - "DataId": 1038708, - "Position": { - "X": 408.31604, - "Y": 65.3329, - "Z": -130.11371 + "X": 481.8036, + "Y": 66.16195, + "Z": -108.537415 }, "TerritoryId": 956, "InteractionType": "Interact" @@ -127,11 +119,11 @@ "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" @@ -141,6 +133,16 @@ { "Sequence": 6, "Steps": [ + { + "DataId": 2011984, + "Position": { + "X": 497.09314, + "Y": 73.41101, + "Z": -267.23126 + }, + "TerritoryId": 956, + "InteractionType": "AttuneAetherCurrent" + }, { "DataId": 2011843, "Position": { diff --git a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4365_The Full Reports Warts and All.json b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4365_The Full Reports Warts and All.json index c9cfce33..e525de45 100644 --- a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4365_The Full Reports Warts and All.json +++ b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4365_The Full Reports Warts and All.json @@ -56,6 +56,7 @@ "Y": 81.17488, "Z": -534.99316 }, + "StopDistance": 0.25, "TerritoryId": 956, "InteractionType": "WalkTo" } diff --git a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4366_A Guide of Sorts.json b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4366_A Guide of Sorts.json index 9485fef4..350b87f8 100644 --- a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4366_A Guide of Sorts.json +++ b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4366_A Guide of Sorts.json @@ -20,6 +20,24 @@ { "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": { @@ -30,6 +48,15 @@ "TerritoryId": 956, "InteractionType": "AttuneAetherCurrent" }, + { + "Position": { + "X": -327.6718, + "Y": 79.535736, + "Z": -400.00397 + }, + "TerritoryId": 956, + "InteractionType": "WalkTo" + }, { "DataId": 2011983, "Position": { diff --git a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4369_On Low Tide.json b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4369_On Low Tide.json index 5e68fd39..a828bbfa 100644 --- a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4369_On Low Tide.json +++ b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4369_On Low Tide.json @@ -27,28 +27,10 @@ "Y": 6.991216, "Z": 629.2394 }, + "StopDistance": 5, "TerritoryId": 957, "InteractionType": "AttuneAetheryte" }, - { - "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 - }, - "TerritoryId": 957, - "InteractionType": "Interact" - }, { "DataId": 2011948, "Position": { @@ -59,6 +41,17 @@ "TerritoryId": 957, "InteractionType": "Interact" }, + { + "DataId": 1037622, + "Position": { + "X": 189.94562, + "Y": 0.8560083, + "Z": 702.7896 + }, + "StopDistance": 0.25, + "TerritoryId": 957, + "InteractionType": "Interact" + }, { "DataId": 2011949, "Position": { diff --git a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4370_A Fishermans Friend.json b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4370_A Fishermans Friend.json index d9d40c26..85140939 100644 --- a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4370_A Fishermans Friend.json +++ b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4370_A Fishermans Friend.json @@ -109,6 +109,7 @@ "Y": 1.769943, "Z": 738.9843 }, + "StopDistance": 0.25, "TerritoryId": 957, "InteractionType": "Interact" } diff --git a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4371_House of Divinities.json b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4371_House of Divinities.json index f7ffcbc2..f0ec8bbd 100644 --- a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4371_House of Divinities.json +++ b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4371_House of Divinities.json @@ -59,6 +59,7 @@ }, "TerritoryId": 957, "InteractionType": "Combat", + "EnemySpawnType": "AutoOnEnterArea", "KillEnemyDataIds": [ 14006 ] diff --git a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4372_The Great Work.json b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4372_The Great Work.json index 03cc5b2b..cf5170b3 100644 --- a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4372_The Great Work.json +++ b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4372_The Great Work.json @@ -42,6 +42,7 @@ "Y": 4.785123, "Z": 36.76496 }, + "StopDistance": 5, "TerritoryId": 957, "InteractionType": "AttuneAetheryte" }, diff --git a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4373_Shadowed Footsteps.json b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4373_Shadowed Footsteps.json index e49a3073..f5af5151 100644 --- a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4373_Shadowed Footsteps.json +++ b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4373_Shadowed Footsteps.json @@ -29,6 +29,7 @@ }, "TerritoryId": 957, "InteractionType": "Combat", + "EnemySpawnType": "AfterInteraction", "KillEnemyDataIds": [ 14004 ] diff --git a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4374_A Boys Errand.json b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4374_A Boys Errand.json index 6b6603cc..89133562 100644 --- a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4374_A Boys Errand.json +++ b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4374_A Boys Errand.json @@ -121,6 +121,7 @@ "Y": 88.84356, "Z": -608.6588 }, + "StopDistance": 0.25, "TerritoryId": 957, "InteractionType": "WalkTo" } diff --git a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4375_Tipping the Scale.json b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4375_Tipping the Scale.json index 1047bad7..aa5f5bb5 100644 --- a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4375_Tipping the Scale.json +++ b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4375_Tipping the Scale.json @@ -20,6 +20,24 @@ { "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": { diff --git a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4376_The Satrap of Radz at Han.json b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4376_The Satrap of Radz at Han.json index ce1e3f9a..eebb5a89 100644 --- a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4376_The Satrap of Radz at Han.json +++ b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4376_The Satrap of Radz at Han.json @@ -69,6 +69,7 @@ }, "TerritoryId": 962, "InteractionType": "Interact", + "AetheryteShortcut": "Old Sharlayan", "AethernetShortcut": [ "[Old Sharlayan] Aetheryte Plaza", "[Old Sharlayan] The Baldesion Annex" diff --git a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4377_In the Dark of the Tower.json b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4377_In the Dark of the Tower.json index fe2f81f2..788e1b7f 100644 --- a/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4377_In the Dark of the Tower.json +++ b/Questionable/QuestPaths/Endwalker-A-Thavnair1-Labyrinthos1/4377_In the Dark of the Tower.json @@ -28,6 +28,7 @@ "Z": -569.8787 }, "TerritoryId": 957, + "AetheryteShortcut": "Thavnair - Great Work", "InteractionType": "Interact" } ] diff --git a/Questionable/QuestPaths/Endwalker-B-Garlemald/4383_A Frosty Reception.json b/Questionable/QuestPaths/Endwalker-B-Garlemald/4383_A Frosty Reception.json index 407e9ed4..e36f497a 100644 --- a/Questionable/QuestPaths/Endwalker-B-Garlemald/4383_A Frosty Reception.json +++ b/Questionable/QuestPaths/Endwalker-B-Garlemald/4383_A Frosty Reception.json @@ -31,7 +31,8 @@ "Z": 539.6046 }, "TerritoryId": 621, - "InteractionType": "Interact" + "InteractionType": "ManualAction", + "Comment": "Duty - A Frosty Reception" } ] }, diff --git a/Questionable/QuestPaths/Endwalker-E-Elpis/4419_Return to the Crystarium.json b/Questionable/QuestPaths/Endwalker-E-Elpis/4419_Return to the Crystarium.json index c7de2233..94de9928 100644 --- a/Questionable/QuestPaths/Endwalker-E-Elpis/4419_Return to the Crystarium.json +++ b/Questionable/QuestPaths/Endwalker-E-Elpis/4419_Return to the Crystarium.json @@ -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" ] } ] diff --git a/Questionable/QuestPaths/Endwalker-E-Elpis/4420_Hope Upon a Flower.json b/Questionable/QuestPaths/Endwalker-E-Elpis/4420_Hope Upon a Flower.json index 343af2a8..2699e71f 100644 --- a/Questionable/QuestPaths/Endwalker-E-Elpis/4420_Hope Upon a Flower.json +++ b/Questionable/QuestPaths/Endwalker-E-Elpis/4420_Hope Upon a Flower.json @@ -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 similarity index 100% rename from Questionable/QuestPaths/Endwalker-G-UltimaThule/4456_.Roads Paved of Sacrifice.json rename to Questionable/QuestPaths/Endwalker-G-UltimaThule/4456_Roads Paved of Sacrifice.json diff --git a/Questionable/QuestPaths/Endwalker-L-6.5/4744_Seeking the Light.json b/Questionable/QuestPaths/Endwalker-L-6.5/4744_Seeking the Light.json index 99b0da3f..1a2bc813 100644 --- a/Questionable/QuestPaths/Endwalker-L-6.5/4744_Seeking the Light.json +++ b/Questionable/QuestPaths/Endwalker-L-6.5/4744_Seeking the Light.json @@ -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" ] } ] diff --git a/Questionable/QuestPaths/Endwalker-L-6.5/4747_Back to Action.json b/Questionable/QuestPaths/Endwalker-L-6.5/4747_Back to Action.json index 8b557f61..a8cbb035 100644 --- a/Questionable/QuestPaths/Endwalker-L-6.5/4747_Back to Action.json +++ b/Questionable/QuestPaths/Endwalker-L-6.5/4747_Back to Action.json @@ -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" ] } ] diff --git a/Questionable/QuestPaths/Endwalker-L-6.5/4750_Growing Light.json b/Questionable/QuestPaths/Endwalker-L-6.5/4750_Growing Light.json index 68ecbdfa..8da8fc18 100644 --- a/Questionable/QuestPaths/Endwalker-L-6.5/4750_Growing Light.json +++ b/Questionable/QuestPaths/Endwalker-L-6.5/4750_Growing Light.json @@ -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" ] } ] diff --git a/Questionable/QuestSchema/schema_v1.json b/Questionable/QuestSchema/schema_v1.json index bf35fe5a..d260a904 100644 --- a/Questionable/QuestSchema/schema_v1.json +++ b/Questionable/QuestSchema/schema_v1.json @@ -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", @@ -68,6 +68,10 @@ "Z" ] }, + "StopDistance": { + "type": "number", + "exclusiveMinimum": 0 + }, "TerritoryId": { "type": "integer", "exclusiveMinimum": 0 @@ -89,6 +93,72 @@ "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, @@ -147,14 +217,14 @@ "[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", @@ -179,6 +249,13 @@ ] } }, + "EnemySpawnType": { + "type": "string", + "enum": [ + "AutoOnEnterArea", + "AfterInteraction" + ] + }, "KillEnemyDataIds": { "type": "array", "items": { diff --git a/Questionable/Questionable.cs b/Questionable/Questionable.cs index 0351d8c7..9a053139 100644 --- a/Questionable/Questionable.cs +++ b/Questionable/Questionable.cs @@ -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(); } diff --git a/Questionable/Windows/DebugWindow.cs b/Questionable/Windows/DebugWindow.cs index ab557a8b..7e789f2a 100644 --- a/Questionable/Windows/DebugWindow.cs +++ b/Questionable/Windows/DebugWindow.cs @@ -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 {