From 0d232e1fa0d16d912fe47b53b924bc7204cadbfb Mon Sep 17 00:00:00 2001 From: Zeffuro Date: Mon, 29 Dec 2025 23:40:05 +0100 Subject: [PATCH] WIP Abstraction --- .../AddonLifecycles/InventoryLifecycles.cs | 1 + AetherBags/Addons/AddonInventoryWindow.cs | 251 +++--------------- AetherBags/Addons/CategoryWrapper.cs | 1 + AetherBags/Addons/InventoryAddonBase.cs | 208 +++++++++++++++ AetherBags/Commands/CommandHandler.cs | 1 + .../{ => Categories}/CategorizedInventory.cs | 3 +- .../{ => Categories}/CategoryBucketManager.cs | 5 +- .../{ => Categories}/CategoryInfo.cs | 2 +- .../{ => Categories}/InventoryFilter.cs | 3 +- .../{ => Categories}/UserCategoryMatcher.cs | 5 +- .../{ => Context}/InventoryContextState.cs | 2 +- .../InventoryNotificationState.cs | 2 +- .../Inventory/{ => Items}/InventoryStats.cs | 2 +- AetherBags/Inventory/{ => Items}/ItemInfo.cs | 9 +- .../Inventory/{ => Items}/LootedItemInfo.cs | 2 +- .../Inventory/Scanning/AggregatedItem.cs | 9 + .../{ => Scanning}/InventoryScanner.cs | 63 +++-- .../Inventory/Scanning/InventorySource.cs | 56 ++++ .../Inventory/{ => State}/InventoryState.cs | 10 +- .../Inventory/State/InventoryStateBase.cs | 109 ++++++++ AetherBags/Inventory/State/MainBagState.cs | 17 ++ AetherBags/Inventory/State/SaddleBagState.cs | 10 + .../CategoryDefinitionConfigurationNode.cs | 1 + .../Nodes/Inventory/InventoryCategoryNode.cs | 7 +- .../Nodes/Inventory/InventoryDragDropNode.cs | 1 + .../Nodes/Inventory/InventoryFooterNode.cs | 1 + .../Inventory/InventoryNotificationNode.cs | 1 + AetherBags/Plugin.cs | 1 + AetherBags/System.cs | 2 + 29 files changed, 529 insertions(+), 256 deletions(-) create mode 100644 AetherBags/Addons/InventoryAddonBase.cs rename AetherBags/Inventory/{ => Categories}/CategorizedInventory.cs (64%) rename AetherBags/Inventory/{ => Categories}/CategoryBucketManager.cs (99%) rename AetherBags/Inventory/{ => Categories}/CategoryInfo.cs (87%) rename AetherBags/Inventory/{ => Categories}/InventoryFilter.cs (96%) rename AetherBags/Inventory/{ => Categories}/UserCategoryMatcher.cs (98%) rename AetherBags/Inventory/{ => Context}/InventoryContextState.cs (98%) rename AetherBags/Inventory/{ => Context}/InventoryNotificationState.cs (99%) rename AetherBags/Inventory/{ => Items}/InventoryStats.cs (91%) rename AetherBags/Inventory/{ => Items}/ItemInfo.cs (98%) rename AetherBags/Inventory/{ => Items}/LootedItemInfo.cs (76%) create mode 100644 AetherBags/Inventory/Scanning/AggregatedItem.cs rename AetherBags/Inventory/{ => Scanning}/InventoryScanner.cs (74%) create mode 100644 AetherBags/Inventory/Scanning/InventorySource.cs rename AetherBags/Inventory/{ => State}/InventoryState.cs (97%) create mode 100644 AetherBags/Inventory/State/InventoryStateBase.cs create mode 100644 AetherBags/Inventory/State/MainBagState.cs create mode 100644 AetherBags/Inventory/State/SaddleBagState.cs diff --git a/AetherBags/AddonLifecycles/InventoryLifecycles.cs b/AetherBags/AddonLifecycles/InventoryLifecycles.cs index 04f3352..f534a7e 100644 --- a/AetherBags/AddonLifecycles/InventoryLifecycles.cs +++ b/AetherBags/AddonLifecycles/InventoryLifecycles.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using AetherBags.Configuration; using AetherBags.Inventory; +using AetherBags.Inventory.Context; using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.NativeWrapper; diff --git a/AetherBags/Addons/AddonInventoryWindow.cs b/AetherBags/Addons/AddonInventoryWindow.cs index faf0d95..5848543 100644 --- a/AetherBags/Addons/AddonInventoryWindow.cs +++ b/AetherBags/Addons/AddonInventoryWindow.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Numerics; -using AetherBags.Inventory; +using AetherBags.Inventory.Context; +using AetherBags.Inventory.State; using AetherBags.Nodes.Input; using AetherBags.Nodes.Inventory; using AetherBags.Nodes.Layout; @@ -11,43 +9,20 @@ using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Component.GUI; -using KamiToolKit; using KamiToolKit.Nodes; namespace AetherBags.Addons; -public class AddonInventoryWindow : NativeAddon +public unsafe class AddonInventoryWindow : InventoryAddonBase { - private readonly InventoryCategoryHoverCoordinator _hoverCoordinator = new(); - private readonly InventoryCategoryPinCoordinator _pinCoordinator = new(); - private readonly HashSet _hoverSubscribed = new(); - + private readonly MainBagState _inventoryState = new(); private InventoryNotificationNode _notificationNode = null!; - private WrappingGridNode _categoriesNode = null!; - private TextInputWithHintNode _searchInputNode = null!; - private CircleButtonNode _settingsButtonNode = null!; - private InventoryFooterNode _footerNode = null!; - // Window constraints - private const float MinWindowWidth = 300; - private const float MaxWindowWidth = 800; - private const float MinWindowHeight = 200; - private const float MaxWindowHeight = 1000; + protected override InventoryStateBase InventoryState => _inventoryState; - // Layout settings - private const float CategorySpacing = 12; - private const float ItemSize = 40; - private const float ItemPadding = 4; - - private const float FooterHeight = 28f; - private const float FooterTopSpacing = 4f; - - private bool _refreshQueued; - private bool _refreshAutosizeQueued; - - protected override unsafe void OnSetup(AtkUnitBase* addon) + protected override void OnSetup(AtkUnitBase* addon) { - _categoriesNode = new WrappingGridNode + CategoriesNode = new WrappingGridNode { Position = ContentStartPosition, Size = ContentSize, @@ -56,7 +31,7 @@ public class AddonInventoryWindow : NativeAddon TopPadding = 4.0f, BottomPadding = 4.0f, }; - _categoriesNode.AttachNode(this); + CategoriesNode.AttachNode(this); var size = new Vector2(addon->Size.X / 2.0f, 28.0f); @@ -77,49 +52,48 @@ public class AddonInventoryWindow : NativeAddon }; _notificationNode.AttachNode(this); - _searchInputNode = new TextInputWithHintNode + SearchInputNode = new TextInputWithHintNode { Position = new Vector2(x, y), Size = size, OnInputReceived = _ => RefreshCategoriesCore(autosize: false), }; - _searchInputNode.AttachNode(this); + SearchInputNode.AttachNode(this); - _settingsButtonNode = new CircleButtonNode + SettingsButtonNode = new CircleButtonNode { Position = new Vector2(headerW - 48f, y), Size = new Vector2(28f), Icon = ButtonIcon.GearCog, OnClick = System.AddonConfigurationWindow.Toggle }; - _settingsButtonNode.AttachNode(this); + SettingsButtonNode.AttachNode(this); - _footerNode = new InventoryFooterNode + FooterNode = new InventoryFooterNode { Size = ContentSize with { Y = FooterHeight }, - SlotAmountText = InventoryState.GetEmptyItemSlotsString(), + SlotAmountText = _inventoryState.GetEmptySlotsString(), }; - _footerNode.AttachNode(this); + FooterNode.AttachNode(this); LayoutContent(); Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate); addon->SubscribeAtkArrayData(1, (int)NumberArrayType.Inventory); - InventoryState.RefreshFromGame(); - - RefreshCategoriesCore(autosize: true); + _inventoryState.RefreshFromGame(); + RefreshCategoriesCore(autosize: true); base.OnSetup(addon); } - protected override unsafe void OnUpdate(AtkUnitBase* addon) + protected override void OnUpdate(AtkUnitBase* addon) { - if (_refreshQueued) + if (RefreshQueued) { - bool doAutosize = _refreshAutosizeQueued; - _refreshQueued = false; - _refreshAutosizeQueued = false; + bool doAutosize = RefreshAutosizeQueued; + RefreshQueued = false; + RefreshAutosizeQueued = false; RefreshCategoriesCore(doAutosize); } @@ -127,191 +101,31 @@ public class AddonInventoryWindow : NativeAddon base.OnUpdate(addon); } - public void ManualInventoryRefresh() - { - if (!Services.ClientState.IsLoggedIn) return; - InventoryState.RefreshFromGame(); - RefreshCategoriesCore(true); - } - - /*public void UpdateLootedCategory(IReadOnlyList lootedItemInfos) - { - if (!Services.ClientState.IsLoggedIn) return; - _recentlyLootedCategoryNode?.CategorizedInventory.Items.AddRange( - lootedItemInfos.Select(x => new ItemInfo - { - ItemCount = x.Quantity, - Key = uint.MaxValue - 1, - Item = x.Item, - }) - .ToList()); - RefreshCategoriesCore(true); - }*/ - public void ManualCurrencyRefresh() { if (!Services.ClientState.IsLoggedIn) return; - _footerNode.RefreshCurrencies(); + FooterNode.RefreshCurrencies(); } private void OnInventoryUpdate(AddonEvent type, AddonArgs args) { - InventoryState.RefreshFromGame(); - + _inventoryState.RefreshFromGame(); RefreshCategoriesCore(autosize: true); } - protected override unsafe void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) + protected override void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { base.OnRequestedUpdate(addon, numberArrayData, stringArrayData); - InventoryState.RefreshFromGame(); - + _inventoryState.RefreshFromGame(); RefreshCategoriesCore(autosize: true); } - private void RefreshCategoriesCore(bool autosize) - { - _footerNode.SlotAmountText = InventoryState.GetEmptyItemSlotsString(); - _footerNode.RefreshCurrencies(); - - string filter = _searchInputNode.SearchString.ExtractText(); - IReadOnlyList categories = InventoryState.GetInventoryItemCategories(filter); - - float maxContentWidth = MaxWindowWidth - (ContentStartPosition.X * 2); - int maxItemsPerLine = CalculateOptimalItemsPerLine(maxContentWidth); - - _categoriesNode.SyncWithListDataByKey( - dataList: categories, - getKeyFromData: c => c.Key, - getKeyFromNode: n => n.CategorizedInventory.Key, - updateNode: (node, data) => - { - node.CategorizedInventory = data; - node.ItemsPerLine = Math.Min(data.Items.Count, maxItemsPerLine); - }, - createNodeMethod: _ => new InventoryCategoryNode - { - Size = ContentSize with { Y = 120 }, - }); - - bool pinsChanged = _pinCoordinator.ApplyPinnedStates(_categoriesNode); - if (pinsChanged) - _hoverCoordinator.ResetAll(_categoriesNode); - - WireHoverHandlers(); - - if (autosize) AutoSizeWindow(); - else - { - LayoutContent(); - _categoriesNode.RecalculateLayout(); - } - } - - - private void WireHoverHandlers() - { - var nodes = _categoriesNode.Nodes; - - for (int i = 0; i < nodes.Count; i++) - { - if (nodes[i] is not InventoryCategoryNode node) - continue; - - if (!_hoverSubscribed.Add(node)) - continue; - - node.HeaderHoverChanged += (src, hovering) => - { - _hoverCoordinator.OnCategoryHoverChanged(_categoriesNode, src, hovering); - }; - } - } - - private int CalculateOptimalItemsPerLine(float availableWidth) - { - return Math.Clamp((int)MathF.Floor((availableWidth + ItemPadding) / (ItemSize + ItemPadding)), 1, 15); - } - - private void LayoutContent() - { - Vector2 contentPos = ContentStartPosition; - Vector2 contentSize = ContentSize; - - float footerH = FooterHeight; - - _footerNode.Position = new Vector2(contentPos.X, contentPos.Y + contentSize.Y - footerH); - _footerNode.Size = new Vector2(contentSize.X, footerH); - - float gridH = contentSize.Y - footerH - FooterTopSpacing; - if (gridH < 0) gridH = 0; - - _categoriesNode.Position = contentPos; - _categoriesNode.Size = new Vector2(contentSize.X, gridH); - } - - private void AutoSizeWindow() - { - var nodes = _categoriesNode.Nodes; - - float maxChildWidth = 0f; - int childCount = 0; - - for (int i = 0; i < nodes.Count; i++) - { - if (nodes[i] is not InventoryCategoryNode cat) - continue; - - childCount++; - float w = cat.Width; - if (w > maxChildWidth) maxChildWidth = w; - } - - if (childCount == 0) - { - ResizeWindow(MinWindowWidth, MinWindowHeight, recalcLayout: true); - return; - } - - float requiredWidth = maxChildWidth + (ContentStartPosition.X * 2); - float finalWidth = Math.Clamp(requiredWidth, MinWindowWidth, MaxWindowWidth); - - float contentWidth = finalWidth - (ContentStartPosition.X * 2); - - float gridBudget = Math.Max(0f, MaxWindowHeight - FooterHeight - FooterTopSpacing); - - _categoriesNode.Position = ContentStartPosition; - _categoriesNode.Size = new Vector2(contentWidth, gridBudget); - - _categoriesNode.RecalculateLayout(); - - float requiredGridHeight = _categoriesNode.GetRequiredHeight(); - float requiredContentHeight = requiredGridHeight + FooterTopSpacing + FooterHeight; - - float requiredWindowHeight = requiredContentHeight + ContentStartPosition.Y + ContentStartPosition.X; - float finalHeight = Math.Clamp(requiredWindowHeight, MinWindowHeight, MaxWindowHeight); - - ResizeWindow(finalWidth, finalHeight, recalcLayout: false); - } - - private void ResizeWindow(float width, float height, bool recalcLayout) - { - SetWindowSize(width, height); - LayoutContent(); - - if (recalcLayout) - _categoriesNode.RecalculateLayout(); - } - - private void ResizeWindow(float width, float height) - => ResizeWindow(width, height, recalcLayout: true); - public void SetNotification(InventoryNotificationInfo info) { Services.Framework.RunOnTick(() => { - if(IsOpen) _notificationNode.NotificationInfo = info; + if (IsOpen) _notificationNode.NotificationInfo = info; }, delayTicks: 1); } @@ -319,13 +133,12 @@ public class AddonInventoryWindow : NativeAddon { Services.Framework.RunOnTick(() => { - if(IsOpen) _searchInputNode.SearchString = searchText; + if (IsOpen) SearchInputNode.SearchString = searchText; RefreshCategoriesCore(autosize: true); }, delayTicks: 1); } - - protected override unsafe void OnFinalize(AtkUnitBase* addon) + protected override void OnFinalize(AtkUnitBase* addon) { ref var blockingAddonId = ref AgentInventoryContext.Instance()->BlockingAddonId; if (blockingAddonId != 0) @@ -336,10 +149,6 @@ public class AddonInventoryWindow : NativeAddon Services.AddonLifecycle.UnregisterListener(OnInventoryUpdate); addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory); - _hoverSubscribed.Clear(); - _refreshQueued = false; - _refreshAutosizeQueued = false; - base.OnFinalize(addon); } -} +} \ No newline at end of file diff --git a/AetherBags/Addons/CategoryWrapper.cs b/AetherBags/Addons/CategoryWrapper.cs index 790571c..c0e488e 100644 --- a/AetherBags/Addons/CategoryWrapper.cs +++ b/AetherBags/Addons/CategoryWrapper.cs @@ -1,5 +1,6 @@ using AetherBags.Configuration; using AetherBags.Inventory; +using AetherBags.Inventory.Categories; using KamiToolKit.Premade; namespace AetherBags.Addons; diff --git a/AetherBags/Addons/InventoryAddonBase.cs b/AetherBags/Addons/InventoryAddonBase.cs new file mode 100644 index 0000000..0e83b9a --- /dev/null +++ b/AetherBags/Addons/InventoryAddonBase.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using AetherBags; +using AetherBags.Inventory; +using AetherBags.Inventory.Categories; +using AetherBags.Inventory.State; +using AetherBags.Nodes.Input; +using AetherBags.Nodes.Inventory; +using AetherBags.Nodes.Layout; +using FFXIVClientStructs.FFXIV.Component.GUI; +using KamiToolKit; +using KamiToolKit.Nodes; + +public abstract unsafe class InventoryAddonBase : NativeAddon +{ + protected readonly InventoryCategoryHoverCoordinator HoverCoordinator = new(); + protected readonly InventoryCategoryPinCoordinator PinCoordinator = new(); + protected readonly HashSet HoverSubscribed = new(); + + protected WrappingGridNode CategoriesNode = null!; + protected TextInputWithHintNode SearchInputNode = null!; + protected InventoryFooterNode FooterNode = null!; + protected CircleButtonNode SettingsButtonNode = null!; + + protected virtual float MinWindowWidth => 600; + protected virtual float MaxWindowWidth => 800; + protected virtual float MinWindowHeight => 200; + protected virtual float MaxWindowHeight => 1000; + + protected const float CategorySpacing = 12; + protected const float ItemSize = 40; + protected const float ItemPadding = 4; + protected const float FooterHeight = 28f; + protected const float FooterTopSpacing = 4f; + + protected bool RefreshQueued; + protected bool RefreshAutosizeQueued; + + protected abstract InventoryStateBase InventoryState { get; } + + protected virtual bool HasFooter => true; + protected virtual bool HasPinning => true; + + public void ManualInventoryRefresh() + { + if (!Services.ClientState.IsLoggedIn) return; + InventoryState.RefreshFromGame(); + RefreshCategoriesCore(autosize: true); + } + + protected virtual void RefreshCategoriesCore(bool autosize) + { + if (HasFooter) + { + FooterNode.SlotAmountText = InventoryState.GetEmptySlotsString(); + FooterNode.RefreshCurrencies(); + } + + string filter = SearchInputNode.SearchString.ExtractText(); + var categories = InventoryState.GetCategories(filter); + + float maxContentWidth = MaxWindowWidth - (ContentStartPosition.X * 2); + int maxItemsPerLine = CalculateOptimalItemsPerLine(maxContentWidth); + + CategoriesNode.SyncWithListDataByKey( + dataList: categories, + getKeyFromData: categorizedInventory => categorizedInventory.Key, + getKeyFromNode: node => node.CategorizedInventory.Key, + updateNode: (node, data) => + { + node.CategorizedInventory = data; + node.ItemsPerLine = Math.Min(data.Items.Count, maxItemsPerLine); + }, + createNodeMethod: _ => CreateCategoryNode()); + + if (HasPinning) + { + bool pinsChanged = PinCoordinator.ApplyPinnedStates(CategoriesNode); + if (pinsChanged) + HoverCoordinator.ResetAll(CategoriesNode); + } + + WireHoverHandlers(); + + if (autosize) + AutoSizeWindow(); + else + { + LayoutContent(); + CategoriesNode.RecalculateLayout(); + } + } + + protected virtual InventoryCategoryNode CreateCategoryNode() + { + return new InventoryCategoryNode + { + Size = ContentSize with { Y = 120 }, + }; + } + + protected void WireHoverHandlers() + { + var nodes = CategoriesNode.Nodes; + + for (int i = 0; i < nodes.Count; i++) + { + if (nodes[i] is not InventoryCategoryNode node) + continue; + + if (!HoverSubscribed.Add(node)) + continue; + + node.HeaderHoverChanged += (src, hovering) => + { + HoverCoordinator.OnCategoryHoverChanged(CategoriesNode, src, hovering); + }; + } + } + + protected int CalculateOptimalItemsPerLine(float availableWidth) + => Math.Clamp((int)MathF.Floor((availableWidth + ItemPadding) / (ItemSize + ItemPadding)), 1, 15); + + protected virtual void LayoutContent() + { + Vector2 contentPos = ContentStartPosition; + Vector2 contentSize = ContentSize; + + if (HasFooter) + { + FooterNode.Position = new Vector2(contentPos.X, contentPos.Y + contentSize.Y - FooterHeight); + FooterNode.Size = new Vector2(contentSize.X, FooterHeight); + } + + float gridH = contentSize.Y - (HasFooter ? FooterHeight + FooterTopSpacing : 0); + if (gridH < 0) gridH = 0; + + CategoriesNode.Position = contentPos; + CategoriesNode.Size = new Vector2(contentSize.X, gridH); + } + + protected virtual void AutoSizeWindow() + { + var nodes = CategoriesNode.Nodes; + + float maxChildWidth = 0f; + int childCount = 0; + + for (int i = 0; i < nodes.Count; i++) + { + if (nodes[i] is not InventoryCategoryNode cat) + continue; + + childCount++; + float w = cat.Width; + if (w > maxChildWidth) maxChildWidth = w; + } + + if (childCount == 0) + { + ResizeWindow(MinWindowWidth, MinWindowHeight, recalcLayout: true); + return; + } + + float requiredWidth = maxChildWidth + (ContentStartPosition.X * 2); + float finalWidth = Math.Clamp(requiredWidth, MinWindowWidth, MaxWindowWidth); + + float contentWidth = finalWidth - (ContentStartPosition.X * 2); + + float footerSpace = HasFooter ? FooterHeight + FooterTopSpacing : 0; + float gridBudget = Math.Max(0f, MaxWindowHeight - footerSpace); + + CategoriesNode.Position = ContentStartPosition; + CategoriesNode.Size = new Vector2(contentWidth, gridBudget); + + CategoriesNode.RecalculateLayout(); + + float requiredGridHeight = CategoriesNode.GetRequiredHeight(); + float requiredContentHeight = requiredGridHeight + footerSpace; + + float requiredWindowHeight = requiredContentHeight + ContentStartPosition.Y + ContentStartPosition.X; + float finalHeight = Math.Clamp(requiredWindowHeight, MinWindowHeight, MaxWindowHeight); + + ResizeWindow(finalWidth, finalHeight, recalcLayout: false); + } + + protected void ResizeWindow(float width, float height, bool recalcLayout) + { + SetWindowSize(width, height); + LayoutContent(); + + if (recalcLayout) + CategoriesNode.RecalculateLayout(); + } + + protected void ResizeWindow(float width, float height) + => ResizeWindow(width, height, recalcLayout: true); + + protected override void OnFinalize(AtkUnitBase* addon) + { + HoverSubscribed.Clear(); + RefreshQueued = false; + RefreshAutosizeQueued = false; + + base.OnFinalize(addon); + } +} \ No newline at end of file diff --git a/AetherBags/Commands/CommandHandler.cs b/AetherBags/Commands/CommandHandler.cs index 3d36d4e..a5fab08 100644 --- a/AetherBags/Commands/CommandHandler.cs +++ b/AetherBags/Commands/CommandHandler.cs @@ -1,6 +1,7 @@ using System; using AetherBags.Helpers; using AetherBags.Inventory; +using AetherBags.Inventory.State; using Dalamud.Game.Command; namespace AetherBags.Commands; diff --git a/AetherBags/Inventory/CategorizedInventory.cs b/AetherBags/Inventory/Categories/CategorizedInventory.cs similarity index 64% rename from AetherBags/Inventory/CategorizedInventory.cs rename to AetherBags/Inventory/Categories/CategorizedInventory.cs index 6f5a511..8f42bff 100644 --- a/AetherBags/Inventory/CategorizedInventory.cs +++ b/AetherBags/Inventory/Categories/CategorizedInventory.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; +using AetherBags.Inventory.Items; -namespace AetherBags.Inventory; +namespace AetherBags.Inventory.Categories; public readonly record struct CategorizedInventory(uint Key, CategoryInfo Category, List Items); \ No newline at end of file diff --git a/AetherBags/Inventory/CategoryBucketManager.cs b/AetherBags/Inventory/Categories/CategoryBucketManager.cs similarity index 99% rename from AetherBags/Inventory/CategoryBucketManager.cs rename to AetherBags/Inventory/Categories/CategoryBucketManager.cs index a565ee3..886c1e4 100644 --- a/AetherBags/Inventory/CategoryBucketManager.cs +++ b/AetherBags/Inventory/Categories/CategoryBucketManager.cs @@ -1,9 +1,10 @@ -using AetherBags.Configuration; using System; using System.Collections.Generic; using System.Linq; +using AetherBags.Configuration; +using AetherBags.Inventory.Items; -namespace AetherBags.Inventory; +namespace AetherBags.Inventory.Categories; public static class CategoryBucketManager { diff --git a/AetherBags/Inventory/CategoryInfo.cs b/AetherBags/Inventory/Categories/CategoryInfo.cs similarity index 87% rename from AetherBags/Inventory/CategoryInfo.cs rename to AetherBags/Inventory/Categories/CategoryInfo.cs index 819d75e..5ac5588 100644 --- a/AetherBags/Inventory/CategoryInfo.cs +++ b/AetherBags/Inventory/Categories/CategoryInfo.cs @@ -1,7 +1,7 @@ using System.Numerics; using KamiToolKit.Classes; -namespace AetherBags.Inventory; +namespace AetherBags.Inventory.Categories; public class CategoryInfo { diff --git a/AetherBags/Inventory/InventoryFilter.cs b/AetherBags/Inventory/Categories/InventoryFilter.cs similarity index 96% rename from AetherBags/Inventory/InventoryFilter.cs rename to AetherBags/Inventory/Categories/InventoryFilter.cs index 96b2545..42d91ca 100644 --- a/AetherBags/Inventory/InventoryFilter.cs +++ b/AetherBags/Inventory/Categories/InventoryFilter.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; using System.Text.RegularExpressions; +using AetherBags.Inventory.Items; -namespace AetherBags.Inventory; +namespace AetherBags.Inventory.Categories; public static class InventoryFilter { diff --git a/AetherBags/Inventory/UserCategoryMatcher.cs b/AetherBags/Inventory/Categories/UserCategoryMatcher.cs similarity index 98% rename from AetherBags/Inventory/UserCategoryMatcher.cs rename to AetherBags/Inventory/Categories/UserCategoryMatcher.cs index 945b291..32edcdb 100644 --- a/AetherBags/Inventory/UserCategoryMatcher.cs +++ b/AetherBags/Inventory/Categories/UserCategoryMatcher.cs @@ -1,8 +1,9 @@ -using AetherBags.Configuration; using System; using System.Text.RegularExpressions; +using AetherBags.Configuration; +using AetherBags.Inventory.Items; -namespace AetherBags.Inventory; +namespace AetherBags.Inventory.Categories; internal static class UserCategoryMatcher { diff --git a/AetherBags/Inventory/InventoryContextState.cs b/AetherBags/Inventory/Context/InventoryContextState.cs similarity index 98% rename from AetherBags/Inventory/InventoryContextState.cs rename to AetherBags/Inventory/Context/InventoryContextState.cs index 88c8a6b..77337da 100644 --- a/AetherBags/Inventory/InventoryContextState.cs +++ b/AetherBags/Inventory/Context/InventoryContextState.cs @@ -4,7 +4,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Arrays; using FFXIVClientStructs.FFXIV.Client.UI.Misc; -namespace AetherBags.Inventory; +namespace AetherBags.Inventory.Context; public static unsafe class InventoryContextState { diff --git a/AetherBags/Inventory/InventoryNotificationState.cs b/AetherBags/Inventory/Context/InventoryNotificationState.cs similarity index 99% rename from AetherBags/Inventory/InventoryNotificationState.cs rename to AetherBags/Inventory/Context/InventoryNotificationState.cs index 6ce636b..02358cb 100644 --- a/AetherBags/Inventory/InventoryNotificationState.cs +++ b/AetherBags/Inventory/Context/InventoryNotificationState.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using Lumina.Excel.Sheets; using Lumina.Text.ReadOnly; -namespace AetherBags.Inventory; +namespace AetherBags.Inventory.Context; public class InventoryNotificationState { diff --git a/AetherBags/Inventory/InventoryStats.cs b/AetherBags/Inventory/Items/InventoryStats.cs similarity index 91% rename from AetherBags/Inventory/InventoryStats.cs rename to AetherBags/Inventory/Items/InventoryStats.cs index 4f9a997..c9d15b5 100644 --- a/AetherBags/Inventory/InventoryStats.cs +++ b/AetherBags/Inventory/Items/InventoryStats.cs @@ -1,4 +1,4 @@ -namespace AetherBags.Inventory; +namespace AetherBags.Inventory.Items; public readonly struct InventoryStats { diff --git a/AetherBags/Inventory/ItemInfo.cs b/AetherBags/Inventory/Items/ItemInfo.cs similarity index 98% rename from AetherBags/Inventory/ItemInfo.cs rename to AetherBags/Inventory/Items/ItemInfo.cs index 15e3a7d..d3c5f7d 100644 --- a/AetherBags/Inventory/ItemInfo.cs +++ b/AetherBags/Inventory/Items/ItemInfo.cs @@ -1,11 +1,12 @@ -using FFXIVClientStructs.FFXIV.Client.Game; -using Lumina.Excel; -using Lumina.Excel.Sheets; using System; using System.Numerics; using System.Text.RegularExpressions; +using AetherBags.Inventory.Context; +using FFXIVClientStructs.FFXIV.Client.Game; +using Lumina.Excel; +using Lumina.Excel.Sheets; -namespace AetherBags.Inventory; +namespace AetherBags.Inventory.Items; public sealed class ItemInfo : IEquatable { diff --git a/AetherBags/Inventory/LootedItemInfo.cs b/AetherBags/Inventory/Items/LootedItemInfo.cs similarity index 76% rename from AetherBags/Inventory/LootedItemInfo.cs rename to AetherBags/Inventory/Items/LootedItemInfo.cs index 3d4edd9..4e3b1d8 100644 --- a/AetherBags/Inventory/LootedItemInfo.cs +++ b/AetherBags/Inventory/Items/LootedItemInfo.cs @@ -1,5 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.Game; -namespace AetherBags.Inventory; +namespace AetherBags.Inventory.Items; public record LootedItemInfo(int Index, InventoryItem Item, int Quantity); \ No newline at end of file diff --git a/AetherBags/Inventory/Scanning/AggregatedItem.cs b/AetherBags/Inventory/Scanning/AggregatedItem.cs new file mode 100644 index 0000000..2f196b2 --- /dev/null +++ b/AetherBags/Inventory/Scanning/AggregatedItem.cs @@ -0,0 +1,9 @@ +using FFXIVClientStructs.FFXIV.Client.Game; + +namespace AetherBags.Inventory.Scanning; + +public struct AggregatedItem +{ + public InventoryItem First; + public int Total; +} \ No newline at end of file diff --git a/AetherBags/Inventory/InventoryScanner.cs b/AetherBags/Inventory/Scanning/InventoryScanner.cs similarity index 74% rename from AetherBags/Inventory/InventoryScanner.cs rename to AetherBags/Inventory/Scanning/InventoryScanner.cs index 00e9d2b..28c64cc 100644 --- a/AetherBags/Inventory/InventoryScanner.cs +++ b/AetherBags/Inventory/Scanning/InventoryScanner.cs @@ -1,8 +1,9 @@ -using AetherBags.Configuration; -using FFXIVClientStructs.FFXIV.Client.Game; using System.Collections.Generic; +using AetherBags.Configuration; +using AetherBags.Inventory.Items; +using FFXIVClientStructs.FFXIV.Client.Game; -namespace AetherBags.Inventory; +namespace AetherBags.Inventory.Scanning; public static unsafe class InventoryScanner { @@ -46,20 +47,30 @@ public static unsafe class InventoryScanner public static ulong MakeNaturalSlotKey(InventoryType container, int slot) => ((ulong)(uint)container << 32) | (uint)slot; + // Backwards compatible public static void ScanBags( InventoryManager* inventoryManager, InventoryStackMode stackMode, Dictionary aggByKey) + => ScanInventories(inventoryManager, stackMode, aggByKey, InventorySourceType.MainBags); + + public static void ScanInventories( + InventoryManager* inventoryManager, + InventoryStackMode stackMode, + Dictionary aggByKey, + InventorySourceType source) { aggByKey.Clear(); + var inventories = InventorySourceDefinitions.GetInventories(source); + int scannedSlots = 0; int nonEmptySlots = 0; int collisions = 0; - for (int inventoryIndex = 0; inventoryIndex < BagInventories.Length; inventoryIndex++) + for (int inventoryIndex = 0; inventoryIndex < inventories.Length; inventoryIndex++) { - var inventoryType = BagInventories[inventoryIndex]; + var inventoryType = inventories[inventoryIndex]; var container = inventoryManager->GetInventoryContainer(inventoryType); if (container == null) { @@ -164,16 +175,38 @@ public static unsafe class InventoryScanner public static InventoryContainer* GetInventoryContainer(InventoryType inventoryType) => InventoryManager.Instance()->GetInventoryContainer(inventoryType); + // Backwards compability TODO: Remove public static string GetEmptyItemSlotsString() - { - uint empty = InventoryManager.Instance()->GetEmptySlotsInBag(); - uint used = 140 - empty; - return $"{used}/140"; - } -} + => GetEmptySlotsString(InventorySourceType. MainBags); -public struct AggregatedItem -{ - public InventoryItem First; - public int Total; + public static string GetEmptySlotsString(InventorySourceType source) + { + int total = InventorySourceDefinitions.GetTotalSlots(source); + uint empty = source switch + { + InventorySourceType.MainBags => InventoryManager.Instance()->GetEmptySlotsInBag(), + InventorySourceType.SaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.SaddleBag), + InventorySourceType.Retainer => GetEmptySlotsInContainer(InventorySourceDefinitions.Retainer), + _ => 0, + }; + uint used = (uint)total - empty; + return $"{used}/{total}"; + } + + private static uint GetEmptySlotsInContainer(InventoryType[] inventories) + { + uint empty = 0; + var inventoryManager = InventoryManager.Instance(); + foreach (var inv in inventories) + { + var container = inventoryManager->GetInventoryContainer(inv); + if (container == null) continue; + for (int i = 0; i < container->Size; i++) + { + if (container->Items[i]. ItemId == 0) + empty++; + } + } + return empty; + } } \ No newline at end of file diff --git a/AetherBags/Inventory/Scanning/InventorySource.cs b/AetherBags/Inventory/Scanning/InventorySource.cs new file mode 100644 index 0000000..47ff7ba --- /dev/null +++ b/AetherBags/Inventory/Scanning/InventorySource.cs @@ -0,0 +1,56 @@ +using FFXIVClientStructs.FFXIV.Client.Game; + +namespace AetherBags.Inventory.Scanning; + +public enum InventorySourceType +{ + MainBags, + SaddleBag, + Retainer, +} + +public static class InventorySourceDefinitions +{ + public static readonly InventoryType[] MainBags = + [ + InventoryType.Inventory1, + InventoryType.Inventory2, + InventoryType.Inventory3, + InventoryType.Inventory4, + ]; + + public static readonly InventoryType[] SaddleBag = + [ + InventoryType.SaddleBag1, + InventoryType.SaddleBag2, + InventoryType.PremiumSaddleBag1, + InventoryType.PremiumSaddleBag2, + ]; + + public static readonly InventoryType[] Retainer = + [ + InventoryType.RetainerPage1, + InventoryType.RetainerPage2, + InventoryType.RetainerPage3, + InventoryType.RetainerPage4, + InventoryType.RetainerPage5, + InventoryType.RetainerPage6, + InventoryType.RetainerPage7, + ]; + + public static InventoryType[] GetInventories(InventorySourceType source) => source switch + { + InventorySourceType.MainBags => MainBags, + InventorySourceType.SaddleBag => SaddleBag, + InventorySourceType.Retainer => Retainer, + _ => MainBags, + }; + + public static int GetTotalSlots(InventorySourceType source) => source switch + { + InventorySourceType.MainBags => 140, // 4 * 35 + InventorySourceType.SaddleBag => 70, // 2 * 35 TODO: Premium adds another 70 + InventorySourceType.Retainer => 175, // 7 * 25 + _ => 140, + }; +} \ No newline at end of file diff --git a/AetherBags/Inventory/InventoryState.cs b/AetherBags/Inventory/State/InventoryState.cs similarity index 97% rename from AetherBags/Inventory/InventoryState.cs rename to AetherBags/Inventory/State/InventoryState.cs index 1d29106..9886511 100644 --- a/AetherBags/Inventory/InventoryState.cs +++ b/AetherBags/Inventory/State/InventoryState.cs @@ -1,12 +1,16 @@ +using System.Collections.Generic; +using System.Linq; using AetherBags.Configuration; using AetherBags.Currency; +using AetherBags.Inventory.Categories; +using AetherBags.Inventory.Context; +using AetherBags.Inventory.Items; +using AetherBags.Inventory.Scanning; using Dalamud.Game.Inventory; using Dalamud.Game.Inventory.InventoryEventArgTypes; using FFXIVClientStructs.FFXIV.Client.Game; -using System.Collections.Generic; -using System.Linq; -namespace AetherBags.Inventory; +namespace AetherBags.Inventory.State; public static unsafe class InventoryState { diff --git a/AetherBags/Inventory/State/InventoryStateBase.cs b/AetherBags/Inventory/State/InventoryStateBase.cs new file mode 100644 index 0000000..3ecf792 --- /dev/null +++ b/AetherBags/Inventory/State/InventoryStateBase.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using System.Linq; +using AetherBags.Configuration; +using AetherBags.Inventory.Categories; +using AetherBags.Inventory.Items; +using AetherBags.Inventory.Scanning; +using FFXIVClientStructs.FFXIV.Client.Game; + +namespace AetherBags.Inventory.State; + +public abstract class InventoryStateBase +{ + protected readonly Dictionary AggByKey = new(capacity: 512); + protected readonly Dictionary ItemInfoByKey = new(capacity: 512); + protected readonly Dictionary BucketsByKey = new(capacity: 256); + protected readonly List SortedCategoryKeys = new(capacity: 256); + protected readonly List AllCategories = new(capacity: 256); + protected readonly List FilteredCategories = new(capacity: 256); + protected readonly List UserCategoriesSortedScratch = new(capacity: 64); + protected readonly List RemoveKeysScratch = new(capacity: 256); + protected readonly HashSet ClaimedKeys = new(capacity: 512); + + public abstract InventorySourceType SourceType { get; } + public abstract InventoryType[] Inventories { get; } + + public virtual unsafe void RefreshFromGame() + { + InventoryManager* inventoryManager = InventoryManager.Instance(); + if (inventoryManager == null) + { + ClearAll(); + return; + } + + var config = AetherBags.System.Config; + InventoryStackMode stackMode = config.General.StackMode; + + AggByKey.Clear(); + ItemInfoByKey.Clear(); + SortedCategoryKeys.Clear(); + AllCategories.Clear(); + FilteredCategories.Clear(); + ClaimedKeys.Clear(); + + InventoryScanner.ScanInventories(inventoryManager, stackMode, AggByKey, SourceType); + CategoryBucketManager.ResetBuckets(BucketsByKey); + InventoryScanner.BuildItemInfos(AggByKey, ItemInfoByKey); + + OnPostScan(); + + ApplyCategories(config); + + InventoryScanner.PruneStaleItemInfos(AggByKey, ItemInfoByKey, RemoveKeysScratch); + CategoryBucketManager.SortBucketsAndBuildKeyList(BucketsByKey, SortedCategoryKeys); + CategoryBucketManager.BuildCategorizedList(BucketsByKey, SortedCategoryKeys, AllCategories); + } + + protected virtual void OnPostScan() + { + } + + protected virtual void ApplyCategories(SystemConfiguration config) + { + bool userCategoriesEnabled = config.Categories.UserCategoriesEnabled; + bool gameCategoriesEnabled = config.Categories.GameCategoriesEnabled; + var userCategories = config.Categories.UserCategories.Where(c => c.Enabled).ToList(); + + if (userCategoriesEnabled && userCategories.Count > 0) + { + CategoryBucketManager.BucketByUserCategories( + ItemInfoByKey, userCategories, BucketsByKey, ClaimedKeys, UserCategoriesSortedScratch); + } + + if (gameCategoriesEnabled) + { + CategoryBucketManager.BucketByGameCategories( + ItemInfoByKey, BucketsByKey, ClaimedKeys, userCategoriesEnabled); + } + else + { + CategoryBucketManager.BucketUnclaimedToMisc( + ItemInfoByKey, BucketsByKey, ClaimedKeys, userCategoriesEnabled); + } + } + + public IReadOnlyList GetCategories(string filter = "", bool invert = false) + => InventoryFilter.FilterCategories(AllCategories, BucketsByKey, FilteredCategories, filter, invert); + + public string GetEmptySlotsString() => InventoryScanner.GetEmptySlotsString(SourceType); + + protected virtual void ClearAll() + { + AggByKey.Clear(); + ItemInfoByKey.Clear(); + + foreach (var kvp in BucketsByKey) + { + kvp.Value.Items.Clear(); + kvp.Value.FilteredItems.Clear(); + kvp.Value.Used = false; + } + + SortedCategoryKeys.Clear(); + AllCategories.Clear(); + FilteredCategories.Clear(); + RemoveKeysScratch.Clear(); + ClaimedKeys.Clear(); + } +} \ No newline at end of file diff --git a/AetherBags/Inventory/State/MainBagState.cs b/AetherBags/Inventory/State/MainBagState.cs new file mode 100644 index 0000000..945d36a --- /dev/null +++ b/AetherBags/Inventory/State/MainBagState.cs @@ -0,0 +1,17 @@ +using AetherBags.Inventory.Context; +using AetherBags.Inventory.Scanning; +using FFXIVClientStructs.FFXIV.Client.Game; + +namespace AetherBags.Inventory.State; + +public class MainBagState : InventoryStateBase +{ + public override InventorySourceType SourceType => InventorySourceType.MainBags; + public override InventoryType[] Inventories => InventorySourceDefinitions.MainBags; + + protected override void OnPostScan() + { + InventoryContextState.RefreshMaps(); + InventoryContextState.RefreshBlockedSlots(); + } +} \ No newline at end of file diff --git a/AetherBags/Inventory/State/SaddleBagState.cs b/AetherBags/Inventory/State/SaddleBagState.cs new file mode 100644 index 0000000..d0fe562 --- /dev/null +++ b/AetherBags/Inventory/State/SaddleBagState.cs @@ -0,0 +1,10 @@ +using AetherBags.Inventory.Scanning; +using FFXIVClientStructs.FFXIV.Client.Game; + +namespace AetherBags.Inventory.State; + +public class SaddleBagState : InventoryStateBase +{ + public override InventorySourceType SourceType => InventorySourceType.SaddleBag; + public override InventoryType[] Inventories => InventorySourceDefinitions.SaddleBag; +} \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs b/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs index 3b76482..4e54bc8 100644 --- a/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs @@ -2,6 +2,7 @@ using System; using System.Numerics; using AetherBags.Configuration; using AetherBags.Inventory; +using AetherBags.Inventory.Categories; using AetherBags.Nodes.Color; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; diff --git a/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs index 0d4c8d7..8d66311 100644 --- a/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs +++ b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs @@ -2,6 +2,8 @@ using System; using System.Numerics; using AetherBags.Helpers; using AetherBags.Inventory; +using AetherBags.Inventory.Categories; +using AetherBags.Inventory.Items; using AetherBags.Nodes.Layout; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.UI; @@ -33,6 +35,7 @@ public class InventoryCategoryNode : SimpleComponentNode private string _fullHeaderText = string.Empty; public event Action? HeaderHoverChanged; + public Action? OnRefreshRequested { get; set; } public InventoryCategoryNode() { @@ -287,7 +290,7 @@ public class InventoryCategoryNode : SimpleComponentNode Services.Logger.Debug($"[OnPayload] Source and target are in the same container group; no move performed"); node.Payload = payload; node.IconId = item.IconId; - System.AddonInventoryWindow.ManualInventoryRefresh(); + OnRefreshRequested?.Invoke(); return; }; @@ -297,6 +300,6 @@ public class InventoryCategoryNode : SimpleComponentNode sourceLocation.Container, sourceLocation.Slot, targetLocation.Container, targetLocation.Slot ); - System.AddonInventoryWindow.ManualInventoryRefresh(); + OnRefreshRequested?.Invoke(); } } \ No newline at end of file diff --git a/AetherBags/Nodes/Inventory/InventoryDragDropNode.cs b/AetherBags/Nodes/Inventory/InventoryDragDropNode.cs index a2435fb..4e5dfc3 100644 --- a/AetherBags/Nodes/Inventory/InventoryDragDropNode.cs +++ b/AetherBags/Nodes/Inventory/InventoryDragDropNode.cs @@ -1,5 +1,6 @@ using System.Numerics; using AetherBags.Inventory; +using AetherBags.Inventory.Items; using Dalamud.Game.ClientState.Keys; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.UI.Agent; diff --git a/AetherBags/Nodes/Inventory/InventoryFooterNode.cs b/AetherBags/Nodes/Inventory/InventoryFooterNode.cs index 27a9105..5c9747e 100644 --- a/AetherBags/Nodes/Inventory/InventoryFooterNode.cs +++ b/AetherBags/Nodes/Inventory/InventoryFooterNode.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Numerics; using AetherBags.Currency; using AetherBags.Inventory; +using AetherBags.Inventory.State; using AetherBags.Nodes.Currency; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Classes; diff --git a/AetherBags/Nodes/Inventory/InventoryNotificationNode.cs b/AetherBags/Nodes/Inventory/InventoryNotificationNode.cs index 02e578c..1645ed7 100644 --- a/AetherBags/Nodes/Inventory/InventoryNotificationNode.cs +++ b/AetherBags/Nodes/Inventory/InventoryNotificationNode.cs @@ -1,5 +1,6 @@ using System.Numerics; using AetherBags.Inventory; +using AetherBags.Inventory.Context; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Classes; using KamiToolKit.Classes.Timelines; diff --git a/AetherBags/Plugin.cs b/AetherBags/Plugin.cs index 2be11b3..0528874 100644 --- a/AetherBags/Plugin.cs +++ b/AetherBags/Plugin.cs @@ -5,6 +5,7 @@ using AetherBags.Commands; using AetherBags.Helpers; using AetherBags.Hooks; using AetherBags.Inventory; +using AetherBags.Inventory.State; using Dalamud.Plugin; using KamiToolKit; diff --git a/AetherBags/System.cs b/AetherBags/System.cs index 05dee40..3e35168 100644 --- a/AetherBags/System.cs +++ b/AetherBags/System.cs @@ -6,6 +6,8 @@ namespace AetherBags; public static class System { public static AddonInventoryWindow AddonInventoryWindow { get; set; } = null!; + public static AddonInventoryWindow AddonSaddleBagWindow { get; set; } = null!; + public static AddonInventoryWindow AddonRetainerWindow { get; set; } = null!; public static AddonConfigurationWindow AddonConfigurationWindow { get; set; } = null!; public static SystemConfiguration Config { get; set; } = null!; } \ No newline at end of file