Update how quests are embedded
authorLiza Carvelli <liza@carvel.li>
Fri, 14 Jun 2024 09:37:33 +0000 (11:37 +0200)
committerLiza Carvelli <liza@carvel.li>
Fri, 14 Jun 2024 09:37:33 +0000 (11:37 +0200)
64 files changed:
.gitignore
QuestPathGenerator/QuestPathGenerator.csproj [new file with mode: 0644]
QuestPathGenerator/QuestSourceGenerator.cs [new file with mode: 0644]
QuestPathGenerator/RoslynShortcuts.cs [new file with mode: 0644]
QuestPathGenerator/packages.lock.json [new file with mode: 0644]
QuestPaths/AssemblyQuestLoader.cs
QuestPaths/QuestPaths.csproj
QuestPaths/packages.lock.json
Questionable.Model/Questionable.Model.csproj [new file with mode: 0644]
Questionable.Model/V1/AethernetShortcut.cs [new file with mode: 0644]
Questionable.Model/V1/ChatMessage.cs [new file with mode: 0644]
Questionable.Model/V1/Converter/AethernetShortcutConverter.cs [new file with mode: 0644]
Questionable.Model/V1/Converter/AetheryteConverter.cs [new file with mode: 0644]
Questionable.Model/V1/Converter/DialogueChoiceTypeConverter.cs [new file with mode: 0644]
Questionable.Model/V1/Converter/EmoteConverter.cs [new file with mode: 0644]
Questionable.Model/V1/Converter/EnemySpawnTypeConverter.cs [new file with mode: 0644]
Questionable.Model/V1/Converter/EnumConverter.cs [new file with mode: 0644]
Questionable.Model/V1/Converter/ExcelRefConverter.cs [new file with mode: 0644]
Questionable.Model/V1/Converter/InteractionTypeConverter.cs [new file with mode: 0644]
Questionable.Model/V1/Converter/SkipConditionConverter.cs [new file with mode: 0644]
Questionable.Model/V1/Converter/VectorConverter.cs [new file with mode: 0644]
Questionable.Model/V1/DialogueChoice.cs [new file with mode: 0644]
Questionable.Model/V1/EAetheryteLocation.cs [new file with mode: 0644]
Questionable.Model/V1/EDialogChoiceType.cs [new file with mode: 0644]
Questionable.Model/V1/EEmote.cs [new file with mode: 0644]
Questionable.Model/V1/EEnemySpawnType.cs [new file with mode: 0644]
Questionable.Model/V1/EInteractionType.cs [new file with mode: 0644]
Questionable.Model/V1/ESkipCondition.cs [new file with mode: 0644]
Questionable.Model/V1/ExcelRef.cs [new file with mode: 0644]
Questionable.Model/V1/JumpDestination.cs [new file with mode: 0644]
Questionable.Model/V1/QuestData.cs [new file with mode: 0644]
Questionable.Model/V1/QuestSequence.cs [new file with mode: 0644]
Questionable.Model/V1/QuestStep.cs [new file with mode: 0644]
Questionable.Model/packages.lock.json [new file with mode: 0644]
Questionable.sln
Questionable/.gitignore
Questionable/Controller/QuestRegistry.cs
Questionable/Model/V1/AethernetShortcut.cs [deleted file]
Questionable/Model/V1/ChatMessage.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/SkipConditionConverter.cs [deleted file]
Questionable/Model/V1/Converter/VectorConverter.cs [deleted file]
Questionable/Model/V1/DialogueChoice.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/EInteractionType.cs [deleted file]
Questionable/Model/V1/ESkipCondition.cs [deleted file]
Questionable/Model/V1/ExcelRef.cs [deleted file]
Questionable/Model/V1/JumpDestination.cs [deleted file]
Questionable/Model/V1/QuestData.cs [deleted file]
Questionable/Model/V1/QuestSequence.cs [deleted file]
Questionable/Model/V1/QuestStep.cs [deleted file]
Questionable/Model/V1/QuestStepExtensions.cs [new file with mode: 0644]
Questionable/Questionable.csproj
Questionable/packages.lock.json

index 05dc54930855f84d6d6b87a09c16ad5a02723720..f0fb17c2a4f42b857a9f3914fccf67aff9065f81 100644 (file)
@@ -1,2 +1,5 @@
-/.idea
+obj/
+bin/
+/.idea
+/.vs
 *.user
