Schema update
authorLiza Carvelli <liza@carvel.li>
Fri, 2 Aug 2024 16:30:21 +0000 (18:30 +0200)
committerLiza Carvelli <liza@carvel.li>
Fri, 2 Aug 2024 16:30:21 +0000 (18:30 +0200)
152 files changed:
GatheringPathRenderer/GatheringPathRenderer.csproj [new file with mode: 0644]
GatheringPathRenderer/RendererPlugin.cs [new file with mode: 0644]
GatheringPathRenderer/packages.lock.json [new file with mode: 0644]
GatheringPaths/2.x - A Realm Reborn/.gitkeep [new file with mode: 0644]
GatheringPaths/3.x - Heavensward/.gitkeep [new file with mode: 0644]
GatheringPaths/4.x - Stormblood/.gitkeep [new file with mode: 0644]
GatheringPaths/5.x - Shadowbringers/.gitkeep [new file with mode: 0644]
GatheringPaths/6.x - Endwalker/Thavnair/820_Pewter Ore.json [new file with mode: 0644]
GatheringPaths/7.x - Dawntrail/.gitkeep [new file with mode: 0644]
GatheringPaths/AssemblyGatheringLocationLoader.cs [new file with mode: 0644]
GatheringPaths/GatheringPaths.csproj [new file with mode: 0644]
GatheringPaths/gatheringlocation-v1.json [new file with mode: 0644]
GatheringPaths/packages.lock.json [new file with mode: 0644]
QuestPathGenerator.Tests/QuestGeneratorTest.cs
QuestPathGenerator/GatheringSourceGenerator.cs [new file with mode: 0644]
QuestPathGenerator/QuestSourceGenerator.cs
QuestPathGenerator/RoslynShortcuts.cs
QuestPathGenerator/Utils.cs [new file with mode: 0644]
QuestPaths/AssemblyQuestLoader.cs
QuestPaths/QuestPaths.csproj
QuestPaths/quest-v1.json
Questionable.Model/AssemblyModelLoader.cs [new file with mode: 0644]
Questionable.Model/Common/Converter/AetheryteConverter.cs [new file with mode: 0644]
Questionable.Model/Common/Converter/EnumConverter.cs [new file with mode: 0644]
Questionable.Model/Common/Converter/StringListOrValueConverter.cs [new file with mode: 0644]
Questionable.Model/Common/Converter/VectorConverter.cs [new file with mode: 0644]
Questionable.Model/Common/EAetheryteLocation.cs [new file with mode: 0644]
Questionable.Model/Gathering/GatheringNodeLocation.cs [new file with mode: 0644]
Questionable.Model/Gathering/GatheringRoot.cs [new file with mode: 0644]
Questionable.Model/Questing/AethernetShortcut.cs [new file with mode: 0644]
Questionable.Model/Questing/ChatMessage.cs [new file with mode: 0644]
Questionable.Model/Questing/ComplexCombatData.cs [new file with mode: 0644]
Questionable.Model/Questing/Converter/ActionConverter.cs [new file with mode: 0644]
Questionable.Model/Questing/Converter/AethernetShardConverter.cs [new file with mode: 0644]
Questionable.Model/Questing/Converter/AethernetShortcutConverter.cs [new file with mode: 0644]
Questionable.Model/Questing/Converter/DialogueChoiceTypeConverter.cs [new file with mode: 0644]
Questionable.Model/Questing/Converter/EmoteConverter.cs [new file with mode: 0644]
Questionable.Model/Questing/Converter/EnemySpawnTypeConverter.cs [new file with mode: 0644]
Questionable.Model/Questing/Converter/ExcelRefConverter.cs [new file with mode: 0644]
Questionable.Model/Questing/Converter/InteractionTypeConverter.cs [new file with mode: 0644]
Questionable.Model/Questing/Converter/JumpTypeConverter.cs [new file with mode: 0644]
Questionable.Model/Questing/Converter/LockedSkipConditionConverter.cs [new file with mode: 0644]
Questionable.Model/Questing/Converter/QuestWorkConfigConverter.cs [new file with mode: 0644]
Questionable.Model/Questing/Converter/QuestWorkModeConverter.cs [new file with mode: 0644]
Questionable.Model/Questing/Converter/SkipConditionConverter.cs [new file with mode: 0644]
Questionable.Model/Questing/DialogueChoice.cs [new file with mode: 0644]
Questionable.Model/Questing/EAction.cs [new file with mode: 0644]
Questionable.Model/Questing/EDialogChoiceType.cs [new file with mode: 0644]
Questionable.Model/Questing/EEmote.cs [new file with mode: 0644]
Questionable.Model/Questing/EEnemySpawnType.cs [new file with mode: 0644]
Questionable.Model/Questing/EExtraSkipCondition.cs [new file with mode: 0644]
Questionable.Model/Questing/EInteractionType.cs [new file with mode: 0644]
Questionable.Model/Questing/EJumpType.cs [new file with mode: 0644]
Questionable.Model/Questing/ELockedSkipCondition.cs [new file with mode: 0644]
Questionable.Model/Questing/EQuestWorkMode.cs [new file with mode: 0644]
Questionable.Model/Questing/ExcelRef.cs [new file with mode: 0644]
Questionable.Model/Questing/JumpDestination.cs [new file with mode: 0644]
Questionable.Model/Questing/QuestRoot.cs [new file with mode: 0644]
Questionable.Model/Questing/QuestSequence.cs [new file with mode: 0644]
Questionable.Model/Questing/QuestStep.cs [new file with mode: 0644]
Questionable.Model/Questing/QuestWorkValue.cs [new file with mode: 0644]
Questionable.Model/Questing/SkipAetheryteCondition.cs [new file with mode: 0644]
Questionable.Model/Questing/SkipConditions.cs [new file with mode: 0644]
Questionable.Model/Questing/SkipItemConditions.cs [new file with mode: 0644]
Questionable.Model/Questing/SkipStepConditions.cs [new file with mode: 0644]
Questionable.Model/Questionable.Model.csproj
Questionable.Model/V1/AethernetShortcut.cs [deleted file]
Questionable.Model/V1/ChatMessage.cs [deleted file]
Questionable.Model/V1/ComplexCombatData.cs [deleted file]
Questionable.Model/V1/Converter/ActionConverter.cs [deleted file]
Questionable.Model/V1/Converter/AethernetShardConverter.cs [deleted file]
Questionable.Model/V1/Converter/AethernetShortcutConverter.cs [deleted file]
Questionable.Model/V1/Converter/AetheryteConverter.cs [deleted file]
Questionable.Model/V1/Converter/DialogueChoiceTypeConverter.cs [deleted file]
Questionable.Model/V1/Converter/EmoteConverter.cs [deleted file]
Questionable.Model/V1/Converter/EnemySpawnTypeConverter.cs [deleted file]
Questionable.Model/V1/Converter/EnumConverter.cs [deleted file]
Questionable.Model/V1/Converter/ExcelRefConverter.cs [deleted file]
Questionable.Model/V1/Converter/InteractionTypeConverter.cs [deleted file]
Questionable.Model/V1/Converter/JumpTypeConverter.cs [deleted file]
Questionable.Model/V1/Converter/LockedSkipConditionConverter.cs [deleted file]
Questionable.Model/V1/Converter/QuestWorkConfigConverter.cs [deleted file]
Questionable.Model/V1/Converter/QuestWorkModeConverter.cs [deleted file]
Questionable.Model/V1/Converter/SkipConditionConverter.cs [deleted file]
Questionable.Model/V1/Converter/StringListOrValueConverter.cs [deleted file]
Questionable.Model/V1/Converter/VectorConverter.cs [deleted file]
Questionable.Model/V1/DialogueChoice.cs [deleted file]
Questionable.Model/V1/EAction.cs [deleted file]
Questionable.Model/V1/EAetheryteLocation.cs [deleted file]
Questionable.Model/V1/EDialogChoiceType.cs [deleted file]
Questionable.Model/V1/EEmote.cs [deleted file]
Questionable.Model/V1/EEnemySpawnType.cs [deleted file]
Questionable.Model/V1/EExtraSkipCondition.cs [deleted file]
Questionable.Model/V1/EInteractionType.cs [deleted file]
Questionable.Model/V1/EJumpType.cs [deleted file]
Questionable.Model/V1/ELockedSkipCondition.cs [deleted file]
Questionable.Model/V1/EQuestWorkMode.cs [deleted file]
Questionable.Model/V1/ExcelRef.cs [deleted file]
Questionable.Model/V1/JumpDestination.cs [deleted file]
Questionable.Model/V1/QuestRoot.cs [deleted file]
Questionable.Model/V1/QuestSequence.cs [deleted file]
Questionable.Model/V1/QuestStep.cs [deleted file]
Questionable.Model/V1/QuestWorkValue.cs [deleted file]
Questionable.Model/V1/SkipAetheryteCondition.cs [deleted file]
Questionable.Model/V1/SkipConditions.cs [deleted file]
Questionable.Model/V1/SkipItemConditions.cs [deleted file]
Questionable.Model/V1/SkipStepConditions.cs [deleted file]
Questionable.Model/common-schema.json [new file with mode: 0644]
Questionable.sln
Questionable/ChatFunctions.cs
Questionable/Controller/CombatController.cs
Questionable/Controller/GameUiController.cs
Questionable/Controller/MovementController.cs
Questionable/Controller/QuestController.cs
Questionable/Controller/QuestRegistry.cs
Questionable/Controller/Steps/Common/NextQuest.cs
Questionable/Controller/Steps/ITaskFactory.cs
Questionable/Controller/Steps/Interactions/Action.cs
Questionable/Controller/Steps/Interactions/AetherCurrent.cs
Questionable/Controller/Steps/Interactions/AethernetShard.cs
Questionable/Controller/Steps/Interactions/Aetheryte.cs
Questionable/Controller/Steps/Interactions/Combat.cs
Questionable/Controller/Steps/Interactions/Dive.cs
Questionable/Controller/Steps/Interactions/Duty.cs
Questionable/Controller/Steps/Interactions/Emote.cs
Questionable/Controller/Steps/Interactions/EquipItem.cs
Questionable/Controller/Steps/Interactions/Interact.cs
Questionable/Controller/Steps/Interactions/Jump.cs
Questionable/Controller/Steps/Interactions/Say.cs
Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs
Questionable/Controller/Steps/Interactions/UseItem.cs
Questionable/Controller/Steps/Shared/AethernetShortcut.cs
Questionable/Controller/Steps/Shared/AetheryteShortcut.cs
Questionable/Controller/Steps/Shared/Move.cs
Questionable/Controller/Steps/Shared/SkipCondition.cs
Questionable/Controller/Steps/Shared/StepDisabled.cs
Questionable/Controller/Steps/Shared/WaitAtEnd.cs
Questionable/Controller/Steps/Shared/WaitAtStart.cs
Questionable/Controller/Utils/QuestWorkUtils.cs
Questionable/Data/AetheryteData.cs
Questionable/External/LifestreamIpc.cs
Questionable/GameFunctions.cs
Questionable/Model/Quest.cs
Questionable/Validation/Validators/AethernetShortcutValidator.cs
Questionable/Validation/Validators/BasicSequenceValidator.cs
Questionable/Validation/Validators/CompletionFlagsValidator.cs
Questionable/Validation/Validators/JsonSchemaValidator.cs
Questionable/Validation/Validators/UniqueStartStopValidator.cs
Questionable/Windows/DebugOverlay.cs
Questionable/Windows/QuestComponents/ActiveQuestComponent.cs
Questionable/Windows/QuestComponents/CreationUtilsComponent.cs
Questionable/Windows/QuestSelectionWindow.cs

diff --git a/GatheringPathRenderer/GatheringPathRenderer.csproj b/GatheringPathRenderer/GatheringPathRenderer.csproj
new file mode 100644 (file)
index 0000000..5d22cd3
--- /dev/null
@@ -0,0 +1,3 @@
+<Project Sdk="Dalamud.NET.Sdk/10.0.0">
+    <Import Project="..\LLib\LLib.targets"/>
+</Project>
diff --git a/GatheringPathRenderer/RendererPlugin.cs b/GatheringPathRenderer/RendererPlugin.cs
new file mode 100644 (file)
index 0000000..d33eae9
--- /dev/null
@@ -0,0 +1,11 @@
+using Dalamud.Plugin;
+
+namespace GatheringPathRenderer;
+
+public sealed class RendererPlugin : IDalamudPlugin
+{
+    public void Dispose()
+    {
+
+    }
+}
diff --git a/GatheringPathRenderer/packages.lock.json b/GatheringPathRenderer/packages.lock.json
new file mode 100644 (file)
index 0000000..0c669eb
--- /dev/null
@@ -0,0 +1,81 @@
+{
+  "version": 1,
+  "dependencies": {
+    "net8.0-windows7.0": {
+      "DalamudPackager": {
+        "type": "Direct",
+        "requested": "[2.1.13, )",
+        "resolved": "2.1.13",
+        "contentHash": "rMN1omGe8536f4xLMvx9NwfvpAc9YFFfeXJ1t4P4PE6Gu8WCIoFliR1sh07hM+bfODmesk/dvMbji7vNI+B/pQ=="
+      },
+      "DotNet.ReproducibleBuilds": {
+        "type": "Direct",
+        "requested": "[1.1.1, )",
+        "resolved": "1.1.1",
+        "contentHash": "+H2t/t34h6mhEoUvHi8yGXyuZ2GjSovcGYehJrS2MDm2XgmPfZL2Sdxg+uL2lKgZ4M6tTwKHIlxOob2bgh0NRQ==",
+        "dependencies": {
+          "Microsoft.SourceLink.AzureRepos.Git": "1.1.1",
+          "Microsoft.SourceLink.Bitbucket.Git": "1.1.1",
+          "Microsoft.SourceLink.GitHub": "1.1.1",
+          "Microsoft.SourceLink.GitLab": "1.1.1"
+        }
+      },
+      "Microsoft.SourceLink.Gitea": {
+        "type": "Direct",
+        "requested": "[8.0.0, )",
+        "resolved": "8.0.0",
+        "contentHash": "KOBodmDnlWGIqZt2hT47Q69TIoGhIApDVLCyyj9TT5ct8ju16AbHYcB4XeknoHX562wO1pMS/1DfBIZK+V+sxg==",
+        "dependencies": {
+          "Microsoft.Build.Tasks.Git": "8.0.0",
+          "Microsoft.SourceLink.Common": "8.0.0"
+        }
+      },
+      "Microsoft.Build.Tasks.Git": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
+      },
+      "Microsoft.SourceLink.AzureRepos.Git": {
+        "type": "Transitive",
+        "resolved": "1.1.1",
+        "contentHash": "qB5urvw9LO2bG3eVAkuL+2ughxz2rR7aYgm2iyrB8Rlk9cp2ndvGRCvehk3rNIhRuNtQaeKwctOl1KvWiklv5w==",
+        "dependencies": {
+          "Microsoft.Build.Tasks.Git": "1.1.1",
+          "Microsoft.SourceLink.Common": "1.1.1"
+        }
+      },
+      "Microsoft.SourceLink.Bitbucket.Git": {
+        "type": "Transitive",
+        "resolved": "1.1.1",
+        "contentHash": "cDzxXwlyWpLWaH0em4Idj0H3AmVo3L/6xRXKssYemx+7W52iNskj/SQ4FOmfCb8YQt39otTDNMveCZzYtMoucQ==",
+        "dependencies": {
+          "Microsoft.Build.Tasks.Git": "1.1.1",
+          "Microsoft.SourceLink.Common": "1.1.1"
+        }
+      },
+      "Microsoft.SourceLink.Common": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
+      },
+      "Microsoft.SourceLink.GitHub": {
+        "type": "Transitive",
+        "resolved": "1.1.1",
+        "contentHash": "IaJGnOv/M7UQjRJks7B6p7pbPnOwisYGOIzqCz5ilGFTApZ3ktOR+6zJ12ZRPInulBmdAf1SrGdDG2MU8g6XTw==",
+        "dependencies": {
+          "Microsoft.Build.Tasks.Git": "1.1.1",
+          "Microsoft.SourceLink.Common": "1.1.1"
+        }
+      },
+      "Microsoft.SourceLink.GitLab": {
+        "type": "Transitive",
+        "resolved": "1.1.1",
+        "contentHash": "tvsg47DDLqqedlPeYVE2lmiTpND8F0hkrealQ5hYltSmvruy/Gr5nHAKSsjyw5L3NeM/HLMI5ORv7on/M4qyZw==",
+        "dependencies": {
+          "Microsoft.Build.Tasks.Git": "1.1.1",
+          "Microsoft.SourceLink.Common": "1.1.1"
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/GatheringPaths/2.x - A Realm Reborn/.gitkeep b/GatheringPaths/2.x - A Realm Reborn/.gitkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/GatheringPaths/3.x - Heavensward/.gitkeep b/GatheringPaths/3.x - Heavensward/.gitkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/GatheringPaths/4.x - Stormblood/.gitkeep b/GatheringPaths/4.x - Stormblood/.gitkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/GatheringPaths/5.x - Shadowbringers/.gitkeep b/GatheringPaths/5.x - Shadowbringers/.gitkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/GatheringPaths/6.x - Endwalker/Thavnair/820_Pewter Ore.json b/GatheringPaths/6.x - Endwalker/Thavnair/820_Pewter Ore.json
new file mode 100644 (file)
index 0000000..425d77e
--- /dev/null
@@ -0,0 +1,56 @@
+{
+  "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json",
+  "Author": "liza",
+  "TerritoryId": 957,
+  "AetheryteShortcut": "Thavnair - Great Work",
+  "Nodes": [
+    {
+      "DataId": 33918,
+      "Position": {
+        "X": -582.5132,
+        "Y": 40.54578,
+        "Z": -426.0171
+      }
+    },
+    {
+      "DataId": 33919,
+      "Position": {
+        "X": -578.2101,
+        "Y": 41.27147,
+        "Z": -447.6376
+      }
+    },
+    {
+      "DataId": 33920,
+      "Position": {
+        "X": -488.2276,
+        "Y": 34.71221,
+        "Z": -359.6945
+      }
+    },
+    {
+      "DataId": 33921,
+      "Position": {
+        "X": -498.8687,
+        "Y": 31.08014,
+        "Z": -351.9397
+      }
+    },
+    {
+      "DataId": 33922,
+      "Position": {
+        "X": -304.0609,
+        "Y": 68.76999,
+        "Z": -479.1875
+      }
+    },
+    {
+      "DataId": 33923,
+      "Position": {
+        "X": -293.6989,
+        "Y": 68.77935,
+        "Z": -484.2256
+      }
+    }
+  ]
+}
diff --git a/GatheringPaths/7.x - Dawntrail/.gitkeep b/GatheringPaths/7.x - Dawntrail/.gitkeep
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/GatheringPaths/AssemblyGatheringLocationLoader.cs b/GatheringPaths/AssemblyGatheringLocationLoader.cs
new file mode 100644 (file)
index 0000000..0417641
--- /dev/null
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using Questionable.Model.Gathering;
+
+namespace Questionable.GatheringPaths;
+
+[SuppressMessage("ReSharper", "PartialTypeWithSinglePart", Justification = "Required for RELEASE")]
+public static partial class AssemblyGatheringLocationLoader
+{
+    private static Dictionary<ushort, GatheringRoot>? _locations;
+
+    public static IReadOnlyDictionary<ushort, GatheringRoot> GetLocations()
+    {
+        if (_locations == null)
+        {
+            _locations = [];
+#if RELEASE
+            LoadLocations();
+#endif
+        }
+
+        return _locations ?? throw new InvalidOperationException("quest data is not initialized");
+    }
+
+    public static Stream QuestSchema =>
+        typeof(AssemblyGatheringLocationLoader).Assembly.GetManifestResourceStream("Questionable.GatheringPaths.GatheringLocationSchema")!;
+
+    [SuppressMessage("ReSharper", "UnusedMember.Local")]
+    private static void AddLocation(ushort questId, GatheringRoot root) => _locations![questId] = root;
+}
diff --git a/GatheringPaths/GatheringPaths.csproj b/GatheringPaths/GatheringPaths.csproj
new file mode 100644 (file)
index 0000000..f9e9725
--- /dev/null
@@ -0,0 +1,43 @@
+<Project Sdk="Microsoft.NET.Sdk">
+    <PropertyGroup>
+        <TargetFramework>net8.0-windows</TargetFramework>
+        <LangVersion>12</LangVersion>
+        <Nullable>enable</Nullable>
+        <RootNamespace>Questionable.GatheringPaths</RootNamespace>
+        <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+        <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
+        <DebugType>none</DebugType>
+        <PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap>
+        <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
+        <Platforms>x64</Platforms>
+    </PropertyGroup>
+
+    <ItemGroup>
+        <ProjectReference Include="..\Questionable.Model\Questionable.Model.csproj" />
+        <ProjectReference Include="..\QuestPathGenerator\QuestPathGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
+    </ItemGroup>
+
+    <ItemGroup>
+        <None Remove="gatheringlocation-v1.json" />
+        <EmbeddedResource Include="gatheringlocation-v1.json">
+            <LogicalName>Questionable.GatheringPaths.GatheringLocationSchema</LogicalName>
+        </EmbeddedResource>
+        <AdditionalFiles Include="gatheringlocation-v1.json" />
+        <AdditionalFiles Include="..\Questionable.Model\common-schema.json" />
+    </ItemGroup>
+
+    <ItemGroup Condition="'$(Configuration)' == 'Release'">
+        <None Remove="2.x - A Realm Reborn" />
+        <None Remove="3.x - Heavensward" />
+        <None Remove="4.x - Stormblood" />
+        <None Remove="5.x - Shadowbringers" />
+        <None Remove="6.x - Endwalker" />
+        <None Remove="7.x - Dawntrail" />
+        <AdditionalFiles Include="2.x - A Realm Reborn\**\*.json" />
+        <AdditionalFiles Include="3.x - Heavensward\**\*.json" />
+        <AdditionalFiles Include="4.x - Stormblood\**\*.json" />
+        <AdditionalFiles Include="5.x - Shadowbringers\**\*.json" />
+        <AdditionalFiles Include="6.x - Endwalker\**\*.json" />
+        <AdditionalFiles Include="7.x - Dawntrail\**\*.json" />
+    </ItemGroup>
+</Project>
diff --git a/GatheringPaths/gatheringlocation-v1.json b/GatheringPaths/gatheringlocation-v1.json
new file mode 100644 (file)
index 0000000..53b34f7
--- /dev/null
@@ -0,0 +1,98 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json",
+  "title": "Gathering Location V1",
+  "description": "A series of gathering locationsk",
+  "type": "object",
+  "properties": {
+    "$schema": {
+      "type": "string",
+      "const": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json"
+    },
+    "Author": {
+      "description": "Author of the gathering location data",
+      "type": [
+        "string",
+        "array"
+      ],
+      "items": {
+        "type": "string"
+      }
+    },
+    "TerritoryId": {
+      "type": "number"
+    },
+    "AetheryteShortcut": {
+      "$ref": "https://git.carvel.li/liza/Questionable/raw/branch/master/Questionable.Model/common-schema.json#/$defs/Aetheryte"
+    },
+    "Nodes": {
+      "type": "array",
+      "items": {
+        "type": "object",
+        "properties": {
+          "DataId": {
+            "type": "number",
+            "minimum": 30000,
+            "maximum": 50000
+          },
+          "Position": {
+            "$ref": "#/$defs/Vector3"
+          },
+          "MinimumAngle": {
+            "type": "number",
+            "minimum": -360,
+            "maximum": 360
+          },
+          "MaximumAngle": {
+            "type": "number",
+            "minimum": -360,
+            "maximum": 360
+          },
+          "MinimumDistance": {
+            "type": "number",
+            "minimum": 0
+          },
+          "MaximumDistance": {
+            "type": "number",
+            "exclusiveMinimum": 0
+          }
+        },
+        "required": [
+          "DataId",
+          "Position"
+        ],
+        "additionalProperties": false
+      }
+    }
+  },
+  "required": [
+    "$schema",
+    "Author",
+    "TerritoryId",
+    "AetheryteShortcut",
+    "Nodes"
+  ],
+  "additionalProperties": false,
+  "$defs": {
+    "Vector3": {
+      "type": "object",
+      "description": "Position to (typically) walk to",
+      "properties": {
+        "X": {
+          "type": "number"
+        },
+        "Y": {
+          "type": "number"
+        },
+        "Z": {
+          "type": "number"
+        }
+      },
+      "required": [
+        "X",
+        "Y",
+        "Z"
+      ]
+    }
+  }
+}
diff --git a/GatheringPaths/packages.lock.json b/GatheringPaths/packages.lock.json
new file mode 100644 (file)
index 0000000..408e267
--- /dev/null
@@ -0,0 +1,26 @@
+{
+  "version": 1,
+  "dependencies": {
+    "net8.0-windows7.0": {
+      "System.Text.Encodings.Web": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ=="
+      },
+      "System.Text.Json": {
+        "type": "Transitive",
+        "resolved": "8.0.4",
+        "contentHash": "bAkhgDJ88XTsqczoxEMliSrpijKZHhbJQldhAmObj/RbrN3sU5dcokuXmWJWsdQAhiMJ9bTayWsL1C9fbbCRhw==",
+        "dependencies": {
+          "System.Text.Encodings.Web": "8.0.0"
+        }
+      },
+      "questionable.model": {
+        "type": "Project",
+        "dependencies": {
+          "System.Text.Json": "[8.0.4, )"
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
index 55d4cd64b73f5b746faba852b4c068e85c762e11..f08606c187c08dbf69bafbf62385549348ee1cc0 100644 (file)
@@ -1,4 +1,4 @@
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 using Questionable.QuestPathGenerator;
 using Xunit;
 
diff --git a/QuestPathGenerator/GatheringSourceGenerator.cs b/QuestPathGenerator/GatheringSourceGenerator.cs
new file mode 100644 (file)
index 0000000..a10acd0
--- /dev/null
@@ -0,0 +1,163 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using Json.Schema;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Questionable.Model.Gathering;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+using static Questionable.QuestPathGenerator.RoslynShortcuts;
+
+namespace Questionable.QuestPathGenerator;
+
+[Generator]
+[SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008")]
+public class GatheringSourceGenerator : ISourceGenerator
+{
+    private static readonly DiagnosticDescriptor InvalidJson = new("GPG0001",
+        "Invalid JSON",
+        "Invalid gathering file: {0}",
+        nameof(GatheringSourceGenerator),
+        DiagnosticSeverity.Error,
+        true);
+
+    public void Initialize(GeneratorInitializationContext context)
+    {
+        // No initialization required for this generator.
+    }
+
+    public void Execute(GeneratorExecutionContext context)
+    {
+        // Find schema definition
+        AdditionalText? gatheringSchema =
+            context.AdditionalFiles.SingleOrDefault(x => Path.GetFileName(x.Path) == "gatheringlocation-v1.json");
+        if (gatheringSchema != null)
+            GenerateGatheringSource(context, gatheringSchema);
+    }
+
+    private void GenerateGatheringSource(GeneratorExecutionContext context, AdditionalText jsonSchemaFile)
+    {
+        var gatheringSchema = JsonSchema.FromText(jsonSchemaFile.GetText()!.ToString());
+
+        List<(ushort, GatheringRoot)> gatheringLocations = [];
+        foreach (var (id, node) in Utils.GetAdditionalFiles(context, jsonSchemaFile, gatheringSchema, InvalidJson))
+        {
+            var gatheringLocation = node.Deserialize<GatheringRoot>()!;
+            gatheringLocations.Add((id, gatheringLocation));
+        }
+
+        if (gatheringLocations.Count == 0)
+            return;
+
+        var partitionedLocations = gatheringLocations
+            .OrderBy(x => x.Item1)
+            .GroupBy(x => $"LoadLocation{x.Item1 / 100}")
+            .ToList();
+
+        var methods = Utils.CreateMethods("LoadLocations", partitionedLocations, CreateInitializer);
+
+        var code =
+            CompilationUnit()
+                .WithUsings(
+                    List(
+                        new[]
+                        {
+                            UsingDirective(
+                                IdentifierName("System")),
+                            UsingDirective(
+                                QualifiedName(
+                                    IdentifierName("System"),
+                                    IdentifierName("Numerics"))),
+                            UsingDirective(
+                                QualifiedName(
+                                    IdentifierName("System"),
+                                    IdentifierName("IO"))),
+                            UsingDirective(
+                                QualifiedName(
+                                    QualifiedName(
+                                        IdentifierName("System"), IdentifierName("Collections")),
+                                    IdentifierName("Generic"))),
+                            UsingDirective(
+                                QualifiedName(
+                                    QualifiedName(
+                                        IdentifierName("Questionable"),
+                                        IdentifierName("Model")),
+                                    IdentifierName("Gathering"))),
+                            UsingDirective(
+                                QualifiedName(
+                                    QualifiedName(
+                                        IdentifierName("Questionable"),
+                                        IdentifierName("Model")),
+                                    IdentifierName("Common")))
+                        }))
+                .WithMembers(
+                    SingletonList<MemberDeclarationSyntax>(
+                        FileScopedNamespaceDeclaration(
+                                QualifiedName(
+                                    IdentifierName("Questionable"),
+                                    IdentifierName("GatheringPaths")))
+                            .WithMembers(
+                                SingletonList<MemberDeclarationSyntax>(
+                                    ClassDeclaration("AssemblyGatheringLocationLoader")
+                                        .WithModifiers(
+                                            TokenList(Token(SyntaxKind.PartialKeyword)))
+                                        .WithMembers(List<MemberDeclarationSyntax>(methods))))))
+                .NormalizeWhitespace();
+
+        // Add the source code to the compilation.
+        context.AddSource("AssemblyGatheringLocationLoader.g.cs", code.ToFullString());
+    }
+
+    private static StatementSyntax[] CreateInitializer(List<(ushort QuestId, GatheringRoot Root)> quests)
+    {
+        List<StatementSyntax> statements = [];
+
+        foreach (var quest in quests)
+        {
+            statements.Add(
+                ExpressionStatement(
+                    InvocationExpression(
+                            IdentifierName("AddLocation"))
+                        .WithArgumentList(
+                            ArgumentList(
+                                SeparatedList<ArgumentSyntax>(
+                                    new SyntaxNodeOrToken[]
+                                    {
+                                        Argument(
+                                            LiteralExpression(SyntaxKind.NumericLiteralExpression,
+                                                Literal(quest.QuestId))),
+                                        Token(SyntaxKind.CommaToken),
+                                        Argument(CreateGatheringRootExpression(quest.QuestId, quest.Root))
+                                    })))));
+        }
+
+        return statements.ToArray();
+    }
+
+    private static ObjectCreationExpressionSyntax CreateGatheringRootExpression(ushort locationId, GatheringRoot root)
+    {
+        try
+        {
+            return ObjectCreationExpression(
+                    IdentifierName(nameof(GatheringRoot)))
+                .WithInitializer(
+                    InitializerExpression(
+                        SyntaxKind.ObjectInitializerExpression,
+                        SeparatedList<ExpressionSyntax>(
+                            SyntaxNodeList(
+                                AssignmentList(nameof(GatheringRoot.Author), root.Author).AsSyntaxNodeOrToken(),
+                                Assignment(nameof(GatheringRoot.TerritoryId), root.TerritoryId, default)
+                                    .AsSyntaxNodeOrToken(),
+                                Assignment(nameof(GatheringRoot.AetheryteShortcut), root.AetheryteShortcut, null),
+                                AssignmentList(nameof(GatheringRoot.Nodes), root.Nodes).AsSyntaxNodeOrToken()))));
+        }
+        catch (Exception e)
+        {
+            throw new Exception($"GatheringGen[{locationId}]: {e.Message}", e);
+        }
+    }
+}
index 47d311f206591cc2e7bba542c58e47d0f21c088b..b6cccbaba0b4cd23e74bdda5112ff7f38b8ae530 100644 (file)
@@ -1,16 +1,14 @@
 using System;
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Text.Json;
-using System.Text.Json.Nodes;
 using Json.Schema;
 using Microsoft.CodeAnalysis;
 using Microsoft.CodeAnalysis.CSharp;
 using Microsoft.CodeAnalysis.CSharp.Syntax;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
 using static Questionable.QuestPathGenerator.RoslynShortcuts;
 
@@ -38,47 +36,21 @@ public class QuestSourceGenerator : ISourceGenerator
 
     public void Execute(GeneratorExecutionContext context)
     {
-        List<(ushort, QuestRoot)> quests = [];
-
         // Find schema definition
-        AdditionalText jsonSchemaFile =
-            context.AdditionalFiles.Single(x => Path.GetFileName(x.Path) == "quest-v1.json");
+        AdditionalText? questSchema =
+            context.AdditionalFiles.SingleOrDefault(x => Path.GetFileName(x.Path) == "quest-v1.json");
+        if (questSchema != null)
+            GenerateQuestSource(context, questSchema);
+    }
+
+    private void GenerateQuestSource(GeneratorExecutionContext context, AdditionalText jsonSchemaFile)
+    {
         var questSchema = JsonSchema.FromText(jsonSchemaFile.GetText()!.ToString());
 
-        // Go through all files marked as an Additional File in file properties.
-        foreach (var additionalFile in context.AdditionalFiles)
+        List<(ushort, QuestRoot)> quests = [];
+        foreach (var (id, node) in Utils.GetAdditionalFiles(context, jsonSchemaFile, questSchema, InvalidJson))
         {
-            if (additionalFile == null || additionalFile == jsonSchemaFile)
-                continue;
-
-            if (Path.GetExtension(additionalFile.Path) != ".json")
-                continue;
-
-            string name = Path.GetFileName(additionalFile.Path);
-            if (!name.Contains('_'))
-                continue;
-
-            ushort id = ushort.Parse(name.Substring(0, name.IndexOf('_')));
-
-            var text = additionalFile.GetText();
-            if (text == null)
-                continue;
-
-            var questNode = JsonNode.Parse(text.ToString());
-            var evaluationResult = questSchema.Evaluate(questNode, new EvaluationOptions
-            {
-                Culture = CultureInfo.InvariantCulture,
-                OutputFormat = OutputFormat.List
-            });
-            if (!evaluationResult.IsValid)
-            {
-                var error = Diagnostic.Create(InvalidJson,
-                    null,
-                    Path.GetFileName(additionalFile.Path));
-                context.ReportDiagnostic(error);
-            }
-
-            var quest = questNode.Deserialize<QuestRoot>()!;
+            var quest = node.Deserialize<QuestRoot>()!;
             if (quest.Disabled)
             {
                 quest.Author = [];
@@ -97,38 +69,7 @@ public class QuestSourceGenerator : ISourceGenerator
             .GroupBy(x => $"LoadQuests{x.Item1 / 50}")
             .ToList();
 
-        List<MethodDeclarationSyntax> methods =
-        [
-            MethodDeclaration(
-                    PredefinedType(
-                        Token(SyntaxKind.VoidKeyword)),
-                    Identifier("LoadQuests"))
-                .WithModifiers(
-                    TokenList(
-                        Token(SyntaxKind.PrivateKeyword),
-                        Token(SyntaxKind.StaticKeyword)))
-                .WithBody(
-                    Block(
-                        partitionedQuests
-                            .Select(x =>
-                                ExpressionStatement(
-                                    InvocationExpression(
-                                        IdentifierName(x.Key))))))
-        ];
-
-        foreach (var partition in partitionedQuests)
-        {
-            methods.Add(MethodDeclaration(
-                    PredefinedType(
-                        Token(SyntaxKind.VoidKeyword)),
-                    Identifier(partition.Key))
-                .WithModifiers(
-                    TokenList(
-                        Token(SyntaxKind.PrivateKeyword),
-                        Token(SyntaxKind.StaticKeyword)))
-                .WithBody(
-                    Block(CreateInitializer(partition.ToList()))));
-        }
+        var methods = Utils.CreateMethods("LoadQuests", partitionedQuests, CreateInitializer);
 
         var code =
             CompilationUnit()
@@ -156,7 +97,13 @@ public class QuestSourceGenerator : ISourceGenerator
                                     QualifiedName(
                                         IdentifierName("Questionable"),
                                         IdentifierName("Model")),
-                                    IdentifierName("V1")))
+                                    IdentifierName("Questing"))),
+                            UsingDirective(
+                                QualifiedName(
+                                    QualifiedName(
+                                        IdentifierName("Questionable"),
+                                        IdentifierName("Model")),
+                                    IdentifierName("Common")))
                         }))
                 .WithMembers(
                     SingletonList<MemberDeclarationSyntax>(
index 2bacaba4fc5724a30e60a3dd73e53cfc3f131e60..4f5a8fb008d42653c8233dea4212d580829bff03 100644 (file)
@@ -6,7 +6,9 @@ using System.Numerics;
 using Microsoft.CodeAnalysis;
 using Microsoft.CodeAnalysis.CSharp;
 using Microsoft.CodeAnalysis.CSharp.Syntax;
-using Questionable.Model.V1;
+using Questionable.Model.Common;
+using Questionable.Model.Gathering;
+using Questionable.Model.Questing;
 using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
 
 namespace Questionable.QuestPathGenerator;
@@ -312,6 +314,32 @@ public static class RoslynShortcuts
                                     Assignment(nameof(SkipAetheryteCondition.InSameTerritory),
                                         skipAetheryteCondition.InSameTerritory, emptyAetheryte.InSameTerritory)))));
             }
