Schema update

This commit is contained in:
Liza 2024-08-02 20:04:45 +02:00
parent ae87b4ccc5
commit 9bfbc99144
Signed by: liza
GPG Key ID: 7199F8D727D55F67
20 changed files with 502 additions and 104 deletions

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "LLib"]
path = LLib
url = https://git.carvel.li/liza/LLib.git
[submodule "vendor/ECommons"]
path = vendor/ECommons
url = https://github.com/NightmareXIV/ECommons.git

View File

@ -1,3 +1,8 @@
<Project Sdk="Dalamud.NET.Sdk/10.0.0">
<ItemGroup>
<ProjectReference Include="..\Questionable.Model\Questionable.Model.csproj" />
<ProjectReference Include="..\vendor\ECommons\ECommons\ECommons.csproj" />
</ItemGroup>
<Import Project="..\LLib\LLib.targets"/>
</Project>

View File

@ -0,0 +1,6 @@
{
"Name": "GatheringPathRenderer",
"Author": "Liza Carvelli",
"Punchline": "dev only plugin: Renders gathering location.",
"Description": "dev only plugin: Renders gathering location (without ECommons polluting the entire normal project)."
}

View File

@ -1,11 +1,189 @@
using Dalamud.Plugin;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ECommons;
using ECommons.Schedulers;
using ECommons.SplatoonAPI;
using Questionable.Model.Gathering;
namespace GatheringPathRenderer;
public sealed class RendererPlugin : IDalamudPlugin
{
private const long OnTerritoryChange = -2;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly IClientState _clientState;
private readonly IPluginLog _pluginLog;
private readonly List<(ushort Id, GatheringRoot Root)> _gatheringLocations = [];
public RendererPlugin(IDalamudPluginInterface pluginInterface, IClientState clientState, IPluginLog pluginLog)
{
_pluginInterface = pluginInterface;
_clientState = clientState;
_pluginLog = pluginLog;
_pluginInterface.GetIpcSubscriber<object>("Questionable.ReloadData")
.Subscribe(Reload);
ECommonsMain.Init(pluginInterface, this, Module.SplatoonAPI);
LoadGatheringLocationsFromDirectory();
_clientState.TerritoryChanged += TerritoryChanged;
if (_clientState.IsLoggedIn)
TerritoryChanged(_clientState.TerritoryType);
}
private void Reload()
{
LoadGatheringLocationsFromDirectory();
TerritoryChanged(_clientState.TerritoryType);
}
private void LoadGatheringLocationsFromDirectory()
{
_gatheringLocations.Clear();
DirectoryInfo? solutionDirectory = _pluginInterface.AssemblyLocation.Directory?.Parent?.Parent?.Parent;
if (solutionDirectory != null)
{
DirectoryInfo pathProjectDirectory =
new DirectoryInfo(Path.Combine(solutionDirectory.FullName, "GatheringPaths"));
if (pathProjectDirectory.Exists)
{
try
{
LoadFromDirectory(
new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, "2.x - A Realm Reborn")));
LoadFromDirectory(
new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, "3.x - Heavensward")));
LoadFromDirectory(
new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, "4.x - Stormblood")));
LoadFromDirectory(
new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, "5.x - Shadowbringers")));
LoadFromDirectory(
new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, "6.x - Endwalker")));
LoadFromDirectory(
new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, "7.x - Dawntrail")));
_pluginLog.Information(
$"Loaded {_gatheringLocations.Count} gathering root locations from project directory");
}
catch (Exception e)
{
_pluginLog.Error(e, "Failed to load quests from project directory");
}
}
else
_pluginLog.Warning($"Project directory {pathProjectDirectory} does not exist");
}
else
_pluginLog.Warning($"Solution directory {solutionDirectory} does not exist");
}
private void LoadFromDirectory(DirectoryInfo directory)
{
if (!directory.Exists)
return;
_pluginLog.Information($"Loading locations from {directory}");
foreach (FileInfo fileInfo in directory.GetFiles("*.json"))
{
try
{
using FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
LoadLocationFromStream(fileInfo.Name, stream);
}
catch (Exception e)
{
throw new InvalidDataException($"Unable to load file {fileInfo.FullName}", e);
}
}
foreach (DirectoryInfo childDirectory in directory.GetDirectories())
LoadFromDirectory(childDirectory);
}
private void LoadLocationFromStream(string fileName, Stream stream)
{
var locationNode = JsonNode.Parse(stream)!;
GatheringRoot root = locationNode.Deserialize<GatheringRoot>()!;
_gatheringLocations.Add((ushort.Parse(fileName.Split('_')[0]), root));
}
private void TerritoryChanged(ushort territoryId)
{
Splatoon.RemoveDynamicElements("GatheringPathRenderer");
var elements = _gatheringLocations
.Where(x => x.Root.TerritoryId == territoryId)
.SelectMany(v =>
v.Root.Groups.SelectMany(group =>
group.Nodes.SelectMany(node => node.Locations
.SelectMany(x =>
new List<Element>
{
new Element(x.IsCone()
? ElementType.ConeAtFixedCoordinates
: ElementType.CircleAtFixedCoordinates)
{
refX = x.Position.X,
refY = x.Position.Z,
refZ = x.Position.Y,
Filled = true,
radius = x.MinimumDistance,
Donut = x.MaximumDistance - x.MinimumDistance,
color = 0x2020FF80,
Enabled = true,
coneAngleMin = x.IsCone() ? (int)x.MinimumAngle.GetValueOrDefault() : 0,
coneAngleMax = x.IsCone() ? (int)x.MaximumAngle.GetValueOrDefault() : 0
},
new Element(ElementType.CircleAtFixedCoordinates)
{
refX = x.Position.X,
refY = x.Position.Z,
refZ = x.Position.Y,
color = 0x00000000,
Enabled = true,
overlayText = $"{v.Id} // {node.DataId} / {node.Locations.IndexOf(x)}"
}
}))))
.ToList();
if (elements.Count == 0)
{
_pluginLog.Information("No new elements to render.");
return;
}
_ = new TickScheduler(delegate
{
try
{
Splatoon.AddDynamicElements("GatheringPathRenderer",
elements.ToArray(),
new[] { OnTerritoryChange });
_pluginLog.Information($"Created {elements.Count} splatoon elements.");
}
catch (Exception e)
{
_pluginLog.Error(e, "Unable to create splatoon layer");
}
});
}
public void Dispose()
{
_clientState.TerritoryChanged -= TerritoryChanged;
Splatoon.RemoveDynamicElements("GatheringPathRenderer");
ECommonsMain.Dispose();
_pluginInterface.GetIpcSubscriber<object>("Questionable.ReloadData")
.Unsubscribe(Reload);
}
}