diff --git a/QuestPathGenerator/QuestPathGenerator.csproj b/QuestPathGenerator/QuestPathGenerator.csproj
new file mode 100644 (file)
index 0000000..b62d287
--- /dev/null
@@ -0,0 +1,37 @@
+<Project Sdk="Microsoft.NET.Sdk">
+    <PropertyGroup>
+        <TargetFramework>netstandard2.0</TargetFramework>
+        <IsPackable>false</IsPackable>
+        <LangVersion>12</LangVersion>
+        <Nullable>enable</Nullable>
+        <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+        <RootNamespace>Questionable.QuestPathGenerator</RootNamespace>
+        <DebugType>portable</DebugType>
+
+        <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
+        <IsRoslynComponent>true</IsRoslynComponent>
+
+        <PackageId>QuestPathGenerator</PackageId>
+        <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
+    </PropertyGroup>
+
+    <ItemGroup>
+        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
+            <PrivateAssets>all</PrivateAssets>
+            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+        </PackageReference>
+        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2"/>
+        <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.9.2"/>
+        <PackageReference Include="System.Text.Json" Version="8.0.3" PrivateAssets="all"/>
+    </ItemGroup>
+
+    <ItemGroup>
+        <ProjectReference Include="..\Questionable.Model\Questionable.Model.csproj" />
+    </ItemGroup>
+
+    <Target Name="GetDependencyTargetPaths" AfterTargets="ResolvePackageDependenciesForBuild">
+        <ItemGroup>
+            <TargetPathWithTargetPlatformMoniker Include="..\Questionable.Model\$(OutputPath)\*.dll" IncludeRuntimeDependency="false" />
+        </ItemGroup>
+    </Target>
+</Project>
diff --git a/QuestPathGenerator/QuestSourceGenerator.cs b/QuestPathGenerator/QuestSourceGenerator.cs
new file mode 100644 (file)
index 0000000..9bc6cc2
--- /dev/null
@@ -0,0 +1,316 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Questionable.Model.V1;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+using static Questionable.QuestPathGenerator.RoslynShortcuts;
+
+namespace Questionable.QuestPathGenerator;
+
+/// <summary>
+/// A sample source generator that creates C# classes based on the text file (in this case, Domain Driven Design ubiquitous language registry).
+/// When using a simple text file as a baseline, we can create a non-incremental source generator.
+/// </summary>
+[Generator]
+public class QuestSourceGenerator : ISourceGenerator
+{
+    public void Initialize(GeneratorInitializationContext context)
+    {
+        // No initialization required for this generator.
+    }
+
+    public void Execute(GeneratorExecutionContext context)
+    {
+        List<(ushort, QuestData)> quests = [];
+
+        // Go through all files marked as an Additional File in file properties.
+        foreach (var additionalFile in context.AdditionalFiles)
+        {
+            if (additionalFile == null)
+                continue;
+
+            if (Path.GetExtension(additionalFile.Path) != ".json")
+                continue;
+
+            string name = Path.GetFileName(additionalFile.Path);
+            ushort id = ushort.Parse(name.Substring(0, name.IndexOf('_')));
+
+            var text = additionalFile.GetText();
+            if (text == null)
+                continue;
+
+            var quest = JsonSerializer.Deserialize<QuestData>(text.ToString())!;
+            quests.Add((id, quest));
+        }
+
+        quests = quests.OrderBy(x => x.Item1).ToList();
+
+        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("V1")))
+                        }))
+                .WithMembers(
+                    SingletonList<MemberDeclarationSyntax>(
+                        FileScopedNamespaceDeclaration(
+                                QualifiedName(
+                                    IdentifierName("Questionable"),
+                                    IdentifierName("QuestPaths")))
+                            .WithMembers(
+                                SingletonList<MemberDeclarationSyntax>(
+                                    ClassDeclaration("AssemblyQuestLoader")
+                                        .WithModifiers(
+                                            TokenList(
+                                            [
+                                                Token(SyntaxKind.PartialKeyword)
+                                            ]))
+                                        .WithMembers(
+                                            SingletonList<MemberDeclarationSyntax>(
+                                                FieldDeclaration(
+                                                        VariableDeclaration(
+                                                                GenericName(
+                                                                        Identifier("IReadOnlyDictionary"))
+                                                                    .WithTypeArgumentList(
+                                                                        TypeArgumentList(
+                                                                            SeparatedList<TypeSyntax>(
+                                                                                new SyntaxNodeOrToken[]
+                                                                                {
+                                                                                    PredefinedType(
+                                                                                        Token(SyntaxKind
+                                                                                            .UShortKeyword)),
+                                                                                    Token(SyntaxKind.CommaToken),
+                                                                                    IdentifierName("QuestData")
+                                                                                }))))
+                                                            .WithVariables(
+                                                                SingletonSeparatedList(
+                                                                    VariableDeclarator(
+                                                                            Identifier("Quests"))
+                                                                        .WithInitializer(
+                                                                            EqualsValueClause(
+                                                                                ObjectCreationExpression(
+                                                                                        GenericName(
+                                                                                                Identifier(
+                                                                                                    "Dictionary"))
+                                                                                            .WithTypeArgumentList(
+                                                                                                TypeArgumentList(
+                                                                                                    SeparatedList<
+                                                                                                        TypeSyntax>(
+                                                                                                        new
+                                                                                                            SyntaxNodeOrToken
+                                                                                                            []
+                                                                                                            {
+                                                                                                                PredefinedType(
+                                                                                                                    Token(
+                                                                                                                        SyntaxKind
+                                                                                                                            .UShortKeyword)),
+                                                                                                                Token(
+                                                                                                                    SyntaxKind
+                                                                                                                        .CommaToken),
+                                                                                                                IdentifierName(
+                                                                                                                    "QuestData")
+                                                                                                            }))))
+                                                                                    .WithArgumentList(
+                                                                                        ArgumentList())
+                                                                                    .WithInitializer(
+                                                                                        InitializerExpression(
+                                                                                            SyntaxKind
+                                                                                                .CollectionInitializerExpression,
+                                                                                            SeparatedList<
+                                                                                                ExpressionSyntax>(
+                                                                                                quests.SelectMany(x =>
+                                                                                                    CreateQuestInitializer(
+                                                                                                            x.Item1,
+                                                                                                            x.Item2)
+                                                                                                        .ToArray())))))))))
+                                                    .WithModifiers(
+                                                        TokenList(
+                                                        [
+                                                            Token(SyntaxKind.InternalKeyword),
+                                                            Token(SyntaxKind.StaticKeyword)
+                                                        ]))))))))
+                .NormalizeWhitespace();
+
+        // Add the source code to the compilation.
+        context.AddSource("AssemblyQuestLoader.g.cs", code.ToFullString());
+    }
+
+    private static IEnumerable<SyntaxNodeOrToken> CreateQuestInitializer(ushort questId, QuestData quest)
+    {
+        return new SyntaxNodeOrToken[]
+        {
+            InitializerExpression(
+                SyntaxKind.ComplexElementInitializerExpression,
+                SeparatedList<ExpressionSyntax>(
+                    new SyntaxNodeOrToken[]
+                    {
+                        LiteralExpression(
+                            SyntaxKind.NumericLiteralExpression,
+                            Literal(questId)),
+                        Token(SyntaxKind.CommaToken),
+                        ObjectCreationExpression(
+                                IdentifierName(nameof(QuestData)))
+                            .WithInitializer(
+                                InitializerExpression(
+                                    SyntaxKind.ObjectInitializerExpression,
+                                    SeparatedList<ExpressionSyntax>(
+                                        SyntaxNodeList(
+                                            Assignment(nameof(QuestData.Author), quest.Author, null)
+                                                .AsSyntaxNodeOrToken(),
+                                            AssignmentList(nameof(QuestData.Contributors), quest.Contributors)
+                                                .AsSyntaxNodeOrToken(),
+                                            Assignment(nameof(QuestData.Comment), quest.Comment, null)
+                                                .AsSyntaxNodeOrToken(),
+                                            AssignmentList(nameof(QuestData.TerritoryBlacklist),
+                                                quest.TerritoryBlacklist).AsSyntaxNodeOrToken(),
+                                            AssignmentExpression(
+                                                SyntaxKind.SimpleAssignmentExpression,
+                                                IdentifierName(nameof(QuestData.QuestSequence)),
+                                                CreateQuestSequence(quest.QuestSequence))
+                                        ))))
+                    })),
+            Token(SyntaxKind.CommaToken)
+        };
+    }
+
+    private static ExpressionSyntax CreateQuestSequence(List<QuestSequence> sequences)
+    {
+        return CollectionExpression(
+            SeparatedList<CollectionElementSyntax>(
+                sequences.SelectMany(sequence => new SyntaxNodeOrToken[]
+                {
+                    ExpressionElement(
+                        ObjectCreationExpression(
+                                IdentifierName(nameof(QuestSequence)))
+                            .WithInitializer(
+                                InitializerExpression(
+                                    SyntaxKind.ObjectInitializerExpression,
+                                    SeparatedList<ExpressionSyntax>(
+                                        SyntaxNodeList(
+                                            Assignment<int?>(nameof(QuestSequence.Sequence), sequence.Sequence, null)
+                                                .AsSyntaxNodeOrToken(),
+                                            Assignment(nameof(QuestSequence.Comment), sequence.Comment, null)
+                                                .AsSyntaxNodeOrToken(),
+                                            AssignmentExpression(
+                                                SyntaxKind.SimpleAssignmentExpression,
+                                                IdentifierName(nameof(QuestSequence.Steps)),
+                                                CreateQuestSteps(sequence.Steps))))))),
+                    Token(SyntaxKind.CommaToken),
+                }.ToArray())));
+    }
+
+    private static ExpressionSyntax CreateQuestSteps(List<QuestStep> steps)
+    {
+        QuestStep emptyStep = new();
+        return CollectionExpression(
+            SeparatedList<CollectionElementSyntax>(
+                steps.SelectMany(step => new SyntaxNodeOrToken[]
+                {
+                    ExpressionElement(
+                        ObjectCreationExpression(
+                                IdentifierName(nameof(QuestStep)))
+                            .WithArgumentList(
+                                ArgumentList(
+                                    SeparatedList<ArgumentSyntax>(
+                                        new SyntaxNodeOrToken[]
+                                        {
+                                            Argument(LiteralValue(step.InteractionType)),
+                                            Token(SyntaxKind.CommaToken),
+                                            Argument(LiteralValue(step.DataId)),
+                                            Token(SyntaxKind.CommaToken),
+                                            Argument(LiteralValue(step.Position)),
+                                            Token(SyntaxKind.CommaToken),
+                                            Argument(LiteralValue(step.TerritoryId))
+                                        })))
+                            .WithInitializer(
+                                InitializerExpression(
+                                    SyntaxKind.ObjectInitializerExpression,
+                                    SeparatedList<ExpressionSyntax>(
+                                        SyntaxNodeList(
+                                            Assignment(nameof(QuestStep.StopDistance), step.StopDistance,
+                                                    emptyStep.StopDistance)
+                                                .AsSyntaxNodeOrToken(),
+                                            Assignment(nameof(QuestStep.TargetTerritoryId), step.TargetTerritoryId,
+                                                    emptyStep.TargetTerritoryId)
+                                                .AsSyntaxNodeOrToken(),
+                                            Assignment(nameof(QuestStep.Disabled), step.Disabled, emptyStep.Disabled)
+                                                .AsSyntaxNodeOrToken(),
+                                            Assignment(nameof(QuestStep.DisableNavmesh), step.DisableNavmesh,
+                                                    emptyStep.DisableNavmesh)
+                                                .AsSyntaxNodeOrToken(),
+                                            Assignment(nameof(QuestStep.Mount), step.Mount, emptyStep.Mount)
+                                                .AsSyntaxNodeOrToken(),
+                                            Assignment(nameof(QuestStep.Fly), step.Fly, emptyStep.Fly)
+                                                .AsSyntaxNodeOrToken(),
+                                            Assignment(nameof(QuestStep.Sprint), step.Sprint, emptyStep.Sprint)
+                                                .AsSyntaxNodeOrToken(),
+                                            Assignment(nameof(QuestStep.Comment), step.Comment, emptyStep.Comment)
+                                                .AsSyntaxNodeOrToken(),
+                                            Assignment(nameof(QuestStep.AetheryteShortcut), step.AetheryteShortcut,
+                                                    emptyStep.AetheryteShortcut)
+                                                .AsSyntaxNodeOrToken(),
+                                            Assignment(nameof(QuestStep.AethernetShortcut), step.AethernetShortcut,
+                                                    emptyStep.AethernetShortcut)
+                                                .AsSyntaxNodeOrToken(),
+                                            Assignment(nameof(QuestStep.AetherCurrentId), step.AetherCurrentId,
+                                                    emptyStep.AetherCurrentId)
+                                                .AsSyntaxNodeOrToken(),
+                                            Assignment(nameof(QuestStep.ItemId), step.ItemId, emptyStep.ItemId)
+                                                .AsSyntaxNodeOrToken(),
+                                            Assignment(nameof(QuestStep.GroundTarget), step.GroundTarget,
+                                                    emptyStep.GroundTarget)
+                                                .AsSyntaxNodeOrToken(),
+                                            Assignment(nameof(QuestStep.Emote), step.Emote, emptyStep.Emote)
+                                                .AsSyntaxNodeOrToken(),
+                                            Assignment(nameof(QuestStep.ChatMessage), step.ChatMessage,
+                                                    emptyStep.ChatMessage)
+                                                .AsSyntaxNodeOrToken(),
+                                            Assignment(nameof(QuestStep.EnemySpawnType), step.EnemySpawnType,
+                                                    emptyStep.EnemySpawnType)
+                                                .AsSyntaxNodeOrToken(),
+                                            AssignmentList(nameof(QuestStep.KillEnemyDataIds), step.KillEnemyDataIds)
+                                                .AsSyntaxNodeOrToken(),
+                                            Assignment(nameof(QuestStep.JumpDestination), step.JumpDestination,
+                                                    emptyStep.JumpDestination)
+                                                .AsSyntaxNodeOrToken(),
+                                            Assignment(nameof(QuestStep.ContentFinderConditionId),
+                                                    step.ContentFinderConditionId, emptyStep.ContentFinderConditionId)
+                                                .AsSyntaxNodeOrToken(),
+                                            AssignmentList(nameof(QuestStep.SkipIf), step.SkipIf)
+                                                .AsSyntaxNodeOrToken(),
+                                            AssignmentList(nameof(QuestStep.CompletionQuestVariablesFlags),
+                                                    step.CompletionQuestVariablesFlags)
+                                                .AsSyntaxNodeOrToken(),
+                                            AssignmentList(nameof(QuestStep.DialogueChoices), step.DialogueChoices)
+                                                .AsSyntaxNodeOrToken()))))),
+                    Token(SyntaxKind.CommaToken),
+                }.ToArray())));
+    }
+}
diff --git a/QuestPathGenerator/RoslynShortcuts.cs b/QuestPathGenerator/RoslynShortcuts.cs
new file mode 100644 (file)
index 0000000..545cb27
--- /dev/null
@@ -0,0 +1,208 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Questionable.Model.V1;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+
+namespace Questionable.QuestPathGenerator;
+
+public static class RoslynShortcuts
+{
+    public static IEnumerable<SyntaxNodeOrToken> SyntaxNodeList(params SyntaxNodeOrToken?[] nodes)
+    {
+        nodes = nodes.Where(x => x != null).ToArray();
+        if (nodes.Length == 0)
+            return [];
+
+        List<SyntaxNodeOrToken> list = new();
+        for (int i = 0; i < nodes.Length; ++i)
+        {
+            if (i > 0)
+                list.Add(Token(SyntaxKind.CommaToken));
+            list.Add(nodes[i].GetValueOrDefault());
+        }
+
+        return list;
+    }
+
+    public static ExpressionSyntax LiteralValue<T>(T? value)
+    {
+        if (value is string s)
+            return LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(s));
+        else if (value is bool b)
+            return LiteralExpression(b ? SyntaxKind.TrueLiteralExpression : SyntaxKind.FalseLiteralExpression);
+        else if (value is short i16)
+            return LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(i16));
+        else if (value is int i32)
+            return LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(i32));
+        else if (value is ushort u16)
+            return LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(u16));
+        else if (value is uint u32)
+            return LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(u32));
+        else if (value is float f)
+            return LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(f));
+        else if (value != null && value.GetType().IsEnum)
+            return MemberAccessExpression(
+                SyntaxKind.SimpleMemberAccessExpression,
+                IdentifierName(value.GetType().Name),
+                IdentifierName(value.GetType().GetEnumName(value)!));
+        else if (value is Vector3 vector)
+        {
+            return ObjectCreationExpression(
+                    IdentifierName(nameof(Vector3)))
+                .WithArgumentList(
+                    ArgumentList(
+                        SeparatedList<ArgumentSyntax>(
+                            new SyntaxNodeOrToken[]
+                            {
+                                Argument(LiteralValue(vector.X)),
+                                Token(SyntaxKind.CommaToken),
+                                Argument(LiteralValue(vector.Y)),
+                                Token(SyntaxKind.CommaToken),
+                                Argument(LiteralValue(vector.Z))
+                            })));
+        }
+        else if (value is AethernetShortcut aethernetShortcut)
+        {
+            return ObjectCreationExpression(
+                    IdentifierName(nameof(AethernetShortcut)))
+                .WithInitializer(
+                    InitializerExpression(
+                        SyntaxKind.ObjectInitializerExpression,
+                        SeparatedList<ExpressionSyntax>(
+                            SyntaxNodeList(
+                                Assignment<EAetheryteLocation?>(nameof(AethernetShortcut.From), aethernetShortcut.From,
+                                        null)
+                                    .AsSyntaxNodeOrToken(),
+                                Assignment<EAetheryteLocation?>(nameof(AethernetShortcut.To), aethernetShortcut.To,
+                                        null)
+                                    .AsSyntaxNodeOrToken()))));
+        }
+        else if (value is ChatMessage chatMessage)
+        {
+            ChatMessage emptyMessage = new();
+            return ObjectCreationExpression(
+                    IdentifierName(nameof(ChatMessage)))
+                .WithInitializer(
+                    InitializerExpression(
+                        SyntaxKind.ObjectInitializerExpression,
+                        SeparatedList<ExpressionSyntax>(
+                            SyntaxNodeList(
+                                Assignment(nameof(ChatMessage.ExcelSheet), chatMessage.ExcelSheet,
+                                        emptyMessage.ExcelSheet)
+                                    .AsSyntaxNodeOrToken(),
+                                Assignment(nameof(ChatMessage.Key), chatMessage.Key,
+                                        emptyMessage.Key)
+                                    .AsSyntaxNodeOrToken()))));
+        }
+        else if (value is DialogueChoice dialogueChoice)
+        {
+            DialogueChoice emptyChoice = new();
+            return ObjectCreationExpression(
+                    IdentifierName(nameof(DialogueChoice)))
+                .WithInitializer(
+                    InitializerExpression(
+                        SyntaxKind.ObjectInitializerExpression,
+                        SeparatedList<ExpressionSyntax>(
+                            SyntaxNodeList(
+                                Assignment<EDialogChoiceType?>(nameof(DialogueChoice.Type), dialogueChoice.Type, null)
+                                    .AsSyntaxNodeOrToken(),
+                                Assignment(nameof(DialogueChoice.ExcelSheet), dialogueChoice.ExcelSheet,
+                                        emptyChoice.ExcelSheet)
+                                    .AsSyntaxNodeOrToken(),
+                                Assignment(nameof(DialogueChoice.Prompt), dialogueChoice.Prompt, emptyChoice.Prompt)
+                                    .AsSyntaxNodeOrToken(),
+                                Assignment(nameof(DialogueChoice.Yes), dialogueChoice.Yes, emptyChoice.Yes)
+                                    .AsSyntaxNodeOrToken(),
+                                Assignment(nameof(DialogueChoice.Answer), dialogueChoice.Answer, emptyChoice.Answer)
+                                    .AsSyntaxNodeOrToken(),
+                                Assignment(nameof(DialogueChoice.DataId), dialogueChoice.DataId, emptyChoice.DataId)
+                                    .AsSyntaxNodeOrToken()))));
+        }
+        else if (value is JumpDestination jumpDestination)
+        {
+            return ObjectCreationExpression(
+                    IdentifierName(nameof(JumpDestination)))
+                .WithInitializer(
+                    InitializerExpression(
+                        SyntaxKind.ObjectInitializerExpression,
+                        SeparatedList<ExpressionSyntax>(
+                            SyntaxNodeList(
+                                Assignment<Vector3?>(nameof(JumpDestination.Position), jumpDestination.Position, null)
+                                    .AsSyntaxNodeOrToken(),
+                                Assignment(nameof(JumpDestination.StopDistance), jumpDestination.StopDistance, null)
+                                    .AsSyntaxNodeOrToken(),
+                                Assignment(nameof(JumpDestination.DelaySeconds), jumpDestination.DelaySeconds, null)
+                                    .AsSyntaxNodeOrToken()))));
+        }
+        else if (value is ExcelRef excelRef)
+        {
+            if (excelRef.Type == ExcelRef.EType.Key)
+            {
+                return ObjectCreationExpression(
+                        IdentifierName(nameof(ExcelRef)))
+                    .WithArgumentList(
+                        ArgumentList(
+                            SingletonSeparatedList(
+                                Argument(LiteralValue(excelRef.AsKey())))));
+            }
+            else if (excelRef.Type == ExcelRef.EType.RowId)
+            {
+                return ObjectCreationExpression(
+                        IdentifierName(nameof(ExcelRef)))
+                    .WithArgumentList(
+                        ArgumentList(
+                            SingletonSeparatedList(
+                                Argument(LiteralValue(excelRef.AsRowId())))));
+            }
+            else
+                throw new Exception($"Unsupported ExcelRef type {excelRef.Type}");
+        }
+        else if (value is null)
+            return LiteralExpression(SyntaxKind.NullLiteralExpression);
+        else
+            throw new Exception($"Unsupported data type {value.GetType()} = {value}");
+    }
+
+    public static AssignmentExpressionSyntax? Assignment<T>(string name, T? value, T? defaultValue)
+    {
+        if (value == null && defaultValue == null)
+            return null;
+
+        if (value != null && defaultValue != null && value.Equals(defaultValue))
+            return null;
+
+        return AssignmentExpression(
+            SyntaxKind.SimpleAssignmentExpression,
+            IdentifierName(name),
+            LiteralValue(value));
+    }
+
+    public static AssignmentExpressionSyntax? AssignmentList<T>(string name, IEnumerable<T> value)
+    {
+        IEnumerable<T> list = value.ToList();
+        if (!list.Any())
+            return null;
+
+        return AssignmentExpression(
+            SyntaxKind.SimpleAssignmentExpression,
+            IdentifierName(name),
+            CollectionExpression(
+                SeparatedList<CollectionElementSyntax>(
+                    SyntaxNodeList(list.Select(x => ExpressionElement(
+                        LiteralValue(x)).AsSyntaxNodeOrToken()).ToArray())
+                )));
+    }
+
+    public static SyntaxNodeOrToken? AsSyntaxNodeOrToken(this SyntaxNode? node)
+    {
+        if (node == null)
+            return null;
+
+        return node;
+    }
+}
diff --git a/QuestPathGenerator/packages.lock.json b/QuestPathGenerator/packages.lock.json
new file mode 100644 (file)
index 0000000..a9d0508
--- /dev/null
@@ -0,0 +1,244 @@
+{
+  "version": 1,
+  "dependencies": {
+    ".NETStandard,Version=v2.0": {
+      "Microsoft.CodeAnalysis.Analyzers": {
+        "type": "Direct",
+        "requested": "[3.3.4, )",
+        "resolved": "3.3.4",
+        "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g=="
+      },
+      "Microsoft.CodeAnalysis.CSharp": {
+        "type": "Direct",
+        "requested": "[4.9.2, )",
+        "resolved": "4.9.2",
+        "contentHash": "HGIo7E9Mf3exAJbUdYpDFfLoYkSVaHDJXPyusWTYUTBaOPCowGw+Gap5McE1w+K+ryIXre72oiqL88sQHmHBmg==",
+        "dependencies": {
+          "Microsoft.CodeAnalysis.Common": "[4.9.2]"
+        }
+      },
+      "Microsoft.CodeAnalysis.CSharp.Workspaces": {
+        "type": "Direct",
+        "requested": "[4.9.2, )",
+        "resolved": "4.9.2",
+        "contentHash": "c74oxEil3fiZ3nXchnIgY6mXS4roHGiQBT6p3X6dMWokVqluHiqi3PNcXyxH8N/w28rQeXprF3mca83rPPNrMw==",
+        "dependencies": {
+          "Humanizer.Core": "2.14.1",
+          "Microsoft.CodeAnalysis.CSharp": "[4.9.2]",
+          "Microsoft.CodeAnalysis.Common": "[4.9.2]",
+          "Microsoft.CodeAnalysis.Workspaces.Common": "[4.9.2]"
+        }
+      },
+      "NETStandard.Library": {
+        "type": "Direct",
+        "requested": "[2.0.3, )",
+        "resolved": "2.0.3",
+        "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
+        "dependencies": {
+          "Microsoft.NETCore.Platforms": "1.1.0"
+        }
+      },
+      "System.Text.Json": {
+        "type": "Direct",
+        "requested": "[8.0.3, )",
+        "resolved": "8.0.3",
+        "contentHash": "hpagS9joOwv6efWfrMmV9MjQXpiXZH72PgN067Ysfr6AWMSD1/1hEcvh/U5mUpPLezEWsOJSuVrmqDIVD958iA==",
+        "dependencies": {
+          "Microsoft.Bcl.AsyncInterfaces": "8.0.0",
+          "System.Buffers": "4.5.1",
+          "System.Memory": "4.5.5",
+          "System.Runtime.CompilerServices.Unsafe": "6.0.0",
+          "System.Text.Encodings.Web": "8.0.0",
+          "System.Threading.Tasks.Extensions": "4.5.4"
+        }
+      },
+      "Humanizer.Core": {
+        "type": "Transitive",
+        "resolved": "2.14.1",
+        "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw=="
+      },
+      "Microsoft.Bcl.AsyncInterfaces": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==",
+        "dependencies": {
+          "System.Threading.Tasks.Extensions": "4.5.4"
+        }
+      },
+      "Microsoft.CodeAnalysis.Common": {
+        "type": "Transitive",
+        "resolved": "4.9.2",
+        "contentHash": "M5PThug7b2AdxL7xKmQs50KzAQTl9jENw5jMT3iUt16k+DAFlw1S87juU3UuPs3gvBm8trMBSOEvSFDr31c9Vw==",
+        "dependencies": {
+          "Microsoft.CodeAnalysis.Analyzers": "3.3.4",
+          "System.Collections.Immutable": "8.0.0",
+          "System.Memory": "4.5.5",
+          "System.Reflection.Metadata": "8.0.0",
+          "System.Runtime.CompilerServices.Unsafe": "6.0.0",
+          "System.Text.Encoding.CodePages": "8.0.0",
+          "System.Threading.Tasks.Extensions": "4.5.4"
+        }
+      },
+      "Microsoft.CodeAnalysis.Workspaces.Common": {
+        "type": "Transitive",
+        "resolved": "4.9.2",
+        "contentHash": "sgBlkBjKwUdpbtwM7SnBdOxvQxuaTtO9F8QgvKY5cH/OnlwDTZqmkK8hfDbhxv9wnN2wME10BL2vIv1fLJwFGA==",
+        "dependencies": {
+          "Humanizer.Core": "2.14.1",
+          "Microsoft.Bcl.AsyncInterfaces": "8.0.0",
+          "Microsoft.CodeAnalysis.Common": "[4.9.2]",
+          "System.Composition": "8.0.0",
+          "System.IO.Pipelines": "8.0.0",
+          "System.Threading.Channels": "8.0.0"
+        }
+      },
+      "Microsoft.NETCore.Platforms": {
+        "type": "Transitive",
+        "resolved": "1.1.0",
+        "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
+      },
+      "System.Buffers": {
+        "type": "Transitive",
+        "resolved": "4.5.1",
+        "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
+      },
+      "System.Collections.Immutable": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==",
+        "dependencies": {
+          "System.Memory": "4.5.5",
+          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+        }
+      },
+      "System.Composition": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "E9oO9olNNxA39J8CxQwf7ceIPm+j/B/PhYpyK9M4LhN/OLLRw6u5fNInkhVqaWueMB9iXxYqnwqwgz+W91loIA==",
+        "dependencies": {
+          "System.Composition.AttributedModel": "8.0.0",
+          "System.Composition.Convention": "8.0.0",
+          "System.Composition.Hosting": "8.0.0",
+          "System.Composition.Runtime": "8.0.0",
+          "System.Composition.TypedParts": "8.0.0"
+        }
+      },
+      "System.Composition.AttributedModel": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "NyElSuvmBMYdn2iPG0n29i7Igu0bq99izOP3MAtEwskY3OP9jqsavvVmPn9lesVaj/KT/o/QkNjA43dOJTsDQw=="
+      },
+      "System.Composition.Convention": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "UuVkc1B3vQU/LzEbWLMZ1aYVssv4rpShzf8wPEyrUqoGNqdYKREmB8bXR73heOMKkwS6ZnPz3PjGODT2MenukQ==",
+        "dependencies": {
+          "System.Composition.AttributedModel": "8.0.0"
+        }
+      },
+      "System.Composition.Hosting": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "qwbONqoxlazxcbiohvb3t1JWZgKIKcRdXS5uEeLbo5wtuBupIbAvdC3PYTAeBCZrZeERvrtAbhYHuuS43Zr1bQ==",
+        "dependencies": {
+          "System.Composition.Runtime": "8.0.0"
+        }
+      },
+      "System.Composition.Runtime": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "G+kRyB5/6+3ucRRQz+DF4uSHGqpkK8Q4ilVdbt4zvxpmvLVZNmSkyFAQpJLcbOyVF85aomJx0m+TGMDVlwx7ZQ=="
+      },
+      "System.Composition.TypedParts": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "DsSklhuA+Dsgo3ZZrar8hjBFvq1wa1grrkNCTt+6SoX3vq0Vy+HXJnVXrU/nNH1BjlGH684A7h4hJQHZd/u5mA==",
+        "dependencies": {
+          "System.Composition.AttributedModel": "8.0.0",
+          "System.Composition.Hosting": "8.0.0",
+          "System.Composition.Runtime": "8.0.0"
+        }
+      },
+      "System.IO.Pipelines": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "FHNOatmUq0sqJOkTx+UF/9YK1f180cnW5FVqnQMvYUN0elp6wFzbtPSiqbo1/ru8ICp43JM1i7kKkk6GsNGHlA==",
+        "dependencies": {
+          "System.Buffers": "4.5.1",
+          "System.Memory": "4.5.5",
+          "System.Threading.Tasks.Extensions": "4.5.4"
+        }
+      },
+      "System.Memory": {
+        "type": "Transitive",
+        "resolved": "4.5.5",
+        "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
+        "dependencies": {
+          "System.Buffers": "4.5.1",
+          "System.Numerics.Vectors": "4.4.0",
+          "System.Runtime.CompilerServices.Unsafe": "4.5.3"
+        }
+      },
+      "System.Numerics.Vectors": {
+        "type": "Transitive",
+        "resolved": "4.4.0",
+        "contentHash": "UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ=="
+      },
+      "System.Reflection.Metadata": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==",
+        "dependencies": {
+          "System.Collections.Immutable": "8.0.0",
+          "System.Memory": "4.5.5"
+        }
+      },
+      "System.Runtime.CompilerServices.Unsafe": {
+        "type": "Transitive",
+        "resolved": "6.0.0",
+        "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
+      },
+      "System.Text.Encoding.CodePages": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "OZIsVplFGaVY90G2SbpgU7EnCoOO5pw1t4ic21dBF3/1omrJFpAGoNAVpPyMVOC90/hvgkGG3VFqR13YgZMQfg==",
+        "dependencies": {
+          "System.Memory": "4.5.5",
+          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+        }
+      },
+      "System.Text.Encodings.Web": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==",
+        "dependencies": {
+          "System.Buffers": "4.5.1",
+          "System.Memory": "4.5.5",
+          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+        }
+      },
+      "System.Threading.Channels": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "CMaFr7v+57RW7uZfZkPExsPB6ljwzhjACWW1gfU35Y56rk72B/Wu+sTqxVmGSk4SFUlPc3cjeKND0zktziyjBA==",
+        "dependencies": {
+          "System.Threading.Tasks.Extensions": "4.5.4"
+        }
+      },
+      "System.Threading.Tasks.Extensions": {
+        "type": "Transitive",
+        "resolved": "4.5.4",
+        "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==",
+        "dependencies": {
+          "System.Runtime.CompilerServices.Unsafe": "4.5.3"
+        }
+      },
+      "questionable.model": {
+        "type": "Project",
+        "dependencies": {
+          "System.Text.Json": "[8.0.3, )"
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
index 95faeb46bf84989b7f638a5a244bb7c219a3f1a0..9601c4dcb2165d3dcb71811825f6e30243079698 100644 (file)
@@ -1,30 +1,11 @@
-#if RELEASE
-using System;
-using System.IO;
-using System.IO.Compression;
+using System.Collections.Generic;
+using Questionable.Model.V1;
 
+#if RELEASE
 namespace Questionable.QuestPaths;
 
-public static class AssemblyQuestLoader
+public static partial class AssemblyQuestLoader
 {
-    public static void LoadQuestsFromEmbeddedResources(Action<string, Stream> loadFunction)
-    {
-        foreach (string resourceName in typeof(AssemblyQuestLoader).Assembly.GetManifestResourceNames())
-        {
-            if (resourceName.EndsWith(".zip"))
-            {
-                using ZipArchive zipArchive =
-                    new ZipArchive(typeof(AssemblyQuestLoader).Assembly.GetManifestResourceStream(resourceName)!);
-                foreach (ZipArchiveEntry entry in zipArchive.Entries)
-                {
-                    if (entry.Name.EndsWith(".json"))
-                    {
-                        using Stream stream = entry.Open();
-                        loadFunction(entry.Name, stream);
-                    }
-                }
-            }
-        }
-    }
+    public static IReadOnlyDictionary<ushort, QuestData> GetQuests() => Quests;
 }
 #endif
index 3ea68608c5623c3fef8d250ba7ee45dc6dd6a481..2a20464785ed5192089a3faefab78263b4eadf47 100644 (file)
@@ -8,29 +8,22 @@
         <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
         <DebugType>none</DebugType>
         <PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap>
+        <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
     </PropertyGroup>
 
-    <Target Name="ZipARealmReborn" BeforeTargets="BeforeResGen" Condition="'$(Configuration)' == 'Release'">
-        <ZipDirectory SourceDirectory="$(ProjectDir)\ARealmReborn"
-                      DestinationFile="$(TargetDir)\ARealmReborn.zip"
-                      Overwrite="true"/>
-    </Target>
-
-    <Target Name="ZipShadowbringers" BeforeTargets="BeforeResGen" Condition="'$(Configuration)' == 'Release'">
-        <ZipDirectory SourceDirectory="$(ProjectDir)\Shadowbringers"
-                      DestinationFile="$(TargetDir)\Shadowbringers.zip"
-                      Overwrite="true"/>
-    </Target>
-
-    <Target Name="ZipEndwalker" BeforeTargets="BeforeResGen" Condition="'$(Configuration)' == 'Release'">
-        <ZipDirectory SourceDirectory="$(ProjectDir)\Endwalker"
-                      DestinationFile="$(TargetDir)\Endwalker.zip"
-                      Overwrite="true"/>
-    </Target>
+    <ItemGroup>
+        <ProjectReference Include="..\Questionable.Model\Questionable.Model.csproj" />
+        <ProjectReference Include="..\QuestPathGenerator\QuestPathGenerator.csproj"
+                          OutputItemType="Analyzer"
+                          ReferenceOutputAssembly="false" />
+    </ItemGroup>
 
     <ItemGroup Condition="'$(Configuration)' == 'Release'">
-        <EmbeddedResource Include="$(TargetDir)\ARealmReborn.zip"/>
-        <EmbeddedResource Include="$(TargetDir)\Shadowbringers.zip"/>
-        <EmbeddedResource Include="$(TargetDir)\Endwalker.zip"/>
+        <None Remove="ARealmReborn"/>
+        <None Remove="Shadowbringers"/>
+        <None Remove="Endwalker"/>
+        <AdditionalFiles Include="ARealmReborn\**\*.json" />
+        <AdditionalFiles Include="Shadowbringers\**\*.json" />
+        <AdditionalFiles Include="Endwalker\**\*.json" />
     </ItemGroup>
 </Project>
index f9ae177542471ddefd2ecd307220078fc3200a45..aced35f404bf34153f7a81d266771f45d5a811d9 100644 (file)
@@ -1,6 +1,105 @@
 {
   "version": 1,
   "dependencies": {
-    "net8.0-windows7.0": {}
+    "net8.0-windows7.0": {
+      "Dalamud.Extensions.MicrosoftLogging": {
+        "type": "Transitive",
+        "resolved": "4.0.1",
+        "contentHash": "fMEL2ajtF/30SBBku7vMyG0yye5eHN/A9fgT//1CEjUth/Wz2CYco5Ehye21T8KN1IuAPwoqJuu49rB71j+8ug==",
+        "dependencies": {
+          "Microsoft.Extensions.Logging": "8.0.0"
+        }
+      },
+      "DalamudPackager": {
+        "type": "Transitive",
+        "resolved": "2.1.12",
+        "contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg=="
+      },
+      "JetBrains.Annotations": {
+        "type": "Transitive",
+        "resolved": "2023.3.0",
+        "contentHash": "PHfnvdBUdGaTVG9bR/GEfxgTwWM0Z97Y6X3710wiljELBISipSfF5okn/vz+C2gfO+ihoEyVPjaJwn8ZalVukA=="
+      },
+      "Microsoft.Extensions.DependencyInjection": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==",
+        "dependencies": {
+          "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
+        }
+      },
+      "Microsoft.Extensions.DependencyInjection.Abstractions": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg=="
+      },
+      "Microsoft.Extensions.Logging": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==",
+        "dependencies": {
+          "Microsoft.Extensions.DependencyInjection": "8.0.0",
+          "Microsoft.Extensions.Logging.Abstractions": "8.0.0",
+          "Microsoft.Extensions.Options": "8.0.0"
+        }
+      },
+      "Microsoft.Extensions.Logging.Abstractions": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
+        "dependencies": {
+          "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
+        }
+      },
+      "Microsoft.Extensions.Options": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==",
+        "dependencies": {
+          "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
+          "Microsoft.Extensions.Primitives": "8.0.0"
+        }
+      },
+      "Microsoft.Extensions.Primitives": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
+      },
+      "System.Text.Encodings.Web": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ=="
+      },
+      "System.Text.Json": {
+        "type": "Transitive",
+        "resolved": "8.0.3",
+        "contentHash": "hpagS9joOwv6efWfrMmV9MjQXpiXZH72PgN067Ysfr6AWMSD1/1hEcvh/U5mUpPLezEWsOJSuVrmqDIVD958iA==",
+        "dependencies": {
+          "System.Text.Encodings.Web": "8.0.0"
+        }
+      },
+      "llib": {
+        "type": "Project"
+      },
+      "questionable": {
+        "type": "Project",
+        "dependencies": {
+          "Dalamud.Extensions.MicrosoftLogging": "[4.0.1, )",
+          "DalamudPackager": "[2.1.12, )",
+          "JetBrains.Annotations": "[2023.3.0, )",
+          "LLib": "[1.0.0, )",
+          "Microsoft.Extensions.DependencyInjection": "[8.0.0, )",
+          "QuestPaths": "[1.0.0, )",
+          "Questionable.Model": "[1.0.0, )",
+          "System.Text.Json": "[8.0.3, )"
+        }
+      },
+      "questionable.model": {
+        "type": "Project",
+        "dependencies": {
+          "System.Text.Json": "[8.0.3, )"
+        }
+      }
+    }
   }
 }