+            else if (value is GatheringNodeLocation nodeLocation)
+            {
+                var emptyLocation = new GatheringNodeLocation();
+                return ObjectCreationExpression(
+                        IdentifierName(nameof(GatheringNodeLocation)))
+                    .WithInitializer(
+                        InitializerExpression(
+                            SyntaxKind.ObjectInitializerExpression,
+                            SeparatedList<ExpressionSyntax>(
+                                SyntaxNodeList(
+                                    Assignment(nameof(GatheringNodeLocation.DataId), nodeLocation.DataId,
+                                            emptyLocation.DataId)
+                                        .AsSyntaxNodeOrToken(),
+                                    Assignment(nameof(GatheringNodeLocation.Position), nodeLocation.Position,
+                                        emptyLocation.Position).AsSyntaxNodeOrToken(),
+                                    Assignment(nameof(GatheringNodeLocation.MinimumAngle), nodeLocation.MinimumAngle,
+                                        emptyLocation.MinimumAngle).AsSyntaxNodeOrToken(),
+                                    Assignment(nameof(GatheringNodeLocation.MaximumAngle), nodeLocation.MaximumAngle,
+                                        emptyLocation.MaximumAngle).AsSyntaxNodeOrToken(),
+                                    Assignment(nameof(GatheringNodeLocation.MinimumDistance),
+                                            nodeLocation.MinimumDistance, emptyLocation.MinimumDistance)
+                                        .AsSyntaxNodeOrToken(),
+                                    Assignment(nameof(GatheringNodeLocation.MaximumDistance),
+                                            nodeLocation.MaximumDistance, emptyLocation.MaximumDistance)
+                                        .AsSyntaxNodeOrToken()))));
+            }
             else if (value is null)
                 return LiteralExpression(SyntaxKind.NullLiteralExpression);
         }
diff --git a/QuestPathGenerator/Utils.cs b/QuestPathGenerator/Utils.cs
new file mode 100644 (file)
index 0000000..12873a7
--- /dev/null
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text.Json.Nodes;
+using Json.Schema;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+
+namespace Questionable.QuestPathGenerator;
+
+public static class Utils
+{
+    public static IEnumerable<(ushort, JsonNode)> GetAdditionalFiles(GeneratorExecutionContext context,
+        AdditionalText jsonSchemaFile, JsonSchema jsonSchema, DiagnosticDescriptor invalidJson)
+    {
+        var commonSchemaFile = context.AdditionalFiles.Single(x => Path.GetFileName(x.Path) == "common-schema.json");
+        List<AdditionalText> jsonSchemaFiles = [jsonSchemaFile, commonSchemaFile];
+
+        SchemaRegistry.Global.Register(
+            new Uri("https://git.carvel.li/liza/Questionable/raw/branch/master/Questionable.Model/common-schema.json"),
+            JsonSchema.FromText(commonSchemaFile.GetText()!.ToString()));
+
+        foreach (var additionalFile in context.AdditionalFiles)
+        {
+            if (additionalFile == null || jsonSchemaFiles.Contains(additionalFile))
+                continue;
+
+            if (Path.GetExtension(additionalFile.Path) != ".json")
+                continue;
+
+            string name = Path.GetFileName(additionalFile.Path);
+            if (!name.Contains("_"))
+                continue;
+
+            ushort id = ushort.Parse(name.Substring(0, name.IndexOf('_')));
+
+            var text = additionalFile.GetText();
+            if (text == null)
+                continue;
+
+            var node = JsonNode.Parse(text.ToString());
+            if (node == null)
+                continue;
+
+            string? schemaLocation = node["$schema"]?.GetValue<string?>();
+            if (schemaLocation == null || new Uri(schemaLocation) != jsonSchema.GetId())
+                continue;
+
+            var evaluationResult = jsonSchema.Evaluate(node, new EvaluationOptions
+            {
+                Culture = CultureInfo.InvariantCulture,
+                OutputFormat = OutputFormat.List,
+            });
+            if (!evaluationResult.IsValid)
+            {
+                var error = Diagnostic.Create(invalidJson,
+                    null,
+                    Path.GetFileName(additionalFile.Path));
+                context.ReportDiagnostic(error);
+            }
+
+            yield return (id, node);
+        }
+    }
+
+    public static List<MethodDeclarationSyntax> CreateMethods<T>(string prefix,
+        List<IGrouping<string, (ushort, T)>> partitions,
+        Func<List<(ushort, T)>, StatementSyntax[]> toInitializers)
+    {
+        List<MethodDeclarationSyntax> methods =
+        [
+            MethodDeclaration(
+                    PredefinedType(
+                        Token(SyntaxKind.VoidKeyword)),
+                    Identifier(prefix))
+                .WithModifiers(
+                    TokenList(
+                        Token(SyntaxKind.PrivateKeyword),
+                        Token(SyntaxKind.StaticKeyword)))
+                .WithBody(
+                    Block(
+                        partitions
+                            .Select(x =>
+                                ExpressionStatement(
+                                    InvocationExpression(
+                                        IdentifierName(x.Key))))))
+        ];
+
+        foreach (var partition in partitions)
+        {
+            methods.Add(MethodDeclaration(
+                    PredefinedType(
+                        Token(SyntaxKind.VoidKeyword)),
+                    Identifier(partition.Key))
+                .WithModifiers(
+                    TokenList(
+                        Token(SyntaxKind.PrivateKeyword),
+                        Token(SyntaxKind.StaticKeyword)))
+                .WithBody(
+                    Block(toInitializers(partition.ToList()))));
+        }
+
+        return methods;
+    }
+}
index 90ba89a1a4889513e3bed692eaab30ba44e85ef4..3ef7df15ceb1af26c58174e5875d7a655c0992e6 100644 (file)
@@ -2,8 +2,7 @@
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.IO;
-using System.Reflection;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.QuestPaths;
 
index e914b62d5d512ecf980968d2235e218c0dc93718..5d91a016f3785ea43860cd1011a60edd6248ef16 100644 (file)
@@ -23,6 +23,7 @@
             <LogicalName>Questionable.QuestPaths.QuestSchema</LogicalName>
         </EmbeddedResource>
         <AdditionalFiles Include="quest-v1.json" />
+        <AdditionalFiles Include="..\Questionable.Model\common-schema.json" />
     </ItemGroup>
 
     <ItemGroup Condition="'$(Configuration)' == 'Release'">
