GE update
authorLiza Carvelli <liza@carvel.li>
Sat, 3 Aug 2024 01:21:11 +0000 (03:21 +0200)
committerLiza Carvelli <liza@carvel.li>
Sat, 3 Aug 2024 01:38:59 +0000 (03:38 +0200)
12 files changed:
GatheringPathRenderer/EditorCommands.cs [new file with mode: 0644]
GatheringPathRenderer/RendererPlugin.cs
GatheringPathRenderer/Windows/EditorWindow.cs [new file with mode: 0644]
GatheringPaths/7.x - Dawntrail/.gitkeep [deleted file]
GatheringPaths/7.x - Dawntrail/Urqopacha/974_Mountain Chromite Ore.json [new file with mode: 0644]
GatheringPaths/7.x - Dawntrail/Urqopacha/992_Snow Cotton.json [new file with mode: 0644]
GatheringPaths/7.x - Dawntrail/Urqopacha/993_Turali Aloe.json [new file with mode: 0644]
GatheringPaths/gatheringlocation-v1.json
Questionable.Model/Common/Converter/VectorConverter.cs
Questionable.Model/ExpansionVersion.cs [new file with mode: 0644]
Questionable.Model/Gathering/GatheringLocation.cs
Questionable/Controller/QuestRegistry.cs

diff --git a/GatheringPathRenderer/EditorCommands.cs b/GatheringPathRenderer/EditorCommands.cs
new file mode 100644 (file)
index 0000000..d5008b2
--- /dev/null
@@ -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<string> 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<string> arguments)
+    {
+        var target = _targetManager.Target;
+        if (target == null || target.ObjectKind != ObjectKind.GatheringPoint)
+            throw new Exception("No valid target");
+
+        var gatheringPoint = _dataManager.GetExcelSheet<GatheringPoint>()!.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<TerritoryType>()!.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");
+    }
+}
index 9c9a50d2c5e1633f141c943b2aa474fbbed09ec3..8e1c864bbaca1afafc4bc381fa1d1a28be3af260 100644 (file)
@@ -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<uint> _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<GatheringLocationContext> _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<object>("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")));
-
-                    _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");
+            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 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<GatheringRoot>()!;
-        _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<GatheringLocationContext> 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<Element>
+                        {
+                            bool isCone = false;
+                            int minimumAngle = 0;
+                            int maximumAngle = 0;
+                            if (_editorWindow.TryGetOverride(x.InternalId, out LocationOverride? locationOverride) && locationOverride != null)
+                            {
+                                if (locationOverride.IsCone())
+                                {
+                                    isCone = true;
+                                    minimumAngle = locationOverride.MinimumAngle.GetValueOrDefault();
+                                    maximumAngle = locationOverride.MaximumAngle.GetValueOrDefault();
+                                }
+                            }
+
+                            if (!isCone && x.IsCone())
                             {
-                                new Element(x.IsCone()
+                                isCone = true;
+                                minimumAngle = x.MinimumAngle.GetValueOrDefault();
+                                maximumAngle = x.MaximumAngle.GetValueOrDefault();
+                            }
+
+                            return new List<Element>
+                            {
+                                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<object>("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 (file)
index 0000000..ae8a77d
--- /dev/null
@@ -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<Guid, LocationOverride> _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<GatheringPoint>()!.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 (file)
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 (file)
index 0000000..83bd613
--- /dev/null
@@ -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 (file)
index 0000000..7cf1be2
--- /dev/null
@@ -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 (file)
index 0000000..e17f0f0
--- /dev/null
@@ -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
index cba558a64607a4e416e7a93b703b046c2a0979a3..5c71b8760c8b026aa62582090e9e19c380aae70f 100644 (file)
@@ -92,8 +92,7 @@
     "$schema",
     "Author",
     "TerritoryId",
-    "AetheryteShortcut",
-    "Nodes"
+    "Groups"
   ],
   "additionalProperties": false,
   "$defs": {
index 7b4833ef72393d9996f8bebfa82c8a1ea16305b5..868e0216af0b2c268e8a6398edd70b9898796cd8 100644 (file)
@@ -57,8 +57,8 @@ public sealed class VectorConverter : JsonConverter<Vector3>
     {
         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 (file)
index 0000000..fe92f16
--- /dev/null
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace Questionable.Model;
+
+public static class ExpansionData
+{
+    public static IReadOnlyDictionary<byte, string> ExpansionFolders = new Dictionary<byte, string>()
+    {
+        { 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" }
+    };
+}
index 5d14a55dc97399e149326c1ff9f77934077f233c..badf7c2da04422dba861e5c2ca78f2282448745e 100644 (file)
@@ -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;
 }
index 038fe1293c9db7c0b677f8de53d57e380a537c33..da557891e065979b7649aa1c7ec1322756f2efa4 100644 (file)
@@ -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)
                 {