View File

@ -75,6 +75,34 @@
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"System.Text.Encodings.Web": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ=="
},
"System.Text.Json": {
"type": "Transitive",
"resolved": "8.0.4",
"contentHash": "bAkhgDJ88XTsqczoxEMliSrpijKZHhbJQldhAmObj/RbrN3sU5dcokuXmWJWsdQAhiMJ9bTayWsL1C9fbbCRhw==",
"dependencies": {
"System.Text.Encodings.Web": "8.0.0"
}
},
"ecommons": {
"type": "Project"
},
"gatheringpaths": {
"type": "Project",
"dependencies": {
"Questionable.Model": "[1.0.0, )"
}
},
"questionable.model": {
"type": "Project",
"dependencies": {
"System.Text.Json": "[8.0.4, )"
}
}
}
}

View File

@ -3,54 +3,148 @@
"Author": "liza",
"TerritoryId": 957,
"AetheryteShortcut": "Thavnair - Great Work",
"Nodes": [
"Groups": [
{
"DataId": 33918,
"Position": {
"X": -582.5132,
"Y": 40.54578,
"Z": -426.0171
}
"Nodes": [
{
"DataId": 33918,
"Locations": [
{
"Position": {
"X": -582.5132,
"Y": 40.54578,
"Z": -426.0171
},
"MinimumAngle": -50,
"MaximumAngle": 90
}
]
},
{
"DataId": 33919,
"Locations": [
{
"Position": {
"X": -578.2101,
"Y": 41.27147,
"Z": -447.6376
},
"MinimumAngle": 130,
"MaximumAngle": 220
},
{
"Position": {
"X": -546.2882,
"Y": 44.52267,
"Z": -435.8184
},
"MinimumAngle": 200,
"MaximumAngle": 360
}
]
}
]
},
{
"DataId": 33919,
"Position": {
"X": -578.2101,
"Y": 41.27147,
"Z": -447.6376
}
"Nodes": [
{
"DataId": 33920,
"Locations": [
{
"Position": {
"X": -488.2276,
"Y": 34.71221,
"Z": -359.6945
},
"MinimumAngle": 20,
"MaximumAngle": 128,
"MinimumDistance": 1.3
}
]
},
{
"DataId": 33921,
"Locations": [
{
"Position": {
"X": -498.8687,
"Y": 31.08014,
"Z": -351.9397
},
"MinimumAngle": 40,
"MaximumAngle": 190
},
{
"Position": {
"X": -490.7759,
"Y": 28.70215,
"Z": -344.4114
},
"MinimumAngle": -110,
"MaximumAngle": 60
},
{
"Position": {
"X": -494.1286,
"Y": 32.89971,
"Z": -355.0208
},
"MinimumAngle": 80,
"MaximumAngle": 230
}
]
}
]
},
{
"DataId": 33920,
"Position": {
"X": -488.2276,
"Y": 34.71221,
"Z": -359.6945
}
},
{
"DataId": 33921,
"Position": {
"X": -498.8687,
"Y": 31.08014,
"Z": -351.9397
}
},
{
"DataId": 33922,
"Position": {
"X": -304.0609,
"Y": 68.76999,
"Z": -479.1875
}
},
{
"DataId": 33923,
"Position": {
"X": -293.6989,
"Y": 68.77935,
"Z": -484.2256
}
"Nodes": [
{
"DataId": 33922,
"Locations": [
{
"Position": {
"X": -304.0609,
"Y": 68.76999,
"Z": -479.1875
},
"MinimumAngle": -110,
"MaximumAngle": 70
}
]
},
{
"DataId": 33923,
"Locations": [
{
"Position": {
"X": -293.6989,
"Y": 68.77935,
"Z": -484.2256
},
"MinimumAngle": -30,
"MaximumAngle": 110
},
{
"Position": {
"X": -295.0806,
"Y": 69.12621,
"Z": -498.1898
},
"MinimumAngle": 10,
"MaximumAngle": 200
},
{
"Position": {
"X": -281.4858,
"Y": 67.64153,
"Z": -477.6673
},
"MinimumAngle": -90,
"MaximumAngle": 60
}
]
}
]
}
]
}

