--- /dev/null
+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");
+ }
+}
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;
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)
try
{
using FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
- LoadLocationFromStream(fileInfo.Name, stream);
+ LoadLocationFromStream(fileInfo, stream);
}
catch (Exception e)
{
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)
{
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)
{
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)
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);
}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+{
+ "$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
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
--- /dev/null
+{
+ "$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
--- /dev/null
+{
+ "$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
"$schema",
"Author",
"TerritoryId",
- "AetheryteShortcut",
- "Nodes"
+ "Groups"
],
"additionalProperties": false,
"$defs": {
{
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();
}
}
--- /dev/null
+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" }
+ };
+}
-using System.Numerics;
+using System;
+using System.Numerics;
using System.Text.Json.Serialization;
using Questionable.Model.Common.Converter;
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;
}
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);
}
{
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)
{