diff --git a/GatheringPathRenderer/EditorCommands.cs b/GatheringPathRenderer/EditorCommands.cs new file mode 100644 index 0000000..d5008b2 --- /dev/null +++ b/GatheringPathRenderer/EditorCommands.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.Command; +using Dalamud.Plugin.Services; +using Lumina.Excel.GeneratedSheets; +using Questionable.Model; +using Questionable.Model.Gathering; + +namespace GatheringPathRenderer; + +internal sealed class EditorCommands : IDisposable +{ + private readonly RendererPlugin _plugin; + private readonly IDataManager _dataManager; + private readonly ICommandManager _commandManager; + private readonly ITargetManager _targetManager; + private readonly IClientState _clientState; + private readonly IChatGui _chatGui; + + public EditorCommands(RendererPlugin plugin, IDataManager dataManager, ICommandManager commandManager, + ITargetManager targetManager, IClientState clientState, IChatGui chatGui) + { + _plugin = plugin; + _dataManager = dataManager; + _commandManager = commandManager; + _targetManager = targetManager; + _clientState = clientState; + _chatGui = chatGui; + + _commandManager.AddHandler("/qg", new CommandInfo(ProcessCommand)); + } + + private void ProcessCommand(string command, string argument) + { + string[] parts = argument.Split(' '); + string subCommand = parts[0]; + List arguments = parts.Skip(1).ToList(); + + try + { + switch (subCommand) + { + case "add": + CreateOrAddLocationToGroup(arguments); + break; + } + } + catch (Exception e) + { + _chatGui.PrintError(e.ToString(), "qG"); + } + } + + private void CreateOrAddLocationToGroup(List arguments) + { + var target = _targetManager.Target; + if (target == null || target.ObjectKind != ObjectKind.GatheringPoint) + throw new Exception("No valid target"); + + var gatheringPoint = _dataManager.GetExcelSheet()!.GetRow(target.DataId); + if (gatheringPoint == null) + throw new Exception("Invalid gathering point"); + + FileInfo targetFile; + GatheringRoot root; + var locationsInTerritory = _plugin.GetLocationsInTerritory(_clientState.TerritoryType).ToList(); + var location = locationsInTerritory.SingleOrDefault(x => x.Id == gatheringPoint.GatheringPointBase.Row); + if (location != null) + { + targetFile = location.File; + root = location.Root; + + // if this is an existing node, ignore it + var existingNode = root.Groups.SelectMany(x => x.Nodes.Where(y => y.DataId == target.DataId)) + .Any(x => x.Locations.Any(y => Vector3.Distance(y.Position, target.Position) < 0.1f)); + if (existingNode) + throw new Exception("Node already exists"); + + if (arguments.Contains("group")) + AddToNewGroup(root, target); + else + AddToExistingGroup(root, target); + } + else + { + (targetFile, root) = CreateNewFile(gatheringPoint, target, string.Join(" ", arguments)); + _chatGui.Print($"Creating new file under {targetFile.FullName}", "qG"); + } + + _plugin.Save(targetFile, root); + } + + public void AddToNewGroup(GatheringRoot root, IGameObject target) + { + root.Groups.Add(new GatheringNodeGroup + { + Nodes = + [ + new GatheringNode + { + DataId = target.DataId, + Locations = + [ + new GatheringLocation + { + Position = target.Position, + } + ] + } + ] + }); + _chatGui.Print("Added group.", "qG"); + } + + public void AddToExistingGroup(GatheringRoot root, IGameObject target) + { + // find the same data id + var node = root.Groups.SelectMany(x => x.Nodes) + .SingleOrDefault(x => x.DataId == target.DataId); + if (node != null) + { + node.Locations.Add(new GatheringLocation + { + Position = target.Position, + }); + _chatGui.Print($"Added location to existing node {target.DataId}.", "qG"); + } + else + { + // find the closest group + var closestGroup = root.Groups + .Select(group => new + { + Group = group, + Distance = group.Nodes.Min(x => + x.Locations.Min(y => + Vector3.Distance(_clientState.LocalPlayer!.Position, y.Position))) + }) + .OrderBy(x => x.Distance) + .First(); + + closestGroup.Group.Nodes.Add(new GatheringNode + { + DataId = target.DataId, + Locations = + [ + new GatheringLocation + { + Position = target.Position, + } + ] + }); + _chatGui.Print($"Added new node {target.DataId}.", "qG"); + } + } + + public (FileInfo targetFile, GatheringRoot root) CreateNewFile(GatheringPoint gatheringPoint, IGameObject target, + string fileName) + { + if (string.IsNullOrEmpty(fileName)) + throw new ArgumentException(nameof(fileName)); + + // determine target folder + DirectoryInfo? targetFolder = _plugin.GetLocationsInTerritory(_clientState.TerritoryType).FirstOrDefault() + ?.File.Directory; + if (targetFolder == null) + { + var territoryInfo = _dataManager.GetExcelSheet()!.GetRow(_clientState.TerritoryType)!; + targetFolder = _plugin.PathsDirectory + .CreateSubdirectory(ExpansionData.ExpansionFolders[(byte)territoryInfo.ExVersion.Row]) + .CreateSubdirectory(territoryInfo.PlaceName.Value!.Name.ToString()); + } + + FileInfo targetFile = + new FileInfo( + Path.Combine(targetFolder.FullName, $"{gatheringPoint.GatheringPointBase.Row}_{fileName}.json")); + var root = new GatheringRoot + { + TerritoryId = _clientState.TerritoryType, + Groups = + [ + new GatheringNodeGroup + { + Nodes = + [ + new GatheringNode + { + DataId = target.DataId, + Locations = + [ + new GatheringLocation + { + Position = target.Position + } + ] + } + ] + } + ] + }; + return (targetFile, root); + } + + public void Dispose() + { + _commandManager.RemoveHandler("/qg"); + } +} diff --git a/GatheringPathRenderer/RendererPlugin.cs b/GatheringPathRenderer/RendererPlugin.cs index 9c9a50d..8e1c864 100644 --- a/GatheringPathRenderer/RendererPlugin.cs +++ b/GatheringPathRenderer/RendererPlugin.cs @@ -4,11 +4,16 @@ using System.IO; using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Dalamud.Plugin.Services; using ECommons; using ECommons.Schedulers; using ECommons.SplatoonAPI; +using GatheringPathRenderer.Windows; +using Questionable.Model; using Questionable.Model.Gathering; namespace GatheringPathRenderer; @@ -16,73 +21,83 @@ namespace GatheringPathRenderer; public sealed class RendererPlugin : IDalamudPlugin { private const long OnTerritoryChange = -2; + + private readonly WindowSystem _windowSystem = new(nameof(RendererPlugin)); + private readonly List _colors = [0xFFFF2020, 0xFF20FF20, 0xFF2020FF, 0xFFFFFF20, 0xFFFF20FF, 0xFF20FFFF]; + 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) + private readonly EditorCommands _editorCommands; + private readonly EditorWindow _editorWindow; + + private readonly List _gatheringLocations = []; + + public RendererPlugin(IDalamudPluginInterface pluginInterface, IClientState clientState, + ICommandManager commandManager, IDataManager dataManager, ITargetManager targetManager, IChatGui chatGui, + IPluginLog pluginLog) { _pluginInterface = pluginInterface; _clientState = clientState; _pluginLog = pluginLog; + _editorCommands = new EditorCommands(this, dataManager, commandManager, targetManager, clientState, chatGui); + _editorWindow = new EditorWindow(this, _editorCommands, dataManager, targetManager, clientState) { IsOpen = true }; + _windowSystem.AddWindow(_editorWindow); + _pluginInterface.GetIpcSubscriber("Questionable.ReloadData") .Subscribe(Reload); ECommonsMain.Init(pluginInterface, this, Module.SplatoonAPI); LoadGatheringLocationsFromDirectory(); + _pluginInterface.UiBuilder.Draw += _windowSystem.Draw; _clientState.TerritoryChanged += TerritoryChanged; if (_clientState.IsLoggedIn) TerritoryChanged(_clientState.TerritoryType); } - private void Reload() + internal DirectoryInfo PathsDirectory + { + get + { + DirectoryInfo? solutionDirectory = _pluginInterface.AssemblyLocation.Directory?.Parent?.Parent?.Parent; + if (solutionDirectory != null) + { + DirectoryInfo pathProjectDirectory = + new DirectoryInfo(Path.Combine(solutionDirectory.FullName, "GatheringPaths")); + if (pathProjectDirectory.Exists) + return pathProjectDirectory; + } + + throw new Exception("Unable to resolve project path"); + } + } + + internal void Reload() { LoadGatheringLocationsFromDirectory(); - TerritoryChanged(_clientState.TerritoryType); + Redraw(); } private void LoadGatheringLocationsFromDirectory() { _gatheringLocations.Clear(); - DirectoryInfo? solutionDirectory = _pluginInterface.AssemblyLocation.Directory?.Parent?.Parent?.Parent; - if (solutionDirectory != null) + try { - 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"))); + foreach (var expansionFolder in ExpansionData.ExpansionFolders.Values) + LoadFromDirectory( + new DirectoryInfo(Path.Combine(PathsDirectory.FullName, expansionFolder))); - _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"); + _pluginLog.Information( + $"Loaded {_gatheringLocations.Count} gathering root locations from project directory"); + } + catch (Exception e) + { + _pluginLog.Error(e, "Failed to load paths from project directory"); } - else - _pluginLog.Warning($"Solution directory {solutionDirectory} does not exist"); } private void LoadFromDirectory(DirectoryInfo directory) @@ -96,7 +111,7 @@ public sealed class RendererPlugin : IDalamudPlugin try { using FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read); - LoadLocationFromStream(fileInfo.Name, stream); + LoadLocationFromStream(fileInfo, stream); } catch (Exception e) { @@ -108,26 +123,78 @@ public sealed class RendererPlugin : IDalamudPlugin LoadFromDirectory(childDirectory); } - private void LoadLocationFromStream(string fileName, Stream stream) + private void LoadLocationFromStream(FileInfo fileInfo, Stream stream) { var locationNode = JsonNode.Parse(stream)!; GatheringRoot root = locationNode.Deserialize()!; - _gatheringLocations.Add((ushort.Parse(fileName.Split('_')[0]), root)); + _gatheringLocations.Add(new GatheringLocationContext(fileInfo, ushort.Parse(fileInfo.Name.Split('_')[0]), + root)); } - private void TerritoryChanged(ushort territoryId) + internal IEnumerable GetLocationsInTerritory(ushort territoryId) + => _gatheringLocations.Where(x => x.Root.TerritoryId == territoryId); + + internal void Save(FileInfo targetFile, GatheringRoot root) + { + JsonSerializerOptions options = new() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, + WriteIndented = true, + }; + using (var stream = File.Create(targetFile.FullName)) + { + var jsonNode = (JsonObject)JsonSerializer.SerializeToNode(root, options)!; + var newNode = new JsonObject(); + newNode.Add("$schema", + "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json"); + foreach (var (key, value) in jsonNode) + newNode.Add(key, value?.DeepClone()); + + using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions + { + Indented = true + }); + newNode.WriteTo(writer, options); + } + + Reload(); + } + + private void TerritoryChanged(ushort territoryId) => Redraw(); + + internal void Redraw() { Splatoon.RemoveDynamicElements("GatheringPathRenderer"); - var elements = _gatheringLocations - .Where(x => x.Root.TerritoryId == territoryId) - .SelectMany(v => - v.Root.Groups.SelectMany(group => + var elements = GetLocationsInTerritory(_clientState.TerritoryType) + .SelectMany(location => + location.Root.Groups.SelectMany(group => group.Nodes.SelectMany(node => node.Locations .SelectMany(x => - new List + { + bool isCone = false; + int minimumAngle = 0; + int maximumAngle = 0; + if (_editorWindow.TryGetOverride(x.InternalId, out LocationOverride? locationOverride) && locationOverride != null) { - new Element(x.IsCone() + if (locationOverride.IsCone()) + { + isCone = true; + minimumAngle = locationOverride.MinimumAngle.GetValueOrDefault(); + maximumAngle = locationOverride.MaximumAngle.GetValueOrDefault(); + } + } + + if (!isCone && x.IsCone()) + { + isCone = true; + minimumAngle = x.MinimumAngle.GetValueOrDefault(); + maximumAngle = x.MaximumAngle.GetValueOrDefault(); + } + + return new List + { + new Element(isCone ? ElementType.ConeAtFixedCoordinates : ElementType.CircleAtFixedCoordinates) { @@ -135,12 +202,13 @@ public sealed class RendererPlugin : IDalamudPlugin refY = x.Position.Z, refZ = x.Position.Y, Filled = true, - radius = x.MinimumDistance, - Donut = x.MaximumDistance - x.MinimumDistance, - color = 0x2020FF80, + radius = x.CalculateMinimumDistance(), + Donut = x.CalculateMaximumDistance() - x.CalculateMinimumDistance(), + color = _colors[location.Root.Groups.IndexOf(group) % _colors.Count], Enabled = true, - coneAngleMin = x.IsCone() ? (int)x.MinimumAngle.GetValueOrDefault() : 0, - coneAngleMax = x.IsCone() ? (int)x.MaximumAngle.GetValueOrDefault() : 0 + coneAngleMin = minimumAngle, + coneAngleMax = maximumAngle, + tether = false, }, new Element(ElementType.CircleAtFixedCoordinates) { @@ -149,9 +217,11 @@ public sealed class RendererPlugin : IDalamudPlugin refZ = x.Position.Y, color = 0x00000000, Enabled = true, - overlayText = $"{v.Id} // {node.DataId} / {node.Locations.IndexOf(x)}" + overlayText = + $"{location.Root.Groups.IndexOf(group)} // {node.DataId} / {node.Locations.IndexOf(x)}", } - })))) + }; + })))) .ToList(); if (elements.Count == 0) @@ -179,11 +249,16 @@ public sealed class RendererPlugin : IDalamudPlugin public void Dispose() { _clientState.TerritoryChanged -= TerritoryChanged; + _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw; Splatoon.RemoveDynamicElements("GatheringPathRenderer"); ECommonsMain.Dispose(); _pluginInterface.GetIpcSubscriber("Questionable.ReloadData") .Unsubscribe(Reload); + + _editorCommands.Dispose(); } + + internal sealed record GatheringLocationContext(FileInfo File, ushort Id, GatheringRoot Root); } diff --git a/GatheringPathRenderer/Windows/EditorWindow.cs b/GatheringPathRenderer/Windows/EditorWindow.cs new file mode 100644 index 0000000..ae8a77d --- /dev/null +++ b/GatheringPathRenderer/Windows/EditorWindow.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Numerics; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Interface.Windowing; +using Dalamud.Plugin.Services; +using ImGuiNET; +using Lumina.Excel.GeneratedSheets; +using Questionable.Model.Gathering; + +namespace GatheringPathRenderer.Windows; + +internal sealed class EditorWindow : Window +{ + private readonly RendererPlugin _plugin; + private readonly EditorCommands _editorCommands; + private readonly IDataManager _dataManager; + private readonly ITargetManager _targetManager; + private readonly IClientState _clientState; + + private readonly Dictionary _changes = []; + + private IGameObject? _target; + private (RendererPlugin.GatheringLocationContext, GatheringLocation)? _targetLocation; + private string _newFileName = string.Empty; + + public EditorWindow(RendererPlugin plugin, EditorCommands editorCommands, IDataManager dataManager, + ITargetManager targetManager, IClientState clientState) + : base("Gathering Path Editor###QuestionableGatheringPathEditor") + { + _plugin = plugin; + _editorCommands = editorCommands; + _dataManager = dataManager; + _targetManager = targetManager; + _clientState = clientState; + + SizeConstraints = new WindowSizeConstraints + { + MinimumSize = new Vector2(300, 300), + }; + } + + public override void Update() + { + _target = _targetManager.Target; + if (_target == null || _target.ObjectKind != ObjectKind.GatheringPoint) + { + _targetLocation = null; + return; + } + + var gatheringLocations = _plugin.GetLocationsInTerritory(_clientState.TerritoryType); + var location = gatheringLocations.SelectMany(context => + context.Root.Groups.SelectMany(group => + group.Nodes + .Where(node => node.DataId == _target.DataId) + .SelectMany(node => node.Locations) + .Where(location => Vector3.Distance(location.Position, _target.Position) < 0.1f) + .Select(location => new { Context = context, Location = location }))) + .FirstOrDefault(); + if (location == null) + { + _targetLocation = null; + return; + } + + _targetLocation = (location.Context, location.Location); + } + + public override bool DrawConditions() + { + return _target != null || _targetLocation != null; + } + + public override void Draw() + { + if (_target != null && _targetLocation != null) + { + var context = _targetLocation.Value.Item1; + var location = _targetLocation.Value.Item2; + ImGui.Text(context.File.Directory?.Name ?? string.Empty); + ImGui.Indent(); + ImGui.Text(context.File.Name); + ImGui.Unindent(); + ImGui.Text($"{_target.DataId} // {location.InternalId}"); + ImGui.Text(string.Create(CultureInfo.InvariantCulture, $"{location.Position:G}")); + + if (!_changes.TryGetValue(location.InternalId, out LocationOverride? locationOverride)) + { + locationOverride = new LocationOverride(); + _changes[location.InternalId] = locationOverride; + } + + int minAngle = locationOverride.MinimumAngle ?? location.MinimumAngle.GetValueOrDefault(); + if (ImGui.DragInt("Min Angle", ref minAngle, 5, -180, 360)) + { + locationOverride.MinimumAngle = minAngle; + locationOverride.MaximumAngle ??= location.MaximumAngle.GetValueOrDefault(); + _plugin.Redraw(); + } + + int maxAngle = locationOverride.MaximumAngle ?? location.MaximumAngle.GetValueOrDefault(); + if (ImGui.DragInt("Max Angle", ref maxAngle, 5, -180, 360)) + { + locationOverride.MinimumAngle ??= location.MinimumAngle.GetValueOrDefault(); + locationOverride.MaximumAngle = maxAngle; + _plugin.Redraw(); + } + + ImGui.BeginDisabled(locationOverride.MinimumAngle == null && locationOverride.MaximumAngle == null); + if (ImGui.Button("Save")) + { + location.MinimumAngle = locationOverride.MinimumAngle; + location.MaximumAngle = locationOverride.MaximumAngle; + _plugin.Save(context.File, context.Root); + } + ImGui.SameLine(); + if (ImGui.Button("Reset")) + { + _changes[location.InternalId] = new LocationOverride(); + _plugin.Redraw(); + } + ImGui.EndDisabled(); + + } + else if (_target != null) + { + var gatheringPoint = _dataManager.GetExcelSheet()!.GetRow(_target.DataId); + if (gatheringPoint == null) + return; + + var locationsInTerritory = _plugin.GetLocationsInTerritory(_clientState.TerritoryType).ToList(); + var location = locationsInTerritory.SingleOrDefault(x => x.Id == gatheringPoint.GatheringPointBase.Row); + if (location != null) + { + var targetFile = location.File; + var root = location.Root; + + if (ImGui.Button("Add to closest group")) + { + _editorCommands.AddToExistingGroup(root, _target); + _plugin.Save(targetFile, root); + } + + ImGui.BeginDisabled(root.Groups.Any(group => group.Nodes.Any(node => node.DataId == _target.DataId))); + ImGui.SameLine(); + if (ImGui.Button("Add as new group")) + { + _editorCommands.AddToNewGroup(root, _target); + _plugin.Save(targetFile, root); + } + ImGui.EndDisabled(); + } + else + { + ImGui.InputText("File Name", ref _newFileName, 128); + ImGui.BeginDisabled(string.IsNullOrEmpty(_newFileName)); + if (ImGui.Button("Create location")) + { + var (targetFile, root) = _editorCommands.CreateNewFile(gatheringPoint, _target, _newFileName); + _plugin.Save(targetFile, root); + _newFileName = string.Empty; + } + + ImGui.EndDisabled(); + } + } + } + + public bool TryGetOverride(Guid internalId, out LocationOverride? locationOverride) + => _changes.TryGetValue(internalId, out locationOverride); +} + +internal class LocationOverride +{ + public int? MinimumAngle { get; set; } + public int? MaximumAngle { get; set; } + public float? MinimumDistance { get; set; } + public float? MaximumDistance { get; set; } + + public bool IsCone() + { + return MinimumAngle != null && MaximumAngle != null && MinimumAngle != MaximumAngle; + } +} diff --git a/GatheringPaths/7.x - Dawntrail/.gitkeep b/GatheringPaths/7.x - Dawntrail/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/GatheringPaths/7.x - Dawntrail/Urqopacha/974_Mountain Chromite Ore.json b/GatheringPaths/7.x - Dawntrail/Urqopacha/974_Mountain Chromite Ore.json new file mode 100644 index 0000000..83bd613 --- /dev/null +++ b/GatheringPaths/7.x - Dawntrail/Urqopacha/974_Mountain Chromite Ore.json @@ -0,0 +1,112 @@ +{ + "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json", + "Author": [], + "TerritoryId": 1187, + "Groups": [ + { + "Nodes": [ + { + "DataId": 34749, + "Locations": [ + { + "Position": { + "X": -392.813, + "Y": -47.04364, + "Z": -386.862 + }, + "MinimumAngle": -10, + "MaximumAngle": 240 + } + ] + }, + { + "DataId": 34750, + "Locations": [ + { + "Position": { + "X": -402.8987, + "Y": -45.59287, + "Z": -390.7613 + }, + "MinimumAngle": 220, + "MaximumAngle": 305 + }, + { + "Position": { + "X": -388.9036, + "Y": -46.86702, + "Z": -381.3985 + }, + "MinimumAngle": -50, + "MaximumAngle": 210 + } + ] + } + ] + }, + { + "Nodes": [ + { + "DataId": 34753, + "Locations": [ + { + "Position": { + "X": -541.7726, + "Y": -22.952, + "Z": -517.8604 + }, + "MinimumAngle": 215, + "MaximumAngle": 330 + } + ] + }, + { + "DataId": 34754, + "Locations": [ + { + "Position": { + "X": -522.9433, + "Y": -25.87319, + "Z": -537.3257 + }, + "MinimumAngle": 225, + "MaximumAngle": 360 + } + ] + } + ] + }, + { + "Nodes": [ + { + "DataId": 34751, + "Locations": [ + { + "Position": { + "X": -448.8079, + "Y": -14.9586, + "Z": -658.0133 + }, + "MinimumAngle": -45, + "MaximumAngle": 115 + } + ] + }, + { + "DataId": 34752, + "Locations": [ + { + "Position": { + "X": -452.2813, + "Y": -12.43015, + "Z": -665.0275 + }, + "MinimumAngle": 0, + "MaximumAngle": 150 + } + ] + } + ] + } + ] +} diff --git a/GatheringPaths/7.x - Dawntrail/Urqopacha/992_Snow Cotton.json b/GatheringPaths/7.x - Dawntrail/Urqopacha/992_Snow Cotton.json new file mode 100644 index 0000000..7cf1be2 --- /dev/null +++ b/GatheringPaths/7.x - Dawntrail/Urqopacha/992_Snow Cotton.json @@ -0,0 +1,95 @@ +{ + "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json", + "Author": [], + "TerritoryId": 1187, + "Groups": [ + { + "Nodes": [ + { + "DataId": 34857, + "Locations": [ + { + "Position": { + "X": -12.48859, + "Y": -133.2091, + "Z": -427.7497 + } + } + ] + }, + { + "DataId": 34858, + "Locations": [ + { + "Position": { + "X": -22.41956, + "Y": -129.3952, + "Z": -396.6573 + } + } + ] + } + ] + }, + { + "Nodes": [ + { + "DataId": 34861, + "Locations": [ + { + "Position": { + "X": -234.8222, + "Y": -99.01237, + "Z": -376.7287 + }, + "MinimumAngle": -180, + "MaximumAngle": 40 + } + ] + }, + { + "DataId": 34862, + "Locations": [ + { + "Position": { + "X": -236.0182, + "Y": -97.50027, + "Z": -372.1523 + }, + "MinimumAngle": -180, + "MaximumAngle": 45 + } + ] + } + ] + }, + { + "Nodes": [ + { + "DataId": 34860, + "Locations": [ + { + "Position": { + "X": -169.8177, + "Y": -85.61841, + "Z": -240.1007 + } + } + ] + }, + { + "DataId": 34859, + "Locations": [ + { + "Position": { + "X": -131.9198, + "Y": -89.88039, + "Z": -249.5422 + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/GatheringPaths/7.x - Dawntrail/Urqopacha/993_Turali Aloe.json b/GatheringPaths/7.x - Dawntrail/Urqopacha/993_Turali Aloe.json new file mode 100644 index 0000000..e17f0f0 --- /dev/null +++ b/GatheringPaths/7.x - Dawntrail/Urqopacha/993_Turali Aloe.json @@ -0,0 +1,95 @@ +{ + "$schema": "https://git.carvel.li/liza/Questionable/raw/branch/master/GatheringPaths/gatheringlocation-v1.json", + "Author": [], + "TerritoryId": 1187, + "Groups": [ + { + "Nodes": [ + { + "DataId": 34866, + "Locations": [ + { + "Position": { + "X": 242.7737, + "Y": -135.9734, + "Z": -431.2313 + } + } + ] + }, + { + "DataId": 34865, + "Locations": [ + { + "Position": { + "X": 269.7338, + "Y": -134.0488, + "Z": -381.6242 + } + } + ] + } + ] + }, + { + "Nodes": [ + { + "DataId": 34868, + "Locations": [ + { + "Position": { + "X": 389.1952, + "Y": -154.3099, + "Z": -368.3658 + }, + "MinimumAngle": 105, + "MaximumAngle": 345 + } + ] + }, + { + "DataId": 34867, + "Locations": [ + { + "Position": { + "X": 399.1297, + "Y": -152.1141, + "Z": -394.71 + }, + "MinimumAngle": 120, + "MaximumAngle": 330 + } + ] + } + ] + }, + { + "Nodes": [ + { + "DataId": 34864, + "Locations": [ + { + "Position": { + "X": 359.517, + "Y": -161.1972, + "Z": -644.0471 + } + } + ] + }, + { + "DataId": 34863, + "Locations": [ + { + "Position": { + "X": 323.8758, + "Y": -162.9682, + "Z": -648.8156 + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/GatheringPaths/gatheringlocation-v1.json b/GatheringPaths/gatheringlocation-v1.json index cba558a..5c71b87 100644 --- a/GatheringPaths/gatheringlocation-v1.json +++ b/GatheringPaths/gatheringlocation-v1.json @@ -92,8 +92,7 @@ "$schema", "Author", "TerritoryId", - "AetheryteShortcut", - "Nodes" + "Groups" ], "additionalProperties": false, "$defs": { diff --git a/Questionable.Model/Common/Converter/VectorConverter.cs b/Questionable.Model/Common/Converter/VectorConverter.cs index 7b4833e..868e021 100644 --- a/Questionable.Model/Common/Converter/VectorConverter.cs +++ b/Questionable.Model/Common/Converter/VectorConverter.cs @@ -57,8 +57,8 @@ public sealed class VectorConverter : JsonConverter { writer.WriteStartObject(); writer.WriteNumber(nameof(Vector3.X), value.X); - writer.WriteNumber(nameof(Vector3.Y), value.X); - writer.WriteNumber(nameof(Vector3.Z), value.X); + writer.WriteNumber(nameof(Vector3.Y), value.Y); + writer.WriteNumber(nameof(Vector3.Z), value.Z); writer.WriteEndObject(); } } diff --git a/Questionable.Model/ExpansionVersion.cs b/Questionable.Model/ExpansionVersion.cs new file mode 100644 index 0000000..fe92f16 --- /dev/null +++ b/Questionable.Model/ExpansionVersion.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Questionable.Model; + +public static class ExpansionData +{ + public static IReadOnlyDictionary ExpansionFolders = new Dictionary() + { + { 0, "2.x - A Realm Reborn" }, + { 1, "3.x - Heavensward" }, + { 2, "4.x - Stormblood" }, + { 3, "5.x - Shadowbringers" }, + { 4, "6.x - Endwalker" }, + { 5, "7.x - Dawntrail" } + }; +} diff --git a/Questionable.Model/Gathering/GatheringLocation.cs b/Questionable.Model/Gathering/GatheringLocation.cs index 5d14a55..badf7c2 100644 --- a/Questionable.Model/Gathering/GatheringLocation.cs +++ b/Questionable.Model/Gathering/GatheringLocation.cs @@ -1,4 +1,5 @@ -using System.Numerics; +using System; +using System.Numerics; using System.Text.Json.Serialization; using Questionable.Model.Common.Converter; @@ -6,16 +7,22 @@ namespace Questionable.Model.Gathering; public sealed class GatheringLocation { + [JsonIgnore] + public Guid InternalId { get; } = Guid.NewGuid(); + [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 int? MinimumAngle { get; set; } + public int? MaximumAngle { get; set; } + public float? MinimumDistance { get; set; } + public float? MaximumDistance { get; set; } public bool IsCone() { return MinimumAngle != null && MaximumAngle != null; } + + public float CalculateMinimumDistance() => MinimumDistance ?? 1f; + public float CalculateMaximumDistance() => MaximumDistance ?? 3f; } diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs index 038fe12..da55789 100644 --- a/Questionable/Controller/QuestRegistry.cs +++ b/Questionable/Controller/QuestRegistry.cs @@ -69,7 +69,16 @@ internal sealed class QuestRegistry ValidateQuests(); Reloaded?.Invoke(this, EventArgs.Empty); - _reloadDataIpc.SendMessage(); + try + { + _reloadDataIpc.SendMessage(); + } + catch (Exception e) + { + // why does this even throw + _logger.LogWarning(e, "Error during Reload.SendMessage IPC"); + } + _logger.LogInformation("Loaded {Count} quests in total", _quests.Count); } @@ -105,24 +114,10 @@ internal sealed class QuestRegistry { try { - LoadFromDirectory( - new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, "2.x - A Realm Reborn")), - LogLevel.Trace); - LoadFromDirectory( - new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, "3.x - Heavensward")), - LogLevel.Trace); - LoadFromDirectory( - new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, "4.x - Stormblood")), - LogLevel.Trace); - LoadFromDirectory( - new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, "5.x - Shadowbringers")), - LogLevel.Trace); - LoadFromDirectory( - new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, "6.x - Endwalker")), - LogLevel.Trace); - LoadFromDirectory( - new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, "7.x - Dawntrail")), - LogLevel.Trace); + foreach (var expansionFolder in ExpansionData.ExpansionFolders.Values) + LoadFromDirectory( + new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, expansionFolder)), + LogLevel.Trace); } catch (Exception e) {