From c0f12a4734a003e2961041a8fee5b2800e023227 Mon Sep 17 00:00:00 2001 From: Anna Date: Sat, 17 Feb 2024 21:36:57 -0500 Subject: [PATCH] feat: start handling metadata --- Command.cs | 38 ++++++++++++-- Configuration.cs | 8 +-- Plugin.cs | 15 ++++++ Screenie.csproj | 12 ++++- ScreenshotMetadata.cs | 118 ++++++++++++++++++++++++++++++++++++++++++ Ui/PluginUi.cs | 12 +++-- packages.lock.json | 32 ++++++++++++ 7 files changed, 222 insertions(+), 13 deletions(-) create mode 100644 ScreenshotMetadata.cs diff --git a/Command.cs b/Command.cs index fe946dc..0d3b3a0 100644 --- a/Command.cs +++ b/Command.cs @@ -1,5 +1,8 @@ +using System.Drawing; using System.Drawing.Imaging; using Dalamud.Game.Command; +using Newtonsoft.Json; +using WebP.Net; namespace Screenie; @@ -26,6 +29,7 @@ internal class Command : IDisposable { private void OnCommand(string command, string arguments) { if (arguments == "config") { this.Plugin.Ui.Visible ^= true; + return; } using var bitmap = Photographer.Capture(); @@ -33,16 +37,36 @@ internal class Command : IDisposable { return; } - var encoder = GetEncoder(this.Plugin.Config.SaveFormat.ToImageFormat()); + var saveAs = this.Plugin.Config.SaveFormat; + if (saveAs.ToImageFormat() is { } format) { + this.SaveNative(format, bitmap); + } else if (saveAs is Format.WebpLossless or Format.WebpLossy) { + using var webp = new WebPObject(bitmap); + var bytes = saveAs == Format.WebpLossless + ? webp.GetWebPLossless() + : webp.GetWebPLossy(this.Plugin.Config.SaveFormatData); + using var stream = this.OpenFile("webp"); + stream.Write(bytes); + } + + var meta = ScreenshotMetadata.Capture(this.Plugin); + var json = JsonConvert.SerializeObject(meta, Formatting.Indented); + Plugin.Log.Info(json); + + this.Plugin.ChatGui.Print("Screenshot saved."); + } + + private void SaveNative(ImageFormat format, Image bitmap) { + var encoder = GetEncoder(format); if (encoder == null) { return; } + // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault var (param, ext) = this.Plugin.Config.SaveFormat switch { Format.Jpg => (Encoder.Quality, "jpg"), - Format.Webp => (Encoder.Quality, "webp"), Format.Png => (Encoder.Compression, "png"), - _ => throw new ArgumentOutOfRangeException(nameof(this.Plugin.Config.SaveFormat), this.Plugin.Config.SaveFormat, null), + _ => throw new ArgumentException("not a native-save format", nameof(format)), }; var parameters = new EncoderParameters(1) { @@ -51,13 +75,17 @@ internal class Command : IDisposable { ], }; + using var stream = this.OpenFile(ext); + bitmap.Save(stream, encoder, parameters); + } + + private FileStream OpenFile(string ext) { Directory.CreateDirectory(this.Plugin.Config.SaveDirectory); var path = Path.ChangeExtension( Path.Join(this.Plugin.Config.SaveDirectory, "screenie"), ext ); - using var stream = new FileStream(path, FileMode.Create, FileAccess.Write); - bitmap.Save(stream, encoder, parameters); + return new FileStream(path, FileMode.Create, FileAccess.Write); } } diff --git a/Configuration.cs b/Configuration.cs index 0c65ad1..c72d5ba 100644 --- a/Configuration.cs +++ b/Configuration.cs @@ -14,15 +14,17 @@ public class Configuration : IPluginConfiguration { public enum Format { Png, - Webp, + WebpLossless, + WebpLossy, Jpg, } public static class FormatExt { - public static ImageFormat ToImageFormat(this Format format) { + public static ImageFormat? ToImageFormat(this Format format) { return format switch { Format.Png => ImageFormat.Png, - Format.Webp => ImageFormat.Webp, + Format.WebpLossless => null, + Format.WebpLossy => null, Format.Jpg => ImageFormat.Jpeg, _ => throw new ArgumentOutOfRangeException(nameof(format), format, null), }; diff --git a/Plugin.cs b/Plugin.cs index a0bed2b..8a750b1 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -14,12 +14,27 @@ public class Plugin : IDalamudPlugin { [PluginService] internal DalamudPluginInterface Interface { get; init; } + [PluginService] + internal IChatGui ChatGui { get; init; } + + [PluginService] + internal IClientState ClientState { get; init; } + [PluginService] internal ICommandManager CommandManager { get; init; } + [PluginService] + internal IDataManager DataManager { get; init; } + [PluginService] internal IFramework Framework { get; init; } + [PluginService] + internal IGameGui GameGui { get; init; } + + [PluginService] + internal IObjectTable ObjectTable { get; init; } + internal Configuration Config { get; } internal PluginUi Ui { get; } diff --git a/Screenie.csproj b/Screenie.csproj index 02cf03c..226da03 100644 --- a/Screenie.csproj +++ b/Screenie.csproj @@ -41,14 +41,24 @@ $(DalamudLibPath)\Newtonsoft.Json.dll false + + $(DalamudLibPath)\Lumina.dll + false + + + $(DalamudLibPath)\Lumina.Excel.dll + false + + all - + + diff --git a/ScreenshotMetadata.cs b/ScreenshotMetadata.cs new file mode 100644 index 0000000..6248a0b --- /dev/null +++ b/ScreenshotMetadata.cs @@ -0,0 +1,118 @@ +using System.Numerics; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Utility; +using FFXIVWeather.Lumina; +using Lumina.Excel.GeneratedSheets; + +namespace Screenie; + +[Serializable] +public class ScreenshotMetadata { + public required string Blake3Hash; + public required Character? ActiveCharacter; + public required string Territory; + public required uint TerritoryId; + public required string World; + public required uint WorldId; + public required DateTime CapturedAtLocal; + public required DateTime CapturedAtUtc; + public required string EorzeaTime; + public required string Weather; + public required uint Ward; + public required uint Plot; + public required Character[] VisibleCharacters; + + private const string Unknown = "Unknown"; + + private static Character GetCharacter(PlayerCharacter player) { + return new Character { + Name = player.Name.TextValue, + HomeWorld = player.HomeWorld.GameData?.Name.ToDalamudString().TextValue ?? Unknown, + HomeWorldId = player.HomeWorld.Id, + Position = player.Position, + Level = player.Level, + Job = player.ClassJob.GameData?.Name.ToDalamudString().TextValue ?? Unknown, + JobId = player.ClassJob.Id, + }; + } + + internal static ScreenshotMetadata Capture(Plugin plugin) { + Character? active = null; + World? world = null; + if (plugin.ClientState.LocalPlayer is { } player) { + world = plugin.DataManager.GetExcelSheet()?.GetRow(player.CurrentWorld.Id); + active = GetCharacter(player); + } + + var timeUtc = DateTime.UtcNow; + var eorzea = GetEorzeaTime(timeUtc); + var territory = plugin.DataManager.GetExcelSheet()?.GetRow(plugin.ClientState.TerritoryType); + var visible = plugin.ObjectTable + .Where(obj => obj is PlayerCharacter) + .Cast() + .Where(chara => plugin.GameGui.WorldToScreen(chara.Position, out _, out var inView) && inView) + .Select(GetCharacter) + .ToArray(); + + var (weather, _) = new FFXIVWeatherLuminaService(plugin.DataManager.GameData) + .GetCurrentWeather(plugin.ClientState.TerritoryType); + + return new ScreenshotMetadata { + Blake3Hash = "", + ActiveCharacter = active, + Territory = territory?.Name.ToDalamudString().TextValue ?? Unknown, + TerritoryId = plugin.ClientState.TerritoryType, + World = world?.Name.ToDalamudString().TextValue ?? Unknown, + WorldId = world?.RowId ?? 0, + CapturedAtLocal = timeUtc.ToLocalTime(), + CapturedAtUtc = timeUtc, + EorzeaTime = $"{eorzea.Hour:00}:{eorzea.Minute:00}", + Weather = weather?.Name.ToDalamudString().TextValue ?? Unknown, + Ward = 0, // TODO + Plot = 0, // TODO + VisibleCharacters = visible, + }; + } + + private static EorzeaTime GetEorzeaTime(DateTime time) { + const double eorzeaTimeConstant = (double) 3600 / 175; + const double year = 33177600; + const double month = 2764800; + const double day = 86400; + const double hour = 3600; + const double minute = 60; + const double second = 1; + + var unix = ((DateTimeOffset) time).ToUnixTimeSeconds(); + var eorzea = (ulong) Math.Floor(unix * eorzeaTimeConstant); + + return new EorzeaTime { + Year = (ulong) Math.Floor(eorzea / year) + 1, + Month = (ulong) Math.Floor(eorzea / month % 12) + 1, + Day = (ulong) Math.Floor(eorzea / day % 32) + 1, + Hour = (ulong) Math.Floor(eorzea / hour % 24), + Minute = (ulong) Math.Floor(eorzea / minute % 60), + Second = (ulong) Math.Floor(eorzea / second % 60), + }; + } +} + +[Serializable] +public class Character { + public required string Name; + public required string HomeWorld; + public required uint HomeWorldId; + public required Vector3 Position; + public required uint Level; + public required string Job; + public required uint JobId; +} + +public struct EorzeaTime { + public required ulong Year; + public required ulong Month; + public required ulong Day; + public required ulong Hour; + public required ulong Minute; + public required ulong Second; +} diff --git a/Ui/PluginUi.cs b/Ui/PluginUi.cs index bc186d7..38cd7a7 100644 --- a/Ui/PluginUi.cs +++ b/Ui/PluginUi.cs @@ -52,6 +52,7 @@ internal class PluginUi : IDisposable { var anyChanged = false; anyChanged |= this.DrawScreenshotsFolderInput(); + ImGui.TextUnformatted("Save format"); ImGui.SetNextItemWidth(-1); if (ImGui.BeginCombo("##file-format", Enum.GetName(this.Plugin.Config.SaveFormat))) { using var endCombo = new OnDispose(ImGui.EndCombo); @@ -65,14 +66,17 @@ internal class PluginUi : IDisposable { var label = this.Plugin.Config.SaveFormat switch { Format.Jpg => "Quality", - Format.Webp => "Quality", + Format.WebpLossless => null, + Format.WebpLossy => "Quality", Format.Png => "Compression level", _ => "Unknown", }; - ImGui.TextUnformatted(label); - ImGui.SetNextItemWidth(-1); - anyChanged |= ImGui.SliderInt("##format-data", ref this.Plugin.Config.SaveFormatData, 0, 100); + if (label != null) { + ImGui.TextUnformatted(label); + ImGui.SetNextItemWidth(-1); + anyChanged |= ImGui.SliderInt("##format-data", ref this.Plugin.Config.SaveFormatData, 0, 100); + } if (anyChanged) { this.Plugin.SaveConfig(); diff --git a/packages.lock.json b/packages.lock.json index 0d9c22e..a8f040e 100644 --- a/packages.lock.json +++ b/packages.lock.json @@ -8,6 +8,16 @@ "resolved": "2.1.12", "contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg==" }, + "FFXIVWeather.Lumina": { + "type": "Direct", + "requested": "[2.2.0, )", + "resolved": "2.2.0", + "contentHash": "zGYBjw7iRY3fUYuDGDaroiPES123esZ07th+H/cKsIZsfs5960B9EDjyEHxu1NlA0txKMGLbNMjINUmE4XImsg==", + "dependencies": { + "Lumina": "3.9.0", + "Lumina.Excel": "6.2.0" + } + }, "Microsoft.Windows.CsWin32": { "type": "Direct", "requested": "[0.3.49-beta, )", @@ -30,6 +40,28 @@ "Microsoft.Win32.SystemEvents": "8.0.0" } }, + "WebP_Net": { + "type": "Direct", + "requested": "[1.1.1, )", + "resolved": "1.1.1", + "contentHash": "dFMBV4TXUbbIkspxa/Pb3420Qrel9AsHdthWHCwswn1D7dTa7CGb47ON10C1ERqcFaYdD0CgMUMNv2x4fJr4hg==", + "dependencies": { + "System.Drawing.Common": "7.0.0" + } + }, + "Lumina": { + "type": "Transitive", + "resolved": "3.9.0", + "contentHash": "2ADC9iN8yUHXELq3IIQAK1cvi2kp53l1CDmAnrsTwBXJ9o9anIC+X6TzFRxWuRvTVI/MEMHTjwBobGwBGc3XhQ==" + }, + "Lumina.Excel": { + "type": "Transitive", + "resolved": "6.2.0", + "contentHash": "gIPr/Q4HhYDL65h/9b0srdL+Nfxk90T7aIciLnpl/QaBdc7tGRPzUmq0FU2QjErkikhrFOrb4Fp5NkQaN3DQDA==", + "dependencies": { + "Lumina": "3.9.0" + } + }, "Microsoft.Win32.SystemEvents": { "type": "Transitive", "resolved": "8.0.0",