View File

@ -16,12 +16,10 @@ public static partial class AssemblyGatheringLocationLoader
if (_locations == null)
{
_locations = [];
#if RELEASE
LoadLocations();
#endif
}
return _locations ?? throw new InvalidOperationException("quest data is not initialized");
return _locations ?? throw new InvalidOperationException("location data is not initialized");
}
public static Stream QuestSchema =>

View File

@ -26,7 +26,7 @@
<AdditionalFiles Include="..\Questionable.Model\common-schema.json" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Release'">
<ItemGroup>
<None Remove="2.x - A Realm Reborn" />
<None Remove="3.x - Heavensward" />
<None Remove="4.x - Stormblood" />

View File

@ -25,41 +25,64 @@
"AetheryteShortcut": {
"$ref": "https://git.carvel.li/liza/Questionable/raw/branch/master/Questionable.Model/common-schema.json#/$defs/Aetheryte"
},
"Nodes": {
"Groups": {
"type": "array",
"items": {
"type": "object",
"properties": {
"DataId": {
"type": "number",
"minimum": 30000,
"maximum": 50000
},
"Position": {
"$ref": "#/$defs/Vector3"
},
"MinimumAngle": {
"type": "number",
"minimum": -360,
"maximum": 360
},
"MaximumAngle": {
"type": "number",
"minimum": -360,
"maximum": 360
},
"MinimumDistance": {
"type": "number",
"minimum": 0
},
"MaximumDistance": {
"type": "number",
"exclusiveMinimum": 0
"Nodes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"DataId": {
"type": "number",
"minimum": 30000,
"maximum": 50000
},
"Locations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Position": {
"$ref": "#/$defs/Vector3"
},
"MinimumAngle": {
"type": "number",
"minimum": -360,
"maximum": 360
},
"MaximumAngle": {
"type": "number",
"minimum": -360,
"maximum": 360
},
"MinimumDistance": {
"type": "number",
"minimum": 0
},
"MaximumDistance": {
"type": "number",
"exclusiveMinimum": 0
}
},
"required": [
"Position"
],
"additionalProperties": false
}
}
},
"required": [
"DataId"
],
"additionalProperties": false
}
}
},
"required": [
"DataId",
"Position"
"Nodes"
],
"additionalProperties": false
}

View File

