From a0e675cbdcdca1f048c1acbc379e8dcfca5e07d6 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Mon, 15 Jul 2024 03:05:37 +0200 Subject: [PATCH 01/11] Add basic quest validation --- .../5039_Traveler to the Rescue.json | 10 -- .../Urqopacha/5038_In Pursuit of Mezcal.json | 2 +- .../5007_Sights of the West and Beyond.json | 2 +- .../Instant/5008_Dawn of a New Deal.json | 2 +- QuestPaths/AssemblyQuestLoader.cs | 9 +- Questionable/Controller/QuestRegistry.cs | 158 +++++++++++++++--- 6 files changed, 147 insertions(+), 36 deletions(-) diff --git a/QuestPaths/7.x - Dawntrail/Aether Currents/Urqopacha/5039_Traveler to the Rescue.json b/QuestPaths/7.x - Dawntrail/Aether Currents/Urqopacha/5039_Traveler to the Rescue.json index c9da7b59..9dd6fa0c 100644 --- a/QuestPaths/7.x - Dawntrail/Aether Currents/Urqopacha/5039_Traveler to the Rescue.json +++ b/QuestPaths/7.x - Dawntrail/Aether Currents/Urqopacha/5039_Traveler to the Rescue.json @@ -65,16 +65,6 @@ "TerritoryId": 1187, "InteractionType": "CompleteQuest", "AetheryteShortcut": "Urqopacha - Wachunpelo" - }, - { - "DataId": 1050684, - "Position": { - "X": 391.37854, - "Y": -156.07434, - "Z": -388.50995 - }, - "TerritoryId": 1187, - "InteractionType": "CompleteQuest" } ] } diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Urqopacha/5038_In Pursuit of Mezcal.json b/QuestPaths/7.x - Dawntrail/Side Quests/Urqopacha/5038_In Pursuit of Mezcal.json index e463241d..44f4d8a7 100644 --- a/QuestPaths/7.x - Dawntrail/Side Quests/Urqopacha/5038_In Pursuit of Mezcal.json +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Urqopacha/5038_In Pursuit of Mezcal.json @@ -50,7 +50,7 @@ }, "StopDistance": 0.5, "TerritoryId": 1187, - "InteractionType": "AcceptQuest", + "InteractionType": "CompleteQuest", "AetheryteShortcut": "Urqopacha - Wachunpelo", "Fly": true } diff --git a/QuestPaths/7.x - Dawntrail/Unlocks/Instant/5007_Sights of the West and Beyond.json b/QuestPaths/7.x - Dawntrail/Unlocks/Instant/5007_Sights of the West and Beyond.json index cb2725b3..fd2ab340 100644 --- a/QuestPaths/7.x - Dawntrail/Unlocks/Instant/5007_Sights of the West and Beyond.json +++ b/QuestPaths/7.x - Dawntrail/Unlocks/Instant/5007_Sights of the West and Beyond.json @@ -13,7 +13,7 @@ "Z": -52.99463 }, "TerritoryId": 1186, - "InteractionType": "Interact", + "InteractionType": "AcceptQuest", "Comment": "Quest is completed instantly" } ] diff --git a/QuestPaths/7.x - Dawntrail/Unlocks/Instant/5008_Dawn of a New Deal.json b/QuestPaths/7.x - Dawntrail/Unlocks/Instant/5008_Dawn of a New Deal.json index 720a825f..4cc2f982 100644 --- a/QuestPaths/7.x - Dawntrail/Unlocks/Instant/5008_Dawn of a New Deal.json +++ b/QuestPaths/7.x - Dawntrail/Unlocks/Instant/5008_Dawn of a New Deal.json @@ -13,7 +13,7 @@ "Z": -38.132385 }, "TerritoryId": 1186, - "InteractionType": "Interact", + "InteractionType": "AcceptQuest", "Comment": "Quest is completed instantly" } ] diff --git a/QuestPaths/AssemblyQuestLoader.cs b/QuestPaths/AssemblyQuestLoader.cs index 4abfb4c7..b0b8efc0 100644 --- a/QuestPaths/AssemblyQuestLoader.cs +++ b/QuestPaths/AssemblyQuestLoader.cs @@ -1,11 +1,14 @@ using System.Collections.Generic; using Questionable.Model.V1; -#if RELEASE namespace Questionable.QuestPaths; public static partial class AssemblyQuestLoader { - public static IReadOnlyDictionary GetQuests() => Quests; -} + public static IReadOnlyDictionary GetQuests() => +#if RELEASE + Quests; +#else + new Dictionary(); #endif +} diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs index b7450c53..f8a41253 100644 --- a/Questionable/Controller/QuestRegistry.cs +++ b/Questionable/Controller/QuestRegistry.cs @@ -1,13 +1,17 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.IO; +using System.Numerics; using System.Text.Json; +using System.Threading.Tasks; using Dalamud.Plugin; using Dalamud.Plugin.Services; using Microsoft.Extensions.Logging; +using Questionable.Controller.Utils; using Questionable.Data; using Questionable.Model; using Questionable.Model.V1; @@ -18,15 +22,17 @@ internal sealed class QuestRegistry { private readonly IDalamudPluginInterface _pluginInterface; private readonly QuestData _questData; + private readonly IChatGui _chatGui; private readonly ILogger _logger; private readonly Dictionary _quests = new(); - public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData, + public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData, IChatGui chatGui, ILogger logger) { _pluginInterface = pluginInterface; _questData = questData; + _chatGui = chatGui; _logger = logger; } @@ -36,7 +42,26 @@ internal sealed class QuestRegistry { _quests.Clear(); -#if RELEASE + LoadQuestsFromAssembly(); + LoadQuestsFromProjectDirectory(); + + try + { + LoadFromDirectory(new DirectoryInfo(Path.Combine(_pluginInterface.ConfigDirectory.FullName, "Quests"))); + } + catch (Exception e) + { + _logger.LogError(e, + "Failed to load all quests from user directory (some may have been successfully loaded)"); + } + + ValidateQuests(); + _logger.LogInformation("Loaded {Count} quests", _quests.Count); + } + + [Conditional("RELEASE")] + private void LoadQuestsFromAssembly() + { _logger.LogInformation("Loading quests from assembly"); foreach ((ushort questId, QuestRoot questRoot) in QuestPaths.AssemblyQuestLoader.GetQuests()) @@ -49,7 +74,11 @@ internal sealed class QuestRegistry }; _quests[questId] = quest; } -#else + } + + [Conditional("DEBUG")] + private void LoadQuestsFromProjectDirectory() + { DirectoryInfo? solutionDirectory = _pluginInterface.AssemblyLocation.Directory?.Parent?.Parent; if (solutionDirectory != null) { @@ -75,27 +104,116 @@ internal sealed class QuestRegistry } } } -#endif + } - try + [Conditional("DEBUG")] + private void ValidateQuests() + { + Task.Run(() => { - LoadFromDirectory(new DirectoryInfo(Path.Combine(_pluginInterface.ConfigDirectory.FullName, "Quests"))); - } - catch (Exception e) - { - _logger.LogError(e, "Failed to load all quests from user directory (some may have been successfully loaded)"); - } + try + { + int foundProblems = 0; + foreach (var quest in _quests.Values) + { + int missingSteps = quest.Root.QuestSequence.Where(x => x.Sequence < 255).Max(x => x.Sequence) - + quest.Root.QuestSequence.Count(x => x.Sequence < 255) + 1; + if (missingSteps != 0) + { + _logger.LogWarning("Quest has missing steps: {QuestId} / {QuestName} → {Count}", quest.QuestId, + quest.Info.Name, missingSteps); + ++foundProblems; + } -#if !RELEASE - foreach (var quest in _quests.Values) - { - int missingSteps = quest.Root.QuestSequence.Where(x => x.Sequence < 255).Max(x => x.Sequence) - quest.Root.QuestSequence.Count(x => x.Sequence < 255) + 1; - if (missingSteps != 0) - _logger.LogWarning("Quest has missing steps: {QuestId} / {QuestName} → {Count}", quest.QuestId, quest.Info.Name, missingSteps); - } -#endif + var totalSequenceCount = quest.Root.QuestSequence.Count; + var distinctSequenceCount = quest.Root.QuestSequence.Select(x => x.Sequence).Distinct().Count(); + if (totalSequenceCount != distinctSequenceCount) + { + _logger.LogWarning("Quest has duplicate sequence numbers: {QuestId} / {QuestName}", quest.QuestId, + quest.Info.Name); + ++foundProblems; + } - _logger.LogInformation("Loaded {Count} quests", _quests.Count); + foreach (var sequence in quest.Root.QuestSequence) + { + if (sequence.Sequence == 0 && + sequence.Steps.LastOrDefault()?.InteractionType != EInteractionType.AcceptQuest) + { + _logger.LogWarning( + "Quest likely has AcceptQuest configured wrong: {QuestId} / {QuestName} → {Sequence} / {Step}", + quest.QuestId, quest.Info.Name, sequence.Sequence, sequence.Steps.Count - 1); + ++foundProblems; + } + else if (sequence.Sequence == 255 && + sequence.Steps.LastOrDefault()?.InteractionType != EInteractionType.CompleteQuest) + { + _logger.LogWarning( + "Quest likely has CompleteQuest configured wrong: {QuestId} / {QuestName} → {Sequence} / {Step}", + quest.QuestId, quest.Info.Name, sequence.Sequence, sequence.Steps.Count - 1); + ++foundProblems; + } + + + var acceptQuestSteps = sequence.Steps + .Where(x => x is { InteractionType: EInteractionType.AcceptQuest, PickupQuestId: null }) + .Where(x => sequence.Sequence != 0 || x != sequence.Steps.Last()); + foreach (var step in acceptQuestSteps) + { + _logger.LogWarning( + "Quest has unexpected AcceptQuest steps: {QuestId} / {QuestName} → {Sequence} / {Step}", + quest.QuestId, quest.Info.Name, sequence.Sequence, sequence.Steps.IndexOf(step)); + ++foundProblems; + } + + var completeQuestSteps = sequence.Steps + .Where(x => x is { InteractionType: EInteractionType.CompleteQuest, TurnInQuestId: null }) + .Where(x => sequence.Sequence != 255 || x != sequence.Steps.Last()); + foreach (var step in completeQuestSteps) + { + _logger.LogWarning( + "Quest has unexpected CompleteQuest steps: {QuestId} / {QuestName} → {Sequence} / {Step}", + quest.QuestId, quest.Info.Name, sequence.Sequence, sequence.Steps.IndexOf(step)); + ++foundProblems; + } + + var completionFlags = sequence.Steps.Select(x => x.CompletionQuestVariablesFlags) + .Where(QuestWorkUtils.HasCompletionFlags) + .GroupBy(x => + { + return Enumerable.Range(0, 6).Select(y => + { + short? value = x[y]; + if (value == null || value.Value < 0) + return (long)0; + return (long)BitOperations.RotateLeft((ulong)value.Value, 8 * y); + }) + .Sum(); + }) + .Where(x => x.Key != 0) + .Where(x => x.Count() > 1); + foreach (var duplicate in completionFlags) + { + _logger.LogWarning( + "Quest step has duplicate completion flags: {QuestId} / {QuestName} → {Sequence} → {Flags}", + quest.QuestId, quest.Info.Name, sequence.Sequence, string.Join(", ", duplicate.First())); + ++foundProblems; + } + } + } + + if (foundProblems > 0) + { + _chatGui.Print( + $"[Questionable] Quest validation has found {foundProblems} problems. Check the log for details."); + } + } + catch (Exception e) + { + _logger.LogError(e, "Unable to validate quests"); + _chatGui.PrintError( + $"[Questionable] Unable to validate quests. Check the log for details."); + } + }); } From 0eb77927b36c03f0d1fae50526f875d8033f4095 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Mon, 15 Jul 2024 20:20:46 +0200 Subject: [PATCH 02/11] Show quest dependencies in quest selection window, add /qst zone as shortcut to show all (mapped) quests starting in the current zone --- QuestPathGenerator/RoslynShortcuts.cs | 2 + .../Yak T'el/5090_Preying Mantises.json | 56 +++++ .../5091_Getting to the Bottom of Things.json | 102 ++++++++ .../Yak T'el/5092_The Crimson Cenote.json | 78 +++++++ .../5093_Watching the Watchtowers.json | 67 ++++++ .../Yak T'el/5095_Hunters with Bite.json | 75 ++++++ .../Yak T'el/5096_Reminders of the Past.json | 99 ++++++++ .../Side Quests/Yak T'el/5097_Sick Day.json | 103 ++++++++ .../Yak T'el/5098_To Forge in the Forest.json | 185 +++++++++++++++ .../Yak T'el/5099_A Stomach for Change.json | 52 +++++ .../Yak T'el/5100_Carved in Stone.json | 53 +++++ .../Yak T'el/5101_Two Hearts Aflutter.json | 152 ++++++++++++ .../Yak T'el/5102_Proper Respects.json | 116 ++++++++++ .../Yak T'el/5104_Spirit's Call.json | 163 +++++++++++++ .../Yak T'el/5105_Seek the Sun.json | 92 ++++++++ .../Yak T'el/5106_Secret of Sweetness.json | 90 +++++++ .../Yak T'el/5107_Growing Grove.json | 107 +++++++++ .../Yak T'el/5108_Age Is Just a Number.json | 60 +++++ .../Yak T'el/5109_I'm with Wivre.json | 97 ++++++++ .../Yak T'el/5111_Hunting for the Family.json | 97 ++++++++ .../Yak T'el/5112_A Hunter's Dream.json | 77 ++++++ .../Yak T'el/5113_The Trial Commences.json | 90 +++++++ .../Yak T'el/5115_Remembering a Dream.json | 125 ++++++++++ .../Yak T'el/5116_Budding Promise.json | 80 +++++++ .../Yak T'el/5117_Skill of a Veteran.json | 86 +++++++ .../Side Quests/Yak T'el/5118_One Forest.json | 219 ++++++++++++++++++ QuestPaths/quest-v1.json | 4 + Questionable.Model/V1/ComplexCombatData.cs | 4 + Questionable/Controller/CommandHandler.cs | 18 +- Questionable/Controller/QuestRegistry.cs | 1 + .../Controller/Steps/Common/NextQuest.cs | 17 +- Questionable/GameFunctions.cs | 39 +++- Questionable/Model/QuestInfo.cs | 22 ++ Questionable/Windows/QuestSelectionWindow.cs | 178 ++++++++++++-- Questionable/Windows/QuestWindow.cs | 2 +- 35 files changed, 2775 insertions(+), 33 deletions(-) create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5090_Preying Mantises.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5091_Getting to the Bottom of Things.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5092_The Crimson Cenote.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5093_Watching the Watchtowers.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5095_Hunters with Bite.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5096_Reminders of the Past.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5097_Sick Day.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5098_To Forge in the Forest.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5099_A Stomach for Change.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5100_Carved in Stone.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5101_Two Hearts Aflutter.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5102_Proper Respects.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5104_Spirit's Call.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5105_Seek the Sun.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5106_Secret of Sweetness.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5107_Growing Grove.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5108_Age Is Just a Number.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5109_I'm with Wivre.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5111_Hunting for the Family.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5112_A Hunter's Dream.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5113_The Trial Commences.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5115_Remembering a Dream.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5116_Budding Promise.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5117_Skill of a Veteran.json create mode 100644 QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5118_One Forest.json diff --git a/QuestPathGenerator/RoslynShortcuts.cs b/QuestPathGenerator/RoslynShortcuts.cs index 72a15a39..186de3e0 100644 --- a/QuestPathGenerator/RoslynShortcuts.cs +++ b/QuestPathGenerator/RoslynShortcuts.cs @@ -173,6 +173,8 @@ public static class RoslynShortcuts SyntaxNodeList( Assignment(nameof(ComplexCombatData.DataId), complexCombatData.DataId, default(uint)) .AsSyntaxNodeOrToken(), + Assignment(nameof(ComplexCombatData.MinimumKillCount), complexCombatData.MinimumKillCount, null) + .AsSyntaxNodeOrToken(), Assignment(nameof(ComplexCombatData.RewardItemId), complexCombatData.RewardItemId, null) .AsSyntaxNodeOrToken(), Assignment(nameof(ComplexCombatData.RewardItemCount), complexCombatData.RewardItemCount, null) diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5090_Preying Mantises.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5090_Preying Mantises.json new file mode 100644 index 00000000..3814284c --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5090_Preying Mantises.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1048960, + "Position": { + "X": -405.84424, + "Y": 23.562847, + "Z": -476.61554 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "Position": { + "X": -455.06216, + "Y": 20.027777, + "Z": 46.272446 + }, + "TerritoryId": 1189, + "InteractionType": "Combat", + "EnemySpawnType": "OverworldEnemies", + "KillEnemyDataIds": [ + 17268 + ], + "Fly": true + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1048960, + "Position": { + "X": -405.84424, + "Y": 23.562847, + "Z": -476.61554 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "AetheryteShortcut": "Yak T'el - Iq Br'aax" + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5091_Getting to the Bottom of Things.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5091_Getting to the Bottom of Things.json new file mode 100644 index 00000000..98fffc22 --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5091_Getting to the Bottom of Things.json @@ -0,0 +1,102 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1051071, + "Position": { + "X": -554.03986, + "Y": 1.3156406, + "Z": -489.3416 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "Position": { + "X": -596.55597, + "Y": 2.3708515, + "Z": -493.64908 + }, + "TerritoryId": 1189, + "InteractionType": "WalkTo", + "Fly": true + }, + { + "Position": { + "X": -615.2386, + "Y": -44.093876, + "Z": -495.7548 + }, + "TerritoryId": 1189, + "InteractionType": "WalkTo", + "Fly": true, + "DisableNavmesh": true + }, + { + "DataId": 2014307, + "Position": { + "X": -729.7932, + "Y": -107.83557, + "Z": -674.8608 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "Position": { + "X": -615.2386, + "Y": -44.093876, + "Z": -495.7548 + }, + "TerritoryId": 1189, + "InteractionType": "WalkTo", + "Fly": true + }, + { + "Position": { + "X": -596.55597, + "Y": 2.3708515, + "Z": -493.64908 + }, + "TerritoryId": 1189, + "InteractionType": "WalkTo", + "Fly": true, + "DisableNavmesh": true + }, + { + "DataId": 1051071, + "Position": { + "X": -554.03986, + "Y": 1.3156406, + "Z": -489.3416 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "Fly": true, + "DialogueChoices": [ + { + "Type": "List", + "Prompt": "TEXT_KINGZD002_05091_Q1_000_000", + "Answer": "TEXT_KINGZD002_05091_A1_000_001" + } + ] + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5092_The Crimson Cenote.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5092_The Crimson Cenote.json new file mode 100644 index 00000000..76482417 --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5092_The Crimson Cenote.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1051057, + "Position": { + "X": -343.67902, + "Y": 18.885578, + "Z": -404.22675 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "Position": { + "X": 158.97704, + "Y": 0.80940175, + "Z": -773.5188 + }, + "StopDistance": 1, + "TerritoryId": 1189, + "InteractionType": "Combat", + "AetheryteShortcut": "Tuliyollal", + "EnemySpawnType": "AutoOnEnterArea", + "KillEnemyDataIds": [ + 17663 + ], + "AethernetShortcut": [ + "[Tuliyollal] Aetheryte Plaza", + "[Tuliyollal] Dirigible Landing (Yak T'el)" + ] + } + ] + }, + { + "Sequence": 2, + "Steps": [ + { + "DataId": 1051058, + "Position": { + "X": 159.47205, + "Y": 0.81595546, + "Z": -770.19916 + }, + "StopDistance": 7, + "TerritoryId": 1189, + "InteractionType": "Interact" + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1051057, + "Position": { + "X": -343.67902, + "Y": 18.885578, + "Z": -404.22675 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "AetheryteShortcut": "Yak T'el - Iq Br'aax", + "Fly": true + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5093_Watching the Watchtowers.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5093_Watching the Watchtowers.json new file mode 100644 index 00000000..8b1c5c63 --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5093_Watching the Watchtowers.json @@ -0,0 +1,67 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1048928, + "Position": { + "X": 36.301147, + "Y": 8.205902, + "Z": -645.1362 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 1051072, + "Position": { + "X": -39.07837, + "Y": 4.3051004, + "Z": -332.87555 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true + } + ] + }, + { + "Sequence": 2, + "Steps": [ + { + "DataId": 1051072, + "Position": { + "X": -39.07837, + "Y": 4.3051004, + "Z": -332.87555 + }, + "TerritoryId": 1189, + "InteractionType": "Interact" + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1048928, + "Position": { + "X": 36.301147, + "Y": 8.205902, + "Z": -645.1362 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest" + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5095_Hunters with Bite.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5095_Hunters with Bite.json new file mode 100644 index 00000000..5bc9a423 --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5095_Hunters with Bite.json @@ -0,0 +1,75 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1051061, + "Position": { + "X": -407.79742, + "Y": 28.068892, + "Z": -360.55542 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 1051062, + "Position": { + "X": -489.5247, + "Y": -29.96456, + "Z": -94.83484 + }, + "TerritoryId": 1189, + "InteractionType": "Combat", + "EnemySpawnType": "AfterInteraction", + "KillEnemyDataIds": [ + 17664, + 17665 + ], + "Fly": true + } + ] + }, + { + "Sequence": 2, + "Steps": [ + { + "DataId": 1051062, + "Position": { + "X": -489.5247, + "Y": -29.96456, + "Z": -94.83484 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "StopDistance": 7 + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1051061, + "Position": { + "X": -407.79742, + "Y": 28.068892, + "Z": -360.55542 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "AetheryteShortcut": "Yak T'el - Iq Br'aax", + "Fly": true + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5096_Reminders of the Past.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5096_Reminders of the Past.json new file mode 100644 index 00000000..c4172c71 --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5096_Reminders of the Past.json @@ -0,0 +1,99 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1051065, + "Position": { + "X": 380.7583, + "Y": 21.437008, + "Z": -496.84903 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 2014264, + "Position": { + "X": 405.1422, + "Y": 20.85907, + "Z": -482.10883 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] + }, + { + "DataId": 2014266, + "Position": { + "X": 430.83838, + "Y": 20.126648, + "Z": -456.71783 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 + ] + }, + { + "DataId": 2014265, + "Position": { + "X": 395.92578, + "Y": 20.34021, + "Z": -453.02515 + }, + "TerritoryId": 1189, + "InteractionType": "Combat", + "EnemySpawnType": "AfterInteraction", + "KillEnemyDataIds": [ + 17666 + ], + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 32 + ] + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1051065, + "Position": { + "X": 380.7583, + "Y": 21.437008, + "Z": -496.84903 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest" + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5097_Sick Day.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5097_Sick Day.json new file mode 100644 index 00000000..c7e82c8b --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5097_Sick Day.json @@ -0,0 +1,103 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1051364, + "Position": { + "X": -9.353821, + "Y": 8.205902, + "Z": -652.9183 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 1048961, + "Position": { + "X": -444.38855, + "Y": 28.068893, + "Z": -363.66827 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "AetheryteShortcut": "Yak T'el - Iq Br'aax", + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 + ] + }, + { + "Position": { + "X": -632.6718, + "Y": 25.245409, + "Z": -159.94264 + }, + "TerritoryId": 1189, + "InteractionType": "WalkTo", + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + -128 + ] + }, + { + "DataId": 1048995, + "Position": { + "X": -639.27673, + "Y": 26.716875, + "Z": -161.05902 + }, + "StopDistance": 7, + "TerritoryId": 1189, + "InteractionType": "Interact", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1051410, + "Position": { + "X": -9.140198, + "Y": 8.205902, + "Z": -652.9183 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "AetheryteShortcut": "Tuliyollal", + "AethernetShortcut": [ + "[Tuliyollal] Aetheryte Plaza", + "[Tuliyollal] Dirigible Landing (Yak T'el)" + ] + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5098_To Forge in the Forest.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5098_To Forge in the Forest.json new file mode 100644 index 00000000..118dc4c8 --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5098_To Forge in the Forest.json @@ -0,0 +1,185 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1048996, + "Position": { + "X": -618.43286, + "Y": 25.709108, + "Z": -153.91785 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 1051365, + "Position": { + "X": -738.3383, + "Y": 22.137896, + "Z": -85.404785 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true + } + ] + }, + { + "Sequence": 2, + "Steps": [ + { + "Position": { + "X": -580.44604, + "Y": 22.457048, + "Z": -3.286837 + }, + "TerritoryId": 1189, + "InteractionType": "WalkTo", + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + -128 + ] + }, + { + "DataId": 2014313, + "Position": { + "X": -581.75024, + "Y": 23.697266, + "Z": -2.9450073 + }, + "TerritoryId": 1189, + "InteractionType": "UseItem", + "ItemId": 2003683, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] + }, + { + "Position": { + "X": -506.6366, + "Y": 22.817732, + "Z": 32.985153 + }, + "TerritoryId": 1189, + "InteractionType": "WalkTo", + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + -64 + ] + }, + { + "DataId": 2014314, + "Position": { + "X": -505.9129, + "Y": 23.88031, + "Z": 32.455933 + }, + "TerritoryId": 1189, + "InteractionType": "UseItem", + "ItemId": 2003683, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 + ] + }, + { + "Position": { + "X": -414.84598, + "Y": 20.493914, + "Z": 74.74898 + }, + "TerritoryId": 1189, + "InteractionType": "WalkTo", + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + -32 + ] + }, + { + "DataId": 2014315, + "Position": { + "X": -413.96204, + "Y": 21.896606, + "Z": 74.021484 + }, + "TerritoryId": 1189, + "InteractionType": "UseItem", + "ItemId": 2003683, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 32 + ] + } + ] + }, + { + "Sequence": 3, + "Steps": [ + { + "DataId": 1051365, + "Position": { + "X": -738.3383, + "Y": 22.137896, + "Z": -85.404785 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1048996, + "Position": { + "X": -618.43286, + "Y": 25.709108, + "Z": -153.91785 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "Fly": true + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5099_A Stomach for Change.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5099_A Stomach for Change.json new file mode 100644 index 00000000..66e5f31d --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5099_A Stomach for Change.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1051366, + "Position": { + "X": -399.95422, + "Y": 20.145584, + "Z": -402.4262 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 1051367, + "Position": { + "X": -503.95975, + "Y": 29.007706, + "Z": -394.43048 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1051368, + "Position": { + "X": -501.27414, + "Y": 28.982998, + "Z": -394.3084 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest" + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5100_Carved in Stone.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5100_Carved in Stone.json new file mode 100644 index 00000000..a0143ab6 --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5100_Carved in Stone.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1048982, + "Position": { + "X": 595.45276, + "Y": -137.17401, + "Z": 564.8126 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 2014250, + "Position": { + "X": 598.3214, + "Y": -137.19391, + "Z": 557.7018 + }, + "TerritoryId": 1189, + "InteractionType": "Instruction", + "Comment": "Inspect Flag, Red Head, Blue Head" + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1048982, + "Position": { + "X": 595.45276, + "Y": -137.17401, + "Z": 564.8126 + }, + "StopDistance": 7, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest" + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5101_Two Hearts Aflutter.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5101_Two Hearts Aflutter.json new file mode 100644 index 00000000..570d2f2f --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5101_Two Hearts Aflutter.json @@ -0,0 +1,152 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1048984, + "Position": { + "X": 673.06006, + "Y": -135.17874, + "Z": 577.47766 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "Position": { + "X": -92.00457, + "Y": -212.91975, + "Z": 624.4216 + }, + "TerritoryId": 1189, + "InteractionType": "WalkTo", + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + -128 + ] + }, + { + "DataId": 1051049, + "Position": { + "X": -89.49426, + "Y": -213.64497, + "Z": 625.574 + }, + "StopDistance": 4, + "TerritoryId": 1189, + "InteractionType": "Interact", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + -128 + ] + }, + { + "Position": { + "X": -122.188805, + "Y": -214.08376, + "Z": 674.54083 + }, + "TerritoryId": 1189, + "InteractionType": "WalkTo", + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + -32 + ] + }, + { + "DataId": 1051051, + "Position": { + "X": -122.88098, + "Y": -213.78055, + "Z": 673.39575 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 32 + ] + }, + { + "Position": { + "X": -142.65875, + "Y": -213.42487, + "Z": 608.63165 + }, + "TerritoryId": 1189, + "InteractionType": "WalkTo", + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + -64 + ] + }, + { + "DataId": 1051050, + "Position": { + "X": -143.63324, + "Y": -213.18237, + "Z": 609.0028 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 + ] + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1048984, + "Position": { + "X": 673.06006, + "Y": -135.17874, + "Z": 577.47766 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "AetheryteShortcut": "Yak T'el - Mamook", + "Fly": true + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5102_Proper Respects.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5102_Proper Respects.json new file mode 100644 index 00000000..3bb5132e --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5102_Proper Respects.json @@ -0,0 +1,116 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1048988, + "Position": { + "X": 584.4967, + "Y": -143.46597, + "Z": 648.64575 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 2014369, + "Position": { + "X": 328.20618, + "Y": -157.21375, + "Z": 429.4651 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 16 + ] + }, + { + "DataId": 2014368, + "Position": { + "X": 323.7201, + "Y": -157.76306, + "Z": 434.19543 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 + ] + }, + { + "DataId": 2014256, + "Position": { + "X": 139.17749, + "Y": -162.24927, + "Z": 679.95715 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] + }, + { + "DataId": 2014257, + "Position": { + "X": 144.67078, + "Y": -163.25635, + "Z": 663.84375 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 32 + ] + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1048988, + "Position": { + "X": 584.4967, + "Y": -143.46597, + "Z": 648.64575 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "Fly": true + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5104_Spirit's Call.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5104_Spirit's Call.json new file mode 100644 index 00000000..42c30b86 --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5104_Spirit's Call.json @@ -0,0 +1,163 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1048987, + "Position": { + "X": 596.97876, + "Y": -142.60623, + "Z": 441.7334 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 1051053, + "Position": { + "X": 257.1908, + "Y": -165.78062, + "Z": 146.74597 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true, + "DialogueChoices": [ + { + "Type": "YesNo", + "Prompt": "TEXT_KINGZD105_05104_SYSTEM_100_004", + "Yes": true + } + ] + } + ] + }, + { + "Sequence": 2, + "Steps": [ + { + "Position": { + "X": 272.75598, + "Y": -161.77667, + "Z": 117.129616 + }, + "TerritoryId": 1189, + "InteractionType": "WalkTo", + "Mount": true, + "DisableNavmesh": true + }, + { + "Position": { + "X": 255.7041, + "Y": -161.34225, + "Z": 101.95002 + }, + "TerritoryId": 1189, + "InteractionType": "WalkTo", + "DisableNavmesh": true + }, + { + "DataId": 1051053, + "Position": { + "X": 218.3051, + "Y": -160.72849, + "Z": 78.95244 + }, + "TerritoryId": 1189, + "InteractionType": "WaitForNpcAtPosition", + "DisableNavmesh": true + }, + { + "DataId": 1051053, + "Position": { + "X": 168.0448, + "Y": -158.83824, + "Z": 31.171473 + }, + "TerritoryId": 1189, + "InteractionType": "WaitForNpcAtPosition", + "DisableNavmesh": true + }, + { + "DataId": 1051053, + "Position": { + "X": 144.163, + "Y": -156.25887, + "Z": -57.818764 + }, + "TerritoryId": 1189, + "InteractionType": "WaitForNpcAtPosition", + "DisableNavmesh": true + }, + { + "Position": { + "X": 83.32947, + "Y": -157.21858, + "Z": -67.64331 + }, + "StopDistance": 1, + "TerritoryId": 1189, + "InteractionType": "WalkTo", + "DisableNavmesh": true + } + ] + }, + { + "Sequence": 3, + "Steps": [ + { + "DataId": 1051054, + "Position": { + "X": 83.32947, + "Y": -157.21858, + "Z": -67.64331 + }, + "TerritoryId": 1189, + "InteractionType": "Interact" + } + ] + }, + { + "Sequence": 4, + "Steps": [ + { + "DataId": 2014259, + "Position": { + "X": 65.049194, + "Y": -184.1612, + "Z": -70.237305 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "DisableNavmesh": true, + "Mount": false + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1048987, + "Position": { + "X": 596.97876, + "Y": -142.60623, + "Z": 441.7334 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "AetheryteShortcut": "Yak T'el - Mamook", + "Fly": true + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5105_Seek the Sun.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5105_Seek the Sun.json new file mode 100644 index 00000000..9a38e35c --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5105_Seek the Sun.json @@ -0,0 +1,92 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1051055, + "Position": { + "X": 563.5918, + "Y": -144.04688, + "Z": 702.26587 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "Position": { + "X": -45.87303, + "Y": -174.86252, + "Z": 649.25995 + }, + "TerritoryId": 1189, + "InteractionType": "WalkTo", + "Fly": true + }, + { + "DataId": 1051056, + "Position": { + "X": -49.149292, + "Y": -174.58734, + "Z": 651.4839 + }, + "StopDistance": 5, + "TerritoryId": 1189, + "InteractionType": "Interact", + "DisableNavmesh": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] + } + ] + }, + { + "Sequence": 2, + "Steps": [ + { + "DataId": 1051056, + "Position": { + "X": -49.149292, + "Y": -174.58734, + "Z": 651.4839 + }, + "StopDistance": 5, + "TerritoryId": 1189, + "InteractionType": "Say", + "ChatMessage": { + "Key": "TEXT_KINGZD106_05105_SAYTODO_000_011" + } + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1051055, + "Position": { + "X": 563.5918, + "Y": -144.04688, + "Z": 702.26587 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "AetheryteShortcut": "Yak T'el - Mamook", + "Fly": true + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5106_Secret of Sweetness.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5106_Secret of Sweetness.json new file mode 100644 index 00000000..63f926c7 --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5106_Secret of Sweetness.json @@ -0,0 +1,90 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1048991, + "Position": { + "X": 211.10852, + "Y": -160.27475, + "Z": 432.4253 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "Position": { + "X": 178.30347, + "Y": -195.42366, + "Z": 241.28633 + }, + "TerritoryId": 1189, + "InteractionType": "WalkTo", + "Fly": true + }, + { + "DataId": 2014260, + "Position": { + "X": 177.44714, + "Y": -194.11005, + "Z": 239.61243 + }, + "TerritoryId": 1189, + "InteractionType": "UseItem", + "ItemId": 2003667, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] + }, + { + "DataId": 2014261, + "Position": { + "X": 164.47693, + "Y": -194.2627, + "Z": 237.50659 + }, + "TerritoryId": 1189, + "InteractionType": "UseItem", + "ItemId": 2003667, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 + ] + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1048991, + "Position": { + "X": 211.10852, + "Y": -160.27475, + "Z": 432.4253 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "Fly": true + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5107_Growing Grove.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5107_Growing Grove.json new file mode 100644 index 00000000..ddcb322b --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5107_Growing Grove.json @@ -0,0 +1,107 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1048998, + "Position": { + "X": 220.29443, + "Y": -156.80957, + "Z": 457.02295 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 1051408, + "Position": { + "X": 631.2809, + "Y": -137.12654, + "Z": 509.6665 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "AetheryteShortcut": "Yak T'el - Mamook", + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 + ] + }, + { + "DataId": 1048974, + "Position": { + "X": 621.51514, + "Y": -135.12726, + "Z": 531.1207 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 32 + ] + }, + { + "DataId": 1048989, + "Position": { + "X": 523.4302, + "Y": -135.12724, + "Z": 578.14905 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1048998, + "Position": { + "X": 220.29443, + "Y": -156.80957, + "Z": 457.02295 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "Fly": true, + "DialogueChoices": [ + { + "Type": "List", + "Prompt": "TEXT_KINGZD108_05107_Q1_000_000", + "Answer": "TEXT_KINGZD108_05107_A1_000_001" + } + ] + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5108_Age Is Just a Number.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5108_Age Is Just a Number.json new file mode 100644 index 00000000..2bfe6c05 --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5108_Age Is Just a Number.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1051067, + "Position": { + "X": -372.70166, + "Y": -162.24626, + "Z": 546.25757 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest", + "Fly": true + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 2014262, + "Position": { + "X": -443.83917, + "Y": -158.1903, + "Z": 343.34326 + }, + "TerritoryId": 1189, + "InteractionType": "Combat", + "EnemySpawnType": "AfterInteraction", + "KillEnemyDataIds": [ + 17743, + 17744, + 17745 + ], + "Fly": true + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1051067, + "Position": { + "X": -372.70166, + "Y": -162.24626, + "Z": 546.25757 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "Fly": true + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5109_I'm with Wivre.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5109_I'm with Wivre.json new file mode 100644 index 00000000..1e3e90e0 --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5109_I'm with Wivre.json @@ -0,0 +1,97 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1048997, + "Position": { + "X": -363.9124, + "Y": -162.23582, + "Z": 550.01135 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest", + "Fly": true + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 1051387, + "Position": { + "X": -383.4745, + "Y": -166.23917, + "Z": 662.95874 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true + } + ] + }, + { + "Sequence": 2, + "Steps": [ + { + "Position": { + "X": -433.617, + "Y": -156.24463, + "Z": 776.9229 + }, + "TerritoryId": 1189, + "InteractionType": "Instruction", + "Comment": "TODO Needs item use; manual for now (also unsure on health percent)", + "EnemySpawnType": "OverworldEnemies", + "ComplexCombatData": [ + { + "DataId": 17267, + "MinimumKillCount": 2, + "ItemId": 2003669, + "ItemUseHealthMaxPercent": 25, + "RewardItemId": 2003670, + "RewardItemCount": 2 + } + ], + "Fly": true + } + ] + }, + { + "Sequence": 3, + "Steps": [ + { + "DataId": 1051387, + "Position": { + "X": -383.4745, + "Y": -166.23917, + "Z": 662.95874 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1048997, + "Position": { + "X": -363.9124, + "Y": -162.23582, + "Z": 550.01135 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "Fly": true + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5111_Hunting for the Family.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5111_Hunting for the Family.json new file mode 100644 index 00000000..b2e79a7b --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5111_Hunting for the Family.json @@ -0,0 +1,97 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1050873, + "Position": { + "X": -455.10034, + "Y": 17.027555, + "Z": -478.8739 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 1050874, + "Position": { + "X": -508.87314, + "Y": 29.61908, + "Z": -350.1183 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true + } + ] + }, + { + "Sequence": 2, + "Steps": [ + { + "DataId": 1050876, + "Position": { + "X": -455.92438, + "Y": 30.374397, + "Z": -423.148 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] + }, + { + "DataId": 1050875, + "Position": { + "X": -391.01245, + "Y": 28.068893, + "Z": -365.0111 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 + ] + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1050874, + "Position": { + "X": -508.87314, + "Y": 29.61908, + "Z": -350.1183 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "Fly": true, + "NextQuestId": 5112 + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5112_A Hunter's Dream.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5112_A Hunter's Dream.json new file mode 100644 index 00000000..9afc64aa --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5112_A Hunter's Dream.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1050874, + "Position": { + "X": -508.87314, + "Y": 29.61908, + "Z": -350.1183 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest", + "DialogueChoices": [ + { + "Type": "List", + "Prompt": "TEXT_KINGZD203_05112_Q1_000_000", + "Answer": "TEXT_KINGZD203_05112_A1_000_001" + } + ] + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 1050877, + "Position": { + "X": -39.871887, + "Y": 0.35915944, + "Z": -312.1233 + }, + "StopDistance": 1, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true + } + ] + }, + { + "Sequence": 2, + "Steps": [ + { + "DataId": 2014144, + "Position": { + "X": -43.10681, + "Y": 0.503479, + "Z": -312.36743 + }, + "TerritoryId": 1189, + "InteractionType": "Interact" + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1050878, + "Position": { + "X": 475.30322, + "Y": 18.197147, + "Z": -326.95508 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "Fly": true, + "NextQuestId": 5113 + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5113_The Trial Commences.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5113_The Trial Commences.json new file mode 100644 index 00000000..732d1ecc --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5113_The Trial Commences.json @@ -0,0 +1,90 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1050878, + "Position": { + "X": 475.30322, + "Y": 18.197147, + "Z": -326.95508 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 1050879, + "Position": { + "X": 595.7274, + "Y": 18.8562, + "Z": -85.55737 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true + } + ] + }, + { + "Sequence": 2, + "Steps": [ + { + "Position": { + "X": 705.4334, + "Y": 28.302685, + "Z": 2.3153868 + }, + "StopDistance": 1, + "TerritoryId": 1189, + "InteractionType": "Combat", + "EnemySpawnType": "AutoOnEnterArea", + "KillEnemyDataIds": [ + 17671, + 17672 + ], + "Fly": true + } + ] + }, + { + "Sequence": 3, + "Steps": [ + { + "DataId": 1050881, + "Position": { + "X": 595.6052, + "Y": 18.84498, + "Z": -85.55737 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1050882, + "Position": { + "X": -408.31616, + "Y": 20.383144, + "Z": -399.28284 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "AetheryteShortcut": "Yak T'el - Iq Br'aax" + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5115_Remembering a Dream.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5115_Remembering a Dream.json new file mode 100644 index 00000000..05dc0263 --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5115_Remembering a Dream.json @@ -0,0 +1,125 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1050888, + "Position": { + "X": 515.58704, + "Y": -146.73436, + "Z": 477.0122 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 2014147, + "Position": { + "X": 440.66528, + "Y": -150.77448, + "Z": 427.7256 + }, + "TerritoryId": 1189, + "InteractionType": "Combat", + "EnemySpawnType": "AfterInteraction", + "KillEnemyDataIds": [ + 17674 + ], + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] + }, + { + "DataId": 2014148, + "Position": { + "X": 395.4679, + "Y": -152.42242, + "Z": 372.7931 + }, + "TerritoryId": 1189, + "InteractionType": "Combat", + "EnemySpawnType": "AfterInteraction", + "KillEnemyDataIds": [ + 17674 + ], + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 + ] + }, + { + "DataId": 2014149, + "Position": { + "X": 267.9026, + "Y": -160.05188, + "Z": 354.45166 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 32 + ] + } + ] + }, + { + "Sequence": 2, + "Steps": [ + { + "DataId": 2014150, + "Position": { + "X": 236.95728, + "Y": -167.52887, + "Z": 307.97278 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1050890, + "Position": { + "X": 661.7991, + "Y": -137.174, + "Z": 557.76294 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "AetheryteShortcut": "Yak T'el - Mamook", + "Fly": true, + "NextQuestId": 5116 + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5116_Budding Promise.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5116_Budding Promise.json new file mode 100644 index 00000000..1acb4ee6 --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5116_Budding Promise.json @@ -0,0 +1,80 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1050891, + "Position": { + "X": 662.8672, + "Y": -137.17401, + "Z": 556.115 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 1048985, + "Position": { + "X": 595.11694, + "Y": -143.83003, + "Z": 710.68884 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true + } + ] + }, + { + "Sequence": 2, + "Steps": [ + { + "Position": { + "X": -61.729744, + "Y": -212.69492, + "Z": 523.0088 + }, + "TerritoryId": 1189, + "InteractionType": "WalkTo", + "Fly": true + }, + { + "DataId": 2014151, + "Position": { + "X": -60.95984, + "Y": -210.83392, + "Z": 522.02637 + }, + "TerritoryId": 1189, + "InteractionType": "Interact" + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1050891, + "Position": { + "X": 662.8672, + "Y": -137.17401, + "Z": 556.115 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "AetheryteShortcut": "Yak T'el - Mamook", + "Fly": true, + "NextQuestId": 5117 + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5117_Skill of a Veteran.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5117_Skill of a Veteran.json new file mode 100644 index 00000000..44fbb7fe --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5117_Skill of a Veteran.json @@ -0,0 +1,86 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1050891, + "Position": { + "X": 662.8672, + "Y": -137.17401, + "Z": 556.115 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 2014153, + "Position": { + "X": 500.5111, + "Y": -150.04199, + "Z": 704.12744 + }, + "TerritoryId": 1189, + "InteractionType": "UseItem", + "ItemId": 2003624, + "GroundTarget": true, + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 + ] + }, + { + "DataId": 2014152, + "Position": { + "X": 486.90015, + "Y": -150.04199, + "Z": 355.8556 + }, + "TerritoryId": 1189, + "InteractionType": "UseItem", + "ItemId": 2003624, + "GroundTarget": true, + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1050891, + "Position": { + "X": 662.8672, + "Y": -137.17401, + "Z": 556.115 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "AetheryteShortcut": "Yak T'el - Mamook", + "Fly": true, + "NextQuestId": 5118 + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5118_One Forest.json b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5118_One Forest.json new file mode 100644 index 00000000..2b3b9810 --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Side Quests/Yak T'el/5118_One Forest.json @@ -0,0 +1,219 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1050891, + "Position": { + "X": 662.8672, + "Y": -137.17401, + "Z": 556.115 + }, + "TerritoryId": 1189, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 1050897, + "Position": { + "X": 755.94714, + "Y": -132.59648, + "Z": 505.94324 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true, + "DialogueChoices": [ + { + "Type": "List", + "Prompt": "TEXT_KINGZD401_05118_Q1_000_000", + "Answer": "TEXT_KINGZD401_05118_A1_000_001" + } + ] + } + ] + }, + { + "Sequence": 2, + "Steps": [ + { + "DataId": 1050891, + "Position": { + "X": 662.8672, + "Y": -137.17401, + "Z": 556.115 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true + } + ] + }, + { + "Sequence": 3, + "Steps": [ + { + "DataId": 1050901, + "Position": { + "X": 627.25244, + "Y": -137.174, + "Z": 591.8517 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 + ] + }, + { + "DataId": 1050900, + "Position": { + "X": 575.55493, + "Y": -137.174, + "Z": 540.0929 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true, + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] + } + ] + }, + { + "Sequence": 4, + "Steps": [ + { + "DataId": 1048989, + "Position": { + "X": 523.4302, + "Y": -135.12724, + "Z": 578.14905 + }, + "TerritoryId": 1189, + "InteractionType": "Interact" + } + ] + }, + { + "Sequence": 5, + "Steps": [ + { + "DataId": 1050891, + "Position": { + "X": 662.8672, + "Y": -137.17401, + "Z": 556.115 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true + } + ] + }, + { + "Sequence": 6, + "Steps": [ + { + "DataId": 1050904, + "Position": { + "X": 263.84375, + "Y": -157.31726, + "Z": 738.6129 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true + } + ] + }, + { + "Sequence": 7, + "Steps": [ + { + "DataId": 2014154, + "Position": { + "X": 255.9701, + "Y": -150.68292, + "Z": 807.64465 + }, + "TerritoryId": 1189, + "InteractionType": "UseItem", + "ItemId": 2003625, + "Fly": true + } + ] + }, + { + "Sequence": 8, + "Steps": [ + { + "DataId": 2014248, + "Position": { + "X": 255.9701, + "Y": -150.68292, + "Z": 807.64465 + }, + "TerritoryId": 1189, + "InteractionType": "Combat", + "EnemySpawnType": "AfterInteraction", + "KillEnemyDataIds": [ + 17675 + ] + } + ] + }, + { + "Sequence": 9, + "Steps": [ + { + "DataId": 1050904, + "Position": { + "X": 263.84375, + "Y": -157.31726, + "Z": 738.6129 + }, + "TerritoryId": 1189, + "InteractionType": "Interact", + "Fly": true + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1050903, + "Position": { + "X": 663.9962, + "Y": -137.17401, + "Z": 554.589 + }, + "TerritoryId": 1189, + "InteractionType": "CompleteQuest", + "AetheryteShortcut": "Yak T'el - Mamook", + "Fly": true + } + ] + } + ] +} diff --git a/QuestPaths/quest-v1.json b/QuestPaths/quest-v1.json index 6c1d43ff..04b2a1fd 100644 --- a/QuestPaths/quest-v1.json +++ b/QuestPaths/quest-v1.json @@ -550,6 +550,10 @@ "description": "The enemy data id which is supposed to be killed", "type": "integer" }, + "MinimumKillCount": { + "description": "Overworld mobs: If this number of mobs has been killed, will wait a bit before attempting to pull another mob to see if the quest progresses", + "type": "integer" + }, "RewardItemId": { "type": "integer" }, diff --git a/Questionable.Model/V1/ComplexCombatData.cs b/Questionable.Model/V1/ComplexCombatData.cs index 29d0f223..69949ef7 100644 --- a/Questionable.Model/V1/ComplexCombatData.cs +++ b/Questionable.Model/V1/ComplexCombatData.cs @@ -5,6 +5,10 @@ namespace Questionable.Model.V1; public sealed class ComplexCombatData { public uint DataId { get; set; } + + // TODO Use this + public uint? MinimumKillCount { get; set; } + public uint? RewardItemId { get; set; } public int? RewardItemCount { get; set; } public IList CompletionQuestVariablesFlags { get; set; } = new List(); diff --git a/Questionable/Controller/CommandHandler.cs b/Questionable/Controller/CommandHandler.cs index da002e2c..33ccf29a 100644 --- a/Questionable/Controller/CommandHandler.cs +++ b/Questionable/Controller/CommandHandler.cs @@ -22,11 +22,14 @@ internal sealed class CommandHandler : IDisposable private readonly QuestWindow _questWindow; private readonly QuestSelectionWindow _questSelectionWindow; private readonly ITargetManager _targetManager; + private readonly GameFunctions _gameFunctions; + private readonly IClientState _clientState; public CommandHandler(ICommandManager commandManager, IChatGui chatGui, QuestController questController, MovementController movementController, QuestRegistry questRegistry, ConfigWindow configWindow, DebugOverlay debugOverlay, QuestWindow questWindow, - QuestSelectionWindow questSelectionWindow, ITargetManager targetManager) + QuestSelectionWindow questSelectionWindow, ITargetManager targetManager, GameFunctions gameFunctions, + IClientState clientState) { _commandManager = commandManager; _chatGui = chatGui; @@ -38,6 +41,8 @@ internal sealed class CommandHandler : IDisposable _questWindow = questWindow; _questSelectionWindow = questSelectionWindow; _targetManager = targetManager; + _gameFunctions = gameFunctions; + _clientState = clientState; _commandManager.AddHandler("/qst", new CommandInfo(ProcessCommand) { @@ -77,7 +82,12 @@ internal sealed class CommandHandler : IDisposable break; case "which": - _questSelectionWindow.Open(_targetManager.Target); + _questSelectionWindow.OpenForTarget(_targetManager.Target); + break; + + case "z": + case "zone": + _questSelectionWindow.OpenForZone(_clientState.TerritoryType); break; default: @@ -96,7 +106,9 @@ internal sealed class CommandHandler : IDisposable if (arguments.Length >= 1 && ushort.TryParse(arguments[0], out ushort questId)) { - if (_questRegistry.TryGetQuest(questId, out Quest? quest)) + if (_gameFunctions.IsQuestLocked(questId, 0)) + _chatGui.PrintError($"[Questionable] Quest {questId} is locked."); + else if (_questRegistry.TryGetQuest(questId, out Quest? quest)) { _debugOverlay.HighlightedQuest = questId; _chatGui.Print($"[Questionable] Set highlighted quest to {questId} ({quest.Info.Name})."); diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs index f8a41253..dc656fbc 100644 --- a/Questionable/Controller/QuestRegistry.cs +++ b/Questionable/Controller/QuestRegistry.cs @@ -36,6 +36,7 @@ internal sealed class QuestRegistry _logger = logger; } + public IEnumerable AllQuests => _quests.Values; public int Count => _quests.Count; public void Reload() diff --git a/Questionable/Controller/Steps/Common/NextQuest.cs b/Questionable/Controller/Steps/Common/NextQuest.cs index 32d0f113..30a705e6 100644 --- a/Questionable/Controller/Steps/Common/NextQuest.cs +++ b/Questionable/Controller/Steps/Common/NextQuest.cs @@ -18,24 +18,33 @@ internal static class NextQuest if (step.NextQuestId == null) return null; + if (step.NextQuestId.Value == quest.QuestId) + return null; + return serviceProvider.GetRequiredService() - .With(step.NextQuestId.Value); + .With(step.NextQuestId.Value, quest.QuestId); } } - internal sealed class SetQuest(QuestRegistry questRegistry, QuestController questController, ILogger logger) : ITask + internal sealed class SetQuest(QuestRegistry questRegistry, QuestController questController, GameFunctions gameFunctions, ILogger logger) : ITask { public ushort NextQuestId { get; set; } + public ushort CurrentQuestId { get; set; } - public ITask With(ushort nextQuestId) + public ITask With(ushort nextQuestId, ushort currentQuestId) { NextQuestId = nextQuestId; + CurrentQuestId = currentQuestId; return this; } public bool Start() { - if (questRegistry.TryGetQuest(NextQuestId, out Quest? quest)) + if (gameFunctions.IsQuestLocked(NextQuestId, CurrentQuestId)) + { + logger.LogInformation("Can't set next quest to {QuestId}, quest is locked", NextQuestId); + } + else if (questRegistry.TryGetQuest(NextQuestId, out Quest? quest)) { logger.LogInformation("Setting next quest to {QuestId}: '{QuestName}'", NextQuestId, quest.Info.Name); questController.SetNextQuest(quest); diff --git a/Questionable/GameFunctions.cs b/Questionable/GameFunctions.cs index 31e3a622..f25cec48 100644 --- a/Questionable/GameFunctions.cs +++ b/Questionable/GameFunctions.cs @@ -21,6 +21,8 @@ using Lumina.Excel.CustomSheets; using Lumina.Excel.GeneratedSheets2; using Microsoft.Extensions.Logging; using Questionable.Controller; +using Questionable.Data; +using Questionable.Model; using Questionable.Model.V1; using Action = Lumina.Excel.GeneratedSheets2.Action; using BattleChara = FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara; @@ -45,6 +47,7 @@ internal sealed unsafe class GameFunctions private readonly ICondition _condition; private readonly IClientState _clientState; private readonly QuestRegistry _questRegistry; + private readonly QuestData _questData; private readonly IGameGui _gameGui; private readonly Configuration _configuration; private readonly ILogger _logger; @@ -55,6 +58,7 @@ internal sealed unsafe class GameFunctions ICondition condition, IClientState clientState, QuestRegistry questRegistry, + QuestData questData, IGameGui gameGui, Configuration configuration, ILogger logger) @@ -65,6 +69,7 @@ internal sealed unsafe class GameFunctions _condition = condition; _clientState = clientState; _questRegistry = questRegistry; + _questData = questData; _gameGui = gameGui; _configuration = configuration; _logger = logger; @@ -246,7 +251,7 @@ internal sealed unsafe class GameFunctions public bool IsQuestAcceptedOrComplete(ushort questId) { - return QuestManager.IsQuestComplete(questId) || IsQuestAccepted(questId); + return IsQuestComplete(questId) || IsQuestAccepted(questId); } public bool IsQuestAccepted(ushort questId) @@ -255,6 +260,38 @@ internal sealed unsafe class GameFunctions return questManager->IsQuestAccepted(questId); } + public bool IsQuestComplete(ushort questId) + { + return QuestManager.IsQuestComplete(questId); + } + + public bool IsQuestLocked(ushort questId, ushort? extraCompletedQuest = null) + { + var questInfo = _questData.GetQuestInfo(questId); + if (questInfo.QuestLocks.Count > 0) + { + var completedQuests = questInfo.QuestLocks.Count(x => IsQuestComplete(x) || x == extraCompletedQuest); + if (questInfo.QuestLockJoin == QuestInfo.QuestJoin.All && questInfo.QuestLocks.Count == completedQuests) + return true; + else if (questInfo.QuestLockJoin == QuestInfo.QuestJoin.AtLeastOne && completedQuests > 0) + return true; + } + + if (questInfo.PreviousQuests.Count > 0) + { + var completedQuests = questInfo.PreviousQuests.Count(x => IsQuestComplete(x) || x == extraCompletedQuest); + if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.All && + questInfo.PreviousQuests.Count == completedQuests) + return false; + else if (questInfo.PreviousQuestJoin == QuestInfo.QuestJoin.AtLeastOne && completedQuests > 0) + return false; + else + return true; + } + + return false; + } + public bool IsAetheryteUnlocked(uint aetheryteId, out byte subIndex) { subIndex = 0; diff --git a/Questionable/Model/QuestInfo.cs b/Questionable/Model/QuestInfo.cs index 14ac7e7c..5b7523e8 100644 --- a/Questionable/Model/QuestInfo.cs +++ b/Questionable/Model/QuestInfo.cs @@ -1,5 +1,9 @@ using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using Dalamud.Game.Text; +using JetBrains.Annotations; using ExcelQuest = Lumina.Excel.GeneratedSheets.Quest; namespace Questionable.Model; @@ -13,6 +17,11 @@ internal sealed class QuestInfo Level = quest.ClassJobLevel0; IssuerDataId = quest.IssuerStart; IsRepeatable = quest.IsRepeatable; + PreviousQuests = quest.PreviousQuest.Select(x => (ushort)(x.Row & 0xFFFF)).Where(x => x != 0).ToImmutableList(); + PreviousQuestJoin = (QuestJoin)quest.PreviousQuestJoin; + QuestLocks = quest.QuestLock.Select(x => (ushort)(x.Row & 0xFFFFF)).Where(x => x != 0).ToImmutableList(); + QuestLockJoin = (QuestJoin)quest.QuestLockJoin; + IsMainScenarioQuest = quest.JournalGenre?.Value?.JournalCategory?.Value?.JournalSection?.Row is 0 or 1; } public ushort QuestId { get; } @@ -20,7 +29,20 @@ internal sealed class QuestInfo public ushort Level { get; } public uint IssuerDataId { get; } public bool IsRepeatable { get; } + public ImmutableList PreviousQuests { get; } + public QuestJoin PreviousQuestJoin { get; } + public bool IsMainScenarioQuest { get; } + public ImmutableList QuestLocks { get; set; } + public QuestJoin QuestLockJoin { get; set; } public string SimplifiedName => Name .TrimStart(SeIconChar.QuestSync.ToIconChar(), SeIconChar.QuestRepeatable.ToIconChar(), ' '); + + [UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Members)] + public enum QuestJoin : byte + { + None = 0, + All = 1, + AtLeastOne = 2, + } } diff --git a/Questionable/Windows/QuestSelectionWindow.cs b/Questionable/Windows/QuestSelectionWindow.cs index a40ca54d..8e0bd971 100644 --- a/Questionable/Windows/QuestSelectionWindow.cs +++ b/Questionable/Windows/QuestSelectionWindow.cs @@ -29,13 +29,15 @@ internal sealed class QuestSelectionWindow : LWindow private readonly QuestController _questController; private readonly QuestRegistry _questRegistry; private readonly IDalamudPluginInterface _pluginInterface; + private readonly TerritoryData _territoryData; private List _quests = []; private List _offeredQuests = []; private bool _onlyAvailableQuests = true; public QuestSelectionWindow(QuestData questData, IGameGui gameGui, IChatGui chatGui, GameFunctions gameFunctions, - QuestController questController, QuestRegistry questRegistry, IDalamudPluginInterface pluginInterface) + QuestController questController, QuestRegistry questRegistry, IDalamudPluginInterface pluginInterface, + TerritoryData territoryData) : base($"Quest Selection{WindowId}") { _questData = questData; @@ -45,6 +47,7 @@ internal sealed class QuestSelectionWindow : LWindow _questController = questController; _questRegistry = questRegistry; _pluginInterface = pluginInterface; + _territoryData = territoryData; Size = new Vector2(500, 200); SizeCondition = ImGuiCond.Once; @@ -57,7 +60,7 @@ internal sealed class QuestSelectionWindow : LWindow public uint TargetId { get; private set; } public string TargetName { get; private set; } = string.Empty; - public unsafe void Open(IGameObject? gameObject) + public unsafe void OpenForTarget(IGameObject? gameObject) { if (gameObject != null) { @@ -85,6 +88,28 @@ internal sealed class QuestSelectionWindow : LWindow IsOpen = _quests.Count > 0; } + public void OpenForZone(ushort territoryId) + { + TargetId = territoryId; + TargetName = _territoryData.GetNameAndId(territoryId); + WindowName = $"Quests starting in {TargetName}{WindowId}"; + + _quests = _questRegistry.AllQuests + .Where(x => x.FindSequence(0)?.FindStep(0)?.TerritoryId == territoryId) + .Select(x => _questData.GetQuestInfo(x.QuestId)) + .ToList(); + _offeredQuests = []; + IsOpen = true; + } + + public override void OnClose() + { + TargetId = default; + TargetName = default; + _quests = []; + _offeredQuests = []; + } + public override void Draw() { if (_offeredQuests.Count != 0) @@ -119,30 +144,40 @@ internal sealed class QuestSelectionWindow : LWindow if (ImGui.TableNextColumn()) { ImGui.AlignTextToFramePadding(); - using var font = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push(); - - FontAwesomeIcon icon; - Vector4 color; - if (_gameFunctions.IsQuestAccepted(quest.QuestId)) + var (color, icon, tooltipText) = GetQuestStyle(quest.QuestId); + using (var _ = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) { - color = ImGuiColors.DalamudYellow; - icon = FontAwesomeIcon.Running; - } - else if (_gameFunctions.IsQuestAcceptedOrComplete(quest.QuestId)) - { - color = ImGuiColors.ParsedGreen; - icon = FontAwesomeIcon.Check; - } - else - { - color = ImGuiColors.DalamudRed; - icon = FontAwesomeIcon.Times; + if (isKnownQuest) + ImGui.TextColored(color, icon.ToIconString()); + else + ImGui.TextColored(ImGuiColors.DalamudGrey, icon.ToIconString()); } - if (isKnownQuest) - ImGui.TextColored(color, icon.ToIconString()); - else - ImGui.TextColored(ImGuiColors.DalamudGrey, icon.ToIconString()); + if (ImGui.IsItemHovered()) + { + using var tooltip = ImRaii.Tooltip(); + if (tooltip) + { + ImGui.TextColored(color, tooltipText); + if (quest.IsRepeatable) + { + ImGui.SameLine(); + ImGui.TextUnformatted("Repeatable"); + } + + if (!_questRegistry.IsKnownQuest(quest.QuestId)) + { + ImGui.SameLine(); + ImGui.TextUnformatted("NoQuestPath"); + } + + if (quest.PreviousQuests.Count > 0) + { + ImGui.Separator(); + DrawQuestUnlocks(quest, 0); + } + } + } } if (ImGui.TableNextColumn()) @@ -165,7 +200,9 @@ internal sealed class QuestSelectionWindow : LWindow ImGui.SameLine(); - if (knownQuest != null && !_gameFunctions.IsQuestAccepted(quest.QuestId) && + if (knownQuest != null && + !_gameFunctions.IsQuestAccepted(quest.QuestId) && + !_gameFunctions.IsQuestLocked(quest.QuestId) && (quest.IsRepeatable || !_gameFunctions.IsQuestAcceptedOrComplete(quest.QuestId))) { ImGui.BeginDisabled(_questController.NextQuest != null || _questController.SimulatedQuest != null); @@ -199,4 +236,97 @@ internal sealed class QuestSelectionWindow : LWindow ImGui.SetClipboardText(fileName); _chatGui.Print($"Copied '{fileName}' to clipboard"); } + + private (Vector4 color, FontAwesomeIcon icon, string status) GetQuestStyle(ushort questId) + { + if (_gameFunctions.IsQuestAccepted(questId)) + return (ImGuiColors.DalamudYellow, FontAwesomeIcon.Running, "Active"); + else if (_gameFunctions.IsQuestAcceptedOrComplete(questId)) + return (ImGuiColors.ParsedGreen, FontAwesomeIcon.Check, "Complete"); + else if (_gameFunctions.IsQuestLocked(questId)) + return (ImGuiColors.DalamudRed, FontAwesomeIcon.Times, "Locked"); + else + return (ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight, "Available"); + } + + private void DrawQuestUnlocks(QuestInfo quest, int counter) + { + if (counter >= 10) + return; + + if (counter != 0 && quest.IsMainScenarioQuest) + return; + + if (counter > 0) + ImGui.Indent(); + + if (quest.PreviousQuests.Count > 0) + { + if (quest.PreviousQuests.Count > 1) + { + if (quest.PreviousQuestJoin == QuestInfo.QuestJoin.All) + ImGui.Text("Requires all:"); + else if (quest.PreviousQuestJoin == QuestInfo.QuestJoin.AtLeastOne) + ImGui.Text("Requires one:"); + } + + foreach (var q in quest.PreviousQuests) + { + var qInfo = _questData.GetQuestInfo(q); + var (iconColor, icon, _) = GetQuestStyle(q); + using (var font = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) + { + if (_questRegistry.IsKnownQuest(qInfo.QuestId)) + ImGui.TextColored(iconColor, icon.ToIconString()); + else + ImGui.TextColored(ImGuiColors.DalamudGrey, icon.ToIconString()); + } + + ImGui.SameLine(); + ImGui.TextUnformatted(FormatQuestUnlockName(qInfo)); + + DrawQuestUnlocks(qInfo, counter + 1); + } + } + + if (counter == 0 && quest.QuestLocks.Count > 0) + { + if (quest.QuestLocks.Count > 1) + { + if (quest.QuestLockJoin == QuestInfo.QuestJoin.All) + ImGui.Text("Blocked if all completed:"); + else if (quest.QuestLockJoin == QuestInfo.QuestJoin.AtLeastOne) + ImGui.Text("Blocked if at least completed:"); + } + else + ImGui.Text("Blocked by (if completed):"); + + foreach (var q in quest.QuestLocks) + { + var qInfo = _questData.GetQuestInfo(q); + var (iconColor, icon, _) = GetQuestStyle(q); + using (var font = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) + { + if (_questRegistry.IsKnownQuest(qInfo.QuestId)) + ImGui.TextColored(iconColor, icon.ToIconString()); + else + ImGui.TextColored(ImGuiColors.DalamudGrey, icon.ToIconString()); + } + + ImGui.SameLine(); + ImGui.TextUnformatted(FormatQuestUnlockName(qInfo)); + } + } + + if (counter > 0) + ImGui.Unindent(); + } + + private static string FormatQuestUnlockName(QuestInfo questInfo) + { + if (questInfo.IsMainScenarioQuest) + return $"{questInfo.Name} ({questInfo.QuestId}, MSQ)"; + else + return $"{questInfo.Name} ({questInfo.QuestId})"; + } } diff --git a/Questionable/Windows/QuestWindow.cs b/Questionable/Windows/QuestWindow.cs index 573b90eb..69793cc2 100644 --- a/Questionable/Windows/QuestWindow.cs +++ b/Questionable/Windows/QuestWindow.cs @@ -452,7 +452,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig if (ImGui.IsItemHovered()) ImGui.SetTooltip("Show all Quests starting with your current target."); if (showQuests) - _questSelectionWindow.Open(_targetManager.Target); + _questSelectionWindow.OpenForTarget(_targetManager.Target); ImGui.EndDisabled(); From d20a7689962a5ebe4019b584910916586045ac53 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Mon, 15 Jul 2024 21:38:38 +0200 Subject: [PATCH 03/11] Add fallback for missing versper bay aetheryte tickets --- .../Controller/Steps/Interactions/UseItem.cs | 32 +++++++- .../Steps/Shared/AetheryteShortcut.cs | 6 +- Questionable/Controller/Steps/Shared/Move.cs | 75 +++++++++++++------ 3 files changed, 87 insertions(+), 26 deletions(-) diff --git a/Questionable/Controller/Steps/Interactions/UseItem.cs b/Questionable/Controller/Steps/Interactions/UseItem.cs index 05dbd72e..497ae617 100644 --- a/Questionable/Controller/Steps/Interactions/UseItem.cs +++ b/Questionable/Controller/Steps/Interactions/UseItem.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; +using System.Numerics; using FFXIVClientStructs.FFXIV.Client.Game; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps.Common; +using Questionable.Controller.Steps.Shared; using Questionable.Model; using Questionable.Model.V1; +using AethernetShortcut = Questionable.Controller.Steps.Shared.AethernetShortcut; namespace Questionable.Controller.Steps.Interactions; @@ -13,7 +16,7 @@ internal static class UseItem { public const int VesperBayAetheryteTicket = 30362; - internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory + internal sealed class Factory(IServiceProvider serviceProvider, ILogger logger) : ITaskFactory { public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) { @@ -22,6 +25,16 @@ internal static class UseItem ArgumentNullException.ThrowIfNull(step.ItemId); + if (step.ItemId == VesperBayAetheryteTicket) + { + unsafe + { + InventoryManager* inventoryManager = InventoryManager.Instance(); + if (inventoryManager->GetInventoryItemCount(step.ItemId.Value) == 0) + return CreateVesperBayFallbackTask(); + } + } + var unmount = serviceProvider.GetRequiredService(); if (step.GroundTarget == true) { @@ -47,6 +60,23 @@ internal static class UseItem public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step) => throw new InvalidOperationException(); + + private IEnumerable CreateVesperBayFallbackTask() + { + logger.LogWarning("No vesper bay aetheryte tickets in inventory, navigating via ferry in Limsa instead"); + + uint npcId = 1003540; + ushort territoryId = 129; + Vector3 destination = new(-360.9217f, 8f, 38.92566f); + yield return serviceProvider.GetRequiredService() + .With(null, EAetheryteLocation.Limsa, territoryId); + yield return serviceProvider.GetRequiredService() + .With(EAetheryteLocation.Limsa, EAetheryteLocation.LimsaArcanist); + yield return serviceProvider.GetRequiredService() + .With(territoryId, destination, dataId: npcId, sprint: false); + yield return serviceProvider.GetRequiredService() + .With(npcId, true); + } } internal abstract class UseItemBase(ILogger logger) : ITask diff --git a/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs b/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs index 2f645ea6..f897fe64 100644 --- a/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs +++ b/Questionable/Controller/Steps/Shared/AetheryteShortcut.cs @@ -45,7 +45,7 @@ internal static class AetheryteShortcut { private DateTime _continueAt; - public QuestStep Step { get; set; } = null!; + public QuestStep? Step { get; set; } public EAetheryteLocation TargetAetheryte { get; set; } /// @@ -54,7 +54,7 @@ internal static class AetheryteShortcut /// public ushort ExpectedTerritoryId { get; set; } - public ITask With(QuestStep step, EAetheryteLocation targetAetheryte, ushort expectedTerritoryId) + public ITask With(QuestStep? step, EAetheryteLocation targetAetheryte, ushort expectedTerritoryId) { Step = step; TargetAetheryte = targetAetheryte; @@ -66,7 +66,7 @@ internal static class AetheryteShortcut { _continueAt = DateTime.Now.AddSeconds(8); ushort territoryType = clientState.TerritoryType; - if (ExpectedTerritoryId == territoryType) + if (Step != null && ExpectedTerritoryId == territoryType) { if (Step.SkipIf.Contains(ESkipCondition.AetheryteShortcutIfInSameTerritory)) { diff --git a/Questionable/Controller/Steps/Shared/Move.cs b/Questionable/Controller/Steps/Shared/Move.cs index d6da1491..75fd691a 100644 --- a/Questionable/Controller/Steps/Shared/Move.cs +++ b/Questionable/Controller/Steps/Shared/Move.cs @@ -99,15 +99,7 @@ internal static class Move if (actualDistance > distance) { yield return serviceProvider.GetRequiredService() - .With(Destination, m => - { - m.NavigateTo(EMovementType.Quest, Step.DataId, Destination, - fly: Step.Fly == true && gameFunctions.IsFlyingUnlocked(Step.TerritoryId), - sprint: Step.Sprint != false, - stopDistance: distance, - ignoreDistanceToObject: Step.IgnoreDistanceToObject == true, - land: Step.Land == true); - }); + .With(Step, Destination); } } else @@ -116,14 +108,7 @@ internal static class Move if (actualDistance > distance) { yield return serviceProvider.GetRequiredService() - .With(Destination, m => - { - m.NavigateTo(EMovementType.Quest, Step.DataId, [Destination], - fly: Step.Fly == true && gameFunctions.IsFlyingUnlockedInCurrentZone(), - sprint: Step.Sprint != false, - stopDistance: distance, - land: Step.Land == true); - }); + .With(Step, Destination); } } @@ -132,22 +117,68 @@ internal static class Move } } - internal sealed class MoveInternal(MovementController movementController, ILogger logger) : ITask + internal sealed class MoveInternal( + MovementController movementController, + GameFunctions gameFunctions, + ILogger logger) : ITask { - public Action StartAction { get; set; } = null!; + public Action StartAction { get; set; } = null!; public Vector3 Destination { get; set; } - public ITask With(Vector3 destination, Action startAction) + public ITask With(QuestStep step, Vector3 destination) + { + return With( + territoryId: step.TerritoryId, + destination: destination, + stopDistance: step.StopDistance, + dataId: step.DataId, + disableNavMesh: step.DisableNavmesh, + sprint: step.Sprint != false, + fly: step.Fly == true, + land: step.Land == true, + ignoreDistanceToObject: step.IgnoreDistanceToObject == true); + } + + public ITask With(ushort territoryId, Vector3 destination, float? stopDistance = null, uint? dataId = null, + bool disableNavMesh = false, bool sprint = true, bool fly = false, bool land = false, + bool ignoreDistanceToObject = false) { Destination = destination; - StartAction = startAction; + + if (!gameFunctions.IsFlyingUnlocked(territoryId)) + { + fly = false; + land = false; + } + + if (!disableNavMesh) + { + StartAction = () => + movementController.NavigateTo(EMovementType.Quest, dataId, Destination, + fly: fly, + sprint: sprint, + stopDistance: stopDistance, + ignoreDistanceToObject: ignoreDistanceToObject, + land: land); + } + else + { + StartAction = () => + movementController.NavigateTo(EMovementType.Quest, dataId, [Destination], + fly: fly, + sprint: sprint, + stopDistance: stopDistance, + ignoreDistanceToObject: ignoreDistanceToObject, + land: land); + } + return this; } public bool Start() { logger.LogInformation("Moving to {Destination}", Destination.ToString("G", CultureInfo.InvariantCulture)); - StartAction(movementController); + StartAction(); return true; } From 202abcf3a805039d712ddedf31909bdfabfb5b0b Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Mon, 15 Jul 2024 23:09:09 +0200 Subject: [PATCH 04/11] Add a (dev) option to not load certain quests --- QuestPathGenerator/QuestSourceGenerator.cs | 3 +++ QuestPaths/quest-v1.json | 3 +++ Questionable.Model/V1/QuestRoot.cs | 1 + Questionable/Controller/QuestRegistry.cs | 13 +++++++++++-- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/QuestPathGenerator/QuestSourceGenerator.cs b/QuestPathGenerator/QuestSourceGenerator.cs index de026fc0..c5837885 100644 --- a/QuestPathGenerator/QuestSourceGenerator.cs +++ b/QuestPathGenerator/QuestSourceGenerator.cs @@ -79,6 +79,9 @@ public class QuestSourceGenerator : ISourceGenerator } var quest = questNode.Deserialize()!; + if (quest.Disabled) + continue; + quests.Add((id, quest)); } diff --git a/QuestPaths/quest-v1.json b/QuestPaths/quest-v1.json index 04b2a1fd..d22e38bf 100644 --- a/QuestPaths/quest-v1.json +++ b/QuestPaths/quest-v1.json @@ -19,6 +19,9 @@ "type": "string" } }, + "Disabled": { + "type": "boolean" + }, "Comment": { "type": "string" }, diff --git a/Questionable.Model/V1/QuestRoot.cs b/Questionable.Model/V1/QuestRoot.cs index 880e34ce..60693898 100644 --- a/Questionable.Model/V1/QuestRoot.cs +++ b/Questionable.Model/V1/QuestRoot.cs @@ -6,6 +6,7 @@ public sealed class QuestRoot { public string Author { get; set; } = null!; public List Contributors { get; set; } = new(); + public bool Disabled { get; set; } public string? Comment { get; set; } public List TerritoryBlacklist { get; set; } = new(); public List QuestSequence { get; set; } = new(); diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs index dc656fbc..96d5c847 100644 --- a/Questionable/Controller/QuestRegistry.cs +++ b/Questionable/Controller/QuestRegistry.cs @@ -130,7 +130,8 @@ internal sealed class QuestRegistry var distinctSequenceCount = quest.Root.QuestSequence.Select(x => x.Sequence).Distinct().Count(); if (totalSequenceCount != distinctSequenceCount) { - _logger.LogWarning("Quest has duplicate sequence numbers: {QuestId} / {QuestName}", quest.QuestId, + _logger.LogWarning("Quest has duplicate sequence numbers: {QuestId} / {QuestName}", + quest.QuestId, quest.Info.Name); ++foundProblems; } @@ -196,7 +197,8 @@ internal sealed class QuestRegistry { _logger.LogWarning( "Quest step has duplicate completion flags: {QuestId} / {QuestName} → {Sequence} → {Flags}", - quest.QuestId, quest.Info.Name, sequence.Sequence, string.Join(", ", duplicate.First())); + quest.QuestId, quest.Info.Name, sequence.Sequence, + string.Join(", ", duplicate.First())); ++foundProblems; } } @@ -231,6 +233,13 @@ internal sealed class QuestRegistry Root = JsonSerializer.Deserialize(stream)!, Info = _questData.GetQuestInfo(questId.Value), }; + if (quest.Root.Disabled) + { + _logger.LogWarning("Quest {QuestId} / {QuestName} is disabled and won't be loaded", questId, + quest.Info.Name); + return; + } + _quests[questId.Value] = quest; } From db618786832558bb77d66ff436cdd5a6d022ec8b Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Tue, 16 Jul 2024 00:18:10 +0200 Subject: [PATCH 05/11] Clean up quest validation --- Questionable/Controller/QuestRegistry.cs | 126 ++---------------- Questionable/DalamudInitializer.cs | 4 +- Questionable/GameFunctions.cs | 2 + Questionable/QuestionablePlugin.cs | 9 ++ Questionable/Validation/EIssueSeverity.cs | 7 + Questionable/Validation/IQuestValidator.cs | 9 ++ Questionable/Validation/QuestValidator.cs | 55 ++++++++ Questionable/Validation/ValidationIssue.cs | 10 ++ .../Validators/BasicSequenceValidator.cs | 79 +++++++++++ .../Validators/CompletionFlagsValidator.cs | 54 ++++++++ .../Validators/QuestDisabledValidator.cs | 22 +++ .../Validators/UniqueStartStopValidator.cs | 85 ++++++++++++ Questionable/Windows/QuestSelectionWindow.cs | 2 +- Questionable/Windows/QuestValidationWindow.cs | 69 ++++++++++ Questionable/Windows/QuestWindow.cs | 24 +++- 15 files changed, 435 insertions(+), 122 deletions(-) create mode 100644 Questionable/Validation/EIssueSeverity.cs create mode 100644 Questionable/Validation/IQuestValidator.cs create mode 100644 Questionable/Validation/QuestValidator.cs create mode 100644 Questionable/Validation/ValidationIssue.cs create mode 100644 Questionable/Validation/Validators/BasicSequenceValidator.cs create mode 100644 Questionable/Validation/Validators/CompletionFlagsValidator.cs create mode 100644 Questionable/Validation/Validators/QuestDisabledValidator.cs create mode 100644 Questionable/Validation/Validators/UniqueStartStopValidator.cs create mode 100644 Questionable/Windows/QuestValidationWindow.cs diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs index 96d5c847..d836b21e 100644 --- a/Questionable/Controller/QuestRegistry.cs +++ b/Questionable/Controller/QuestRegistry.cs @@ -15,6 +15,7 @@ using Questionable.Controller.Utils; using Questionable.Data; using Questionable.Model; using Questionable.Model.V1; +using Questionable.Validation; namespace Questionable.Controller; @@ -23,21 +24,24 @@ internal sealed class QuestRegistry private readonly IDalamudPluginInterface _pluginInterface; private readonly QuestData _questData; private readonly IChatGui _chatGui; + private readonly QuestValidator _questValidator; private readonly ILogger _logger; private readonly Dictionary _quests = new(); public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData, IChatGui chatGui, - ILogger logger) + QuestValidator questValidator, ILogger logger) { _pluginInterface = pluginInterface; _questData = questData; _chatGui = chatGui; + _questValidator = questValidator; _logger = logger; } public IEnumerable AllQuests => _quests.Values; public int Count => _quests.Count; + public int ValidationIssueCount => _questValidator.IssueCount; public void Reload() { @@ -110,113 +114,8 @@ internal sealed class QuestRegistry [Conditional("DEBUG")] private void ValidateQuests() { - Task.Run(() => - { - try - { - int foundProblems = 0; - foreach (var quest in _quests.Values) - { - int missingSteps = quest.Root.QuestSequence.Where(x => x.Sequence < 255).Max(x => x.Sequence) - - quest.Root.QuestSequence.Count(x => x.Sequence < 255) + 1; - if (missingSteps != 0) - { - _logger.LogWarning("Quest has missing steps: {QuestId} / {QuestName} → {Count}", quest.QuestId, - quest.Info.Name, missingSteps); - ++foundProblems; - } - - var totalSequenceCount = quest.Root.QuestSequence.Count; - var distinctSequenceCount = quest.Root.QuestSequence.Select(x => x.Sequence).Distinct().Count(); - if (totalSequenceCount != distinctSequenceCount) - { - _logger.LogWarning("Quest has duplicate sequence numbers: {QuestId} / {QuestName}", - quest.QuestId, - quest.Info.Name); - ++foundProblems; - } - - foreach (var sequence in quest.Root.QuestSequence) - { - if (sequence.Sequence == 0 && - sequence.Steps.LastOrDefault()?.InteractionType != EInteractionType.AcceptQuest) - { - _logger.LogWarning( - "Quest likely has AcceptQuest configured wrong: {QuestId} / {QuestName} → {Sequence} / {Step}", - quest.QuestId, quest.Info.Name, sequence.Sequence, sequence.Steps.Count - 1); - ++foundProblems; - } - else if (sequence.Sequence == 255 && - sequence.Steps.LastOrDefault()?.InteractionType != EInteractionType.CompleteQuest) - { - _logger.LogWarning( - "Quest likely has CompleteQuest configured wrong: {QuestId} / {QuestName} → {Sequence} / {Step}", - quest.QuestId, quest.Info.Name, sequence.Sequence, sequence.Steps.Count - 1); - ++foundProblems; - } - - - var acceptQuestSteps = sequence.Steps - .Where(x => x is { InteractionType: EInteractionType.AcceptQuest, PickupQuestId: null }) - .Where(x => sequence.Sequence != 0 || x != sequence.Steps.Last()); - foreach (var step in acceptQuestSteps) - { - _logger.LogWarning( - "Quest has unexpected AcceptQuest steps: {QuestId} / {QuestName} → {Sequence} / {Step}", - quest.QuestId, quest.Info.Name, sequence.Sequence, sequence.Steps.IndexOf(step)); - ++foundProblems; - } - - var completeQuestSteps = sequence.Steps - .Where(x => x is { InteractionType: EInteractionType.CompleteQuest, TurnInQuestId: null }) - .Where(x => sequence.Sequence != 255 || x != sequence.Steps.Last()); - foreach (var step in completeQuestSteps) - { - _logger.LogWarning( - "Quest has unexpected CompleteQuest steps: {QuestId} / {QuestName} → {Sequence} / {Step}", - quest.QuestId, quest.Info.Name, sequence.Sequence, sequence.Steps.IndexOf(step)); - ++foundProblems; - } - - var completionFlags = sequence.Steps.Select(x => x.CompletionQuestVariablesFlags) - .Where(QuestWorkUtils.HasCompletionFlags) - .GroupBy(x => - { - return Enumerable.Range(0, 6).Select(y => - { - short? value = x[y]; - if (value == null || value.Value < 0) - return (long)0; - return (long)BitOperations.RotateLeft((ulong)value.Value, 8 * y); - }) - .Sum(); - }) - .Where(x => x.Key != 0) - .Where(x => x.Count() > 1); - foreach (var duplicate in completionFlags) - { - _logger.LogWarning( - "Quest step has duplicate completion flags: {QuestId} / {QuestName} → {Sequence} → {Flags}", - quest.QuestId, quest.Info.Name, sequence.Sequence, - string.Join(", ", duplicate.First())); - ++foundProblems; - } - } - } - - if (foundProblems > 0) - { - _chatGui.Print( - $"[Questionable] Quest validation has found {foundProblems} problems. Check the log for details."); - } - } - catch (Exception e) - { - _logger.LogError(e, "Unable to validate quests"); - _chatGui.PrintError( - $"[Questionable] Unable to validate quests. Check the log for details."); - } - }); + _questValidator.ClearIssues(); + _questValidator.Validate(_quests.Values); } @@ -233,13 +132,6 @@ internal sealed class QuestRegistry Root = JsonSerializer.Deserialize(stream)!, Info = _questData.GetQuestInfo(questId.Value), }; - if (quest.Root.Disabled) - { - _logger.LogWarning("Quest {QuestId} / {QuestName} is disabled and won't be loaded", questId, - quest.Info.Name); - return; - } - _quests[questId.Value] = quest; } @@ -281,8 +173,8 @@ internal sealed class QuestRegistry return ushort.Parse(parts[0], CultureInfo.InvariantCulture); } - public bool IsKnownQuest(ushort questId) => _quests.ContainsKey(questId); + public bool IsKnownQuest(ushort questId) => TryGetQuest(questId, out _); public bool TryGetQuest(ushort questId, [NotNullWhen(true)] out Quest? quest) - => _quests.TryGetValue(questId, out quest); + => _quests.TryGetValue(questId, out quest) && !quest.Root.Disabled; } diff --git a/Questionable/DalamudInitializer.cs b/Questionable/DalamudInitializer.cs index fa2cfed8..d2cf7af4 100644 --- a/Questionable/DalamudInitializer.cs +++ b/Questionable/DalamudInitializer.cs @@ -29,7 +29,8 @@ internal sealed class DalamudInitializer : IDisposable QuestWindow questWindow, DebugOverlay debugOverlay, ConfigWindow configWindow, - QuestSelectionWindow questSelectionWindow) + QuestSelectionWindow questSelectionWindow, + QuestValidationWindow questValidationWindow) { _pluginInterface = pluginInterface; _framework = framework; @@ -44,6 +45,7 @@ internal sealed class DalamudInitializer : IDisposable _windowSystem.AddWindow(configWindow); _windowSystem.AddWindow(debugOverlay); _windowSystem.AddWindow(questSelectionWindow); + _windowSystem.AddWindow(questValidationWindow); _pluginInterface.UiBuilder.Draw += _windowSystem.Draw; _pluginInterface.UiBuilder.OpenMainUi += _questWindow.Toggle; diff --git a/Questionable/GameFunctions.cs b/Questionable/GameFunctions.cs index f25cec48..7bca4241 100644 --- a/Questionable/GameFunctions.cs +++ b/Questionable/GameFunctions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using Dalamud.Game.ClientState.Conditions; @@ -260,6 +261,7 @@ internal sealed unsafe class GameFunctions return questManager->IsQuestAccepted(questId); } + [SuppressMessage("Performance", "CA1822")] public bool IsQuestComplete(ushort questId) { return QuestManager.IsQuestComplete(questId); diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index 9647fc3b..39c370d0 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -16,6 +16,8 @@ using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Interactions; using Questionable.Data; using Questionable.External; +using Questionable.Validation; +using Questionable.Validation.Validators; using Questionable.Windows; using Action = Questionable.Controller.Steps.Interactions.Action; @@ -128,6 +130,13 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Questionable/Validation/EIssueSeverity.cs b/Questionable/Validation/EIssueSeverity.cs new file mode 100644 index 00000000..25f42539 --- /dev/null +++ b/Questionable/Validation/EIssueSeverity.cs @@ -0,0 +1,7 @@ +namespace Questionable.Validation; + +internal enum EIssueSeverity +{ + None, + Error, +} diff --git a/Questionable/Validation/IQuestValidator.cs b/Questionable/Validation/IQuestValidator.cs new file mode 100644 index 00000000..02a5f182 --- /dev/null +++ b/Questionable/Validation/IQuestValidator.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using Questionable.Model; + +namespace Questionable.Validation; + +internal interface IQuestValidator +{ + IEnumerable Validate(Quest quest); +} diff --git a/Questionable/Validation/QuestValidator.cs b/Questionable/Validation/QuestValidator.cs new file mode 100644 index 00000000..83bb9d42 --- /dev/null +++ b/Questionable/Validation/QuestValidator.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Questionable.Model; + +namespace Questionable.Validation; + +internal sealed class QuestValidator +{ + private readonly IReadOnlyList _validators; + private readonly ILogger _logger; + + private List _validationIssues = new(); + + public QuestValidator(IEnumerable validators, ILogger logger) + { + _validators = validators.ToList(); + _logger = logger; + + _logger.LogInformation("Validators: {Validators}", + string.Join(", ", _validators.Select(x => x.GetType().Name))); + } + + public IReadOnlyList Issues => _validationIssues; + public int IssueCount => _validationIssues.Count; + + public void ClearIssues() => _validationIssues.Clear(); + + public void Validate(IReadOnlyCollection quests) + { + Task.Run(() => + { + foreach (var quest in quests) + { + foreach (var validator in _validators) + { + foreach (var issue in validator.Validate(quest)) + { + _logger.LogWarning( + "Validation failed: {QuestId} ({QuestName}) / {QuestSequence} / {QuestStep} - {Description}", + issue.QuestId, quest.Info.Name, issue.Sequence, issue.Step, issue.Description); + _validationIssues.Add(issue); + } + } + } + + _validationIssues = _validationIssues.OrderBy(x => x.QuestId) + .ThenBy(x => x.Sequence) + .ThenBy(x => x.Step) + .ThenBy(x => x.Description) + .ToList(); + }); + } +} diff --git a/Questionable/Validation/ValidationIssue.cs b/Questionable/Validation/ValidationIssue.cs new file mode 100644 index 00000000..6e842760 --- /dev/null +++ b/Questionable/Validation/ValidationIssue.cs @@ -0,0 +1,10 @@ +namespace Questionable.Validation; + +internal sealed record ValidationIssue +{ + public required ushort QuestId { get; init; } + public required byte? Sequence { get; init; } + public required int? Step { get; init; } + public required EIssueSeverity Severity { get; init; } + public required string Description { get; init; } +} diff --git a/Questionable/Validation/Validators/BasicSequenceValidator.cs b/Questionable/Validation/Validators/BasicSequenceValidator.cs new file mode 100644 index 00000000..8ea19a14 --- /dev/null +++ b/Questionable/Validation/Validators/BasicSequenceValidator.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Linq; +using Questionable.Model; +using Questionable.Model.V1; + +namespace Questionable.Validation.Validators; + +internal sealed class BasicSequenceValidator : IQuestValidator +{ + /// + /// A quest should have sequences from 0 to N, and (if more than 'AcceptQuest' exists), a 255 sequence. + /// + public IEnumerable Validate(Quest quest) + { + var sequences = quest.Root.QuestSequence; + var foundStart = sequences.FirstOrDefault(x => x.Sequence == 0); + if (foundStart == null) + { + yield return new ValidationIssue + { + QuestId = quest.QuestId, + Sequence = 0, + Step = null, + Severity = EIssueSeverity.Error, + Description = "Missing quest start", + }; + yield break; + } + + int maxSequence = sequences.Select(x => x.Sequence) + .Where(x => x != 255) + .Max(); + + for (int i = 0; i < maxSequence; i++) + { + var foundSequences = sequences.Where(x => x.Sequence == i).ToList(); + var issue = ValidateSequences(quest, i, foundSequences); + if (issue != null) + yield return issue; + } + + // some quests finish instantly + if (maxSequence > 0 || foundStart.Steps.Count > 1) + { + var foundEnding = sequences.Where(x => x.Sequence == 255).ToList(); + var endingIssue = ValidateSequences(quest, 255, foundEnding); + if (endingIssue != null) + yield return endingIssue; + } + } + + private static ValidationIssue? ValidateSequences(Quest quest, int sequenceNo, List foundSequences) + { + if (foundSequences.Count == 0) + { + return new ValidationIssue + { + QuestId = quest.QuestId, + Sequence = (byte)sequenceNo, + Step = null, + Severity = EIssueSeverity.Error, + Description = "Missing sequence", + }; + } + else if (foundSequences.Count == 2) + { + return new ValidationIssue + { + QuestId = quest.QuestId, + Sequence = (byte)sequenceNo, + Step = null, + Severity = EIssueSeverity.Error, + Description = "Duplicate sequence", + }; + } + else + return null; + } +} diff --git a/Questionable/Validation/Validators/CompletionFlagsValidator.cs b/Questionable/Validation/Validators/CompletionFlagsValidator.cs new file mode 100644 index 00000000..f1ea4c02 --- /dev/null +++ b/Questionable/Validation/Validators/CompletionFlagsValidator.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Questionable.Controller.Utils; +using Questionable.Model; + +namespace Questionable.Validation.Validators; + +internal sealed class CompletionFlagsValidator : IQuestValidator +{ + public IEnumerable Validate(Quest quest) + { + foreach (var sequence in quest.Root.QuestSequence) + { + var mappedCompletionFlags = sequence.Steps + .Select(x => + { + if (QuestWorkUtils.HasCompletionFlags(x.CompletionQuestVariablesFlags)) + { + return Enumerable.Range(0, 6).Select(y => + { + short? value = x.CompletionQuestVariablesFlags[y]; + if (value == null || value.Value < 0) + return 0; + return (long)BitOperations.RotateLeft((ulong)value.Value, 8 * y); + }) + .Sum(); + } + else + return 0; + }) + .ToList(); + + for (int i = 0; i < sequence.Steps.Count; ++i) + { + var flags = mappedCompletionFlags[i]; + if (flags == 0) + continue; + + if (mappedCompletionFlags.Count(x => x == flags) >= 2) + { + yield return new ValidationIssue + { + QuestId = quest.QuestId, + Sequence = (byte)sequence.Sequence, + Step = i, + Severity = EIssueSeverity.Error, + Description = $"Duplicate completion flags: {string.Join(", ", sequence.Steps[i].CompletionQuestVariablesFlags)}", + }; + } + } + } + } +} diff --git a/Questionable/Validation/Validators/QuestDisabledValidator.cs b/Questionable/Validation/Validators/QuestDisabledValidator.cs new file mode 100644 index 00000000..096fd375 --- /dev/null +++ b/Questionable/Validation/Validators/QuestDisabledValidator.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Questionable.Model; + +namespace Questionable.Validation.Validators; + +internal sealed class QuestDisabledValidator : IQuestValidator +{ + public IEnumerable Validate(Quest quest) + { + if (quest.Root.Disabled) + { + yield return new ValidationIssue + { + QuestId = quest.QuestId, + Sequence = null, + Step = null, + Severity = EIssueSeverity.None, + Description = "Quest is disabled", + }; + } + } +} diff --git a/Questionable/Validation/Validators/UniqueStartStopValidator.cs b/Questionable/Validation/Validators/UniqueStartStopValidator.cs new file mode 100644 index 00000000..83c350a5 --- /dev/null +++ b/Questionable/Validation/Validators/UniqueStartStopValidator.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Linq; +using Questionable.Model; +using Questionable.Model.V1; + +namespace Questionable.Validation.Validators; + +internal sealed class UniqueStartStopValidator : IQuestValidator +{ + public IEnumerable Validate(Quest quest) + { + var questAccepts = FindQuestStepsWithInteractionType(quest, EInteractionType.AcceptQuest) + .Where(x => x.Step.PickupQuestId == null) + .ToList(); + foreach (var accept in questAccepts) + { + if (accept.SequenceId != 0 || accept.StepId != quest.FindSequence(0)!.Steps.Count - 1) + { + yield return new ValidationIssue + { + QuestId = quest.QuestId, + Sequence = (byte)accept.SequenceId, + Step = accept.StepId, + Severity = EIssueSeverity.Error, + Description = "Unexpected AcceptQuest step", + }; + } + } + + if (quest.FindSequence(0) != null && questAccepts.Count == 0) + { + yield return new ValidationIssue + { + QuestId = quest.QuestId, + Sequence = 0, + Step = null, + Severity = EIssueSeverity.Error, + Description = "No AcceptQuest step", + }; + } + + var questCompletes = FindQuestStepsWithInteractionType(quest, EInteractionType.CompleteQuest) + .Where(x => x.Step.TurnInQuestId == null) + .ToList(); + foreach (var complete in questCompletes) + { + if (complete.SequenceId != 255 || complete.StepId != quest.FindSequence(255)!.Steps.Count - 1) + { + yield return new ValidationIssue + { + QuestId = quest.QuestId, + Sequence = (byte)complete.SequenceId, + Step = complete.StepId, + Severity = EIssueSeverity.Error, + Description = "Unexpected CompleteQuest step", + }; + } + } + + if (quest.FindSequence(255) != null && questCompletes.Count == 0) + { + yield return new ValidationIssue + { + QuestId = quest.QuestId, + Sequence = 255, + Step = null, + Severity = EIssueSeverity.Error, + Description = "No CompleteQuest step", + }; + } + } + + private static IEnumerable<(int SequenceId, int StepId, QuestStep Step)> FindQuestStepsWithInteractionType(Quest quest, EInteractionType interactionType) + { + foreach (var sequence in quest.Root.QuestSequence) + { + for (int i = 0; i < sequence.Steps.Count; ++i) + { + var step = sequence.Steps[i]; + if (step.InteractionType == interactionType) + yield return (sequence.Sequence, i, step); + } + } + } +} diff --git a/Questionable/Windows/QuestSelectionWindow.cs b/Questionable/Windows/QuestSelectionWindow.cs index 8e0bd971..55d81bb5 100644 --- a/Questionable/Windows/QuestSelectionWindow.cs +++ b/Questionable/Windows/QuestSelectionWindow.cs @@ -105,7 +105,7 @@ internal sealed class QuestSelectionWindow : LWindow public override void OnClose() { TargetId = default; - TargetName = default; + TargetName = string.Empty; _quests = []; _offeredQuests = []; } diff --git a/Questionable/Windows/QuestValidationWindow.cs b/Questionable/Windows/QuestValidationWindow.cs new file mode 100644 index 00000000..2cb3f5b2 --- /dev/null +++ b/Questionable/Windows/QuestValidationWindow.cs @@ -0,0 +1,69 @@ +using System.Globalization; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; +using Dalamud.Interface.Utility.Raii; +using FFXIVClientStructs.FFXIV.Common.Math; +using ImGuiNET; +using LLib.ImGui; +using Questionable.Data; +using Questionable.Model; +using Questionable.Validation; + +namespace Questionable.Windows; + +internal sealed class QuestValidationWindow : LWindow +{ + private readonly QuestValidator _questValidator; + private readonly QuestData _questData; + + public QuestValidationWindow(QuestValidator questValidator, QuestData questData) : base("Quest Validation###QuestionableValidator") + { + _questValidator = questValidator; + _questData = questData; + + Size = new Vector2(600, 200); + SizeCondition = ImGuiCond.Once; + SizeConstraints = new WindowSizeConstraints + { + MinimumSize = new Vector2(600, 200), + }; + } + + public override void Draw() + { + using var table = ImRaii.Table("QuestSelection", 5, ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY); + if (!table) + { + ImGui.Text("Not table"); + return; + } + + ImGui.TableSetupColumn("Quest", ImGuiTableColumnFlags.WidthFixed, 50); + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 200); + ImGui.TableSetupColumn("Sq", ImGuiTableColumnFlags.WidthFixed, 30); + ImGui.TableSetupColumn("Sp", ImGuiTableColumnFlags.WidthFixed, 30); + ImGui.TableSetupColumn("Issue", ImGuiTableColumnFlags.None, 200); + ImGui.TableHeadersRow(); + + foreach (ValidationIssue validationIssue in _questValidator.Issues) + { + ImGui.TableNextRow(); + + if (ImGui.TableNextColumn()) + ImGui.TextUnformatted(validationIssue.QuestId.ToString(CultureInfo.InvariantCulture)); + + if (ImGui.TableNextColumn()) + ImGui.TextUnformatted(_questData.GetQuestInfo(validationIssue.QuestId).Name); + + if (ImGui.TableNextColumn()) + ImGui.TextUnformatted(validationIssue.Sequence?.ToString(CultureInfo.InvariantCulture) ?? string.Empty); + + if (ImGui.TableNextColumn()) + ImGui.TextUnformatted(validationIssue.Step?.ToString(CultureInfo.InvariantCulture) ?? string.Empty); + + if (ImGui.TableNextColumn()) + ImGui.TextUnformatted(validationIssue.Description); + } + } +} diff --git a/Questionable/Windows/QuestWindow.cs b/Questionable/Windows/QuestWindow.cs index 69793cc2..517d8894 100644 --- a/Questionable/Windows/QuestWindow.cs +++ b/Questionable/Windows/QuestWindow.cs @@ -48,6 +48,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig private readonly ICondition _condition; private readonly IGameGui _gameGui; private readonly QuestSelectionWindow _questSelectionWindow; + private readonly QuestValidationWindow _questValidationWindow; private readonly ILogger _logger; public QuestWindow(IDalamudPluginInterface pluginInterface, @@ -68,6 +69,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig ICondition condition, IGameGui gameGui, QuestSelectionWindow questSelectionWindow, + QuestValidationWindow questValidationWindow, ILogger logger) : base("Questionable###Questionable", ImGuiWindowFlags.AlwaysAutoResize) { @@ -89,6 +91,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig _condition = condition; _gameGui = gameGui; _questSelectionWindow = questSelectionWindow; + _questValidationWindow = questValidationWindow; _logger = logger; #if DEBUG @@ -414,7 +417,8 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig $"Target: {_targetManager.Target.Name} ({_targetManager.Target.ObjectKind}; {_targetManager.Target.DataId})")); GameObject* gameObject = (GameObject*)_targetManager.Target.Address; - ImGui.Text(string.Create(CultureInfo.InvariantCulture, $"Distance: {(_targetManager.Target.Position - _clientState.LocalPlayer.Position).Length():F2}")); + ImGui.Text(string.Create(CultureInfo.InvariantCulture, + $"Distance: {(_targetManager.Target.Position - _clientState.LocalPlayer.Position).Length():F2}")); ImGui.SameLine(); float verticalDistance = _targetManager.Target.Position.Y - _clientState.LocalPlayer.Position.Y; @@ -471,7 +475,8 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig bool copy = ImGuiComponents.IconButton(FontAwesomeIcon.Copy); if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Left click: Copy target position as JSON.\nRight click: Copy target position as C# code."); + ImGui.SetTooltip( + "Left click: Copy target position as JSON.\nRight click: Copy target position as C# code."); if (copy) { string interactionType = gameObject->NamePlateIconId switch @@ -509,7 +514,8 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig { bool copy = ImGuiComponents.IconButton(FontAwesomeIcon.Copy); if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Left click: Copy your position as JSON.\nRight click: Copy your position as C# code."); + ImGui.SetTooltip( + "Left click: Copy your position as JSON.\nRight click: Copy your position as C# code."); if (copy) { ImGui.SetClipboardText($$""" @@ -570,6 +576,18 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig _framework.RunOnTick(() => _gameUiController.HandleCurrentDialogueChoices(), TimeSpan.FromMilliseconds(200)); } + +#if DEBUG + if (_questRegistry.ValidationIssueCount > 0) + { + ImGui.SameLine(); + + using var textColor = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.ExclamationTriangle, + $"{_questRegistry.ValidationIssueCount}")) + _questValidationWindow.IsOpen = true; + } +#endif } private void DrawRemainingTasks() From 4a9fd6076821dc27d3008d362201abdbc72ea089 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Tue, 16 Jul 2024 00:43:38 +0200 Subject: [PATCH 06/11] Load disabled quests in debug; mark a few tribal quests as disabled (as they're randomized & not fully explored) --- .../Dailies/3691_Sharing Is Caring.json | 1 + .../3692_The Aesthetician of Il Mheg.json | 1 + .../3703_Raiders of the Lost Pork.json | 21 +++++- .../Dailies/3703_Raiders of the Lost Pork.md | 8 +++ .../Dailies/3704_Pebble without a Cause.json | 33 ++++++++-- .../3705_There's Always a Stupider Fish.json | 66 +++++++++++++++++++ .../Arkasodara/Dailies/4566_Hippo Scrub.json | 1 - Questionable.Model/V1/QuestRoot.cs | 5 ++ Questionable/Controller/QuestRegistry.cs | 4 +- Questionable/Windows/QuestWindow.cs | 2 +- 10 files changed, 131 insertions(+), 11 deletions(-) create mode 100644 QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3703_Raiders of the Lost Pork.md create mode 100644 QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3705_There's Always a Stupider Fish.json diff --git a/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3691_Sharing Is Caring.json b/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3691_Sharing Is Caring.json index 9dc05e1c..6ba49247 100644 --- a/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3691_Sharing Is Caring.json +++ b/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3691_Sharing Is Caring.json @@ -1,6 +1,7 @@ { "$schema": "https://carvel.li/questionable/quest-1.0", "Author": "liza", + "Disabled": true, "QuestSequence": [ { "Sequence": 0, diff --git a/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3692_The Aesthetician of Il Mheg.json b/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3692_The Aesthetician of Il Mheg.json index fdba712d..9429e210 100644 --- a/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3692_The Aesthetician of Il Mheg.json +++ b/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3692_The Aesthetician of Il Mheg.json @@ -1,6 +1,7 @@ { "$schema": "https://carvel.li/questionable/quest-1.0", "Author": "liza", + "Disabled": true, "QuestSequence": [ { "Sequence": 0, diff --git a/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3703_Raiders of the Lost Pork.json b/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3703_Raiders of the Lost Pork.json index 5f4962f3..49f7f0ee 100644 --- a/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3703_Raiders of the Lost Pork.json +++ b/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3703_Raiders of the Lost Pork.json @@ -1,6 +1,7 @@ { "$schema": "https://carvel.li/questionable/quest-1.0", "Author": "liza", + "Disabled": true, "QuestSequence": [ { "Sequence": 0, @@ -20,6 +21,21 @@ { "Sequence": 1, "Steps": [ + { + "DataId": 1032169, + "Position": { + "X": -423.33105, + "Y": 25.599815, + "Z": 265.94946 + }, + "TerritoryId": 816, + "InteractionType": "Interact", + "Fly": true, + "SkipIf": [ + "NotTargetable" + ], + "$": "Only if QW: 0 48 0 0 0 0" + }, { "DataId": 1032167, "Position": { @@ -29,7 +45,10 @@ }, "TerritoryId": 816, "InteractionType": "Interact", - "Fly": true + "Fly": true, + "SkipIf": [ + "NotTargetable" + ] } ] }, diff --git a/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3703_Raiders of the Lost Pork.md b/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3703_Raiders of the Lost Pork.md new file mode 100644 index 00000000..e1306912 --- /dev/null +++ b/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3703_Raiders of the Lost Pork.md @@ -0,0 +1,8 @@ +## Raiders of the Lost Pork + +QuestWork: +``` +0 x 0 0 0 0 + 48 → Elegant Eulmoran (1032169) + ?? → 1032167 +``` diff --git a/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3704_Pebble without a Cause.json b/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3704_Pebble without a Cause.json index 940bafba..5c1ca655 100644 --- a/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3704_Pebble without a Cause.json +++ b/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3704_Pebble without a Cause.json @@ -1,6 +1,7 @@ { "$schema": "https://carvel.li/questionable/quest-1.0", "Author": "liza", + "Disabled": true, "QuestSequence": [ { "Sequence": 0, @@ -28,19 +29,39 @@ "Z": -55.77173 }, "TerritoryId": 816, - "InteractionType": "Combat", - "EnemySpawnType": "AfterInteraction", - "KillEnemyDataIds": [ - 11451 - ], + "InteractionType": "Interact", "Fly": true, - "Comment": "TODO Combat is optional, check where we should walk" + "$": "Only if QW: 0 0 ??? 0 0 0", + "SkipIf": ["NotTargetable"] + }, + { + "DataId": 2010902, + "Position": { + "X": -405.9358, + "Y": -0.07635498, + "Z": -28.397034 + }, + "TerritoryId": 816, + "InteractionType": "Interact", + "$": "Only if QW: 0 0 1 0 0 0 → if complete, 0 16 0 0 0 0", + "SkipIf": ["NotTargetable"] } ] }, { "Sequence": 255, "Steps": [ + { + "Position": { + "X": -398.9776, + "Y": 0.82966614, + "Z": 8.668919 + }, + "TerritoryId": 816, + "InteractionType": "WalkTo", + "Mount": false, + "DisableNavmesh": true + }, { "DataId": 1031809, "Position": { diff --git a/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3705_There's Always a Stupider Fish.json b/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3705_There's Always a Stupider Fish.json new file mode 100644 index 00000000..a71e2c27 --- /dev/null +++ b/QuestPaths/5.x - Shadowbringers/Tribal/Pixies/Dailies/3705_There's Always a Stupider Fish.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "Disabled": true, + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1031809, + "Position": { + "X": -454.3069, + "Y": 71.43217, + "Z": 575.1278 + }, + "TerritoryId": 816, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "Position": { + "X": -115.77283, + "Y": 7.1942587, + "Z": 130.61378 + }, + "TerritoryId": 816, + "InteractionType": "WalkTo", + "Fly": true + }, + { + "DataId": 2010911, + "Position": { + "X": -113.93915, + "Y": -36.087585, + "Z": 87.6936 + }, + "TerritoryId": 816, + "InteractionType": "Interact", + "DisableNavmesh": true, + "$": "QW: 0 2 0 0 0 0" + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1031809, + "Position": { + "X": -454.3069, + "Y": 71.43217, + "Z": 575.1278 + }, + "TerritoryId": 816, + "InteractionType": "CompleteQuest", + "AetheryteShortcut": "Il Mheg - Lydha Lran", + "Fly": true + } + ] + } + ] +} diff --git a/QuestPaths/6.x - Endwalker/Tribal/Arkasodara/Dailies/4566_Hippo Scrub.json b/QuestPaths/6.x - Endwalker/Tribal/Arkasodara/Dailies/4566_Hippo Scrub.json index 5bb38148..2142ebcd 100644 --- a/QuestPaths/6.x - Endwalker/Tribal/Arkasodara/Dailies/4566_Hippo Scrub.json +++ b/QuestPaths/6.x - Endwalker/Tribal/Arkasodara/Dailies/4566_Hippo Scrub.json @@ -79,7 +79,6 @@ }, "TerritoryId": 957, "InteractionType": "CompleteQuest", - "AetheryteShortcut": "Thavnair - Yedlihmad", "Fly": true } ] diff --git a/Questionable.Model/V1/QuestRoot.cs b/Questionable.Model/V1/QuestRoot.cs index 60693898..cf1967c4 100644 --- a/Questionable.Model/V1/QuestRoot.cs +++ b/Questionable.Model/V1/QuestRoot.cs @@ -6,7 +6,12 @@ public sealed class QuestRoot { public string Author { get; set; } = null!; public List Contributors { get; set; } = new(); + + /// + /// This is only relevant for release builds. + /// public bool Disabled { get; set; } + public string? Comment { get; set; } public List TerritoryBlacklist { get; set; } = new(); public List QuestSequence { get; set; } = new(); diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs index d836b21e..e0e03630 100644 --- a/Questionable/Controller/QuestRegistry.cs +++ b/Questionable/Controller/QuestRegistry.cs @@ -173,8 +173,8 @@ internal sealed class QuestRegistry return ushort.Parse(parts[0], CultureInfo.InvariantCulture); } - public bool IsKnownQuest(ushort questId) => TryGetQuest(questId, out _); + public bool IsKnownQuest(ushort questId) => _quests.ContainsKey(questId); public bool TryGetQuest(ushort questId, [NotNullWhen(true)] out Quest? quest) - => _quests.TryGetValue(questId, out quest) && !quest.Root.Disabled; + => _quests.TryGetValue(questId, out quest); } diff --git a/Questionable/Windows/QuestWindow.cs b/Questionable/Windows/QuestWindow.cs index 517d8894..2709ab0b 100644 --- a/Questionable/Windows/QuestWindow.cs +++ b/Questionable/Windows/QuestWindow.cs @@ -583,7 +583,7 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig ImGui.SameLine(); using var textColor = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); - if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.ExclamationTriangle, + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Flag, $"{_questRegistry.ValidationIssueCount}")) _questValidationWindow.IsOpen = true; } From 9960f971db1671cbe17a86b4131a5106faaf9f8b Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Tue, 16 Jul 2024 00:55:00 +0200 Subject: [PATCH 07/11] Add editorconfig for questpath jsons --- QuestPaths/.editorconfig | 6 ++++++ Questionable/Questionable.csproj | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 QuestPaths/.editorconfig diff --git a/QuestPaths/.editorconfig b/QuestPaths/.editorconfig new file mode 100644 index 00000000..31cbbd3e --- /dev/null +++ b/QuestPaths/.editorconfig @@ -0,0 +1,6 @@ +root = true + +[*.json] +indent_size = 2 +indent_style = space +insert_final_newline = true diff --git a/Questionable/Questionable.csproj b/Questionable/Questionable.csproj index b7c5f724..ba75e706 100644 --- a/Questionable/Questionable.csproj +++ b/Questionable/Questionable.csproj @@ -1,6 +1,6 @@  - 1.7 + 1.8 dist $(SolutionDir)=X:\ From 985fb7f4c1c1f68b6299d8a45764479b7476e63f Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Tue, 16 Jul 2024 10:52:54 +0200 Subject: [PATCH 08/11] Always show validation issues (for quests you can edit) --- Questionable/Controller/QuestRegistry.cs | 16 +++++----------- Questionable/Model/Quest.cs | 1 + Questionable/Validation/QuestValidator.cs | 2 +- Questionable/Windows/QuestWindow.cs | 2 -- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs index e0e03630..b179b858 100644 --- a/Questionable/Controller/QuestRegistry.cs +++ b/Questionable/Controller/QuestRegistry.cs @@ -3,15 +3,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; using System.IO; -using System.Numerics; +using System.Linq; using System.Text.Json; -using System.Threading.Tasks; using Dalamud.Plugin; -using Dalamud.Plugin.Services; using Microsoft.Extensions.Logging; -using Questionable.Controller.Utils; using Questionable.Data; using Questionable.Model; using Questionable.Model.V1; @@ -23,18 +19,16 @@ internal sealed class QuestRegistry { private readonly IDalamudPluginInterface _pluginInterface; private readonly QuestData _questData; - private readonly IChatGui _chatGui; private readonly QuestValidator _questValidator; private readonly ILogger _logger; private readonly Dictionary _quests = new(); - public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData, IChatGui chatGui, + public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData, QuestValidator questValidator, ILogger logger) { _pluginInterface = pluginInterface; _questData = questData; - _chatGui = chatGui; _questValidator = questValidator; _logger = logger; } @@ -76,6 +70,7 @@ internal sealed class QuestRegistry QuestId = questId, Root = questRoot, Info = _questData.GetQuestInfo(questId), + ReadOnly = true, }; _quests[questId] = quest; } @@ -111,14 +106,12 @@ internal sealed class QuestRegistry } } - [Conditional("DEBUG")] private void ValidateQuests() { _questValidator.ClearIssues(); - _questValidator.Validate(_quests.Values); + _questValidator.Validate(_quests.Values.Where(x => !x.ReadOnly)); } - private void LoadQuestFromStream(string fileName, Stream stream) { _logger.LogTrace("Loading quest from '{FileName}'", fileName); @@ -131,6 +124,7 @@ internal sealed class QuestRegistry QuestId = questId.Value, Root = JsonSerializer.Deserialize(stream)!, Info = _questData.GetQuestInfo(questId.Value), + ReadOnly = false, }; _quests[questId.Value] = quest; } diff --git a/Questionable/Model/Quest.cs b/Questionable/Model/Quest.cs index e70911ae..9bdf6cb5 100644 --- a/Questionable/Model/Quest.cs +++ b/Questionable/Model/Quest.cs @@ -8,6 +8,7 @@ internal sealed class Quest public required ushort QuestId { get; init; } public required QuestRoot Root { get; init; } public required QuestInfo Info { get; init; } + public required bool ReadOnly { get; init; } public QuestSequence? FindSequence(byte currentSequence) => Root.QuestSequence.SingleOrDefault(seq => seq.Sequence == currentSequence); diff --git a/Questionable/Validation/QuestValidator.cs b/Questionable/Validation/QuestValidator.cs index 83bb9d42..931d8786 100644 --- a/Questionable/Validation/QuestValidator.cs +++ b/Questionable/Validation/QuestValidator.cs @@ -27,7 +27,7 @@ internal sealed class QuestValidator public void ClearIssues() => _validationIssues.Clear(); - public void Validate(IReadOnlyCollection quests) + public void Validate(IEnumerable quests) { Task.Run(() => { diff --git a/Questionable/Windows/QuestWindow.cs b/Questionable/Windows/QuestWindow.cs index 2709ab0b..92d87681 100644 --- a/Questionable/Windows/QuestWindow.cs +++ b/Questionable/Windows/QuestWindow.cs @@ -577,7 +577,6 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig TimeSpan.FromMilliseconds(200)); } -#if DEBUG if (_questRegistry.ValidationIssueCount > 0) { ImGui.SameLine(); @@ -587,7 +586,6 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig $"{_questRegistry.ValidationIssueCount}")) _questValidationWindow.IsOpen = true; } -#endif } private void DrawRemainingTasks() From e239edb22cdb81a4b800ace10972fc78f4421433 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Tue, 16 Jul 2024 10:54:47 +0200 Subject: [PATCH 09/11] Code cleanup/add suppressions --- QuestPaths/AssemblyQuestLoader.cs | 2 ++ .../V1/Converter/ExcelRefConverter.cs | 12 ++++++------ Questionable.Model/V1/ExcelRef.cs | 18 +++++++----------- Questionable/Controller/CombatController.cs | 2 ++ Questionable/Controller/GameUiController.cs | 1 + Questionable/Windows/QuestSelectionWindow.cs | 2 ++ 6 files changed, 20 insertions(+), 17 deletions(-) diff --git a/QuestPaths/AssemblyQuestLoader.cs b/QuestPaths/AssemblyQuestLoader.cs index b0b8efc0..3c8b7b71 100644 --- a/QuestPaths/AssemblyQuestLoader.cs +++ b/QuestPaths/AssemblyQuestLoader.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Questionable.Model.V1; namespace Questionable.QuestPaths; +[SuppressMessage("ReSharper", "PartialTypeWithSinglePart", Justification = "Required for RELEASE")] public static partial class AssemblyQuestLoader { public static IReadOnlyDictionary GetQuests() => diff --git a/Questionable.Model/V1/Converter/ExcelRefConverter.cs b/Questionable.Model/V1/Converter/ExcelRefConverter.cs index 0c48e5bb..06ba3ff1 100644 --- a/Questionable.Model/V1/Converter/ExcelRefConverter.cs +++ b/Questionable.Model/V1/Converter/ExcelRefConverter.cs @@ -8,12 +8,12 @@ public sealed class ExcelRefConverter : JsonConverter { public override ExcelRef? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonTokenType.String) - return new ExcelRef(reader.GetString()!); - else if (reader.TokenType == JsonTokenType.Number) - return new ExcelRef(reader.GetUInt32()); - else - return null; + return reader.TokenType switch + { + JsonTokenType.String => ExcelRef.FromKey(reader.GetString()!), + JsonTokenType.Number => ExcelRef.FromRowId(reader.GetUInt32()), + _ => null + }; } public override void Write(Utf8JsonWriter writer, ExcelRef? value, JsonSerializerOptions options) diff --git a/Questionable.Model/V1/ExcelRef.cs b/Questionable.Model/V1/ExcelRef.cs index 979a1897..295dd5ba 100644 --- a/Questionable.Model/V1/ExcelRef.cs +++ b/Questionable.Model/V1/ExcelRef.cs @@ -21,20 +21,16 @@ public class ExcelRef Type = EType.RowId; } - /// - /// Only used internally (not serialized) with specific values that have been read from the sheets already. - /// - private ExcelRef(string value, bool v) + private ExcelRef(string? stringValue, uint? rowIdValue, EType type) { - if (!v) - throw new ArgumentException(nameof(v)); - - _stringValue = value; - _rowIdValue = null; - Type = EType.RawString; + _stringValue = stringValue; + _rowIdValue = rowIdValue; + Type = type; } - public static ExcelRef FromSheetValue(string value) => new(value, true); + public static ExcelRef FromKey(string value) => new(value, null, EType.Key); + public static ExcelRef FromRowId(uint rowId) => new(null, rowId, EType.RowId); + public static ExcelRef FromSheetValue(string value) => new(value, null, EType.RawString); public EType Type { get; } diff --git a/Questionable/Controller/CombatController.cs b/Questionable/Controller/CombatController.cs index 51f48385..aada0a1a 100644 --- a/Questionable/Controller/CombatController.cs +++ b/Questionable/Controller/CombatController.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; @@ -104,6 +105,7 @@ internal sealed class CombatController return _condition[ConditionFlag.InCombat]; } + [SuppressMessage("ReSharper", "RedundantJumpStatement")] private IGameObject? FindNextTarget() { if (_currentFight == null) diff --git a/Questionable/Controller/GameUiController.cs b/Questionable/Controller/GameUiController.cs index dba30ec0..efba25f0 100644 --- a/Questionable/Controller/GameUiController.cs +++ b/Questionable/Controller/GameUiController.cs @@ -146,6 +146,7 @@ internal sealed class GameUiController : IDisposable SelectIconStringPostSetup(addonSelectIconString, false); } + [SuppressMessage("ReSharper", "RedundantJumpStatement")] private unsafe void SelectIconStringPostSetup(AddonSelectIconString* addonSelectIconString, bool checkAllSteps) { string? actualPrompt = addonSelectIconString->AtkUnitBase.AtkValues[3].ReadAtkString(); diff --git a/Questionable/Windows/QuestSelectionWindow.cs b/Questionable/Windows/QuestSelectionWindow.cs index 55d81bb5..164f03b6 100644 --- a/Questionable/Windows/QuestSelectionWindow.cs +++ b/Questionable/Windows/QuestSelectionWindow.cs @@ -274,6 +274,7 @@ internal sealed class QuestSelectionWindow : LWindow { var qInfo = _questData.GetQuestInfo(q); var (iconColor, icon, _) = GetQuestStyle(q); + // ReSharper disable once UnusedVariable using (var font = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) { if (_questRegistry.IsKnownQuest(qInfo.QuestId)) @@ -305,6 +306,7 @@ internal sealed class QuestSelectionWindow : LWindow { var qInfo = _questData.GetQuestInfo(q); var (iconColor, icon, _) = GetQuestStyle(q); + // ReSharper disable once UnusedVariable using (var font = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) { if (_questRegistry.IsKnownQuest(qInfo.QuestId)) From fb9e31cd803ab8144adf1131fa5e9984a9886a58 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Tue, 16 Jul 2024 11:15:28 +0200 Subject: [PATCH 10/11] Add currently visible unaccepted quest markers to /qst zone (only for unknown/not mapped quests) --- Questionable/Controller/CommandHandler.cs | 9 ++---- Questionable/Windows/QuestSelectionWindow.cs | 34 ++++++++++++-------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/Questionable/Controller/CommandHandler.cs b/Questionable/Controller/CommandHandler.cs index 33ccf29a..e7fa9767 100644 --- a/Questionable/Controller/CommandHandler.cs +++ b/Questionable/Controller/CommandHandler.cs @@ -1,10 +1,8 @@ using System; -using System.Collections.Generic; using System.Linq; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Command; using Dalamud.Plugin.Services; -using Questionable.Data; using Questionable.Model; using Questionable.Windows; @@ -23,13 +21,11 @@ internal sealed class CommandHandler : IDisposable private readonly QuestSelectionWindow _questSelectionWindow; private readonly ITargetManager _targetManager; private readonly GameFunctions _gameFunctions; - private readonly IClientState _clientState; public CommandHandler(ICommandManager commandManager, IChatGui chatGui, QuestController questController, MovementController movementController, QuestRegistry questRegistry, ConfigWindow configWindow, DebugOverlay debugOverlay, QuestWindow questWindow, - QuestSelectionWindow questSelectionWindow, ITargetManager targetManager, GameFunctions gameFunctions, - IClientState clientState) + QuestSelectionWindow questSelectionWindow, ITargetManager targetManager, GameFunctions gameFunctions) { _commandManager = commandManager; _chatGui = chatGui; @@ -42,7 +38,6 @@ internal sealed class CommandHandler : IDisposable _questSelectionWindow = questSelectionWindow; _targetManager = targetManager; _gameFunctions = gameFunctions; - _clientState = clientState; _commandManager.AddHandler("/qst", new CommandInfo(ProcessCommand) { @@ -87,7 +82,7 @@ internal sealed class CommandHandler : IDisposable case "z": case "zone": - _questSelectionWindow.OpenForZone(_clientState.TerritoryType); + _questSelectionWindow.OpenForCurrentZone(); break; default: diff --git a/Questionable/Windows/QuestSelectionWindow.cs b/Questionable/Windows/QuestSelectionWindow.cs index 164f03b6..569fd28b 100644 --- a/Questionable/Windows/QuestSelectionWindow.cs +++ b/Questionable/Windows/QuestSelectionWindow.cs @@ -9,6 +9,7 @@ using Dalamud.Interface.Components; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.UI; using ImGuiNET; using LLib.GameUI; @@ -30,6 +31,7 @@ internal sealed class QuestSelectionWindow : LWindow private readonly QuestRegistry _questRegistry; private readonly IDalamudPluginInterface _pluginInterface; private readonly TerritoryData _territoryData; + private readonly IClientState _clientState; private List _quests = []; private List _offeredQuests = []; @@ -37,7 +39,7 @@ internal sealed class QuestSelectionWindow : LWindow public QuestSelectionWindow(QuestData questData, IGameGui gameGui, IChatGui chatGui, GameFunctions gameFunctions, QuestController questController, QuestRegistry questRegistry, IDalamudPluginInterface pluginInterface, - TerritoryData territoryData) + TerritoryData territoryData, IClientState clientState) : base($"Quest Selection{WindowId}") { _questData = questData; @@ -48,6 +50,7 @@ internal sealed class QuestSelectionWindow : LWindow _questRegistry = questRegistry; _pluginInterface = pluginInterface; _territoryData = territoryData; + _clientState = clientState; Size = new Vector2(500, 200); SizeCondition = ImGuiCond.Once; @@ -57,18 +60,15 @@ internal sealed class QuestSelectionWindow : LWindow }; } - public uint TargetId { get; private set; } - public string TargetName { get; private set; } = string.Empty; - public unsafe void OpenForTarget(IGameObject? gameObject) { if (gameObject != null) { - TargetId = gameObject.DataId; - TargetName = gameObject.Name.ToString(); - WindowName = $"Quests starting with {TargetName} [{TargetId}]{WindowId}"; + var targetId = gameObject.DataId; + var targetName = gameObject.Name.ToString(); + WindowName = $"Quests starting with {targetName} [{targetId}]{WindowId}"; - _quests = _questData.GetAllByIssuerDataId(TargetId); + _quests = _questData.GetAllByIssuerDataId(targetId); if (_gameGui.TryGetAddonByName("SelectIconString", out var addonSelectIconString)) { var answers = GameUiController.GetChoices(addonSelectIconString); @@ -88,24 +88,30 @@ internal sealed class QuestSelectionWindow : LWindow IsOpen = _quests.Count > 0; } - public void OpenForZone(ushort territoryId) + public unsafe void OpenForCurrentZone() { - TargetId = territoryId; - TargetName = _territoryData.GetNameAndId(territoryId); - WindowName = $"Quests starting in {TargetName}{WindowId}"; + var territoryId = _clientState.TerritoryType; + var territoryName = _territoryData.GetNameAndId(territoryId); + WindowName = $"Quests starting in {territoryName}{WindowId}"; _quests = _questRegistry.AllQuests .Where(x => x.FindSequence(0)?.FindStep(0)?.TerritoryId == territoryId) .Select(x => _questData.GetQuestInfo(x.QuestId)) .ToList(); + + foreach (var unacceptedQuest in Map.Instance()->UnacceptedQuestMarkers) + { + ushort questId = (ushort)(unacceptedQuest.ObjectiveId & 0xFFFF); + if (_quests.All(q => q.QuestId != questId)) + _quests.Add(_questData.GetQuestInfo(questId)); + } + _offeredQuests = []; IsOpen = true; } public override void OnClose() { - TargetId = default; - TargetName = string.Empty; _quests = []; _offeredQuests = []; } From 3a742ea2adec181e2ef521a12ba585e30edefb2d Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Tue, 16 Jul 2024 14:43:31 +0200 Subject: [PATCH 11/11] Add Arcadion quests --- .../4960_A New Challenger Appears.json | 60 ++++++++++++ .../4961_The Claw in the Dark.json | 52 ++++++++++ .../Raid Quests/4962_Sweet Poison.json | 68 +++++++++++++ .../Raid Quests/4963_Yaana's Yarn.json | 55 +++++++++++ .../Raid Quests/4964_Vile Heat.json | 68 +++++++++++++ .../Raid Quests/4965_The Neoteric Witch.json | 96 +++++++++++++++++++ Questionable/Controller/QuestRegistry.cs | 1 + Questionable/Model/Quest.cs | 17 +++- Questionable/QuestionablePlugin.cs | 1 + Questionable/Validation/QuestValidator.cs | 1 + .../Validators/CompletionFlagsValidator.cs | 2 +- .../Validators/NextQuestValidator.cs | 23 +++++ .../Validators/UniqueStartStopValidator.cs | 23 ++--- Questionable/Windows/DebugOverlay.cs | 8 +- Questionable/Windows/QuestValidationWindow.cs | 26 ++++- Questionable/Windows/QuestWindow.cs | 8 +- 16 files changed, 485 insertions(+), 24 deletions(-) create mode 100644 QuestPaths/7.x - Dawntrail/Raid Quests/4960_A New Challenger Appears.json create mode 100644 QuestPaths/7.x - Dawntrail/Raid Quests/4961_The Claw in the Dark.json create mode 100644 QuestPaths/7.x - Dawntrail/Raid Quests/4962_Sweet Poison.json create mode 100644 QuestPaths/7.x - Dawntrail/Raid Quests/4963_Yaana's Yarn.json create mode 100644 QuestPaths/7.x - Dawntrail/Raid Quests/4964_Vile Heat.json create mode 100644 QuestPaths/7.x - Dawntrail/Raid Quests/4965_The Neoteric Witch.json create mode 100644 Questionable/Validation/Validators/NextQuestValidator.cs diff --git a/QuestPaths/7.x - Dawntrail/Raid Quests/4960_A New Challenger Appears.json b/QuestPaths/7.x - Dawntrail/Raid Quests/4960_A New Challenger Appears.json new file mode 100644 index 00000000..997d35fd --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Raid Quests/4960_A New Challenger Appears.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1049786, + "Position": { + "X": 340.13867, + "Y": 50.75, + "Z": 231.37244 + }, + "TerritoryId": 1186, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 1049787, + "Position": { + "X": 364.3396, + "Y": 60.125, + "Z": 357.1068 + }, + "TerritoryId": 1186, + "InteractionType": "Interact", + "DialogueChoices": [ + { + "Type": "YesNo", + "Prompt": "TEXT_KINGRA101_04960_SYSTEM_100_030", + "Yes": true + } + ] + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1049788, + "Position": { + "X": 1.6021729, + "Y": 0, + "Z": -6.088379 + }, + "StopDistance": 5, + "TerritoryId": 1224, + "InteractionType": "CompleteQuest", + "NextQuestId": 4961 + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Raid Quests/4961_The Claw in the Dark.json b/QuestPaths/7.x - Dawntrail/Raid Quests/4961_The Claw in the Dark.json new file mode 100644 index 00000000..766588ce --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Raid Quests/4961_The Claw in the Dark.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "TerritoryBlacklist": [ + 1225 + ], + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1049788, + "Position": { + "X": 1.6021729, + "Y": 0, + "Z": -6.088379 + }, + "StopDistance": 5, + "TerritoryId": 1224, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "TerritoryId": 1224, + "InteractionType": "Duty", + "ContentFinderConditionId": 985 + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1050476, + "Position": { + "X": 0.1373291, + "Y": -3.3667622E-13, + "Z": -9.658997 + }, + "StopDistance": 7, + "TerritoryId": 1224, + "InteractionType": "CompleteQuest", + "NextQuestId": 4962 + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Raid Quests/4962_Sweet Poison.json b/QuestPaths/7.x - Dawntrail/Raid Quests/4962_Sweet Poison.json new file mode 100644 index 00000000..79026f6a --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Raid Quests/4962_Sweet Poison.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "TerritoryBlacklist": [ + 1227 + ], + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1050476, + "Position": { + "X": 0.1373291, + "Y": -3.3667622E-13, + "Z": -9.658997 + }, + "StopDistance": 7, + "TerritoryId": 1224, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "TerritoryId": 1224, + "InteractionType": "Duty", + "ContentFinderConditionId": 987 + } + ] + }, + { + "Sequence": 2, + "Steps": [ + { + "DataId": 1050476, + "Position": { + "X": 0.1373291, + "Y": -3.3667622E-13, + "Z": -9.658997 + }, + "StopDistance": 7, + "TerritoryId": 1224, + "InteractionType": "Interact" + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1050477, + "Position": { + "X": 1.663208, + "Y": -1.9688797E-12, + "Z": -10.727112 + }, + "StopDistance": 7, + "TerritoryId": 1224, + "InteractionType": "CompleteQuest", + "NextQuestId": 4963 + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Raid Quests/4963_Yaana's Yarn.json b/QuestPaths/7.x - Dawntrail/Raid Quests/4963_Yaana's Yarn.json new file mode 100644 index 00000000..cc897c5d --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Raid Quests/4963_Yaana's Yarn.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1050477, + "Position": { + "X": 1.663208, + "Y": -1.9688797E-12, + "Z": -10.727112 + }, + "StopDistance": 7, + "TerritoryId": 1224, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "DataId": 1049790, + "Position": { + "X": 494.7433, + "Y": 59.55, + "Z": 125.10864 + }, + "StopDistance": 5, + "TerritoryId": 1186, + "InteractionType": "Interact" + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1049789, + "Position": { + "X": 2.3651123, + "Y": -4.334177E-12, + "Z": -14.206177 + }, + "StopDistance": 7, + "TerritoryId": 1224, + "InteractionType": "CompleteQuest", + "NextQuestId": 4964 + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Raid Quests/4964_Vile Heat.json b/QuestPaths/7.x - Dawntrail/Raid Quests/4964_Vile Heat.json new file mode 100644 index 00000000..7d7164a7 --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Raid Quests/4964_Vile Heat.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "TerritoryBlacklist": [ + 1229 + ], + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1049789, + "Position": { + "X": 2.3651123, + "Y": -4.334177E-12, + "Z": -14.206177 + }, + "StopDistance": 7, + "TerritoryId": 1224, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "TerritoryId": 1224, + "InteractionType": "Duty", + "ContentFinderConditionId": 989 + } + ] + }, + { + "Sequence": 2, + "Steps": [ + { + "DataId": 1050477, + "Position": { + "X": 1.663208, + "Y": -1.9688797E-12, + "Z": -10.727112 + }, + "StopDistance": 7, + "TerritoryId": 1224, + "InteractionType": "Interact" + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1050477, + "Position": { + "X": 1.663208, + "Y": -1.9688797E-12, + "Z": -10.727112 + }, + "StopDistance": 7, + "TerritoryId": 1224, + "InteractionType": "CompleteQuest", + "NextQuestId": 4965 + } + ] + } + ] +} diff --git a/QuestPaths/7.x - Dawntrail/Raid Quests/4965_The Neoteric Witch.json b/QuestPaths/7.x - Dawntrail/Raid Quests/4965_The Neoteric Witch.json new file mode 100644 index 00000000..1cd29ff4 --- /dev/null +++ b/QuestPaths/7.x - Dawntrail/Raid Quests/4965_The Neoteric Witch.json @@ -0,0 +1,96 @@ +{ + "$schema": "https://carvel.li/questionable/quest-1.0", + "Author": "liza", + "TerritoryBlacklist": [ + 1231 + ], + "QuestSequence": [ + { + "Sequence": 0, + "Steps": [ + { + "DataId": 1049792, + "Position": { + "X": 1.663208, + "Y": -1.9688797E-12, + "Z": -10.727112 + }, + "StopDistance": 7, + "TerritoryId": 1224, + "InteractionType": "AcceptQuest" + } + ] + }, + { + "Sequence": 1, + "Steps": [ + { + "TerritoryId": 1224, + "InteractionType": "Duty", + "ContentFinderConditionId": 991 + } + ] + }, + { + "Sequence": 2, + "Steps": [ + { + "DataId": 1050477, + "Position": { + "X": 1.663208, + "Y": -1.9688797E-12, + "Z": -10.727112 + }, + "StopDistance": 7, + "TerritoryId": 1224, + "InteractionType": "Interact" + } + ] + }, + { + "Sequence": 3, + "Steps": [ + { + "DataId": 2013722, + "Position": { + "X": -0.07635498, + "Y": 1.0527954, + "Z": 8.102478 + }, + "TerritoryId": 1224, + "InteractionType": "Interact", + "TargetTerritoryId": 1186 + }, + { + "DataId": 1049790, + "Position": { + "X": 494.7433, + "Y": 59.55, + "Z": 125.10864 + }, + "TerritoryId": 1186, + "InteractionType": "Interact", + "AethernetShortcut": [ + "[Solution Nine] The Arcadion", + "[Solution Nine] True Vue" + ] + } + ] + }, + { + "Sequence": 255, + "Steps": [ + { + "DataId": 1049790, + "Position": { + "X": 494.7433, + "Y": 59.55, + "Z": 125.10864 + }, + "TerritoryId": 1186, + "InteractionType": "CompleteQuest" + } + ] + } + ] +} diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs index b179b858..93d83f9d 100644 --- a/Questionable/Controller/QuestRegistry.cs +++ b/Questionable/Controller/QuestRegistry.cs @@ -36,6 +36,7 @@ internal sealed class QuestRegistry public IEnumerable AllQuests => _quests.Values; public int Count => _quests.Count; public int ValidationIssueCount => _questValidator.IssueCount; + public int ValidationErrorCount => _questValidator.ErrorCount; public void Reload() { diff --git a/Questionable/Model/Quest.cs b/Questionable/Model/Quest.cs index 9bdf6cb5..a33bcd18 100644 --- a/Questionable/Model/Quest.cs +++ b/Questionable/Model/Quest.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using Questionable.Model.V1; namespace Questionable.Model; @@ -12,4 +13,18 @@ internal sealed class Quest public QuestSequence? FindSequence(byte currentSequence) => Root.QuestSequence.SingleOrDefault(seq => seq.Sequence == currentSequence); + + public IEnumerable AllSequences() => Root.QuestSequence; + + public IEnumerable<(QuestSequence Sequence, int StepId, QuestStep Step)> AllSteps() + { + foreach (var sequence in Root.QuestSequence) + { + for (int i = 0; i < sequence.Steps.Count; ++i) + { + var step = sequence.Steps[i]; + yield return (sequence, i, step); + } + } + } } diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index 39c370d0..58f580e3 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -136,6 +136,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Questionable/Validation/QuestValidator.cs b/Questionable/Validation/QuestValidator.cs index 931d8786..694ff5f4 100644 --- a/Questionable/Validation/QuestValidator.cs +++ b/Questionable/Validation/QuestValidator.cs @@ -24,6 +24,7 @@ internal sealed class QuestValidator public IReadOnlyList Issues => _validationIssues; public int IssueCount => _validationIssues.Count; + public int ErrorCount => _validationIssues.Count(x => x.Severity == EIssueSeverity.Error); public void ClearIssues() => _validationIssues.Clear(); diff --git a/Questionable/Validation/Validators/CompletionFlagsValidator.cs b/Questionable/Validation/Validators/CompletionFlagsValidator.cs index f1ea4c02..0cf97f1d 100644 --- a/Questionable/Validation/Validators/CompletionFlagsValidator.cs +++ b/Questionable/Validation/Validators/CompletionFlagsValidator.cs @@ -10,7 +10,7 @@ internal sealed class CompletionFlagsValidator : IQuestValidator { public IEnumerable Validate(Quest quest) { - foreach (var sequence in quest.Root.QuestSequence) + foreach (var sequence in quest.AllSequences()) { var mappedCompletionFlags = sequence.Steps .Select(x => diff --git a/Questionable/Validation/Validators/NextQuestValidator.cs b/Questionable/Validation/Validators/NextQuestValidator.cs new file mode 100644 index 00000000..891c3864 --- /dev/null +++ b/Questionable/Validation/Validators/NextQuestValidator.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Linq; +using Questionable.Model; + +namespace Questionable.Validation.Validators; + +internal sealed class NextQuestValidator : IQuestValidator +{ + public IEnumerable Validate(Quest quest) + { + foreach (var invalidNextQuest in quest.AllSteps().Where(x => x.Step.NextQuestId == quest.QuestId)) + { + yield return new ValidationIssue + { + QuestId = quest.QuestId, + Sequence = (byte)invalidNextQuest.Sequence.Sequence, + Step = invalidNextQuest.StepId, + Severity = EIssueSeverity.Error, + Description = "Next quest should not reference itself", + }; + } + } +} diff --git a/Questionable/Validation/Validators/UniqueStartStopValidator.cs b/Questionable/Validation/Validators/UniqueStartStopValidator.cs index 83c350a5..495e026e 100644 --- a/Questionable/Validation/Validators/UniqueStartStopValidator.cs +++ b/Questionable/Validation/Validators/UniqueStartStopValidator.cs @@ -14,12 +14,12 @@ internal sealed class UniqueStartStopValidator : IQuestValidator .ToList(); foreach (var accept in questAccepts) { - if (accept.SequenceId != 0 || accept.StepId != quest.FindSequence(0)!.Steps.Count - 1) + if (accept.Sequence.Sequence != 0 || accept.StepId != quest.FindSequence(0)!.Steps.Count - 1) { yield return new ValidationIssue { QuestId = quest.QuestId, - Sequence = (byte)accept.SequenceId, + Sequence = (byte)accept.Sequence.Sequence, Step = accept.StepId, Severity = EIssueSeverity.Error, Description = "Unexpected AcceptQuest step", @@ -44,12 +44,12 @@ internal sealed class UniqueStartStopValidator : IQuestValidator .ToList(); foreach (var complete in questCompletes) { - if (complete.SequenceId != 255 || complete.StepId != quest.FindSequence(255)!.Steps.Count - 1) + if (complete.Sequence.Sequence != 255 || complete.StepId != quest.FindSequence(255)!.Steps.Count - 1) { yield return new ValidationIssue { QuestId = quest.QuestId, - Sequence = (byte)complete.SequenceId, + Sequence = (byte)complete.Sequence.Sequence, Step = complete.StepId, Severity = EIssueSeverity.Error, Description = "Unexpected CompleteQuest step", @@ -70,16 +70,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator } } - private static IEnumerable<(int SequenceId, int StepId, QuestStep Step)> FindQuestStepsWithInteractionType(Quest quest, EInteractionType interactionType) - { - foreach (var sequence in quest.Root.QuestSequence) - { - for (int i = 0; i < sequence.Steps.Count; ++i) - { - var step = sequence.Steps[i]; - if (step.InteractionType == interactionType) - yield return (sequence.Sequence, i, step); - } - } - } + private static IEnumerable<(QuestSequence Sequence, int StepId, QuestStep Step)> FindQuestStepsWithInteractionType( + Quest quest, EInteractionType interactionType) + => quest.AllSteps().Where(x => x.Step.InteractionType == interactionType); } diff --git a/Questionable/Windows/DebugOverlay.cs b/Questionable/Windows/DebugOverlay.cs index e778372d..0d2af7be 100644 --- a/Questionable/Windows/DebugOverlay.cs +++ b/Questionable/Windows/DebugOverlay.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Linq; using System.Numerics; +using Dalamud.Game.ClientState.Conditions; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; @@ -18,10 +19,11 @@ internal sealed class DebugOverlay : Window private readonly QuestRegistry _questRegistry; private readonly IGameGui _gameGui; private readonly IClientState _clientState; + private readonly ICondition _condition; private readonly Configuration _configuration; public DebugOverlay(QuestController questController, QuestRegistry questRegistry, IGameGui gameGui, - IClientState clientState, Configuration configuration) + IClientState clientState, ICondition condition, Configuration configuration) : base("Questionable Debug Overlay###QuestionableDebugOverlay", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoSavedSettings, true) @@ -30,6 +32,7 @@ internal sealed class DebugOverlay : Window _questRegistry = questRegistry; _gameGui = gameGui; _clientState = clientState; + _condition = condition; _configuration = configuration; Position = Vector2.Zero; @@ -44,7 +47,8 @@ internal sealed class DebugOverlay : Window public override bool DrawConditions() { return _configuration.Advanced.DebugOverlay && _clientState is - { IsLoggedIn: true, LocalPlayer: not null, IsPvPExcludingDen: false }; + { IsLoggedIn: true, LocalPlayer: not null, IsPvPExcludingDen: false } && + !_condition[ConditionFlag.OccupiedInCutSceneEvent]; } public override void PreDraw() diff --git a/Questionable/Windows/QuestValidationWindow.cs b/Questionable/Windows/QuestValidationWindow.cs index 2cb3f5b2..83e6d2ec 100644 --- a/Questionable/Windows/QuestValidationWindow.cs +++ b/Questionable/Windows/QuestValidationWindow.cs @@ -3,6 +3,7 @@ using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Utility.Raii; +using Dalamud.Plugin; using FFXIVClientStructs.FFXIV.Common.Math; using ImGuiNET; using LLib.ImGui; @@ -16,11 +17,13 @@ internal sealed class QuestValidationWindow : LWindow { private readonly QuestValidator _questValidator; private readonly QuestData _questData; + private readonly IDalamudPluginInterface _pluginInterface; - public QuestValidationWindow(QuestValidator questValidator, QuestData questData) : base("Quest Validation###QuestionableValidator") + public QuestValidationWindow(QuestValidator questValidator, QuestData questData, IDalamudPluginInterface pluginInterface) : base("Quest Validation###QuestionableValidator") { _questValidator = questValidator; _questData = questData; + _pluginInterface = pluginInterface; Size = new Vector2(600, 200); SizeCondition = ImGuiCond.Once; @@ -41,8 +44,8 @@ internal sealed class QuestValidationWindow : LWindow ImGui.TableSetupColumn("Quest", ImGuiTableColumnFlags.WidthFixed, 50); ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 200); - ImGui.TableSetupColumn("Sq", ImGuiTableColumnFlags.WidthFixed, 30); - ImGui.TableSetupColumn("Sp", ImGuiTableColumnFlags.WidthFixed, 30); + ImGui.TableSetupColumn("Seq", ImGuiTableColumnFlags.WidthFixed, 30); + ImGui.TableSetupColumn("Step", ImGuiTableColumnFlags.WidthFixed, 30); ImGui.TableSetupColumn("Issue", ImGuiTableColumnFlags.None, 200); ImGui.TableHeadersRow(); @@ -63,7 +66,24 @@ internal sealed class QuestValidationWindow : LWindow ImGui.TextUnformatted(validationIssue.Step?.ToString(CultureInfo.InvariantCulture) ?? string.Empty); if (ImGui.TableNextColumn()) + { + // ReSharper disable once UnusedVariable + using (var font = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) + { + if (validationIssue.Severity == EIssueSeverity.Error) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + ImGui.TextUnformatted(FontAwesomeIcon.TimesCircle.ToIconString()); + } + else + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.ParsedBlue); + ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString()); + } + } + ImGui.SameLine(); ImGui.TextUnformatted(validationIssue.Description); + } } } } diff --git a/Questionable/Windows/QuestWindow.cs b/Questionable/Windows/QuestWindow.cs index 92d87681..9fd5c9ec 100644 --- a/Questionable/Windows/QuestWindow.cs +++ b/Questionable/Windows/QuestWindow.cs @@ -581,10 +581,16 @@ internal sealed class QuestWindow : LWindow, IPersistableWindowConfig { ImGui.SameLine(); - using var textColor = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + bool colored = _questRegistry.ValidationErrorCount > 0; + if (colored) + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Flag, $"{_questRegistry.ValidationIssueCount}")) _questValidationWindow.IsOpen = true; + + if (colored) + ImGui.PopStyleColor(); } }