Questionable/GatheringPathRenderer/RendererPlugin.cs

341 lines
13 KiB
C#
Raw Permalink Normal View History

2024-08-02 18:04:45 +00:00
using System;
2024-08-15 23:51:12 +00:00
using System.Collections;
2024-08-02 18:04:45 +00:00
using System.Collections.Generic;
using System.IO;
using System.Linq;
2024-08-11 16:59:42 +00:00
using System.Text.Encodings.Web;
2024-08-02 18:04:45 +00:00
using System.Text.Json;
using System.Text.Json.Nodes;
2024-08-03 01:21:11 +00:00
using System.Text.Json.Serialization;
2024-08-15 23:51:12 +00:00
using System.Text.Json.Serialization.Metadata;
2024-08-03 01:21:11 +00:00
using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface.Windowing;
2024-08-02 18:04:45 +00:00
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ECommons;
using ECommons.Schedulers;
using ECommons.SplatoonAPI;
2024-08-03 01:21:11 +00:00
using GatheringPathRenderer.Windows;
2024-08-12 14:21:34 +00:00
using LLib.GameData;
2024-08-03 01:21:11 +00:00
using Questionable.Model;
2024-08-02 18:04:45 +00:00
using Questionable.Model.Gathering;
2024-08-02 16:30:21 +00:00
namespace GatheringPathRenderer;
public sealed class RendererPlugin : IDalamudPlugin
{
2024-08-02 18:04:45 +00:00
private const long OnTerritoryChange = -2;
2024-08-03 01:21:11 +00:00
private readonly WindowSystem _windowSystem = new(nameof(RendererPlugin));
private readonly List<uint> _colors = [0xFFFF2020, 0xFF20FF20, 0xFF2020FF, 0xFFFFFF20, 0xFFFF20FF, 0xFF20FFFF];
2024-08-02 18:04:45 +00:00
private readonly IDalamudPluginInterface _pluginInterface;
private readonly IClientState _clientState;
private readonly IPluginLog _pluginLog;
2024-08-03 01:21:11 +00:00
private readonly EditorCommands _editorCommands;
private readonly EditorWindow _editorWindow;
private readonly List<GatheringLocationContext> _gatheringLocations = [];
private EClassJob _currentClassJob;
2024-08-03 01:21:11 +00:00
public RendererPlugin(IDalamudPluginInterface pluginInterface, IClientState clientState,
ICommandManager commandManager, IDataManager dataManager, ITargetManager targetManager, IChatGui chatGui,
2024-08-03 09:17:20 +00:00
IObjectTable objectTable, IPluginLog pluginLog)
2024-08-02 18:04:45 +00:00
{
_pluginInterface = pluginInterface;
_clientState = clientState;
_pluginLog = pluginLog;
2024-08-03 19:33:52 +00:00
Configuration? configuration = (Configuration?)pluginInterface.GetPluginConfig();
if (configuration == null)
{
configuration = new Configuration();
pluginInterface.SavePluginConfig(configuration);
}
_editorCommands = new EditorCommands(this, dataManager, commandManager, targetManager, clientState, chatGui,
configuration);
2024-08-03 09:17:20 +00:00
_editorWindow = new EditorWindow(this, _editorCommands, dataManager, targetManager, clientState, objectTable)
{ IsOpen = true };
2024-08-03 01:21:11 +00:00
_windowSystem.AddWindow(_editorWindow);
_currentClassJob = (EClassJob?)_clientState.LocalPlayer?.ClassJob.Id ?? EClassJob.Adventurer;
2024-08-03 01:21:11 +00:00
2024-08-02 18:04:45 +00:00
_pluginInterface.GetIpcSubscriber<object>("Questionable.ReloadData")
.Subscribe(Reload);
ECommonsMain.Init(pluginInterface, this, Module.SplatoonAPI);
LoadGatheringLocationsFromDirectory();
2024-08-03 01:21:11 +00:00
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
2024-08-02 18:04:45 +00:00
_clientState.TerritoryChanged += TerritoryChanged;
2024-08-12 14:21:34 +00:00
_clientState.ClassJobChanged += ClassJobChanged;
2024-08-02 18:04:45 +00:00
if (_clientState.IsLoggedIn)
TerritoryChanged(_clientState.TerritoryType);
}
2024-08-03 01:21:11 +00:00
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()
2024-08-02 18:04:45 +00:00
{
LoadGatheringLocationsFromDirectory();
2024-08-03 01:21:11 +00:00
Redraw();
2024-08-02 18:04:45 +00:00
}
private void LoadGatheringLocationsFromDirectory()
{
_gatheringLocations.Clear();
2024-08-03 01:21:11 +00:00
try
2024-08-02 18:04:45 +00:00
{
2024-08-03 01:21:11 +00:00
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");
2024-08-02 18:04:45 +00:00
}
}
private void LoadFromDirectory(DirectoryInfo directory)
{
if (!directory.Exists)
return;
_pluginLog.Information($"Loading locations from {directory}");
foreach (FileInfo fileInfo in directory.GetFiles("*.json"))
{
try
{
using FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
2024-08-03 01:21:11 +00:00
LoadLocationFromStream(fileInfo, stream);
2024-08-02 18:04:45 +00:00
}
catch (Exception e)
{
throw new InvalidDataException($"Unable to load file {fileInfo.FullName}", e);
}
}
foreach (DirectoryInfo childDirectory in directory.GetDirectories())
LoadFromDirectory(childDirectory);
}
2024-08-03 01:21:11 +00:00
private void LoadLocationFromStream(FileInfo fileInfo, Stream stream)
2024-08-02 18:04:45 +00:00
{
var locationNode = JsonNode.Parse(stream)!;
GatheringRoot root = locationNode.Deserialize<GatheringRoot>()!;
2024-08-03 01:21:11 +00:00
_gatheringLocations.Add(new GatheringLocationContext(fileInfo, ushort.Parse(fileInfo.Name.Split('_')[0]),
root));
2024-08-02 18:04:45 +00:00
}
2024-08-03 01:21:11 +00:00
internal IEnumerable<GatheringLocationContext> GetLocationsInTerritory(ushort territoryId)
=> _gatheringLocations.Where(x => x.Root.Steps.LastOrDefault()?.TerritoryId == territoryId);
2024-08-03 01:21:11 +00:00
internal void Save(FileInfo targetFile, GatheringRoot root)
{
JsonSerializerOptions options = new()
{
2024-08-11 16:59:42 +00:00
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
2024-08-03 01:21:11 +00:00
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
WriteIndented = true,
2024-08-15 23:51:12 +00:00
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { NoEmptyCollectionModifier }
},
2024-08-03 01:21:11 +00:00
};
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
{
2024-08-11 16:59:42 +00:00
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
2024-08-03 01:21:11 +00:00
Indented = true
});
newNode.WriteTo(writer, options);
}
Reload();
}
2024-08-15 23:51:12 +00:00
private static void NoEmptyCollectionModifier(JsonTypeInfo typeInfo)
{
foreach (var property in typeInfo.Properties)
{
if (typeof(ICollection).IsAssignableFrom(property.PropertyType))
{
property.ShouldSerialize = (_, val) => val is ICollection { Count: > 0 };
}
}
}
2024-08-03 01:21:11 +00:00
private void TerritoryChanged(ushort territoryId) => Redraw();
private void ClassJobChanged(uint classJobId)
{
_currentClassJob = (EClassJob)classJobId;
Redraw(_currentClassJob);
}
2024-08-12 14:21:34 +00:00
internal void Redraw() => Redraw(_currentClassJob);
2024-08-12 14:21:34 +00:00
private void Redraw(EClassJob classJob)
2024-08-02 18:04:45 +00:00
{
Splatoon.RemoveDynamicElements("GatheringPathRenderer");
2024-08-12 14:21:34 +00:00
if (!classJob.IsGatherer())
return;
2024-08-02 18:04:45 +00:00
2024-08-03 01:21:11 +00:00
var elements = GetLocationsInTerritory(_clientState.TerritoryType)
.SelectMany(location =>
location.Root.Groups.SelectMany(group =>
2024-08-02 18:04:45 +00:00
group.Nodes.SelectMany(node => node.Locations
.SelectMany(x =>
2024-08-03 01:21:11 +00:00
{
bool isUnsaved = false;
2024-08-03 01:21:11 +00:00
bool isCone = false;
int minimumAngle = 0;
int maximumAngle = 0;
2024-08-03 09:17:20 +00:00
if (_editorWindow.TryGetOverride(x.InternalId, out LocationOverride? locationOverride) &&
locationOverride != null)
2024-08-03 01:21:11 +00:00
{
isUnsaved = locationOverride.NeedsSave();
2024-08-03 01:21:11 +00:00
if (locationOverride.IsCone())
{
isCone = true;
minimumAngle = locationOverride.MinimumAngle.GetValueOrDefault();
maximumAngle = locationOverride.MaximumAngle.GetValueOrDefault();
}
}
if (!isCone && x.IsCone())
2024-08-02 18:04:45 +00:00
{
2024-08-03 01:21:11 +00:00
isCone = true;
minimumAngle = x.MinimumAngle.GetValueOrDefault();
maximumAngle = x.MaximumAngle.GetValueOrDefault();
}
2024-08-20 00:50:47 +00:00
#if false
2024-08-03 09:17:20 +00:00
var a = GatheringMath.CalculateLandingLocation(x, 0, 0);
var b = GatheringMath.CalculateLandingLocation(x, 1, 1);
2024-08-20 00:50:47 +00:00
#endif
2024-08-03 01:21:11 +00:00
return new List<Element>
{
new Element(isCone
2024-08-02 18:04:45 +00:00
? ElementType.ConeAtFixedCoordinates
: ElementType.CircleAtFixedCoordinates)
{
refX = x.Position.X,
refY = x.Position.Z,
refZ = x.Position.Y,
Filled = true,
2024-08-11 19:22:19 +00:00
radius = locationOverride?.MinimumDistance ?? x.CalculateMinimumDistance(),
2024-08-15 23:51:12 +00:00
Donut = (locationOverride?.MaximumDistance ?? x.CalculateMaximumDistance()) -
(locationOverride?.MinimumDistance ?? x.CalculateMinimumDistance()),
2024-08-03 01:21:11 +00:00
color = _colors[location.Root.Groups.IndexOf(group) % _colors.Count],
2024-08-02 18:04:45 +00:00
Enabled = true,
2024-08-03 01:21:11 +00:00
coneAngleMin = minimumAngle,
coneAngleMax = maximumAngle,
tether = false,
2024-08-02 18:04:45 +00:00
},
new Element(ElementType.CircleAtFixedCoordinates)
{
refX = x.Position.X,
refY = x.Position.Z,
refZ = x.Position.Y,
2024-08-03 19:33:52 +00:00
color = 0xFFFFFFFF,
radius = 0.1f,
2024-08-02 18:04:45 +00:00
Enabled = true,
2024-08-03 01:21:11 +00:00
overlayText =
$"{location.Root.Groups.IndexOf(group)} // {node.DataId} / {node.Locations.IndexOf(x)}",
2024-08-11 16:59:42 +00:00
overlayBGColor = isUnsaved ? 0xFF2020FF : 0xFF000000,
2024-08-03 09:17:20 +00:00
},
2024-08-12 14:21:17 +00:00
#if false
2024-08-03 09:17:20 +00:00
new Element(ElementType.CircleAtFixedCoordinates)
{
refX = a.X,
refY = a.Z,
refZ = a.Y,
color = _colors[0],
radius = 0.1f,
Enabled = true,
overlayText = "Min Angle"
},
new Element(ElementType.CircleAtFixedCoordinates)
{
refX = b.X,
refY = b.Z,
refZ = b.Y,
color = _colors[1],
radius = 0.1f,
Enabled = true,
overlayText = "Max Angle"
2024-08-02 18:04:45 +00:00
}
2024-08-12 14:21:17 +00:00
#endif
2024-08-03 01:21:11 +00:00
};
}))))
2024-08-02 18:04:45 +00:00
.ToList();
if (elements.Count == 0)
{
_pluginLog.Information("No new elements to render.");
return;
}
_ = new TickScheduler(delegate
{
try
{
Splatoon.AddDynamicElements("GatheringPathRenderer",
elements.ToArray(),
new[] { OnTerritoryChange });
_pluginLog.Information($"Created {elements.Count} splatoon elements.");
}
catch (Exception e)
{
_pluginLog.Error(e, "Unable to create splatoon layer");
}
});
}
2024-08-02 16:30:21 +00:00
public void Dispose()
{
2024-08-12 14:21:34 +00:00
_clientState.ClassJobChanged -= ClassJobChanged;
2024-08-02 18:04:45 +00:00
_clientState.TerritoryChanged -= TerritoryChanged;
2024-08-03 01:21:11 +00:00
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
2024-08-02 18:04:45 +00:00
Splatoon.RemoveDynamicElements("GatheringPathRenderer");
ECommonsMain.Dispose();
2024-08-02 16:30:21 +00:00
2024-08-02 18:04:45 +00:00
_pluginInterface.GetIpcSubscriber<object>("Questionable.ReloadData")
.Unsubscribe(Reload);
2024-08-03 01:21:11 +00:00
_editorCommands.Dispose();
2024-08-02 16:30:21 +00:00
}
2024-08-03 01:21:11 +00:00
internal sealed record GatheringLocationContext(FileInfo File, ushort Id, GatheringRoot Root);
2024-08-02 16:30:21 +00:00
}