@ -153,7 +153,7 @@ public class GatheringSourceGenerator : ISourceGenerator
Assignment(nameof(GatheringRoot.TerritoryId), root.TerritoryId, default)
.AsSyntaxNodeOrToken(),
Assignment(nameof(GatheringRoot.AetheryteShortcut), root.AetheryteShortcut, null),
AssignmentList(nameof(GatheringRoot.Nodes), root.Nodes).AsSyntaxNodeOrToken()))));
AssignmentList(nameof(GatheringRoot.Groups), root.Groups).AsSyntaxNodeOrToken()))));
}
catch (Exception e)
{

View File

@ -314,30 +314,55 @@ public static class RoslynShortcuts
Assignment(nameof(SkipAetheryteCondition.InSameTerritory),
skipAetheryteCondition.InSameTerritory, emptyAetheryte.InSameTerritory)))));
}
else if (value is GatheringNodeLocation nodeLocation)
else if (value is GatheringNodeGroup nodeGroup)
{
var emptyLocation = new GatheringNodeLocation();
return ObjectCreationExpression(
IdentifierName(nameof(GatheringNodeLocation)))
IdentifierName(nameof(GatheringNodeGroup)))
.WithInitializer(
InitializerExpression(
SyntaxKind.ObjectInitializerExpression,
SeparatedList<ExpressionSyntax>(
SyntaxNodeList(
Assignment(nameof(GatheringNodeLocation.DataId), nodeLocation.DataId,
AssignmentList(nameof(GatheringNodeGroup.Nodes), nodeGroup.Nodes)
.AsSyntaxNodeOrToken()))));
}
else if (value is GatheringNode nodeLocation)
{
var emptyLocation = new GatheringNode();
return ObjectCreationExpression(
IdentifierName(nameof(GatheringNode)))
.WithInitializer(
InitializerExpression(
SyntaxKind.ObjectInitializerExpression,
SeparatedList<ExpressionSyntax>(
SyntaxNodeList(
Assignment(nameof(GatheringNode.DataId), nodeLocation.DataId,
emptyLocation.DataId)
.AsSyntaxNodeOrToken(),
Assignment(nameof(GatheringNodeLocation.Position), nodeLocation.Position,
AssignmentList(nameof(GatheringNode.Locations), nodeLocation.Locations)
.AsSyntaxNodeOrToken()))));
}
else if (value is GatheringLocation location)
{
var emptyLocation = new GatheringLocation();
return ObjectCreationExpression(
IdentifierName(nameof(GatheringLocation)))
.WithInitializer(
InitializerExpression(
SyntaxKind.ObjectInitializerExpression,
SeparatedList<ExpressionSyntax>(
SyntaxNodeList(
Assignment(nameof(GatheringLocation.Position), location.Position,
emptyLocation.Position).AsSyntaxNodeOrToken(),
Assignment(nameof(GatheringNodeLocation.MinimumAngle), nodeLocation.MinimumAngle,
Assignment(nameof(GatheringLocation.MinimumAngle), location.MinimumAngle,
emptyLocation.MinimumAngle).AsSyntaxNodeOrToken(),
Assignment(nameof(GatheringNodeLocation.MaximumAngle), nodeLocation.MaximumAngle,
Assignment(nameof(GatheringLocation.MaximumAngle), location.MaximumAngle,
emptyLocation.MaximumAngle).AsSyntaxNodeOrToken(),
Assignment(nameof(GatheringNodeLocation.MinimumDistance),
nodeLocation.MinimumDistance, emptyLocation.MinimumDistance)
Assignment(nameof(GatheringLocation.MinimumDistance),
location.MinimumDistance, emptyLocation.MinimumDistance)
.AsSyntaxNodeOrToken(),
Assignment(nameof(GatheringNodeLocation.MaximumDistance),
nodeLocation.MaximumDistance, emptyLocation.MaximumDistance)
Assignment(nameof(GatheringLocation.MaximumDistance),
location.MaximumDistance, emptyLocation.MaximumDistance)
.AsSyntaxNodeOrToken()))));
}
else if (value is null)

View File

@ -55,12 +55,13 @@ public static class Utils
Culture = CultureInfo.InvariantCulture,
OutputFormat = OutputFormat.List,
});
if (!evaluationResult.IsValid)
if (evaluationResult.HasErrors)
{
var error = Diagnostic.Create(invalidJson,
null,
Path.GetFileName(additionalFile.Path));
context.ReportDiagnostic(error);
continue;
}
yield return (id, node);

View File

@ -0,0 +1,21 @@
using System.Numerics;
using System.Text.Json.Serialization;
using Questionable.Model.Common.Converter;
namespace Questionable.Model.Gathering;
public sealed class GatheringLocation
{
[JsonConverter(typeof(VectorConverter))]
public Vector3 Position { get; set; }
public float? MinimumAngle { get; set; }
public float? MaximumAngle { get; set; }
public float MinimumDistance { get; set; } = 1f;
public float MaximumDistance { get; set; } = 3f;
public bool IsCone()
{
return MinimumAngle != null && MaximumAngle != null;
}
}

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace Questionable.Model.Gathering;
public sealed class GatheringNode
{
public uint DataId { get; set; }
public List<GatheringLocation> Locations { get; set; } = [];
}

View File

@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace Questionable.Model.Gathering;
public sealed class GatheringNodeGroup
{
public List<GatheringNode> Nodes { get; set; } = [];
}

View File

@ -1,13 +0,0 @@
using System.Numerics;
namespace Questionable.Model.Gathering;
public sealed class GatheringNodeLocation
{
public uint DataId { get; set; }
public Vector3 Position { get; set; }
public float? MinimumAngle { get; set; }
public float? MaximumAngle { get; set; }
public float? MinimumDistance { get; set; } = 0.5f;
public float? MaximumDistance { get; set; } = 3f;
}

View File

@ -14,5 +14,5 @@ public sealed class GatheringRoot
[JsonConverter(typeof(AetheryteConverter))]
public EAetheryteLocation? AetheryteShortcut { get; set; }
public List<GatheringNodeLocation> Nodes { get; set; } = [];
public List<GatheringNodeGroup> Groups { get; set; } = [];
}