index 751e4730ad4fd7a8f1960f4221f6f0f5f95737fe..a11a610c6be5d1687243eac6e181f11af3ca8c22 100644 (file)
                 },
                 "AetheryteShortcut": {
                   "description": "The Aetheryte to teleport to (before moving)",
-                  "$ref": "#/$defs/Aetheryte"
+                  "$ref": "https://git.carvel.li/liza/Questionable/raw/branch/master/Questionable.Model/common-schema.json#/$defs/Aetheryte"
                 },
                 "AethernetShortcut": {
                   "type": "array",
                   "minItems": 2,
                   "maxItems": 2,
                   "items": {
-                    "$ref": "#/$defs/AethernetShard"
+                    "$ref": "https://git.carvel.li/liza/Questionable/raw/branch/master/Questionable.Model/common-schema.json#/$defs/AethernetShard"
                   }
                 },
                 "ItemId": {
                           "type": "boolean"
                         },
                         "CompletionQuestVariablesFlags": {
-                          "$ref": "#/$defs/CompletionFlags"
+                          "$ref": "https://git.carvel.li/liza/Questionable/raw/branch/master/Questionable.Model/common-schema.json#/$defs/CompletionFlags"
                         },
                         "Flying": {
                           "type": "string",
                   "additionalProperties": false
                 },
                 "CompletionQuestVariablesFlags": {
-                  "$ref": "#/$defs/CompletionFlags"
+                  "$ref": "https://git.carvel.li/liza/Questionable/raw/branch/master/Questionable.Model/common-schema.json#/$defs/CompletionFlags"
                 },
                 "RequiredQuestVariables": {
                   "type": "array",
                   "then": {
                     "properties": {
                       "Aetheryte": {
-                        "$ref": "#/$defs/Aetheryte"
+                        "$ref": "https://git.carvel.li/liza/Questionable/raw/branch/master/Questionable.Model/common-schema.json#/$defs/Aetheryte"
                       },
                       "DataId": {
                         "type": "null"
                   "then": {
                     "properties": {
                       "AethernetShard": {
-                        "$ref": "#/$defs/AethernetShard"
+                        "$ref": "https://git.carvel.li/liza/Questionable/raw/branch/master/Questionable.Model/common-schema.json#/$defs/AethernetShard"
                       },
                       "DataId": {
                         "type": "null"
                               "type": "integer"
                             },
                             "CompletionQuestVariablesFlags": {
-                              "$ref": "#/$defs/CompletionFlags"
+                              "$ref": "https://git.carvel.li/liza/Questionable/raw/branch/master/Questionable.Model/common-schema.json#/$defs/CompletionFlags"
                             },
                             "IgnoreQuestMarker": {
                               "type": "boolean"
     "QuestSequence",
     "Author"
   ],
-  "additionalProperties": false,
-  "$defs": {
-    "Aetheryte": {
-      "type": "string",
-      "enum": [
-        "Gridania",
-        "Central Shroud - Bentbranch Meadows",
-        "East Shroud - Hawthorne Hut",
-        "South Shroud - Quarrymill",
-        "South Shroud - Camp Tranquil",
-        "North Shroud - Fallgourd Float",
-        "Ul'dah",
-        "Western Thanalan - Horizon",
-        "Central Thanalan - Black Brush Station",
-        "Eastern Thanalan - Camp Drybone",
-        "Southern Thanalan - Little Ala Mhigo",
-        "Southern Thanalan - Forgotten Springs",
-        "Northern Thanalan - Camp Bluefog",
-        "Northern Thanalan - Ceruleum Processing Plant",
-        "Limsa Lominsa",
-        "Middle La Noscea - Summerford Farms",
-        "Lower La Noscea - Moraby Drydocks",
-        "Eastern La Noscea - Costa Del Sol",
-        "Eastern La Noscea - Wineport",
-        "Western La Noscea - Swiftperch",
-        "Western La Noscea - Aleport",
-        "Upper La Noscea - Camp Bronze Lake",
-        "Outer La Noscea - Camp Overlook",
-        "Coerthas Central Highlands - Camp Dragonhead",
-        "Mor Dhona",
-        "Gold Saucer",
-        "Wolves' Den Pier",
-        "Ishgard",
-        "Idyllshire",
-        "Coerthas Western Highlands - Falcon's Nest",
-        "The Sea of Clouds - Camp Cloudtop",
-        "The Sea of Clouds - Ok' Zundu",
-        "Azys Lla - Helix",
-        "The Dravanian Forelands - Tailfeather",
-        "The Dravanian Forelands - Anyx Trine",
-        "The Churning Mists - Moghome",
-        "The Churning Mists - Zenith",
-        "Rhalgr's Reach",
-        "Fringes - Castrum Oriens",
-        "Fringes - Peering Stones",
-        "Peaks - Ala Gannha",
-        "Peaks - Ala Ghiri",
-        "Lochs - Porta Praetoria",
-        "Lochs - Ala Mhigan Quarter",
-        "Kugane",
-        "Ruby Sea - Tamamizu",
-        "Ruby Sea - Onokoro",
-        "Yanxia - Namai",
-        "Yanxia - House of the Fierce",
-        "Azim Steppe - Reunion",
-        "Azim Steppe - Dawn Throne",
-        "Azim Steppe - Dhoro Iloh",
-        "Doman Enclave",
-        "Crystarium",
-        "Eulmore",
-        "Lakeland - Fort Jobb",
-        "Lakeland - Ostall Imperative",
-        "Kholusia - Stilltide",
-        "Kholusia - Wright",
-        "Kholusia - Tomra",
-        "Amh Araeng - Mord Souq",
-        "Amh Araeng - Inn at Journey's Head",
-        "Amh Araeng - Twine",
-        "Rak'tika - Slitherbough",
-        "Rak'tika - Fanow",
-        "Il Mheg - Lydha Lran",
-        "Il Mheg - Pia Enni",
-        "Il Mheg - Wolekdorf",
-        "Tempest - Ondo Cups",
-        "Tempest - Macarenses Angle",
-        "Old Sharlayan",
-        "Radz-at-Han",
-        "Labyrinthos - Archeion",
-        "Labyrinthos - Sharlayan Hamlet",
-        "Labyrinthos - Aporia",
-        "Thavnair - Yedlihmad",
-        "Thavnair - Great Work",
-        "Thavnair - Palaka's Stand",
-        "Garlemald - Camp Broken Glass",
-        "Garlemald - Tertium",
-        "Mare Lamentorum - Sinus Lacrimarum",
-        "Mare Lamentorum - Bestways Burrow",
-        "Elpis - Anagnorisis",
-        "Elpis - Twelve Wonders",
-        "Elpis - Poieten Oikos",
-        "Ultima Thule - Reah Tahra",
-        "Ultima Thule - Abode of the Ea",
-        "Ultima Thule - Base Omicron",
-        "Tuliyollal",
-        "Solution Nine",
-        "Urqopacha - Wachunpelo",
-        "Urqopacha - Worlar's Echo",
-        "Kozama'uka - Ok'hanu",
-        "Kozama'uka - Many Fires",
-        "Kozama'uka - Earthenshire",
-        "Yak T'el - Iq Br'aax",
-        "Yak T'el - Mamook",
-        "Shaaloani - Hhusatahwi",
-        "Shaaloani - Sheshenewezi Springs",
-        "Shaaloani - Mehwahhetsoan",
-        "Heritage Found - Yyasulani Station",
-        "Heritage Found - The Outskirts",
-        "Heritage Found - Electrope Strike",
-        "Living Memory - Leynode Mnemo",
-        "Living Memory - Leynode Pyro",
-        "Living Memory - Leynode Aero"
-      ]
-    },
-    "AethernetShard": {
-      "type": "string",
-      "enum": [
-        "[Gridania] Aetheryte Plaza",
-        "[Gridania] Archers' Guild",
-        "[Gridania] Leatherworkers' Guild & Shaded Bower",
-        "[Gridania] Lancers' Guild",
-        "[Gridania] Conjurers' Guild",
-        "[Gridania] Botanists' Guild",
-        "[Gridania] Mih Khetto's Amphitheatre",
-        "[Gridania] Blue Badger Gate (Central Shroud)",
-        "[Gridania] Yellow Serpent Gate (North Shroud)",
-        "[Gridania] White Wolf Gate (Central Shroud)",
-        "[Gridania] Airship Landing",
-        "[Ul'dah] Aetheryte Plaza",
-        "[Ul'dah] Adventurers' Guild",
-        "[Ul'dah] Thaumaturges' Guild",
-        "[Ul'dah] Gladiators' Guild",
-        "[Ul'dah] Miners' Guild",
-        "[Ul'dah] Weavers' Guild",
-        "[Ul'dah] Goldsmiths' Guild",
-        "[Ul'dah] Sapphire Avenue Exchange",
-        "[Ul'dah] Alchemists' Guild",
-        "[Ul'dah] Gate of the Sultana (Western Thanalan)",
-        "[Ul'dah] Gate of Nald (Central Thanalan)",
-        "[Ul'dah] Gate of Thal (Central Thanalan)",
-        "[Ul'dah] The Chamber of Rule",
-        "[Ul'dah] Airship Landing",
-        "[Limsa Lominsa] Aetheryte Plaza",
-        "[Limsa Lominsa] Arcanists' Guild",
-        "[Limsa Lominsa] Fishermens' Guild",
-        "[Limsa Lominsa] Hawkers' Alley",
-        "[Limsa Lominsa] The Aftcastle",
-        "[Limsa Lominsa] Culinarians' Guild",
-        "[Limsa Lominsa] Marauders' Guild",
-        "[Limsa Lominsa] Zephyr Gate (Middle La Noscea)",
-        "[Limsa Lominsa] Tempest Gate (Lower La Noscea)",
-        "[Limsa Lominsa] Airship Landing",
-        "[Ishgard] Aetheryte Plaza",
-        "[Ishgard] The Forgotten Knight",
-        "[Ishgard] Skysteel Manufactory",
-        "[Ishgard] The Brume",
-        "[Ishgard] Athenaeum Astrologicum",
-        "[Ishgard] The Jeweled Crozier",
-        "[Ishgard] Saint Reymanaud's Cathedral",
-        "[Ishgard] The Tribunal",
-        "[Ishgard] The Last Vigil",
-        "[Ishgard] The Gates of Judgement (Coerthas Central Highlands)",
-        "[Idyllshire] Aetheryte Plaza",
-        "[Idyllshire] West Idyllshire",
-        "[Idyllshire] Prologue Gate (Western Hinterlands)",
-        "[Idyllshire] Epilogue Gate (Eastern Hinterlands)",
-        "[Rhalgr's Reach] Aetheryte Plaza",
-        "[Rhalgr's Reach] Western Rhalgr's Reach",
-        "[Rhalgr's Reach] Northeastern Rhalgr's Reach",
-        "[Rhalgr's Reach] Fringes Gate",
-        "[Rhalgr's Reach] Peaks Gate",
-        "[Kugane] Aetheryte Plaza",
-        "[Kugane] Shiokaze Hostelry",
-        "[Kugane] Pier #1",
-        "[Kugane] Thavnairian Consulate",
-        "[Kugane] Kogane Dori Markets",
-        "[Kugane] Bokairo Inn",
-        "[Kugane] The Ruby Bazaar",
-        "[Kugane] Sekiseigumi Barracks",
-        "[Kugane] Rakuza District",
-        "[Kugane] The Ruby Price",
-        "[Kugane] Airship Landing",
-        "[Crystarium] Aetheryte Plaza",
-        "[Crystarium] Musica Universalis Markets",
-        "[Crystarium] Temenos Rookery",
-        "[Crystarium] The Dossal Gate",
-        "[Crystarium] The Pendants",
-        "[Crystarium] The Amaro Launch",
-        "[Crystarium] The Crystalline Mean",
-        "[Crystarium] The Cabinet of Curiosity",
-        "[Crystarium] Tessellation (Lakeland)",
-        "[Eulmore] Aetheryte Plaza",
-        "[Eulmore] Southeast Derelicts",
-        "[Eulmore] Nightsoil Pots",
-        "[Eulmore] The Glory Gate",
-        "[Eulmore] The Mainstay",
-        "[Eulmore] The Path to Glory (Kholusia)",
-        "[Old Sharlayan] Aetheryte Plaza",
-        "[Old Sharlayan] The Studium",
-        "[Old Sharlayan] The Baldesion Annex",
-        "[Old Sharlayan] The Rostra",
-        "[Old Sharlayan] The Leveilleur Estate",
-        "[Old Sharlayan] Journey's End",
-        "[Old Sharlayan] Scholar's Harbor",
-        "[Old Sharlayan] The Hall of Artifice (Labyrinthos)",
-        "[Radz-at-Han] Aetheryte Plaza",
-        "[Radz-at-Han] Meghaduta",
-        "[Radz-at-Han] Ruveydah Fibers",
-        "[Radz-at-Han] Airship Landing",
-        "[Radz-at-Han] Alzadaal's Peace",
-        "[Radz-at-Han] Hall of the Radiant Host",
-        "[Radz-at-Han] Mehryde's Meyhane",
-        "[Radz-at-Han] Kama",
-        "[Radz-at-Han] The High Crucible of Al-Kimiya",
-        "[Radz-at-Han] The Gate of First Sight (Thavnair)",
-        "[Tuliyollal] Aetheryte Plaza",
-        "[Tuliyollal] Dirigible Landing",
-        "[Tuliyollal] The Resplendent Quarter",
-        "[Tuliyollal] The For'ard Cabins",
-        "[Tuliyollal] Bayside Bevy Marketplace",
-        "[Tuliyollal] Vollok Shoonsa",
-        "[Tuliyollal] Wachumeqimeqi",
-        "[Tuliyollal] Brightploom Post",
-        "[Tuliyollal] Arch of the Dawn (Urqopacha)",
-        "[Tuliyollal] Arch of the Dawn (Kozama'uka)",
-        "[Tuliyollal] Ihuykatumu (Kozama'uka)",
-        "[Tuliyollal] Dirigible Landing (Yak T'el)",
-        "[Tuliyollal] Xak Tural Skygate (Shaaloani)",
-        "[Solution Nine] Aetheryte Plaza",
-        "[Solution Nine] Information Center",
-        "[Solution Nine] True Vue",
-        "[Solution Nine] Neon Stein",
-        "[Solution Nine] The Arcadion",
-        "[Solution Nine] Resolution",
-        "[Solution Nine] Nexus Arcade",
-        "[Solution Nine] Residential Sector",
-        "[Solution Nine] Scanning Port Nine (Heritage Found)"
-      ]
-    },
-    "CompletionFlags": {
-      "type": "array",
-      "description": "Quest Variables that dictate whether or not this step is skipped: null is don't check, positive values need to be set, negative values need to be unset",
-      "items": {
-        "oneOf": [
-          {
-            "type": "object",
-            "properties": {
-              "High": {
-                "type": [
-                  "number",
-                  "null"
-                ],
-                "minimum": 0,
-                "maximum": 15
-              },
-              "Low": {
-                "type": [
-                  "number",
-                  "null"
-                ],
-                "minimum": 0,
-                "maximum": 15
-              },
-              "Negative": {
-                "type": "boolean"
-              },
-              "Mode": {
-                "type": "string",
-                "enum": [
-                  "Bitwise",
-                  "Exact"
-                ]
-              }
-            }
-          },
-          {
-            "type": "number",
-            "enum": [
-              1,
-              2,
-              4,
-              8,
-              16,
-              32,
-              64,
-              128
-            ]
-          },
-          {
-            "type": "null"
-          }
-        ]
-      },
-      "minItems": 6,
-      "maxItems": 6
-    }
-  }
+  "additionalProperties": false
 }
diff --git a/Questionable.Model/AssemblyModelLoader.cs b/Questionable.Model/AssemblyModelLoader.cs
new file mode 100644 (file)
index 0000000..d167cb2
--- /dev/null
@@ -0,0 +1,9 @@
+using System.IO;
+
+namespace Questionable.Model;
+
+public static class AssemblyModelLoader
+{
+    public static Stream CommonSchema =>
+        typeof(AssemblyModelLoader).Assembly.GetManifestResourceStream("Questionable.Model.CommonSchema")!;
+}
diff --git a/Questionable.Model/Common/Converter/AetheryteConverter.cs b/Questionable.Model/Common/Converter/AetheryteConverter.cs
new file mode 100644 (file)
index 0000000..3887305
--- /dev/null
@@ -0,0 +1,127 @@
+using System.Collections.Generic;
+using Questionable.Model.Common.Converter;
+
+namespace Questionable.Model.Common.Converter;
+
+public sealed class AetheryteConverter() : EnumConverter<EAetheryteLocation>(Values)
+{
+    private static readonly Dictionary<EAetheryteLocation, string> Values = new()
+    {
+        { EAetheryteLocation.Gridania, "Gridania" },
+        { EAetheryteLocation.CentralShroudBentbranchMeadows, "Central Shroud - Bentbranch Meadows" },
+        { EAetheryteLocation.EastShroudHawthorneHut, "East Shroud - Hawthorne Hut" },
+        { EAetheryteLocation.SouthShroudQuarrymill, "South Shroud - Quarrymill" },
+        { EAetheryteLocation.SouthShroudCampTranquil, "South Shroud - Camp Tranquil" },
+        { EAetheryteLocation.NorthShroudFallgourdFloat, "North Shroud - Fallgourd Float" },
+
+        { EAetheryteLocation.Uldah, "Ul'dah" },
+        { EAetheryteLocation.WesternThanalanHorizon, "Western Thanalan - Horizon" },
+        { EAetheryteLocation.CentralThanalanBlackBrushStation, "Central Thanalan - Black Brush Station" },
+        { EAetheryteLocation.EasternThanalanCampDrybone, "Eastern Thanalan - Camp Drybone" },
+        { EAetheryteLocation.SouthernThanalanLittleAlaMhigo, "Southern Thanalan - Little Ala Mhigo" },
+        { EAetheryteLocation.SouthernThanalanForgottenSprings, "Southern Thanalan - Forgotten Springs" },
+        { EAetheryteLocation.NorthernThanalanCampBluefog, "Northern Thanalan - Camp Bluefog" },
+        { EAetheryteLocation.NorthernThanalanCeruleumProcessingPlant, "Northern Thanalan - Ceruleum Processing Plant" },
+
+        { EAetheryteLocation.Limsa, "Limsa Lominsa" },
+        { EAetheryteLocation.MiddleLaNosceaSummerfordFarms, "Middle La Noscea - Summerford Farms" },
+        { EAetheryteLocation.LowerLaNosceaMorabyDrydocks, "Lower La Noscea - Moraby Drydocks" },
+        { EAetheryteLocation.EasternLaNosceaCostaDelSol, "Eastern La Noscea - Costa Del Sol" },
+        { EAetheryteLocation.EasternLaNosceaWineport, "Eastern La Noscea - Wineport" },
+        { EAetheryteLocation.WesternLaNosceaSwiftperch, "Western La Noscea - Swiftperch" },
+        { EAetheryteLocation.WesternLaNosceaAleport, "Western La Noscea - Aleport" },
+        { EAetheryteLocation.UpperLaNosceaCampBronzeLake, "Upper La Noscea - Camp Bronze Lake" },
+        { EAetheryteLocation.OuterLaNosceaCampOverlook, "Outer La Noscea - Camp Overlook" },
+
+        { EAetheryteLocation.CoerthasCentralHighlandsCampDragonhead, "Coerthas Central Highlands - Camp Dragonhead" },
+        { EAetheryteLocation.MorDhona, "Mor Dhona" },
+        { EAetheryteLocation.GoldSaucer, "Gold Saucer" },
+        { EAetheryteLocation.WolvesDenPier, "Wolves' Den Pier" },
+
+        { EAetheryteLocation.Ishgard, "Ishgard" },
+        { EAetheryteLocation.Idyllshire, "Idyllshire" },
+        { EAetheryteLocation.CoerthasWesternHighlandsFalconsNest, "Coerthas Western Highlands - Falcon's Nest" },
+        { EAetheryteLocation.SeaOfCloudsCampCloudtop, "The Sea of Clouds - Camp Cloudtop" },
+        { EAetheryteLocation.SeaOfCloudsOkZundu, "The Sea of Clouds - Ok' Zundu" },
+        { EAetheryteLocation.AzysLlaHelix, "Azys Lla - Helix" },
+        { EAetheryteLocation.DravanianForelandsTailfeather, "The Dravanian Forelands - Tailfeather" },
+        { EAetheryteLocation.DravanianForelandsAnyxTrine, "The Dravanian Forelands - Anyx Trine" },
+        { EAetheryteLocation.ChurningMistsMoghome, "The Churning Mists - Moghome" },
+        { EAetheryteLocation.ChurningMistsZenith, "The Churning Mists - Zenith" },
+
+        { EAetheryteLocation.RhalgrsReach, "Rhalgr's Reach" },
+        { EAetheryteLocation.FringesCastrumOriens, "Fringes - Castrum Oriens" },
+        { EAetheryteLocation.FringesPeeringStones, "Fringes - Peering Stones" },
+        { EAetheryteLocation.PeaksAlaGannha, "Peaks - Ala Gannha" },
+        { EAetheryteLocation.PeaksAlaGhiri, "Peaks - Ala Ghiri" },
+        { EAetheryteLocation.LochsPortaPraetoria, "Lochs - Porta Praetoria" },
+        { EAetheryteLocation.LochsAlaMhiganQuarter, "Lochs - Ala Mhigan Quarter" },
+        { EAetheryteLocation.Kugane, "Kugane" },
+        { EAetheryteLocation.RubySeaTamamizu, "Ruby Sea - Tamamizu" },
+        { EAetheryteLocation.RubySeaOnokoro, "Ruby Sea - Onokoro" },
+        { EAetheryteLocation.YanxiaNamai, "Yanxia - Namai" },
+        { EAetheryteLocation.YanxiaHouseOfTheFierce, "Yanxia - House of the Fierce" },
+        { EAetheryteLocation.AzimSteppeReunion, "Azim Steppe - Reunion" },
+        { EAetheryteLocation.AzimSteppeDawnThrone, "Azim Steppe - Dawn Throne" },
+        { EAetheryteLocation.AzimSteppeDhoroIloh, "Azim Steppe - Dhoro Iloh" },
+        { EAetheryteLocation.DomanEnclave, "Doman Enclave" },
+
+        { EAetheryteLocation.Crystarium, "Crystarium" },
+        { EAetheryteLocation.Eulmore, "Eulmore" },
+        { EAetheryteLocation.LakelandFortJobb, "Lakeland - Fort Jobb" },
+        { EAetheryteLocation.LakelandOstallImperative, "Lakeland - Ostall Imperative" },
+        { EAetheryteLocation.KholusiaStilltide, "Kholusia - Stilltide" },
+        { EAetheryteLocation.KholusiaWright, "Kholusia - Wright" },
+        { EAetheryteLocation.KholusiaTomra, "Kholusia - Tomra" },
+        { EAetheryteLocation.AmhAraengMordSouq, "Amh Araeng - Mord Souq" },
+        { EAetheryteLocation.AmhAraengInnAtJourneysHead, "Amh Araeng - Inn at Journey's Head" },
+        { EAetheryteLocation.AmhAraengTwine, "Amh Araeng - Twine" },
+        { EAetheryteLocation.RaktikaSlitherbough, "Rak'tika - Slitherbough" },
+        { EAetheryteLocation.RaktikaFanow, "Rak'tika - Fanow" },
+        { EAetheryteLocation.IlMhegLydhaLran, "Il Mheg - Lydha Lran" },
+        { EAetheryteLocation.IlMhegPiaEnni, "Il Mheg - Pia Enni" },
+        { EAetheryteLocation.IlMhegWolekdorf, "Il Mheg - Wolekdorf" },
+        { EAetheryteLocation.TempestOndoCups, "Tempest - Ondo Cups" },
+        { EAetheryteLocation.TempestMacarensesAngle, "Tempest - Macarenses Angle" },
+
+        { EAetheryteLocation.OldSharlayan, "Old Sharlayan" },
+        { EAetheryteLocation.RadzAtHan, "Radz-at-Han" },
+        { EAetheryteLocation.LabyrinthosArcheion, "Labyrinthos - Archeion" },
+        { EAetheryteLocation.LabyrinthosSharlayanHamlet, "Labyrinthos - Sharlayan Hamlet" },
+        { EAetheryteLocation.LabyrinthosAporia, "Labyrinthos - Aporia" },
+        { EAetheryteLocation.ThavnairYedlihmad, "Thavnair - Yedlihmad" },
+        { EAetheryteLocation.ThavnairGreatWork, "Thavnair - Great Work" },
+        { EAetheryteLocation.ThavnairPalakasStand, "Thavnair - Palaka's Stand" },
+        { EAetheryteLocation.GarlemaldCampBrokenGlass, "Garlemald - Camp Broken Glass" },
+        { EAetheryteLocation.GarlemaldTertium, "Garlemald - Tertium" },
+        { EAetheryteLocation.MareLamentorumSinusLacrimarum, "Mare Lamentorum - Sinus Lacrimarum" },
+        { EAetheryteLocation.MareLamentorumBestwaysBurrow, "Mare Lamentorum - Bestways Burrow" },
+        { EAetheryteLocation.ElpisAnagnorisis, "Elpis - Anagnorisis" },
+        { EAetheryteLocation.ElpisTwelveWonders, "Elpis - Twelve Wonders" },
+        { EAetheryteLocation.ElpisPoietenOikos, "Elpis - Poieten Oikos" },
+        { EAetheryteLocation.UltimaThuleReahTahra, "Ultima Thule - Reah Tahra" },
+        { EAetheryteLocation.UltimaThuleAbodeOfTheEa, "Ultima Thule - Abode of the Ea" },
+        { EAetheryteLocation.UltimaThuleBaseOmicron, "Ultima Thule - Base Omicron" },
+
+        { EAetheryteLocation.Tuliyollal, "Tuliyollal" },
+        { EAetheryteLocation.SolutionNine, "Solution Nine" },
+        { EAetheryteLocation.UrqopachaWachunpelo, "Urqopacha - Wachunpelo" },
+        { EAetheryteLocation.UrqopachaWorlarsEcho, "Urqopacha - Worlar's Echo" },
+        { EAetheryteLocation.KozamaukaOkHanu, "Kozama'uka - Ok'hanu" },
+        { EAetheryteLocation.KozamaukaManyFires, "Kozama'uka - Many Fires" },
+        { EAetheryteLocation.KozamaukaEarthenshire, "Kozama'uka - Earthenshire" },
+        { EAetheryteLocation.YakTelIqBraax, "Yak T'el - Iq Br'aax" },
+        { EAetheryteLocation.YakTelMamook, "Yak T'el - Mamook" },
+        { EAetheryteLocation.ShaaloaniHhusatahwi, "Shaaloani - Hhusatahwi" },
+        { EAetheryteLocation.ShaaloaniShesheneweziSprings, "Shaaloani - Sheshenewezi Springs" },
+        { EAetheryteLocation.ShaaloaniMehwahhetsoan, "Shaaloani - Mehwahhetsoan" },
+        { EAetheryteLocation.HeritageFoundYyasulaniStation, "Heritage Found - Yyasulani Station" },
+        { EAetheryteLocation.HeritageFoundTheOutskirts, "Heritage Found - The Outskirts" },
+        { EAetheryteLocation.HeritageFoundElectropeStrike, "Heritage Found - Electrope Strike" },
+        { EAetheryteLocation.LivingMemoryLeynodeMnemo, "Living Memory - Leynode Mnemo" },
+        { EAetheryteLocation.LivingMemoryLeynodePyro, "Living Memory - Leynode Pyro" },
+        { EAetheryteLocation.LivingMemoryLeynodeAero, "Living Memory - Leynode Aero" },
+    };
+
+    public static bool IsLargeAetheryte(EAetheryteLocation aetheryte) => Values.ContainsKey(aetheryte);
+}
diff --git a/Questionable.Model/Common/Converter/EnumConverter.cs b/Questionable.Model/Common/Converter/EnumConverter.cs
new file mode 100644 (file)
index 0000000..3708e28
--- /dev/null
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Questionable.Model.Common.Converter;
+
+public abstract class EnumConverter<T> : JsonConverter<T>
+    where T : Enum
+{
+    private readonly ReadOnlyDictionary<T, string> _enumToString;
+    private readonly ReadOnlyDictionary<string, T> _stringToEnum;
+
+    protected EnumConverter(IReadOnlyDictionary<T, string> values)
+    {
+        _enumToString = values is IDictionary<T, string> dict
+            ? new ReadOnlyDictionary<T, string>(dict)
+            : new ReadOnlyDictionary<T, string>(values.ToDictionary(x => x.Key, x => x.Value));
+        _stringToEnum = new ReadOnlyDictionary<string, T>(_enumToString.ToDictionary(x => x.Value, x => x.Key));
+    }
+
+    public override T? Read(ref Utf8JsonReader reader, Type typeToConvert,
+        JsonSerializerOptions options)
+    {
+        if (reader.TokenType != JsonTokenType.String)
+            throw new JsonException();
+
+        string? str = reader.GetString();
+        if (str == null)
+            throw new JsonException();
+
+        return _stringToEnum.TryGetValue(str, out T? value) ? value : throw new JsonException();
+    }
+
+    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
+    {
+        writer.WriteStringValue(_enumToString[value]);
+    }
+}
diff --git a/Questionable.Model/Common/Converter/StringListOrValueConverter.cs b/Questionable.Model/Common/Converter/StringListOrValueConverter.cs
new file mode 100644 (file)
index 0000000..ebdec1c
--- /dev/null
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Questionable.Model.Common.Converter;
+
+public sealed class StringListOrValueConverter : JsonConverter<List<string>>
+{
+    public override List<string> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+    {
+        if (reader.TokenType == JsonTokenType.String)
+            return [reader.GetString()!];
+
+        if (reader.TokenType != JsonTokenType.StartArray)
+            throw new JsonException();
+        reader.Read();
+
+        List<string> value = [];
+        while (reader.TokenType != JsonTokenType.EndArray)
+        {
+            value.Add(reader.GetString()!);
+            reader.Read();
+        }
+
+        return value;
+    }
+
+    public override void Write(Utf8JsonWriter writer, List<string>? value, JsonSerializerOptions options)
+    {
+        if (value == null)
+            writer.WriteNullValue();
+        else if (value.Count == 1)
+            writer.WriteStringValue(value[0]);
+        else
+        {
+            writer.WriteStartArray();
+            foreach (var v in value)
+                writer.WriteStringValue(v);
+            writer.WriteEndArray();
+        }
+    }
+}
diff --git a/Questionable.Model/Common/Converter/VectorConverter.cs b/Questionable.Model/Common/Converter/VectorConverter.cs
new file mode 100644 (file)
index 0000000..7b4833e
--- /dev/null
@@ -0,0 +1,64 @@
+using System;
+using System.Numerics;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Questionable.Model.Common.Converter;
+
+public sealed class VectorConverter : JsonConverter<Vector3>
+{
+    public override Vector3 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+    {
+        if (reader.TokenType != JsonTokenType.StartObject)
+            throw new JsonException();
+
+        Vector3 vec = new Vector3();
+        while (reader.Read())
+        {
+            switch (reader.TokenType)
+            {
+                case JsonTokenType.PropertyName:
+                    string? propertyName = reader.GetString();
+                    if (propertyName == null || !reader.Read())
+                        throw new JsonException();
+
+                    switch (propertyName)
+                    {
+                        case nameof(Vector3.X):
+                            vec.X = reader.GetSingle();
+                            break;
+
+                        case nameof(Vector3.Y):
+                            vec.Y = reader.GetSingle();
+                            break;
+
+                        case nameof(Vector3.Z):
+                            vec.Z = reader.GetSingle();
+                            break;
+
+                        default:
+                            throw new JsonException();
+                    }
+
+                    break;
+
+                case JsonTokenType.EndObject:
+                    return vec;
+
+                default:
+                    throw new JsonException();
+            }
+        }
+
+        throw new JsonException();
+    }
+
+    public override void Write(Utf8JsonWriter writer, Vector3 value, JsonSerializerOptions options)
+    {
+        writer.WriteStartObject();
+        writer.WriteNumber(nameof(Vector3.X), value.X);
+        writer.WriteNumber(nameof(Vector3.Y), value.X);
+        writer.WriteNumber(nameof(Vector3.Z), value.X);
+        writer.WriteEndObject();
+    }
+}
diff --git a/Questionable.Model/Common/EAetheryteLocation.cs b/Questionable.Model/Common/EAetheryteLocation.cs
new file mode 100644 (file)
index 0000000..a00f7b5
--- /dev/null
@@ -0,0 +1,252 @@
+using System.Text.Json.Serialization;
+using Questionable.Model.Common.Converter;
+using Questionable.Model.Questing.Converter;
+
+namespace Questionable.Model.Common;
+
+[JsonConverter(typeof(AetheryteConverter))]
+public enum EAetheryteLocation
+{
+    None = 0,
+
+    Gridania = 2,
+    GridaniaArcher = 25,
+    GridaniaLeatherworker = 26,
+    GridaniaLancer = 27,
+    GridaniaConjurer = 28,
+    GridaniaBotanist = 29,
+    GridaniaAmphitheatre = 30,
+    GridaniaBlueBadgerGate = 31,
+    GridaniaYellowSerpentGate = 32,
+    GridaniaWhiteWolfGate = 54,
+    GridaniaAirship = 94,
+
+    CentralShroudBentbranchMeadows = 3,
+    EastShroudHawthorneHut = 4,
+    SouthShroudQuarrymill = 5,
+    SouthShroudCampTranquil = 6,
+    NorthShroudFallgourdFloat = 7,
+
+    Uldah = 9,
+    UldahAdventurers = 33,
+    UldahThaumaturge = 34,
+    UldahGladiator = 35,
+    UldahMiner = 36,
+    UldahAlchemist = 37,
+    UldahWeaver = 47,
+    UldahGoldsmith = 50,
+    UldahChamberOfRule = 51,
+    UldahAirship = 95,
+    UldahGateOfTheSultana = 38,
+    UldahGateOfNald = 39,
+    UldahGateOfThal = 40,
+    UldahSapphireAvenue = 125,
+
+    WesternThanalanHorizon = 17,
+    EasternThanalanCampDrybone = 18,
+    SouthernThanalanLittleAlaMhigo = 19,
+    SouthernThanalanForgottenSprings = 20,
+    NorthernThanalanCampBluefog = 21,
+    NorthernThanalanCeruleumProcessingPlant = 22,
+    CentralThanalanBlackBrushStation = 53,
+
+    Limsa = 8,
+    LimsaAftcastle = 41,
+    LimsaCulinarian = 42,
+    LimsaArcanist = 43,
+    LimsaFisher = 44,
+    LimsaMarauder = 48,
+    LimsaHawkersAlley = 49,
+    LimsaZephyrGate = 45,
+    LimsaTempestGate = 46,
+    LimsaAirship = 93,
+
+    LowerLaNosceaMorabyDrydocks = 10,
+    EasternLaNosceaCostaDelSol = 11,
+    EasternLaNosceaWineport = 12,
+    WesternLaNosceaSwiftperch = 13,
+    WesternLaNosceaAleport = 14,
+    UpperLaNosceaCampBronzeLake = 15,
+    OuterLaNosceaCampOverlook = 16,
+    MiddleLaNosceaSummerfordFarms = 52,
+
+    CoerthasCentralHighlandsCampDragonhead = 23,
+    MorDhona = 24,
+    GoldSaucer = 62,
+    WolvesDenPier = 55,
+
+    Ishgard = 70,
+    IshgardForgottenKnight = 80,
+    IshgardSkysteelManufactory = 81,
+    IshgardBrume = 82,
+    IshgardAthenaeumAstrologicum = 83,
+    IshgardJeweledCrozier = 84,
+    IshgardSaintReymanaudsCathedral = 85,
+    IshgardTribunal = 86,
+    IshgardLastVigil = 87,
+    IshgardGatesOfJudgement = 88,
+
+    Idyllshire = 75,
+    IdyllshireWest = 90,
+    IdyllshirePrologueGate = 91,
+    IdyllshireEpilogueGate = 92,
+
+    CoerthasWesternHighlandsFalconsNest = 71,
+    SeaOfCloudsCampCloudtop = 72,
+    SeaOfCloudsOkZundu = 73,
+    AzysLlaHelix = 74,
+    DravanianForelandsTailfeather = 76,
+    DravanianForelandsAnyxTrine = 77,
+    ChurningMistsMoghome = 78,
+    ChurningMistsZenith = 79,
+
+    RhalgrsReach = 104,
+    RhalgrsReachWest = 121,
+    RhalgrsReachNorthEast = 122,
+    RhalgrsReachFringesGate = 123,
+    RhalgrsReachPeaksGate = 124,
+
+    Kugane = 111,
+    KuganeShiokazeHostelry = 112,
+    KuganePier1 = 113,
+    KuganeThavnairianConsulate = 114,
+    KuganeMarkets = 115,
+    KuganeBokairoInn = 116,
+    KuganeRubyBazaar = 117,
+    KuganeSekiseigumiBarracks = 118,
+    KuganeRakuzaDistrict = 119,
+    KuganeRubyPrice = 120,
+    KuganeAirship = 126,
+
+    FringesCastrumOriens = 98,
+    FringesPeeringStones = 99,
+    PeaksAlaGannha = 100,
+    PeaksAlaGhiri = 101,
+    LochsPortaPraetoria = 102,
+    LochsAlaMhiganQuarter = 103,
+    RubySeaTamamizu = 105,
+    RubySeaOnokoro = 106,
+    YanxiaNamai = 107,
+    YanxiaHouseOfTheFierce = 108,
+    AzimSteppeReunion = 109,
+    AzimSteppeDawnThrone = 110,
+    AzimSteppeDhoroIloh = 128,
+
+    DomanEnclave = 127,
+    DomanEnclaveNorthern = 129,
+    DomanEnclaveSouthern = 130,
+    DomanEnclaveOneRiver = 131,
+    DomanEnclaveDocks = 162,
+
+    Crystarium = 133,
+    CrystariumMarkets = 149,
+    CrystariumTemenosRookery = 150,
+    CrystariumDossalGate = 151,
+    CrystariumPendants = 152,
+    CrystariumAmaroLaunch = 153,
+    CrystariumCrystallineMean = 154,
+    CrystariumCabinetOfCuriosity = 155,
+    CrystariumTessellation = 156,
+
+    Eulmore = 134,
+    EulmoreMainstay = 157,
+    EulmoreNightsoilPots = 158,
+    EulmoreGloryGate = 159,
+    EulmoreSoutheastDerelict = 135,
+    EulmorePathToGlory = 160,
+
+    LakelandFortJobb = 132,
+    LakelandOstallImperative = 136,
+    KholusiaStilltide = 137,
+    KholusiaWright = 138,
+    KholusiaTomra = 139,
+    AmhAraengMordSouq = 140,
+    AmhAraengInnAtJourneysHead = 161,
+    AmhAraengTwine = 141,
+    RaktikaSlitherbough = 142,
+    RaktikaFanow = 143,
+    IlMhegLydhaLran = 144,
+    IlMhegPiaEnni = 145,
+    IlMhegWolekdorf = 146,
+    TempestOndoCups = 147,
+    TempestMacarensesAngle = 148,
+
+    OldSharlayan = 182,
+    OldSharlayanStudium = 184,
+    OldSharlayanBaldesionAnnex = 185,
+    OldSharlayanRostra = 186,
+    OldSharlayanLeveilleurEstate = 187,
+    OldSharlayanJourneysEnd = 188,
+    OldSharlayanScholarsHarbor = 189,
+    OldSharlayanHallOfArtifice = 190,
+
+    RadzAtHan = 183,
+    RadzAtHanMeghaduta = 191,
+    RadzAtHanRuveydahFibers = 192,
+    RadzAtHanAirship = 193,
+    RadzAtHanAlzadaalsPeace = 194,
+    RadzAtHanHallOfTheRadiantHost = 195,
+    RadzAtHanMehrydesMeyhane = 196,
+    RadzAtHanKama = 198,
+    RadzAtHanHighCrucible = 199,
+    RadzAtHanGateOfFirstSight = 197,
+
+    LabyrinthosArcheion = 166,
+    LabyrinthosSharlayanHamlet = 167,
+    LabyrinthosAporia = 168,
+    ThavnairYedlihmad = 169,
+    ThavnairGreatWork = 170,
+    ThavnairPalakasStand = 171,
+    GarlemaldCampBrokenGlass = 172,
+    GarlemaldTertium = 173,
+    MareLamentorumSinusLacrimarum = 174,
+    MareLamentorumBestwaysBurrow = 175,
+    ElpisAnagnorisis = 176,
+    ElpisTwelveWonders = 177,
+    ElpisPoietenOikos = 178,
+    UltimaThuleReahTahra = 179,
+    UltimaThuleAbodeOfTheEa = 180,
+    UltimaThuleBaseOmicron = 181,
+
+    Tuliyollal = 216,
+
+    TuliyollalDirigibleLanding = 218,
+    TuliyollalTheResplendentQuarter = 219,
+    TuliyollalTheForardCabins = 220,
+    TuliyollalBaysideBevyMarketplace = 221,
+    TuliyollalVollokShoonsa = 222,
+    TuliyollalWachumeqimeqi = 223,
+    TuliyollalBrightploomPost = 224,
+    TuliyollalArchOfTheDawnUrqopacha = 225,
+    TuliyollalArchOfTheDawnKozamauka = 226,
+    TuliyollalIhuykatumu = 227,
+    TuliyollalDirigibleLandingYakTel = 228,
+    TuliyollalXakTuralSkygate = 229,
+
+    SolutionNine = 217,
+    SolutionNineInformationCenter = 230,
+    SolutionNineTrueVue = 231,
+    SolutionNineNeonStein = 232,
+    SolutionNineTheArcadion = 233,
+    SolutionNineResolution = 234,
+    SolutionNineNexusArcade = 235,
+    SolutionNineResidentialSector = 236,
+    SolutionNineScanningPortNine = 237,
+
+    UrqopachaWachunpelo = 200,
+    UrqopachaWorlarsEcho = 201,
+    KozamaukaOkHanu = 202,
+    KozamaukaManyFires = 203,
+    KozamaukaEarthenshire = 204,
+    YakTelIqBraax = 205,
+    YakTelMamook = 206,
+    ShaaloaniHhusatahwi = 207,
+    ShaaloaniShesheneweziSprings = 208,
+    ShaaloaniMehwahhetsoan = 209,
+    HeritageFoundYyasulaniStation = 210,
+    HeritageFoundTheOutskirts = 211,
+    HeritageFoundElectropeStrike = 212,
+    LivingMemoryLeynodeMnemo = 213,
+    LivingMemoryLeynodePyro = 214,
+    LivingMemoryLeynodeAero = 215,
+}
diff --git a/Questionable.Model/Gathering/GatheringNodeLocation.cs b/Questionable.Model/Gathering/GatheringNodeLocation.cs
new file mode 100644 (file)
index 0000000..49a5e64
--- /dev/null
@@ -0,0 +1,13 @@
+using System.Numerics;
+
+namespace Questionable.Model.Gathering;
+
+public sealed class GatheringNodeLocation
+{
+    public uint DataId { get; set; }
+    public Vector3 Position { get; set; }
+    public float? MinimumAngle { get; set; }
+    public float? MaximumAngle { get; set; }
+    public float? MinimumDistance { get; set; } = 0.5f;
+    public float? MaximumDistance { get; set; } = 3f;
+}
diff --git a/Questionable.Model/Gathering/GatheringRoot.cs b/Questionable.Model/Gathering/GatheringRoot.cs
new file mode 100644 (file)
index 0000000..c572faa
--- /dev/null
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+using Questionable.Model.Common;
+using Questionable.Model.Common.Converter;
+
+namespace Questionable.Model.Gathering;
+
+public sealed class GatheringRoot
+{
+    [JsonConverter(typeof(StringListOrValueConverter))]
+    public List<string> Author { get; set; } = [];
+    public ushort TerritoryId { get; set; }
+
+    [JsonConverter(typeof(AetheryteConverter))]
+    public EAetheryteLocation? AetheryteShortcut { get; set; }
+
+    public List<GatheringNodeLocation> Nodes { get; set; } = [];
+}
diff --git a/Questionable.Model/Questing/AethernetShortcut.cs b/Questionable.Model/Questing/AethernetShortcut.cs
new file mode 100644 (file)
index 0000000..3d046c4
--- /dev/null
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+using Questionable.Model.Common;
+using Questionable.Model.Questing.Converter;
+
+namespace Questionable.Model.Questing;
+
+[JsonConverter(typeof(AethernetShortcutConverter))]
+public sealed class AethernetShortcut
+{
+    public EAetheryteLocation From { get; set; }
+    public EAetheryteLocation To { get; set; }
+}
diff --git a/Questionable.Model/Questing/ChatMessage.cs b/Questionable.Model/Questing/ChatMessage.cs
new file mode 100644 (file)
index 0000000..b027213
--- /dev/null
@@ -0,0 +1,7 @@
+namespace Questionable.Model.Questing;
+
+public sealed class ChatMessage
+{
+    public string? ExcelSheet { get; set; }
+    public string Key { get; set; } = null!;
+}
diff --git a/Questionable.Model/Questing/ComplexCombatData.cs b/Questionable.Model/Questing/ComplexCombatData.cs
new file mode 100644 (file)
index 0000000..5f85c24
--- /dev/null
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+
+namespace Questionable.Model.Questing;
+
+public sealed class ComplexCombatData
+{
+    public uint DataId { get; set; }
+
+    // TODO Use this
+    public uint? MinimumKillCount { get; set; }
+
+    /// <summary>
+    /// If a reward item has been set, this is (ping allowing) given to the player before the dead enemy despawns.
+    /// </summary>
+    public uint? RewardItemId { get; set; }
+    public int? RewardItemCount { get; set; }
+    public IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; set; } = new List<QuestWorkValue?>();
+    public bool IgnoreQuestMarker { get; set; }
+}
diff --git a/Questionable.Model/Questing/Converter/ActionConverter.cs b/Questionable.Model/Questing/Converter/ActionConverter.cs
new file mode 100644 (file)
index 0000000..7e62c01
--- /dev/null
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using Questionable.Model.Common.Converter;
+
+namespace Questionable.Model.Questing.Converter;
+
+public sealed class ActionConverter() : EnumConverter<EAction>(Values)
+{
+    private static readonly Dictionary<EAction, string> Values = new()
+    {
+        { EAction.Cure, "Cure" },
+        { EAction.Esuna, "Esuna" },
+        { EAction.Physick, "Physick" },
+        { EAction.Buffet, "Buffet" },
+        { EAction.Fumigate, "Fumigate" },
+        { EAction.SiphonSnout, "Siphon Snout" },
+        { EAction.RedGulal, "Red Gulal" },
+        { EAction.YellowGulal, "Yellow Gulal" },
+        { EAction.BlueGulal, "Blue Gulal" },
+    };
+}
diff --git a/Questionable.Model/Questing/Converter/AethernetShardConverter.cs b/Questionable.Model/Questing/Converter/AethernetShardConverter.cs
new file mode 100644 (file)
index 0000000..bd07bc3
--- /dev/null
@@ -0,0 +1,144 @@
+using System.Collections.Generic;
+using Questionable.Model.Common;
+using Questionable.Model.Common.Converter;
+
+namespace Questionable.Model.Questing.Converter;
+
+public sealed class AethernetShardConverter() : EnumConverter<EAetheryteLocation>(Values)
+{
+    public static readonly Dictionary<EAetheryteLocation, string> Values = new()
+    {
+        { EAetheryteLocation.Gridania, "[Gridania] Aetheryte Plaza" },
+        { EAetheryteLocation.GridaniaArcher, "[Gridania] Archers' Guild" },
+        { EAetheryteLocation.GridaniaLeatherworker, "[Gridania] Leatherworkers' Guild & Shaded Bower" },
+        { EAetheryteLocation.GridaniaLancer, "[Gridania] Lancers' Guild" },
+        { EAetheryteLocation.GridaniaConjurer, "[Gridania] Conjurers' Guild" },
+        { EAetheryteLocation.GridaniaBotanist, "[Gridania] Botanists' Guild" },
+        { EAetheryteLocation.GridaniaAmphitheatre, "[Gridania] Mih Khetto's Amphitheatre" },
+        { EAetheryteLocation.GridaniaBlueBadgerGate, "[Gridania] Blue Badger Gate (Central Shroud)" },
+        { EAetheryteLocation.GridaniaYellowSerpentGate, "[Gridania] Yellow Serpent Gate (North Shroud)" },
+        { EAetheryteLocation.GridaniaWhiteWolfGate, "[Gridania] White Wolf Gate (Central Shroud)" },
+        { EAetheryteLocation.GridaniaAirship, "[Gridania] Airship Landing" },
+
+        { EAetheryteLocation.Uldah, "[Ul'dah] Aetheryte Plaza" },
+        { EAetheryteLocation.UldahAdventurers, "[Ul'dah] Adventurers' Guild" },
+        { EAetheryteLocation.UldahThaumaturge, "[Ul'dah] Thaumaturges' Guild" },
+        { EAetheryteLocation.UldahGladiator, "[Ul'dah] Gladiators' Guild" },
+        { EAetheryteLocation.UldahMiner, "[Ul'dah] Miners' Guild" },
+        { EAetheryteLocation.UldahWeaver, "[Ul'dah] Weavers' Guild" },
+        { EAetheryteLocation.UldahGoldsmith, "[Ul'dah] Goldsmiths' Guild" },
+        { EAetheryteLocation.UldahSapphireAvenue, "[Ul'dah] Sapphire Avenue Exchange" },
+        { EAetheryteLocation.UldahAlchemist, "[Ul'dah] Alchemists' Guild" },
+        { EAetheryteLocation.UldahChamberOfRule, "[Ul'dah] The Chamber of Rule" },
+        { EAetheryteLocation.UldahGateOfTheSultana, "[Ul'dah] Gate of the Sultana (Western Thanalan)" },
+        { EAetheryteLocation.UldahGateOfNald, "[Ul'dah] Gate of Nald (Central Thanalan)" },
+        { EAetheryteLocation.UldahGateOfThal, "[Ul'dah] Gate of Thal (Central Thanalan)" },
+        { EAetheryteLocation.UldahAirship, "[Ul'dah] Airship Landing" },
+
+        { EAetheryteLocation.Limsa, "[Limsa Lominsa] Aetheryte Plaza" },
+        { EAetheryteLocation.LimsaArcanist, "[Limsa Lominsa] Arcanists' Guild" },
+        { EAetheryteLocation.LimsaFisher, "[Limsa Lominsa] Fishermens' Guild" },
+        { EAetheryteLocation.LimsaHawkersAlley, "[Limsa Lominsa] Hawkers' Alley" },
+        { EAetheryteLocation.LimsaAftcastle, "[Limsa Lominsa] The Aftcastle" },
+        { EAetheryteLocation.LimsaCulinarian, "[Limsa Lominsa] Culinarians' Guild" },
+        { EAetheryteLocation.LimsaMarauder, "[Limsa Lominsa] Marauders' Guild" },
+        { EAetheryteLocation.LimsaZephyrGate, "[Limsa Lominsa] Zephyr Gate (Middle La Noscea)" },
+        { EAetheryteLocation.LimsaTempestGate, "[Limsa Lominsa] Tempest Gate (Lower La Noscea)" },
+        { EAetheryteLocation.LimsaAirship, "[Limsa Lominsa] Airship Landing" },
+
+        { EAetheryteLocation.Ishgard, "[Ishgard] Aetheryte Plaza" },
+        { EAetheryteLocation.IshgardForgottenKnight, "[Ishgard] The Forgotten Knight" },
+        { EAetheryteLocation.IshgardSkysteelManufactory, "[Ishgard] Skysteel Manufactory" },
+        { EAetheryteLocation.IshgardBrume, "[Ishgard] The Brume" },
+        { EAetheryteLocation.IshgardAthenaeumAstrologicum, "[Ishgard] Athenaeum Astrologicum" },
+        { EAetheryteLocation.IshgardJeweledCrozier, "[Ishgard] The Jeweled Crozier" },
+        { EAetheryteLocation.IshgardSaintReymanaudsCathedral, "[Ishgard] Saint Reymanaud's Cathedral" },
+        { EAetheryteLocation.IshgardTribunal, "[Ishgard] The Tribunal" },
+        { EAetheryteLocation.IshgardLastVigil, "[Ishgard] The Last Vigil" },
+        { EAetheryteLocation.IshgardGatesOfJudgement, "[Ishgard] The Gates of Judgement (Coerthas Central Highlands)" },
+
+        { EAetheryteLocation.Idyllshire, "[Idyllshire] Aetheryte Plaza" },
+        { EAetheryteLocation.IdyllshireWest, "[Idyllshire] West Idyllshire" },
+        { EAetheryteLocation.IdyllshirePrologueGate, "[Idyllshire] Prologue Gate (Western Hinterlands)" },
+        { EAetheryteLocation.IdyllshireEpilogueGate, "[Idyllshire] Epilogue Gate (Eastern Hinterlands)" },
+
+        { EAetheryteLocation.RhalgrsReach, "[Rhalgr's Reach] Aetheryte Plaza" },
+        { EAetheryteLocation.RhalgrsReachWest, "[Rhalgr's Reach] Western Rhalgr's Reach" },
+        { EAetheryteLocation.RhalgrsReachNorthEast, "[Rhalgr's Reach] Northeastern Rhalgr's Reach" },
+        { EAetheryteLocation.RhalgrsReachFringesGate, "[Rhalgr's Reach] Fringes Gate" },
+        { EAetheryteLocation.RhalgrsReachPeaksGate, "[Rhalgr's Reach] Peaks Gate" },
+
+        { EAetheryteLocation.Kugane, "[Kugane] Aetheryte Plaza" },
+        { EAetheryteLocation.KuganeShiokazeHostelry, "[Kugane] Shiokaze Hostelry" },
+        { EAetheryteLocation.KuganePier1, "[Kugane] Pier #1" },
+        { EAetheryteLocation.KuganeThavnairianConsulate, "[Kugane] Thavnairian Consulate" },
+        { EAetheryteLocation.KuganeMarkets, "[Kugane] Kogane Dori Markets" },
+        { EAetheryteLocation.KuganeBokairoInn, "[Kugane] Bokairo Inn" },
+        { EAetheryteLocation.KuganeRubyBazaar, "[Kugane] The Ruby Bazaar" },
+        { EAetheryteLocation.KuganeSekiseigumiBarracks, "[Kugane] Sekiseigumi Barracks" },
+        { EAetheryteLocation.KuganeRakuzaDistrict, "[Kugane] Rakuza District" },
+        { EAetheryteLocation.KuganeRubyPrice, "[Kugane] The Ruby Price" },
+        { EAetheryteLocation.KuganeAirship, "[Kugane] Airship Landing" },
+
+        { EAetheryteLocation.Crystarium, "[Crystarium] Aetheryte Plaza" },
+        { EAetheryteLocation.CrystariumMarkets, "[Crystarium] Musica Universalis Markets" },
+        { EAetheryteLocation.CrystariumTemenosRookery, "[Crystarium] Temenos Rookery" },
+        { EAetheryteLocation.CrystariumDossalGate, "[Crystarium] The Dossal Gate" },
+        { EAetheryteLocation.CrystariumPendants, "[Crystarium] The Pendants" },
+        { EAetheryteLocation.CrystariumAmaroLaunch, "[Crystarium] The Amaro Launch" },
+        { EAetheryteLocation.CrystariumCrystallineMean, "[Crystarium] The Crystalline Mean" },
+        { EAetheryteLocation.CrystariumCabinetOfCuriosity, "[Crystarium] The Cabinet of Curiosity" },
+        { EAetheryteLocation.CrystariumTessellation, "[Crystarium] Tessellation (Lakeland)" },
+
+        { EAetheryteLocation.Eulmore, "[Eulmore] Aetheryte Plaza" },
+        { EAetheryteLocation.EulmoreSoutheastDerelict, "[Eulmore] Southeast Derelicts" },
+        { EAetheryteLocation.EulmoreNightsoilPots, "[Eulmore] Nightsoil Pots" },
+        { EAetheryteLocation.EulmoreGloryGate, "[Eulmore] The Glory Gate" },
+        { EAetheryteLocation.EulmoreMainstay, "[Eulmore] The Mainstay" },
+        { EAetheryteLocation.EulmorePathToGlory, "[Eulmore] The Path to Glory (Kholusia)" },
+
+        { EAetheryteLocation.OldSharlayan, "[Old Sharlayan] Aetheryte Plaza" },
+        { EAetheryteLocation.OldSharlayanStudium, "[Old Sharlayan] The Studium" },
+        { EAetheryteLocation.OldSharlayanBaldesionAnnex, "[Old Sharlayan] The Baldesion Annex" },
+        { EAetheryteLocation.OldSharlayanRostra, "[Old Sharlayan] The Rostra" },
+        { EAetheryteLocation.OldSharlayanLeveilleurEstate, "[Old Sharlayan] The Leveilleur Estate" },
+        { EAetheryteLocation.OldSharlayanJourneysEnd, "[Old Sharlayan] Journey's End" },
+        { EAetheryteLocation.OldSharlayanScholarsHarbor, "[Old Sharlayan] Scholar's Harbor" },
+        { EAetheryteLocation.OldSharlayanHallOfArtifice, "[Old Sharlayan] The Hall of Artifice (Labyrinthos)" },
+
+        { EAetheryteLocation.RadzAtHan, "[Radz-at-Han] Aetheryte Plaza" },
+        { EAetheryteLocation.RadzAtHanMeghaduta, "[Radz-at-Han] Meghaduta" },
+        { EAetheryteLocation.RadzAtHanRuveydahFibers, "[Radz-at-Han] Ruveydah Fibers" },
+        { EAetheryteLocation.RadzAtHanAirship, "[Radz-at-Han] Airship Landing" },
+        { EAetheryteLocation.RadzAtHanAlzadaalsPeace, "[Radz-at-Han] Alzadaal's Peace" },
+        { EAetheryteLocation.RadzAtHanHallOfTheRadiantHost, "[Radz-at-Han] Hall of the Radiant Host" },
+        { EAetheryteLocation.RadzAtHanMehrydesMeyhane, "[Radz-at-Han] Mehryde's Meyhane" },
+        { EAetheryteLocation.RadzAtHanKama, "[Radz-at-Han] Kama" },
+        { EAetheryteLocation.RadzAtHanHighCrucible, "[Radz-at-Han] The High Crucible of Al-Kimiya" },
+        { EAetheryteLocation.RadzAtHanGateOfFirstSight, "[Radz-at-Han] The Gate of First Sight (Thavnair)" },
+
+        { EAetheryteLocation.Tuliyollal, "[Tuliyollal] Aetheryte Plaza" },
+        { EAetheryteLocation.TuliyollalDirigibleLanding, "[Tuliyollal] Dirigible Landing" },
+        { EAetheryteLocation.TuliyollalTheResplendentQuarter, "[Tuliyollal] The Resplendent Quarter" },
+        { EAetheryteLocation.TuliyollalTheForardCabins, "[Tuliyollal] The For'ard Cabins" },
+        { EAetheryteLocation.TuliyollalBaysideBevyMarketplace, "[Tuliyollal] Bayside Bevy Marketplace" },
+        { EAetheryteLocation.TuliyollalVollokShoonsa, "[Tuliyollal] Vollok Shoonsa" },
+        { EAetheryteLocation.TuliyollalWachumeqimeqi, "[Tuliyollal] Wachumeqimeqi" },
+        { EAetheryteLocation.TuliyollalBrightploomPost, "[Tuliyollal] Brightploom Post" },
+        { EAetheryteLocation.TuliyollalArchOfTheDawnUrqopacha, "[Tuliyollal] Arch of the Dawn (Urqopacha)" },
+        { EAetheryteLocation.TuliyollalArchOfTheDawnKozamauka, "[Tuliyollal] Arch of the Dawn (Kozama'uka)" },
+        { EAetheryteLocation.TuliyollalIhuykatumu, "[Tuliyollal] Ihuykatumu (Kozama'uka)" },
+        { EAetheryteLocation.TuliyollalDirigibleLandingYakTel, "[Tuliyollal] Dirigible Landing (Yak T'el)" },
+        { EAetheryteLocation.TuliyollalXakTuralSkygate, "[Tuliyollal] Xak Tural Skygate (Shaaloani)" },
+
+        { EAetheryteLocation.SolutionNine, "[Solution Nine] Aetheryte Plaza" },
+        { EAetheryteLocation.SolutionNineInformationCenter, "[Solution Nine] Information Center" },
+        { EAetheryteLocation.SolutionNineTrueVue, "[Solution Nine] True Vue" },
+        { EAetheryteLocation.SolutionNineNeonStein, "[Solution Nine] Neon Stein" },
+        { EAetheryteLocation.SolutionNineTheArcadion, "[Solution Nine] The Arcadion" },
+        { EAetheryteLocation.SolutionNineResolution, "[Solution Nine] Resolution" },
+        { EAetheryteLocation.SolutionNineNexusArcade, "[Solution Nine] Nexus Arcade" },
+        { EAetheryteLocation.SolutionNineResidentialSector, "[Solution Nine] Residential Sector" },
+        { EAetheryteLocation.SolutionNineScanningPortNine, "[Solution Nine] Scanning Port Nine (Heritage Found)" },
+    };
+}
diff --git a/Questionable.Model/Questing/Converter/AethernetShortcutConverter.cs b/Questionable.Model/Questing/Converter/AethernetShortcutConverter.cs
new file mode 100644 (file)
index 0000000..d920d88
--- /dev/null
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Questionable.Model.Common;
+
+namespace Questionable.Model.Questing.Converter;
+
+public sealed class AethernetShortcutConverter : JsonConverter<AethernetShortcut>
+{
+    private static readonly Dictionary<EAetheryteLocation, string> EnumToString = AethernetShardConverter.Values;
+    private static readonly Dictionary<string, EAetheryteLocation> StringToEnum =
+        EnumToString.ToDictionary(x => x.Value, x => x.Key);
+
+    public override AethernetShortcut Read(ref Utf8JsonReader reader, Type typeToConvert,
+        JsonSerializerOptions options)
+    {
+        if (reader.TokenType != JsonTokenType.StartArray)
+            throw new JsonException();
+
+        if (!reader.Read() || reader.TokenType != JsonTokenType.String)
+            throw new JsonException();
+
+        string from = reader.GetString() ?? throw new JsonException();
+
+        if (!reader.Read() || reader.TokenType != JsonTokenType.String)
+            throw new JsonException();
+
+        string to = reader.GetString() ?? throw new JsonException();
+
+        if (!reader.Read() || reader.TokenType != JsonTokenType.EndArray)
+            throw new JsonException();
+
+        return new AethernetShortcut
+        {
+            From = StringToEnum.TryGetValue(from, out var fromEnum) ? fromEnum : throw new JsonException(),
+            To = StringToEnum.TryGetValue(to, out var toEnum) ? toEnum : throw new JsonException()
+        };
+    }
+
+    public override void Write(Utf8JsonWriter writer, AethernetShortcut value, JsonSerializerOptions options)
+    {
+        writer.WriteStartArray();
+        writer.WriteStringValue(EnumToString[value.From]);
+        writer.WriteStringValue(EnumToString[value.To]);
+        writer.WriteEndArray();
+    }
+}
diff --git a/Questionable.Model/Questing/Converter/DialogueChoiceTypeConverter.cs b/Questionable.Model/Questing/Converter/DialogueChoiceTypeConverter.cs
new file mode 100644 (file)
index 0000000..cd66ca9
--- /dev/null
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using Questionable.Model.Common.Converter;
+
+namespace Questionable.Model.Questing.Converter;
+
+public sealed class DialogueChoiceTypeConverter() : EnumConverter<EDialogChoiceType>(Values)
+{
+    private static readonly Dictionary<EDialogChoiceType, string> Values = new()
+    {
+        { EDialogChoiceType.YesNo, "YesNo" },
+        { EDialogChoiceType.List, "List" },
+    };
+}
diff --git a/Questionable.Model/Questing/Converter/EmoteConverter.cs b/Questionable.Model/Questing/Converter/EmoteConverter.cs
new file mode 100644 (file)
index 0000000..20c9769
--- /dev/null
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using Questionable.Model.Common.Converter;
+
+namespace Questionable.Model.Questing.Converter;
+
+public sealed class EmoteConverter() : EnumConverter<EEmote>(Values)
+{
+    private static readonly Dictionary<EEmote, string> Values = new()
+    {
+        { EEmote.Stretch, "stretch" },
+        { EEmote.Wave, "wave" },
+        { EEmote.Rally, "rally" },
+        { EEmote.Deny, "deny" },
+        { EEmote.Pray, "pray" },
+        { EEmote.Slap, "slap" },
+        { EEmote.Doubt, "doubt" },
+        { EEmote.Psych, "psych" },
+        { EEmote.Cheer, "cheer" },
+        { EEmote.Happy, "happy" },
+        { EEmote.Poke, "poke" },
+        { EEmote.Flex, "flex" },
+        { EEmote.Soothe, "soothe" },
+        { EEmote.Me, "me" },
+        { EEmote.Welcome, "welcome" },
+        { EEmote.ImperialSalute, "imperialsalute" },
+        { EEmote.Pet, "pet" },
+        { EEmote.Dance, "dance" },
+        { EEmote.Respect, "respect" },
+        { EEmote.Lookout, "lookout" },
+        { EEmote.Kneel, "kneel" },
+        { EEmote.Bow, "bow" },
+    };
+}
diff --git a/Questionable.Model/Questing/Converter/EnemySpawnTypeConverter.cs b/Questionable.Model/Questing/Converter/EnemySpawnTypeConverter.cs
new file mode 100644 (file)
index 0000000..e97faa4
--- /dev/null
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using Questionable.Model.Common.Converter;
+
+namespace Questionable.Model.Questing.Converter;
+
+public sealed class EnemySpawnTypeConverter() : EnumConverter<EEnemySpawnType>(Values)
+{
+    private static readonly Dictionary<EEnemySpawnType, string> Values = new()
+    {
+        { EEnemySpawnType.AfterInteraction, "AfterInteraction" },
+        { EEnemySpawnType.AfterItemUse, "AfterItemUse" },
+        { EEnemySpawnType.AutoOnEnterArea, "AutoOnEnterArea" },
+        { EEnemySpawnType.OverworldEnemies, "OverworldEnemies" },
+    };
+}
diff --git a/Questionable.Model/Questing/Converter/ExcelRefConverter.cs b/Questionable.Model/Questing/Converter/ExcelRefConverter.cs
new file mode 100644 (file)
index 0000000..03a8168
--- /dev/null
@@ -0,0 +1,30 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Questionable.Model.Questing.Converter;
+
+public sealed class ExcelRefConverter : JsonConverter<ExcelRef>
+{
+    public override ExcelRef? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+    {
+        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)
+    {
+        if (value == null)
+            writer.WriteNullValue();
+        else if (value.Type == ExcelRef.EType.Key)
+            writer.WriteStringValue(value.AsKey());
+        else if (value.Type == ExcelRef.EType.RowId)
+            writer.WriteNumberValue(value.AsRowId());
+        else
+            throw new JsonException();
+    }
+}
diff --git a/Questionable.Model/Questing/Converter/InteractionTypeConverter.cs b/Questionable.Model/Questing/Converter/InteractionTypeConverter.cs
new file mode 100644 (file)
index 0000000..23f0366
--- /dev/null
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using Questionable.Model.Common.Converter;
+
+namespace Questionable.Model.Questing.Converter;
+
+public sealed class InteractionTypeConverter() : EnumConverter<EInteractionType>(Values)
+{
+    private static readonly Dictionary<EInteractionType, string> Values = new()
+    {
+        { EInteractionType.Interact, "Interact" },
+        { EInteractionType.WalkTo, "WalkTo" },
+        { EInteractionType.AttuneAethernetShard, "AttuneAethernetShard" },
+        { EInteractionType.AttuneAetheryte, "AttuneAetheryte" },
+        { EInteractionType.AttuneAetherCurrent, "AttuneAetherCurrent" },
+        { EInteractionType.Combat, "Combat" },
+        { EInteractionType.UseItem, "UseItem" },
+        { EInteractionType.EquipItem, "EquipItem" },
+        { EInteractionType.Say, "Say" },
+        { EInteractionType.Emote, "Emote" },
+        { EInteractionType.Action, "Action" },
+        { EInteractionType.WaitForObjectAtPosition, "WaitForNpcAtPosition" },
+        { EInteractionType.WaitForManualProgress, "WaitForManualProgress" },
+        { EInteractionType.Duty, "Duty" },
+        { EInteractionType.SinglePlayerDuty, "SinglePlayerDuty" },
+        { EInteractionType.Jump, "Jump" },
+        { EInteractionType.Dive, "Dive" },
+        { EInteractionType.Instruction, "Instruction" },
+        { EInteractionType.AcceptQuest, "AcceptQuest" },
+        { EInteractionType.CompleteQuest, "CompleteQuest" },
+    };
+}
diff --git a/Questionable.Model/Questing/Converter/JumpTypeConverter.cs b/Questionable.Model/Questing/Converter/JumpTypeConverter.cs
new file mode 100644 (file)
index 0000000..95bb084
--- /dev/null
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using Questionable.Model.Common.Converter;
+
+namespace Questionable.Model.Questing.Converter;
+
+public sealed class JumpTypeConverter() : EnumConverter<EJumpType>(Values)
+{
+    private static readonly Dictionary<EJumpType, string> Values = new()
+    {
+        { EJumpType.SingleJump, "SingleJump" },
+        { EJumpType.RepeatedJumps, "RepeatedJumps" },
+    };
+}
diff --git a/Questionable.Model/Questing/Converter/LockedSkipConditionConverter.cs b/Questionable.Model/Questing/Converter/LockedSkipConditionConverter.cs
new file mode 100644 (file)
index 0000000..72eb9de
--- /dev/null
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using Questionable.Model.Common.Converter;
+
+namespace Questionable.Model.Questing.Converter;
+
+public sealed class LockedSkipConditionConverter() : EnumConverter<ELockedSkipCondition>(Values)
+{
+    private static readonly Dictionary<ELockedSkipCondition, string> Values = new()
+    {
+        { ELockedSkipCondition.Locked, "Locked" },
+        { ELockedSkipCondition.Unlocked, "Unlocked" },
+    };
+}
diff --git a/Questionable.Model/Questing/Converter/QuestWorkConfigConverter.cs b/Questionable.Model/Questing/Converter/QuestWorkConfigConverter.cs
new file mode 100644 (file)
index 0000000..b6cccc4
--- /dev/null
@@ -0,0 +1,63 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Questionable.Model.Questing.Converter;
+
+public sealed class QuestWorkConfigConverter : JsonConverter<QuestWorkValue>
+{
+    public override QuestWorkValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+    {
+        if (reader.TokenType == JsonTokenType.Number)
+            return new QuestWorkValue(reader.GetByte());
+
+        if (reader.TokenType != JsonTokenType.StartObject)
+            throw new JsonException();
+
+        byte? high = null, low = null;
+        EQuestWorkMode mode = EQuestWorkMode.Bitwise;
+        while (reader.Read())
+        {
+            switch (reader.TokenType)
+            {
+                case JsonTokenType.PropertyName:
+                    string? propertyName = reader.GetString();
+                    if (propertyName == null || !reader.Read())
+                        throw new JsonException();
+
+                    switch (propertyName)
+                    {
+                        case nameof(QuestWorkValue.High):
+                            high = reader.GetByte();
+                            break;
+
+                        case nameof(QuestWorkValue.Low):
+                            low = reader.GetByte();
+                            break;
+
+                        case nameof(QuestWorkValue.Mode):
+                            mode = new QuestWorkModeConverter().Read(ref reader, typeof(EQuestWorkMode), options);
+                            break;
+
+                        default:
+                            throw new JsonException();
+                    }
+
+                    break;
+
+                case JsonTokenType.EndObject:
+                    return new QuestWorkValue(high, low, mode);
+
+                default:
+                    throw new JsonException();
+            }
+        }
+
+        throw new JsonException();
+    }
+
+    public override void Write(Utf8JsonWriter writer, QuestWorkValue value, JsonSerializerOptions options)
+    {
+        throw new NotImplementedException();
+    }
+}
diff --git a/Questionable.Model/Questing/Converter/QuestWorkModeConverter.cs b/Questionable.Model/Questing/Converter/QuestWorkModeConverter.cs
new file mode 100644 (file)
index 0000000..322d6a1
--- /dev/null
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using Questionable.Model.Common.Converter;
+
+namespace Questionable.Model.Questing.Converter;
+
+public sealed class QuestWorkModeConverter() : EnumConverter<EQuestWorkMode>(Values)
+{
+    private static readonly Dictionary<EQuestWorkMode, string> Values = new()
+    {
+        { EQuestWorkMode.Bitwise, "Bitwise" },
+        { EQuestWorkMode.Exact, "Exact" },
+    };
+}
diff --git a/Questionable.Model/Questing/Converter/SkipConditionConverter.cs b/Questionable.Model/Questing/Converter/SkipConditionConverter.cs
new file mode 100644 (file)
index 0000000..eadd84b
--- /dev/null
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+using Questionable.Model.Common.Converter;
+
+namespace Questionable.Model.Questing.Converter;
+
+public sealed class SkipConditionConverter() : EnumConverter<EExtraSkipCondition>(Values)
+{
+    private static readonly Dictionary<EExtraSkipCondition, string> Values = new()
+    {
+        { EExtraSkipCondition.WakingSandsMainArea, "WakingSandsMainArea" },
+    };
+}
diff --git a/Questionable.Model/Questing/DialogueChoice.cs b/Questionable.Model/Questing/DialogueChoice.cs
new file mode 100644 (file)
index 0000000..91370f5
--- /dev/null
@@ -0,0 +1,24 @@
+using System.Text.Json.Serialization;
+using Questionable.Model.Questing.Converter;
+
+namespace Questionable.Model.Questing;
+
+public sealed class DialogueChoice
+{
+    [JsonConverter(typeof(DialogueChoiceTypeConverter))]
+    public EDialogChoiceType Type { get; set; }
+    public string? ExcelSheet { get; set; }
+
+    [JsonConverter(typeof(ExcelRefConverter))]
+    public ExcelRef? Prompt { get; set; }
+
+    public bool Yes { get; set; } = true;
+
+    [JsonConverter(typeof(ExcelRefConverter))]
+    public ExcelRef? Answer { get; set; }
+
+    /// <summary>
+    /// If set, only applies when focusing the given target id.
+    /// </summary>
+    public uint? DataId { get; set; }
+}
diff --git a/Questionable.Model/Questing/EAction.cs b/Questionable.Model/Questing/EAction.cs
new file mode 100644 (file)
index 0000000..4b24dee
--- /dev/null
@@ -0,0 +1,32 @@
+using System.Text.Json.Serialization;
+using Questionable.Model.Questing.Converter;
+
+namespace Questionable.Model.Questing;
+
+[JsonConverter(typeof(ActionConverter))]
+public enum EAction
+{
+    Cure = 120,
+    Esuna = 7568,
+    Physick = 190,
+    Buffet = 4931,
+    Fumigate = 5872,
+    SiphonSnout = 18187,
+    RedGulal = 29382,
+    YellowGulal = 29383,
+    BlueGulal = 29384,
+}
+
+public static class EActionExtensions
+{
+    public static bool RequiresMount(this EAction action)
+    {
+        return action
+            is EAction.Buffet
+            or EAction.Fumigate
+            or EAction.SiphonSnout
+            or EAction.RedGulal
+            or EAction.YellowGulal
+            or EAction.BlueGulal;
+    }
+}
diff --git a/Questionable.Model/Questing/EDialogChoiceType.cs b/Questionable.Model/Questing/EDialogChoiceType.cs
new file mode 100644 (file)
index 0000000..9b53747
--- /dev/null
@@ -0,0 +1,8 @@
+namespace Questionable.Model.Questing;
+
+public enum EDialogChoiceType
+{
+    None,
+    YesNo,
+    List,
+}
diff --git a/Questionable.Model/Questing/EEmote.cs b/Questionable.Model/Questing/EEmote.cs
new file mode 100644 (file)
index 0000000..022930e
--- /dev/null
@@ -0,0 +1,33 @@
+using System.Text.Json.Serialization;
+using Questionable.Model.Questing.Converter;
+
+namespace Questionable.Model.Questing;
+
+[JsonConverter(typeof(EmoteConverter))]
+public enum EEmote
+{
+    None = 0,
+
+    Stretch = 37,
+    Wave = 16,
+    Rally = 34,
+    Deny = 25,
+    Pray = 58,
+    Slap = 111,
+    Doubt = 12,
+    Psych = 30,
+    Cheer = 6,
+    Happy = 48,
+    Poke = 28,
+    Flex = 139,
+    Soothe = 35,
+    Me = 23,
+    Welcome = 41,
+    ImperialSalute = 59,
+    Pet = 105,
+    Dance = 11,
+    Respect = 140,
+    Lookout = 22,
+    Kneel = 19,
+    Bow = 5,
+}
diff --git a/Questionable.Model/Questing/EEnemySpawnType.cs b/Questionable.Model/Questing/EEnemySpawnType.cs
new file mode 100644 (file)
index 0000000..3c42b9f
--- /dev/null
@@ -0,0 +1,14 @@
+using System.Text.Json.Serialization;
+using Questionable.Model.Questing.Converter;
+
+namespace Questionable.Model.Questing;
+
+[JsonConverter(typeof(EnemySpawnTypeConverter))]
+public enum EEnemySpawnType
+{
+    None = 0,
+    AfterInteraction,
+    AfterItemUse,
+    AutoOnEnterArea,
+    OverworldEnemies,
+}
diff --git a/Questionable.Model/Questing/EExtraSkipCondition.cs b/Questionable.Model/Questing/EExtraSkipCondition.cs
new file mode 100644 (file)
index 0000000..3f2836a
--- /dev/null
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+using Questionable.Model.Questing.Converter;
+
+namespace Questionable.Model.Questing;
+
+[JsonConverter(typeof(SkipConditionConverter))]
+public enum EExtraSkipCondition
+{
+    None,
+    WakingSandsMainArea,
+}
diff --git a/Questionable.Model/Questing/EInteractionType.cs b/Questionable.Model/Questing/EInteractionType.cs
new file mode 100644 (file)
index 0000000..5080714
--- /dev/null
@@ -0,0 +1,34 @@
+using System.Text.Json.Serialization;
+using Questionable.Model.Questing.Converter;
+
+namespace Questionable.Model.Questing;
+
+[JsonConverter(typeof(InteractionTypeConverter))]
+public enum EInteractionType
+{
+    Interact,
+    WalkTo,
+    AttuneAethernetShard,
+    AttuneAetheryte,
+    AttuneAetherCurrent,
+    Combat,
+    UseItem,
+    EquipItem,
+    Say,
+    Emote,
+    Action,
+    WaitForObjectAtPosition,
+    WaitForManualProgress,
+    Duty,
+    SinglePlayerDuty,
+    Jump,
+    Dive,
+
+    /// <summary>
+    /// Needs to be manually continued.
+    /// </summary>
+    Instruction,
+
+    AcceptQuest,
+    CompleteQuest,
+}
diff --git a/Questionable.Model/Questing/EJumpType.cs b/Questionable.Model/Questing/EJumpType.cs
new file mode 100644 (file)
index 0000000..e89931a
--- /dev/null
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+using Questionable.Model.Questing.Converter;
+
+namespace Questionable.Model.Questing;
+
+[JsonConverter(typeof(JumpTypeConverter))]
+public enum EJumpType
+{
+    SingleJump,
+    RepeatedJumps,
+}
diff --git a/Questionable.Model/Questing/ELockedSkipCondition.cs b/Questionable.Model/Questing/ELockedSkipCondition.cs
new file mode 100644 (file)
index 0000000..7755b12
--- /dev/null
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+using Questionable.Model.Questing.Converter;
+
+namespace Questionable.Model.Questing;
+
+[JsonConverter(typeof(LockedSkipConditionConverter))]
+public enum ELockedSkipCondition
+{
+    Locked,
+    Unlocked,
+}
diff --git a/Questionable.Model/Questing/EQuestWorkMode.cs b/Questionable.Model/Questing/EQuestWorkMode.cs
new file mode 100644 (file)
index 0000000..3d83522
--- /dev/null
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+using Questionable.Model.Questing.Converter;
+
+namespace Questionable.Model.Questing;
+
+[JsonConverter(typeof(QuestWorkModeConverter))]
+public enum EQuestWorkMode
+{
+    Bitwise,
+    Exact,
+}
diff --git a/Questionable.Model/Questing/ExcelRef.cs b/Questionable.Model/Questing/ExcelRef.cs
new file mode 100644 (file)
index 0000000..cdb5501
--- /dev/null
@@ -0,0 +1,68 @@
+using System;
+
+namespace Questionable.Model.Questing;
+
+public class ExcelRef
+{
+    private readonly string? _stringValue;
+    private readonly uint? _rowIdValue;
+
+    public ExcelRef(string value)
+    {
+        _stringValue = value;
+        _rowIdValue = null;
+        Type = EType.Key;
+    }
+
+    public ExcelRef(uint value)
+    {
+        _stringValue = null;
+        _rowIdValue = value;
+        Type = EType.RowId;
+    }
+
+    private ExcelRef(string? stringValue, uint? rowIdValue, EType type)
+    {
+        _stringValue = stringValue;
+        _rowIdValue = rowIdValue;
+        Type = type;
+    }
+
+    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; }
+
+    public string AsKey()
+    {
+        if (Type != EType.Key)
+            throw new InvalidOperationException();
+
+        return _stringValue!;
+    }
+
+    public uint AsRowId()
+    {
+        if (Type != EType.RowId)
+            throw new InvalidOperationException();
+
+        return _rowIdValue!.Value;
+    }
+
+    public string AsRawString()
+    {
+        if (Type != EType.RawString)
+            throw new InvalidOperationException();
+
+        return _stringValue!;
+    }
+
+    public enum EType
+    {
+        None,
+        Key,
+        RowId,
+        RawString,
+    }
+}
diff --git a/Questionable.Model/Questing/JumpDestination.cs b/Questionable.Model/Questing/JumpDestination.cs
new file mode 100644 (file)
index 0000000..9682e21
--- /dev/null
@@ -0,0 +1,18 @@
+using System.Numerics;
+using System.Text.Json.Serialization;
+using Questionable.Model.Common.Converter;
+using Questionable.Model.Questing.Converter;
+
+namespace Questionable.Model.Questing;
+
+public sealed class JumpDestination
+{
+    [JsonConverter(typeof(VectorConverter))]
+    public Vector3 Position { get; set; }
+
+    public float? StopDistance { get; set; }
+    public float? DelaySeconds { get; set; }
+    public EJumpType Type { get; set; } = EJumpType.SingleJump;
+
+    public float CalculateStopDistance() => StopDistance ?? 1f;
+}
diff --git a/Questionable.Model/Questing/QuestRoot.cs b/Questionable.Model/Questing/QuestRoot.cs
new file mode 100644 (file)
index 0000000..e1158fc
--- /dev/null
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+using Questionable.Model.Common.Converter;
+using Questionable.Model.Questing.Converter;
+
+namespace Questionable.Model.Questing;
+
+public sealed class QuestRoot
+{
+    [JsonConverter(typeof(StringListOrValueConverter))]
+    public List<string> Author { get; set; } = new();
+
+    /// <summary>
+    /// This is only relevant for release builds.
+    /// </summary>
+    public bool Disabled { get; set; }
+
+    public string? Comment { get; set; }
+    public List<ushort> TerritoryBlacklist { get; set; } = new();
+    public List<QuestSequence> QuestSequence { get; set; } = new();
+}
diff --git a/Questionable.Model/Questing/QuestSequence.cs b/Questionable.Model/Questing/QuestSequence.cs
new file mode 100644 (file)
index 0000000..3e42e09
--- /dev/null
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Questionable.Model.Questing;
+
+public sealed class QuestSequence
+{
+    public int Sequence { get; set; }
+    public string? Comment { get; set; }
+    public List<QuestStep> Steps { get; set; } = new();
+
+    public QuestStep? FindStep(int step)
+    {
+        if (step < 0 || step >= Steps.Count)
+            return null;
+
+        return Steps[step];
+    }
+
+    public QuestStep? LastStep() => Steps.LastOrDefault();
+}
diff --git a/Questionable.Model/Questing/QuestStep.cs b/Questionable.Model/Questing/QuestStep.cs
new file mode 100644 (file)
index 0000000..53ed7ce
--- /dev/null
@@ -0,0 +1,101 @@
+using System.Collections.Generic;
+using System.Numerics;
+using System.Text.Json.Serialization;
+using Questionable.Model.Common;
+using Questionable.Model.Common.Converter;
+using Questionable.Model.Questing.Converter;
+
+namespace Questionable.Model.Questing;
+
+public sealed class QuestStep
+{
+    public const float DefaultStopDistance = 3f;
+
+    public EInteractionType InteractionType { get; set; }
+
+    public uint? DataId { get; set; }
+
+    [JsonConverter(typeof(VectorConverter))]
+    public Vector3? Position { get; set; }
+
+    public float? StopDistance { get; set; }
+    public float? NpcWaitDistance { get; set; }
+    public ushort TerritoryId { get; set; }
+    public ushort? TargetTerritoryId { get; set; }
+    public float? DelaySecondsAtStart { get; set; }
+
+    public bool Disabled { get; set; }
+    public bool DisableNavmesh { get; set; }
+    public bool? Mount { get; set; }
+    public bool? Fly { get; set; }
+    public bool? Land { get; set; }
+    public bool? Sprint { get; set; }
+    public bool? IgnoreDistanceToObject { get; set; }
+    public string? Comment { get; set; }
+
+    /// <summary>
+    /// Only used when attuning to an aetheryte.
+    /// </summary>
+    public EAetheryteLocation? Aetheryte { get; set; }
+
+    /// <summary>
+    /// Only used when attuning to an aethernet shard.
+    /// </summary>
+    [JsonConverter(typeof(AethernetShardConverter))]
+    public EAetheryteLocation? AethernetShard { get; set; }
+
+    public EAetheryteLocation? AetheryteShortcut { get; set; }
+
+    public AethernetShortcut? AethernetShortcut { get; set; }
+    public uint? AetherCurrentId { get; set; }
+
+    public uint? ItemId { get; set; }
+    public bool? GroundTarget { get; set; }
+
+    public EEmote? Emote { get; set; }
+    public ChatMessage? ChatMessage { get; set; }
+    public EAction? Action { get; set; }
+
+    public EEnemySpawnType? EnemySpawnType { get; set; }
+    public IList<uint> KillEnemyDataIds { get; set; } = new List<uint>();
+    public IList<ComplexCombatData> ComplexCombatData { get; set; } = new List<ComplexCombatData>();
+    public float? CombatDelaySecondsAtStart { get; set; }
+
+    public JumpDestination? JumpDestination { get; set; }
+    public uint? ContentFinderConditionId { get; set; }
+    public SkipConditions? SkipConditions { get; set; }
+
+    public List<List<QuestWorkValue>?> RequiredQuestVariables { get; set; } = new();
+    public IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; set; } = new List<QuestWorkValue?>();
+    public IList<DialogueChoice> DialogueChoices { get; set; } = new List<DialogueChoice>();
+    public IList<uint> PointMenuChoices { get; set; } = new List<uint>();
+
+    // TODO: Not implemented
+    public ushort? PickUpQuestId { get; set; }
+
+    public ushort? TurnInQuestId { get; set; }
+    public ushort? NextQuestId { get; set; }
+
+    [JsonConstructor]
+    public QuestStep()
+    {
+    }
+
+    public QuestStep(EInteractionType interactionType, uint? dataId, Vector3? position, ushort territoryId)
+    {
+        InteractionType = interactionType;
+        DataId = dataId;
+        Position = position;
+        TerritoryId = territoryId;
+    }
+
+    public float CalculateActualStopDistance()
+    {
+        if (InteractionType == EInteractionType.WalkTo)
+            return StopDistance ?? 0.25f;
+        if (InteractionType == EInteractionType.AttuneAetheryte)
+            return StopDistance ?? 10f;
+        else
+            return StopDistance ?? DefaultStopDistance;
+    }
+}
diff --git a/Questionable.Model/Questing/QuestWorkValue.cs b/Questionable.Model/Questing/QuestWorkValue.cs
new file mode 100644 (file)
index 0000000..d4db77e
--- /dev/null
@@ -0,0 +1,30 @@
+using System;
+using System.Text.Json.Serialization;
+using Questionable.Model.Questing.Converter;
+
+namespace Questionable.Model.Questing;
+
+[JsonConverter(typeof(QuestWorkConfigConverter))]
+public sealed class QuestWorkValue(byte? high, byte? low, EQuestWorkMode mode)
+{
+    public QuestWorkValue(byte value)
+        : this((byte)(value >> 4), (byte)(value & 0xF), EQuestWorkMode.Bitwise)
+    {
+    }
+
+    public byte? High { get; set; } = high;
+    public byte? Low { get; set; } = low;
+    public EQuestWorkMode Mode { get; set; } = mode;
+
+    public override string ToString()
+    {
+        if (High != null && Low != null)
+            return ((byte)(High << 4) + Low).ToString();
+        else if (High != null)
+            return High + "H";
+        else if (Low != null)
+            return Low + "L";
+        else
+            return "-";
+    }
+}
diff --git a/Questionable.Model/Questing/SkipAetheryteCondition.cs b/Questionable.Model/Questing/SkipAetheryteCondition.cs
new file mode 100644 (file)
index 0000000..0109e4d
--- /dev/null
@@ -0,0 +1,7 @@
+namespace Questionable.Model.Questing;
+
+public sealed class SkipAetheryteCondition
+{
+    public bool Never { get; set; }
+    public bool InSameTerritory { get; set; }
+}
diff --git a/Questionable.Model/Questing/SkipConditions.cs b/Questionable.Model/Questing/SkipConditions.cs
new file mode 100644 (file)
index 0000000..6525ec0
--- /dev/null
@@ -0,0 +1,8 @@
+namespace Questionable.Model.Questing;
+
+public sealed class SkipConditions
+{
+    public SkipStepConditions? StepIf { get; set; }
+    public SkipAetheryteCondition? AetheryteShortcutIf { get; set; }
+    public SkipAetheryteCondition? AethernetShortcutIf { get; set; }
+}
diff --git a/Questionable.Model/Questing/SkipItemConditions.cs b/Questionable.Model/Questing/SkipItemConditions.cs
new file mode 100644 (file)
index 0000000..04c38f6
--- /dev/null
@@ -0,0 +1,6 @@
+namespace Questionable.Model.Questing;
+
+public sealed class SkipItemConditions
+{
+    public bool NotInInventory { get; set; }
+}
diff --git a/Questionable.Model/Questing/SkipStepConditions.cs b/Questionable.Model/Questing/SkipStepConditions.cs
new file mode 100644 (file)
index 0000000..ee7e7dc
--- /dev/null
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Questionable.Model.Questing;
+
+public sealed class SkipStepConditions
+{
+    public bool Never { get; set; }
+    public IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; set; } = new List<QuestWorkValue?>();
+    public ELockedSkipCondition? Flying { get; set; }
+    public ELockedSkipCondition? Chocobo { get; set; }
+    public bool NotTargetable { get; set; }
+    public List<ushort> InTerritory { get; set; } = new();
+    public List<ushort> NotInTerritory { get; set; } = new();
+    public SkipItemConditions? Item { get; set; }
+    public List<ushort> QuestsAccepted { get; set; } = new();
+    public List<ushort> QuestsCompleted { get; set; } = new();
+    public EExtraSkipCondition? ExtraCondition { get; set; }
+
+    public bool HasSkipConditions()
+    {
+        if (Never)
+            return false;
+        return (CompletionQuestVariablesFlags.Count > 0 && CompletionQuestVariablesFlags.Any(x => x != null)) ||
+               Flying != null ||
+               Chocobo != null ||
+               NotTargetable ||
+               InTerritory.Count > 0 ||
+               NotInTerritory.Count > 0 ||
+               Item != null ||
+               QuestsAccepted.Count > 0 ||
+               QuestsCompleted.Count > 0 ||
+               ExtraCondition != null;
+    }
+
+    public override string ToString()
+    {
+        return
+            $"{nameof(Never)}: {Never}, {nameof(CompletionQuestVariablesFlags)}: {CompletionQuestVariablesFlags}, {nameof(Flying)}: {Flying}, {nameof(Chocobo)}: {Chocobo}, {nameof(NotTargetable)}: {NotTargetable}, {nameof(InTerritory)}: {string.Join(" ", InTerritory)}, {nameof(NotInTerritory)}: {string.Join(" ", NotInTerritory)}, {nameof(Item)}: {Item}, {nameof(QuestsAccepted)}: {string.Join(" ", QuestsAccepted)}, {nameof(QuestsCompleted)}: {string.Join(" ", QuestsCompleted)}, {nameof(ExtraCondition)}: {ExtraCondition}";
+    }
+}
index cb320ec55be7762ceaf8a5c1232dbcc190dcb865..54301430aa0ac8cbdfba63a5fea3acc1da3f7a2b 100644 (file)
     <ItemGroup>
         <PackageReference Include="System.Text.Json" Version="8.0.4" />
     </ItemGroup>
+
+
+    <ItemGroup>
+        <None Remove="common-schema.json" />
+        <EmbeddedResource Include="common-schema.json">
+            <LogicalName>Questionable.Model.CommonSchema</LogicalName>
+        </EmbeddedResource>
+    </ItemGroup>
 </Project>
diff --git a/Questionable.Model/V1/AethernetShortcut.cs b/Questionable.Model/V1/AethernetShortcut.cs
deleted file mode 100644 (file)
index 40514b5..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-[JsonConverter(typeof(AethernetShortcutConverter))]
-public sealed class AethernetShortcut
-{
-    public EAetheryteLocation From { get; set; }
-    public EAetheryteLocation To { get; set; }
-}
diff --git a/Questionable.Model/V1/ChatMessage.cs b/Questionable.Model/V1/ChatMessage.cs
deleted file mode 100644 (file)
index 7172138..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Questionable.Model.V1;
-
-public sealed class ChatMessage
-{
-    public string? ExcelSheet { get; set; }
-    public string Key { get; set; } = null!;
-}
diff --git a/Questionable.Model/V1/ComplexCombatData.cs b/Questionable.Model/V1/ComplexCombatData.cs
deleted file mode 100644 (file)
index d31de7e..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Collections.Generic;
-
-namespace Questionable.Model.V1;
-
-public sealed class ComplexCombatData
-{
-    public uint DataId { get; set; }
-
-    // TODO Use this
-    public uint? MinimumKillCount { get; set; }
-
-    /// <summary>
-    /// If a reward item has been set, this is (ping allowing) given to the player before the dead enemy despawns.
-    /// </summary>
-    public uint? RewardItemId { get; set; }
-    public int? RewardItemCount { get; set; }
-    public IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; set; } = new List<QuestWorkValue?>();
-    public bool IgnoreQuestMarker { get; set; }
-}
diff --git a/Questionable.Model/V1/Converter/ActionConverter.cs b/Questionable.Model/V1/Converter/ActionConverter.cs
deleted file mode 100644 (file)
index eaf5baa..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Collections.Generic;
-
-namespace Questionable.Model.V1.Converter;
-
-public sealed class ActionConverter() : EnumConverter<EAction>(Values)
-{
-    private static readonly Dictionary<EAction, string> Values = new()
-    {
-        { EAction.Cure, "Cure" },
-        { EAction.Esuna, "Esuna" },
-        { EAction.Physick, "Physick" },
-        { EAction.Buffet, "Buffet" },
-        { EAction.Fumigate, "Fumigate" },
-        { EAction.SiphonSnout, "Siphon Snout" },
-        { EAction.RedGulal, "Red Gulal" },
-        { EAction.YellowGulal, "Yellow Gulal" },
-        { EAction.BlueGulal, "Blue Gulal" },
-    };
-}
diff --git a/Questionable.Model/V1/Converter/AethernetShardConverter.cs b/Questionable.Model/V1/Converter/AethernetShardConverter.cs
deleted file mode 100644 (file)
index bfb259c..0000000
+++ /dev/null
@@ -1,142 +0,0 @@
-using System.Collections.Generic;
-
-namespace Questionable.Model.V1.Converter;
-
-public sealed class AethernetShardConverter() : EnumConverter<EAetheryteLocation>(Values)
-{
-    public static readonly Dictionary<EAetheryteLocation, string> Values = new()
-    {
-        { EAetheryteLocation.Gridania, "[Gridania] Aetheryte Plaza" },
-        { EAetheryteLocation.GridaniaArcher, "[Gridania] Archers' Guild" },
-        { EAetheryteLocation.GridaniaLeatherworker, "[Gridania] Leatherworkers' Guild & Shaded Bower" },
-        { EAetheryteLocation.GridaniaLancer, "[Gridania] Lancers' Guild" },
-        { EAetheryteLocation.GridaniaConjurer, "[Gridania] Conjurers' Guild" },
-        { EAetheryteLocation.GridaniaBotanist, "[Gridania] Botanists' Guild" },
-        { EAetheryteLocation.GridaniaAmphitheatre, "[Gridania] Mih Khetto's Amphitheatre" },
-        { EAetheryteLocation.GridaniaBlueBadgerGate, "[Gridania] Blue Badger Gate (Central Shroud)" },
-        { EAetheryteLocation.GridaniaYellowSerpentGate, "[Gridania] Yellow Serpent Gate (North Shroud)" },
-        { EAetheryteLocation.GridaniaWhiteWolfGate, "[Gridania] White Wolf Gate (Central Shroud)" },
-        { EAetheryteLocation.GridaniaAirship, "[Gridania] Airship Landing" },
-
-        { EAetheryteLocation.Uldah, "[Ul'dah] Aetheryte Plaza" },
-        { EAetheryteLocation.UldahAdventurers, "[Ul'dah] Adventurers' Guild" },
-        { EAetheryteLocation.UldahThaumaturge, "[Ul'dah] Thaumaturges' Guild" },
-        { EAetheryteLocation.UldahGladiator, "[Ul'dah] Gladiators' Guild" },
-        { EAetheryteLocation.UldahMiner, "[Ul'dah] Miners' Guild" },
-        { EAetheryteLocation.UldahWeaver, "[Ul'dah] Weavers' Guild" },
-        { EAetheryteLocation.UldahGoldsmith, "[Ul'dah] Goldsmiths' Guild" },
-        { EAetheryteLocation.UldahSapphireAvenue, "[Ul'dah] Sapphire Avenue Exchange" },
-        { EAetheryteLocation.UldahAlchemist, "[Ul'dah] Alchemists' Guild" },
-        { EAetheryteLocation.UldahChamberOfRule, "[Ul'dah] The Chamber of Rule" },
-        { EAetheryteLocation.UldahGateOfTheSultana, "[Ul'dah] Gate of the Sultana (Western Thanalan)" },
-        { EAetheryteLocation.UldahGateOfNald, "[Ul'dah] Gate of Nald (Central Thanalan)" },
-        { EAetheryteLocation.UldahGateOfThal, "[Ul'dah] Gate of Thal (Central Thanalan)" },
-        { EAetheryteLocation.UldahAirship, "[Ul'dah] Airship Landing" },
-
-        { EAetheryteLocation.Limsa, "[Limsa Lominsa] Aetheryte Plaza" },
-        { EAetheryteLocation.LimsaArcanist, "[Limsa Lominsa] Arcanists' Guild" },
-        { EAetheryteLocation.LimsaFisher, "[Limsa Lominsa] Fishermens' Guild" },
-        { EAetheryteLocation.LimsaHawkersAlley, "[Limsa Lominsa] Hawkers' Alley" },
-        { EAetheryteLocation.LimsaAftcastle, "[Limsa Lominsa] The Aftcastle" },
-        { EAetheryteLocation.LimsaCulinarian, "[Limsa Lominsa] Culinarians' Guild" },
-        { EAetheryteLocation.LimsaMarauder, "[Limsa Lominsa] Marauders' Guild" },
-        { EAetheryteLocation.LimsaZephyrGate, "[Limsa Lominsa] Zephyr Gate (Middle La Noscea)" },
-        { EAetheryteLocation.LimsaTempestGate, "[Limsa Lominsa] Tempest Gate (Lower La Noscea)" },
-        { EAetheryteLocation.LimsaAirship, "[Limsa Lominsa] Airship Landing" },
-
-        { EAetheryteLocation.Ishgard, "[Ishgard] Aetheryte Plaza" },
-        { EAetheryteLocation.IshgardForgottenKnight, "[Ishgard] The Forgotten Knight" },
-        { EAetheryteLocation.IshgardSkysteelManufactory, "[Ishgard] Skysteel Manufactory" },
-        { EAetheryteLocation.IshgardBrume, "[Ishgard] The Brume" },
-        { EAetheryteLocation.IshgardAthenaeumAstrologicum, "[Ishgard] Athenaeum Astrologicum" },
-        { EAetheryteLocation.IshgardJeweledCrozier, "[Ishgard] The Jeweled Crozier" },
-        { EAetheryteLocation.IshgardSaintReymanaudsCathedral, "[Ishgard] Saint Reymanaud's Cathedral" },
-        { EAetheryteLocation.IshgardTribunal, "[Ishgard] The Tribunal" },
-        { EAetheryteLocation.IshgardLastVigil, "[Ishgard] The Last Vigil" },
-        { EAetheryteLocation.IshgardGatesOfJudgement, "[Ishgard] The Gates of Judgement (Coerthas Central Highlands)" },
-
-        { EAetheryteLocation.Idyllshire, "[Idyllshire] Aetheryte Plaza" },
-        { EAetheryteLocation.IdyllshireWest, "[Idyllshire] West Idyllshire" },
-        { EAetheryteLocation.IdyllshirePrologueGate, "[Idyllshire] Prologue Gate (Western Hinterlands)" },
-        { EAetheryteLocation.IdyllshireEpilogueGate, "[Idyllshire] Epilogue Gate (Eastern Hinterlands)" },
-
-        { EAetheryteLocation.RhalgrsReach, "[Rhalgr's Reach] Aetheryte Plaza" },
-        { EAetheryteLocation.RhalgrsReachWest, "[Rhalgr's Reach] Western Rhalgr's Reach" },
-        { EAetheryteLocation.RhalgrsReachNorthEast, "[Rhalgr's Reach] Northeastern Rhalgr's Reach" },
-        { EAetheryteLocation.RhalgrsReachFringesGate, "[Rhalgr's Reach] Fringes Gate" },
-        { EAetheryteLocation.RhalgrsReachPeaksGate, "[Rhalgr's Reach] Peaks Gate" },
-
-        { EAetheryteLocation.Kugane, "[Kugane] Aetheryte Plaza" },
-        { EAetheryteLocation.KuganeShiokazeHostelry, "[Kugane] Shiokaze Hostelry" },
-        { EAetheryteLocation.KuganePier1, "[Kugane] Pier #1" },
-        { EAetheryteLocation.KuganeThavnairianConsulate, "[Kugane] Thavnairian Consulate" },
-        { EAetheryteLocation.KuganeMarkets, "[Kugane] Kogane Dori Markets" },
-        { EAetheryteLocation.KuganeBokairoInn, "[Kugane] Bokairo Inn" },
-        { EAetheryteLocation.KuganeRubyBazaar, "[Kugane] The Ruby Bazaar" },
-        { EAetheryteLocation.KuganeSekiseigumiBarracks, "[Kugane] Sekiseigumi Barracks" },
-        { EAetheryteLocation.KuganeRakuzaDistrict, "[Kugane] Rakuza District" },
-        { EAetheryteLocation.KuganeRubyPrice, "[Kugane] The Ruby Price" },
-        { EAetheryteLocation.KuganeAirship, "[Kugane] Airship Landing" },
-
-        { EAetheryteLocation.Crystarium, "[Crystarium] Aetheryte Plaza" },
-        { EAetheryteLocation.CrystariumMarkets, "[Crystarium] Musica Universalis Markets" },
-        { EAetheryteLocation.CrystariumTemenosRookery, "[Crystarium] Temenos Rookery" },
-        { EAetheryteLocation.CrystariumDossalGate, "[Crystarium] The Dossal Gate" },
-        { EAetheryteLocation.CrystariumPendants, "[Crystarium] The Pendants" },
-        { EAetheryteLocation.CrystariumAmaroLaunch, "[Crystarium] The Amaro Launch" },
-        { EAetheryteLocation.CrystariumCrystallineMean, "[Crystarium] The Crystalline Mean" },
-        { EAetheryteLocation.CrystariumCabinetOfCuriosity, "[Crystarium] The Cabinet of Curiosity" },
-        { EAetheryteLocation.CrystariumTessellation, "[Crystarium] Tessellation (Lakeland)" },
-
-        { EAetheryteLocation.Eulmore, "[Eulmore] Aetheryte Plaza" },
-        { EAetheryteLocation.EulmoreSoutheastDerelict, "[Eulmore] Southeast Derelicts" },
-        { EAetheryteLocation.EulmoreNightsoilPots, "[Eulmore] Nightsoil Pots" },
-        { EAetheryteLocation.EulmoreGloryGate, "[Eulmore] The Glory Gate" },
-        { EAetheryteLocation.EulmoreMainstay, "[Eulmore] The Mainstay" },
-        { EAetheryteLocation.EulmorePathToGlory, "[Eulmore] The Path to Glory (Kholusia)" },
-
-        { EAetheryteLocation.OldSharlayan, "[Old Sharlayan] Aetheryte Plaza" },
-        { EAetheryteLocation.OldSharlayanStudium, "[Old Sharlayan] The Studium" },
-        { EAetheryteLocation.OldSharlayanBaldesionAnnex, "[Old Sharlayan] The Baldesion Annex" },
-        { EAetheryteLocation.OldSharlayanRostra, "[Old Sharlayan] The Rostra" },
-        { EAetheryteLocation.OldSharlayanLeveilleurEstate, "[Old Sharlayan] The Leveilleur Estate" },
-        { EAetheryteLocation.OldSharlayanJourneysEnd, "[Old Sharlayan] Journey's End" },
-        { EAetheryteLocation.OldSharlayanScholarsHarbor, "[Old Sharlayan] Scholar's Harbor" },
-        { EAetheryteLocation.OldSharlayanHallOfArtifice, "[Old Sharlayan] The Hall of Artifice (Labyrinthos)" },
-
-        { EAetheryteLocation.RadzAtHan, "[Radz-at-Han] Aetheryte Plaza" },
-        { EAetheryteLocation.RadzAtHanMeghaduta, "[Radz-at-Han] Meghaduta" },
-        { EAetheryteLocation.RadzAtHanRuveydahFibers, "[Radz-at-Han] Ruveydah Fibers" },
-        { EAetheryteLocation.RadzAtHanAirship, "[Radz-at-Han] Airship Landing" },
-        { EAetheryteLocation.RadzAtHanAlzadaalsPeace, "[Radz-at-Han] Alzadaal's Peace" },
-        { EAetheryteLocation.RadzAtHanHallOfTheRadiantHost, "[Radz-at-Han] Hall of the Radiant Host" },
-        { EAetheryteLocation.RadzAtHanMehrydesMeyhane, "[Radz-at-Han] Mehryde's Meyhane" },
-        { EAetheryteLocation.RadzAtHanKama, "[Radz-at-Han] Kama" },
-        { EAetheryteLocation.RadzAtHanHighCrucible, "[Radz-at-Han] The High Crucible of Al-Kimiya" },
-        { EAetheryteLocation.RadzAtHanGateOfFirstSight, "[Radz-at-Han] The Gate of First Sight (Thavnair)" },
-
-        { EAetheryteLocation.Tuliyollal, "[Tuliyollal] Aetheryte Plaza" },
-        { EAetheryteLocation.TuliyollalDirigibleLanding, "[Tuliyollal] Dirigible Landing" },
-        { EAetheryteLocation.TuliyollalTheResplendentQuarter, "[Tuliyollal] The Resplendent Quarter" },
-        { EAetheryteLocation.TuliyollalTheForardCabins, "[Tuliyollal] The For'ard Cabins" },
-        { EAetheryteLocation.TuliyollalBaysideBevyMarketplace, "[Tuliyollal] Bayside Bevy Marketplace" },
-        { EAetheryteLocation.TuliyollalVollokShoonsa, "[Tuliyollal] Vollok Shoonsa" },
-        { EAetheryteLocation.TuliyollalWachumeqimeqi, "[Tuliyollal] Wachumeqimeqi" },
-        { EAetheryteLocation.TuliyollalBrightploomPost, "[Tuliyollal] Brightploom Post" },
-        { EAetheryteLocation.TuliyollalArchOfTheDawnUrqopacha, "[Tuliyollal] Arch of the Dawn (Urqopacha)" },
-        { EAetheryteLocation.TuliyollalArchOfTheDawnKozamauka, "[Tuliyollal] Arch of the Dawn (Kozama'uka)" },
-        { EAetheryteLocation.TuliyollalIhuykatumu, "[Tuliyollal] Ihuykatumu (Kozama'uka)" },
-        { EAetheryteLocation.TuliyollalDirigibleLandingYakTel, "[Tuliyollal] Dirigible Landing (Yak T'el)" },
-        { EAetheryteLocation.TuliyollalXakTuralSkygate, "[Tuliyollal] Xak Tural Skygate (Shaaloani)" },
-
-        { EAetheryteLocation.SolutionNine, "[Solution Nine] Aetheryte Plaza" },
-        { EAetheryteLocation.SolutionNineInformationCenter, "[Solution Nine] Information Center" },
-        { EAetheryteLocation.SolutionNineTrueVue, "[Solution Nine] True Vue" },
-        { EAetheryteLocation.SolutionNineNeonStein, "[Solution Nine] Neon Stein" },
-        { EAetheryteLocation.SolutionNineTheArcadion, "[Solution Nine] The Arcadion" },
-        { EAetheryteLocation.SolutionNineResolution, "[Solution Nine] Resolution" },
-        { EAetheryteLocation.SolutionNineNexusArcade, "[Solution Nine] Nexus Arcade" },
-        { EAetheryteLocation.SolutionNineResidentialSector, "[Solution Nine] Residential Sector" },
-        { EAetheryteLocation.SolutionNineScanningPortNine, "[Solution Nine] Scanning Port Nine (Heritage Found)" },
-    };
-}
diff --git a/Questionable.Model/V1/Converter/AethernetShortcutConverter.cs b/Questionable.Model/V1/Converter/AethernetShortcutConverter.cs
deleted file mode 100644 (file)
index 8b90769..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace Questionable.Model.V1.Converter;
-
-public sealed class AethernetShortcutConverter : JsonConverter<AethernetShortcut>
-{
-    private static readonly Dictionary<EAetheryteLocation, string> EnumToString = AethernetShardConverter.Values;
-    private static readonly Dictionary<string, EAetheryteLocation> StringToEnum =
-        EnumToString.ToDictionary(x => x.Value, x => x.Key);
-
-    public override AethernetShortcut Read(ref Utf8JsonReader reader, Type typeToConvert,
-        JsonSerializerOptions options)
-    {
-        if (reader.TokenType != JsonTokenType.StartArray)
-            throw new JsonException();
-
-        if (!reader.Read() || reader.TokenType != JsonTokenType.String)
-            throw new JsonException();
-
-        string from = reader.GetString() ?? throw new JsonException();
-
-        if (!reader.Read() || reader.TokenType != JsonTokenType.String)
-            throw new JsonException();
-
-        string to = reader.GetString() ?? throw new JsonException();
-
-        if (!reader.Read() || reader.TokenType != JsonTokenType.EndArray)
-            throw new JsonException();
-
-        return new AethernetShortcut
-        {
-            From = StringToEnum.TryGetValue(from, out var fromEnum) ? fromEnum : throw new JsonException(),
-            To = StringToEnum.TryGetValue(to, out var toEnum) ? toEnum : throw new JsonException()
-        };
-    }
-
-    public override void Write(Utf8JsonWriter writer, AethernetShortcut value, JsonSerializerOptions options)
-    {
-        writer.WriteStartArray();
-        writer.WriteStringValue(EnumToString[value.From]);
-        writer.WriteStringValue(EnumToString[value.To]);
-        writer.WriteEndArray();
-    }
-}
diff --git a/Questionable.Model/V1/Converter/AetheryteConverter.cs b/Questionable.Model/V1/Converter/AetheryteConverter.cs
deleted file mode 100644 (file)
index ab0dd54..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-using System.Collections.Generic;
-
-namespace Questionable.Model.V1.Converter;
-
-public sealed class AetheryteConverter() : EnumConverter<EAetheryteLocation>(Values)
-{
-    private static readonly Dictionary<EAetheryteLocation, string> Values = new()
-    {
-        { EAetheryteLocation.Gridania, "Gridania" },
-        { EAetheryteLocation.CentralShroudBentbranchMeadows, "Central Shroud - Bentbranch Meadows" },
-        { EAetheryteLocation.EastShroudHawthorneHut, "East Shroud - Hawthorne Hut" },
-        { EAetheryteLocation.SouthShroudQuarrymill, "South Shroud - Quarrymill" },
-        { EAetheryteLocation.SouthShroudCampTranquil, "South Shroud - Camp Tranquil" },
-        { EAetheryteLocation.NorthShroudFallgourdFloat, "North Shroud - Fallgourd Float" },
-
-        { EAetheryteLocation.Uldah, "Ul'dah" },
-        { EAetheryteLocation.WesternThanalanHorizon, "Western Thanalan - Horizon" },
-        { EAetheryteLocation.CentralThanalanBlackBrushStation, "Central Thanalan - Black Brush Station" },
-        { EAetheryteLocation.EasternThanalanCampDrybone, "Eastern Thanalan - Camp Drybone" },
-        { EAetheryteLocation.SouthernThanalanLittleAlaMhigo, "Southern Thanalan - Little Ala Mhigo" },
-        { EAetheryteLocation.SouthernThanalanForgottenSprings, "Southern Thanalan - Forgotten Springs" },
-        { EAetheryteLocation.NorthernThanalanCampBluefog, "Northern Thanalan - Camp Bluefog" },
-        { EAetheryteLocation.NorthernThanalanCeruleumProcessingPlant, "Northern Thanalan - Ceruleum Processing Plant" },
-
-        { EAetheryteLocation.Limsa, "Limsa Lominsa" },
-        { EAetheryteLocation.MiddleLaNosceaSummerfordFarms, "Middle La Noscea - Summerford Farms" },
-        { EAetheryteLocation.LowerLaNosceaMorabyDrydocks, "Lower La Noscea - Moraby Drydocks" },
-        { EAetheryteLocation.EasternLaNosceaCostaDelSol, "Eastern La Noscea - Costa Del Sol" },
-        { EAetheryteLocation.EasternLaNosceaWineport, "Eastern La Noscea - Wineport" },
-        { EAetheryteLocation.WesternLaNosceaSwiftperch, "Western La Noscea - Swiftperch" },
-        { EAetheryteLocation.WesternLaNosceaAleport, "Western La Noscea - Aleport" },
-        { EAetheryteLocation.UpperLaNosceaCampBronzeLake, "Upper La Noscea - Camp Bronze Lake" },
-        { EAetheryteLocation.OuterLaNosceaCampOverlook, "Outer La Noscea - Camp Overlook" },
-
-        { EAetheryteLocation.CoerthasCentralHighlandsCampDragonhead, "Coerthas Central Highlands - Camp Dragonhead" },
-        { EAetheryteLocation.MorDhona, "Mor Dhona" },
-        { EAetheryteLocation.GoldSaucer, "Gold Saucer" },
-        { EAetheryteLocation.WolvesDenPier, "Wolves' Den Pier" },
-
-        { EAetheryteLocation.Ishgard, "Ishgard" },
-        { EAetheryteLocation.Idyllshire, "Idyllshire" },
-        { EAetheryteLocation.CoerthasWesternHighlandsFalconsNest, "Coerthas Western Highlands - Falcon's Nest" },
-        { EAetheryteLocation.SeaOfCloudsCampCloudtop, "The Sea of Clouds - Camp Cloudtop" },
-        { EAetheryteLocation.SeaOfCloudsOkZundu, "The Sea of Clouds - Ok' Zundu" },
-        { EAetheryteLocation.AzysLlaHelix, "Azys Lla - Helix" },
-        { EAetheryteLocation.DravanianForelandsTailfeather, "The Dravanian Forelands - Tailfeather" },
-        { EAetheryteLocation.DravanianForelandsAnyxTrine, "The Dravanian Forelands - Anyx Trine" },
-        { EAetheryteLocation.ChurningMistsMoghome, "The Churning Mists - Moghome" },
-        { EAetheryteLocation.ChurningMistsZenith, "The Churning Mists - Zenith" },
-
-        { EAetheryteLocation.RhalgrsReach, "Rhalgr's Reach" },
-        { EAetheryteLocation.FringesCastrumOriens, "Fringes - Castrum Oriens" },
-        { EAetheryteLocation.FringesPeeringStones, "Fringes - Peering Stones" },
-        { EAetheryteLocation.PeaksAlaGannha, "Peaks - Ala Gannha" },
-        { EAetheryteLocation.PeaksAlaGhiri, "Peaks - Ala Ghiri" },
-        { EAetheryteLocation.LochsPortaPraetoria, "Lochs - Porta Praetoria" },
-        { EAetheryteLocation.LochsAlaMhiganQuarter, "Lochs - Ala Mhigan Quarter" },
-        { EAetheryteLocation.Kugane, "Kugane" },
-        { EAetheryteLocation.RubySeaTamamizu, "Ruby Sea - Tamamizu" },
-        { EAetheryteLocation.RubySeaOnokoro, "Ruby Sea - Onokoro" },
-        { EAetheryteLocation.YanxiaNamai, "Yanxia - Namai" },
-        { EAetheryteLocation.YanxiaHouseOfTheFierce, "Yanxia - House of the Fierce" },
-        { EAetheryteLocation.AzimSteppeReunion, "Azim Steppe - Reunion" },
-        { EAetheryteLocation.AzimSteppeDawnThrone, "Azim Steppe - Dawn Throne" },
-        { EAetheryteLocation.AzimSteppeDhoroIloh, "Azim Steppe - Dhoro Iloh" },
-        { EAetheryteLocation.DomanEnclave, "Doman Enclave" },
-
-        { EAetheryteLocation.Crystarium, "Crystarium" },
-        { EAetheryteLocation.Eulmore, "Eulmore" },
-        { EAetheryteLocation.LakelandFortJobb, "Lakeland - Fort Jobb" },
-        { EAetheryteLocation.LakelandOstallImperative, "Lakeland - Ostall Imperative" },
-        { EAetheryteLocation.KholusiaStilltide, "Kholusia - Stilltide" },
-        { EAetheryteLocation.KholusiaWright, "Kholusia - Wright" },
-        { EAetheryteLocation.KholusiaTomra, "Kholusia - Tomra" },
-        { EAetheryteLocation.AmhAraengMordSouq, "Amh Araeng - Mord Souq" },
-        { EAetheryteLocation.AmhAraengInnAtJourneysHead, "Amh Araeng - Inn at Journey's Head" },
-        { EAetheryteLocation.AmhAraengTwine, "Amh Araeng - Twine" },
-        { EAetheryteLocation.RaktikaSlitherbough, "Rak'tika - Slitherbough" },
-        { EAetheryteLocation.RaktikaFanow, "Rak'tika - Fanow" },
-        { EAetheryteLocation.IlMhegLydhaLran, "Il Mheg - Lydha Lran" },
-        { EAetheryteLocation.IlMhegPiaEnni, "Il Mheg - Pia Enni" },
-        { EAetheryteLocation.IlMhegWolekdorf, "Il Mheg - Wolekdorf" },
-        { EAetheryteLocation.TempestOndoCups, "Tempest - Ondo Cups" },
-        { EAetheryteLocation.TempestMacarensesAngle, "Tempest - Macarenses Angle" },
-
-        { EAetheryteLocation.OldSharlayan, "Old Sharlayan" },
-        { EAetheryteLocation.RadzAtHan, "Radz-at-Han" },
-        { EAetheryteLocation.LabyrinthosArcheion, "Labyrinthos - Archeion" },
-        { EAetheryteLocation.LabyrinthosSharlayanHamlet, "Labyrinthos - Sharlayan Hamlet" },
-        { EAetheryteLocation.LabyrinthosAporia, "Labyrinthos - Aporia" },
-        { EAetheryteLocation.ThavnairYedlihmad, "Thavnair - Yedlihmad" },
-        { EAetheryteLocation.ThavnairGreatWork, "Thavnair - Great Work" },
-        { EAetheryteLocation.ThavnairPalakasStand, "Thavnair - Palaka's Stand" },
-        { EAetheryteLocation.GarlemaldCampBrokenGlass, "Garlemald - Camp Broken Glass" },
-        { EAetheryteLocation.GarlemaldTertium, "Garlemald - Tertium" },
-        { EAetheryteLocation.MareLamentorumSinusLacrimarum, "Mare Lamentorum - Sinus Lacrimarum" },
-        { EAetheryteLocation.MareLamentorumBestwaysBurrow, "Mare Lamentorum - Bestways Burrow" },
-        { EAetheryteLocation.ElpisAnagnorisis, "Elpis - Anagnorisis" },
-        { EAetheryteLocation.ElpisTwelveWonders, "Elpis - Twelve Wonders" },
-        { EAetheryteLocation.ElpisPoietenOikos, "Elpis - Poieten Oikos" },
-        { EAetheryteLocation.UltimaThuleReahTahra, "Ultima Thule - Reah Tahra" },
-        { EAetheryteLocation.UltimaThuleAbodeOfTheEa, "Ultima Thule - Abode of the Ea" },
-        { EAetheryteLocation.UltimaThuleBaseOmicron, "Ultima Thule - Base Omicron" },
-
-        { EAetheryteLocation.Tuliyollal, "Tuliyollal" },
-        { EAetheryteLocation.SolutionNine, "Solution Nine" },
-        { EAetheryteLocation.UrqopachaWachunpelo, "Urqopacha - Wachunpelo" },
-        { EAetheryteLocation.UrqopachaWorlarsEcho, "Urqopacha - Worlar's Echo" },
-        { EAetheryteLocation.KozamaukaOkHanu, "Kozama'uka - Ok'hanu" },
-        { EAetheryteLocation.KozamaukaManyFires, "Kozama'uka - Many Fires" },
-        { EAetheryteLocation.KozamaukaEarthenshire, "Kozama'uka - Earthenshire" },
-        { EAetheryteLocation.YakTelIqBraax, "Yak T'el - Iq Br'aax" },
-        { EAetheryteLocation.YakTelMamook, "Yak T'el - Mamook" },
-        { EAetheryteLocation.ShaaloaniHhusatahwi, "Shaaloani - Hhusatahwi" },
-        { EAetheryteLocation.ShaaloaniShesheneweziSprings, "Shaaloani - Sheshenewezi Springs" },
-        { EAetheryteLocation.ShaaloaniMehwahhetsoan, "Shaaloani - Mehwahhetsoan" },
-        { EAetheryteLocation.HeritageFoundYyasulaniStation, "Heritage Found - Yyasulani Station" },
-        { EAetheryteLocation.HeritageFoundTheOutskirts, "Heritage Found - The Outskirts" },
-        { EAetheryteLocation.HeritageFoundElectropeStrike, "Heritage Found - Electrope Strike" },
-        { EAetheryteLocation.LivingMemoryLeynodeMnemo, "Living Memory - Leynode Mnemo" },
-        { EAetheryteLocation.LivingMemoryLeynodePyro, "Living Memory - Leynode Pyro" },
-        { EAetheryteLocation.LivingMemoryLeynodeAero, "Living Memory - Leynode Aero" },
-    };
-
-    public static bool IsLargeAetheryte(EAetheryteLocation aetheryte) => Values.ContainsKey(aetheryte);
-}
diff --git a/Questionable.Model/V1/Converter/DialogueChoiceTypeConverter.cs b/Questionable.Model/V1/Converter/DialogueChoiceTypeConverter.cs
deleted file mode 100644 (file)
index ea832ca..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-using System.Collections.Generic;
-
-namespace Questionable.Model.V1.Converter;
-
-public sealed class DialogueChoiceTypeConverter() : EnumConverter<EDialogChoiceType>(Values)
-{
-    private static readonly Dictionary<EDialogChoiceType, string> Values = new()
-    {
-        { EDialogChoiceType.YesNo, "YesNo" },
-        { EDialogChoiceType.List, "List" },
-    };
-}
diff --git a/Questionable.Model/V1/Converter/EmoteConverter.cs b/Questionable.Model/V1/Converter/EmoteConverter.cs
deleted file mode 100644 (file)
index 8a32751..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-using System.Collections.Generic;
-
-namespace Questionable.Model.V1.Converter;
-
-public sealed class EmoteConverter() : EnumConverter<EEmote>(Values)
-{
-    private static readonly Dictionary<EEmote, string> Values = new()
-    {
-        { EEmote.Stretch, "stretch" },
-        { EEmote.Wave, "wave" },
-        { EEmote.Rally, "rally" },
-        { EEmote.Deny, "deny" },
-        { EEmote.Pray, "pray" },
-        { EEmote.Slap, "slap" },
-        { EEmote.Doubt, "doubt" },
-        { EEmote.Psych, "psych" },
-        { EEmote.Cheer, "cheer" },
-        { EEmote.Happy, "happy" },
-        { EEmote.Poke, "poke" },
-        { EEmote.Flex, "flex" },
-        { EEmote.Soothe, "soothe" },
-        { EEmote.Me, "me" },
-        { EEmote.Welcome, "welcome" },
-        { EEmote.ImperialSalute, "imperialsalute" },
-        { EEmote.Pet, "pet" },
-        { EEmote.Dance, "dance" },
-        { EEmote.Respect, "respect" },
-        { EEmote.Lookout, "lookout" },
-        { EEmote.Kneel, "kneel" },
-        { EEmote.Bow, "bow" },
-    };
-}
diff --git a/Questionable.Model/V1/Converter/EnemySpawnTypeConverter.cs b/Questionable.Model/V1/Converter/EnemySpawnTypeConverter.cs
deleted file mode 100644 (file)
index 5c5de53..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-using System.Collections.Generic;
-
-namespace Questionable.Model.V1.Converter;
-
-public sealed class EnemySpawnTypeConverter() : EnumConverter<EEnemySpawnType>(Values)
-{
-    private static readonly Dictionary<EEnemySpawnType, string> Values = new()
-    {
-        { EEnemySpawnType.AfterInteraction, "AfterInteraction" },
-        { EEnemySpawnType.AfterItemUse, "AfterItemUse" },
-        { EEnemySpawnType.AutoOnEnterArea, "AutoOnEnterArea" },
-        { EEnemySpawnType.OverworldEnemies, "OverworldEnemies" },
-    };
-}
diff --git a/Questionable.Model/V1/Converter/EnumConverter.cs b/Questionable.Model/V1/Converter/EnumConverter.cs
deleted file mode 100644 (file)
index 3ed1267..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.Linq;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace Questionable.Model.V1.Converter;
-
-public abstract class EnumConverter<T> : JsonConverter<T>
-    where T : Enum
-{
-    private readonly ReadOnlyDictionary<T, string> _enumToString;
-    private readonly ReadOnlyDictionary<string, T> _stringToEnum;
-
-    protected EnumConverter(IReadOnlyDictionary<T, string> values)
-    {
-        _enumToString = values is IDictionary<T, string> dict
-            ? new ReadOnlyDictionary<T, string>(dict)
-            : new ReadOnlyDictionary<T, string>(values.ToDictionary(x => x.Key, x => x.Value));
-        _stringToEnum = new ReadOnlyDictionary<string, T>(_enumToString.ToDictionary(x => x.Value, x => x.Key));
-    }
-
-    public override T? Read(ref Utf8JsonReader reader, Type typeToConvert,
-        JsonSerializerOptions options)
-    {
-        if (reader.TokenType != JsonTokenType.String)
-            throw new JsonException();
-
-        string? str = reader.GetString();
-        if (str == null)
-            throw new JsonException();
-
-        return _stringToEnum.TryGetValue(str, out T? value) ? value : throw new JsonException();
-    }
-
-    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
-    {
-        writer.WriteStringValue(_enumToString[value]);
-    }
-}
diff --git a/Questionable.Model/V1/Converter/ExcelRefConverter.cs b/Questionable.Model/V1/Converter/ExcelRefConverter.cs
deleted file mode 100644 (file)
index 06ba3ff..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-using System;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace Questionable.Model.V1.Converter;
-
-public sealed class ExcelRefConverter : JsonConverter<ExcelRef>
-{
-    public override ExcelRef? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-    {
-        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)
-    {
-        if (value == null)
-            writer.WriteNullValue();
-        else if (value.Type == ExcelRef.EType.Key)
-            writer.WriteStringValue(value.AsKey());
-        else if (value.Type == ExcelRef.EType.RowId)
-            writer.WriteNumberValue(value.AsRowId());
-        else
-            throw new JsonException();
-    }
-}
diff --git a/Questionable.Model/V1/Converter/InteractionTypeConverter.cs b/Questionable.Model/V1/Converter/InteractionTypeConverter.cs
deleted file mode 100644 (file)
index 08be6ff..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-using System.Collections.Generic;
-
-namespace Questionable.Model.V1.Converter;
-
-public sealed class InteractionTypeConverter() : EnumConverter<EInteractionType>(Values)
-{
-    private static readonly Dictionary<EInteractionType, string> Values = new()
-    {
-        { EInteractionType.Interact, "Interact" },
-        { EInteractionType.WalkTo, "WalkTo" },
-        { EInteractionType.AttuneAethernetShard, "AttuneAethernetShard" },
-        { EInteractionType.AttuneAetheryte, "AttuneAetheryte" },
-        { EInteractionType.AttuneAetherCurrent, "AttuneAetherCurrent" },
-        { EInteractionType.Combat, "Combat" },
-        { EInteractionType.UseItem, "UseItem" },
-        { EInteractionType.EquipItem, "EquipItem" },
-        { EInteractionType.Say, "Say" },
-        { EInteractionType.Emote, "Emote" },
-        { EInteractionType.Action, "Action" },
-        { EInteractionType.WaitForObjectAtPosition, "WaitForNpcAtPosition" },
-        { EInteractionType.WaitForManualProgress, "WaitForManualProgress" },
-        { EInteractionType.Duty, "Duty" },
-        { EInteractionType.SinglePlayerDuty, "SinglePlayerDuty" },
-        { EInteractionType.Jump, "Jump" },
-        { EInteractionType.Dive, "Dive" },
-        { EInteractionType.Instruction, "Instruction" },
-        { EInteractionType.AcceptQuest, "AcceptQuest" },
-        { EInteractionType.CompleteQuest, "CompleteQuest" },
-    };
-}
diff --git a/Questionable.Model/V1/Converter/JumpTypeConverter.cs b/Questionable.Model/V1/Converter/JumpTypeConverter.cs
deleted file mode 100644 (file)
index 94c8ea2..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-using System.Collections.Generic;
-
-namespace Questionable.Model.V1.Converter;
-
-public sealed class JumpTypeConverter() : EnumConverter<EJumpType>(Values)
-{
-    private static readonly Dictionary<EJumpType, string> Values = new()
-    {
-        { EJumpType.SingleJump, "SingleJump" },
-        { EJumpType.RepeatedJumps, "RepeatedJumps" },
-    };
-}
diff --git a/Questionable.Model/V1/Converter/LockedSkipConditionConverter.cs b/Questionable.Model/V1/Converter/LockedSkipConditionConverter.cs
deleted file mode 100644 (file)
index 5d53058..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-using System.Collections.Generic;
-
-namespace Questionable.Model.V1.Converter;
-
-public sealed class LockedSkipConditionConverter() : EnumConverter<ELockedSkipCondition>(Values)
-{
-    private static readonly Dictionary<ELockedSkipCondition, string> Values = new()
-    {
-        { ELockedSkipCondition.Locked, "Locked" },
-        { ELockedSkipCondition.Unlocked, "Unlocked" },
-    };
-}
diff --git a/Questionable.Model/V1/Converter/QuestWorkConfigConverter.cs b/Questionable.Model/V1/Converter/QuestWorkConfigConverter.cs
deleted file mode 100644 (file)
index 2b37693..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-using System;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace Questionable.Model.V1.Converter;
-
-public sealed class QuestWorkConfigConverter : JsonConverter<QuestWorkValue>
-{
-    public override QuestWorkValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-    {
-        if (reader.TokenType == JsonTokenType.Number)
-            return new QuestWorkValue(reader.GetByte());
-
-        if (reader.TokenType != JsonTokenType.StartObject)
-            throw new JsonException();
-
-        byte? high = null, low = null;
-        EQuestWorkMode mode = EQuestWorkMode.Bitwise;
-        while (reader.Read())
-        {
-            switch (reader.TokenType)
-            {
-                case JsonTokenType.PropertyName:
-                    string? propertyName = reader.GetString();
-                    if (propertyName == null || !reader.Read())
-                        throw new JsonException();
-
-                    switch (propertyName)
-                    {
-                        case nameof(QuestWorkValue.High):
-                            high = reader.GetByte();
-                            break;
-
-                        case nameof(QuestWorkValue.Low):
-                            low = reader.GetByte();
-                            break;
-
-                        case nameof(QuestWorkValue.Mode):
-                            mode = new QuestWorkModeConverter().Read(ref reader, typeof(EQuestWorkMode), options);
-                            break;
-
-                        default:
-                            throw new JsonException();
-                    }
-
-                    break;
-
-                case JsonTokenType.EndObject:
-                    return new QuestWorkValue(high, low, mode);
-
-                default:
-                    throw new JsonException();
-            }
-        }
-
-        throw new JsonException();
-    }
-
-    public override void Write(Utf8JsonWriter writer, QuestWorkValue value, JsonSerializerOptions options)
-    {
-        throw new NotImplementedException();
-    }
-}
diff --git a/Questionable.Model/V1/Converter/QuestWorkModeConverter.cs b/Questionable.Model/V1/Converter/QuestWorkModeConverter.cs
deleted file mode 100644 (file)
index a3697d6..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-using System.Collections.Generic;
-
-namespace Questionable.Model.V1.Converter;
-
-public sealed class QuestWorkModeConverter() : EnumConverter<EQuestWorkMode>(Values)
-{
-    private static readonly Dictionary<EQuestWorkMode, string> Values = new()
-    {
-        { EQuestWorkMode.Bitwise, "Bitwise" },
-        { EQuestWorkMode.Exact, "Exact" },
-    };
-}
diff --git a/Questionable.Model/V1/Converter/SkipConditionConverter.cs b/Questionable.Model/V1/Converter/SkipConditionConverter.cs
deleted file mode 100644 (file)
index f4b4e6f..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-using System.Collections.Generic;
-
-namespace Questionable.Model.V1.Converter;
-
-public sealed class SkipConditionConverter() : EnumConverter<EExtraSkipCondition>(Values)
-{
-    private static readonly Dictionary<EExtraSkipCondition, string> Values = new()
-    {
-        { EExtraSkipCondition.WakingSandsMainArea, "WakingSandsMainArea" },
-    };
-}
diff --git a/Questionable.Model/V1/Converter/StringListOrValueConverter.cs b/Questionable.Model/V1/Converter/StringListOrValueConverter.cs
deleted file mode 100644 (file)
index b6da81a..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace Questionable.Model.V1.Converter;
-
-public sealed class StringListOrValueConverter : JsonConverter<List<string>>
-{
-    public override List<string> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-    {
-        if (reader.TokenType == JsonTokenType.String)
-            return [reader.GetString()!];
-
-        if (reader.TokenType != JsonTokenType.StartArray)
-            throw new JsonException();
-        reader.Read();
-
-        List<string> value = [];
-        while (reader.TokenType != JsonTokenType.EndArray)
-        {
-            value.Add(reader.GetString()!);
-            reader.Read();
-        }
-
-        return value;
-    }
-
-    public override void Write(Utf8JsonWriter writer, List<string>? value, JsonSerializerOptions options)
-    {
-        if (value == null)
-            writer.WriteNullValue();
-        else if (value.Count == 1)
-            writer.WriteStringValue(value[0]);
-        else
-        {
-            writer.WriteStartArray();
-            foreach (var v in value)
-                writer.WriteStringValue(v);
-            writer.WriteEndArray();
-        }
-    }
-}
diff --git a/Questionable.Model/V1/Converter/VectorConverter.cs b/Questionable.Model/V1/Converter/VectorConverter.cs
deleted file mode 100644 (file)
index f75ffe6..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-using System;
-using System.Numerics;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace Questionable.Model.V1.Converter;
-
-public sealed class VectorConverter : JsonConverter<Vector3>
-{
-    public override Vector3 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-    {
-        if (reader.TokenType != JsonTokenType.StartObject)
-            throw new JsonException();
-
-        Vector3 vec = new Vector3();
-        while (reader.Read())
-        {
-            switch (reader.TokenType)
-            {
-                case JsonTokenType.PropertyName:
-                    string? propertyName = reader.GetString();
-                    if (propertyName == null || !reader.Read())
-                        throw new JsonException();
-
-                    switch (propertyName)
-                    {
-                        case nameof(Vector3.X):
-                            vec.X = reader.GetSingle();
-                            break;
-
-                        case nameof(Vector3.Y):
-                            vec.Y = reader.GetSingle();
-                            break;
-
-                        case nameof(Vector3.Z):
-                            vec.Z = reader.GetSingle();
-                            break;
-
-                        default:
-                            throw new JsonException();
-                    }
-
-                    break;
-
-                case JsonTokenType.EndObject:
-                    return vec;
-
-                default:
-                    throw new JsonException();
-            }
-        }
-
-        throw new JsonException();
-    }
-
-    public override void Write(Utf8JsonWriter writer, Vector3 value, JsonSerializerOptions options)
-    {
-        writer.WriteStartObject();
-        writer.WriteNumber(nameof(Vector3.X), value.X);
-        writer.WriteNumber(nameof(Vector3.Y), value.X);
-        writer.WriteNumber(nameof(Vector3.Z), value.X);
-        writer.WriteEndObject();
-    }
-}
diff --git a/Questionable.Model/V1/DialogueChoice.cs b/Questionable.Model/V1/DialogueChoice.cs
deleted file mode 100644 (file)
index 80b49d0..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-public sealed class DialogueChoice
-{
-    [JsonConverter(typeof(DialogueChoiceTypeConverter))]
-    public EDialogChoiceType Type { get; set; }
-    public string? ExcelSheet { get; set; }
-
-    [JsonConverter(typeof(ExcelRefConverter))]
-    public ExcelRef? Prompt { get; set; }
-
-    public bool Yes { get; set; } = true;
-
-    [JsonConverter(typeof(ExcelRefConverter))]
-    public ExcelRef? Answer { get; set; }
-
-    /// <summary>
-    /// If set, only applies when focusing the given target id.
-    /// </summary>
-    public uint? DataId { get; set; }
-}
diff --git a/Questionable.Model/V1/EAction.cs b/Questionable.Model/V1/EAction.cs
deleted file mode 100644 (file)
index 8aca752..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-[JsonConverter(typeof(ActionConverter))]
-public enum EAction
-{
-    Cure = 120,
-    Esuna = 7568,
-    Physick = 190,
-    Buffet = 4931,
-    Fumigate = 5872,
-    SiphonSnout = 18187,
-    RedGulal = 29382,
-    YellowGulal = 29383,
-    BlueGulal = 29384,
-}
-
-public static class EActionExtensions
-{
-    public static bool RequiresMount(this EAction action)
-    {
-        return action
-            is EAction.Buffet
-            or EAction.Fumigate
-            or EAction.SiphonSnout
-            or EAction.RedGulal
-            or EAction.YellowGulal
-            or EAction.BlueGulal;
-    }
-}
diff --git a/Questionable.Model/V1/EAetheryteLocation.cs b/Questionable.Model/V1/EAetheryteLocation.cs
deleted file mode 100644 (file)
index 1ddde30..0000000
+++ /dev/null
@@ -1,251 +0,0 @@
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-[JsonConverter(typeof(AetheryteConverter))]
-public enum EAetheryteLocation
-{
-    None = 0,
-
-    Gridania = 2,
-    GridaniaArcher = 25,
-    GridaniaLeatherworker = 26,
-    GridaniaLancer = 27,
-    GridaniaConjurer = 28,
-    GridaniaBotanist = 29,
-    GridaniaAmphitheatre = 30,
-    GridaniaBlueBadgerGate = 31,
-    GridaniaYellowSerpentGate = 32,
-    GridaniaWhiteWolfGate = 54,
-    GridaniaAirship = 94,
-
-    CentralShroudBentbranchMeadows = 3,
-    EastShroudHawthorneHut = 4,
-    SouthShroudQuarrymill = 5,
-    SouthShroudCampTranquil = 6,
-    NorthShroudFallgourdFloat = 7,
-
-    Uldah = 9,
-    UldahAdventurers = 33,
-    UldahThaumaturge = 34,
-    UldahGladiator = 35,
-    UldahMiner = 36,
-    UldahAlchemist = 37,
-    UldahWeaver = 47,
-    UldahGoldsmith = 50,
-    UldahChamberOfRule = 51,
-    UldahAirship = 95,
-    UldahGateOfTheSultana = 38,
-    UldahGateOfNald = 39,
-    UldahGateOfThal = 40,
-    UldahSapphireAvenue = 125,
-
-    WesternThanalanHorizon = 17,
-    EasternThanalanCampDrybone = 18,
-    SouthernThanalanLittleAlaMhigo = 19,
-    SouthernThanalanForgottenSprings = 20,
-    NorthernThanalanCampBluefog = 21,
-    NorthernThanalanCeruleumProcessingPlant = 22,
-    CentralThanalanBlackBrushStation = 53,
-
-    Limsa = 8,
-    LimsaAftcastle = 41,
-    LimsaCulinarian = 42,
-    LimsaArcanist = 43,
-    LimsaFisher = 44,
-    LimsaMarauder = 48,
-    LimsaHawkersAlley = 49,
-    LimsaZephyrGate = 45,
-    LimsaTempestGate = 46,
-    LimsaAirship = 93,
-
-    LowerLaNosceaMorabyDrydocks = 10,
-    EasternLaNosceaCostaDelSol = 11,
-    EasternLaNosceaWineport = 12,
-    WesternLaNosceaSwiftperch = 13,
-    WesternLaNosceaAleport = 14,
-    UpperLaNosceaCampBronzeLake = 15,
-    OuterLaNosceaCampOverlook = 16,
-    MiddleLaNosceaSummerfordFarms = 52,
-
-    CoerthasCentralHighlandsCampDragonhead = 23,
-    MorDhona = 24,
-    GoldSaucer = 62,
-    WolvesDenPier = 55,
-
-    Ishgard = 70,
-    IshgardForgottenKnight = 80,
-    IshgardSkysteelManufactory = 81,
-    IshgardBrume = 82,
-    IshgardAthenaeumAstrologicum = 83,
-    IshgardJeweledCrozier = 84,
-    IshgardSaintReymanaudsCathedral = 85,
-    IshgardTribunal = 86,
-    IshgardLastVigil = 87,
-    IshgardGatesOfJudgement = 88,
-
-    Idyllshire = 75,
-    IdyllshireWest = 90,
-    IdyllshirePrologueGate = 91,
-    IdyllshireEpilogueGate = 92,
-
-    CoerthasWesternHighlandsFalconsNest = 71,
-    SeaOfCloudsCampCloudtop = 72,
-    SeaOfCloudsOkZundu = 73,
-    AzysLlaHelix = 74,
-    DravanianForelandsTailfeather = 76,
-    DravanianForelandsAnyxTrine = 77,
-    ChurningMistsMoghome = 78,
-    ChurningMistsZenith = 79,
-
-    RhalgrsReach = 104,
-    RhalgrsReachWest = 121,
-    RhalgrsReachNorthEast = 122,
-    RhalgrsReachFringesGate = 123,
-    RhalgrsReachPeaksGate = 124,
-
-    Kugane = 111,
-    KuganeShiokazeHostelry = 112,
-    KuganePier1 = 113,
-    KuganeThavnairianConsulate = 114,
-    KuganeMarkets = 115,
-    KuganeBokairoInn = 116,
-    KuganeRubyBazaar = 117,
-    KuganeSekiseigumiBarracks = 118,
-    KuganeRakuzaDistrict = 119,
-    KuganeRubyPrice = 120,
-    KuganeAirship = 126,
-
-    FringesCastrumOriens = 98,
-    FringesPeeringStones = 99,
-    PeaksAlaGannha = 100,
-    PeaksAlaGhiri = 101,
-    LochsPortaPraetoria = 102,
-    LochsAlaMhiganQuarter = 103,
-    RubySeaTamamizu = 105,
-    RubySeaOnokoro = 106,
-    YanxiaNamai = 107,
-    YanxiaHouseOfTheFierce = 108,
-    AzimSteppeReunion = 109,
-    AzimSteppeDawnThrone = 110,
-    AzimSteppeDhoroIloh = 128,
-
-    DomanEnclave = 127,
-    DomanEnclaveNorthern = 129,
-    DomanEnclaveSouthern = 130,
-    DomanEnclaveOneRiver = 131,
-    DomanEnclaveDocks = 162,
-
-    Crystarium = 133,
-    CrystariumMarkets = 149,
-    CrystariumTemenosRookery = 150,
-    CrystariumDossalGate = 151,
-    CrystariumPendants = 152,
-    CrystariumAmaroLaunch = 153,
-    CrystariumCrystallineMean = 154,
-    CrystariumCabinetOfCuriosity = 155,
-    CrystariumTessellation = 156,
-
-    Eulmore = 134,
-    EulmoreMainstay = 157,
-    EulmoreNightsoilPots = 158,
-    EulmoreGloryGate = 159,
-    EulmoreSoutheastDerelict = 135,
-    EulmorePathToGlory = 160,
-
-    LakelandFortJobb = 132,
-    LakelandOstallImperative = 136,
-    KholusiaStilltide = 137,
-    KholusiaWright = 138,
-    KholusiaTomra = 139,
-    AmhAraengMordSouq = 140,
-    AmhAraengInnAtJourneysHead = 161,
-    AmhAraengTwine = 141,
-    RaktikaSlitherbough = 142,
-    RaktikaFanow = 143,
-    IlMhegLydhaLran = 144,
-    IlMhegPiaEnni = 145,
-    IlMhegWolekdorf = 146,
-    TempestOndoCups = 147,
-    TempestMacarensesAngle = 148,
-
-    OldSharlayan = 182,
-    OldSharlayanStudium = 184,
-    OldSharlayanBaldesionAnnex = 185,
-    OldSharlayanRostra = 186,
-    OldSharlayanLeveilleurEstate = 187,
-    OldSharlayanJourneysEnd = 188,
-    OldSharlayanScholarsHarbor = 189,
-    OldSharlayanHallOfArtifice = 190,
-
-    RadzAtHan = 183,
-    RadzAtHanMeghaduta = 191,
-    RadzAtHanRuveydahFibers = 192,
-    RadzAtHanAirship = 193,
-    RadzAtHanAlzadaalsPeace = 194,
-    RadzAtHanHallOfTheRadiantHost = 195,
-    RadzAtHanMehrydesMeyhane = 196,
-    RadzAtHanKama = 198,
-    RadzAtHanHighCrucible = 199,
-    RadzAtHanGateOfFirstSight = 197,
-
-    LabyrinthosArcheion = 166,
-    LabyrinthosSharlayanHamlet = 167,
-    LabyrinthosAporia = 168,
-    ThavnairYedlihmad = 169,
-    ThavnairGreatWork = 170,
-    ThavnairPalakasStand = 171,
-    GarlemaldCampBrokenGlass = 172,
-    GarlemaldTertium = 173,
-    MareLamentorumSinusLacrimarum = 174,
-    MareLamentorumBestwaysBurrow = 175,
-    ElpisAnagnorisis = 176,
-    ElpisTwelveWonders = 177,
-    ElpisPoietenOikos = 178,
-    UltimaThuleReahTahra = 179,
-    UltimaThuleAbodeOfTheEa = 180,
-    UltimaThuleBaseOmicron = 181,
-
-    Tuliyollal = 216,
-
-    TuliyollalDirigibleLanding = 218,
-    TuliyollalTheResplendentQuarter = 219,
-    TuliyollalTheForardCabins = 220,
-    TuliyollalBaysideBevyMarketplace = 221,
-    TuliyollalVollokShoonsa = 222,
-    TuliyollalWachumeqimeqi = 223,
-    TuliyollalBrightploomPost = 224,
-    TuliyollalArchOfTheDawnUrqopacha = 225,
-    TuliyollalArchOfTheDawnKozamauka = 226,
-    TuliyollalIhuykatumu = 227,
-    TuliyollalDirigibleLandingYakTel = 228,
-    TuliyollalXakTuralSkygate = 229,
-
-    SolutionNine = 217,
-    SolutionNineInformationCenter = 230,
-    SolutionNineTrueVue = 231,
-    SolutionNineNeonStein = 232,
-    SolutionNineTheArcadion = 233,
-    SolutionNineResolution = 234,
-    SolutionNineNexusArcade = 235,
-    SolutionNineResidentialSector = 236,
-    SolutionNineScanningPortNine = 237,
-
-    UrqopachaWachunpelo = 200,
-    UrqopachaWorlarsEcho = 201,
-    KozamaukaOkHanu = 202,
-    KozamaukaManyFires = 203,
-    KozamaukaEarthenshire = 204,
-    YakTelIqBraax = 205,
-    YakTelMamook = 206,
-    ShaaloaniHhusatahwi = 207,
-    ShaaloaniShesheneweziSprings = 208,
-    ShaaloaniMehwahhetsoan = 209,
-    HeritageFoundYyasulaniStation = 210,
-    HeritageFoundTheOutskirts = 211,
-    HeritageFoundElectropeStrike = 212,
-    LivingMemoryLeynodeMnemo = 213,
-    LivingMemoryLeynodePyro = 214,
-    LivingMemoryLeynodeAero = 215,
-}
diff --git a/Questionable.Model/V1/EDialogChoiceType.cs b/Questionable.Model/V1/EDialogChoiceType.cs
deleted file mode 100644 (file)
index 066639a..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Questionable.Model.V1;
-
-public enum EDialogChoiceType
-{
-    None,
-    YesNo,
-    List,
-}
diff --git a/Questionable.Model/V1/EEmote.cs b/Questionable.Model/V1/EEmote.cs
deleted file mode 100644 (file)
index 6657a1d..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-[JsonConverter(typeof(EmoteConverter))]
-public enum EEmote
-{
-    None = 0,
-
-    Stretch = 37,
-    Wave = 16,
-    Rally = 34,
-    Deny = 25,
-    Pray = 58,
-    Slap = 111,
-    Doubt = 12,
-    Psych = 30,
-    Cheer = 6,
-    Happy = 48,
-    Poke = 28,
-    Flex = 139,
-    Soothe = 35,
-    Me = 23,
-    Welcome = 41,
-    ImperialSalute = 59,
-    Pet = 105,
-    Dance = 11,
-    Respect = 140,
-    Lookout = 22,
-    Kneel = 19,
-    Bow = 5,
-}
diff --git a/Questionable.Model/V1/EEnemySpawnType.cs b/Questionable.Model/V1/EEnemySpawnType.cs
deleted file mode 100644 (file)
index 8465f01..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-[JsonConverter(typeof(EnemySpawnTypeConverter))]
-public enum EEnemySpawnType
-{
-    None = 0,
-    AfterInteraction,
-    AfterItemUse,
-    AutoOnEnterArea,
-    OverworldEnemies,
-}
diff --git a/Questionable.Model/V1/EExtraSkipCondition.cs b/Questionable.Model/V1/EExtraSkipCondition.cs
deleted file mode 100644 (file)
index 227bf0f..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-[JsonConverter(typeof(SkipConditionConverter))]
-public enum EExtraSkipCondition
-{
-    None,
-    WakingSandsMainArea,
-}
diff --git a/Questionable.Model/V1/EInteractionType.cs b/Questionable.Model/V1/EInteractionType.cs
deleted file mode 100644 (file)
index 843f600..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-[JsonConverter(typeof(InteractionTypeConverter))]
-public enum EInteractionType
-{
-    Interact,
-    WalkTo,
-    AttuneAethernetShard,
-    AttuneAetheryte,
-    AttuneAetherCurrent,
-    Combat,
-    UseItem,
-    EquipItem,
-    Say,
-    Emote,
-    Action,
-    WaitForObjectAtPosition,
-    WaitForManualProgress,
-    Duty,
-    SinglePlayerDuty,
-    Jump,
-    Dive,
-
-    /// <summary>
-    /// Needs to be manually continued.
-    /// </summary>
-    Instruction,
-
-    AcceptQuest,
-    CompleteQuest,
-}
diff --git a/Questionable.Model/V1/EJumpType.cs b/Questionable.Model/V1/EJumpType.cs
deleted file mode 100644 (file)
index b06365e..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-[JsonConverter(typeof(JumpTypeConverter))]
-public enum EJumpType
-{
-    SingleJump,
-    RepeatedJumps,
-}
diff --git a/Questionable.Model/V1/ELockedSkipCondition.cs b/Questionable.Model/V1/ELockedSkipCondition.cs
deleted file mode 100644 (file)
index 2863c26..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-[JsonConverter(typeof(LockedSkipConditionConverter))]
-public enum ELockedSkipCondition
-{
-    Locked,
-    Unlocked,
-}
diff --git a/Questionable.Model/V1/EQuestWorkMode.cs b/Questionable.Model/V1/EQuestWorkMode.cs
deleted file mode 100644 (file)
index ef225b2..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-[JsonConverter(typeof(QuestWorkModeConverter))]
-public enum EQuestWorkMode
-{
-    Bitwise,
-    Exact,
-}
diff --git a/Questionable.Model/V1/ExcelRef.cs b/Questionable.Model/V1/ExcelRef.cs
deleted file mode 100644 (file)
index 295dd5b..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-using System;
-
-namespace Questionable.Model.V1;
-
-public class ExcelRef
-{
-    private readonly string? _stringValue;
-    private readonly uint? _rowIdValue;
-
-    public ExcelRef(string value)
-    {
-        _stringValue = value;
-        _rowIdValue = null;
-        Type = EType.Key;
-    }
-
-    public ExcelRef(uint value)
-    {
-        _stringValue = null;
-        _rowIdValue = value;
-        Type = EType.RowId;
-    }
-
-    private ExcelRef(string? stringValue, uint? rowIdValue, EType type)
-    {
-        _stringValue = stringValue;
-        _rowIdValue = rowIdValue;
-        Type = type;
-    }
-
-    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; }
-
-    public string AsKey()
-    {
-        if (Type != EType.Key)
-            throw new InvalidOperationException();
-
-        return _stringValue!;
-    }
-
-    public uint AsRowId()
-    {
-        if (Type != EType.RowId)
-            throw new InvalidOperationException();
-
-        return _rowIdValue!.Value;
-    }
-
-    public string AsRawString()
-    {
-        if (Type != EType.RawString)
-            throw new InvalidOperationException();
-
-        return _stringValue!;
-    }
-
-    public enum EType
-    {
-        None,
-        Key,
-        RowId,
-        RawString,
-    }
-}
diff --git a/Questionable.Model/V1/JumpDestination.cs b/Questionable.Model/V1/JumpDestination.cs
deleted file mode 100644 (file)
index 2497560..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-using System.Numerics;
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-public sealed class JumpDestination
-{
-    [JsonConverter(typeof(VectorConverter))]
-    public Vector3 Position { get; set; }
-
-    public float? StopDistance { get; set; }
-    public float? DelaySeconds { get; set; }
-    public EJumpType Type { get; set; } = EJumpType.SingleJump;
-
-    public float CalculateStopDistance() => StopDistance ?? 1f;
-}
diff --git a/Questionable.Model/V1/QuestRoot.cs b/Questionable.Model/V1/QuestRoot.cs
deleted file mode 100644 (file)
index ada2e9f..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-using System.Collections.Generic;
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-public sealed class QuestRoot
-{
-    [JsonConverter(typeof(StringListOrValueConverter))]
-    public List<string> Author { get; set; } = new();
-
-    /// <summary>
-    /// This is only relevant for release builds.
-    /// </summary>
-    public bool Disabled { get; set; }
-
-    public string? Comment { get; set; }
-    public List<ushort> TerritoryBlacklist { get; set; } = new();
-    public List<QuestSequence> QuestSequence { get; set; } = new();
-}
diff --git a/Questionable.Model/V1/QuestSequence.cs b/Questionable.Model/V1/QuestSequence.cs
deleted file mode 100644 (file)
index 73807e0..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-
-namespace Questionable.Model.V1;
-
-public sealed class QuestSequence
-{
-    public int Sequence { get; set; }
-    public string? Comment { get; set; }
-    public List<QuestStep> Steps { get; set; } = new();
-
-    public QuestStep? FindStep(int step)
-    {
-        if (step < 0 || step >= Steps.Count)
-            return null;
-
-        return Steps[step];
-    }
-
-    public QuestStep? LastStep() => Steps.LastOrDefault();
-}
diff --git a/Questionable.Model/V1/QuestStep.cs b/Questionable.Model/V1/QuestStep.cs
deleted file mode 100644 (file)
index accd662..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-using System.Collections.Generic;
-using System.Numerics;
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-public sealed class QuestStep
-{
-    public const float DefaultStopDistance = 3f;
-
-    public EInteractionType InteractionType { get; set; }
-
-    public uint? DataId { get; set; }
-
-    [JsonConverter(typeof(VectorConverter))]
-    public Vector3? Position { get; set; }
-
-    public float? StopDistance { get; set; }
-    public float? NpcWaitDistance { get; set; }
-    public ushort TerritoryId { get; set; }
-    public ushort? TargetTerritoryId { get; set; }
-    public float? DelaySecondsAtStart { get; set; }
-
-    public bool Disabled { get; set; }
-    public bool DisableNavmesh { get; set; }
-    public bool? Mount { get; set; }
-    public bool? Fly { get; set; }
-    public bool? Land { get; set; }
-    public bool? Sprint { get; set; }
-    public bool? IgnoreDistanceToObject { get; set; }
-    public string? Comment { get; set; }
-
-    /// <summary>
-    /// Only used when attuning to an aetheryte.
-    /// </summary>
-    public EAetheryteLocation? Aetheryte { get; set; }
-
-    /// <summary>
-    /// Only used when attuning to an aethernet shard.
-    /// </summary>
-    [JsonConverter(typeof(AethernetShardConverter))]
-    public EAetheryteLocation? AethernetShard { get; set; }
-
-    public EAetheryteLocation? AetheryteShortcut { get; set; }
-
-    public AethernetShortcut? AethernetShortcut { get; set; }
-    public uint? AetherCurrentId { get; set; }
-
-    public uint? ItemId { get; set; }
-    public bool? GroundTarget { get; set; }
-
-    public EEmote? Emote { get; set; }
-    public ChatMessage? ChatMessage { get; set; }
-    public EAction? Action { get; set; }
-
-    public EEnemySpawnType? EnemySpawnType { get; set; }
-    public IList<uint> KillEnemyDataIds { get; set; } = new List<uint>();
-    public IList<ComplexCombatData> ComplexCombatData { get; set; } = new List<ComplexCombatData>();
-    public float? CombatDelaySecondsAtStart { get; set; }
-
-    public JumpDestination? JumpDestination { get; set; }
-    public uint? ContentFinderConditionId { get; set; }
-    public SkipConditions? SkipConditions { get; set; }
-
-    public List<List<QuestWorkValue>?> RequiredQuestVariables { get; set; } = new();
-    public IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; set; } = new List<QuestWorkValue?>();
-    public IList<DialogueChoice> DialogueChoices { get; set; } = new List<DialogueChoice>();
-    public IList<uint> PointMenuChoices { get; set; } = new List<uint>();
-
-    // TODO: Not implemented
-    public ushort? PickUpQuestId { get; set; }
-
-    public ushort? TurnInQuestId { get; set; }
-    public ushort? NextQuestId { get; set; }
-
-    [JsonConstructor]
-    public QuestStep()
-    {
-    }
-
-    public QuestStep(EInteractionType interactionType, uint? dataId, Vector3? position, ushort territoryId)
-    {
-        InteractionType = interactionType;
-        DataId = dataId;
-        Position = position;
-        TerritoryId = territoryId;
-    }
-
-    public float CalculateActualStopDistance()
-    {
-        if (InteractionType == EInteractionType.WalkTo)
-            return StopDistance ?? 0.25f;
-        if (InteractionType == EInteractionType.AttuneAetheryte)
-            return StopDistance ?? 10f;
-        else
-            return StopDistance ?? DefaultStopDistance;
-    }
-}
diff --git a/Questionable.Model/V1/QuestWorkValue.cs b/Questionable.Model/V1/QuestWorkValue.cs
deleted file mode 100644 (file)
index 75c6851..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-using System;
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-[JsonConverter(typeof(QuestWorkConfigConverter))]
-public sealed class QuestWorkValue(byte? high, byte? low, EQuestWorkMode mode)
-{
-    public QuestWorkValue(byte value)
-        : this((byte)(value >> 4), (byte)(value & 0xF), EQuestWorkMode.Bitwise)
-    {
-    }
-
-    public byte? High { get; set; } = high;
-    public byte? Low { get; set; } = low;
-    public EQuestWorkMode Mode { get; set; } = mode;
-
-    public override string ToString()
-    {
-        if (High != null && Low != null)
-            return ((byte)(High << 4) + Low).ToString();
-        else if (High != null)
-            return High + "H";
-        else if (Low != null)
-            return Low + "L";
-        else
-            return "-";
-    }
-}
diff --git a/Questionable.Model/V1/SkipAetheryteCondition.cs b/Questionable.Model/V1/SkipAetheryteCondition.cs
deleted file mode 100644 (file)
index 86d0a1c..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Questionable.Model.V1;
-
-public sealed class SkipAetheryteCondition
-{
-    public bool Never { get; set; }
-    public bool InSameTerritory { get; set; }
-}
diff --git a/Questionable.Model/V1/SkipConditions.cs b/Questionable.Model/V1/SkipConditions.cs
deleted file mode 100644 (file)
index 5f86686..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Questionable.Model.V1;
-
-public sealed class SkipConditions
-{
-    public SkipStepConditions? StepIf { get; set; }
-    public SkipAetheryteCondition? AetheryteShortcutIf { get; set; }
-    public SkipAetheryteCondition? AethernetShortcutIf { get; set; }
-}
diff --git a/Questionable.Model/V1/SkipItemConditions.cs b/Questionable.Model/V1/SkipItemConditions.cs
deleted file mode 100644 (file)
index 9154dcd..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Questionable.Model.V1;
-
-public sealed class SkipItemConditions
-{
-    public bool NotInInventory { get; set; }
-}
diff --git a/Questionable.Model/V1/SkipStepConditions.cs b/Questionable.Model/V1/SkipStepConditions.cs
deleted file mode 100644 (file)
index be8df96..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-
-namespace Questionable.Model.V1;
-
-public sealed class SkipStepConditions
-{
-    public bool Never { get; set; }
-    public IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; set; } = new List<QuestWorkValue?>();
-    public ELockedSkipCondition? Flying { get; set; }
-    public ELockedSkipCondition? Chocobo { get; set; }
-    public bool NotTargetable { get; set; }
-    public List<ushort> InTerritory { get; set; } = new();
-    public List<ushort> NotInTerritory { get; set; } = new();
-    public SkipItemConditions? Item { get; set; }
-    public List<ushort> QuestsAccepted { get; set; } = new();
-    public List<ushort> QuestsCompleted { get; set; } = new();
-    public EExtraSkipCondition? ExtraCondition { get; set; }
-
-    public bool HasSkipConditions()
-    {
-        if (Never)
-            return false;
-        return (CompletionQuestVariablesFlags.Count > 0 && CompletionQuestVariablesFlags.Any(x => x != null)) ||
-               Flying != null ||
-               Chocobo != null ||
-               NotTargetable ||
-               InTerritory.Count > 0 ||
-               NotInTerritory.Count > 0 ||
-               Item != null ||
-               QuestsAccepted.Count > 0 ||
-               QuestsCompleted.Count > 0 ||
-               ExtraCondition != null;
-    }
-
-    public override string ToString()
-    {
-        return
-            $"{nameof(Never)}: {Never}, {nameof(CompletionQuestVariablesFlags)}: {CompletionQuestVariablesFlags}, {nameof(Flying)}: {Flying}, {nameof(Chocobo)}: {Chocobo}, {nameof(NotTargetable)}: {NotTargetable}, {nameof(InTerritory)}: {string.Join(" ", InTerritory)}, {nameof(NotInTerritory)}: {string.Join(" ", NotInTerritory)}, {nameof(Item)}: {Item}, {nameof(QuestsAccepted)}: {string.Join(" ", QuestsAccepted)}, {nameof(QuestsCompleted)}: {string.Join(" ", QuestsCompleted)}, {nameof(ExtraCondition)}: {ExtraCondition}";
-    }
-}
diff --git a/Questionable.Model/common-schema.json b/Questionable.Model/common-schema.json
new file mode 100644 (file)
index 0000000..6017ecb
--- /dev/null
@@ -0,0 +1,299 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "https://git.carvel.li/liza/Questionable/raw/branch/master/Questionable.Model/common-schema.json",
+  "$defs": {
+    "Aetheryte": {
+      "type": "string",
+      "enum": [
+        "Gridania",
+        "Central Shroud - Bentbranch Meadows",
+        "East Shroud - Hawthorne Hut",
+        "South Shroud - Quarrymill",
+        "South Shroud - Camp Tranquil",
+        "North Shroud - Fallgourd Float",
+        "Ul'dah",
+        "Western Thanalan - Horizon",
+        "Central Thanalan - Black Brush Station",
+        "Eastern Thanalan - Camp Drybone",
+        "Southern Thanalan - Little Ala Mhigo",
+        "Southern Thanalan - Forgotten Springs",
+        "Northern Thanalan - Camp Bluefog",
+        "Northern Thanalan - Ceruleum Processing Plant",
+        "Limsa Lominsa",
+        "Middle La Noscea - Summerford Farms",
+        "Lower La Noscea - Moraby Drydocks",
+        "Eastern La Noscea - Costa Del Sol",
+        "Eastern La Noscea - Wineport",
+        "Western La Noscea - Swiftperch",
+        "Western La Noscea - Aleport",
+        "Upper La Noscea - Camp Bronze Lake",
+        "Outer La Noscea - Camp Overlook",
+        "Coerthas Central Highlands - Camp Dragonhead",
+        "Mor Dhona",
+        "Gold Saucer",
+        "Wolves' Den Pier",
+        "Ishgard",
+        "Idyllshire",
+        "Coerthas Western Highlands - Falcon's Nest",
+        "The Sea of Clouds - Camp Cloudtop",
+        "The Sea of Clouds - Ok' Zundu",
+        "Azys Lla - Helix",
+        "The Dravanian Forelands - Tailfeather",
+        "The Dravanian Forelands - Anyx Trine",
+        "The Churning Mists - Moghome",
+        "The Churning Mists - Zenith",
+        "Rhalgr's Reach",
+        "Fringes - Castrum Oriens",
+        "Fringes - Peering Stones",
+        "Peaks - Ala Gannha",
+        "Peaks - Ala Ghiri",
+        "Lochs - Porta Praetoria",
+        "Lochs - Ala Mhigan Quarter",
+        "Kugane",
+        "Ruby Sea - Tamamizu",
+        "Ruby Sea - Onokoro",
+        "Yanxia - Namai",
+        "Yanxia - House of the Fierce",
+        "Azim Steppe - Reunion",
+        "Azim Steppe - Dawn Throne",
+        "Azim Steppe - Dhoro Iloh",
+        "Doman Enclave",
+        "Crystarium",
+        "Eulmore",
+        "Lakeland - Fort Jobb",
+        "Lakeland - Ostall Imperative",
+        "Kholusia - Stilltide",
+        "Kholusia - Wright",
+        "Kholusia - Tomra",
+        "Amh Araeng - Mord Souq",
+        "Amh Araeng - Inn at Journey's Head",
+        "Amh Araeng - Twine",
+        "Rak'tika - Slitherbough",
+        "Rak'tika - Fanow",
+        "Il Mheg - Lydha Lran",
+        "Il Mheg - Pia Enni",
+        "Il Mheg - Wolekdorf",
+        "Tempest - Ondo Cups",
+        "Tempest - Macarenses Angle",
+        "Old Sharlayan",
+        "Radz-at-Han",
+        "Labyrinthos - Archeion",
+        "Labyrinthos - Sharlayan Hamlet",
+        "Labyrinthos - Aporia",
+        "Thavnair - Yedlihmad",
+        "Thavnair - Great Work",
+        "Thavnair - Palaka's Stand",
+        "Garlemald - Camp Broken Glass",
+        "Garlemald - Tertium",
+        "Mare Lamentorum - Sinus Lacrimarum",
+        "Mare Lamentorum - Bestways Burrow",
+        "Elpis - Anagnorisis",
+        "Elpis - Twelve Wonders",
+        "Elpis - Poieten Oikos",
+        "Ultima Thule - Reah Tahra",
+        "Ultima Thule - Abode of the Ea",
+        "Ultima Thule - Base Omicron",
+        "Tuliyollal",
+        "Solution Nine",
+        "Urqopacha - Wachunpelo",
+        "Urqopacha - Worlar's Echo",
+        "Kozama'uka - Ok'hanu",
+        "Kozama'uka - Many Fires",
+        "Kozama'uka - Earthenshire",
+        "Yak T'el - Iq Br'aax",
+        "Yak T'el - Mamook",
+        "Shaaloani - Hhusatahwi",
+        "Shaaloani - Sheshenewezi Springs",
+        "Shaaloani - Mehwahhetsoan",
+        "Heritage Found - Yyasulani Station",
+        "Heritage Found - The Outskirts",
+        "Heritage Found - Electrope Strike",
+        "Living Memory - Leynode Mnemo",
+        "Living Memory - Leynode Pyro",
+        "Living Memory - Leynode Aero"
+      ]
+    },
+    "AethernetShard": {
+      "type": "string",
+      "enum": [
+        "[Gridania] Aetheryte Plaza",
+        "[Gridania] Archers' Guild",
+        "[Gridania] Leatherworkers' Guild & Shaded Bower",
+        "[Gridania] Lancers' Guild",
+        "[Gridania] Conjurers' Guild",
+        "[Gridania] Botanists' Guild",
+        "[Gridania] Mih Khetto's Amphitheatre",
+        "[Gridania] Blue Badger Gate (Central Shroud)",
+        "[Gridania] Yellow Serpent Gate (North Shroud)",
+        "[Gridania] White Wolf Gate (Central Shroud)",
+        "[Gridania] Airship Landing",
+        "[Ul'dah] Aetheryte Plaza",
+        "[Ul'dah] Adventurers' Guild",
+        "[Ul'dah] Thaumaturges' Guild",
+        "[Ul'dah] Gladiators' Guild",
+        "[Ul'dah] Miners' Guild",
+        "[Ul'dah] Weavers' Guild",
+        "[Ul'dah] Goldsmiths' Guild",
+        "[Ul'dah] Sapphire Avenue Exchange",
+        "[Ul'dah] Alchemists' Guild",
+        "[Ul'dah] Gate of the Sultana (Western Thanalan)",
+        "[Ul'dah] Gate of Nald (Central Thanalan)",
+        "[Ul'dah] Gate of Thal (Central Thanalan)",
+        "[Ul'dah] The Chamber of Rule",
+        "[Ul'dah] Airship Landing",
+        "[Limsa Lominsa] Aetheryte Plaza",
+        "[Limsa Lominsa] Arcanists' Guild",
+        "[Limsa Lominsa] Fishermens' Guild",
+        "[Limsa Lominsa] Hawkers' Alley",
+        "[Limsa Lominsa] The Aftcastle",
+        "[Limsa Lominsa] Culinarians' Guild",
+        "[Limsa Lominsa] Marauders' Guild",
+        "[Limsa Lominsa] Zephyr Gate (Middle La Noscea)",
+        "[Limsa Lominsa] Tempest Gate (Lower La Noscea)",
+        "[Limsa Lominsa] Airship Landing",
+        "[Ishgard] Aetheryte Plaza",
+        "[Ishgard] The Forgotten Knight",
+        "[Ishgard] Skysteel Manufactory",
+        "[Ishgard] The Brume",
+        "[Ishgard] Athenaeum Astrologicum",
+        "[Ishgard] The Jeweled Crozier",
+        "[Ishgard] Saint Reymanaud's Cathedral",
+        "[Ishgard] The Tribunal",
+        "[Ishgard] The Last Vigil",
+        "[Ishgard] The Gates of Judgement (Coerthas Central Highlands)",
+        "[Idyllshire] Aetheryte Plaza",
+        "[Idyllshire] West Idyllshire",
+        "[Idyllshire] Prologue Gate (Western Hinterlands)",
+        "[Idyllshire] Epilogue Gate (Eastern Hinterlands)",
+        "[Rhalgr's Reach] Aetheryte Plaza",
+        "[Rhalgr's Reach] Western Rhalgr's Reach",
+        "[Rhalgr's Reach] Northeastern Rhalgr's Reach",
+        "[Rhalgr's Reach] Fringes Gate",
+        "[Rhalgr's Reach] Peaks Gate",
+        "[Kugane] Aetheryte Plaza",
+        "[Kugane] Shiokaze Hostelry",
+        "[Kugane] Pier #1",
+        "[Kugane] Thavnairian Consulate",
+        "[Kugane] Kogane Dori Markets",
+        "[Kugane] Bokairo Inn",
+        "[Kugane] The Ruby Bazaar",
+        "[Kugane] Sekiseigumi Barracks",
+        "[Kugane] Rakuza District",
+        "[Kugane] The Ruby Price",
+        "[Kugane] Airship Landing",
+        "[Crystarium] Aetheryte Plaza",
+        "[Crystarium] Musica Universalis Markets",
+        "[Crystarium] Temenos Rookery",
+        "[Crystarium] The Dossal Gate",
+        "[Crystarium] The Pendants",
+        "[Crystarium] The Amaro Launch",
+        "[Crystarium] The Crystalline Mean",
+        "[Crystarium] The Cabinet of Curiosity",
+        "[Crystarium] Tessellation (Lakeland)",
+        "[Eulmore] Aetheryte Plaza",
+        "[Eulmore] Southeast Derelicts",
+        "[Eulmore] Nightsoil Pots",
+        "[Eulmore] The Glory Gate",
+        "[Eulmore] The Mainstay",
+        "[Eulmore] The Path to Glory (Kholusia)",
+        "[Old Sharlayan] Aetheryte Plaza",
+        "[Old Sharlayan] The Studium",
+        "[Old Sharlayan] The Baldesion Annex",
+        "[Old Sharlayan] The Rostra",
+        "[Old Sharlayan] The Leveilleur Estate",
+        "[Old Sharlayan] Journey's End",
+        "[Old Sharlayan] Scholar's Harbor",
+        "[Old Sharlayan] The Hall of Artifice (Labyrinthos)",
+        "[Radz-at-Han] Aetheryte Plaza",
+        "[Radz-at-Han] Meghaduta",
+        "[Radz-at-Han] Ruveydah Fibers",
+        "[Radz-at-Han] Airship Landing",
+        "[Radz-at-Han] Alzadaal's Peace",
+        "[Radz-at-Han] Hall of the Radiant Host",
+        "[Radz-at-Han] Mehryde's Meyhane",
+        "[Radz-at-Han] Kama",
+        "[Radz-at-Han] The High Crucible of Al-Kimiya",
+        "[Radz-at-Han] The Gate of First Sight (Thavnair)",
+        "[Tuliyollal] Aetheryte Plaza",
+        "[Tuliyollal] Dirigible Landing",
+        "[Tuliyollal] The Resplendent Quarter",
+        "[Tuliyollal] The For'ard Cabins",
+        "[Tuliyollal] Bayside Bevy Marketplace",
+        "[Tuliyollal] Vollok Shoonsa",
+        "[Tuliyollal] Wachumeqimeqi",
+        "[Tuliyollal] Brightploom Post",
+        "[Tuliyollal] Arch of the Dawn (Urqopacha)",
+        "[Tuliyollal] Arch of the Dawn (Kozama'uka)",
+        "[Tuliyollal] Ihuykatumu (Kozama'uka)",
+        "[Tuliyollal] Dirigible Landing (Yak T'el)",
+        "[Tuliyollal] Xak Tural Skygate (Shaaloani)",
+        "[Solution Nine] Aetheryte Plaza",
+        "[Solution Nine] Information Center",
+        "[Solution Nine] True Vue",
+        "[Solution Nine] Neon Stein",
+        "[Solution Nine] The Arcadion",
+        "[Solution Nine] Resolution",
+        "[Solution Nine] Nexus Arcade",
+        "[Solution Nine] Residential Sector",
+        "[Solution Nine] Scanning Port Nine (Heritage Found)"
+      ]
+    },
+    "CompletionFlags": {
+      "type": "array",
+      "description": "Quest Variables that dictate whether or not this step is skipped: null is don't check, positive values need to be set, negative values need to be unset",
+      "items": {
+        "oneOf": [
+          {
+            "type": "object",
+            "properties": {
+              "High": {
+                "type": [
+                  "number",
+                  "null"
+                ],
+                "minimum": 0,
+                "maximum": 15
+              },
+              "Low": {
+                "type": [
+                  "number",
+                  "null"
+                ],
+                "minimum": 0,
+                "maximum": 15
+              },
+              "Negative": {
+                "type": "boolean"
+              },
+              "Mode": {
+                "type": "string",
+                "enum": [
+                  "Bitwise",
+                  "Exact"
+                ]
+              }
+            }
+          },
+          {
+            "type": "number",
+            "enum": [
+              1,
+              2,
+              4,
+              8,
+              16,
+              32,
+              64,
+              128
+            ]
+          },
+          {
+            "type": "null"
+          }
+        ]
+      },
+      "minItems": 6,
+      "maxItems": 6
+    }
+  }
+}
index a083acfbe41fe69261eec986c5e86be268fa9ede..b531428724f61a534e2a31ab8426aaa311f45c45 100644 (file)
@@ -15,6 +15,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Questionable.Model", "Quest
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuestPathGenerator.Tests", "QuestPathGenerator.Tests\QuestPathGenerator.Tests.csproj", "{4FD6F346-8961-4BD5-BDA2-E5F426DE4FC7}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GatheringPaths", "GatheringPaths\GatheringPaths.csproj", "{8BF98BEF-6F00-4197-91ED-75F8F1C35FFB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GatheringPathRenderer", "GatheringPathRenderer\GatheringPathRenderer.csproj", "{F514DA95-9867-4F3F-8062-ACE0C62E8740}"
+EndProject
 Global
        GlobalSection(SolutionConfigurationPlatforms) = preSolution
                Debug|x64 = Debug|x64
@@ -45,6 +49,14 @@ Global
                {C91EEF13-A1AC-4A40-B695-DD4E378E5989}.Debug|x64.Build.0 = Debug|x64
                {C91EEF13-A1AC-4A40-B695-DD4E378E5989}.Release|x64.ActiveCfg = Release|x64
                {C91EEF13-A1AC-4A40-B695-DD4E378E5989}.Release|x64.Build.0 = Release|x64
+               {8BF98BEF-6F00-4197-91ED-75F8F1C35FFB}.Debug|x64.ActiveCfg = Debug|Any CPU
+               {8BF98BEF-6F00-4197-91ED-75F8F1C35FFB}.Debug|x64.Build.0 = Debug|Any CPU
+               {8BF98BEF-6F00-4197-91ED-75F8F1C35FFB}.Release|x64.ActiveCfg = Release|Any CPU
+               {8BF98BEF-6F00-4197-91ED-75F8F1C35FFB}.Release|x64.Build.0 = Release|Any CPU
+               {F514DA95-9867-4F3F-8062-ACE0C62E8740}.Debug|x64.ActiveCfg = Debug|Any CPU
+               {F514DA95-9867-4F3F-8062-ACE0C62E8740}.Debug|x64.Build.0 = Debug|Any CPU
+               {F514DA95-9867-4F3F-8062-ACE0C62E8740}.Release|x64.ActiveCfg = Release|Any CPU
+               {F514DA95-9867-4F3F-8062-ACE0C62E8740}.Release|x64.Build.0 = Release|Any CPU
        EndGlobalSection
        GlobalSection(SolutionProperties) = preSolution
                HideSolutionNode = FALSE
index 26ca9a2232cb706234502d47619832191332a37c..d28946a819e01cce5cfe461d0addbb478f158fb2 100644 (file)
@@ -14,7 +14,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Memory;
 using FFXIVClientStructs.FFXIV.Client.System.String;
 using Lumina.Excel.GeneratedSheets;
 using Microsoft.Extensions.Logging;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable;
 
index f747966ed4d54406698193667df413095ad35a0c..1f95e9da128ed36f3ecb407ddd6cea6b4b7b0651 100644 (file)
@@ -14,7 +14,7 @@ using FFXIVClientStructs.FFXIV.Common.Math;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.CombatModules;
 using Questionable.Controller.Utils;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller;
 
index 0bb4e12b462547018496a6aa861edbb72a664228..61d31fbd5153e62ae5e8819d50f76adf53ad9f24 100644 (file)
@@ -14,7 +14,7 @@ using LLib.GameUI;
 using Lumina.Excel.GeneratedSheets;
 using Microsoft.Extensions.Logging;
 using Questionable.Data;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 using Quest = Questionable.Model.Quest;
 using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
 
index 1db1d5cfe2b8fd40a5c86ce7b40af655b60a99a5..d7fc74a9776110033d50548b225eb38db2c5d8fe 100644 (file)
@@ -18,8 +18,10 @@ using Microsoft.Extensions.Logging;
 using Questionable.Controller.NavigationOverrides;
 using Questionable.External;
 using Questionable.Model;
-using Questionable.Model.V1;
-using Questionable.Model.V1.Converter;
+using Questionable.Model.Common;
+using Questionable.Model.Common.Converter;
+using Questionable.Model.Questing;
+using Questionable.Model.Questing.Converter;
 
 namespace Questionable.Controller;
 
index c92fee2ba6b68e8d57a0b2c89324dfb2d1769800..e50891d2171cab165b8ffd23a9f6dcdcca2c7fcc 100644 (file)
@@ -10,7 +10,7 @@ using Questionable.Controller.Steps;
 using Questionable.Controller.Steps.Shared;
 using Questionable.External;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller;
 
index 71750a1c07836fbcd26f50ff45b984af194f36f8..f472c115e4cd22c146bd589b80cff838a3855190 100644 (file)
@@ -11,7 +11,7 @@ using Dalamud.Plugin;
 using Microsoft.Extensions.Logging;
 using Questionable.Data;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 using Questionable.QuestPaths;
 using Questionable.Validation;
 using Questionable.Validation.Validators;
index 30a705e691663708c277478679e62cc23d295187..dddc8f9db6f345d3c8ba4fdfd6d409ffa8d0f63e 100644 (file)
@@ -2,7 +2,7 @@
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps.Common;
 
index 159d5c123bb7f21d6fe3074a4b0ab3c70c33aebb..287f029f8fef2cb0fc10b868c8f59605262dbd58 100644 (file)
@@ -1,6 +1,6 @@
 using System.Collections.Generic;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps;
 
index 94125e43a67700d4559f8ef664adc9c0f817f57b..000811ca66de29df46933c0dcf752066443e8c49 100644 (file)
@@ -7,7 +7,7 @@ using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Steps.Common;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps.Interactions;
 
index 82b13d27855b904c40d9044c921a82915899a31e..3ae2c4b31365328115f6937c129cc21deba5c4a0 100644 (file)
@@ -4,7 +4,7 @@ using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Data;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps.Interactions;
 
index 87a4f348d0946a3e663151aea8fa3ca28efb31ea..b3219a6e5fe1fcfa06ee5c078138322da359a7de 100644 (file)
@@ -1,9 +1,9 @@
 using System;
-using FFXIVClientStructs.FFXIV.Client.Game.Object;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Common;
+using Questionable.Model.Questing;
 using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
 
 namespace Questionable.Controller.Steps.Interactions;
index a6bccc4caeeae1ebdd91905ccfacfe87f291ff88..c2cab7df423b72fb194ddfea9c56015b3b52b29b 100644 (file)
@@ -2,7 +2,8 @@
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Common;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps.Interactions;
 
index 97201b4656f87240150028bde3786682021a0e02..2d7de4022fd5e75488abb81d827d128b321eb947 100644 (file)
@@ -6,7 +6,7 @@ using Questionable.Controller.Steps.Common;
 using Questionable.Controller.Steps.Shared;
 using Questionable.Controller.Utils;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps.Interactions;
 
index 05e9cf443bb986b86bf262306a6a60e940cc059c..3976eb73f958fa4fd50ea4d82fa98e70e67dec2b 100644 (file)
@@ -12,7 +12,7 @@ using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Steps.Common;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps.Interactions;
 
index a7de058c8c42461de44531ffb4989ec51d50e8bb..ab2afdcd1fa97f4bbc67135a77661cd8f79ab7f5 100644 (file)
@@ -3,7 +3,7 @@ using Dalamud.Game.ClientState.Conditions;
 using Dalamud.Plugin.Services;
 using Microsoft.Extensions.DependencyInjection;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps.Interactions;
 
index 44478475e137cba80d57927d2db2a24d46d18309..0a5e9064994f45bfa5f7c0c087f66156aa46a6a9 100644 (file)
@@ -3,7 +3,7 @@ using System.Collections.Generic;
 using Microsoft.Extensions.DependencyInjection;
 using Questionable.Controller.Steps.Common;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps.Interactions;
 
index 9322563df90ec0c527a3635a416c8bfe403c421f..95aad266795fe2be5d912c9cf64c1a5a2d19db6e 100644 (file)
@@ -6,7 +6,7 @@ using FFXIVClientStructs.FFXIV.Client.Game;
 using Lumina.Excel.GeneratedSheets;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 using Quest = Questionable.Model.Quest;
 
 namespace Questionable.Controller.Steps.Interactions;
index f49394dec0595650aad7cbe6ee4c2c37a7dcd1b1..7dba0d44f8abf6112d5ba93b7a1e0dde2d077759 100644 (file)
@@ -8,7 +8,7 @@ using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Steps.Shared;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps.Interactions;
 
index 5c3d9addc0f093e5de1f92488cdeb78d682ade71..0b0b099d7d0aa884a328b6e6356367689f87fe6d 100644 (file)
@@ -5,7 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Steps.Common;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps.Interactions;
 
index c833badc65077709b877607dbae9f2ed8dd27414..20fc7f673cb2fcd26ca5aad047cafe52f2c2fe52 100644 (file)
@@ -3,7 +3,7 @@ using System.Collections.Generic;
 using Microsoft.Extensions.DependencyInjection;
 using Questionable.Controller.Steps.Common;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps.Interactions;
 
index f7d5172a15aaad2109f81ce7922ec2b9a79a669b..648627c1ad22f560f27bde241255f9ef84183f23 100644 (file)
@@ -4,7 +4,7 @@ using Dalamud.Plugin.Services;
 using Microsoft.Extensions.DependencyInjection;
 using Questionable.External;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps.Interactions;
 
index 3e1212f8a8508eae43b74bd3c3a3447f09396470..e17f005e4a90e39cf1b2c18a742470b67ed2213a 100644 (file)
@@ -13,7 +13,8 @@ using Questionable.Controller.Steps.Shared;
 using Questionable.Controller.Utils;
 using Questionable.Data;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Common;
+using Questionable.Model.Questing;
 using AethernetShortcut = Questionable.Controller.Steps.Shared.AethernetShortcut;
 
 namespace Questionable.Controller.Steps.Interactions;
index df7f5788f36e080ffd25a536a83c6e66dc62040e..a66e7c0f5f3c15c5da5892a400729c7932272e93 100644 (file)
@@ -8,8 +8,10 @@ using Microsoft.Extensions.Logging;
 using Questionable.Data;
 using Questionable.External;
 using Questionable.Model;
-using Questionable.Model.V1;
-using Questionable.Model.V1.Converter;
+using Questionable.Model.Common;
+using Questionable.Model.Common.Converter;
+using Questionable.Model.Questing;
+using Questionable.Model.Questing.Converter;
 
 namespace Questionable.Controller.Steps.Shared;
 
index 413dd37a3000e16e92ac0a15ee454c1b780df177..ed8d1c07ad65baea3e13304bf99c06c8521e9dec 100644 (file)
@@ -7,7 +7,8 @@ using Microsoft.Extensions.Logging;
 using Questionable.Controller.Steps.Common;
 using Questionable.Data;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Common;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps.Shared;
 
index e74746881e0d4841ade914e2fcec21c085c8473d..24d3e7f88da25a03d5b14c2c92d36541e3ff9dd0 100644 (file)
@@ -13,7 +13,7 @@ using Questionable.Controller.NavigationOverrides;
 using Questionable.Controller.Steps.Common;
 using Questionable.Data;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps.Shared;
 
index 2f768ecdd74265e3253bc132ba53ee2958b12f2b..5d50ba183ee4167db57025f4523a101587ba46bc 100644 (file)
@@ -11,7 +11,8 @@ using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Utils;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Common;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps.Shared;
 
index 16bb83650668e69c0cc3d58cff47b544a68f8740..609f3143e337e6fb25f972e5fba4c8245f493ad8 100644 (file)
@@ -2,7 +2,7 @@
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps.Shared;
 
index 0d44663ce2df4f8487fd0de31e538868c4826b8b..6d54623e0a31b1bebf4a61767533d1ce8ea4b76e 100644 (file)
@@ -12,7 +12,7 @@ using Questionable.Controller.Steps.Common;
 using Questionable.Controller.Utils;
 using Questionable.Data;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps.Shared;
 
index 4a1f411eb6906a3f15167c323ab636f67216b24b..b39e301dde277ef35671bff8b9e23c8fef55a700 100644 (file)
@@ -2,7 +2,7 @@
 using Microsoft.Extensions.DependencyInjection;
 using Questionable.Controller.Steps.Common;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Steps.Shared;
 
index 6bb771806c6250085f1acbd353bee679ad558e91..5923591810bf724192b09ca754873da6ea49bd4d 100644 (file)
@@ -4,7 +4,7 @@ using System.Linq;
 using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
 using Microsoft.Extensions.Logging;
 using Questionable.Controller.Steps.Shared;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Controller.Utils;
 
index 9e216cdcdb75391305927d7f974ab9c3cae35d42..28d8e459d39aba6dcbb2d19054cb7477f79a0862 100644 (file)
@@ -4,8 +4,7 @@ using System.Linq;
 using System.Numerics;
 using Dalamud.Plugin.Services;
 using Lumina.Excel.GeneratedSheets;
-using Microsoft.Extensions.Logging;
-using Questionable.Model.V1;
+using Questionable.Model.Common;
 
 namespace Questionable.Data;
 
index deab3a247b401c164f902f8d0ffd89ea4ea9a38a..f326770902db124a75b673f1a8858bc2fa1d5d56 100644 (file)
@@ -1,7 +1,7 @@
 using Dalamud.Plugin;
 using Dalamud.Plugin.Ipc;
 using Questionable.Data;
-using Questionable.Model.V1;
+using Questionable.Model.Common;
 
 namespace Questionable.External;
 
index 6487eb98d38d54766d513012b003fe7505722d93..724f2f11e948abdff9b43cb1be2624b8f296ab1f 100644 (file)
@@ -24,7 +24,8 @@ using Microsoft.Extensions.Logging;
 using Questionable.Controller;
 using Questionable.Data;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Common;
+using Questionable.Model.Questing;
 using Action = Lumina.Excel.GeneratedSheets2.Action;
 using BattleChara = FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara;
 using ContentFinderCondition = Lumina.Excel.GeneratedSheets.ContentFinderCondition;
index a33bcd181274e5f556e9e50051e89ea0caa54890..5124de9dd733eed4e4c92b78a384f4b36276ed96 100644 (file)
@@ -1,6 +1,6 @@
 using System.Collections.Generic;
 using System.Linq;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Model;
 
index 36643c3e7074ce8b5028728c9809656622cf70fb..e620d8a54da39bf94ffb9e883f7a4a69ca744fbb 100644 (file)
@@ -2,7 +2,7 @@
 using System.Linq;
 using Questionable.Data;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Validation.Validators;
 
index 0c10e032808426aaf0fe188d83da0002151b6e27..0127bc195e24e226fb62c39de2e56bd903646d48 100644 (file)
@@ -1,7 +1,7 @@
 using System.Collections.Generic;
 using System.Linq;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Validation.Validators;
 
index 1d36c8c4b5cf7a48bc7c856f9e6cc22f3fffdf65..2cfc58d7e2301172ac701f889a77b78f87c04c5f 100644 (file)
@@ -3,7 +3,7 @@ using System.Linq;
 using System.Numerics;
 using Questionable.Controller.Utils;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Validation.Validators;
 
index b3ed6f4ec8f3d822bd5698a64f9043a5ad9fe986..7cacc520acc839e9521978f28c410b61032e05e1 100644 (file)
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using System.Globalization;
 using System.Text.Json.Nodes;
 using Json.Schema;
@@ -12,6 +13,13 @@ internal sealed class JsonSchemaValidator : IQuestValidator
     private readonly Dictionary<ushort, JsonNode> _questNodes = new();
     private JsonSchema? _questSchema;
 
+    public JsonSchemaValidator()
+    {
+        SchemaRegistry.Global.Register(
+            new Uri("https://git.carvel.li/liza/Questionable/raw/branch/master/Questionable.Model/common-schema.json"),
+            JsonSchema.FromStream(AssemblyModelLoader.CommonSchema).AsTask().Result);
+    }
+
     public IEnumerable<ValidationIssue> Validate(Quest quest)
     {
         _questSchema ??= JsonSchema.FromStream(AssemblyQuestLoader.QuestSchema).AsTask().Result;
@@ -36,7 +44,6 @@ internal sealed class JsonSchemaValidator : IQuestValidator
                 };
             }
         }