\ No newline at end of file
diff --git a/Questionable.Model/Questionable.Model.csproj b/Questionable.Model/Questionable.Model.csproj
new file mode 100644 (file)
index 0000000..c1fcd0f
--- /dev/null
@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+    <PropertyGroup>
+        <TargetFramework>netstandard2.0</TargetFramework>
+        <LangVersion>12</LangVersion>
+        <Nullable>enable</Nullable>
+        <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+        <PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap>
+        <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
+        <DebugType>portable</DebugType>
+    </PropertyGroup>
+
+    <ItemGroup>
+        <PackageReference Include="System.Text.Json" Version="8.0.3"/>
+    </ItemGroup>
+</Project>
diff --git a/Questionable.Model/V1/AethernetShortcut.cs b/Questionable.Model/V1/AethernetShortcut.cs
new file mode 100644 (file)
index 0000000..40514b5
--- /dev/null
@@ -0,0 +1,11 @@
+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
new file mode 100644 (file)
index 0000000..7172138
--- /dev/null
@@ -0,0 +1,7 @@
+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/Converter/AethernetShortcutConverter.cs b/Questionable.Model/V1/Converter/AethernetShortcutConverter.cs
new file mode 100644 (file)
index 0000000..76a0655
--- /dev/null
@@ -0,0 +1,159 @@
+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 = 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" },
+        { EAetheryteLocation.IdyllshireEpilogueGate, "[Idyllshire] Epilogue Gate" },
+
+        { 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.CrystariumThemenosRookery, "[Crystarium] Themenos Rookery" },
+        { EAetheryteLocation.CrystariumDossalGate, "[Crystarium] The Dossal Gate" },
+        { EAetheryteLocation.CrystariumPendants, "[Crystarium] The Pendants" },
+        { EAetheryteLocation.CrystariumAmaroLaunch, "[Crystarium] The Amaro Launch" },
+        { EAetheryteLocation.CrystariumCrystallineMean, "[Crystarium] The Crystalline Mean" },
+        { EAetheryteLocation.CrystariumCabinetOfCuriosity, "[Crystarium] The Cabinet of Curiosity" },
+        { EAetheryteLocation.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)" },
+    };
+
+    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
new file mode 100644 (file)
index 0000000..80b92f7
--- /dev/null
@@ -0,0 +1,108 @@
+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" }
+    };
+
+    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
new file mode 100644 (file)
index 0000000..ea832ca
--- /dev/null
@@ -0,0 +1,12 @@
+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
new file mode 100644 (file)
index 0000000..8cc5002
--- /dev/null
@@ -0,0 +1,18 @@
+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" },
+    };
+}
diff --git a/Questionable.Model/V1/Converter/EnemySpawnTypeConverter.cs b/Questionable.Model/V1/Converter/EnemySpawnTypeConverter.cs
new file mode 100644 (file)
index 0000000..5c5de53
--- /dev/null
@@ -0,0 +1,14 @@
+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
new file mode 100644 (file)
index 0000000..3ed1267
--- /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.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
new file mode 100644 (file)
index 0000000..0c48e5b
--- /dev/null
@@ -0,0 +1,30 @@
+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)
+    {
+        if (reader.TokenType == JsonTokenType.String)
+            return new ExcelRef(reader.GetString()!);
+        else if (reader.TokenType == JsonTokenType.Number)
+            return new ExcelRef(reader.GetUInt32());
+        else
+            return 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
new file mode 100644 (file)
index 0000000..6c43696
--- /dev/null
@@ -0,0 +1,27 @@
+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.WaitForObjectAtPosition, "WaitForNpcAtPosition" },
+        { EInteractionType.WaitForManualProgress, "WaitForManualProgress" },
+        { EInteractionType.Duty, "Duty" },
+        { EInteractionType.SinglePlayerDuty, "SinglePlayerDuty" },
+        { EInteractionType.Jump, "Jump" },
+        { EInteractionType.ShouldBeAJump, "ShouldBeAJump" },
+        { EInteractionType.Instruction, "Instruction" },
+    };
+}
diff --git a/Questionable.Model/V1/Converter/SkipConditionConverter.cs b/Questionable.Model/V1/Converter/SkipConditionConverter.cs
new file mode 100644 (file)
index 0000000..dd38ac4
--- /dev/null
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+
+namespace Questionable.Model.V1.Converter;
+
+public sealed class SkipConditionConverter() : EnumConverter<ESkipCondition>(Values)
+{
+    private static readonly Dictionary<ESkipCondition, string> Values = new()
+    {
+        { ESkipCondition.Never, "Never" },
+        { ESkipCondition.FlyingLocked, "FlyingLocked" },
+        { ESkipCondition.FlyingUnlocked, "FlyingUnlocked" },
+    };
+}
diff --git a/Questionable.Model/V1/Converter/VectorConverter.cs b/Questionable.Model/V1/Converter/VectorConverter.cs
new file mode 100644 (file)
index 0000000..f75ffe6
--- /dev/null
@@ -0,0 +1,64 @@
+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
new file mode 100644 (file)
index 0000000..80b49d0
--- /dev/null
@@ -0,0 +1,24 @@
+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/EAetheryteLocation.cs b/Questionable.Model/V1/EAetheryteLocation.cs
new file mode 100644 (file)
index 0000000..a49a471
--- /dev/null
@@ -0,0 +1,209 @@
+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,
+    CrystariumThemenosRookery = 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
+}
diff --git a/Questionable.Model/V1/EDialogChoiceType.cs b/Questionable.Model/V1/EDialogChoiceType.cs
new file mode 100644 (file)
index 0000000..066639a
--- /dev/null
@@ -0,0 +1,8 @@
+namespace Questionable.Model.V1;
+
+public enum EDialogChoiceType
+{
+    None,
+    YesNo,
+    List,
+}
diff --git a/Questionable.Model/V1/EEmote.cs b/Questionable.Model/V1/EEmote.cs
new file mode 100644 (file)
index 0000000..3ad0afb
--- /dev/null
@@ -0,0 +1,19 @@
+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,
+}
diff --git a/Questionable.Model/V1/EEnemySpawnType.cs b/Questionable.Model/V1/EEnemySpawnType.cs
new file mode 100644 (file)
index 0000000..8465f01
--- /dev/null
@@ -0,0 +1,14 @@
+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/EInteractionType.cs b/Questionable.Model/V1/EInteractionType.cs
new file mode 100644 (file)
index 0000000..6e3b7da
--- /dev/null
@@ -0,0 +1,34 @@
+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,
+    WaitForObjectAtPosition,
+    WaitForManualProgress,
+    Duty,
+    SinglePlayerDuty,
+    Jump,
+
+    /// <summary>
+    /// Needs to be adjusted for coords etc. in the quest data.
+    /// </summary>
+    ShouldBeAJump,
+
+    /// <summary>
+    /// Needs to be manually continued.
+    /// </summary>
+    Instruction,
+}
diff --git a/Questionable.Model/V1/ESkipCondition.cs b/Questionable.Model/V1/ESkipCondition.cs
new file mode 100644 (file)
index 0000000..4f2639d
--- /dev/null
@@ -0,0 +1,13 @@
+using System.Text.Json.Serialization;
+using Questionable.Model.V1.Converter;
+
+namespace Questionable.Model.V1;
+
+[JsonConverter(typeof(SkipConditionConverter))]
+public enum ESkipCondition
+{
+    None,
+    Never,
+    FlyingLocked,
+    FlyingUnlocked,
+}
diff --git a/Questionable.Model/V1/ExcelRef.cs b/Questionable.Model/V1/ExcelRef.cs
new file mode 100644 (file)
index 0000000..c6451ac
--- /dev/null
@@ -0,0 +1,48 @@
+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;
+    }
+
+    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 enum EType
+    {
+        None,
+        Key,
+        RowId,
+    }
+}
diff --git a/Questionable.Model/V1/JumpDestination.cs b/Questionable.Model/V1/JumpDestination.cs
new file mode 100644 (file)
index 0000000..e6a709e
--- /dev/null
@@ -0,0 +1,14 @@
+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; }
+}
diff --git a/Questionable.Model/V1/QuestData.cs b/Questionable.Model/V1/QuestData.cs
new file mode 100644 (file)
index 0000000..111bd25
--- /dev/null
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+
+namespace Questionable.Model.V1;
+
+public sealed class QuestData
+{
+    public string Author { get; set; } = null!;
+    public List<string> Contributors { get; set; } = new();
+    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
new file mode 100644 (file)
index 0000000..6273641
--- /dev/null
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+
+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];
+    }
+}
diff --git a/Questionable.Model/V1/QuestStep.cs b/Questionable.Model/V1/QuestStep.cs
new file mode 100644 (file)
index 0000000..029060e
--- /dev/null
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+using System.Text.Json.Serialization;
+using Questionable.Model.V1.Converter;
+
+namespace Questionable.Model.V1;
+
+public sealed class QuestStep
+{
+    public EInteractionType InteractionType { get; set; }
+
+    public uint? DataId { get; set; }
+
+    [JsonConverter(typeof(VectorConverter))]
+    public Vector3? Position { get; set; }
+
+    public float? StopDistance { get; set; }
+    public ushort TerritoryId { get; set; }
+    public ushort? TargetTerritoryId { get; set; }
+
+    public bool Disabled { get; set; }
+    public bool DisableNavmesh { get; set; }
+    public bool? Mount { get; set; }
+    public bool? Fly { get; set; }
+    public bool? Sprint { get; set; }
+    public string? Comment { 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 EEnemySpawnType? EnemySpawnType { get; set; }
+    public IList<uint> KillEnemyDataIds { get; set; } = new List<uint>();
+
+    public JumpDestination? JumpDestination { get; set; }
+    public uint? ContentFinderConditionId { get; set; }
+
+    public IList<ESkipCondition> SkipIf { get; set; } = new List<ESkipCondition>();
+    public IList<short?> CompletionQuestVariablesFlags { get; set; } = new List<short?>();
+    public IList<DialogueChoice> DialogueChoices { get; set; } = new List<DialogueChoice>();
+
+    [JsonConstructor]
+    public QuestStep()
+    {
+    }
+
+    public QuestStep(EInteractionType interactionType, uint? dataId, Vector3? position, ushort territoryId)
+    {
+        InteractionType = interactionType;
+        DataId = dataId;
+        Position = position;
+        TerritoryId = territoryId;
+    }
+}
diff --git a/Questionable.Model/packages.lock.json b/Questionable.Model/packages.lock.json
new file mode 100644 (file)
index 0000000..7bcc7e4
--- /dev/null
@@ -0,0 +1,86 @@
+{
+  "version": 1,
+  "dependencies": {
+    ".NETStandard,Version=v2.0": {
+      "NETStandard.Library": {
+        "type": "Direct",
+        "requested": "[2.0.3, )",
+        "resolved": "2.0.3",
+        "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
+        "dependencies": {
+          "Microsoft.NETCore.Platforms": "1.1.0"
+        }
+      },
+      "System.Text.Json": {
+        "type": "Direct",
+        "requested": "[8.0.3, )",
+        "resolved": "8.0.3",
+        "contentHash": "hpagS9joOwv6efWfrMmV9MjQXpiXZH72PgN067Ysfr6AWMSD1/1hEcvh/U5mUpPLezEWsOJSuVrmqDIVD958iA==",
+        "dependencies": {
+          "Microsoft.Bcl.AsyncInterfaces": "8.0.0",
+          "System.Buffers": "4.5.1",
+          "System.Memory": "4.5.5",
+          "System.Runtime.CompilerServices.Unsafe": "6.0.0",
+          "System.Text.Encodings.Web": "8.0.0",
+          "System.Threading.Tasks.Extensions": "4.5.4"
+        }
+      },
+      "Microsoft.Bcl.AsyncInterfaces": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==",
+        "dependencies": {
+          "System.Threading.Tasks.Extensions": "4.5.4"
+        }
+      },
+      "Microsoft.NETCore.Platforms": {
+        "type": "Transitive",
+        "resolved": "1.1.0",
+        "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
+      },
+      "System.Buffers": {
+        "type": "Transitive",
+        "resolved": "4.5.1",
+        "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
+      },
+      "System.Memory": {
+        "type": "Transitive",
+        "resolved": "4.5.5",
+        "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
+        "dependencies": {
+          "System.Buffers": "4.5.1",
+          "System.Numerics.Vectors": "4.4.0",
+          "System.Runtime.CompilerServices.Unsafe": "4.5.3"
+        }
+      },
+      "System.Numerics.Vectors": {
+        "type": "Transitive",
+        "resolved": "4.4.0",
+        "contentHash": "UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ=="
+      },
+      "System.Runtime.CompilerServices.Unsafe": {
+        "type": "Transitive",
+        "resolved": "6.0.0",
+        "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
+      },
+      "System.Text.Encodings.Web": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==",
+        "dependencies": {
+          "System.Buffers": "4.5.1",
+          "System.Memory": "4.5.5",
+          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+        }
+      },
+      "System.Threading.Tasks.Extensions": {
+        "type": "Transitive",
+        "resolved": "4.5.4",
+        "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==",
+        "dependencies": {
+          "System.Runtime.CompilerServices.Unsafe": "4.5.3"
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
index 1ad54b6d83438afda512141befdc8bf06f0a18e1..5a420a9c4a0cf8141447acf94f8aa0ed7b7a5796 100644 (file)
@@ -6,6 +6,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LLib", "LLib\LLib.csproj",
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPaths", "QuestPaths\QuestPaths.csproj", "{7A136F28-8D5C-478D-B993-0F39F1451A47}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPaths", "QuestPathGenerator\QuestPathGenerator.csproj", "{DFFD56A8-FA89-4585-A47B-C6AB27B65F0F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Questionable.Model", "Questionable.Model\Questionable.Model.csproj", "{E15144A5-AFF5-4D86-9561-AFF7DF7F505D}"
+EndProject
 Global
        GlobalSection(SolutionConfigurationPlatforms) = preSolution
                Debug|Any CPU = Debug|Any CPU
@@ -24,5 +28,13 @@ Global
                {7A136F28-8D5C-478D-B993-0F39F1451A47}.Debug|Any CPU.Build.0 = Debug|Any CPU
                {7A136F28-8D5C-478D-B993-0F39F1451A47}.Release|Any CPU.ActiveCfg = Release|Any CPU
                {7A136F28-8D5C-478D-B993-0F39F1451A47}.Release|Any CPU.Build.0 = Release|Any CPU
+               {DFFD56A8-FA89-4585-A47B-C6AB27B65F0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+               {DFFD56A8-FA89-4585-A47B-C6AB27B65F0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+               {DFFD56A8-FA89-4585-A47B-C6AB27B65F0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+               {DFFD56A8-FA89-4585-A47B-C6AB27B65F0F}.Release|Any CPU.Build.0 = Release|Any CPU
+               {E15144A5-AFF5-4D86-9561-AFF7DF7F505D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+               {E15144A5-AFF5-4D86-9561-AFF7DF7F505D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+               {E15144A5-AFF5-4D86-9561-AFF7DF7F505D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+               {E15144A5-AFF5-4D86-9561-AFF7DF7F505D}.Release|Any CPU.Build.0 = Release|Any CPU
        EndGlobalSection
 EndGlobal
index 958518b5a96c21ba82dced6c6d23ed2f315746d0..a60a458a9a0e85d2bdf9a8597f62d8d2cee84687 100644 (file)
@@ -1,3 +1 @@
 /dist
-/obj
-/bin
index 1bc3a44efda41eb96878c9b937f8e252fd7de97d..71b91a193eda3f8d6c8873609e785b378b9f0b36 100644 (file)
@@ -33,7 +33,17 @@ internal sealed class QuestRegistry
 
 #if RELEASE
         _logger.LogInformation("Loading quests from assembly");
-        QuestPaths.AssemblyQuestLoader.LoadQuestsFromEmbeddedResources(LoadQuestFromStream);
+
+        foreach ((ushort questId, QuestData questData) in QuestPaths.AssemblyQuestLoader.GetQuests())
+        {
+            Quest quest = new()
+            {
+                QuestId = questId,
+                Name = string.Empty,
+                Data = questData,
+            };
+            _quests[questId] = quest;
+        }
 #else
         DirectoryInfo? solutionDirectory = _pluginInterface.AssemblyLocation?.Directory?.Parent?.Parent;
         if (solutionDirectory != null)
@@ -48,6 +58,7 @@ internal sealed class QuestRegistry
             }
         }
 #endif
+
         LoadFromDirectory(new DirectoryInfo(Path.Combine(_pluginInterface.ConfigDirectory.FullName, "Quests")));
 
         foreach (var (questId, quest) in _quests)
@@ -60,6 +71,8 @@ internal sealed class QuestRegistry
             quest.Name = questData.Name.ToString();
             quest.Level = questData.ClassJobLevel0;
         }
+
+        _logger.LogInformation("Loaded {Count} quests", _quests.Count);
     }
 
 
diff --git a/Questionable/Model/V1/AethernetShortcut.cs b/Questionable/Model/V1/AethernetShortcut.cs
deleted file mode 100644 (file)
index 2fd707a..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-[JsonConverter(typeof(AethernetShortcutConverter))]
-internal 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 c33bf43..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-using JetBrains.Annotations;
-
-namespace Questionable.Model.V1;
-
-[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
-internal sealed class ChatMessage
-{
-    public string? ExcelSheet { get; set; }
-    public string Key { get; set; } = null!;
-}
diff --git a/Questionable/Model/V1/Converter/AethernetShortcutConverter.cs b/Questionable/Model/V1/Converter/AethernetShortcutConverter.cs
deleted file mode 100644 (file)
index 9393e9c..0000000
+++ /dev/null
@@ -1,162 +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;
-
-internal sealed class AethernetShortcutConverter : JsonConverter<AethernetShortcut>
-{
-    private static readonly Dictionary<EAetheryteLocation, string> EnumToString = 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" },
-        { EAetheryteLocation.IdyllshireEpilogueGate, "[Idyllshire] Epilogue Gate" },
-
-        { 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.CrystariumThemenosRookery, "[Crystarium] Themenos Rookery" },
-        { EAetheryteLocation.CrystariumDossalGate, "[Crystarium] The Dossal Gate" },
-        { EAetheryteLocation.CrystariumPendants, "[Crystarium] The Pendants" },
-        { EAetheryteLocation.CrystariumAmaroLaunch, "[Crystarium] The Amaro Launch" },
-        { EAetheryteLocation.CrystariumCrystallineMean, "[Crystarium] The Crystalline Mean" },
-        { EAetheryteLocation.CrystariumCabinetOfCuriosity, "[Crystarium] The Cabinet of Curiosity" },
-        { EAetheryteLocation.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)" },
-    };
-
-    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)
-    {
-        ArgumentNullException.ThrowIfNull(writer);
-        ArgumentNullException.ThrowIfNull(value);
-
-        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 6d72245..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-using System.Collections.Generic;
-
-namespace Questionable.Model.V1.Converter;
-
-internal 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" }
-    };
-
-    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 b40304d..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-using System.Collections.Generic;
-
-namespace Questionable.Model.V1.Converter;
-
-internal 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 bf8e62d..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-using System.Collections.Generic;
-
-namespace Questionable.Model.V1.Converter;
-
-internal 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" },
-    };
-}
diff --git a/Questionable/Model/V1/Converter/EnemySpawnTypeConverter.cs b/Questionable/Model/V1/Converter/EnemySpawnTypeConverter.cs
deleted file mode 100644 (file)
index a0ffe84..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-using System.Collections.Generic;
-
-namespace Questionable.Model.V1.Converter;
-
-internal 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 afd37fd..0000000
+++ /dev/null
@@ -1,43 +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;
-
-internal 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)
-            : values.ToDictionary(x => x.Key, x => x.Value).AsReadOnly();
-        _stringToEnum = _enumToString.ToDictionary(x => x.Value, x => x.Key)
-            .AsReadOnly();
-    }
-
-    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)
-    {
-        ArgumentNullException.ThrowIfNull(writer);
-        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 0c446e4..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-using System;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace Questionable.Model.V1.Converter;
-
-internal sealed class ExcelRefConverter : JsonConverter<ExcelRef>
-{
-    public override ExcelRef? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-    {
-        if (reader.TokenType == JsonTokenType.String)
-            return new ExcelRef(reader.GetString()!);
-        else if (reader.TokenType == JsonTokenType.Number)
-            return new ExcelRef(reader.GetUInt32());
-        else
-            return null;
-    }
-
-    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 d67435f..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-using System.Collections.Generic;
-
-namespace Questionable.Model.V1.Converter;
-
-internal 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.WaitForObjectAtPosition, "WaitForNpcAtPosition" },
-        { EInteractionType.WaitForManualProgress, "WaitForManualProgress" },
-        { EInteractionType.Duty, "Duty" },
-        { EInteractionType.SinglePlayerDuty, "SinglePlayerDuty" },
-        { EInteractionType.Jump, "Jump" },
-        { EInteractionType.ShouldBeAJump, "ShouldBeAJump" },
-        { EInteractionType.Instruction, "Instruction" },
-    };
-}
diff --git a/Questionable/Model/V1/Converter/SkipConditionConverter.cs b/Questionable/Model/V1/Converter/SkipConditionConverter.cs
deleted file mode 100644 (file)
index a65d504..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-using System.Collections.Generic;
-
-namespace Questionable.Model.V1.Converter;
-
-internal sealed class SkipConditionConverter() : EnumConverter<ESkipCondition>(Values)
-{
-    private static readonly Dictionary<ESkipCondition, string> Values = new()
-    {
-        { ESkipCondition.Never, "Never" },
-        { ESkipCondition.FlyingLocked, "FlyingLocked" },
-        { ESkipCondition.FlyingUnlocked, "FlyingUnlocked" },
-    };
-}
diff --git a/Questionable/Model/V1/Converter/VectorConverter.cs b/Questionable/Model/V1/Converter/VectorConverter.cs
deleted file mode 100644 (file)
index 575fd88..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-using System;
-using System.Numerics;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-
-namespace Questionable.Model.V1.Converter;
-
-internal 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)
-    {
-        ArgumentNullException.ThrowIfNull(writer);
-
-        writer.WriteStartObject();
-        writer.WriteNumber(nameof(Vector3.X), value.X);
-        writer.WriteNumber(nameof(Vector3.Y), value.X);
-        writer.WriteNumber(nameof(Vector3.Z), value.X);
-        writer.WriteEndObject();
-    }
-}
diff --git a/Questionable/Model/V1/DialogueChoice.cs b/Questionable/Model/V1/DialogueChoice.cs
deleted file mode 100644 (file)
index ddfc0ae..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-using System.Text.Json.Serialization;
-using JetBrains.Annotations;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
-internal 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/EAetheryteLocation.cs b/Questionable/Model/V1/EAetheryteLocation.cs
deleted file mode 100644 (file)
index c6ec79f..0000000
+++ /dev/null
@@ -1,209 +0,0 @@
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-[JsonConverter(typeof(AetheryteConverter))]
-internal 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,
-    CrystariumThemenosRookery = 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
-}
diff --git a/Questionable/Model/V1/EDialogChoiceType.cs b/Questionable/Model/V1/EDialogChoiceType.cs
deleted file mode 100644 (file)
index e8f18dd..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Questionable.Model.V1;
-
-internal 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 2e751d3..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-[JsonConverter(typeof(EmoteConverter))]
-internal enum EEmote
-{
-    None = 0,
-
-    Stretch = 37,
-    Wave = 16,
-    Rally = 34,
-    Deny = 25,
-    Pray = 58,
-    Slap = 111,
-    Doubt = 12,
-    Psych = 30,
-}
diff --git a/Questionable/Model/V1/EEnemySpawnType.cs b/Questionable/Model/V1/EEnemySpawnType.cs
deleted file mode 100644 (file)
index 1c19132..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-[JsonConverter(typeof(EnemySpawnTypeConverter))]
-internal enum EEnemySpawnType
-{
-    None = 0,
-    AfterInteraction,
-    AfterItemUse,
-    AutoOnEnterArea,
-    OverworldEnemies,
-}
diff --git a/Questionable/Model/V1/EInteractionType.cs b/Questionable/Model/V1/EInteractionType.cs
deleted file mode 100644 (file)
index edb27ce..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-[JsonConverter(typeof(InteractionTypeConverter))]
-internal enum EInteractionType
-{
-    Interact,
-    WalkTo,
-    AttuneAethernetShard,
-    AttuneAetheryte,
-    AttuneAetherCurrent,
-    Combat,
-    UseItem,
-    EquipItem,
-    Say,
-    Emote,
-    WaitForObjectAtPosition,
-    WaitForManualProgress,
-    Duty,
-    SinglePlayerDuty,
-    Jump,
-
-    /// <summary>
-    /// Needs to be adjusted for coords etc. in the quest data.
-    /// </summary>
-    ShouldBeAJump,
-
-    /// <summary>
-    /// Needs to be manually continued.
-    /// </summary>
-    Instruction,
-}
diff --git a/Questionable/Model/V1/ESkipCondition.cs b/Questionable/Model/V1/ESkipCondition.cs
deleted file mode 100644 (file)
index f7e173c..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-using System.Text.Json.Serialization;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-[JsonConverter(typeof(SkipConditionConverter))]
-internal enum ESkipCondition
-{
-    None,
-    Never,
-    FlyingLocked,
-    FlyingUnlocked,
-}
diff --git a/Questionable/Model/V1/ExcelRef.cs b/Questionable/Model/V1/ExcelRef.cs
deleted file mode 100644 (file)
index c6451ac..0000000
+++ /dev/null
@@ -1,48 +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;
-    }
-
-    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 enum EType
-    {
-        None,
-        Key,
-        RowId,
-    }
-}
diff --git a/Questionable/Model/V1/JumpDestination.cs b/Questionable/Model/V1/JumpDestination.cs
deleted file mode 100644 (file)
index 1827f87..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-using System.Numerics;
-using System.Text.Json.Serialization;
-using JetBrains.Annotations;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
-internal sealed class JumpDestination
-{
-    [JsonConverter(typeof(VectorConverter))]
-    public Vector3 Position { get; set; }
-
-    public float? StopDistance { get; set; }
-    public float? DelaySeconds { get; set; }
-}
diff --git a/Questionable/Model/V1/QuestData.cs b/Questionable/Model/V1/QuestData.cs
deleted file mode 100644 (file)
index d484481..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-using System.Collections.Generic;
-using JetBrains.Annotations;
-
-namespace Questionable.Model.V1;
-
-[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
-internal sealed class QuestData
-{
-    public required string Author { get; set; }
-    public List<string> Contributors { get; set; } = new();
-    public string? Comment { get; set; }
-    public List<ushort> TerritoryBlacklist { get; set; } = new();
-    public required 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 58393c3..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-using System.Collections.Generic;
-using JetBrains.Annotations;
-
-namespace Questionable.Model.V1;
-
-[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
-internal sealed class QuestSequence
-{
-    public required 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];
-    }
-}
diff --git a/Questionable/Model/V1/QuestStep.cs b/Questionable/Model/V1/QuestStep.cs
deleted file mode 100644 (file)
index 44e3ce4..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Numerics;
-using System.Text.Json.Serialization;
-using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
-using JetBrains.Annotations;
-using Questionable.Model.V1.Converter;
-
-namespace Questionable.Model.V1;
-
-[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
-internal sealed class QuestStep
-{
-    public EInteractionType InteractionType { get; set; }
-
-    public uint? DataId { get; set; }
-
-    [JsonConverter(typeof(VectorConverter))]
-    public Vector3? Position { get; set; }
-
-    public float? StopDistance { get; set; }
-    public ushort TerritoryId { get; set; }
-    public ushort? TargetTerritoryId { get; set; }
-
-    public bool Disabled { get; set; }
-    public bool DisableNavmesh { get; set; }
-    public bool? Mount { get; set; }
-    public bool? Fly { get; set; }
-    public bool? Sprint { get; set; }
-    public string? Comment { 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 EEnemySpawnType? EnemySpawnType { get; set; }
-
-    public IList<uint> KillEnemyDataIds { get; set; } = new List<uint>();
-    public JumpDestination? JumpDestination { get; set; }
-    public uint? ContentFinderConditionId { get; set; }
-
-    public IList<ESkipCondition> SkipIf { get; set; } = new List<ESkipCondition>();
-    public IList<short?> CompletionQuestVariablesFlags { get; set; } = new List<short?>();
-    public IList<DialogueChoice> DialogueChoices { get; set; } = new List<DialogueChoice>();
-
-    /// <summary>
-    /// Positive values: Must be set to this value; will wait for the step to have these set.
-    /// Negative values: Will skip if set to this value, won't wait for this to be set.
-    /// </summary>
-    public unsafe bool MatchesQuestVariables(QuestWork questWork, bool forSkip)
-    {
-        if (CompletionQuestVariablesFlags.Count != 6)
-            return false;
-
-        for (int i = 0; i < 6; ++i)
-        {
-            short? check = CompletionQuestVariablesFlags[i];
-            if (check == null)
-                continue;
-
-            byte actualValue = questWork.Variables[i];
-            byte checkByte = check > 0 ? (byte)check : (byte)-check;
-            if (forSkip)
-            {
-                byte expectedValue = (byte)Math.Abs(check.Value);
-                if ((actualValue & checkByte) != expectedValue)
-                    return false;
-            }
-            else if (!forSkip && check > 0)
-            {
-                byte expectedValue = check > 0 ? (byte)check : (byte)0;
-                if ((actualValue & checkByte) != expectedValue)
-                    return false;
-            }
-        }
-
-        return true;
-    }
-}
diff --git a/Questionable/Model/V1/QuestStepExtensions.cs b/Questionable/Model/V1/QuestStepExtensions.cs
new file mode 100644 (file)
index 0000000..dedf2eb
--- /dev/null
@@ -0,0 +1,41 @@
+using System;
+using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
+
+namespace Questionable.Model.V1;
+
+internal static class QuestStepExtensions
+{
+    /// <summary>
+    /// Positive values: Must be set to this value; will wait for the step to have these set.
+    /// Negative values: Will skip if set to this value, won't wait for this to be set.
+    /// </summary>
+    public static unsafe bool MatchesQuestVariables(this QuestStep step, QuestWork questWork, bool forSkip)
+    {
+        if (step.CompletionQuestVariablesFlags.Count != 6)
+            return false;
+
+        for (int i = 0; i < 6; ++i)
+        {
+            short? check = step.CompletionQuestVariablesFlags[i];
+            if (check == null)
+                continue;
+
+            byte actualValue = questWork.Variables[i];
+            byte checkByte = check > 0 ? (byte)check : (byte)-check;
+            if (forSkip)
+            {
+                byte expectedValue = (byte)Math.Abs(check.Value);
+                if ((actualValue & checkByte) != expectedValue)
+                    return false;
+            }
+            else if (!forSkip && check > 0)
+            {
+                byte expectedValue = check > 0 ? (byte)check : (byte)0;
+                if ((actualValue & checkByte) != expectedValue)
+                    return false;
+            }
+        }
+
+        return true;
+    }
+}
index 2e9031b456d0300154343b69f979b3101dea669d..64a7caf1106ae9bcf5bb7ac736d8a784eeab956c 100644 (file)
@@ -59,6 +59,7 @@
 
     <ItemGroup>
       <ProjectReference Include="..\LLib\LLib.csproj" />
-      <ProjectReference Include="..\QuestPaths\QuestPaths.csproj" />
+        <ProjectReference Include="..\Questionable.Model\Questionable.Model.csproj" />
+        <ProjectReference Include="..\QuestPaths\QuestPaths.csproj" />
     </ItemGroup>
 </Project>
index e4dec3d5ab5ba2acaf4acc167d7caafc184709ac..f9ea92b06609e8ef05b91466eb02ec3ed39817a1 100644 (file)
       "llib": {
         "type": "Project"
       },
+      "questionable.model": {
+        "type": "Project",
+        "dependencies": {
+          "System.Text.Json": "[8.0.3, )"
+        }
+      },
       "questpaths": {
-        "type": "Project"
+        "type": "Project",
+        "dependencies": {
+          "Questionable.Model": "[1.0.0, )"
+        }
       }
     }
   }