View File

@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GatheringPaths", "Gathering
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GatheringPathRenderer", "GatheringPathRenderer\GatheringPathRenderer.csproj", "{F514DA95-9867-4F3F-8062-ACE0C62E8740}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ECommons", "vendor\ECommons\ECommons\ECommons.csproj", "{A12D7B4B-8E6E-4DCF-A41A-12F62E9FF94B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@ -57,6 +59,10 @@ Global
{F514DA95-9867-4F3F-8062-ACE0C62E8740}.Debug|x64.Build.0 = Debug|Any CPU
{F514DA95-9867-4F3F-8062-ACE0C62E8740}.Release|x64.ActiveCfg = Release|Any CPU
{F514DA95-9867-4F3F-8062-ACE0C62E8740}.Release|x64.Build.0 = Release|Any CPU
{A12D7B4B-8E6E-4DCF-A41A-12F62E9FF94B}.Debug|x64.ActiveCfg = Debug|x64
{A12D7B4B-8E6E-4DCF-A41A-12F62E9FF94B}.Debug|x64.Build.0 = Debug|x64
{A12D7B4B-8E6E-4DCF-A41A-12F62E9FF94B}.Release|x64.ActiveCfg = Release|x64
{A12D7B4B-8E6E-4DCF-A41A-12F62E9FF94B}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -8,6 +8,7 @@ using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using Microsoft.Extensions.Logging;
using Questionable.Data;
using Questionable.Model;
@ -23,8 +24,9 @@ internal sealed class QuestRegistry
private readonly IDalamudPluginInterface _pluginInterface;
private readonly QuestData _questData;
private readonly QuestValidator _questValidator;
private readonly ILogger<QuestRegistry> _logger;
private readonly JsonSchemaValidator _jsonSchemaValidator;
private readonly ILogger<QuestRegistry> _logger;
private readonly ICallGateProvider<object> _reloadDataIpc;
private readonly Dictionary<ushort, Quest> _quests = new();
@ -37,6 +39,7 @@ internal sealed class QuestRegistry
_questValidator = questValidator;
_jsonSchemaValidator = jsonSchemaValidator;
_logger = logger;
_reloadDataIpc = _pluginInterface.GetIpcProvider<object>("Questionable.ReloadData");
}
public IEnumerable<Quest> AllQuests => _quests.Values;
@ -66,6 +69,7 @@ internal sealed class QuestRegistry
ValidateQuests();
Reloaded?.Invoke(this, EventArgs.Empty);
_reloadDataIpc.SendMessage();
_logger.LogInformation("Loaded {Count} quests in total", _quests.Count);
}

1
vendor/ECommons vendored Submodule

@ -0,0 +1 @@
Subproject commit 9e90d0032f0efd4c9e65d9c5a8e8bd0e99557d68