-
     }
 
     public void Enqueue(ushort questId, JsonNode questNode) => _questNodes[questId] = questNode;
index b95522f1f85a7dd9ace112960f9ec3972a7ab947..b385306d5a3f77f4dd9d39c1f15cb9ba1dbaeaf6 100644 (file)
@@ -1,7 +1,7 @@
 using System.Collections.Generic;
 using System.Linq;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Validation.Validators;
 
index 20b572fe24a04d1e44b1f1e890e684fadcaf17f0..e7cc34e791bde3f8257c9f71fffed873d5d98f00 100644 (file)
@@ -11,7 +11,7 @@ using ImGuiNET;
 using Questionable.Controller;
 using Questionable.Data;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Windows;
 
index f819526a1afee08e90185b86c807fe52ba670b60..14b628d3f2bafe726ba7b42c72cbc81e984d826c 100644 (file)
@@ -13,7 +13,7 @@ using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
 using ImGuiNET;
 using Questionable.Controller;
 using Questionable.Controller.Steps.Shared;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 
 namespace Questionable.Windows.QuestComponents;
 
index a1fac0a4bbe733ba24c7b7886b01851bb1f03dfc..b824f1af28091991ce2ad471db58f4d8dd2bc68b 100644 (file)
@@ -16,7 +16,8 @@ using Microsoft.Extensions.Logging;
 using Questionable.Controller;
 using Questionable.Data;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Common;
