diff --git a/Influx/AllaganTools/AllaganToolsIpc.cs b/Influx/AllaganTools/AllaganToolsIpc.cs new file mode 100644 index 0000000..d2302d3 --- /dev/null +++ b/Influx/AllaganTools/AllaganToolsIpc.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game.Gui; +using Dalamud.Logging; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using Dalamud.Plugin.Ipc.Exceptions; +using ECommons.Reflection; + +namespace Influx.AllaganTools; + +internal sealed class AllaganToolsIpc : IDisposable +{ + private readonly DalamudPluginInterface _pluginInterface; + private readonly ChatGui _chatGui; + private readonly Configuration _configuration; + private readonly ICallGateSubscriber? _initalized; + private readonly ICallGateSubscriber? _isInitialized; + + public ICharacterMonitor Characters { get; private set; } = new UnavailableCharacterMonitor(); + public IInventoryMonitor Inventories { get; private set; } = new UnavailableInventoryMonitor(); + + public AllaganToolsIpc(DalamudPluginInterface pluginInterface, ChatGui chatGui, Configuration configuration) + { + _pluginInterface = pluginInterface; + _chatGui = chatGui; + _configuration = configuration; + + _initalized = _pluginInterface.GetIpcSubscriber("AllaganTools.Initialized"); + _isInitialized = _pluginInterface.GetIpcSubscriber("AllaganTools.IsInitialized"); + _initalized.Subscribe(ConfigureIpc); + + try + { + bool isInitialized = _isInitialized.InvokeFunc(); + if (isInitialized) + ConfigureIpc(true); + } + catch (IpcNotReadyError e) + { + PluginLog.Debug(e, "Not initializing ATools yet"); + } + } + + private void ConfigureIpc(bool initialized) + { + try + { + if (DalamudReflector.TryGetDalamudPlugin("Allagan Tools", out var it, false, true) && + _isInitialized != null && _isInitialized.InvokeFunc()) + { + var pluginService = it.GetType().Assembly.GetType("InventoryTools.PluginService")!; + + Characters = new CharacterMonitor(pluginService.GetProperty("CharacterMonitor")!.GetValue(null)!); + Inventories = new InventoryMonitor( + pluginService.GetProperty("InventoryMonitor")!.GetValue(null)!); + } + } + catch (Exception e) + { + PluginLog.Error(e, "Could not initialize IPC"); + _chatGui.PrintError(e.ToString()); + } + } + + public Dictionary CountCurrencies() + { + var characters = Characters.All.ToDictionary(x => x.CharacterId, x => x); + return Inventories.All + .Where(x => !_configuration.ExcludedCharacters.Contains(x.Key)) + .ToDictionary( + x => characters[x.Value.CharacterId], + y => + { + var inv = new InventoryWrapper(y.Value.GetAllItems()); + return new Currencies + { + Gil = inv.Sum(1), + FcCredits = inv.Sum(80), + Ventures = inv.Sum(21072), + CeruleumTanks = inv.Sum(10155), + RepairKits = inv.Sum(10373), + }; + }); + } + + public void Dispose() + { + _initalized?.Unsubscribe(ConfigureIpc); + Characters = new UnavailableCharacterMonitor(); + Inventories = new UnavailableInventoryMonitor(); + } + + private sealed class InventoryWrapper + { + private readonly IEnumerable _items; + + public InventoryWrapper(IEnumerable items) + { + _items = items; + } + + public long Sum(int itemId) => _items.Where(x => x.ItemId == itemId).Sum(x => x.Quantity); + } +} diff --git a/Influx/AllaganTools/Character.cs b/Influx/AllaganTools/Character.cs new file mode 100644 index 0000000..8d793a3 --- /dev/null +++ b/Influx/AllaganTools/Character.cs @@ -0,0 +1,26 @@ +using System.Reflection; + +namespace Influx.AllaganTools; + +internal sealed class Character +{ + private readonly object _delegate; + private readonly FieldInfo _name; + + public Character(object @delegate) + { + _delegate = @delegate; + _name = _delegate.GetType().GetField("Name")!; + + CharacterId = (ulong)_delegate.GetType().GetField("CharacterId")!.GetValue(_delegate)!; + CharacterType = (CharacterType)_delegate.GetType().GetProperty("CharacterType")!.GetValue(_delegate)!; + OwnerId = (ulong)_delegate.GetType().GetField("OwnerId")!.GetValue(_delegate)!; + FreeCompanyId = (ulong)_delegate.GetType().GetField("FreeCompanyId")!.GetValue(_delegate)!; + } + + public ulong CharacterId { get; } + public CharacterType CharacterType { get; } + public ulong OwnerId { get; } + public ulong FreeCompanyId { get; } + public string Name => (string)_name.GetValue(_delegate)!; +} diff --git a/Influx/AllaganTools/CharacterMonitor.cs b/Influx/AllaganTools/CharacterMonitor.cs new file mode 100644 index 0000000..cf69e96 --- /dev/null +++ b/Influx/AllaganTools/CharacterMonitor.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Influx.AllaganTools; + +internal sealed class CharacterMonitor : ICharacterMonitor +{ + private readonly object _delegate; + private readonly MethodInfo _getPlayerCharacters; + private readonly MethodInfo _allCharacters; + + public CharacterMonitor(object @delegate) + { + _delegate = @delegate; + _getPlayerCharacters = _delegate.GetType().GetMethod("GetPlayerCharacters")!; + _allCharacters = _delegate.GetType().GetMethod("AllCharacters")!; + } + + public IEnumerable PlayerCharacters => GetCharactersInternal(_getPlayerCharacters); + public IEnumerable All => GetCharactersInternal(_allCharacters); + + private IEnumerable GetCharactersInternal(MethodInfo methodInfo) + { + return ((IEnumerable)methodInfo.Invoke(_delegate, Array.Empty())!) + .Cast() + .Select(x => x.GetType().GetProperty("Value")!.GetValue(x)!) + .Select(x => new Character(x)) + .ToList(); + } +} diff --git a/Influx/AllaganTools/CharacterType.cs b/Influx/AllaganTools/CharacterType.cs new file mode 100644 index 0000000..c756c5c --- /dev/null +++ b/Influx/AllaganTools/CharacterType.cs @@ -0,0 +1,10 @@ +namespace Influx.AllaganTools; + +internal enum CharacterType +{ + Character, + Retainer, + FreeCompanyChest, + Housing, + Unknown, +} diff --git a/Influx/AllaganTools/Currencies.cs b/Influx/AllaganTools/Currencies.cs new file mode 100644 index 0000000..274db81 --- /dev/null +++ b/Influx/AllaganTools/Currencies.cs @@ -0,0 +1,11 @@ +namespace Influx.AllaganTools; + +internal struct Currencies +{ + public long Gil { get; init; } + public long GcSeals { get; init; } + public long FcCredits { get; init; } + public long Ventures { get; init; } + public long CeruleumTanks { get; init; } + public long RepairKits { get; init; } +} diff --git a/Influx/AllaganTools/ICharacterMonitor.cs b/Influx/AllaganTools/ICharacterMonitor.cs new file mode 100644 index 0000000..dc56953 --- /dev/null +++ b/Influx/AllaganTools/ICharacterMonitor.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Influx.AllaganTools; + +internal interface ICharacterMonitor +{ + IEnumerable PlayerCharacters { get; } + IEnumerable All { get; } +} diff --git a/Influx/AllaganTools/IInventoryMonitor.cs b/Influx/AllaganTools/IInventoryMonitor.cs new file mode 100644 index 0000000..d28ff1e --- /dev/null +++ b/Influx/AllaganTools/IInventoryMonitor.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Influx.AllaganTools; + +internal interface IInventoryMonitor +{ + public IReadOnlyDictionary All { get; } +} diff --git a/Influx/AllaganTools/Inventory.cs b/Influx/AllaganTools/Inventory.cs new file mode 100644 index 0000000..3a4ff80 --- /dev/null +++ b/Influx/AllaganTools/Inventory.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Influx.AllaganTools; + +internal sealed class Inventory +{ + private readonly object _delegate; + private readonly MethodInfo _getAllInventories; + + public Inventory(object @delegate) + { + _delegate = @delegate; + _getAllInventories = _delegate.GetType().GetMethod("GetAllInventories")!; + CharacterId = (ulong)_delegate.GetType().GetProperty("CharacterId")!.GetValue(_delegate)!; + } + + public ulong CharacterId { get; } + + public IEnumerable GetAllItems() => + ((IEnumerable)_getAllInventories.Invoke(_delegate, Array.Empty())!) + .Cast() + .SelectMany(x => x.Cast()) + .Select(x => new InventoryItem(x)) + .ToList(); +} diff --git a/Influx/AllaganTools/InventoryItem.cs b/Influx/AllaganTools/InventoryItem.cs new file mode 100644 index 0000000..b640b17 --- /dev/null +++ b/Influx/AllaganTools/InventoryItem.cs @@ -0,0 +1,16 @@ +namespace Influx.AllaganTools; + +internal sealed class InventoryItem +{ + private readonly object _delegate; + + public InventoryItem(object @delegate) + { + _delegate = @delegate; + ItemId = (uint)_delegate.GetType().GetField("ItemId")!.GetValue(_delegate)!; + Quantity = (uint)_delegate.GetType().GetField("Quantity")!.GetValue(_delegate)!; + } + + public uint ItemId { get; } + public uint Quantity { get; } +} diff --git a/Influx/AllaganTools/InventoryMonitor.cs b/Influx/AllaganTools/InventoryMonitor.cs new file mode 100644 index 0000000..eb40780 --- /dev/null +++ b/Influx/AllaganTools/InventoryMonitor.cs @@ -0,0 +1,25 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Influx.AllaganTools; + +internal sealed class InventoryMonitor : IInventoryMonitor +{ + private readonly object _delegate; + private readonly PropertyInfo _inventories; + + public InventoryMonitor(object @delegate) + { + _delegate = @delegate; + _inventories = _delegate.GetType().GetProperty("Inventories")!; + } + + public IReadOnlyDictionary All => + ((IEnumerable)_inventories.GetValue(_delegate)!) + .Cast() + .Select(x => x.GetType().GetProperty("Value")!.GetValue(x)!) + .Select(x => new Inventory(x)) + .ToDictionary(x => x.CharacterId, x => x); +} diff --git a/Influx/AllaganTools/UnavailableCharacterMonitor.cs b/Influx/AllaganTools/UnavailableCharacterMonitor.cs new file mode 100644 index 0000000..1f43961 --- /dev/null +++ b/Influx/AllaganTools/UnavailableCharacterMonitor.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace Influx.AllaganTools; + +internal sealed class UnavailableCharacterMonitor : ICharacterMonitor +{ + public IEnumerable PlayerCharacters => Array.Empty(); + public IEnumerable All => Array.Empty(); +} diff --git a/Influx/AllaganTools/UnavailableInventoryMonitor.cs b/Influx/AllaganTools/UnavailableInventoryMonitor.cs new file mode 100644 index 0000000..771c411 --- /dev/null +++ b/Influx/AllaganTools/UnavailableInventoryMonitor.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Influx.AllaganTools; + +internal sealed class UnavailableInventoryMonitor : IInventoryMonitor +{ + public IReadOnlyDictionary All => new Dictionary(); +} diff --git a/Influx/AllaganToolsIPC.cs b/Influx/AllaganToolsIPC.cs deleted file mode 100644 index 998e9c6..0000000 --- a/Influx/AllaganToolsIPC.cs +++ /dev/null @@ -1,219 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Dalamud.Game.ClientState; -using Dalamud.Game.Gui; -using Dalamud.Plugin; -using Dalamud.Plugin.Ipc; -using ECommons.Reflection; - -namespace Influx; - -internal sealed class AllaganToolsIPC : IDisposable -{ - private readonly DalamudPluginInterface _pluginInterface; - private readonly ChatGui _chatGui; - private readonly Configuration _configuration; - private readonly ICallGateSubscriber? _initalized; - private readonly ICallGateSubscriber? _isInitialized; - - public CharacterMonitor Characters { get; private set; } - public InventoryMonitor Inventories { get; private set; } - - public AllaganToolsIPC(DalamudPluginInterface pluginInterface, ChatGui chatGui, Configuration configuration) - { - _pluginInterface = pluginInterface; - _chatGui = chatGui; - _configuration = configuration; - - _initalized = _pluginInterface.GetIpcSubscriber("AllaganTools.Initialized"); - _isInitialized = _pluginInterface.GetIpcSubscriber("AllaganTools.IsInitialized"); - _initalized.Subscribe(ConfigureIpc); - - ConfigureIpc(true); - } - - private void ConfigureIpc(bool initialized) - { - try - { - if (DalamudReflector.TryGetDalamudPlugin("Allagan Tools", out var it, false, true) && - _isInitialized != null && _isInitialized.InvokeFunc()) - { - var pluginService = it.GetType().Assembly.GetType("InventoryTools.PluginService")!; - - Characters = new CharacterMonitor(pluginService.GetProperty("CharacterMonitor")!.GetValue(null)!); - Inventories = new InventoryMonitor( - pluginService.GetProperty("InventoryMonitor")!.GetValue(null)!); - } - } - catch (Exception e) - { - _chatGui.PrintError(e.ToString()); - } - } - - public Dictionary CountCurrencies() - { - var characters = Characters.All.ToDictionary(x => x.CharacterId, x => x); - return Inventories.All - .Where(x => !_configuration.ExcludedCharacters.Contains(x.Key)) - .ToDictionary( - x => characters[x.Value.CharacterId], - y => - { - var inv = new InventoryWrapper(y.Value.GetAllItems()); - return new Currencies - { - Gil = inv.Sum(1), - FcCredits = inv.Sum(80), - Ventures = inv.Sum(21072), - CeruleumTanks = inv.Sum(10155), - RepairKits = inv.Sum(10373), - }; - }); - } - - public void Dispose() - { - _initalized?.Unsubscribe(ConfigureIpc); - } - - public class CharacterMonitor - { - private readonly object _delegate; - private readonly MethodInfo _getPlayerCharacters; - private readonly MethodInfo _allCharacters; - - public CharacterMonitor(object @delegate) - { - _delegate = @delegate; - _getPlayerCharacters = _delegate.GetType().GetMethod("GetPlayerCharacters")!; - _allCharacters = _delegate.GetType().GetMethod("AllCharacters")!; - } - - public IEnumerable PlayerCharacters => GetCharactersInternal(_getPlayerCharacters); - public IEnumerable All => GetCharactersInternal(_allCharacters); - - private IEnumerable GetCharactersInternal(MethodInfo methodInfo) - { - return ((IEnumerable)methodInfo.Invoke(_delegate, Array.Empty())!) - .Cast() - .Select(x => x.GetType().GetProperty("Value")!.GetValue(x)!) - .Select(x => new Character(x)) - .ToList(); - } - } - - public class Character - { - private readonly object _delegate; - private readonly FieldInfo _name; - - public Character(object @delegate) - { - _delegate = @delegate; - _name = _delegate.GetType().GetField("Name")!; - - CharacterId = (ulong)_delegate.GetType().GetField("CharacterId")!.GetValue(_delegate)!; - CharacterType = (CharacterType)_delegate.GetType().GetProperty("CharacterType")!.GetValue(_delegate)!; - OwnerId = (ulong)_delegate.GetType().GetField("OwnerId")!.GetValue(_delegate)!; - FreeCompanyId = (ulong)_delegate.GetType().GetField("FreeCompanyId")!.GetValue(_delegate)!; - } - - public ulong CharacterId { get; } - public CharacterType CharacterType { get; } - public ulong OwnerId { get; } - public ulong FreeCompanyId { get; } - public string Name => (string)_name.GetValue(_delegate)!; - } - - public enum CharacterType - { - Character, - Retainer, - FreeCompanyChest, - Housing, - Unknown, - } - - public struct Currencies - { - public long Gil { get; init; } - public long GcSeals { get; init; } - public long FcCredits { get; init; } - public long Ventures { get; init; } - public long CeruleumTanks { get; init; } - public long RepairKits { get; init; } - } - - public sealed class InventoryMonitor - { - private readonly object _delegate; - private readonly PropertyInfo _inventories; - - public InventoryMonitor(object @delegate) - { - _delegate = @delegate; - _inventories = _delegate.GetType().GetProperty("Inventories")!; - } - - public IReadOnlyDictionary All => - ((IEnumerable)_inventories.GetValue(_delegate)!) - .Cast() - .Select(x => x.GetType().GetProperty("Value")!.GetValue(x)!) - .Select(x => new Inventory(x)) - .ToDictionary(x => x.CharacterId, x => x); - } - - public sealed class Inventory - { - private readonly object _delegate; - private readonly MethodInfo _getAllInventories; - - public Inventory(object @delegate) - { - _delegate = @delegate; - _getAllInventories = _delegate.GetType().GetMethod("GetAllInventories")!; - CharacterId = (ulong)_delegate.GetType().GetProperty("CharacterId")!.GetValue(_delegate)!; - } - - public ulong CharacterId { get; } - - public IEnumerable GetAllItems() => - ((IEnumerable)_getAllInventories.Invoke(_delegate, Array.Empty())!) - .Cast() - .SelectMany(x => x.Cast()) - .Select(x => new InventoryItem(x)) - .ToList(); - } - - public sealed class InventoryItem - { - private readonly object _delegate; - - public InventoryItem(object @delegate) - { - _delegate = @delegate; - ItemId = (uint)_delegate.GetType().GetField("ItemId")!.GetValue(_delegate)!; - Quantity = (uint)_delegate.GetType().GetField("Quantity")!.GetValue(_delegate)!; - } - - public uint ItemId { get; } - public uint Quantity { get; } - } - - public sealed class InventoryWrapper - { - private readonly IEnumerable _items; - - public InventoryWrapper(IEnumerable items) - { - _items = items; - } - - public long Sum(int itemId) => _items.Where(x => x.ItemId == itemId).Sum(x => x.Quantity); - } -} diff --git a/Influx/Influx/InfluxStatisticsClient.cs b/Influx/Influx/InfluxStatisticsClient.cs index eca6a6a..ea84013 100644 --- a/Influx/Influx/InfluxStatisticsClient.cs +++ b/Influx/Influx/InfluxStatisticsClient.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Dalamud.Game.Gui; +using Influx.AllaganTools; using InfluxDB.Client; using InfluxDB.Client.Api.Domain; using InfluxDB.Client.Writes; @@ -36,10 +37,10 @@ internal class InfluxStatisticsClient : IDisposable return; DateTime date = DateTime.UtcNow; - IReadOnlyDictionary stats = update.Currencies; + IReadOnlyDictionary stats = update.Currencies; var validFcIds = stats.Keys - .Where(x => x.CharacterType == AllaganToolsIPC.CharacterType.Character) + .Where(x => x.CharacterType == CharacterType.Character) .Select(x => x.FreeCompanyId) .ToList(); Task.Run(async () => @@ -49,7 +50,7 @@ internal class InfluxStatisticsClient : IDisposable List values = new(); foreach (var (character, currencies) in stats) { - if (character.CharacterType == AllaganToolsIPC.CharacterType.Character) + if (character.CharacterType == CharacterType.Character) { values.Add(PointData.Measurement("currency") .Tag("id", character.CharacterId.ToString()) @@ -61,7 +62,7 @@ internal class InfluxStatisticsClient : IDisposable .Field("repair_kits", currencies.RepairKits) .Timestamp(date, WritePrecision.S)); } - else if (character.CharacterType == AllaganToolsIPC.CharacterType.Retainer) + else if (character.CharacterType == CharacterType.Retainer) { var owner = stats.Keys.First(x => x.CharacterId == character.OwnerId); values.Add(PointData.Measurement("currency") @@ -74,7 +75,7 @@ internal class InfluxStatisticsClient : IDisposable .Field("repair_kits", currencies.RepairKits) .Timestamp(date, WritePrecision.S)); } - else if (character.CharacterType == AllaganToolsIPC.CharacterType.FreeCompanyChest && + else if (character.CharacterType == CharacterType.FreeCompanyChest && validFcIds.Contains(character.CharacterId)) { values.Add(PointData.Measurement("currency") diff --git a/Influx/InfluxPlugin.cs b/Influx/InfluxPlugin.cs index f7134dd..2760fdc 100644 --- a/Influx/InfluxPlugin.cs +++ b/Influx/InfluxPlugin.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Threading; -using System.Threading.Tasks; using Dalamud.Game.ClientState; using Dalamud.Game.Command; using Dalamud.Game.Gui; @@ -11,11 +8,9 @@ using Dalamud.Interface.Windowing; using Dalamud.Logging; using Dalamud.Plugin; using ECommons; +using Influx.AllaganTools; using Influx.Influx; using Influx.Windows; -using InfluxDB.Client; -using InfluxDB.Client.Api.Domain; -using InfluxDB.Client.Writes; namespace Influx; @@ -28,7 +23,7 @@ public class InfluxPlugin : IDalamudPlugin private readonly Configuration _configuration; private readonly ClientState _clientState; private readonly CommandManager _commandManager; - private readonly AllaganToolsIPC _allaganToolsIpc; + private readonly AllaganToolsIpc _allaganToolsIpc; private readonly InfluxStatisticsClient _influxStatisticsClient; private readonly WindowSystem _windowSystem; private readonly StatisticsWindow _statisticsWindow; @@ -44,7 +39,7 @@ public class InfluxPlugin : IDalamudPlugin _configuration = LoadConfig(); _clientState = clientState; _commandManager = commandManager; - _allaganToolsIpc = new AllaganToolsIPC(pluginInterface, chatGui, _configuration); + _allaganToolsIpc = new AllaganToolsIpc(pluginInterface, chatGui, _configuration); _influxStatisticsClient = new InfluxStatisticsClient(chatGui, _configuration); _windowSystem = new WindowSystem(typeof(InfluxPlugin).FullName); diff --git a/Influx/StatisticsUpdate.cs b/Influx/StatisticsUpdate.cs index 5e730d6..3344ada 100644 --- a/Influx/StatisticsUpdate.cs +++ b/Influx/StatisticsUpdate.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; +using Influx.AllaganTools; namespace Influx; internal sealed class StatisticsUpdate { - public IReadOnlyDictionary Currencies { get; init; } + public required IReadOnlyDictionary Currencies { get; init; } } diff --git a/Influx/Windows/StatisticsWindow.cs b/Influx/Windows/StatisticsWindow.cs index c352a7e..a20f6c4 100644 --- a/Influx/Windows/StatisticsWindow.cs +++ b/Influx/Windows/StatisticsWindow.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Numerics; using Dalamud.Interface.Windowing; using ImGuiNET; +using Influx.AllaganTools; namespace Influx.Windows; @@ -46,14 +46,14 @@ internal sealed class StatisticsWindow : Window public void OnStatisticsUpdate(StatisticsUpdate update) { var retainers = update.Currencies - .Where(x => x.Key.CharacterType == AllaganToolsIPC.CharacterType.Retainer) + .Where(x => x.Key.CharacterType == CharacterType.Retainer) .GroupBy(x => update.Currencies.FirstOrDefault(y => y.Key.CharacterId == x.Key.OwnerId).Key) .ToDictionary(x => x.Key, x => x.Select(y => y.Value).ToList()); - _rows = update.Currencies.Where(x => x.Key.CharacterType == AllaganToolsIPC.CharacterType.Character) + _rows = update.Currencies.Where(x => x.Key.CharacterType == CharacterType.Character) .Select(x => { - var currencies = new List { x.Value }; + var currencies = new List { x.Value }; if (retainers.TryGetValue(x.Key, out var retainerCurrencies)) currencies.AddRange(retainerCurrencies); return new StatisticsRow @@ -71,8 +71,8 @@ internal sealed class StatisticsWindow : Window public sealed class StatisticsRow { - public string Name { get; init; } - public string Type { get; init; } + public required string Name { get; init; } + public required string Type { get; init; } public long Gil { get; init; } public long FcCredits { get; init; } }