341 lines
13 KiB
C#
341 lines
13 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text.Encodings.Web;
|
|
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.Interface.Windowing;
|
|
using Dalamud.Plugin;
|
|
using Dalamud.Plugin.Services;
|
|
using ECommons;
|
|
using ECommons.Schedulers;
|
|
using ECommons.SplatoonAPI;
|
|
using GatheringPathRenderer.Windows;
|
|
using LLib.GameData;
|
|
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 EditorCommands _editorCommands;
|
|
private readonly EditorWindow _editorWindow;
|
|
|
|
private readonly List<GatheringLocationContext> _gatheringLocations = [];
|
|
private EClassJob _currentClassJob;
|
|
|
|
public RendererPlugin(IDalamudPluginInterface pluginInterface, IClientState clientState,
|
|
ICommandManager commandManager, IDataManager dataManager, ITargetManager targetManager, IChatGui chatGui,
|
|
IObjectTable objectTable, IPluginLog pluginLog)
|
|
{
|
|
_pluginInterface = pluginInterface;
|
|
_clientState = clientState;
|
|
_pluginLog = pluginLog;
|
|
|
|
Configuration? configuration = (Configuration?)pluginInterface.GetPluginConfig();
|
|
if (configuration == null)
|
|
{
|
|
configuration = new Configuration();
|
|
pluginInterface.SavePluginConfig(configuration);
|
|
}
|
|
|
|
_editorCommands = new EditorCommands(this, dataManager, commandManager, targetManager, clientState, chatGui,
|
|
configuration);
|
|
_editorWindow = new EditorWindow(this, _editorCommands, dataManager, targetManager, clientState, objectTable)
|
|
{ IsOpen = true };
|
|
_windowSystem.AddWindow(_editorWindow);
|
|
_currentClassJob = (EClassJob?)_clientState.LocalPlayer?.ClassJob.Id ?? EClassJob.Adventurer;
|
|
|
|
_pluginInterface.GetIpcSubscriber<object>("Questionable.ReloadData")
|
|
.Subscribe(Reload);
|
|
|
|
ECommonsMain.Init(pluginInterface, this, Module.SplatoonAPI);
|
|
LoadGatheringLocationsFromDirectory();
|
|
|
|
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
|
|
_clientState.TerritoryChanged += TerritoryChanged;
|
|
_clientState.ClassJobChanged += ClassJobChanged;
|
|
if (_clientState.IsLoggedIn)
|
|
TerritoryChanged(_clientState.TerritoryType);
|
|
}
|
|
|
|
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();
|
|
Redraw();
|
|
}
|
|
|
|
private void LoadGatheringLocationsFromDirectory()
|
|
{
|
|
_gatheringLocations.Clear();
|
|
|
|
try
|
|
{
|
|
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");
|
|
}
|
|
}
|
|
|
|
private void LoadFromDirectory(DirectoryInfo directory)
|
|
{
|
|
if (!directory.Exists)
|
|
return;
|
|
|
|
//_pluginLog.Information($"Loading locations from {directory}");
|
|
foreach (FileInfo fileInfo in directory.GetFiles("*.json"))
|
|
{
|
|
try
|
|
{
|
|
using FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
|
|
LoadLocationFromStream(fileInfo, stream);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
throw new InvalidDataException($"Unable to load file {fileInfo.FullName}", e);
|
|
}
|
|
}
|
|
|
|
foreach (DirectoryInfo childDirectory in directory.GetDirectories())
|
|
LoadFromDirectory(childDirectory);
|
|
}
|
|
|
|
private void LoadLocationFromStream(FileInfo fileInfo, Stream stream)
|
|
{
|
|
var locationNode = JsonNode.Parse(stream)!;
|
|
GatheringRoot root = locationNode.Deserialize<GatheringRoot>()!;
|
|
_gatheringLocations.Add(new GatheringLocationContext(fileInfo, ushort.Parse(fileInfo.Name.Split('_')[0]),
|
|
root));
|
|
}
|
|
|
|
internal IEnumerable<GatheringLocationContext> GetLocationsInTerritory(ushort territoryId)
|
|
=> _gatheringLocations.Where(x => x.Root.Steps.LastOrDefault()?.TerritoryId == territoryId);
|
|
|
|
internal void Save(FileInfo targetFile, GatheringRoot root)
|
|
{
|
|
JsonSerializerOptions options = new()
|
|
{
|
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
|
|
WriteIndented = true,
|
|
TypeInfoResolver = new DefaultJsonTypeInfoResolver
|
|
{
|
|
Modifiers = { NoEmptyCollectionModifier }
|
|
},
|
|
};
|
|
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
|
|
{
|
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
|
Indented = true
|
|
});
|
|
newNode.WriteTo(writer, options);
|
|
}
|
|
|
|
Reload();
|
|
}
|
|
|
|
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 };
|
|
}
|
|
}
|
|
}
|
|
|
|
private void TerritoryChanged(ushort territoryId) => Redraw();
|
|
|
|
private void ClassJobChanged(uint classJobId)
|
|
{
|
|
_currentClassJob = (EClassJob)classJobId;
|
|
Redraw(_currentClassJob);
|
|
}
|
|
|
|
internal void Redraw() => Redraw(_currentClassJob);
|
|
|
|
private void Redraw(EClassJob classJob)
|
|
{
|
|
Splatoon.RemoveDynamicElements("GatheringPathRenderer");
|
|
if (!classJob.IsGatherer())
|
|
return;
|
|
|
|
var elements = GetLocationsInTerritory(_clientState.TerritoryType)
|
|
.SelectMany(location =>
|
|
location.Root.Groups.SelectMany(group =>
|
|
group.Nodes.SelectMany(node => node.Locations
|
|
.SelectMany(x =>
|
|
{
|
|
bool isUnsaved = false;
|
|
bool isCone = false;
|
|
int minimumAngle = 0;
|
|
int maximumAngle = 0;
|
|
if (_editorWindow.TryGetOverride(x.InternalId, out LocationOverride? locationOverride) &&
|
|
locationOverride != null)
|
|
{
|
|
isUnsaved = locationOverride.NeedsSave();
|
|
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();
|
|
}
|
|
|
|
#if false
|
|
var a = GatheringMath.CalculateLandingLocation(x, 0, 0);
|
|
var b = GatheringMath.CalculateLandingLocation(x, 1, 1);
|
|
#endif
|
|
return new List<Element>
|
|
{
|
|
new Element(isCone
|
|
? ElementType.ConeAtFixedCoordinates
|
|
: ElementType.CircleAtFixedCoordinates)
|
|
{
|
|
refX = x.Position.X,
|
|
refY = x.Position.Z,
|
|
refZ = x.Position.Y,
|
|
Filled = true,
|
|
radius = locationOverride?.MinimumDistance ?? x.CalculateMinimumDistance(),
|
|
Donut = (locationOverride?.MaximumDistance ?? x.CalculateMaximumDistance()) -
|
|
(locationOverride?.MinimumDistance ?? x.CalculateMinimumDistance()),
|
|
color = _colors[location.Root.Groups.IndexOf(group) % _colors.Count],
|
|
Enabled = true,
|
|
coneAngleMin = minimumAngle,
|
|
coneAngleMax = maximumAngle,
|
|
tether = false,
|
|
},
|
|
new Element(ElementType.CircleAtFixedCoordinates)
|
|
{
|
|
refX = x.Position.X,
|
|
refY = x.Position.Z,
|
|
refZ = x.Position.Y,
|
|
color = 0xFFFFFFFF,
|
|
radius = 0.1f,
|
|
Enabled = true,
|
|
overlayText =
|
|
$"{location.Root.Groups.IndexOf(group)} // {node.DataId} / {node.Locations.IndexOf(x)}",
|
|
overlayBGColor = isUnsaved ? 0xFF2020FF : 0xFF000000,
|
|
},
|
|
#if false
|
|
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"
|
|
}
|
|
#endif
|
|
};
|
|
}))))
|
|
.ToList();
|
|
|
|
if (elements.Count == 0)
|
|
{
|
|
_pluginLog.Information("No new elements to render.");
|
|
return;
|
|
}
|
|
|
|
_ = new TickScheduler(delegate
|
|
{
|
|
try
|
|
{
|
|
Splatoon.AddDynamicElements("GatheringPathRenderer",
|
|
elements.ToArray(),
|
|
new[] { OnTerritoryChange });
|
|
_pluginLog.Information($"Created {elements.Count} splatoon elements.");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
_pluginLog.Error(e, "Unable to create splatoon layer");
|
|
}
|
|
});
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_clientState.ClassJobChanged -= ClassJobChanged;
|
|
_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);
|
|
}
|