+using Questionable.Model.Questing;
 using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
 
 namespace Questionable.Windows.QuestComponents;
@@ -160,23 +161,38 @@ internal sealed class CreationUtilsComponent
                     "Left click: Copy target position as JSON.\nRight click: Copy target position as C# code.");
             if (copy)
             {
-                string interactionType = gameObject->NamePlateIconId switch
+                var target = _targetManager.Target;
+                if (target.ObjectKind == ObjectKind.GatheringPoint)
                 {
-                    71201 or 71211 or 71221 or 71231 or 71341 or 71351 => "AcceptQuest",
-                    71202 or 71212 or 71222 or 71232 or 71342 or 71352 => "AcceptQuest", // repeatable
-                    71205 or 71215 or 71225 or 71235 or 71345 or 71355 => "CompleteQuest",
-                    _ => "Interact",
-                };
-                ImGui.SetClipboardText($$"""
-                                         "DataId": {{_targetManager.Target.DataId}},
-                                         "Position": {
-                                             "X": {{_targetManager.Target.Position.X.ToString(CultureInfo.InvariantCulture)}},
-                                             "Y": {{_targetManager.Target.Position.Y.ToString(CultureInfo.InvariantCulture)}},
-                                             "Z": {{_targetManager.Target.Position.Z.ToString(CultureInfo.InvariantCulture)}}
-                                         },
-                                         "TerritoryId": {{_clientState.TerritoryType}},
-                                         "InteractionType": "{{interactionType}}"
-                                         """);
+                    ImGui.SetClipboardText($$"""
+                                             "DataId": {{target.DataId}},
+                                             "Position": {
+                                                 "X": {{target.Position.X.ToString(CultureInfo.InvariantCulture)}},
+                                                 "Y": {{target.Position.Y.ToString(CultureInfo.InvariantCulture)}},
+                                                 "Z": {{target.Position.Z.ToString(CultureInfo.InvariantCulture)}}
+                                             }
+                                             """);
+                }
+                else
+                {
+                    string interactionType = gameObject->NamePlateIconId switch
+                    {
+                        71201 or 71211 or 71221 or 71231 or 71341 or 71351 => "AcceptQuest",
+                        71202 or 71212 or 71222 or 71232 or 71342 or 71352 => "AcceptQuest", // repeatable
+                        71205 or 71215 or 71225 or 71235 or 71345 or 71355 => "CompleteQuest",
+                        _ => "Interact",
+                    };
+                    ImGui.SetClipboardText($$"""
+                                             "DataId": {{target.DataId}},
+                                             "Position": {
+                                                 "X": {{target.Position.X.ToString(CultureInfo.InvariantCulture)}},
+                                                 "Y": {{target.Position.Y.ToString(CultureInfo.InvariantCulture)}},
+                                                 "Z": {{target.Position.Z.ToString(CultureInfo.InvariantCulture)}}
+                                             },
+                                             "TerritoryId": {{_clientState.TerritoryType}},
+                                             "InteractionType": "{{interactionType}}"
+                                             """);
+                }
             }
             else if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
             {
index 6f4207860c5493131306d6dc8ace6af29fafc590..8da2c2d080c28afa938b50bda960359c25def86d 100644 (file)
@@ -19,7 +19,7 @@ using LLib.ImGui;
 using Questionable.Controller;
 using Questionable.Data;
 using Questionable.Model;
-using Questionable.Model.V1;
+using Questionable.Model.Questing;
 using Questionable.Windows.QuestComponents;
 
 namespace Questionable.Windows;