diff --git a/AetherBags.sln.DotSettings.user b/AetherBags.sln.DotSettings.user deleted file mode 100644 index 6ef2c30..0000000 --- a/AetherBags.sln.DotSettings.user +++ /dev/null @@ -1,10 +0,0 @@ - - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded - ForceIncluded \ No newline at end of file diff --git a/AetherBags/AddonLifecycles/InventoryLifecycles.cs b/AetherBags/AddonLifecycles/InventoryLifecycles.cs index 04f3352..d35639b 100644 --- a/AetherBags/AddonLifecycles/InventoryLifecycles.cs +++ b/AetherBags/AddonLifecycles/InventoryLifecycles.cs @@ -2,12 +2,16 @@ 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.ClientState.Conditions; using Dalamud.Game.NativeWrapper; using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Text.ReadOnly; +using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; namespace AetherBags.AddonLifecycles; @@ -16,10 +20,90 @@ public class InventoryLifecycles : IDisposable public InventoryLifecycles() { - Services.AddonLifecycle.RegisterListener(AddonEvent.PreRefresh, ["Inventory", "InventoryLarge", "InventoryExpansion"], PreRefreshHandler); + var bags = new[] { "Inventory", "InventoryLarge", "InventoryExpansion" }; + var saddle = new[] { "InventoryBuddy" }; + var retainer = new[] { "InventoryRetainer", "InventoryRetainerLarge" }; + + Services.AddonLifecycle.RegisterListener(AddonEvent.PostSetup, saddle, OnPostSetup); + Services.AddonLifecycle.RegisterListener(AddonEvent.PostSetup, retainer, OnPostSetup); + + Services.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, saddle, OnPreFinalize); + Services.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, retainer, OnPreFinalize); + + // PreRefresh Handlers + Services.AddonLifecycle.RegisterListener(AddonEvent.PreRefresh, ["Inventory", "InventoryLarge", "InventoryExpansion"], InventoryPreRefreshHandler); + + // PostRequestedUpdate + Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate); + Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "InventoryBuddy", OnSaddleBagUpdate); + Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, ["InventoryRetainer", "InventoryRetainerLarge"], OnRetainerInventoryUpdate); + + // PreShow + Services.AddonLifecycle.RegisterListener(AddonEvent.PreOpen, "InventoryBuddy", OnSaddleBagOpen); + Services.Logger.Verbose("InventoryLifecycles initialized"); } + private void OnPreFinalize(AddonEvent type, AddonArgs args) + { + CloseInventories(args.AddonName); + } + + private void OnPostSetup(AddonEvent type, AddonArgs args) + { + OpenInventories(args.AddonName); + } + + private unsafe void OpenInventories(string name) + { + GeneralSettings config = System.Config.General; + if (name.Contains("Retainer") && config.OpenRetainerWithGameInventory) + { + System.AddonRetainerWindow.Open(); + if (config.HideGameRetainer) + { + var addon = RaptureAtkUnitManager.Instance()->GetAddonByName("InventoryRetainer"); + if (addon != null) + { + addon->IsVisible = false; + } + + addon = RaptureAtkUnitManager.Instance()->GetAddonByName("InventoryRetainerLarge"); + if (addon != null) + { + addon->IsVisible = false; + } + } + } + + if (name.Contains("InventoryBuddy") && config.OpenSaddleBagsWithGameInventory) + { + System.AddonSaddleBagWindow.Open(); + if (config.HideGameSaddleBags) + { + var addon = RaptureAtkUnitManager.Instance()->GetAddonByName("InventoryBuddy"); + if (addon != null) + { + addon->IsVisible = false; + } + } + } + } + + private void CloseInventories(string name) + { + if (name.Contains("Retainer")) System.AddonRetainerWindow.Close(); + if (name.Contains("InventoryBuddy")) System.AddonSaddleBagWindow.Close(); + } + + private static bool IsInUnsafeState() + { + if (!Services.ClientState.IsLoggedIn) + return true; + + return Services.Condition.Any(ConditionFlag.BetweenAreas, ConditionFlag.BetweenAreas51); + } + /* values[0] = OpenType values[1] = OpenTitleId @@ -31,14 +115,17 @@ public class InventoryLifecycles : IDisposable values[7] = can use Saddlebags (Agent InventoryBuddy IsActivatable) */ - private unsafe void PreRefreshHandler(AddonEvent type, AddonArgs args) + private unsafe void InventoryPreRefreshHandler(AddonEvent type, AddonArgs args) { if (args is not AddonRefreshArgs refreshArgs) return; + if (IsInUnsafeState()) + return; + GeneralSettings config = System.Config.General; - Services.Logger.Debug("PreRefresh event for Inventory detected"); + Services.Logger.DebugOnly("PreRefresh event for Inventory detected"); AtkValuePtr[] atkValues = refreshArgs.AtkValueEnumerable.ToArray(); @@ -48,6 +135,9 @@ public class InventoryLifecycles : IDisposable AtkValue* value5 = (AtkValue*)atkValues[5].Address; AtkValue* value6 = (AtkValue*)atkValues[6].Address; + if (value5->Type != ValueType.ManagedString || value6->Type != ValueType.ManagedString) + return; + int openTitleId = value1->Int; ReadOnlySeString title = value5->String.AsReadOnlySeString(); ReadOnlySeString upperTitle = value6->String.AsReadOnlySeString(); @@ -68,8 +158,71 @@ public class InventoryLifecycles : IDisposable } } + // TODO: Inventory/Retainers are not perma open, need some way to close it too. + private void InventoryBuddyPreRefreshHandler(AddonEvent type, AddonArgs args) + { + if (args is not AddonRefreshArgs refreshArgs) + return; + + if (IsInUnsafeState()) + return; + + GeneralSettings config = System.Config.General; + + if (config.HideGameSaddleBags) refreshArgs.AtkValueCount = 0; + if (config.OpenSaddleBagsWithGameInventory) + { + System.AddonSaddleBagWindow.Toggle(); + } + } + + + private void OnInventoryUpdate(AddonEvent type, AddonArgs args) + { + if (IsInUnsafeState()) + return; + + System.AddonInventoryWindow?.RefreshFromLifecycle(); + } + + private void OnSaddleBagUpdate(AddonEvent type, AddonArgs args) + { + if (IsInUnsafeState()) + return; + + System.AddonSaddleBagWindow?.RefreshFromLifecycle(); + } + + private void OnRetainerInventoryUpdate(AddonEvent type, AddonArgs args) + { + if (IsInUnsafeState()) + return; + + System.AddonRetainerWindow?.RefreshFromLifecycle(); + } + + private void OnSaddleBagOpen(AddonEvent type, AddonArgs args) + { + if (args is not AddonShowArgs showArgs) + return; + } + public void Dispose() { + Services.AddonLifecycle.UnregisterListener(AddonEvent.PostSetup, "InventoryBuddy", OnPostSetup); + Services.AddonLifecycle.UnregisterListener(AddonEvent.PostSetup, "InventoryRetainer, InventoryRetainerLarge", OnPostSetup); + + Services.AddonLifecycle.UnregisterListener(AddonEvent.PreFinalize, "InventoryBuddy", OnPreFinalize); + Services.AddonLifecycle.UnregisterListener(AddonEvent.PreFinalize, "InventoryRetainer, InventoryRetainerLarge", OnPreFinalize); + Services.AddonLifecycle.UnregisterListener(AddonEvent.PreRefresh, ["Inventory", "InventoryLarge", "InventoryExpansion"]); + Services.AddonLifecycle.UnregisterListener(AddonEvent.PreRefresh, ["InventoryBuddy"]); + Services.AddonLifecycle.UnregisterListener(AddonEvent.PreRefresh, ["InventoryRetainer", "InventoryRetainerLarge"]); + + Services.AddonLifecycle.UnregisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate); + Services.AddonLifecycle.UnregisterListener(AddonEvent.PostRequestedUpdate, "InventoryBuddy", OnSaddleBagUpdate); + Services.AddonLifecycle.UnregisterListener(AddonEvent.PostRequestedUpdate, ["InventoryRetainer", "InventoryRetainerLarge"], OnRetainerInventoryUpdate); + + Services.AddonLifecycle.UnregisterListener(AddonEvent.PreShow, ["InventoryBuddy"], OnSaddleBagOpen); } } \ No newline at end of file diff --git a/AetherBags/Addons/AddonCategoryConfigurationWindow.cs b/AetherBags/Addons/AddonCategoryConfigurationWindow.cs index 4d46049..484e1e4 100644 --- a/AetherBags/Addons/AddonCategoryConfigurationWindow.cs +++ b/AetherBags/Addons/AddonCategoryConfigurationWindow.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; using AetherBags.Configuration; +using AetherBags.Inventory; using AetherBags.Nodes.Configuration.Category; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit; @@ -77,7 +78,7 @@ public class AddonCategoryConfigurationWindow : NativeAddon .ToList(); } - private void OnOptionChanged(CategoryWrapper? newOption) + private void OnOptionChanged(CategoryWrapper? newOption) { if (_configNode is null) return; @@ -118,7 +119,7 @@ public class AddonCategoryConfigurationWindow : NativeAddon listNode.AddOption(newWrapper); RefreshSelectionList(); - System.AddonInventoryWindow.ManualInventoryRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); } private void OnRemoveCategory(CategoryWrapper categoryWrapper) @@ -134,7 +135,7 @@ public class AddonCategoryConfigurationWindow : NativeAddon { OnOptionChanged(null); } - System.AddonInventoryWindow.ManualInventoryRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); } private void RefreshSelectionList() diff --git a/AetherBags/Addons/AddonConfigurationWindow.cs b/AetherBags/Addons/AddonConfigurationWindow.cs index 85e6692..bf95edd 100644 --- a/AetherBags/Addons/AddonConfigurationWindow.cs +++ b/AetherBags/Addons/AddonConfigurationWindow.cs @@ -37,8 +37,6 @@ public class AddonConfigurationWindow : NativeAddon { Position = ContentStartPosition with { Y = tabContentY }, Size = ContentSize with { Y = tabContentHeight }, - ContentHeight = 400, - ScrollSpeed = 25, IsVisible = true, }; _generalScrollingAreaNode.AttachNode(this); @@ -47,8 +45,6 @@ public class AddonConfigurationWindow : NativeAddon { Position = ContentStartPosition with { Y = tabContentY }, Size = ContentSize with { Y = tabContentHeight }, - ContentHeight = 400, - ScrollSpeed = 25, IsVisible = false, }; _categoryScrollingAreaNode.AttachNode(this); @@ -57,8 +53,6 @@ public class AddonConfigurationWindow : NativeAddon { Position = ContentStartPosition with { Y = tabContentY }, Size = ContentSize with { Y = tabContentHeight }, - ContentHeight = 400, - ScrollSpeed = 25, IsVisible = false, }; _currencyScrollingAreaNode.AttachNode(this); diff --git a/AetherBags/Addons/AddonInventoryWindow.cs b/AetherBags/Addons/AddonInventoryWindow.cs index faf0d95..8348b7b 100644 --- a/AetherBags/Addons/AddonInventoryWindow.cs +++ b/AetherBags/Addons/AddonInventoryWindow.cs @@ -1,53 +1,28 @@ -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; -using Dalamud.Game.Addon.Lifecycle; -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 + InitializeBackgroundDropTarget(); + + CategoriesNode = new WrappingGridNode { Position = ContentStartPosition, Size = ContentSize, @@ -56,276 +31,69 @@ 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); - - var header = addon->WindowHeaderCollisionNode; - - float headerX = header->X; - float headerY = header->Y; - float headerW = header->Width; - float headerH = header->Height; - - float x = headerX + (headerW - size.X) * 0.5f; - float y = headerY + (headerH - size.Y) * 0.5f; + var header = CalculateHeaderLayout(addon); _notificationNode = new InventoryNotificationNode { Position = new Vector2(WindowNode!.X - 4f, WindowNode!.Y - 32f), - Size = new Vector2(headerW, 28f), + Size = new Vector2(header.HeaderWidth, 28f), }; _notificationNode.AttachNode(this); - _searchInputNode = new TextInputWithHintNode + SearchInputNode = new TextInputWithButtonNode { - Position = new Vector2(x, y), - Size = size, - OnInputReceived = _ => RefreshCategoriesCore(autosize: false), + Position = header.SearchPosition, + Size = header.SearchSize, + OnInputReceived = _ => ItemRefresh(), + OnButtonClicked = () => InventoryAddonContextMenu.OpenMain(this) }; - _searchInputNode.AttachNode(this); + SearchInputNode.AttachNode(this); - _settingsButtonNode = new CircleButtonNode + SettingsButtonNode = new CircleButtonNode { - Position = new Vector2(headerW - 48f, y), + Position = new Vector2(header.HeaderWidth - SettingsButtonOffset, header.HeaderY), 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(); + _isSetupComplete = true; + _inventoryState.RefreshFromGame(); RefreshCategoriesCore(autosize: true); base.OnSetup(addon); } - protected override unsafe void OnUpdate(AtkUnitBase* addon) - { - if (_refreshQueued) - { - bool doAutosize = _refreshAutosizeQueued; - _refreshQueued = false; - _refreshAutosizeQueued = false; - - RefreshCategoriesCore(doAutosize); - } - - 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(); - - RefreshCategoriesCore(autosize: true); - } - - protected override unsafe void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) - { - base.OnRequestedUpdate(addon, numberArrayData, stringArrayData); - - 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; - }, delayTicks: 1); + if (IsOpen) _notificationNode.NotificationInfo = info; + }, delayTicks: 3); } - public void SetSearchText(string searchText) - { - Services.Framework.RunOnTick(() => - { - 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) @@ -333,13 +101,9 @@ public class AddonInventoryWindow : NativeAddon RaptureAtkModule.Instance()->CloseAddon(blockingAddonId); } - Services.AddonLifecycle.UnregisterListener(OnInventoryUpdate); addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory); - _hoverSubscribed.Clear(); - _refreshQueued = false; - _refreshAutosizeQueued = false; - + _isSetupComplete = false; base.OnFinalize(addon); } -} +} \ No newline at end of file diff --git a/AetherBags/Addons/AddonRetainerWindow.cs b/AetherBags/Addons/AddonRetainerWindow.cs new file mode 100644 index 0000000..f710013 --- /dev/null +++ b/AetherBags/Addons/AddonRetainerWindow.cs @@ -0,0 +1,188 @@ +using System.Linq; +using System.Numerics; +using AetherBags.Inventory; +using AetherBags.Inventory.State; +using AetherBags.Nodes.Input; +using AetherBags.Nodes.Inventory; +using AetherBags.Nodes.Layout; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Component.GUI; +using KamiToolKit.Classes; +using KamiToolKit.Nodes; + +namespace AetherBags.Addons; + +public unsafe class AddonRetainerWindow : InventoryAddonBase +{ + private readonly RetainerState _inventoryState = new(); + private TextNode _slotCounterNode = null!; + private TextNode _retainerNameNode = null!; + private TextButtonNode _entrustDuplicatesButton = null!; + + protected override InventoryStateBase InventoryState => _inventoryState; + + protected override bool HasFooter => false; + protected override bool HasSlotCounter => true; + + private readonly Vector3 _tintColor = new(8f / 255f, -8f / 255f, -4f / 255f); + + protected override float MinWindowWidth => 400; + protected override float MaxWindowWidth => 700; + + private readonly string[] _retainerAddonNames = { "InventoryRetainer", "InventoryRetainerLarge" }; + + protected override void OnSetup(AtkUnitBase* addon) + { + InitializeBackgroundDropTarget(); + + WindowNode?.AddColor = _tintColor; + + CategoriesNode = new WrappingGridNode + { + Position = ContentStartPosition, + Size = ContentSize, + HorizontalSpacing = CategorySpacing, + VerticalSpacing = CategorySpacing, + TopPadding = 4.0f, + BottomPadding = 4.0f, + }; + CategoriesNode.AttachNode(this); + + var header = CalculateHeaderLayout(addon); + + SearchInputNode = new TextInputWithButtonNode + { + Position = header.SearchPosition, + Size = header.SearchSize, + OnInputReceived = _ => ItemRefresh(), + OnButtonClicked = () => InventoryAddonContextMenu.OpenMain(this) + }; + SearchInputNode.AttachNode(this); + + SettingsButtonNode = new CircleButtonNode + { + Position = new Vector2(header.HeaderWidth - SettingsButtonOffset, header.HeaderY), + Size = new Vector2(28f), + Icon = ButtonIcon.GearCog, + OnClick = System.AddonConfigurationWindow.Toggle + }; + SettingsButtonNode.AttachNode(this); + + _retainerNameNode = new TextNode + { + Position = new Vector2(8f, 0), + Size = new Vector2(200, 20), + AlignmentType = AlignmentType.Left, + FontType = FontType.MiedingerMed, + TextFlags = TextFlags.Glare, + TextColor = ColorHelper.GetColor(50), + TextOutlineColor = ColorHelper.GetColor(32), + }; + _retainerNameNode.AttachNode(this); + + _entrustDuplicatesButton = new TextButtonNode + { + Size = new Vector2(120, 28), + AddColor = _tintColor, + String = "Entrust Duplicates", + OnClick = OnEntrustDuplicates, + }; + _entrustDuplicatesButton.AttachNode(this); + + // Slot counter + _slotCounterNode = new TextNode + { + Position = new Vector2(Size.X - 10, 0), + Size = new Vector2(82, 20), + AlignmentType = AlignmentType.Right, + FontType = FontType.MiedingerMed, + TextFlags = TextFlags.Glare, + TextColor = ColorHelper.GetColor(50), + TextOutlineColor = ColorHelper.GetColor(32), + }; + _slotCounterNode.AttachNode(this); + SlotCounterNode = _slotCounterNode; + + LayoutContent(); + + _inventoryState.RefreshFromGame(); + _isSetupComplete = true; + + RefreshCategoriesCore(autosize: true); + + base.OnSetup(addon); + } + + protected override void RefreshCategoriesCore(bool autosize) + { + if (!_isSetupComplete) + return; + + _slotCounterNode.String = _inventoryState.GetEmptySlotsString(); + _retainerNameNode.String = RetainerState.CurrentRetainerName; + + base.RefreshCategoriesCore(autosize); + } + + protected override void LayoutContent() + { + base.LayoutContent(); + + Vector2 contentPos = ContentStartPosition; + Vector2 contentSize = ContentSize; + + float footerY = contentPos.Y + contentSize.Y - FooterHeight + 4f; + + _retainerNameNode.Position = new Vector2(contentPos.X + 8f, footerY); + + float buttonWidth = _entrustDuplicatesButton.Width; + float buttonX = contentPos.X + (contentSize.X - buttonWidth) / 2f; + _entrustDuplicatesButton.Position = new Vector2(buttonX, footerY - 2f); + + if (SlotCounterNode != null) + SlotCounterNode.Position = new Vector2(contentSize.X - 80f, footerY); + } + + private void CloseRetainerWindows() + { + var manager = RaptureAtkUnitManager.Instance(); + foreach (var name in _retainerAddonNames) + { + var addon = manager->GetAddonByName(name); + if (addon != null) + { + addon->IsVisible = true; + addon->Close(true); + } + } + } + + private bool IsAnyRetainerWindowLoaded() + { + return _retainerAddonNames.Any(name => RaptureAtkUnitManager.Instance()->GetAddonByName(name) != null); + } + + protected override void OnShow(AtkUnitBase* addon) + { + base.OnShow(addon); + + InventoryOrchestrator.RefreshAll(updateMaps: true); + } + + private void OnEntrustDuplicates() + { + if (!IsAnyRetainerWindowLoaded()) return; + var agent = AgentModule.Instance()->GetAgentByInternalId(AgentId.Retainer); + agent->SendCommand(0, [0]); + } + + protected override void OnFinalize(AtkUnitBase* addon) + { + _isSetupComplete = false; + + CloseRetainerWindows(); + + base.OnFinalize(addon); + } +} \ No newline at end of file diff --git a/AetherBags/Addons/AddonSaddleBagWindow.cs b/AetherBags/Addons/AddonSaddleBagWindow.cs new file mode 100644 index 0000000..71ceabc --- /dev/null +++ b/AetherBags/Addons/AddonSaddleBagWindow.cs @@ -0,0 +1,116 @@ +using System.Numerics; +using AetherBags.Inventory.State; +using AetherBags.Nodes.Input; +using AetherBags.Nodes.Inventory; +using AetherBags.Nodes.Layout; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.GUI; +using KamiToolKit.Classes; +using KamiToolKit.Nodes; + +namespace AetherBags.Addons; + +public unsafe class AddonSaddleBagWindow : InventoryAddonBase +{ + private readonly SaddleBagState _inventoryState = new(); + private TextNode _slotCounterNode = null!; + + protected override InventoryStateBase InventoryState => _inventoryState; + + protected override bool HasFooter => false; + protected override bool HasSlotCounter => true; + + private readonly Vector3 _tintColor = new (-16f / 255f, -4f / 255f, 8f / 255f); + + protected override float MinWindowWidth => 400; + protected override float MaxWindowWidth => 600; + + protected override void OnSetup(AtkUnitBase* addon) + { + InitializeBackgroundDropTarget(); + + WindowNode?.AddColor = _tintColor; + + CategoriesNode = new WrappingGridNode + { + Position = ContentStartPosition, + Size = ContentSize, + HorizontalSpacing = CategorySpacing, + VerticalSpacing = CategorySpacing, + TopPadding = 4.0f, + BottomPadding = 4.0f, + }; + CategoriesNode.AttachNode(this); + + var header = CalculateHeaderLayout(addon); + + SearchInputNode = new TextInputWithButtonNode + { + Position = header.SearchPosition, + Size = header.SearchSize, + OnInputReceived = _ => ItemRefresh(), + OnButtonClicked = () => InventoryAddonContextMenu.OpenMain(this) + }; + SearchInputNode.AttachNode(this); + + SettingsButtonNode = new CircleButtonNode + { + Position = new Vector2(header.HeaderWidth - SettingsButtonOffset, header.HeaderY), + Size = new Vector2(28f), + AddColor = _tintColor, + Icon = ButtonIcon.GearCog, + OnClick = System.AddonConfigurationWindow.Toggle + }; + SettingsButtonNode.AttachNode(this); + + _slotCounterNode = new TextNode + { + Position = new Vector2(Size.X - 10, 0), + Size = new Vector2(82, 20), + AlignmentType = AlignmentType.Right, + FontType = FontType.MiedingerMed, + TextFlags = TextFlags.Glare, + TextColor = ColorHelper.GetColor(50), + TextOutlineColor = ColorHelper.GetColor(32) + }; + _slotCounterNode.AttachNode(this); + SlotCounterNode = _slotCounterNode; + + LayoutContent(); + + _inventoryState.RefreshFromGame(); + + _isSetupComplete = true; + + RefreshCategoriesCore(autosize: true); + + base.OnSetup(addon); + } + + protected override void RefreshCategoriesCore(bool autosize) + { + if (!_isSetupComplete) + return; + + _slotCounterNode.String = _inventoryState.GetEmptySlotsString(); + + base.RefreshCategoriesCore(autosize); + } + + protected override void OnFinalize(AtkUnitBase* addon) + { + _isSetupComplete = false; + + if (System.Config.General.HideGameSaddleBags) + { + var saddleAddon = RaptureAtkUnitManager.Instance()->GetAddonByName("InventoryBuddy"); + if (saddleAddon != null) + { + saddleAddon->IsVisible = true; + saddleAddon->Close(true); + } + } + + 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/IInventoryWindow.cs b/AetherBags/Addons/IInventoryWindow.cs new file mode 100644 index 0000000..b8abd4b --- /dev/null +++ b/AetherBags/Addons/IInventoryWindow.cs @@ -0,0 +1,11 @@ +namespace AetherBags.Addons; + +public interface IInventoryWindow +{ + bool IsOpen { get; } + void Toggle(); + void Close(); + void ManualRefresh(); + void ItemRefresh(); + void SetSearchText(string searchText); +} \ No newline at end of file diff --git a/AetherBags/Addons/InventoryAddonBase.cs b/AetherBags/Addons/InventoryAddonBase.cs new file mode 100644 index 0000000..b516d7e --- /dev/null +++ b/AetherBags/Addons/InventoryAddonBase.cs @@ -0,0 +1,437 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using AetherBags.Configuration; +using AetherBags.Helpers; +using AetherBags.Inventory; +using AetherBags.Inventory.Categories; +using AetherBags.Inventory.Context; +using AetherBags.Inventory.Scanning; +using AetherBags.Inventory.State; +using AetherBags.Nodes.Input; +using AetherBags.Nodes.Inventory; +using AetherBags.Nodes.Layout; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Component.GUI; +using KamiToolKit; +using KamiToolKit.Classes; +using KamiToolKit.Classes.ContextMenu; +using KamiToolKit.Nodes; + +namespace AetherBags.Addons; + +public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow +{ + protected readonly InventoryCategoryHoverCoordinator HoverCoordinator = new(); + protected readonly InventoryCategoryPinCoordinator PinCoordinator = new(); + protected readonly HashSet HoverSubscribed = new(); + + protected DragDropNode BackgroundDropTarget = null!; + protected WrappingGridNode CategoriesNode = null!; + protected TextInputWithButtonNode SearchInputNode = null!; + protected InventoryFooterNode FooterNode = null!; + protected TextNode? SlotCounterNode { get; set; } + protected CircleButtonNode SettingsButtonNode = null!; + + internal ContextMenu ContextMenu = 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 const float SettingsButtonOffset = 48f; + + protected bool RefreshQueued; + protected bool RefreshAutosizeQueued; + private bool _isRefreshing; + protected bool _isSetupComplete; + + protected abstract InventoryStateBase InventoryState { get; } + + protected virtual bool HasFooter => true; + protected virtual bool HasPinning => true; + protected virtual bool HasSlotCounter => false; + + private readonly HashSet _searchMatchScratch = new(); + + public void ManualRefresh() + { + if (!IsOpen) return; + if (!Services.ClientState.IsLoggedIn) return; + if (_isRefreshing) return; + if (!_isSetupComplete) return; + + try + { + _isRefreshing = true; + InventoryState.RefreshFromGame(); + RefreshCategoriesCore(autosize: true); + } + finally + { + _isRefreshing = false; + } + } + + + public string GetSearchText() => SearchInputNode?.SearchString.ExtractText() ?? string.Empty; + + public virtual void SetSearchText(string searchText) + { + Services.Framework.RunOnTick(() => + { + if (IsOpen) SearchInputNode.SearchString = searchText; + RefreshCategoriesCore(autosize: true); + }, delayTicks: 3); + } + + public void RefreshFromLifecycle() + { + if (!_isSetupComplete) return; + if (!IsOpen) return; + if (_isRefreshing) return; + + try + { + _isRefreshing = true; + InventoryState.RefreshFromGame(); + RefreshCategoriesCore(autosize: true); + } + finally + { + _isRefreshing = false; + } + } + + protected virtual void RefreshCategoriesCore(bool autosize) + { + if (!_isSetupComplete) + return; + + var config = System.Config.General; + string searchText = SearchInputNode.SearchString.ExtractText(); + bool isSearching = !string.IsNullOrWhiteSpace(searchText); + + if (config.SearchMode == SearchMode.Highlight && isSearching) + { + _searchMatchScratch.Clear(); + var allData = InventoryState.GetCategories(string.Empty); + + for (int i = 0; i < allData.Count; i++) + { + var cat = allData[i]; + for (int j = 0; j < cat.Items.Count; j++) + { + var item = cat.Items[j]; + if (item.IsRegexMatch(searchText)) + { + _searchMatchScratch.Add(item.Item.ItemId); + } + } + } + HighlightState.SetFilter(HighlightSource.Search, _searchMatchScratch); + } + else + { + HighlightState.ClearFilter(HighlightSource.Search); + } + + if (SearchInputNode != null) + { + bool atActive = !string.IsNullOrEmpty(HighlightState.SelectedAllaganToolsFilterKey); + + SearchInputNode.HintAddColor = (atActive) + ? new Vector3(0.0f, 0.3f, 0.3f) + : Vector3.Zero; + } + + if (HasFooter) + { + FooterNode.SlotAmountText = InventoryState.GetEmptySlotsString(); + FooterNode.RefreshCurrencies(); + } + + string dataFilter = config.SearchMode == SearchMode.Filter ? searchText : string.Empty; + var categories = InventoryState.GetCategories(dataFilter); + + 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); + node.RefreshNodeVisuals(); + }, + createNodeMethod: _ => CreateCategoryNode()); + + if (HasPinning) + { + bool pinsChanged = PinCoordinator.ApplyPinnedStates(CategoriesNode); + if (pinsChanged) HoverCoordinator.ResetAll(CategoriesNode); + } + + WireHoverHandlers(); + + if (autosize) + AutoSizeWindow(); + else + { + LayoutContent(); + CategoriesNode.RecalculateLayout(); + } + } + + protected readonly struct HeaderLayout + { + public Vector2 SearchPosition { get; init; } + public Vector2 SearchSize { get; init; } + public float HeaderWidth { get; init; } + public float HeaderY { get; init; } + } + + protected HeaderLayout CalculateHeaderLayout(AtkUnitBase* addon) + { + var header = addon->WindowHeaderCollisionNode; + float headerW = header->Width; + float headerH = header->Height; + + // Center the search bar, width is 50% of header + float searchWidth = headerW * 0.5f; + var searchSize = new Vector2(searchWidth, 28f); + + float searchX = (headerW - searchWidth) * 0.5f; + float itemY = header->Y + (headerH - 28f) * 0.5f; + + return new HeaderLayout + { + SearchPosition = new Vector2(searchX, itemY), + SearchSize = searchSize, + HeaderWidth = headerW, + HeaderY = itemY + }; + } + + + protected void InitializeBackgroundDropTarget() + { + BackgroundDropTarget = new DragDropNode + { + Position = ContentStartPosition, + Size = ContentSize, + IconId = 0, + IsDraggable = false, + IsClickable = false, + AcceptedType = DragDropType.Item, + }; + + BackgroundDropTarget.DragDropBackgroundNode.IsVisible = false; + BackgroundDropTarget.IconNode.IsVisible = false; + + BackgroundDropTarget.OnPayloadAccepted = OnBackgroundPayloadAccepted; + + BackgroundDropTarget.AttachNode(this); + } + + protected virtual InventoryCategoryNode CreateCategoryNode() + { + return new InventoryCategoryNode + { + Size = ContentSize with { Y = 120 }, + OnRefreshRequested = ManualRefresh, + OnDragEnd = () => InventoryOrchestrator.RefreshAll(updateMaps: true), + }; + } + + private void OnBackgroundPayloadAccepted(DragDropNode node, DragDropPayload acceptedPayload) + { + if (!acceptedPayload.IsValidInventoryPayload) return; + + InventoryLocation emptyLocation = InventoryScanner.GetFirstEmptySlot(InventoryState.SourceType); + + if (!emptyLocation.IsValid) + { + Services.Logger.Error("No empty slots available to receive drop."); + return; + } + + InventoryMappedLocation visualLocation = InventoryContextState.GetVisualLocation(emptyLocation.Container, emptyLocation.Slot); + + var visualInvType = InventoryType.GetInventoryTypeFromContainerId(visualLocation.Container); + int absoluteIndex = visualInvType.GetInventoryStartIndex + visualLocation.Slot; + + var targetPayload = new DragDropPayload + { + Type = DragDropType.Item, + Int1 = visualLocation.Container, + Int2 = visualLocation.Slot, + ReferenceIndex = (short)absoluteIndex + }; + + Services.Logger.DebugOnly($"[BackgroundDrop] Target: {emptyLocation} -> Visual: {visualLocation} (Ref: {absoluteIndex})"); + + InventoryMoveHelper.HandleItemMovePayload(acceptedPayload, targetPayload); + + ManualRefresh(); + } + + 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; + + float footerH = HasFooter || HasSlotCounter ? FooterHeight : 0; + + if (HasFooter) + { + FooterNode.Position = new Vector2(contentPos.X, contentPos.Y + contentSize.Y - footerH); + FooterNode.Size = new Vector2(contentSize.X, footerH); + } + else if (HasSlotCounter && SlotCounterNode != null) + { + SlotCounterNode.Position = new Vector2(contentSize.X -80f, contentPos.Y + contentSize.Y - footerH + 4f); + } + + 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 || HasSlotCounter ? 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); + + if (BackgroundDropTarget != null) + { + BackgroundDropTarget.Size = ContentSize; + } + + LayoutContent(); + + if (recalcLayout) + CategoriesNode.RecalculateLayout(); + } + + protected void ResizeWindow(float width, float height) + => ResizeWindow(width, height, recalcLayout: true); + + public void ItemRefresh() => RefreshCategoriesCore(false); + + protected override void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) + { + base.OnRequestedUpdate(addon, numberArrayData, stringArrayData); + + InventoryState.RefreshFromGame(); + RefreshCategoriesCore(autosize: true); + } + + protected override void OnSetup(AtkUnitBase* addon) + { + ContextMenu = new ContextMenu(); + + base.OnSetup(addon); + } + + protected override void OnUpdate(AtkUnitBase* addon) + { + if (RefreshQueued) + { + bool doAutosize = RefreshAutosizeQueued; + RefreshQueued = false; + RefreshAutosizeQueued = false; + + RefreshCategoriesCore(doAutosize); + } + + base.OnUpdate(addon); + } + + protected override void OnFinalize(AtkUnitBase* addon) + { + ContextMenu?.Dispose(); + HoverSubscribed.Clear(); + RefreshQueued = false; + RefreshAutosizeQueued = false; + + base.OnFinalize(addon); + } +} \ No newline at end of file diff --git a/AetherBags/Addons/InventoryAddonContextMenu.cs b/AetherBags/Addons/InventoryAddonContextMenu.cs new file mode 100644 index 0000000..cee746a --- /dev/null +++ b/AetherBags/Addons/InventoryAddonContextMenu.cs @@ -0,0 +1,84 @@ +using System; +using AetherBags.Configuration; +using AetherBags.Inventory; +using AetherBags.Inventory.Context; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using KamiToolKit.Classes.ContextMenu; + +namespace AetherBags.Addons; + +public static class InventoryAddonContextMenu +{ + private static ContextMenuItem Separator => new() + { + Name = "---------------------------", + IsEnabled = false, + OnClick = () => { } + }; + + public static void OpenMain(InventoryAddonBase parent) + { + if (parent?.ContextMenu == null || System.Config == null) return; + + var menu = parent.ContextMenu; + menu.Clear(); + + bool hasActiveAtFilter = !string.IsNullOrEmpty(HighlightState.SelectedAllaganToolsFilterKey); + string searchText = parent.GetSearchText(); + if (HighlightState.IsFilterActive || hasActiveAtFilter || !string.IsNullOrEmpty(searchText)) + { + menu.AddItem("Clear All Filters", () => + { + HighlightState.ClearAll(); + parent.SetSearchText(string.Empty); + InventoryOrchestrator.RefreshAll(updateMaps: false); + }); + menu.AddItem(Separator); + } + + var currentMode = System.Config.General.SearchMode; + string modeLabel = currentMode == SearchMode.Filter ? "Mode: Hide Non-Matches" : "Mode: Fade Non-Matches"; + menu.AddItem(modeLabel, () => + { + System.Config.General.SearchMode = currentMode == SearchMode.Filter ? SearchMode.Highlight : SearchMode.Filter; + parent.ManualRefresh(); + }); + + if (System.IPC.AllaganTools is { IsReady: true } && System.Config.Categories.AllaganToolsCategoriesEnabled) + { + var atFilters = System.IPC.AllaganTools.GetSearchFilters(); + if (atFilters is { Count: > 0 }) + { + var subMenu = new ContextMenuSubItem + { + Name = "Allagan Tools Filters...", + OnClick = () => { } + }; + + foreach (var (key, name) in atFilters) + { + var capturedKey = key; + bool isActive = HighlightState.SelectedAllaganToolsFilterKey == key; + subMenu.AddItem(isActive ?$"✓ {name}" : $" {name}", () => + { + HighlightState.SelectedAllaganToolsFilterKey = isActive ? string.Empty : capturedKey; + InventoryOrchestrator.RefreshAll(updateMaps: false); + }); + } + + menu.AddItem(subMenu); + } + } + + menu.Open(); + } + + public static unsafe void Close() + { + var agent = AgentContext.Instance(); + if (agent != null) + { + agent->ClearMenu(); + } + } +} \ No newline at end of file diff --git a/AetherBags/Commands/CommandHandler.cs b/AetherBags/Commands/CommandHandler.cs index 3d36d4e..275f84d 100644 --- a/AetherBags/Commands/CommandHandler.cs +++ b/AetherBags/Commands/CommandHandler.cs @@ -1,7 +1,9 @@ using System; using AetherBags.Helpers; using AetherBags.Inventory; +using AetherBags.Inventory.State; using Dalamud.Game.Command; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; namespace AetherBags.Commands; @@ -28,7 +30,7 @@ public class CommandHandler : IDisposable }); } - private void OnCommand(string command, string args) + private unsafe void OnCommand(string command, string args) { var argsParts = args.Trim().Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); var subCommand = argsParts.Length > 0 ? argsParts[0].ToLowerInvariant() : string.Empty; @@ -57,7 +59,7 @@ public class CommandHandler : IDisposable break; case "refresh": - System.AddonInventoryWindow.ManualInventoryRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); PrintChat("Inventory refreshed."); break; @@ -67,7 +69,7 @@ public class CommandHandler : IDisposable case "import-sk": ImportExportResetHelper.TryImportSortaKindaFromClipboard(true); - System.AddonInventoryWindow.ManualInventoryRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); break; case "export": @@ -76,12 +78,12 @@ public class CommandHandler : IDisposable case "import": ImportExportResetHelper.TryImportConfigFromClipboard(); - System.AddonInventoryWindow.ManualInventoryRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); break; case "reset": ImportExportResetHelper.TryResetConfig(); - System.AddonInventoryWindow.ManualInventoryRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); break; case "count": @@ -90,6 +92,14 @@ public class CommandHandler : IDisposable PrintChat($"{stats.UsedSlots}/{stats.TotalSlots} slots used ({stats.UsagePercent:F0}%) | {stats.TotalItems} unique items | {stats.CategoryCount} categories"); break; + case "saddle": + System.AddonSaddleBagWindow.Toggle(); + break; + + case "retainer": + System.AddonRetainerWindow.Toggle(); + break; + case "help": case "?": PrintHelp(); diff --git a/AetherBags/Configuration/CategorySettings.cs b/AetherBags/Configuration/CategorySettings.cs index 898a201..12547d3 100644 --- a/AetherBags/Configuration/CategorySettings.cs +++ b/AetherBags/Configuration/CategorySettings.cs @@ -8,8 +8,12 @@ namespace AetherBags.Configuration; public class CategorySettings { + public bool CategoriesEnabled { get; set; } = true; public bool GameCategoriesEnabled { get; set; } = true; public bool UserCategoriesEnabled { get; set; } = true; + public bool BisBuddyEnabled { get; set; } = true; + public bool AllaganToolsCategoriesEnabled { get; set; } = false; + public AllaganToolsFilterMode AllaganToolsMode { get; set; } = AllaganToolsFilterMode.Highlight; public List UserCategories { get; set; } = new(); } @@ -75,4 +79,10 @@ public enum ToggleFilterState Ignored = 0, Allow = 1, Disallow = 2, +} + +public enum AllaganToolsFilterMode +{ + Categorize = 0, + Highlight = 1, } \ No newline at end of file diff --git a/AetherBags/Configuration/GeneralSettings.cs b/AetherBags/Configuration/GeneralSettings.cs index c8e8b3c..79583f5 100644 --- a/AetherBags/Configuration/GeneralSettings.cs +++ b/AetherBags/Configuration/GeneralSettings.cs @@ -3,6 +3,7 @@ namespace AetherBags.Configuration; public class GeneralSettings { public InventoryStackMode StackMode { get; set; } = InventoryStackMode.AggregateByItemId; + public SearchMode SearchMode { get; set; } = SearchMode.Highlight; public bool DebugEnabled { get; set; } = false; public bool CompactPackingEnabled { get; set; } = true; public int CompactLookahead { get; set; } = 24; @@ -10,6 +11,10 @@ public class GeneralSettings public bool CompactStableInsert { get; set; } = true; public bool OpenWithGameInventory { get; set; } = true; public bool HideGameInventory { get; set; } = false; + public bool OpenSaddleBagsWithGameInventory { get; set; } = true; + public bool HideGameSaddleBags { get; set; } = false; + public bool OpenRetainerWithGameInventory { get; set; } = true; + public bool HideGameRetainer { get; set; } = false; public bool ShowCategoryItemCount { get; set; } = false; public bool LinkItemEnabled { get; set; } = false; } @@ -18,4 +23,10 @@ public enum InventoryStackMode : byte { NaturalStacks = 0, AggregateByItemId = 1, +} + +public enum SearchMode : byte +{ + Filter = 0, + Highlight = 1, } \ No newline at end of file diff --git a/AetherBags/Extensions/AgentInterfaceExtensions.cs b/AetherBags/Extensions/AgentInterfaceExtensions.cs new file mode 100644 index 0000000..2236e0a --- /dev/null +++ b/AetherBags/Extensions/AgentInterfaceExtensions.cs @@ -0,0 +1,23 @@ +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace AetherBags.Extensions; + +public static unsafe class AgentInterfaceExtensions { + + extension(ref AgentInterface agent) + { + public void SendCommand(uint eventKind, int[] commandValues) + { + using var returnValue = new AtkValue(); + var command = stackalloc AtkValue[commandValues.Length]; + + for (var index = 0; index < commandValues.Length; index++) + { + command[index].SetInt(commandValues[index]); + } + + agent.ReceiveEvent(&returnValue, command, (uint)commandValues.Length, eventKind); + } + } +} diff --git a/AetherBags/Extensions/AtkResNodeExtensions.cs b/AetherBags/Extensions/AtkResNodeExtensions.cs deleted file mode 100644 index f5da790..0000000 --- a/AetherBags/Extensions/AtkResNodeExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using FFXIVClientStructs.FFXIV.Client.Game; -using FFXIVClientStructs.FFXIV.Component.GUI; - -namespace AetherBags.Extensions; - -public static unsafe class AtkResNodeExtensions -{ - extension(ref AtkResNode node) - { - public void ShowInventoryItemTooltip(InventoryType container, short slot) { - fixed (AtkResNode* nodePointer = &node) { - AtkStage.Instance()->ShowInventoryItemTooltip(nodePointer, container, slot); - } - } - } -} \ No newline at end of file diff --git a/AetherBags/Extensions/DragDropPayloadExtensions.cs b/AetherBags/Extensions/DragDropPayloadExtensions.cs index a8cd0dd..197f6ed 100644 --- a/AetherBags/Extensions/DragDropPayloadExtensions.cs +++ b/AetherBags/Extensions/DragDropPayloadExtensions.cs @@ -1,4 +1,4 @@ -using AetherBags.Interop; + using AetherBags.Inventory; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -8,55 +8,8 @@ using Lumina.Text; namespace AetherBags.Extensions; -// TODO: Remove FixedInterface when CS is merged into Dalamud. public static unsafe class DragDropPayloadExtensions { - public static DragDropPayload FromFixedInterface(AtkDragDropInterface* dragDropInterface) - { - // Cast to our manual fixed struct - var fixedInterface = (AtkDragDropInterfaceFixed*)dragDropInterface; - - // Calls Index 12 - var payloadContainer = fixedInterface->GetPayloadContainer(); - - return new DragDropPayload - { - Type = fixedInterface->DragDropType, - ReferenceIndex = fixedInterface->DragDropReferenceIndex, - Int1 = payloadContainer->Int1, - Int2 = payloadContainer->Int2, - Text = new ReadOnlySeString(payloadContainer->Text), - }; - } - - public static void ToFixedInterface(this DragDropPayload payload, AtkDragDropInterface* dragDropInterface, bool writeToPayloadContainer = true) - { - var fixedInterface = (AtkDragDropInterfaceFixed*)dragDropInterface; - - fixedInterface->DragDropType = payload.Type; - fixedInterface->DragDropReferenceIndex = payload.ReferenceIndex; - - if (writeToPayloadContainer) - { - // Calls Index 12 - var payloadContainer = fixedInterface->GetPayloadContainer(); - - payloadContainer->Clear(); - payloadContainer->Int1 = payload.Int1; - payloadContainer->Int2 = payload.Int2; - - if (payload.Text.IsEmpty) - { - payloadContainer->Text.Clear(); - } - else - { - var stringBuilder = new SeStringBuilder().Append(payload.Text); - payloadContainer->Text.SetString(stringBuilder.GetViewAsSpan()); - } - } - } - extension(DragDropPayload payload) { public bool IsValidInventoryPayload => @@ -65,6 +18,15 @@ public static unsafe class DragDropPayloadExtensions or DragDropType.RemoteInventory_Item or DragDropType.Item; + public bool IsSameBaseContainer(DragDropPayload otherPayload) { + if (payload.InventoryLocation.Container.IsSameContainerGroup(otherPayload.InventoryLocation.Container)) + { + return true; + } + + return false; + } + public InventoryLocation InventoryLocation { get @@ -84,7 +46,7 @@ public static unsafe class DragDropPayloadExtensions if (sourceContainer == 0) return new InventoryLocation(0, 0); - // Retainers have special handling: UI has 5 tabs × 35 slots, data has 7 pages × 25 slots + // Retainers have special handling: UI has 5 tabs × 35 slots, data has 7 pages × 25 slots if (sourceContainer.IsRetainer) { // Container IDs 52-56 = UI tabs 0-4 diff --git a/AetherBags/Extensions/InventoryItemExtensions.cs b/AetherBags/Extensions/InventoryItemExtensions.cs index 7189894..1dbe710 100644 --- a/AetherBags/Extensions/InventoryItemExtensions.cs +++ b/AetherBags/Extensions/InventoryItemExtensions.cs @@ -115,6 +115,9 @@ public static unsafe class InventoryItemExtensions { if (InventoryManager.Instance()->GetInventoryItemCount(itemId, true) > 0) itemId += 1_000_000; + if (!item.Container.IsMainInventory) + return; + AgentInventoryContext.Instance()->UseItem(itemId, type); } } diff --git a/AetherBags/Extensions/InventoryTypeExtensions.cs b/AetherBags/Extensions/InventoryTypeExtensions.cs index 37a2b7d..77c6e43 100644 --- a/AetherBags/Extensions/InventoryTypeExtensions.cs +++ b/AetherBags/Extensions/InventoryTypeExtensions.cs @@ -1,6 +1,7 @@ using AetherBags.Inventory; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using InventoryManager = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager; namespace AetherBags.Extensions; @@ -107,17 +108,17 @@ public static unsafe class InventoryTypeExtensions }; public int GetInventoryStartIndex => inventoryType switch { - InventoryType.Inventory2 => inventoryType.GetInventorySorter->ItemsPerPage, - InventoryType.Inventory3 => inventoryType.GetInventorySorter->ItemsPerPage * 2, - InventoryType.Inventory4 => inventoryType.GetInventorySorter->ItemsPerPage * 3, - InventoryType.SaddleBag2 => inventoryType.GetInventorySorter->ItemsPerPage, - InventoryType.PremiumSaddleBag2 => inventoryType.GetInventorySorter->ItemsPerPage, - InventoryType.RetainerPage2 => inventoryType.GetInventorySorter->ItemsPerPage, - InventoryType.RetainerPage3 => inventoryType.GetInventorySorter->ItemsPerPage * 2, - InventoryType.RetainerPage4 => inventoryType.GetInventorySorter->ItemsPerPage * 3, - InventoryType.RetainerPage5 => inventoryType.GetInventorySorter->ItemsPerPage * 4, - InventoryType.RetainerPage6 => inventoryType.GetInventorySorter->ItemsPerPage * 5, - InventoryType.RetainerPage7 => inventoryType.GetInventorySorter->ItemsPerPage * 6, + InventoryType.Inventory2 => inventoryType.UIPageSize, + InventoryType.Inventory3 => inventoryType.UIPageSize * 2, + InventoryType.Inventory4 => inventoryType.UIPageSize * 3, + InventoryType.SaddleBag2 => inventoryType.UIPageSize, + InventoryType.PremiumSaddleBag2 => inventoryType.UIPageSize, + InventoryType.RetainerPage2 => inventoryType.UIPageSize, + InventoryType.RetainerPage3 => inventoryType.UIPageSize * 2, + InventoryType.RetainerPage4 => inventoryType.UIPageSize * 3, + InventoryType.RetainerPage5 => inventoryType.UIPageSize * 4, + InventoryType.RetainerPage6 => inventoryType.UIPageSize * 5, + InventoryType.RetainerPage7 => inventoryType.UIPageSize * 6, _ => 0, }; @@ -156,6 +157,14 @@ public static unsafe class InventoryTypeExtensions InventoryType.RetainerPage6 or InventoryType.RetainerPage7; + public int UIPageSize => inventoryType switch + { + _ when (inventoryType.IsMainInventory || inventoryType.IsRetainer) => 35, + _ when inventoryType.IsSaddleBag => 70, + _ when inventoryType.IsArmory => 50, + _ => 0, + }; + public int ContainerGroup => inventoryType switch { _ when inventoryType.IsMainInventory => 1, @@ -165,6 +174,8 @@ public static unsafe class InventoryTypeExtensions _ => 0, }; + public bool IsLoaded => InventoryManager.Instance()->GetInventoryContainer(inventoryType)->IsLoaded; + public bool IsSameContainerGroup(InventoryType other) => inventoryType.ContainerGroup == other.ContainerGroup; diff --git a/AetherBags/Extensions/LoggerExtensions.cs b/AetherBags/Extensions/LoggerExtensions.cs index dd6e759..3882e87 100644 --- a/AetherBags/Extensions/LoggerExtensions.cs +++ b/AetherBags/Extensions/LoggerExtensions.cs @@ -2,13 +2,16 @@ namespace AetherBags.Extensions; public static class LoggerExtensions { - public static void DebugOnly(this object logger, string message) + extension(object logger) { - if(System.Config.General.DebugEnabled) Services.Logger.Debug(message); - } + public void DebugOnly(string message) + { + if (System.Config?.General?.DebugEnabled == true) + { + Services.Logger.DebugOnly(message); + } + } - public static void DebugOnly(this object logger, string message, params object[] args) - { - if(System.Config.General.DebugEnabled) Services.Logger.Debug(message); + public void DebugOnly(string message, params object[] args) => DebugOnly(logger, string.Format(message, args)); } } \ No newline at end of file diff --git a/AetherBags/Helpers/BackupHelper.cs b/AetherBags/Helpers/BackupHelper.cs index 60466cc..4446e13 100644 --- a/AetherBags/Helpers/BackupHelper.cs +++ b/AetherBags/Helpers/BackupHelper.cs @@ -14,7 +14,7 @@ public static class BackupHelper { private const int MaxBackups = 10; private const string Name = "AetherBags"; public static void DoConfigBackup(IDalamudPluginInterface pluginInterface) { - Services.Logger.Debug("Backup configuration start."); + Services.Logger.DebugOnly("Backup configuration start."); try { var configDirectory = pluginInterface.ConfigDirectory; if (!configDirectory.Exists) { diff --git a/AetherBags/Helpers/InventoryMoveHelper.cs b/AetherBags/Helpers/InventoryMoveHelper.cs index 9fef5cc..4523303 100644 --- a/AetherBags/Helpers/InventoryMoveHelper.cs +++ b/AetherBags/Helpers/InventoryMoveHelper.cs @@ -1,45 +1,52 @@ +using System; using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using FFXIVClientStructs.FFXIV.Component.GUI; +using KamiToolKit.Classes; +using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; namespace AetherBags. Helpers; public static unsafe class InventoryMoveHelper { - // Requires the visual UI slots instead of actual slots. public static void MoveItem(InventoryType sourceContainer, ushort sourceSlot, InventoryType destContainer, ushort destSlot) { - Services.Logger.Debug($"[MoveItem] {sourceContainer}@{sourceSlot} -> {destContainer}@{destSlot}"); + Services.Logger.DebugOnly($"[MoveItem] {sourceContainer}@{sourceSlot} -> {destContainer}@{destSlot}"); InventoryManager.Instance()->MoveItemSlot(sourceContainer, sourceSlot, destContainer, destSlot, true); - Services.Framework.DelayTicks(2); - Services.Framework.RunOnFrameworkThread(System.AddonInventoryWindow.ManualInventoryRefresh); + Services.Framework.DelayTicks(3); + Services.Framework.RunOnFrameworkThread(System.AddonInventoryWindow.ManualRefresh); } - /* - private static void MoveItemViaAgent(InventoryType sourceInventory, ushort sourceSlot, InventoryType destInventory, ushort destSlot) + public static void HandleItemMovePayload(DragDropPayload source, DragDropPayload target) { - uint sourceContainerId = sourceInventory.AgentItemContainerId; - uint destContainerId = destInventory.AgentItemContainerId; + uint srcContainer = (uint)source.Int1; + uint dstContainer = (uint)target.Int1; - if (sourceContainerId == 0 || destContainerId == 0) - { - Services.Logger.Warning($"[MoveItemViaAgent] Invalid container IDs: src={sourceContainerId}, dst={destContainerId}"); - return; - } + uint srcSlot = (uint)source.Int2; + uint dstSlot = (uint)target.Int2; - Services.Logger.Debug($"[MoveItemViaAgent] {sourceContainerId}:{sourceSlot} -> {destContainerId}:{destSlot}"); + short srcRi = source.ReferenceIndex; + short dstRi = target.ReferenceIndex; + + if (srcContainer == 0 || dstContainer == 0) return; + + Services.Logger.DebugOnly($"[MoveItemViaAgent] {srcContainer}:{srcSlot}:{srcRi} -> {dstContainer}:{dstSlot}:{dstRi}"); var atkValues = stackalloc AtkValue[4]; for (var i = 0; i < 4; i++) - atkValues[i]. Type = ValueType.UInt; + { + atkValues[i].Type = ValueType.UInt; + } - atkValues[0].SetUInt(sourceContainerId); - atkValues[1].SetUInt(sourceSlot); - atkValues[2].SetUInt(destContainerId); - atkValues[3].SetUInt(destSlot); + atkValues[0].UInt = srcContainer; + atkValues[1].UInt = srcSlot; + atkValues[2].UInt = dstContainer; + atkValues[3].UInt = dstSlot; var retVal = stackalloc AtkValue[1]; RaptureAtkModule* atkModule = RaptureAtkModule.Instance(); atkModule->HandleItemMove(retVal, atkValues, 4); } - */ } \ No newline at end of file diff --git a/AetherBags/Hooks/InventoryHook.cs b/AetherBags/Hooks/InventoryHook.cs index f5df87c..996b84d 100644 --- a/AetherBags/Hooks/InventoryHook.cs +++ b/AetherBags/Hooks/InventoryHook.cs @@ -37,7 +37,7 @@ public sealed unsafe class InventoryHooks : IDisposable MoveItemSlotDetour); _moveItemSlotHook.Enable(); - Services.Logger.Debug("MoveItemSlot hooked successfully."); + Services.Logger.DebugOnly("MoveItemSlot hooked successfully."); } catch (Exception e) { @@ -51,7 +51,7 @@ public sealed unsafe class InventoryHooks : IDisposable OpenInventoryDetour); _openInventoryHook.Enable(); - Services.Logger.Debug("OpenInventory hooked successfully."); + Services.Logger.DebugOnly("OpenInventory hooked successfully."); } catch (Exception e) { @@ -64,7 +64,7 @@ public sealed unsafe class InventoryHooks : IDisposable HandleInventoryEventDetour); _handleInventoryEventHook.Enable(); - Services.Logger.Debug("HandleInventoryEvent hooked successfully."); + Services.Logger.DebugOnly("HandleInventoryEvent hooked successfully."); } catch (Exception e) { @@ -77,7 +77,7 @@ public sealed unsafe class InventoryHooks : IDisposable OpenAddonDetour); _openAddonHook.Enable(); - Services.Logger.Debug("OpenAddon hooked successfully."); + Services.Logger.DebugOnly("OpenAddon hooked successfully."); } catch (Exception e) { @@ -93,10 +93,11 @@ public sealed unsafe class InventoryHooks : IDisposable ushort dstSlot, bool unk) { - InventoryItem* sourceItem = InventoryManager.Instance()->GetInventorySlot(srcType, srcSlot); - InventoryItem* destItem = InventoryManager.Instance()->GetInventorySlot(dstType, dstSlot); + //InventoryItem* sourceItem = InventoryManager.Instance()->GetInventorySlot(srcType, srcSlot); + //InventoryItem* destItem = InventoryManager.Instance()->GetInventorySlot(dstType, dstSlot); - Services.Logger.Debug($"[MoveItemSlot Hook] Moving {srcType}@{srcSlot} ID:{sourceItem->ItemId} -> {dstType}@{dstSlot} ID:{destItem->ItemId} Unk: {unk}"); + Services.Logger.DebugOnly($"[MoveItemSlot Hook] Moving {srcType}@{srcSlot} -> {dstType}@{dstSlot} I Unk: {unk}"); + //Services.Logger.DebugOnly($"[MoveItemSlot Hook] Moving {srcType}@{srcSlot} ID:{sourceItem->ItemId} -> {dstType}@{dstSlot} ID:{destItem->ItemId} Unk: {unk}"); return _moveItemSlotHook!.Original(manager, srcType, srcSlot, dstType, dstSlot, unk); } @@ -104,7 +105,7 @@ public sealed unsafe class InventoryHooks : IDisposable /* private void OpenInventoryDetour(UIModule* uiModule, byte type) { - Services.Logger.Debug($"[OpenInventory Hook] Opening inventory of type {type}"); + Services.Logger.DebugOnly($"[OpenInventory Hook] Opening inventory of type {type}"); _openInventoryHook?.Original(uiModule, type); } @@ -112,7 +113,7 @@ public sealed unsafe class InventoryHooks : IDisposable { for(int i = 0; i < valueCount; i++) { - Services.Logger.Debug($"[HandleInventoryEvent Hook] AtkValue[{i}]: Type={atkValue[i].Type}, ToString: {atkValue[i].ToString()} "); + Services.Logger.DebugOnly($"[HandleInventoryEvent Hook] AtkValue[{i}]: Type={atkValue[i].Type}, ToString: {atkValue[i].ToString()} "); } _handleInventoryEventHook?.Original(eventInterface, atkValue, valueCount); } @@ -121,7 +122,7 @@ public sealed unsafe class InventoryHooks : IDisposable { for(int i = 0; i < valueCount; i++) { - Services.Logger.Debug($"[OpenAddon Hook] AtkValue[{i}]: ToString: {values[i].ToString()} "); + Services.Logger.DebugOnly($"[OpenAddon Hook] AtkValue[{i}]: ToString: {values[i].ToString()} "); } return _openAddonHook!.Original(thisPtr, addonNameId, valueCount, values, eventInterface, eventKind, parentAddonId, depthLayer); } diff --git a/AetherBags/IPC/AllaganToolsIPC.cs b/AetherBags/IPC/AllaganToolsIPC.cs new file mode 100644 index 0000000..c534b99 --- /dev/null +++ b/AetherBags/IPC/AllaganToolsIPC.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; +using AetherBags.Inventory; +using AetherBags.Inventory.Context; +using Dalamud.Plugin.Ipc; + +namespace AetherBags.IPC; + +public class AllaganToolsIPC : IDisposable +{ + private ICallGateSubscriber? _isInitialized; + private ICallGateSubscriber? _initialized; + private ICallGateSubscriber>? _getFilterItems; + private ICallGateSubscriber>? _getSearchFilters; + private ICallGateSubscriber? _enableUiFilter; + private ICallGateSubscriber? _toggleUiFilter; + + public bool IsReady { get; private set; } + + /// + /// Cached filter items. Key = filterKey, Value = (ItemId -> Quantity). + /// + public Dictionary> CachedFilterItems { get; } = new(); + + /// + /// Cached search filters. Key -> Name. + /// + public Dictionary CachedSearchFilters { get; } = new(); + + /// + /// Quick lookup: ItemId -> List of filter keys that contain this item. + /// + public Dictionary> ItemToFilters { get; } = new(); + + public event Action? OnInitialized; + public event Action? OnFiltersRefreshed; + + public AllaganToolsIPC() + { + try + { + _isInitialized = Services.PluginInterface.GetIpcSubscriber("AllaganTools.IsInitialized"); + _initialized = Services.PluginInterface.GetIpcSubscriber("AllaganTools.Initialized"); + _getFilterItems = Services.PluginInterface.GetIpcSubscriber>("AllaganTools.GetFilterItems"); + _getSearchFilters = Services.PluginInterface.GetIpcSubscriber>("AllaganTools.GetSearchFilters"); + _enableUiFilter = Services.PluginInterface.GetIpcSubscriber("AllaganTools.EnableUiFilter"); + _toggleUiFilter = Services.PluginInterface.GetIpcSubscriber("AllaganTools.ToggleUiFilter"); + + _initialized.Subscribe(OnAllaganInitialized); + + try + { + IsReady = _isInitialized.InvokeFunc(); + if (IsReady) + { + RefreshFilters(); + } + } + catch + { + IsReady = false; + } + } + catch (Exception ex) + { + Services.Logger.DebugOnly($"Allagan Tools not available: {ex.Message}"); + IsReady = false; + } + } + + private void OnAllaganInitialized(bool initialized) + { + IsReady = initialized; + if (initialized) + { + Services.Logger.Information("Allagan Tools IPC connected"); + RefreshFilters(); + OnInitialized?.Invoke(); + } + } + + /// + /// Refreshes all cached filter data from Allagan Tools. + /// Call this when you need updated filter information. + /// + public void RefreshFilters() + { + if (!IsReady) return; + + try + { + CachedSearchFilters.Clear(); + CachedFilterItems.Clear(); + ItemToFilters.Clear(); + + var filters = _getSearchFilters?.InvokeFunc(); + if (filters == null) return; + + foreach (var (key, name) in filters) + { + CachedSearchFilters[key] = name; + + var items = _getFilterItems?.InvokeFunc(key); + if (items != null && items.Count > 0) + { + CachedFilterItems[key] = items; + + // Build reverse lookup + foreach (var itemId in items.Keys) + { + if (!ItemToFilters.TryGetValue(itemId, out var filterList)) + { + filterList = new List(capacity: 4); + ItemToFilters[itemId] = filterList; + } + filterList.Add(key); + } + } + } + + Services.Logger.DebugOnly($"Refreshed {CachedSearchFilters.Count} Allagan Tools filters, {ItemToFilters.Count} unique items"); + OnFiltersRefreshed?.Invoke(); + } + catch (Exception ex) + { + Services.Logger.Warning($"Failed to refresh Allagan Tools filters: {ex.Message}"); + } + } + + /// + /// Checks if an item is in any Allagan Tools filter. + /// + public bool IsItemInAnyFilter(uint itemId) + => ItemToFilters.ContainsKey(itemId); + + /// + /// Gets all filter keys that contain this item. + /// + public IReadOnlyList? GetFiltersForItem(uint itemId) + => ItemToFilters.TryGetValue(itemId, out var list) ? list : null; + + /// + /// Gets items from a specific filter. Returns ItemId -> Quantity. + /// + public Dictionary? GetFilterItems(string filterKey) + { + // Try cache first + if (CachedFilterItems.TryGetValue(filterKey, out var cached)) + return cached; + + if (!IsReady) return null; + + try + { + return _getFilterItems?.InvokeFunc(filterKey); + } + catch (Exception ex) + { + Services.Logger.Warning($"GetFilterItems failed: {ex.Message}"); + return null; + } + } + + /// + /// Gets all available search filters. Returns Key -> Name. + /// + public Dictionary? GetSearchFilters() + { + if (CachedSearchFilters.Count > 0) + return CachedSearchFilters; + + if (!IsReady) return null; + + try + { + return _getSearchFilters?.InvokeFunc(); + } + catch (Exception ex) + { + Services.Logger.Warning($"GetSearchFilters failed: {ex.Message}"); + return null; + } + } + + public void SelectFilter(string filterKey) + { + HighlightState.SelectedAllaganToolsFilterKey = filterKey; + InventoryOrchestrator.RefreshHighlights(); + } + + public void Dispose() + { + _initialized?.Unsubscribe(OnAllaganInitialized); + } +} \ No newline at end of file diff --git a/AetherBags/IPC/BisBuddyIPC.cs b/AetherBags/IPC/BisBuddyIPC.cs new file mode 100644 index 0000000..1317fe4 --- /dev/null +++ b/AetherBags/IPC/BisBuddyIPC.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using AetherBags.Inventory; +using AetherBags.Inventory.Context; +using Dalamud.Plugin.Ipc; + +namespace AetherBags.IPC; + +public class BisBuddyIPC : IDisposable +{ + private ICallGateSubscriber? _isInitialized; + private ICallGateSubscriber? _initialized; + private ICallGateSubscriber>? _getBisItems; + private ICallGateSubscriber, bool>? _bisItemsChanged; + + public bool IsReady { get; private set; } + private static readonly Vector3 BisColor = new(0.0f, 0.3f, 0.0f); + + public BisBuddyIPC() + { + try + { + _isInitialized = Services.PluginInterface.GetIpcSubscriber("BisBuddy.IsInitialized"); + _initialized = Services.PluginInterface.GetIpcSubscriber("BisBuddy.Initialized"); + _getBisItems = Services.PluginInterface.GetIpcSubscriber>("BisBuddy.GetBisItems"); + _bisItemsChanged = Services.PluginInterface.GetIpcSubscriber, bool>("BisBuddy.BisItemsChanged"); + + _initialized.Subscribe(OnInitialized); + _bisItemsChanged.Subscribe(UpdateHighlights); + + try { IsReady = _isInitialized.InvokeFunc(); } catch { IsReady = false; } + + if (IsReady) RequestUpdate(); + } + catch (Exception ex) + { + Services.Logger.DebugOnly($"BisBuddy not available: {ex.Message}"); + } + } + + private void OnInitialized(bool ready) + { + IsReady = ready; + if (ready) RequestUpdate(); + else HighlightState.ClearLabel(HighlightSource.BiSBuddy); + } + + public void RequestUpdate() + { + if (!IsReady) return; + try + { + var items = _getBisItems?.InvokeFunc(); + if (items != null) UpdateHighlights(items); + } + catch { IsReady = false; } + } + + private void UpdateHighlights(List? itemIds) + { + if (!System.Config.Categories.BisBuddyEnabled || itemIds == null || itemIds.Count == 0) + { + HighlightState.ClearLabel(HighlightSource.BiSBuddy); + } + else + { + HighlightState.SetLabel(HighlightSource.BiSBuddy, itemIds, BisColor); + } + + InventoryOrchestrator.RefreshHighlights(); + } + + public void Dispose() + { + _initialized?.Unsubscribe(OnInitialized); + _bisItemsChanged?.Unsubscribe(UpdateHighlights); + } +} \ No newline at end of file diff --git a/AetherBags/IPC/IPCService.cs b/AetherBags/IPC/IPCService.cs new file mode 100644 index 0000000..38bc733 --- /dev/null +++ b/AetherBags/IPC/IPCService.cs @@ -0,0 +1,19 @@ +using System; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; + +namespace AetherBags.IPC; + +public class IPCService : IDisposable +{ + public AllaganToolsIPC AllaganTools { get; } = new(); + public WotsItIPC WotsIt { get; } = new(); + public BisBuddyIPC BisBuddy { get; } = new(); + + public void Dispose() + { + AllaganTools.Dispose(); + WotsIt.Dispose(); + BisBuddy.Dispose(); + } +} \ No newline at end of file diff --git a/AetherBags/IPC/WotsItIPC.cs b/AetherBags/IPC/WotsItIPC.cs new file mode 100644 index 0000000..5e13b15 --- /dev/null +++ b/AetherBags/IPC/WotsItIPC.cs @@ -0,0 +1,80 @@ +using System; +using Dalamud.Plugin.Ipc; + +namespace AetherBags.IPC; + +public class WotsItIPC : IDisposable +{ + private ICallGateSubscriber? _registerWithSearch; + private ICallGateSubscriber? _invoke; + private ICallGateSubscriber? _unregisterAll; + + private string? _searchGuid; + + public WotsItIPC() + { + try + { + _registerWithSearch = Services.PluginInterface.GetIpcSubscriber("FA.RegisterWithSearch"); + _unregisterAll = Services.PluginInterface.GetIpcSubscriber("FA.UnregisterAll"); + _invoke = Services.PluginInterface.GetIpcSubscriber("FA.Invoke"); + + _invoke.Subscribe(OnInvoke); + + Register(); + } + catch (Exception ex) + { + Services.Logger.DebugOnly($"WotsIt not available: {ex.Message}"); + } + } + + private void Register() + { + try + { + UnregisterAll(); + + _searchGuid = _registerWithSearch?.InvokeFunc( + Services.PluginInterface.InternalName, + "AetherBags: Search Inventory", + "AetherBags Search", + 66472 // Icon ID + ); + } + catch (Exception ex) + { + Services.Logger.DebugOnly($"Failed to register with WotsIt: {ex.Message}"); + } + } + + private void OnInvoke(string guid) + { + if (guid == _searchGuid) + { + if (! System.AddonInventoryWindow.IsOpen) + { + System.AddonInventoryWindow.Open(); + } + } + } + + private bool UnregisterAll() + { + try + { + _unregisterAll?.InvokeFunc(Services.PluginInterface.InternalName); + return true; + } + catch + { + return false; + } + } + + public void Dispose() + { + _invoke?.Unsubscribe(OnInvoke); + UnregisterAll(); + } +} \ No newline at end of file diff --git a/AetherBags/Interop/AtkDragDropInterfaceFixed.cs b/AetherBags/Interop/AtkDragDropInterfaceFixed.cs deleted file mode 100644 index 34daf17..0000000 --- a/AetherBags/Interop/AtkDragDropInterfaceFixed.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using FFXIVClientStructs.FFXIV.Component.GUI; - -namespace AetherBags.Interop; - -// Size 0x30 (48) matches the original struct -[StructLayout(LayoutKind.Explicit, Size = 48)] -public unsafe struct AtkDragDropInterfaceFixed -{ - // Offset 0 is the Virtual Table Pointer (void**) - [FieldOffset(0)] public void** VirtualTable; - - // Map specific fields needed for Payload logic - [FieldOffset(36)] public DragDropType DragDropType; - [FieldOffset(40)] public short DragDropReferenceIndex; - - // Helper to get 'this' as a pointer - private AtkDragDropInterfaceFixed* ThisPtr => (AtkDragDropInterfaceFixed*)Unsafe.AsPointer(ref this); - - // [VirtualFunction(1)] - public void GetScreenPosition(float* screenX, float* screenY) - { - var fnPtr = (delegate* unmanaged)VirtualTable[1]; - fnPtr(ThisPtr, screenX, screenY); - } - - // [VirtualFunction(3)] - public AtkComponentNode* GetComponentNode() - { - var fnPtr = (delegate* unmanaged)VirtualTable[3]; - return fnPtr(ThisPtr); - } - - // [VirtualFunction(5)] - public void SetComponentNode(AtkComponentNode* node) - { - var fnPtr = (delegate* unmanaged)VirtualTable[5]; - fnPtr(ThisPtr, node); - } - - // [VirtualFunction(6)] - public AtkResNode* GetActiveNode() - { - var fnPtr = (delegate* unmanaged)VirtualTable[6]; - return fnPtr(ThisPtr); - } - - // [VirtualFunction(8)] - public AtkComponentBase* GetComponent() - { - var fnPtr = (delegate* unmanaged)VirtualTable[8]; - return fnPtr(ThisPtr); - } - - // [VirtualFunction(9)] - public bool HandleMouseUpEvent(AtkEventData.AtkMouseData* mouseData) - { - var fnPtr = (delegate* unmanaged)VirtualTable[9]; - return fnPtr(ThisPtr, mouseData) != 0; - } - - // [VirtualFunction(12)] - public AtkDragDropPayloadContainer* GetPayloadContainer() - { - var fnPtr = (delegate* unmanaged)VirtualTable[12]; - return fnPtr(ThisPtr); - } -} \ No newline at end of file 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/Categories/CategoryBucket.cs b/AetherBags/Inventory/Categories/CategoryBucket.cs new file mode 100644 index 0000000..3dda341 --- /dev/null +++ b/AetherBags/Inventory/Categories/CategoryBucket.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using AetherBags.Inventory.Items; + +namespace AetherBags.Inventory.Categories; + +public sealed class CategoryBucket +{ + public uint Key; + public CategoryInfo Category = null!; + public List Items = null!; + public List FilteredItems = null!; + public bool Used; +} + +public sealed class ItemCountDescComparer : IComparer +{ + public static readonly ItemCountDescComparer Instance = new(); + + public int Compare(ItemInfo? left, ItemInfo? right) + { + if (ReferenceEquals(left, right)) return 0; + if (left is null) return 1; + if (right is null) return -1; + + int leftCount = left.ItemCount; + int rightCount = right.ItemCount; + + if (leftCount > rightCount) return -1; + if (leftCount < rightCount) return 1; + return 0; + } +} \ No newline at end of file diff --git a/AetherBags/Inventory/CategoryBucketManager.cs b/AetherBags/Inventory/Categories/CategoryBucketManager.cs similarity index 74% rename from AetherBags/Inventory/CategoryBucketManager.cs rename to AetherBags/Inventory/Categories/CategoryBucketManager.cs index a565ee3..0ca3e4d 100644 --- a/AetherBags/Inventory/CategoryBucketManager.cs +++ b/AetherBags/Inventory/Categories/CategoryBucketManager.cs @@ -1,9 +1,11 @@ -using AetherBags.Configuration; using System; using System.Collections.Generic; using System.Linq; +using AetherBags.Configuration; +using AetherBags.Inventory.Items; +using KamiToolKit.Classes; -namespace AetherBags.Inventory; +namespace AetherBags.Inventory.Categories; public static class CategoryBucketManager { @@ -17,6 +19,15 @@ public static class CategoryBucketManager public static bool IsUserCategoryKey(uint key) => (key & UserCategoryKeyFlag) != 0; + private const uint AllaganFilterKeyFlag = 0x4000_0000; + + public static uint MakeAllaganFilterKey(int index) + => AllaganFilterKeyFlag | (uint)(index & 0x3FFF_FFFF); + + public static bool IsAllaganFilterKey(uint key) + => (key & AllaganFilterKeyFlag) != 0 && (key & UserCategoryKeyFlag) == 0; + + /// /// Resets all buckets for a new refresh cycle. /// @@ -147,6 +158,74 @@ public static class CategoryBucketManager } } + public static void BucketByAllaganFilters( + Dictionary itemInfoByKey, + Dictionary bucketsByKey, + HashSet claimedKeys, + bool allaganCategoriesEnabled) + { + if (!allaganCategoriesEnabled) return; + if (!System.IPC.AllaganTools.IsReady) return; + + var filters = System.IPC.AllaganTools.CachedSearchFilters; + var filterItems = System.IPC.AllaganTools.CachedFilterItems; + + int index = 0; + foreach (var (filterKey, filterName) in filters) + { + if (!filterItems. TryGetValue(filterKey, out var itemIds)) + { + index++; + continue; + } + + uint bucketKey = MakeAllaganFilterKey(index); + + if (!bucketsByKey.TryGetValue(bucketKey, out CategoryBucket? bucket)) + { + bucket = new CategoryBucket + { + Key = bucketKey, + Category = new CategoryInfo + { + Name = $"[AT] {filterName}", + Description = $"Allagan Tools filter: {filterName}", + Color = ColorHelper.GetColor(32), + }, + Items = new List(capacity: 16), + FilteredItems = new List(capacity: 16), + Used = true, + }; + bucketsByKey. Add(bucketKey, bucket); + } + else + { + bucket.Used = true; + bucket.Category.Name = $"[AT] {filterName}"; + } + + foreach (var itemKvp in itemInfoByKey) + { + ulong itemKey = itemKvp.Key; + ItemInfo item = itemKvp.Value; + + if (claimedKeys.Contains(itemKey)) + continue; + + if (itemIds.ContainsKey(item.Item.ItemId)) + { + bucket.Items.Add(item); + claimedKeys.Add(itemKey); + } + } + + if (bucket.Items. Count == 0) + bucket.Used = false; + + index++; + } + } + public static void BucketUnclaimedToMisc( Dictionary itemInfoByKey, Dictionary bucketsByKey, @@ -214,9 +293,13 @@ public static class CategoryBucketManager sortedCategoryKeys.Sort((left, right) => { - bool leftCategory = IsUserCategoryKey(left); - bool rightCategory = IsUserCategoryKey(right); - if (leftCategory != rightCategory) return leftCategory ? -1 : 1; + bool leftUser = IsUserCategoryKey(left); + bool rightUser = IsUserCategoryKey(right); + bool leftAllagan = IsAllaganFilterKey(left); + bool rightAllagan = IsAllaganFilterKey(right); + if (leftUser != rightUser) return leftUser ? -1 : 1; + if (leftAllagan != rightAllagan) return leftAllagan ? -1 : 1; + return left.CompareTo(right); }); } @@ -275,32 +358,4 @@ public static class CategoryBucketManager Name = name, }; } -} - -public sealed class CategoryBucket -{ - public uint Key; - public CategoryInfo Category = null!; - public List Items = null!; - public List FilteredItems = null!; - public bool Used; -} - -public sealed class ItemCountDescComparer : IComparer -{ - public static readonly ItemCountDescComparer Instance = new(); - - public int Compare(ItemInfo? left, ItemInfo? right) - { - if (ReferenceEquals(left, right)) return 0; - if (left is null) return 1; - if (right is null) return -1; - - int leftCount = left.ItemCount; - int rightCount = right.ItemCount; - - if (leftCount > rightCount) return -1; - if (leftCount < rightCount) return 1; - return 0; - } } \ No newline at end of file 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/Context/HighlightState.cs b/AetherBags/Inventory/Context/HighlightState.cs new file mode 100644 index 0000000..e278e5f --- /dev/null +++ b/AetherBags/Inventory/Context/HighlightState.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Numerics; + +namespace AetherBags.Inventory.Context; + +public enum HighlightSource +{ + Search, + AllaganTools, + BiSBuddy, +} + +public static class HighlightState +{ + private static readonly Dictionary> Filters = new(); + private static readonly Dictionary ids, Vector3 color)> Labels = new(); + + public static string? SelectedAllaganToolsFilterKey { get; set; } = string.Empty; + + public static bool IsFilterActive => Filters.Count > 0; + + public static void SetFilter(HighlightSource source, IEnumerable ids) + => Filters[source] = new HashSet(ids); + + public static bool IsInActiveFilters(uint itemId) + { + if (Filters.Count == 0) return true; + foreach (var filter in Filters.Values) + if (filter.Contains(itemId)) return true; + return false; + } + + public static Vector3? GetLabelColor(uint itemId) + { + foreach (var label in Labels.Values) + if (label.ids.Contains(itemId)) return label.color; + return null; + } + + public static void SetLabel(HighlightSource source, IEnumerable ids, Vector3 color) + => Labels[source] = (new HashSet(ids), color); + + + public static void ClearAll() + { + Filters.Clear(); + Labels.Clear(); + SelectedAllaganToolsFilterKey = string.Empty; + } + + public static void ClearFilter(HighlightSource source) => Filters.Remove(source); + public static void ClearLabel(HighlightSource source) => Labels.Remove(source); +} \ No newline at end of file diff --git a/AetherBags/Inventory/Context/InventoryContextState.cs b/AetherBags/Inventory/Context/InventoryContextState.cs new file mode 100644 index 0000000..1f1e8d6 --- /dev/null +++ b/AetherBags/Inventory/Context/InventoryContextState.cs @@ -0,0 +1,157 @@ +using System.Collections.Generic; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Client.UI.Arrays; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; + +namespace AetherBags.Inventory.Context; + +public static unsafe class InventoryContextState +{ + private static readonly HashSet<(int page, int slot)> EligibleSlots = new(); + private static readonly HashSet<(InventoryType container, int slot)> BlockedSlots = new(); + + private static readonly Dictionary VisualLocationMap = new(); + private static readonly Dictionary> GroupedLocationMaps = new(); + + private static uint _lastContextId; + + public static uint ActiveContextId => _lastContextId; + + public static bool HasActiveContext => _lastContextId != 0; + + public static void RefreshMaps() + { + EligibleSlots.Clear(); + VisualLocationMap.Clear(); + GroupedLocationMaps.Clear(); + + var itemOrderModule = ItemOrderModule.Instance(); + if (itemOrderModule == null) return; + + var agentInventory = AgentInventory.Instance(); + bool hasContext = agentInventory != null && agentInventory->OpenTitleId != 0; + _lastContextId = hasContext ? agentInventory->OpenTitleId : 0; + + var invArray = hasContext ? InventoryNumberArray.Instance() : null; + + // Helper local to process any sorter + void ProcessSorter(ItemOrderModuleSorter* sorter) + { + if (sorter == null) return; + + // Determine actual page size. + // We prefer the physical container size over the sorter's 'ItemsPerPage' + var baseInventoryType = sorter->InventoryType; + var inventoryManager = InventoryManager.Instance(); + var container = inventoryManager != null ? inventoryManager->GetInventoryContainer(baseInventoryType) : null; + + // Fallback to sorter value if container isn't loaded, but default to 35 for main/retainer + int itemsPerPage = baseInventoryType.UIPageSize; + if (itemsPerPage <= 0) itemsPerPage = 35; + + var baseAgentId = (int)baseInventoryType.AgentItemContainerId; + if (baseAgentId == 0) return; + + long count = sorter->Items.LongCount; + for (int displayIdx = 0; displayIdx < count; displayIdx++) + { + var entry = sorter->Items[displayIdx].Value; + if (entry == null) continue; + + var realContainer = (InventoryType)((int)baseInventoryType + entry->Page); + int realSlot = entry->Slot; + + int visualPage = displayIdx / itemsPerPage; + int visualSlot = displayIdx % itemsPerPage; + int visualContainerId = baseAgentId + visualPage; + + var realKey = new InventoryMappedLocation((int)realContainer, realSlot); + var visualValue = new InventoryMappedLocation(visualContainerId, visualSlot); + + VisualLocationMap[realKey] = visualValue; + + if (hasContext && invArray != null && baseInventoryType.IsMainInventory) + { + var itemData = invArray->Items[displayIdx]; + if (itemData.IconId != 0) + { + bool eligible = itemData.ItemFlags.MirageFlag == 0; + if (eligible) + EligibleSlots.Add(((int)realContainer - (int)InventoryType.Inventory1, realSlot)); + } + } + } + } + + ProcessSorter(itemOrderModule->InventorySorter); + + ProcessSorter(itemOrderModule->ArmouryMainHandSorter); + ProcessSorter(itemOrderModule->ArmouryOffHandSorter); + ProcessSorter(itemOrderModule->ArmouryHeadSorter); + ProcessSorter(itemOrderModule->ArmouryBodySorter); + ProcessSorter(itemOrderModule->ArmouryHandsSorter); + ProcessSorter(itemOrderModule->ArmouryLegsSorter); + ProcessSorter(itemOrderModule->ArmouryFeetSorter); + ProcessSorter(itemOrderModule->ArmouryEarsSorter); + ProcessSorter(itemOrderModule->ArmouryNeckSorter); + ProcessSorter(itemOrderModule->ArmouryWristsSorter); + ProcessSorter(itemOrderModule->ArmouryRingsSorter); + ProcessSorter(itemOrderModule->ArmourySoulCrystalSorter); + + ProcessSorter(itemOrderModule->SaddleBagSorter); + ProcessSorter(itemOrderModule->PremiumSaddleBagSorter); + + try + { + var activeRetainerSorter = itemOrderModule->GetActiveRetainerSorter(); + ProcessSorter(activeRetainerSorter); + } + catch + { + // GetActiveRetainerSorter is a member function — guard just in case + } + } + + public static void RefreshBlockedSlots() + { + BlockedSlots.Clear(); + + var inventoryManager = InventoryManager.Instance(); + if (inventoryManager == null) return; + + var blockedContainer = inventoryManager->GetInventoryContainer(InventoryType.BlockedItems); + if (blockedContainer == null) return; + + for (int i = 0; i < blockedContainer->Size; i++) + { + ref var item = ref blockedContainer->Items[i]; + if (item.ItemId == 0) continue; + + BlockedSlots.Add((item.Container, item.Slot)); + } + } + + public static bool IsEligible(int page, int slot) + => EligibleSlots.Contains((page, slot)); + + public static bool IsSlotBlocked(InventoryType container, int slot) + => BlockedSlots.Contains((container, slot)); + + public static InventoryMappedLocation GetVisualLocation(InventoryType realContainer, int slot) + { + var key = new InventoryMappedLocation((int)realContainer, slot); + if (VisualLocationMap.TryGetValue(key, out var result)) + return result; + + // default fallback: use the agent container id for the real container (works for Inventory1..4, RetainerPageN, etc.) + var defaultAgentId = (int)realContainer.AgentItemContainerId; + if (defaultAgentId == 0) + { + // final fallback: Inventory1 base at 48 + defaultAgentId = 48; + } + + return new InventoryMappedLocation(defaultAgentId, slot); + } +} \ No newline at end of file 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/InventoryContextState.cs b/AetherBags/Inventory/InventoryContextState.cs deleted file mode 100644 index 88c8a6b..0000000 --- a/AetherBags/Inventory/InventoryContextState.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System.Collections.Generic; -using FFXIVClientStructs.FFXIV.Client.Game; -using FFXIVClientStructs.FFXIV.Client.UI.Agent; -using FFXIVClientStructs.FFXIV.Client.UI.Arrays; -using FFXIVClientStructs.FFXIV.Client.UI.Misc; - -namespace AetherBags.Inventory; - -public static unsafe class InventoryContextState -{ - private static readonly HashSet<(int page, int slot)> EligibleSlots = new(); - private static readonly HashSet<(InventoryType container, int slot)> BlockedSlots = new(); - private static readonly Dictionary VisualLocationMap = new(); - private static uint _lastContextId; - - public static void RefreshMaps() - { - EligibleSlots.Clear(); - VisualLocationMap.Clear(); - - var sorter = ItemOrderModule.Instance()->InventorySorter; - if (sorter == null) return; - - var agentInventory = AgentInventory.Instance(); - bool hasContext = agentInventory != null && agentInventory->OpenTitleId != 0; - _lastContextId = hasContext ? agentInventory->OpenTitleId : 0; - - var invArray = hasContext ? InventoryNumberArray.Instance() : null; - - int itemsPerPage = sorter->ItemsPerPage; - - for (int displayIdx = 0; displayIdx < 140; displayIdx++) - { - var entry = sorter->Items[displayIdx].Value; - if (entry == null) continue; - - int realPage = entry->Page; - int realSlot = entry->Slot; - - int visualPage = displayIdx / itemsPerPage; - int visualSlot = displayIdx % itemsPerPage; - int visualContainerId = 48 + visualPage; - - VisualLocationMap[new InventoryMappedLocation(realPage, realSlot)] = new InventoryMappedLocation(visualContainerId, visualSlot); - - if (hasContext && invArray != null) - { - var itemData = invArray->Items[displayIdx]; - if (itemData.IconId == 0) continue; - - bool eligible = itemData.ItemFlags.MirageFlag == 0; - if (eligible) - { - EligibleSlots.Add((realPage, realSlot)); - } - } - } - } - - public static void RefreshBlockedSlots() - { - BlockedSlots.Clear(); - - var inventoryManager = InventoryManager.Instance(); - if (inventoryManager == null) return; - - var blockedContainer = inventoryManager->GetInventoryContainer(InventoryType.BlockedItems); - if (blockedContainer == null) return; - - for (int i = 0; i < blockedContainer->Size; i++) - { - ref var item = ref blockedContainer->Items[i]; - if (item.ItemId == 0) continue; - - BlockedSlots.Add((item.Container, item.Slot)); - } - } - - public static bool IsEligible(int page, int slot) - => EligibleSlots.Contains((page, slot)); - - public static bool IsSlotBlocked(InventoryType container, int slot) - => BlockedSlots.Contains((container, slot)); - - public static bool HasActiveContext - => _lastContextId != 0; - - public static InventoryMappedLocation GetVisualLocation(int page, int slot) - => VisualLocationMap.TryGetValue(new InventoryMappedLocation(page, slot), out var result) ? result : new InventoryMappedLocation(48 + page, slot); -} \ No newline at end of file diff --git a/AetherBags/Inventory/InventoryLocation.cs b/AetherBags/Inventory/InventoryLocation.cs index 640eec8..7dd5265 100644 --- a/AetherBags/Inventory/InventoryLocation.cs +++ b/AetherBags/Inventory/InventoryLocation.cs @@ -17,7 +17,7 @@ public readonly record struct InventoryLocation(InventoryType Container, ushort public readonly record struct InventoryMappedLocation(int Container, int Slot) { - public static readonly InventoryMappedLocation Invalid = new(0, 0); + public static readonly InventoryMappedLocation Invalid = new(-1, -1); public bool IsValid => Container != 0; diff --git a/AetherBags/Inventory/InventoryOrchestrator.cs b/AetherBags/Inventory/InventoryOrchestrator.cs new file mode 100644 index 0000000..36c3632 --- /dev/null +++ b/AetherBags/Inventory/InventoryOrchestrator.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using AetherBags.Addons; +using AetherBags.Inventory.Context; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; + +namespace AetherBags.Inventory; + +public static unsafe class InventoryOrchestrator +{ + private static readonly InventoryNotificationState NotificationState = new(); + + public static void RefreshAll(bool updateMaps = true) + { + if (updateMaps) + { + InventoryContextState.RefreshMaps(); + InventoryContextState.RefreshBlockedSlots(); + } + + var agent = AgentInventory.Instance(); + var contextId = agent != null ? agent->OpenTitleId : 0; + var notification = NotificationState.GetNotificationInfo(contextId); + + Services.Framework.RunOnTick(() => + { + if (System.AddonInventoryWindow.IsOpen) + System.AddonInventoryWindow.SetNotification(notification!); + + foreach (var window in GetAllWindows()) + { + if (window.IsOpen) + window.ManualRefresh(); + } + }); + } + + public static void CloseAll() + { + foreach (var window in GetAllWindows()) + { + window.Close(); + } + } + + public static void RefreshHighlights() + { + Services.Framework.RunOnTick(() => + { + foreach (var window in GetAllWindows()) + { + window.ItemRefresh(); + } + }); + } + + private static IEnumerable GetAllWindows() + { + yield return System.AddonInventoryWindow; + yield return System.AddonSaddleBagWindow; + yield return System.AddonRetainerWindow; + } +} \ No newline at end of file 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 70% rename from AetherBags/Inventory/ItemInfo.cs rename to AetherBags/Inventory/Items/ItemInfo.cs index 15e3a7d..b4a59fd 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 { @@ -57,15 +58,13 @@ public sealed class ItemInfo : IEquatable public bool IsHq => Item.Flags.HasFlag(InventoryItem.ItemFlags.HighQuality); public bool IsDesynthesizable => Row.Desynth > 0; - public bool IsCraftable => Row.ItemAction.RowId != 0 || Row.CanBeHq; // Simplified check + public bool IsCraftable => Row.ItemAction.RowId != 0 || Row.CanBeHq; public bool IsGlamourable => Row.IsGlamorous; public bool IsSpiritbonded => Item.SpiritbondOrCollectability >= 10000; // 100% = 10000 private string Description => _description ??= Row.Description.ToString(); - public InventoryMappedLocation VisualLocation => - IsMainInventory ? InventoryContextState.GetVisualLocation(InventoryPage, Item.Slot) - : new InventoryMappedLocation((int)Item.Container.AgentItemContainerId, Item. Slot); + public InventoryMappedLocation VisualLocation => InventoryContextState.GetVisualLocation(Item.Container, Item.Slot); public int InventoryPage => Item.Container switch @@ -83,13 +82,55 @@ public sealed class ItemInfo : IEquatable { get { - if (!InventoryContextState.HasActiveContext) - return true; + if (IsSlotBlocked) return false; + if (!CheckNativeContextEligibility()) return false; + if (!HighlightState.IsInActiveFilters(Item.ItemId)) return false; - return IsMainInventory && InventoryContextState.IsEligible(InventoryPage, Item.Slot); + return true; } } + public float VisualAlpha => IsEligibleForContext ? 1.0f : 0.4f; + + public Vector3 HighlightOverlayColor + { + get + { + if (!System.Config.Categories.BisBuddyEnabled) + return Vector3.Zero; + + return HighlightState.GetLabelColor(Item.ItemId) ?? Vector3.Zero; + } + } + + private bool CheckNativeContextEligibility() + { + uint contextId = InventoryContextState.ActiveContextId; + if (contextId == 0) return true; + + bool isRetainerContext = contextId == 4; + bool isSaddlebagContext = contextId == 29; + bool isMainContext = !isRetainerContext && isSaddlebagContext == false; + + if (IsMainInventory) + { + if (!isMainContext) return true; + return InventoryContextState.IsEligible(InventoryPage, Item.Slot); + } + + if (Item.Container.IsRetainer) + { + if (!isRetainerContext) return true; + } + + if (Item.Container.IsSaddleBag) + { + if (!isSaddlebagContext) return true; + } + + return true; + } + public bool IsMainInventory => InventoryPage >= 0; public bool IsRegexMatch(string searchTerms) 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 66% rename from AetherBags/Inventory/InventoryScanner.cs rename to AetherBags/Inventory/Scanning/InventoryScanner.cs index 00e9d2b..286d698 100644 --- a/AetherBags/Inventory/InventoryScanner.cs +++ b/AetherBags/Inventory/Scanning/InventoryScanner.cs @@ -1,19 +1,12 @@ -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 { - private static readonly InventoryType[] BagInventories = - [ - InventoryType.Inventory1, - InventoryType.Inventory2, - InventoryType.Inventory3, - InventoryType.Inventory4, - ]; - public static readonly InventoryType[] StandardInventories = [ InventoryType.Inventory1, @@ -46,20 +39,23 @@ public static unsafe class InventoryScanner public static ulong MakeNaturalSlotKey(InventoryType container, int slot) => ((ulong)(uint)container << 32) | (uint)slot; - public static void ScanBags( + public static void ScanInventories( InventoryManager* inventoryManager, InventoryStackMode stackMode, - Dictionary aggByKey) + 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 +160,58 @@ public static unsafe class InventoryScanner public static InventoryContainer* GetInventoryContainer(InventoryType inventoryType) => InventoryManager.Instance()->GetInventoryContainer(inventoryType); - public static string GetEmptyItemSlotsString() + public static InventoryLocation GetFirstEmptySlot(InventorySourceType source) { - uint empty = InventoryManager.Instance()->GetEmptySlotsInBag(); - uint used = 140 - empty; - return $"{used}/140"; - } -} + var manager = InventoryManager.Instance(); + var containers = InventorySourceDefinitions.GetContainersForSource(source); -public struct AggregatedItem -{ - public InventoryItem First; - public int Total; + foreach (var type in containers) + { + var container = manager->GetInventoryContainer(type); + if (container == null || container->Size == 0) continue; + + for (int i = 0; i < container->Size; i++) + { + if (container->Items[i].ItemId == 0) + return new InventoryLocation(type, (ushort)i); + } + } + + return InventoryLocation.Invalid; + } + + 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.PremiumSaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.PremiumSaddleBag), + InventorySourceType.AllSaddleBags => GetEmptySlotsInContainer(InventorySourceDefinitions.AllSaddleBags), + 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); + var containerSize = container->Size; + + if (container == null) continue; + for (int i = 0; i < containerSize; 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..8bdc8a5 --- /dev/null +++ b/AetherBags/Inventory/Scanning/InventorySource.cs @@ -0,0 +1,82 @@ +using FFXIVClientStructs.FFXIV.Client.Game; + +namespace AetherBags.Inventory.Scanning; + +public enum InventorySourceType +{ + MainBags, + SaddleBag, + PremiumSaddleBag, + AllSaddleBags, + 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, + ]; + + public static readonly InventoryType[] PremiumSaddleBag = + [ + InventoryType.PremiumSaddleBag1, + InventoryType.PremiumSaddleBag2, + ]; + + public static readonly InventoryType[] AllSaddleBags = + [ + 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 InventoryType[] GetContainersForSource(InventorySourceType source) => source switch + { + InventorySourceType.MainBags => MainBags, + InventorySourceType.SaddleBag => SaddleBag, + InventorySourceType.PremiumSaddleBag => PremiumSaddleBag, + InventorySourceType.AllSaddleBags => AllSaddleBags, + InventorySourceType.Retainer => Retainer, + _ => MainBags, + }; + + public static int GetTotalSlots(InventorySourceType source) => source switch + { + InventorySourceType.MainBags => 140, // 4 * 35 + InventorySourceType.SaddleBag => 70, // 2 * 35 + InventorySourceType.PremiumSaddleBag => 70, // 2 * 35 + InventorySourceType.AllSaddleBags => 140, // 2 * 35 + InventorySourceType.Retainer => Retainer.Length * 25, // 7 * 25 + _ => 140, + }; +} \ No newline at end of file diff --git a/AetherBags/Inventory/InventoryState.cs b/AetherBags/Inventory/State/InventoryState.cs similarity index 63% rename from AetherBags/Inventory/InventoryState.cs rename to AetherBags/Inventory/State/InventoryState.cs index 1d29106..17bdebc 100644 --- a/AetherBags/Inventory/InventoryState.cs +++ b/AetherBags/Inventory/State/InventoryState.cs @@ -1,16 +1,19 @@ +using System.Collections.Generic; +using System.Linq; using AetherBags.Configuration; using AetherBags.Currency; +using AetherBags.Inventory.Categories; +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 { - public static IReadOnlyList StandardInventories => InventoryScanner.StandardInventories; + private static IReadOnlyList StandardInventories => InventoryScanner.StandardInventories; private static readonly Dictionary AggByKey = new(capacity: 512); private static readonly Dictionary ItemInfoByKey = new(capacity: 512); @@ -28,68 +31,6 @@ public static unsafe class InventoryState public static bool Contains(this IReadOnlyCollection inventoryTypes, GameInventoryType type) => inventoryTypes.Contains((InventoryType)type); - public static void RefreshFromGame() - { - InventoryManager* inventoryManager = InventoryManager.Instance(); - if (inventoryManager == null) - { - ClearAll(); - return; - } - - var config = System.Config; - InventoryStackMode stackMode = config.General.StackMode; - bool userCategoriesEnabled = config.Categories.UserCategoriesEnabled; - bool gameCategoriesEnabled = config.Categories.GameCategoriesEnabled; - List userCategories = config.Categories.UserCategories.Where(category => category.Enabled).ToList(); - - Services.Logger.DebugOnly($"RefreshFromGame StackMode={stackMode}"); - - AggByKey.Clear(); - ItemInfoByKey.Clear(); - SortedCategoryKeys.Clear(); - AllCategories.Clear(); - FilteredCategories.Clear(); - ClaimedKeys.Clear(); - - InventoryScanner.ScanBags(inventoryManager, stackMode, AggByKey); - CategoryBucketManager.ResetBuckets(BucketsByKey); - InventoryScanner.BuildItemInfos(AggByKey, ItemInfoByKey); - InventoryContextState.RefreshMaps(); - InventoryContextState.RefreshBlockedSlots(); - - 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); - } - - InventoryScanner.PruneStaleItemInfos(AggByKey, ItemInfoByKey, RemoveKeysScratch); - CategoryBucketManager.SortBucketsAndBuildKeyList(BucketsByKey, SortedCategoryKeys); - CategoryBucketManager.BuildCategorizedList(BucketsByKey, SortedCategoryKeys, AllCategories); - } - public static IReadOnlyList GetInventoryItemCategories(string filterString = "", bool invert = false) { return InventoryFilter.FilterCategories( @@ -110,7 +51,7 @@ public static unsafe class InventoryState totalQuantity += kvp.Value.ItemCount; } - uint emptySlots = InventoryManager.Instance()->GetEmptySlotsInBag(); + uint emptySlots = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance()->GetEmptySlotsInBag(); const int totalSlots = 140; var categories = GetInventoryItemCategories(string.Empty); @@ -126,9 +67,6 @@ public static unsafe class InventoryState }; } - public static string GetEmptyItemSlotsString() - => InventoryScanner.GetEmptyItemSlotsString(); - public static IReadOnlyList GetCurrencyInfoList(uint[] currencyIds) => CurrencyState.GetCurrencyInfoList(currencyIds); diff --git a/AetherBags/Inventory/State/InventoryStateBase.cs b/AetherBags/Inventory/State/InventoryStateBase.cs new file mode 100644 index 0000000..1d8a1af --- /dev/null +++ b/AetherBags/Inventory/State/InventoryStateBase.cs @@ -0,0 +1,149 @@ +using System.Collections.Generic; +using System.Linq; +using AetherBags.Configuration; +using AetherBags.Inventory.Categories; +using AetherBags.Inventory.Context; +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 = 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 categoriesEnabled = config.Categories.CategoriesEnabled; + bool userCategoriesEnabled = config.Categories.UserCategoriesEnabled && categoriesEnabled; + bool gameCategoriesEnabled = config.Categories.GameCategoriesEnabled && categoriesEnabled; + bool allaganCategoriesEnabled = config.Categories.AllaganToolsCategoriesEnabled && categoriesEnabled; + // TODO: Cache this when config changes + var userCategories = config.Categories.UserCategories.Where(c => c.Enabled).ToList(); + + if (userCategoriesEnabled && userCategories.Count > 0) + { + CategoryBucketManager.BucketByUserCategories( + ItemInfoByKey, userCategories, BucketsByKey, ClaimedKeys, UserCategoriesSortedScratch); + } + + if (allaganCategoriesEnabled) + { + if (config.Categories.AllaganToolsMode == AllaganToolsFilterMode.Categorize) + { + CategoryBucketManager.BucketByAllaganFilters(ItemInfoByKey, BucketsByKey, ClaimedKeys, true); + HighlightState.ClearFilter(HighlightSource.AllaganTools); + } + else + { + UpdateAllaganHighlight(HighlightState.SelectedAllaganToolsFilterKey); + } + } + else + { + HighlightState.ClearFilter(HighlightSource.AllaganTools); + } + + if (gameCategoriesEnabled) + { + CategoryBucketManager.BucketByGameCategories( + ItemInfoByKey, BucketsByKey, ClaimedKeys, userCategoriesEnabled); + } + else + { + CategoryBucketManager.BucketUnclaimedToMisc( + ItemInfoByKey, BucketsByKey, ClaimedKeys, userCategoriesEnabled); + } + } + + private void UpdateAllaganHighlight(string? filterKey) + { + if (string.IsNullOrEmpty(filterKey) || !System.IPC.AllaganTools.IsReady) + { + HighlightState.ClearFilter(HighlightSource.AllaganTools); + return; + } + + var filterItems = System.IPC.AllaganTools.GetFilterItems(filterKey); + if (filterItems != null) + { + HighlightState.SetFilter(HighlightSource.AllaganTools, filterItems.Keys); + } + else + { + HighlightState.ClearFilter(HighlightSource.AllaganTools); + } + } + + 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/RetainerState.cs b/AetherBags/Inventory/State/RetainerState.cs new file mode 100644 index 0000000..eabe183 --- /dev/null +++ b/AetherBags/Inventory/State/RetainerState.cs @@ -0,0 +1,66 @@ +using AetherBags. Inventory.Scanning; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; + +namespace AetherBags. Inventory.State; + +public class RetainerState : InventoryStateBase +{ + public override InventorySourceType SourceType => InventorySourceType.Retainer; + public override InventoryType[] Inventories => InventorySourceDefinitions.Retainer; + + + public static unsafe ulong CurrentRetainerId + { + get + { + var retainerManager = RetainerManager.Instance(); + if (retainerManager == null) return 0; + + return retainerManager->LastSelectedRetainerId; + } + } + + public static unsafe string CurrentRetainerName + { + get + { + var retainerManager = RetainerManager.Instance(); + if (retainerManager == null) return string.Empty; + + var retainer = retainerManager->GetActiveRetainer(); + if (retainer == null) return string.Empty; + + return retainer->NameString; + } + } + + public static unsafe bool IsRetainerActive + { + get + { + if (! Services.ClientState.IsLoggedIn) return false; + + var retainerManager = RetainerManager. Instance(); + if (retainerManager == null) return false; + + return retainerManager->LastSelectedRetainerId != 0; + } + } + + public static unsafe bool AreContainersLoaded + { + get + { + if (!IsRetainerActive) return false; + + var inventoryManager = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance(); + if (inventoryManager == null) return false; + + var container = inventoryManager->GetInventoryContainer(InventoryType.RetainerPage1); + return container != null && container->Size > 0; + } + } + + public static bool CanMoveItems => AreContainersLoaded; +} \ 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..608d229 --- /dev/null +++ b/AetherBags/Inventory/State/SaddleBagState.cs @@ -0,0 +1,27 @@ +using AetherBags.Inventory.Scanning; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.UI; + +namespace AetherBags.Inventory.State; + +public class SaddleBagState : InventoryStateBase +{ + public override InventorySourceType SourceType => HasPremiumSaddlebag + ? InventorySourceType.AllSaddleBags + : InventorySourceType.SaddleBag; + + public override InventoryType[] Inventories => HasPremiumSaddlebag + ? InventorySourceDefinitions.AllSaddleBags + : InventorySourceDefinitions.SaddleBag; + + private static unsafe bool HasPremiumSaddlebag + { + get + { + if (!Services.ClientState.IsLoggedIn) return false; + + var playerState = PlayerState.Instance(); + return playerState != null && playerState->HasPremiumSaddlebag; + } + } +} \ No newline at end of file diff --git a/AetherBags/Nodes/Color/ColorInputRow.cs b/AetherBags/Nodes/Color/ColorInputRow.cs index e4e8535..1eb082f 100644 --- a/AetherBags/Nodes/Color/ColorInputRow.cs +++ b/AetherBags/Nodes/Color/ColorInputRow.cs @@ -58,8 +58,8 @@ public class ColorInputRow : HorizontalListNode if (_colorPickerAddon is not null) return; _colorPickerAddon = new ColorPickerAddon { - InternalName = "ColorPicker", - Title = "ColorPicker_AetherBags", + InternalName = "ColorPicker_AetherBags", + Title = "Pick a color", }; } @@ -94,4 +94,5 @@ public class ColorInputRow : HorizontalListNode public Action? OnColorConfirmed { get; set; } public Action? OnColorCanceled { get; set; } public Action? OnColorChange { get; set; } + public Action? OnColorPreviewed { get; set; } } \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/CategoryConfigurationNode.cs b/AetherBags/Nodes/Configuration/Category/CategoryConfigurationNode.cs index f2ddd6c..3296573 100644 --- a/AetherBags/Nodes/Configuration/Category/CategoryConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/Category/CategoryConfigurationNode.cs @@ -5,69 +5,54 @@ using KamiToolKit.Premade.Nodes; namespace AetherBags.Nodes.Configuration.Category; -public class CategoryConfigurationNode : ConfigNode +public class CategoryConfigurationNode : ConfigNode { - private readonly ScrollingAreaNode _categoryList; private CategoryDefinitionConfigurationNode? _activeNode; public Action? OnCategoryChanged { get; set; } public CategoryConfigurationNode() { - _categoryList = new ScrollingAreaNode - { - ContentHeight = 100.0f, - AutoHideScrollBar = true, - }; - _categoryList.ContentNode.FitContents = true; - _categoryList.AttachNode(this); } protected override void OptionChanged(CategoryWrapper? option) { if (option?.CategoryDefinition is null) { - _categoryList.IsVisible = false; + if (_activeNode is not null) + { + _activeNode.IsVisible = false; + } return; } - _categoryList.IsVisible = true; - if (_activeNode is null) { - _activeNode = new CategoryDefinitionConfigurationNode(option.CategoryDefinition) + _activeNode = new CategoryDefinitionConfigurationNode { - Size = _categoryList.ContentNode.Size, - OnLayoutChanged = UpdateScrollHeight, + OnLayoutChanged = RecalculateLayout, OnCategoryPropertyChanged = OnCategoryChanged, }; - _categoryList.ContentNode.AddNode(_activeNode); - } - else - { - _activeNode.SetCategory(option.CategoryDefinition); + _activeNode.AttachNode(this); } - UpdateScrollHeight(); + _activeNode.IsVisible = true; + _activeNode.Size = Size; + _activeNode.SetCategory(option.CategoryDefinition); } - private void UpdateScrollHeight() + private void RecalculateLayout() { - _categoryList.ContentNode.RecalculateLayout(); - _categoryList.ContentHeight = _categoryList.ContentNode.Height; + // Trigger parent layout update if needed } protected override void OnSizeChanged() { base.OnSizeChanged(); - _categoryList.Size = Size; - _categoryList.ContentNode.Width = Width; - foreach (var node in _categoryList.ContentNode.GetNodes()) + if (_activeNode is not null) { - node.Width = Width; + _activeNode.Size = Size; } - - UpdateScrollHeight(); } } \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs b/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs index 3b76482..50f1eb8 100644 --- a/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; 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; @@ -14,515 +16,500 @@ using Action = System.Action; namespace AetherBags.Nodes.Configuration.Category; -public sealed class CategoryDefinitionConfigurationNode : VerticalListNode +public sealed class CategoryDefinitionConfigurationNode : SimpleComponentNode { - private readonly CheckboxNode _enabledCheckbox; - private readonly CheckboxNode _pinnedCheckbox; - private readonly TextInputNode _nameInputNode; - private readonly TextInputNode _descriptionInputNode; - private readonly ColorInputRow _colorInputNode; - private readonly NumericInputNode _priorityInputNode; - private readonly NumericInputNode _orderInputNode; + private static ExcelSheet? ItemSheet => Services.DataManager.GetExcelSheet(); + private static ExcelSheet? UICategorySheet => Services.DataManager.GetExcelSheet(); - private readonly CheckboxNode _levelEnabledCheckbox; - private readonly NumericInputNode _levelMinNode; - private readonly NumericInputNode _levelMaxNode; + public Action? OnLayoutChanged { get; init; } + public Action? OnCategoryPropertyChanged { get; init; } - private readonly CheckboxNode _itemLevelEnabledCheckbox; - private readonly NumericInputNode _itemLevelMinNode; - private readonly NumericInputNode _itemLevelMaxNode; + private UserCategoryDefinition _categoryDefinition = new(); - private readonly CheckboxNode _vendorPriceEnabledCheckbox; - private readonly NumericInputNode _vendorPriceMinNode; - private readonly NumericInputNode _vendorPriceMaxNode; + private readonly ScrollingAreaNode _scrollingArea; + private readonly BasicSettingsSection _basicSettings; + private readonly RangeFiltersSection _rangeFilters; + private readonly StateFiltersSection _stateFilters; + private readonly ListFiltersSection _listFilters; - private readonly StateFilterRowNode _untradableFilter; - private readonly StateFilterRowNode _uniqueFilter; - private readonly StateFilterRowNode _collectableFilter; - private readonly StateFilterRowNode _dyeableFilter; - private readonly StateFilterRowNode _repairableFilter; - private readonly StateFilterRowNode _hqFilter; - private readonly StateFilterRowNode _desynthFilter; - private readonly StateFilterRowNode _glamourFilter; - private readonly StateFilterRowNode _spiritbondFilter; - - private readonly UintListEditorNode _allowedItemIdsEditor; - private readonly StringListEditorNode _allowedNamePatternsEditor; - private readonly UintListEditorNode _allowedUiCategoriesEditor; - private readonly RarityEditorNode _allowedRaritiesEditor; - - private bool _isInitialized; - - private static ExcelSheet? _sItemSheet; - private static ExcelSheet? _sUICategorySheet; - - public Action? OnLayoutChanged { get; set; } - - public Action? OnCategoryPropertyChanged { get; set; } - - private UserCategoryDefinition CategoryDefinition { get; set; } - - public CategoryDefinitionConfigurationNode(UserCategoryDefinition categoryDefinition) + public CategoryDefinitionConfigurationNode() { - CategoryDefinition = categoryDefinition; - - _sItemSheet ??= Services.DataManager.GetExcelSheet(); - _sUICategorySheet ??= Services.DataManager.GetExcelSheet(); - - FitContents = true; - ItemSpacing = 4.0f; - - var catchAllWarningNode = new TextNode + _scrollingArea = new ScrollingAreaNode { - Size = new Vector2(300, 40), - TextFlags = TextFlags.MultiLine | TextFlags.AutoAdjustNodeSize, - SeString = new SeStringBuilder().Append(" Warning: No rules configured\nThis category won't match anything!").ToReadOnlySeString(), - TextColor = ColorHelper.GetColor(17), - LineSpacing = 20, - IsVisible = UserCategoryMatcher.IsCatchAll(CategoryDefinition), + ContentHeight = 100.0f, + AutoHideScrollBar = true, }; - AddNode(catchAllWarningNode); + _scrollingArea.AttachNode(this); - AddNode(CreateSectionHeader("Basic Settings")); + _scrollingArea.ContentNode.OnLayoutUpdate = newHeight => + { + _scrollingArea.ContentHeight = newHeight; + }; + + _scrollingArea.ContentNode.CategoryVerticalSpacing = 4.0f; + + var treeListNode = _scrollingArea.ContentAreaNode; + + _basicSettings = new BasicSettingsSection(() => _categoryDefinition) + { + String = "Basic Settings", + IsCollapsed = false, + OnPropertyChanged = () => + { + NotifyChanged(); + NotifyCategoryPropertyChanged(); + }, + OnValueChanged = NotifyChanged, + }; + _basicSettings.OnToggle = _ => HandleLayoutChange(); + treeListNode.AddCategoryNode(_basicSettings); + + _rangeFilters = new RangeFiltersSection(() => _categoryDefinition) + { + String = "Range Filters", + IsCollapsed = true, + OnValueChanged = NotifyChanged, + }; + _rangeFilters.OnToggle = _ => HandleLayoutChange(); + treeListNode.AddCategoryNode(_rangeFilters); + + _stateFilters = new StateFiltersSection(() => _categoryDefinition) + { + String = "State Filters", + IsCollapsed = true, + OnValueChanged = NotifyChanged, + }; + _stateFilters.OnToggle = _ => HandleLayoutChange(); + treeListNode.AddCategoryNode(_stateFilters); + + _listFilters = new ListFiltersSection(() => _categoryDefinition) + { + String = "List Filters", + IsCollapsed = true, + OnValueChanged = NotifyChanged, + OnListChanged = HandleListChanged, + }; + _listFilters.OnToggle = _ => HandleLayoutChange(); + treeListNode.AddCategoryNode(_listFilters); + } + + protected override void OnSizeChanged() + { + base.OnSizeChanged(); + + _scrollingArea.Size = Size; + + foreach (var categoryNode in _scrollingArea.ContentNode.CategoryNodes) + { + categoryNode.Width = Width - 16.0f; + } + + _scrollingArea.ContentNode.RefreshLayout(); + } + + public void SetCategory(UserCategoryDefinition newCategory) + { + _categoryDefinition = newCategory; + RefreshAllValues(); + } + + private void RefreshAllValues() + { + _basicSettings.Refresh(); + _rangeFilters.Refresh(); + _stateFilters.Refresh(); + _listFilters.Refresh(); + + HandleLayoutChange(); + } + + private void HandleListChanged() + { + NotifyChanged(); + HandleLayoutChange(); + } + + private void HandleLayoutChange() + { + _scrollingArea.ContentNode.RefreshLayout(); + OnLayoutChanged?.Invoke(); + } + + private static void NotifyChanged() => InventoryOrchestrator.RefreshAll(updateMaps: true); + + private void NotifyCategoryPropertyChanged() => OnCategoryPropertyChanged?.Invoke(); + + public static string ResolveItemName(uint itemId) => ItemSheet?.GetRow(itemId).Name.ToString() ?? "Unknown"; + + public static string ResolveUiCategoryName(uint categoryId) => UICategorySheet?.GetRow(categoryId).Name.ToString() ?? "Unknown"; +} + +public abstract class ConfigurationSection : TreeListCategoryNode +{ + private readonly Func _getCategoryDefinition; + + public Action? OnValueChanged { get; init; } + + protected UserCategoryDefinition CategoryDefinition => _getCategoryDefinition(); + + protected ConfigurationSection(Func getCategoryDefinition) + { + _getCategoryDefinition = getCategoryDefinition; + VerticalPadding = 4.0f; + } + + protected static LabelTextNode CreateLabel(string text) => new() + { + TextFlags = TextFlags.AutoAdjustNodeSize, + Size = new Vector2(80, 20), + String = text, + }; +} + +public sealed class BasicSettingsSection : ConfigurationSection +{ + public Action? OnPropertyChanged { get; init; } + + private CheckboxNode? _enabledCheckbox; + private CheckboxNode? _pinnedCheckbox; + private TextInputNode? _nameInput; + private TextInputNode? _descriptionInput; + private ColorInputRow? _colorInput; + private NumericInputNode? _priorityInput; + private NumericInputNode? _orderInput; + + private bool _initialized; + + public BasicSettingsSection(Func getCategoryDefinition) + : base(getCategoryDefinition) + { + } + + private void EnsureInitialized() + { + if (_initialized) return; + _initialized = true; _enabledCheckbox = new CheckboxNode { - Size = new Vector2(200, 20), + Size = new Vector2(Width, 20), String = "Enabled", - IsChecked = CategoryDefinition.Enabled, OnClick = isChecked => { CategoryDefinition.Enabled = isChecked; - NotifyChanged(); - NotifyCategoryPropertyChanged(); + OnPropertyChanged?.Invoke(); }, }; AddNode(_enabledCheckbox); _pinnedCheckbox = new CheckboxNode { - Size = new Vector2(200, 20), + Size = new Vector2(Width, 20), String = "Pinned", - IsChecked = CategoryDefinition.Pinned, OnClick = isChecked => { CategoryDefinition.Pinned = isChecked; - NotifyChanged(); - NotifyCategoryPropertyChanged(); + OnPropertyChanged?.Invoke(); }, }; AddNode(_pinnedCheckbox); - AddNode(new LabelTextNode - { - TextFlags = TextFlags.AutoAdjustNodeSize, - Size = new Vector2(80, 20), - String = "Name:" - }); - _nameInputNode = new TextInputNode + AddNode(CreateLabel("Name: ")); + _nameInput = new TextInputNode { Size = new Vector2(250, 28), - String = CategoryDefinition.Name, - PlaceholderString = CategoryDefinition.Name.IsNullOrWhitespace() ? "Category Name" : "", - OnInputReceived = name => + PlaceholderString = "Category Name", + OnInputReceived = input => { - CategoryDefinition.Name = name.ExtractText(); - NotifyChanged(); - NotifyCategoryPropertyChanged(); + CategoryDefinition.Name = input.ExtractText(); + OnPropertyChanged?.Invoke(); }, }; - AddNode(_nameInputNode); + AddNode(_nameInput); - AddNode(new LabelTextNode - { - TextFlags = TextFlags.AutoAdjustNodeSize, - Size = new Vector2(80, 20), - String = "Description:" - }); - _descriptionInputNode = new TextInputNode + AddNode(CreateLabel("Description:")); + _descriptionInput = new TextInputNode { Size = new Vector2(250, 28), - String = CategoryDefinition.Description, - PlaceholderString = CategoryDefinition.Description.IsNullOrWhitespace() ? "Optional description" : "", - OnInputReceived = desc => + PlaceholderString = "Optional description", + OnInputReceived = input => { - CategoryDefinition.Description = desc.ExtractText(); - NotifyChanged(); + CategoryDefinition.Description = input.ExtractText(); + OnValueChanged?.Invoke(); }, }; - AddNode(_descriptionInputNode); + AddNode(_descriptionInput); - _colorInputNode = new ColorInputRow + _colorInput = new ColorInputRow { Label = "Color", Size = new Vector2(300, 28), - CurrentColor = CategoryDefinition.Color, + CurrentColor = new UserCategoryDefinition().Color, DefaultColor = new UserCategoryDefinition().Color, - OnColorConfirmed = color => - { - CategoryDefinition.Color = color; - NotifyChanged(); - }, - OnColorCanceled = color => - { - CategoryDefinition.Color = color; - NotifyChanged(); - }, + OnColorConfirmed = c => { CategoryDefinition.Color = c; OnValueChanged?.Invoke(); }, + OnColorCanceled = c => { CategoryDefinition.Color = c; OnValueChanged?.Invoke(); }, + OnColorPreviewed = c => { CategoryDefinition.Color = c; OnValueChanged?.Invoke(); }, }; - AddNode(_colorInputNode); + AddNode(_colorInput); - AddNode(new LabelTextNode - { - TextFlags = TextFlags.AutoAdjustNodeSize, - Size = new Vector2(80, 20), - String = "Priority:" - }); - _priorityInputNode = new NumericInputNode + AddNode(CreateLabel("Priority:")); + _priorityInput = new NumericInputNode { Size = new Vector2(120, 28), Min = 0, Max = 1000, Step = 1, - Value = CategoryDefinition.Priority, OnValueUpdate = val => { CategoryDefinition.Priority = val; - NotifyChanged(); + OnValueChanged?.Invoke(); }, }; - AddNode(_priorityInputNode); + AddNode(_priorityInput); - AddNode(new LabelTextNode { TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(80, 20), String = "Order:" }); - _orderInputNode = new NumericInputNode + AddNode(CreateLabel("Order: ")); + _orderInput = new NumericInputNode { Size = new Vector2(120, 28), Min = 0, Max = 9999, Step = 1, - Value = CategoryDefinition.Order, OnValueUpdate = val => { CategoryDefinition.Order = val; - NotifyChanged(); - NotifyCategoryPropertyChanged(); + OnPropertyChanged?.Invoke(); }, }; - AddNode(_orderInputNode); + AddNode(_orderInput); - AddNode(CreateSectionHeader("Range Filters")); + RecalculateLayout(); + } - (_levelEnabledCheckbox, _levelMinNode, _levelMaxNode) = CreateRangeFilter( - "Level", - CategoryDefinition.Rules.Level, - 0, 200, - (enabled, min, max) => + public void Refresh() + { + EnsureInitialized(); + + _enabledCheckbox!.IsChecked = CategoryDefinition.Enabled; + _pinnedCheckbox!.IsChecked = CategoryDefinition.Pinned; + _nameInput!.String = CategoryDefinition.Name; + _nameInput.PlaceholderString = CategoryDefinition.Name.IsNullOrWhitespace() ? "Category Name" : ""; + _descriptionInput!.String = CategoryDefinition.Description; + _descriptionInput.PlaceholderString = CategoryDefinition.Description.IsNullOrWhitespace() ? "Optional description" : ""; + _colorInput!.CurrentColor = CategoryDefinition.Color; + _priorityInput!.Value = CategoryDefinition.Priority; + _orderInput!.Value = CategoryDefinition.Order; + + RecalculateLayout(); + ParentTreeListNode?.RefreshLayout(); + } +} + +public sealed class RangeFiltersSection : ConfigurationSection +{ + private RangeFilterRow? _levelFilter; + private RangeFilterRow? _itemLevelFilter; + private RangeFilterRowUint? _vendorPriceFilter; + + private bool _initialized; + + public RangeFiltersSection(Func getCategoryDefinition) + : base(getCategoryDefinition) + { + } + + private void EnsureInitialized() + { + if (_initialized) return; + _initialized = true; + + _levelFilter = new RangeFilterRow + { + Label = "Level", + MinBound = 0, + MaxBound = 200, + OnFilterChanged = (enabled, min, max) => { CategoryDefinition.Rules.Level.Enabled = enabled; CategoryDefinition.Rules.Level.Min = min; CategoryDefinition.Rules.Level.Max = max; - NotifyChanged(); - } - ); + OnValueChanged?.Invoke(); + }, + }; + AddNode(_levelFilter); - (_itemLevelEnabledCheckbox, _itemLevelMinNode, _itemLevelMaxNode) = CreateRangeFilter( - "Item Level", - CategoryDefinition.Rules.ItemLevel, - 0, 2000, - (enabled, min, max) => + _itemLevelFilter = new RangeFilterRow + { + Label = "Item Level", + MinBound = 0, + MaxBound = 2000, + OnFilterChanged = (enabled, min, max) => { CategoryDefinition.Rules.ItemLevel.Enabled = enabled; CategoryDefinition.Rules.ItemLevel.Min = min; CategoryDefinition.Rules.ItemLevel.Max = max; - NotifyChanged(); - } - ); - - (_vendorPriceEnabledCheckbox, _vendorPriceMinNode, _vendorPriceMaxNode) = CreateRangeFilterUint( - "Vendor Price", - CategoryDefinition.Rules.VendorPrice, - 0, 9_999_999 - ); - - AddNode(CreateSectionHeader("State Filters")); - - _untradableFilter = new StateFilterRowNode("Untradable", CategoryDefinition.Rules.Untradable, NotifyChanged); - AddNode(_untradableFilter); - - _uniqueFilter = new StateFilterRowNode("Unique", CategoryDefinition.Rules.Unique, NotifyChanged); - AddNode(_uniqueFilter); - - _collectableFilter = new StateFilterRowNode("Collectable", CategoryDefinition.Rules.Collectable, NotifyChanged); - AddNode(_collectableFilter); - - _dyeableFilter = new StateFilterRowNode("Dyeable", CategoryDefinition.Rules.Dyeable, NotifyChanged); - AddNode(_dyeableFilter); - - _repairableFilter = new StateFilterRowNode("Repairable", CategoryDefinition.Rules.Repairable, NotifyChanged); - AddNode(_repairableFilter); - - _hqFilter = new StateFilterRowNode("High Quality", CategoryDefinition.Rules.HighQuality, NotifyChanged); - AddNode(_hqFilter); - - _desynthFilter = new StateFilterRowNode("Desynthesizable", CategoryDefinition.Rules.Desynthesizable, NotifyChanged); - AddNode(_desynthFilter); - - _glamourFilter = new StateFilterRowNode("Glamourable", CategoryDefinition.Rules.Glamourable, NotifyChanged); - AddNode(_glamourFilter); - - _spiritbondFilter = new StateFilterRowNode("Spiritbonded", CategoryDefinition.Rules.FullySpiritbonded, NotifyChanged); - AddNode(_spiritbondFilter); - - AddNode(CreateSectionHeader("List Filters")); - - _allowedItemIdsEditor = new UintListEditorNode( - "Allowed Item IDs:", - CategoryDefinition.Rules.AllowedItemIds, - OnListChanged, - ResolveItemName - ); - AddNode(_allowedItemIdsEditor); - - _allowedNamePatternsEditor = new StringListEditorNode( - "Name Patterns (Regex):", - CategoryDefinition.Rules.AllowedItemNamePatterns, - OnListChanged - ); - AddNode(_allowedNamePatternsEditor); - - _allowedUiCategoriesEditor = new UintListEditorNode( - "UI Categories:", - CategoryDefinition.Rules.AllowedUiCategoryIds, - OnListChanged, - ResolveUiCategoryName - ); - AddNode(_allowedUiCategoriesEditor); - - _allowedRaritiesEditor = new RarityEditorNode( - CategoryDefinition.Rules.AllowedRarities, - NotifyChanged - ); - AddNode(_allowedRaritiesEditor); - - _isInitialized = true; - } - - private void OnListChanged() - { - NotifyChanged(); - RecalculateLayout(); - OnLayoutChanged?.Invoke(); - } - - private static string ResolveItemName(uint itemId) - { - try - { - var item = _sItemSheet?.GetRow(itemId); - return item?.Name.ToString() ?? "Unknown"; - } - catch - { - return "Unknown"; - } - } - - private static string ResolveUiCategoryName(uint categoryId) - { - try - { - var category = _sUICategorySheet?.GetRow(categoryId); - return category?.Name.ToString() ?? "Unknown"; - } - catch - { - return "Unknown"; - } - } - - private static void NotifyChanged() - { - System.AddonInventoryWindow.ManualInventoryRefresh(); - } - - private void NotifyCategoryPropertyChanged() - { - OnCategoryPropertyChanged?.Invoke(); - } - - private static LabelTextNode CreateSectionHeader(string text) - { - return new LabelTextNode - { - Size = new Vector2(300, 22), - String = text, - TextColor = ColorHelper.GetColor(2), - TextOutlineColor = ColorHelper.GetColor(0), + OnValueChanged?.Invoke(); + }, }; - } + AddNode(_itemLevelFilter); - private (CheckboxNode enabled, NumericInputNode min, NumericInputNode max) CreateRangeFilter( - string label, - RangeFilter filter, - int minBound, - int maxBound, - Action onUpdate) - { - var enabledCheckbox = new CheckboxNode + _vendorPriceFilter = new RangeFilterRowUint { - Size = new Vector2(200, 20), - String = $"{label} Filter", - IsChecked = filter.Enabled, + Label = "Vendor Price", + MinBound = 0, + MaxBound = 9_999_999, + OnFilterChanged = (enabled, min, max) => + { + CategoryDefinition.Rules.VendorPrice.Enabled = enabled; + CategoryDefinition.Rules.VendorPrice.Min = min; + CategoryDefinition.Rules.VendorPrice.Max = max; + OnValueChanged?.Invoke(); + }, }; - AddNode(enabledCheckbox); - - var minNode = new NumericInputNode - { - Size = new Vector2(120, 28), - Min = minBound, - Max = maxBound, - Value = filter.Min, - IsEnabled = filter.Enabled, - }; - - var maxNode = new NumericInputNode - { - Size = new Vector2(120, 28), - Min = minBound, - Max = maxBound, - Value = filter.Max, - IsEnabled = filter.Enabled, - }; - - var rangeRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 8.0f }; - rangeRow.AddNode(new LabelTextNode { TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(30, 28), String = "Min:" }); - rangeRow.AddNode(minNode); - rangeRow.AddNode(new LabelTextNode { TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(30, 28), String = "Max:" }); - rangeRow.AddNode(maxNode); - AddNode(rangeRow); - - enabledCheckbox.OnClick = isChecked => - { - minNode.IsEnabled = isChecked; - maxNode.IsEnabled = isChecked; - onUpdate(isChecked, minNode.Value, maxNode.Value); - }; - - minNode.OnValueUpdate = val => onUpdate(enabledCheckbox.IsChecked, val, maxNode.Value); - maxNode.OnValueUpdate = val => onUpdate(enabledCheckbox.IsChecked, minNode.Value, val); - - return (enabledCheckbox, minNode, maxNode); - } - - private (CheckboxNode enabled, NumericInputNode min, NumericInputNode max) CreateRangeFilterUint( - string label, - RangeFilter filter, - int minBound, - int maxBound) - { - var enabledCheckbox = new CheckboxNode - { - Size = new Vector2(200, 20), - String = $"{label} Filter", - IsChecked = filter.Enabled, - }; - AddNode(enabledCheckbox); - - var minNode = new NumericInputNode - { - Size = new Vector2(120, 28), - Min = minBound, - Max = maxBound, - Value = (int)filter.Min, - IsEnabled = filter.Enabled, - }; - - var maxNode = new NumericInputNode - { - Size = new Vector2(120, 28), - Min = minBound, - Max = maxBound, - Value = (int)Math.Min(filter.Max, maxBound), - IsEnabled = filter.Enabled, - }; - - var rangeRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 8.0f }; - rangeRow.AddNode(new LabelTextNode { TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(30, 28), String = "Min:" }); - rangeRow.AddNode(minNode); - rangeRow.AddNode(new LabelTextNode { TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(30, 28), String = "Max:" }); - rangeRow.AddNode(maxNode); - AddNode(rangeRow); - - enabledCheckbox.OnClick = isChecked => - { - minNode.IsEnabled = isChecked; - maxNode.IsEnabled = isChecked; - CategoryDefinition.Rules.VendorPrice.Enabled = isChecked; - NotifyChanged(); - }; - - minNode.OnValueUpdate = value => - { - CategoryDefinition.Rules.VendorPrice.Min = (uint)value; - NotifyChanged(); - }; - - maxNode.OnValueUpdate = value => - { - CategoryDefinition.Rules.VendorPrice.Max = (uint)value; - NotifyChanged(); - }; - - return (enabledCheckbox, minNode, maxNode); - } - - public void SetCategory(UserCategoryDefinition newCategory) - { - CategoryDefinition = newCategory; - RefreshValues(); - } - - private void RefreshValues() - { - if (! _isInitialized) return; - - _enabledCheckbox.IsChecked = CategoryDefinition.Enabled; - _pinnedCheckbox.IsChecked = CategoryDefinition.Pinned; - _colorInputNode.CurrentColor = CategoryDefinition.Color; - _nameInputNode.String = CategoryDefinition.Name; - _descriptionInputNode.String = CategoryDefinition.Description; - _priorityInputNode.Value = CategoryDefinition.Priority; - _orderInputNode.Value = CategoryDefinition.Order; - - RefreshRangeFilter(_levelEnabledCheckbox, _levelMinNode, _levelMaxNode, CategoryDefinition.Rules.Level); - RefreshRangeFilter(_itemLevelEnabledCheckbox, _itemLevelMinNode, _itemLevelMaxNode, CategoryDefinition.Rules.ItemLevel); - - _vendorPriceEnabledCheckbox.IsChecked = CategoryDefinition.Rules.VendorPrice.Enabled; - _vendorPriceMinNode.Value = (int)CategoryDefinition.Rules.VendorPrice.Min; - _vendorPriceMaxNode.Value = (int)Math.Min(CategoryDefinition.Rules.VendorPrice.Max, int.MaxValue); - _vendorPriceMinNode.IsEnabled = CategoryDefinition.Rules.VendorPrice.Enabled; - _vendorPriceMaxNode.IsEnabled = CategoryDefinition.Rules.VendorPrice.Enabled; - - _untradableFilter.SetState(CategoryDefinition.Rules.Untradable); - _uniqueFilter.SetState(CategoryDefinition.Rules.Unique); - _collectableFilter.SetState(CategoryDefinition.Rules.Collectable); - _dyeableFilter.SetState(CategoryDefinition.Rules.Dyeable); - _repairableFilter.SetState(CategoryDefinition.Rules.Repairable); - - _allowedItemIdsEditor.SetList(CategoryDefinition.Rules.AllowedItemIds); - _allowedNamePatternsEditor.SetList(CategoryDefinition.Rules.AllowedItemNamePatterns); - _allowedUiCategoriesEditor.SetList(CategoryDefinition.Rules.AllowedUiCategoryIds); - _allowedRaritiesEditor.SetList(CategoryDefinition.Rules.AllowedRarities); + AddNode(_vendorPriceFilter); RecalculateLayout(); - OnLayoutChanged?.Invoke(); } - private static void RefreshRangeFilter(CheckboxNode enabled, NumericInputNode min, NumericInputNode max, RangeFilter filter) + public void Refresh() { - enabled.IsChecked = filter.Enabled; - min.Value = filter.Min; - max.Value = filter.Max; - min.IsEnabled = filter.Enabled; - max.IsEnabled = filter.Enabled; + EnsureInitialized(); + + _levelFilter!.SetFilter(CategoryDefinition.Rules.Level); + _itemLevelFilter!.SetFilter(CategoryDefinition.Rules.ItemLevel); + _vendorPriceFilter!.SetFilter(CategoryDefinition.Rules.VendorPrice); + + RecalculateLayout(); + ParentTreeListNode?.RefreshLayout(); + } +} + +public sealed class StateFiltersSection : ConfigurationSection +{ + private readonly List<(StateFilterRowNode Node, Func GetFilter)> _filters = []; + private bool _initialized; + + public StateFiltersSection(Func getCategoryDefinition) + : base(getCategoryDefinition) + { + } + + private void EnsureInitialized() + { + if (_initialized) return; + _initialized = true; + + AddFilter("Untradable", def => def.Rules.Untradable); + AddFilter("Unique", def => def.Rules.Unique); + AddFilter("Collectable", def => def.Rules.Collectable); + AddFilter("Dyeable", def => def.Rules.Dyeable); + AddFilter("Repairable", def => def.Rules.Repairable); + AddFilter("High Quality", def => def.Rules.HighQuality); + AddFilter("Desynthesizable", def => def.Rules.Desynthesizable); + AddFilter("Glamourable", def => def.Rules.Glamourable); + AddFilter("Spiritbonded", def => def.Rules.FullySpiritbonded); + + RecalculateLayout(); + } + + private void AddFilter(string label, Func getFilter) + { + var node = new StateFilterRowNode(label, new StateFilter(), () => OnValueChanged?.Invoke()); + _filters.Add((node, getFilter)); + AddNode(node); + } + + public void Refresh() + { + EnsureInitialized(); + + foreach (var (node, getFilter) in _filters) + { + node.SetState(getFilter(CategoryDefinition)); + } + + RecalculateLayout(); + ParentTreeListNode?.RefreshLayout(); + } +} + +public sealed class ListFiltersSection : ConfigurationSection +{ + public Action? OnListChanged { get; init; } + + private UintListEditorNode? _itemIdsEditor; + private StringListEditorNode? _namePatternsEditor; + private UintListEditorNode? _uiCategoriesEditor; + private RarityEditorNode? _raritiesEditor; + + private bool _initialized; + + public ListFiltersSection(Func getCategoryDefinition) + : base(getCategoryDefinition) + { + } + + private void EnsureInitialized() + { + if (_initialized) return; + _initialized = true; + + _itemIdsEditor = new UintListEditorNode + { + Label = "Allowed Item IDs:", + LabelResolver = CategoryDefinitionConfigurationNode.ResolveItemName, + OnChanged = () => + { + OnListChanged?.Invoke(); + RecalculateLayout(); + ParentTreeListNode?.RefreshLayout(); + }, + }; + AddNode(_itemIdsEditor); + + _namePatternsEditor = new StringListEditorNode + { + Label = "Name Patterns (Regex):", + OnChanged = () => + { + OnListChanged?.Invoke(); + RecalculateLayout(); + ParentTreeListNode?.RefreshLayout(); + }, + }; + AddNode(_namePatternsEditor); + + _uiCategoriesEditor = new UintListEditorNode + { + Label = "UI Categories:", + LabelResolver = CategoryDefinitionConfigurationNode.ResolveUiCategoryName, + OnChanged = () => + { + OnListChanged?.Invoke(); + RecalculateLayout(); + ParentTreeListNode?.RefreshLayout(); + }, + }; + AddNode(_uiCategoriesEditor); + + _raritiesEditor = new RarityEditorNode + { + OnChanged = () => OnValueChanged?.Invoke(), + }; + AddNode(_raritiesEditor); + + RecalculateLayout(); + } + + public void Refresh() + { + EnsureInitialized(); + + _itemIdsEditor!.SetList(CategoryDefinition.Rules.AllowedItemIds); + _namePatternsEditor!.SetList(CategoryDefinition.Rules.AllowedItemNamePatterns); + _uiCategoriesEditor!.SetList(CategoryDefinition.Rules.AllowedUiCategoryIds); + _raritiesEditor!.SetList(CategoryDefinition.Rules.AllowedRarities); + + RecalculateLayout(); + ParentTreeListNode?.RefreshLayout(); } } \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/CategoryGeneralConfigurationNode.cs b/AetherBags/Nodes/Configuration/Category/CategoryGeneralConfigurationNode.cs new file mode 100644 index 0000000..4752f5f --- /dev/null +++ b/AetherBags/Nodes/Configuration/Category/CategoryGeneralConfigurationNode.cs @@ -0,0 +1,148 @@ +using System; +using System.Linq; +using System.Numerics; +using AetherBags.Configuration; +using AetherBags.Inventory; +using AetherBags.Inventory.Context; +using AetherBags.Nodes.Color; +using AetherBags.Nodes.Input; +using FFXIVClientStructs.FFXIV.Component.GUI; +using KamiToolKit.Classes; +using KamiToolKit.Nodes; + +namespace AetherBags.Nodes.Configuration.Category; + +public sealed class CategoryGeneralConfigurationNode : TabbedVerticalListNode +{ + private readonly CheckboxNode _allaganToolsCheckbox; + public CategoryGeneralConfigurationNode() + { + CategorySettings config = System.Config.Categories; + + ItemVerticalSpacing = 2; + + LabelTextNode titleNode = new LabelTextNode + { + Size = Size with { Y = 18 }, + String = "Category Configuration", + TextColor = ColorHelper.GetColor(2), + TextOutlineColor = ColorHelper.GetColor(0), + }; + AddNode(titleNode); + + AddTab(1); + + CheckboxNode categoriesEnabled = new CheckboxNode + { + Size = Size with { Y = 18 }, + IsVisible = true, + String = "Categories Enabled", + IsChecked = config.CategoriesEnabled, + OnClick = isChecked => + { + config.CategoriesEnabled = isChecked; + RefreshInventory(); + } + }; + AddNode(categoriesEnabled); + + AddTab(1); + + CheckboxNode gameCategoriesEnabled = new CheckboxNode + { + Size = Size with { Y = 18 }, + IsVisible = true, + String = "Game Categories", + IsChecked = config.GameCategoriesEnabled, + TextTooltip = "Use the game's built-in item categories (e.g., Arms, Tools, Armor).", + OnClick = isChecked => + { + config.GameCategoriesEnabled = isChecked; + RefreshInventory(); + } + }; + AddNode(gameCategoriesEnabled); + + CheckboxNode userCategoriesEnabled = new CheckboxNode + { + Size = Size with { Y = 18 }, + IsVisible = true, + String = "User Categories", + IsChecked = config.UserCategoriesEnabled, + TextTooltip = "Use your custom-defined categories.", + OnClick = isChecked => + { + config.UserCategoriesEnabled = isChecked; + RefreshInventory(); + } + }; + AddNode(userCategoriesEnabled); + + bool bisBuddyReady = System.IPC.BisBuddy?.IsReady ?? false; + + CheckboxNode bisBuddyEnabled = new CheckboxNode + { + Size = Size with { Y = 18 }, + IsVisible = true, + String = bisBuddyReady ? "BISBuddy" : "BISBuddy (Not Available)", + IsChecked = config.BisBuddyEnabled, + TextTooltip = "Allow BISBuddy to highlight items.", + OnClick = isChecked => + { + config.BisBuddyEnabled = isChecked; + System.IPC.BisBuddy?.RequestUpdate(); + RefreshInventory(); + } + }; + AddNode(bisBuddyEnabled); + + bool allaganReady = System.IPC.AllaganTools?.IsReady ?? false; + + LabeledDropdownNode? atModeDropdown = new LabeledDropdownNode + { + Size = new Vector2(300, 20), + LabelText = "Filter Display Mode", + LabelTextFlags = TextFlags.AutoAdjustNodeSize, + IsEnabled = config.AllaganToolsCategoriesEnabled && allaganReady, + Options = Enum.GetNames(typeof(AllaganToolsFilterMode)).ToList(), + SelectedOption = config.AllaganToolsMode.ToString(), + OnOptionSelected = selected => + { + if (Enum.TryParse(selected, out var parsed)) + { + config.AllaganToolsMode = parsed; + if (parsed == AllaganToolsFilterMode.Categorize) + HighlightState.ClearFilter(HighlightSource.AllaganTools); + + RefreshInventory(); + } + } + }; + + _allaganToolsCheckbox = new CheckboxNode + { + Size = Size with { Y = 18 }, + IsVisible = true, + String = allaganReady ? "Allagan Tools Filters" : "Allagan Tools Filters (Not Available)", + IsChecked = config.AllaganToolsCategoriesEnabled, + IsEnabled = allaganReady, + TextTooltip = allaganReady + ? "Use search filters from Allagan Tools as categories. Items matching a filter will be grouped together." + : "Allagan Tools is not installed or not initialized.", + OnClick = isChecked => + { + config.AllaganToolsCategoriesEnabled = isChecked; + if (atModeDropdown != null) atModeDropdown.IsEnabled = isChecked; + if (isChecked) System.IPC?.AllaganTools?.RefreshFilters(); + RefreshInventory(); + } + }; + AddNode(_allaganToolsCheckbox); + + AddTab(1); + AddNode(atModeDropdown); + SubtractTab(1); + } + + private void RefreshInventory() => InventoryOrchestrator.RefreshAll(updateMaps: true); +} \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/CategoryScrollingAreaNode.cs b/AetherBags/Nodes/Configuration/Category/CategoryScrollingAreaNode.cs index 7d489c9..d6b0ef5 100644 --- a/AetherBags/Nodes/Configuration/Category/CategoryScrollingAreaNode.cs +++ b/AetherBags/Nodes/Configuration/Category/CategoryScrollingAreaNode.cs @@ -4,7 +4,7 @@ using KamiToolKit.Nodes; namespace AetherBags.Nodes.Configuration.Category; -public class CategoryScrollingAreaNode : ScrollingAreaNode +public sealed class CategoryScrollingAreaNode : ScrollingListNode { private AddonCategoryConfigurationWindow? _categoryConfigurationAddon; private readonly TextButtonNode _categoryConfigurationButtonNode; @@ -13,13 +13,15 @@ public class CategoryScrollingAreaNode : ScrollingAreaNode { InitializeCategoryAddon(); + AddNode(new CategoryGeneralConfigurationNode()); + _categoryConfigurationButtonNode = new TextButtonNode { Size = new Vector2(300, 28), String = "Configure Categories", OnClick = () => _categoryConfigurationAddon?.Toggle(), }; - _categoryConfigurationButtonNode.AttachNode(this); + AddNode(_categoryConfigurationButtonNode); } private void InitializeCategoryAddon() { diff --git a/AetherBags/Nodes/Configuration/Category/RangeFilterRow.cs b/AetherBags/Nodes/Configuration/Category/RangeFilterRow.cs new file mode 100644 index 0000000..9304a42 --- /dev/null +++ b/AetherBags/Nodes/Configuration/Category/RangeFilterRow.cs @@ -0,0 +1,206 @@ +using System; +using System.Numerics; +using AetherBags.Configuration; +using FFXIVClientStructs.FFXIV.Component.GUI; +using KamiToolKit.Nodes; + +namespace AetherBags.Nodes.Configuration.Category; + +public sealed class RangeFilterRow : VerticalListNode +{ + private readonly CheckboxNode _enabledCheckbox; + private readonly NumericInputNode _minNode; + private readonly NumericInputNode _maxNode; + + public Action? OnFilterChanged { get; set; } + + public required string Label + { + get => _enabledCheckbox.String.Replace(" Filter", ""); + init => _enabledCheckbox.String = $"{value} Filter"; + } + + public int MinBound + { + get => _minNode.Min; + init + { + _minNode.Min = value; + _maxNode.Min = value; + } + } + + public int MaxBound + { + get => _minNode.Max; + init + { + _minNode.Max = value; + _maxNode.Max = value; + } + } + + public RangeFilterRow() + { + FitContents = true; + ItemSpacing = 2.0f; + + _enabledCheckbox = new CheckboxNode + { + Size = new Vector2(200, 20), + OnClick = isChecked => + { + if (_minNode == null || _maxNode == null) return; + _minNode.IsEnabled = isChecked; + _maxNode.IsEnabled = isChecked; + OnFilterChanged?.Invoke(isChecked, _minNode.Value, _maxNode.Value); + }, + }; + AddNode(_enabledCheckbox); + + var rangeRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 8.0f }; + + rangeRow.AddNode(new LabelTextNode + { + TextFlags = TextFlags.AutoAdjustNodeSize, + Size = new Vector2(30, 28), + String = "Min:", + }); + + _minNode = new NumericInputNode + { + Size = new Vector2(100, 28), + OnValueUpdate = val => + { + if (_maxNode != null) OnFilterChanged?.Invoke(_enabledCheckbox.IsChecked, val, _maxNode.Value); + }, + }; + rangeRow.AddNode(_minNode); + + rangeRow.AddNode(new LabelTextNode + { + TextFlags = TextFlags.AutoAdjustNodeSize, + Size = new Vector2(30, 28), + String = "Max:", + }); + + _maxNode = new NumericInputNode + { + Size = new Vector2(100, 28), + OnValueUpdate = val => OnFilterChanged?.Invoke(_enabledCheckbox.IsChecked, _minNode.Value, val), + }; + rangeRow.AddNode(_maxNode); + + AddNode(rangeRow); + } + + public void SetFilter(RangeFilter filter) + { + _enabledCheckbox.IsChecked = filter.Enabled; + _minNode.Value = filter.Min; + _maxNode.Value = filter.Max; + _minNode.IsEnabled = filter.Enabled; + _maxNode.IsEnabled = filter.Enabled; + } +} + +public sealed class RangeFilterRowUint : VerticalListNode +{ + private readonly CheckboxNode _enabledCheckbox; + private readonly NumericInputNode _minNode; + private readonly NumericInputNode _maxNode; + private int _maxBound = int.MaxValue; + + public Action? OnFilterChanged { get; set; } + + public required string Label + { + get => _enabledCheckbox.String.Replace(" Filter", ""); + init => _enabledCheckbox.String = $"{value} Filter"; + } + + public int MinBound + { + get => _minNode.Min; + init + { + _minNode.Min = value; + _maxNode.Min = value; + } + } + + public int MaxBound + { + get => _maxBound; + init + { + _maxBound = value; + _minNode.Max = value; + _maxNode.Max = value; + } + } + + public RangeFilterRowUint() + { + FitContents = true; + ItemSpacing = 2.0f; + + _enabledCheckbox = new CheckboxNode + { + Size = new Vector2(200, 20), + OnClick = isChecked => + { + if (_minNode == null || _maxNode == null) return; + _minNode.IsEnabled = isChecked; + _maxNode.IsEnabled = isChecked; + OnFilterChanged?.Invoke(isChecked, (uint)_minNode.Value, (uint)_maxNode.Value); + }, + }; + AddNode(_enabledCheckbox); + + var rangeRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 8.0f }; + + rangeRow.AddNode(new LabelTextNode + { + TextFlags = TextFlags.AutoAdjustNodeSize, + Size = new Vector2(30, 28), + String = "Min:", + }); + + _minNode = new NumericInputNode + { + Size = new Vector2(100, 28), + OnValueUpdate = val => + { + if (_maxNode != null) + OnFilterChanged?.Invoke(_enabledCheckbox.IsChecked, (uint)val, (uint)_maxNode.Value); + }, + }; + rangeRow.AddNode(_minNode); + + rangeRow.AddNode(new LabelTextNode + { + TextFlags = TextFlags.AutoAdjustNodeSize, + Size = new Vector2(30, 28), + String = "Max:", + }); + + _maxNode = new NumericInputNode + { + Size = new Vector2(100, 28), + OnValueUpdate = val => OnFilterChanged?.Invoke(_enabledCheckbox.IsChecked, (uint)_minNode.Value, (uint)val), + }; + rangeRow.AddNode(_maxNode); + + AddNode(rangeRow); + } + + public void SetFilter(RangeFilter filter) + { + _enabledCheckbox.IsChecked = filter.Enabled; + _minNode.Value = (int)filter.Min; + _maxNode.Value = (int)Math.Min(filter.Max, _maxBound); + _minNode.IsEnabled = filter.Enabled; + _maxNode.IsEnabled = filter.Enabled; + } +} \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/RarityEditorNode.cs b/AetherBags/Nodes/Configuration/Category/RarityEditorNode.cs index c100034..1dc10b4 100644 --- a/AetherBags/Nodes/Configuration/Category/RarityEditorNode.cs +++ b/AetherBags/Nodes/Configuration/Category/RarityEditorNode.cs @@ -1,25 +1,33 @@ -using FFXIVClientStructs.FFXIV.Component.GUI; -using KamiToolKit.Classes; -using KamiToolKit.Nodes; using System; using System.Collections.Generic; using System.Numerics; +using FFXIVClientStructs.FFXIV.Component.GUI; +using KamiToolKit.Classes; +using KamiToolKit.Nodes; namespace AetherBags.Nodes.Configuration.Category; -public sealed class RarityEditorNode : VerticalListNode +public sealed class RarityEditorNode :VerticalListNode { - private static readonly string[] RarityNames = { "Common (White)", "Uncommon (Green)", "Rare (Blue)", "Relic (Purple)", "Aetherial (Pink)" }; + private const float LabelWidth = 120f; + private const float CheckboxWidth = 150f; - private List _list; - private readonly List _checkboxes = new(); - private readonly Action? _onChanged; + private static readonly string[] RarityNames = + [ + "Common (White)", + "Uncommon (Green)", + "Rare (Blue)", + "Relic (Purple)", + "Aetherial (Pink)" + ]; - public RarityEditorNode(List list, Action? onChanged = null) + public Action? OnChanged { get; set; } + + private List _list = []; + private readonly List _checkboxes = []; + + public RarityEditorNode() { - _list = list; - _onChanged = onChanged; - FitContents = true; ItemSpacing = 2.0f; @@ -32,33 +40,35 @@ public sealed class RarityEditorNode : VerticalListNode }; AddNode(headerLabel); - for (int i = 0; i < RarityNames.Length; i++) + for (var i = 0; i < RarityNames.Length; i++) { var rarity = i; var checkbox = new CheckboxNode { - Size = new Vector2(200, 20), + Size = new Vector2(LabelWidth + CheckboxWidth, 22), String = RarityNames[i], - IsChecked = _list.Contains(i), - OnClick = isChecked => - { - if (isChecked && !_list.Contains(rarity)) - { - _list.Add(rarity); - _list.Sort(); - } - else if (!isChecked && _list.Contains(rarity)) - { - _list.Remove(rarity); - } - _onChanged?.Invoke(); - }, + OnClick = isChecked => ToggleRarity(rarity, isChecked), }; _checkboxes.Add(checkbox); AddNode(checkbox); } } + private void ToggleRarity(int rarity, bool isChecked) + { + if (isChecked && !_list.Contains(rarity)) + { + _list.Add(rarity); + _list.Sort(); + } + else if (!isChecked && _list.Contains(rarity)) + { + _list.Remove(rarity); + } + + OnChanged?.Invoke(); + } + public void SetList(List newList) { _list = newList; @@ -67,7 +77,7 @@ public sealed class RarityEditorNode : VerticalListNode public void Refresh() { - for (int i = 0; i < _checkboxes.Count; i++) + for (var i = 0; i < _checkboxes.Count; i++) { _checkboxes[i].IsChecked = _list.Contains(i); } diff --git a/AetherBags/Nodes/Configuration/Category/StateFilterRowNode.cs b/AetherBags/Nodes/Configuration/Category/StateFilterRowNode.cs index d11d251..707a708 100644 --- a/AetherBags/Nodes/Configuration/Category/StateFilterRowNode.cs +++ b/AetherBags/Nodes/Configuration/Category/StateFilterRowNode.cs @@ -2,6 +2,7 @@ using AetherBags.Configuration; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Classes; using KamiToolKit.Nodes; +using KamiToolKit.Premade.Nodes; using System; using System.Numerics; @@ -9,48 +10,54 @@ namespace AetherBags.Nodes.Configuration.Category; public sealed class StateFilterRowNode : HorizontalListNode { - private readonly LabelTextNode _labelNode; - private readonly TextButtonNode _stateButton; + private const float LabelWidth = 120f; + private const float ButtonWidth = 100f; + + private readonly StateFilterButton _stateButton; private readonly Action? _onChanged; private StateFilter _filter; - private static readonly string[] StateLabels = { "Ignored", "Allow", "Disallow" }; - - public StateFilterRowNode(string label, StateFilter filter, Action? onChanged = null) + public StateFilterRowNode(string label, StateFilter filter, Action?onChanged = null) { _filter = filter; _onChanged = onChanged; - Size = new Vector2(280, 24); + Size = new Vector2(LabelWidth + ButtonWidth + 8f, 24); ItemSpacing = 8.0f; - _labelNode = new LabelTextNode + var labelNode = new LabelTextNode { - TextFlags = TextFlags.AutoAdjustNodeSize, - Size = new Vector2(100, 24), + Size = new Vector2(LabelWidth, 24), String = $"{label}:", TextColor = ColorHelper.GetColor(8), + AlignmentType = AlignmentType.Right, }; - AddNode(_labelNode); + AddNode(labelNode); - _stateButton = new TextButtonNode + _stateButton = new StateFilterButton { - Size = new Vector2(100, 24), - String = StateLabels[_filter.State], - OnClick = CycleState, + Size = new Vector2(ButtonWidth, 24), + States = [0, 1, 2], + SelectedState = _filter.State, + OnStateChanged = newState => + { + _filter.State = newState; + _onChanged?.Invoke(); + } }; AddNode(_stateButton); } - private void CycleState() - { - _filter.State = (_filter.State + 1) % 3; - _stateButton.String = StateLabels[_filter.State]; - _onChanged?.Invoke(); - } - public void SetState(StateFilter newFilter) { _filter = newFilter; - _stateButton.String = StateLabels[_filter.State]; + _stateButton.SelectedState = _filter.State; + } + + private sealed class StateFilterButton : MultiStateButtonNode + { + private static readonly string[] StateLabels = ["Ignored", "Required", "Excluded"]; + + protected override string GetStateText(int state) + => state >= 0 && state < StateLabels.Length ?StateLabels[state] : "Unknown"; } } \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/StringListEditorNode.cs b/AetherBags/Nodes/Configuration/Category/StringListEditorNode.cs index cc13f98..5cbcd65 100644 --- a/AetherBags/Nodes/Configuration/Category/StringListEditorNode.cs +++ b/AetherBags/Nodes/Configuration/Category/StringListEditorNode.cs @@ -1,84 +1,77 @@ -using FFXIVClientStructs.FFXIV.Component.GUI; -using KamiToolKit.Classes; -using KamiToolKit.Nodes; using System; using System.Collections.Generic; using System.Numerics; +using FFXIVClientStructs.FFXIV.Component.GUI; +using KamiToolKit.Classes; +using KamiToolKit.Nodes; namespace AetherBags.Nodes.Configuration.Category; public sealed class StringListEditorNode : VerticalListNode { - private List _list; - private readonly TextInputNode _addInput; + private const float LabelWidth = 300f; + private const float RowHeight = 28f; + + private List _list = []; + + private readonly LabelTextNode _headerLabel; private readonly VerticalListNode _itemsContainer; - private readonly Action? _onChanged; + private readonly HorizontalListNode _addRow; + private readonly TextInputNode _addInput; - public StringListEditorNode(string label, List list, Action? onChanged = null) + public Action? OnChanged { get; set; } + + public required string Label { - _list = list; - _onChanged = onChanged; + get => _headerLabel.String; + init => _headerLabel.String = value; + } + public StringListEditorNode() + { FitContents = true; ItemSpacing = 4.0f; - var headerLabel = new LabelTextNode + _headerLabel = new LabelTextNode { TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(280, 18), - String = label, TextColor = ColorHelper.GetColor(8), }; - AddNode(headerLabel); + AddNode(_headerLabel); _itemsContainer = new VerticalListNode { - FitContents = true, + Size = new Vector2(LabelWidth + 40f, 0), ItemSpacing = 2.0f, + FitContents = true, + FirstItemSpacing = 2, }; AddNode(_itemsContainer); - var addRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 4.0f }; + _addRow = new HorizontalListNode + { + Size = new Vector2(LabelWidth + 40f, RowHeight), + ItemSpacing = 4.0f, + }; _addInput = new TextInputNode { - Size = new Vector2(200, 28), + Size = new Vector2(200, RowHeight), PlaceholderString = "Add new...", - OnInputComplete = text => - { - var value = text.ExtractText(); - if (!string.IsNullOrWhiteSpace(value) && ! _list.Contains(value)) - { - _list.Add(value); - _addInput?.String = ""; - RefreshItems(); - _onChanged?.Invoke(); - } - }, + OnInputComplete = _ => AddCurrentValue(), }; - addRow.AddNode(_addInput); + _addRow.AddNode(_addInput); var addButton = new TextButtonNode { - Size = new Vector2(60, 28), + Size = new Vector2(60, RowHeight), String = "Add", - OnClick = () => - { - var value = _addInput.String; - if (!string.IsNullOrWhiteSpace(value) && !_list.Contains(value)) - { - _list.Add(value); - _addInput.String = ""; - RefreshItems(); - _onChanged?.Invoke(); - } - }, + OnClick = AddCurrentValue, }; - addRow.AddNode(addButton); + _addRow.AddNode(addButton); - AddNode(addRow); - - RefreshItems(); + AddNode(_addRow); } public void SetList(List newList) @@ -87,35 +80,54 @@ public sealed class StringListEditorNode : VerticalListNode RefreshItems(); } + private void AddCurrentValue() + { + var value = _addInput.String; + if (!string.IsNullOrWhiteSpace(value) && !_list.Contains(value)) + { + _list.Add(value); + _addInput.String = ""; + RefreshItems(); + OnChanged?.Invoke(); + } + } + private void RefreshItems() { - _itemsContainer.SyncWithListData( - _list, - node => node.Value, - value => new StringListItemNode(value) - { - Size = new Vector2(280, 22), - OnRemove = () => - { - _list.Remove(value); - RefreshItems(); - _onChanged?.Invoke(); - }, - } - ); + _itemsContainer.Clear(); + + foreach (var value in _list) + { + _itemsContainer.AddNode(CreateItemNode(value)); + } + + if (_list.Count == 0) + { + _itemsContainer.Height = 0; + } _itemsContainer.RecalculateLayout(); RecalculateLayout(); } - public void Refresh() + private StringListItemNode CreateItemNode(string value) => new(value) { + Size = new Vector2(LabelWidth + 40f, RowHeight), + OnRemove = () => RemoveValue(value), + }; + + private void RemoveValue(string value) + { + _list.Remove(value); RefreshItems(); + OnChanged?.Invoke(); } } public sealed class StringListItemNode : HorizontalListNode { + private const float LabelWidth = 300f; + public string Value { get; } public Action? OnRemove { get; init; } @@ -124,20 +136,18 @@ public sealed class StringListItemNode : HorizontalListNode Value = value; ItemSpacing = 4.0f; - var itemLabel = new LabelTextNode + AddNode(new LabelTextNode { - Size = new Vector2(220, 22), + Size = new Vector2(LabelWidth, 24), String = value, TextColor = ColorHelper.GetColor(3), - }; - AddNode(itemLabel); + }); - var removeButton = new TextButtonNode + AddNode(new CircleButtonNode { - Size = new Vector2(50, 22), - String = "X", + Size = new Vector2(28, 28), + Icon = ButtonIcon.Cross, OnClick = () => OnRemove?.Invoke(), - }; - AddNode(removeButton); + }); } } \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Category/UintListEditorNode.cs b/AetherBags/Nodes/Configuration/Category/UintListEditorNode.cs index 93dbef1..458ab75 100644 --- a/AetherBags/Nodes/Configuration/Category/UintListEditorNode.cs +++ b/AetherBags/Nodes/Configuration/Category/UintListEditorNode.cs @@ -1,76 +1,79 @@ -using FFXIVClientStructs.FFXIV.Component.GUI; -using KamiToolKit.Classes; -using KamiToolKit.Nodes; using System; using System.Collections.Generic; using System.Numerics; +using FFXIVClientStructs.FFXIV.Component.GUI; +using KamiToolKit.Classes; +using KamiToolKit.Nodes; namespace AetherBags.Nodes.Configuration.Category; public sealed class UintListEditorNode : VerticalListNode { - private List _list; - private readonly NumericInputNode _addInput; + private const float LabelWidth = 300f; + private const float RowHeight = 28f; + + private List _list = []; + + private readonly LabelTextNode _headerLabel; private readonly VerticalListNode _itemsContainer; - private readonly Action? _onChanged; - private readonly Func? _labelResolver; + private readonly HorizontalListNode _addRow; + private readonly NumericInputNode _addInput; - public UintListEditorNode(string label, List list, Action? onChanged = null, Func? labelResolver = null) + public Func? LabelResolver { get; init; } + public Action? OnChanged { get; set; } + + public required string Label { - _list = list; - _onChanged = onChanged; - _labelResolver = labelResolver; + get => _headerLabel.String; + init => _headerLabel.String = value; + } + public UintListEditorNode() + { FitContents = true; ItemSpacing = 4.0f; - var headerLabel = new LabelTextNode + _headerLabel = new LabelTextNode { TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(280, 18), - String = label, TextColor = ColorHelper.GetColor(8), }; - AddNode(headerLabel); + AddNode(_headerLabel); _itemsContainer = new VerticalListNode { - FitContents = true, + Size = new Vector2(LabelWidth + 40f, 0), ItemSpacing = 2.0f, + FitContents = true, + FirstItemSpacing = 2, }; AddNode(_itemsContainer); - var addRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 4.0f }; + _addRow = new HorizontalListNode + { + Size = new Vector2(LabelWidth + 40f, RowHeight), + ItemSpacing = 4.0f, + }; _addInput = new NumericInputNode { - Size = new Vector2(120, 28), + Size = new Vector2(120, RowHeight), Min = 0, Max = int.MaxValue, Value = 0, }; - addRow.AddNode(_addInput); + _addRow.AddNode(_addInput); var addButton = new TextButtonNode { - Size = new Vector2(60, 28), + Size = new Vector2(60, RowHeight), String = "Add", - OnClick = () => - { - var value = (uint)_addInput.Value; - if (! _list.Contains(value)) - { - _list.Add(value); - RefreshItems(); - _onChanged?.Invoke(); - } - }, + OnClick = AddCurrentValue, }; - addRow.AddNode(addButton); + _addRow.AddNode(addButton); - AddNode(addRow); - - RefreshItems(); + AddNode(_addRow); } public void SetList(List newList) @@ -79,35 +82,53 @@ public sealed class UintListEditorNode : VerticalListNode RefreshItems(); } + private void AddCurrentValue() + { + var value = (uint)_addInput.Value; + if (!_list.Contains(value)) + { + _list.Add(value); + RefreshItems(); + OnChanged?.Invoke(); + } + } + private void RefreshItems() { - _itemsContainer.SyncWithListData( - _list, - node => node.Value, - value => new UintListItemNode(value, _labelResolver) - { - Size = new Vector2(280, 22), - OnRemove = () => - { - _list.Remove(value); - RefreshItems(); - _onChanged?.Invoke(); - }, - } - ); + _itemsContainer.Clear(); + + foreach (var value in _list) + { + _itemsContainer.AddNode(CreateItemNode(value)); + } + + if (_list.Count == 0) + { + _itemsContainer.Height = 0; + } _itemsContainer.RecalculateLayout(); RecalculateLayout(); } - public void Refresh() + private UintListItemNode CreateItemNode(uint value) => new(value, LabelResolver) { + Size = new Vector2(LabelWidth + 40f, RowHeight), + OnRemove = () => RemoveValue(value), + }; + + private void RemoveValue(uint value) + { + _list.Remove(value); RefreshItems(); + OnChanged?.Invoke(); } } -public sealed class UintListItemNode : HorizontalListNode +public sealed class UintListItemNode : HorizontalListNode { + private const float LabelWidth = 300f; + public uint Value { get; } public Action? OnRemove { get; init; } @@ -116,22 +137,22 @@ public sealed class UintListItemNode : HorizontalListNode Value = value; ItemSpacing = 4.0f; - var displayText = labelResolver != null ? $"{value} - {labelResolver(value)}" : value.ToString(); - var itemLabel = new LabelTextNode + var displayText = labelResolver is not null + ? $"{value} - {labelResolver(value)}" + : value.ToString(); + + AddNode(new LabelTextNode { - TextFlags = TextFlags.AutoAdjustNodeSize, - Size = new Vector2(220, 22), + Size = new Vector2(LabelWidth, 24), String = displayText, TextColor = ColorHelper.GetColor(3), - }; - AddNode(itemLabel); + }); - var removeButton = new TextButtonNode + AddNode(new CircleButtonNode { - Size = new Vector2(50, 22), - String = "X", + Size = new Vector2(28, 28), + Icon = ButtonIcon.Cross, OnClick = () => OnRemove?.Invoke(), - }; - AddNode(removeButton); + }); } } \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/ConfigurationRoot.cs b/AetherBags/Nodes/Configuration/ConfigurationRoot.cs deleted file mode 100644 index 5d9b617..0000000 --- a/AetherBags/Nodes/Configuration/ConfigurationRoot.cs +++ /dev/null @@ -1,7 +0,0 @@ -using KamiToolKit.Nodes; - -namespace AetherBags.Nodes.Configuration; - -internal class ConfigurationRoot : TabbedVerticalListNode -{ -} \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Currency/CurrencyGeneralConfigurationNode.cs b/AetherBags/Nodes/Configuration/Currency/CurrencyGeneralConfigurationNode.cs index f8c1006..e709455 100644 --- a/AetherBags/Nodes/Configuration/Currency/CurrencyGeneralConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/Currency/CurrencyGeneralConfigurationNode.cs @@ -12,6 +12,8 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode { CurrencySettings config = System.Config.Currency; + ItemVerticalSpacing = 2; + LabelTextNode titleNode = new LabelTextNode { Size = Size with { Y = 18 }, @@ -51,14 +53,13 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode }; AddNode(defaultCurrencyColorNode); - AddNode(); - CheckboxNode cappedEnabledCheckbox = new CheckboxNode { Size = Size with { Y = 18 }, IsVisible = true, - String = "Color When Capped", + String = "Color Weekly Cap", IsChecked = config.ColorWhenCapped, + TextTooltip = "Changes the color of the currency display when you have reached the maximum amount earnable for the current week (e.g., 450/450).", OnClick = isChecked => { config.ColorWhenCapped = isChecked; @@ -69,9 +70,10 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode AddTab(1); + ColorInputRow cappedCurrencyColorNode = new ColorInputRow { - Label = "Capped Currency Color", + Label = "Weekly Cap Color", Size = new Vector2(300, 24), CurrentColor = config.CappedColor, DefaultColor = new CurrencySettings().CappedColor, @@ -89,8 +91,9 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode { Size = Size with { Y = 18 }, IsVisible = true, - String = "Color Weekly Limit", + String = "Color Max Capacity", IsChecked = config.ColorWhenLimited, + TextTooltip = "Changes the color of the currency display when your total held amount has reached its maximum capacity (e.g., 2000/2000).", OnClick = isChecked => { config.ColorWhenLimited = isChecked; @@ -103,7 +106,7 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode ColorInputRow limitCurrencyColorNode = new ColorInputRow { - Label = "Limit Currency Color", + Label = "Max Capacity Color", Size = new Vector2(300, 24), CurrentColor = config.LimitColor, DefaultColor = new CurrencySettings().LimitColor, diff --git a/AetherBags/Nodes/Configuration/Currency/CurrencyScrollingAreaNode.cs b/AetherBags/Nodes/Configuration/Currency/CurrencyScrollingAreaNode.cs index f0e074f..e8188e0 100644 --- a/AetherBags/Nodes/Configuration/Currency/CurrencyScrollingAreaNode.cs +++ b/AetherBags/Nodes/Configuration/Currency/CurrencyScrollingAreaNode.cs @@ -2,11 +2,11 @@ using KamiToolKit.Nodes; namespace AetherBags.Nodes.Configuration.Currency; -public sealed class CurrencyScrollingAreaNode : ScrollingAreaNode +public sealed class CurrencyScrollingAreaNode : ScrollingListNode { public CurrencyScrollingAreaNode() { - ContentNode.AddNode(new CurrencyGeneralConfigurationNode + AddNode(new CurrencyGeneralConfigurationNode { Size = Size }); diff --git a/AetherBags/Nodes/Configuration/General/FunctionalConfigurationNode.cs b/AetherBags/Nodes/Configuration/General/FunctionalConfigurationNode.cs index 19fc24f..825af76 100644 --- a/AetherBags/Nodes/Configuration/General/FunctionalConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/General/FunctionalConfigurationNode.cs @@ -2,6 +2,8 @@ using System; using System.Linq; using System.Numerics; using AetherBags.Configuration; +using AetherBags.Inventory; +using AetherBags.Nodes.Input; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Nodes; @@ -10,12 +12,16 @@ namespace AetherBags.Nodes.Configuration.General; internal sealed class FunctionalConfigurationNode : TabbedVerticalListNode { private readonly CheckboxNode _hideDefaultBagsCheckboxNode; + private readonly CheckboxNode _hideSaddlebagsCheckboxNode; + private readonly CheckboxNode _hideRetainerbagsCheckboxNode; private readonly LabeledDropdownNode _stackDropDown; public FunctionalConfigurationNode() { GeneralSettings config = System.Config.General; + ItemVerticalSpacing = 2; + var titleNode = new CategoryTextNode { Height = 18, @@ -55,6 +61,66 @@ internal sealed class FunctionalConfigurationNode : TabbedVerticalListNode AddNode(_hideDefaultBagsCheckboxNode); SubtractTab(1); + var showSaddleWithGameCheckBox = new CheckboxNode + { + Size = Size with { Y = 18 }, + IsVisible = true, + String = "Auto-open Saddlebags with game Saddlebags", + IsChecked = config.OpenSaddleBagsWithGameInventory, + OnClick = isChecked => + { + config.OpenSaddleBagsWithGameInventory = isChecked; + _hideSaddlebagsCheckboxNode?.IsEnabled = isChecked; + } + }; + AddNode(showSaddleWithGameCheckBox); + + AddTab(1); + _hideSaddlebagsCheckboxNode = new CheckboxNode + { + Size = Size with { Y = 18 }, + IsVisible = true, + String = "Hide default Saddlebags", + IsEnabled = config.OpenSaddleBagsWithGameInventory, + IsChecked = config.HideGameSaddleBags, + OnClick = isChecked => + { + config.HideGameSaddleBags = isChecked; + } + }; + AddNode(_hideSaddlebagsCheckboxNode); + SubtractTab(1); + + var showRetainerWithGameCheckBox = new CheckboxNode + { + Size = Size with { Y = 18 }, + IsVisible = true, + String = "Auto-open Retainer bags with game Retainer bags", + IsChecked = config.OpenRetainerWithGameInventory, + OnClick = isChecked => + { + config.OpenRetainerWithGameInventory = isChecked; + _hideRetainerbagsCheckboxNode?.IsEnabled = isChecked; + } + }; + AddNode(showRetainerWithGameCheckBox); + + AddTab(1); + _hideRetainerbagsCheckboxNode = new CheckboxNode + { + Size = Size with { Y = 18 }, + IsVisible = true, + String = "Hide default Retainer bags", + IsEnabled = config.OpenRetainerWithGameInventory, + IsChecked = config.HideGameRetainer, + OnClick = isChecked => + { + config.HideGameRetainer = isChecked; + } + }; + AddNode(_hideRetainerbagsCheckboxNode); + SubtractTab(1); + var linkItemCheckBox = new CheckboxNode { Size = Size with { Y = 18 }, @@ -68,6 +134,29 @@ internal sealed class FunctionalConfigurationNode : TabbedVerticalListNode }; AddNode(linkItemCheckBox); + AddNode(new ResNode + { + Height = 6 + }); + + var searchModeDropDown = new LabeledDropdownNode + { + Size = new Vector2(300, 20), + LabelText = "Search Mode", + LabelTextFlags = TextFlags.AutoAdjustNodeSize, + Options = Enum.GetNames(typeof(SearchMode)).ToList(), + SelectedOption = config.SearchMode.ToString(), + OnOptionSelected = selected => + { + if (Enum.TryParse(selected, out var parsed)) + { + config.SearchMode = parsed; + InventoryOrchestrator.RefreshAll(updateMaps: false); + } + } + }; + AddNode(searchModeDropDown); + _stackDropDown = new LabeledDropdownNode { Size = new Vector2(300, 20), @@ -81,7 +170,7 @@ internal sealed class FunctionalConfigurationNode : TabbedVerticalListNode if (Enum.TryParse(selected, out var parsed)) { config.StackMode = parsed; - System.AddonInventoryWindow.ManualInventoryRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); } } }; diff --git a/AetherBags/Nodes/Configuration/General/GeneralScrollingAreaNode.cs b/AetherBags/Nodes/Configuration/General/GeneralScrollingAreaNode.cs index 253f864..f9d4e53 100644 --- a/AetherBags/Nodes/Configuration/General/GeneralScrollingAreaNode.cs +++ b/AetherBags/Nodes/Configuration/General/GeneralScrollingAreaNode.cs @@ -5,30 +5,30 @@ using KamiToolKit.Nodes; namespace AetherBags.Nodes.Configuration.General; -public sealed class GeneralScrollingAreaNode : ScrollingAreaNode +public sealed class GeneralScrollingAreaNode : ScrollingListNode { - private readonly CheckboxNode _debugCheckboxNode = null!; - public GeneralScrollingAreaNode() { GeneralSettings config = System.Config.General; - ContentNode.ItemSpacing = 32; + new ImportExportResetNode().AttachNode(this); - ContentNode.AddNode(new FunctionalConfigurationNode()); + ItemSpacing = 10; - ContentNode.AddNode(new LayoutConfigurationNode()); + AddNode(new FunctionalConfigurationNode()); - _debugCheckboxNode = new CheckboxNode + AddNode(new LayoutConfigurationNode()); + + AddNode(new CheckboxNode { Size = new Vector2(300, 20), IsVisible = true, String = "Debug Mode", IsChecked = config.DebugEnabled, - OnClick = isChecked => { config.DebugEnabled = isChecked; } - }; - ContentNode.AddNode(_debugCheckboxNode); + OnClick = isChecked => + { + config.DebugEnabled = isChecked; + } + }); } - - private void RefreshInventory() => System.AddonInventoryWindow.ManualInventoryRefresh(); } \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/General/ImportExportResetNode.cs b/AetherBags/Nodes/Configuration/General/ImportExportResetNode.cs new file mode 100644 index 0000000..a8a82fb --- /dev/null +++ b/AetherBags/Nodes/Configuration/General/ImportExportResetNode.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; +using AetherBags.Helpers; +using AetherBags.Inventory; +using Dalamud.Game.ClientState.Keys; +using KamiToolKit.Classes; +using KamiToolKit.Nodes; + +namespace AetherBags.Nodes.Configuration.General; + +public sealed class ImportExportResetNode : HorizontalListNode +{ + public ImportExportResetNode() + { + Height = 0; + Width = 600; + Alignment = HorizontalListAnchor.Right; + FirstItemSpacing = 3; + ItemSpacing = 2; + IsVisible = true; + + AddNode(new ImGuiIconButtonNode { + Y = 3, + Height = 30, + Width = 30, + IsVisible = true, + TextTooltip = " Import Configuration\n(hold shift to confirm)", + TexturePath = Path.Combine(Services.PluginInterface.AssemblyLocation.Directory?.FullName!, @"Assets\Icons\download.png"), + OnClick = ImportConfig + }); + + AddNode(new ImGuiIconButtonNode { + Y = 3, + Height = 30, + Width = 30, + IsVisible = true, + TextTooltip = "Export Configuration", + TexturePath = Path.Combine(Services.PluginInterface.AssemblyLocation.Directory?.FullName!, @"Assets\Icons\upload.png"), + OnClick = ExportConfig + }); + + AddNode(new HoldButtonNode { + IsVisible = true, + Y = 0, + Height = 32, + Width = 100, + String = "Reset", + TextNode = { TextColor = ColorHelper.GetColor(50) }, + TextTooltip = " Reset configuration\n(hold button to confirm)", + OnClick = ResetConfig + }); + } + + private static void ResetConfig() + { + InventoryOrchestrator.CloseAll(); + ImportExportResetHelper.TryResetConfig(); + System.AddonConfigurationWindow.Close(); + } + + private static void ImportConfig() + { + if (!Services.KeyState[VirtualKey.SHIFT]) return; + + InventoryOrchestrator.CloseAll(); + ImportExportResetHelper.TryImportConfigFromClipboard(); + System.AddonConfigurationWindow.Close(); + } + + private static void ExportConfig() => ImportExportResetHelper.TryExportConfigToClipboard(System.Config); +} \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Layout/CompactLookaheadNode.cs b/AetherBags/Nodes/Configuration/Layout/CompactLookaheadNode.cs index b01c560..5c2e597 100644 --- a/AetherBags/Nodes/Configuration/Layout/CompactLookaheadNode.cs +++ b/AetherBags/Nodes/Configuration/Layout/CompactLookaheadNode.cs @@ -2,6 +2,7 @@ using KamiToolKit.Classes.Timelines; using KamiToolKit.Nodes; using System.Numerics; +using AetherBags.Inventory; using FFXIVClientStructs.FFXIV.Component.GUI; namespace AetherBags.Nodes.Configuration.Layout; @@ -33,7 +34,7 @@ internal sealed class CompactLookaheadNode : SimpleComponentNode OnValueUpdate = value => { config.CompactLookahead = value; - System.AddonInventoryWindow.ManualInventoryRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); } }; CompactLookahead.AttachNode(this); diff --git a/AetherBags/Nodes/Configuration/Layout/LayoutConfigurationNode.cs b/AetherBags/Nodes/Configuration/Layout/LayoutConfigurationNode.cs index 93db895..c68954c 100644 --- a/AetherBags/Nodes/Configuration/Layout/LayoutConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/Layout/LayoutConfigurationNode.cs @@ -1,5 +1,6 @@ using System.Numerics; using AetherBags.Configuration; +using AetherBags.Inventory; using KamiToolKit.Nodes; namespace AetherBags.Nodes.Configuration.Layout; @@ -32,7 +33,7 @@ internal class LayoutConfigurationNode : TabbedVerticalListNode OnClick = isChecked => { config.ShowCategoryItemCount = isChecked; - System.AddonInventoryWindow.ManualInventoryRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); } }; AddNode(showCategoryItemAmountCheckboxNode); @@ -49,7 +50,7 @@ internal class LayoutConfigurationNode : TabbedVerticalListNode _preferLargestFitCheckboxNode.IsEnabled = isChecked; _useStableInsertCheckboxNode.IsEnabled = isChecked; _compactLookaheadNode.CompactLookahead.IsEnabled = isChecked; - System.AddonInventoryWindow.ManualInventoryRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); } }; AddNode(compactPackingCheckboxNode); @@ -65,7 +66,7 @@ internal class LayoutConfigurationNode : TabbedVerticalListNode OnClick = isChecked => { config.CompactPreferLargestFit = isChecked; - System.AddonInventoryWindow.ManualInventoryRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); } }; AddNode(_preferLargestFitCheckboxNode); @@ -80,7 +81,7 @@ internal class LayoutConfigurationNode : TabbedVerticalListNode OnClick = isChecked => { config.CompactStableInsert = isChecked; - System.AddonInventoryWindow.ManualInventoryRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); } }; AddNode(_useStableInsertCheckboxNode); diff --git a/AetherBags/Nodes/Currency/CurrencyNode.cs b/AetherBags/Nodes/Currency/CurrencyNode.cs index a3adf98..25b5c6e 100644 --- a/AetherBags/Nodes/Currency/CurrencyNode.cs +++ b/AetherBags/Nodes/Currency/CurrencyNode.cs @@ -51,7 +51,7 @@ public class CurrencyNode : SimpleComponentNode _countNode.TextColor = isLimited ? config.LimitColor : - isCapped ? config.CappedColor : + isCapped ? config.CappedColor : config.DefaultColor; } } diff --git a/AetherBags/Nodes/DragDropNode.cs b/AetherBags/Nodes/DragDropNode.cs deleted file mode 100644 index f99f3b2..0000000 --- a/AetherBags/Nodes/DragDropNode.cs +++ /dev/null @@ -1,242 +0,0 @@ -using System; -using System.Numerics; -using AetherBags.Interop; -using FFXIVClientStructs.FFXIV.Client.Enums; -using FFXIVClientStructs.FFXIV.Client.UI; -using FFXIVClientStructs.FFXIV.Client.UI.Agent; -using FFXIVClientStructs.FFXIV.Component.GUI; -using KamiToolKit.Classes; -using KamiToolKit.Classes.Timelines; -using KamiToolKit.Nodes; - -namespace AetherBags.Nodes; - -public unsafe class DragDropNode : ComponentNode { - - // FIX: Manually expose the pointers that are 'internal' in KamiToolKit - // We access the raw AtkComponentNode* via 'this.ResNode' and cast from there. - private AtkComponentDragDrop* Component => (AtkComponentDragDrop*)Node->Component; - private AtkUldComponentDataDragDrop* Data => (AtkUldComponentDataDragDrop*)Component->UldManager.ComponentData; - - public readonly ImageNode DragDropBackgroundNode; - public readonly IconNode IconNode; - - public DragDropNode() { - SetInternalComponentType(ComponentType.DragDrop); - - DragDropBackgroundNode = new SimpleImageNode { - NodeId = 3, - Size = new Vector2(44.0f, 44.0f), - TexturePath = "ui/uld/DragTargetA.tex", - TextureCoordinates = new Vector2(0.0f, 0.0f), - TextureSize = new Vector2(44.0f, 44.0f), - WrapMode = WrapMode.Tile, - NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents, - }; - DragDropBackgroundNode.AttachNode(this); - - IconNode = new IconNode { - NodeId = 2, - Size = new Vector2(44.0f, 48.0f), - NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents, - }; - IconNode.AttachNode(this); - - LoadTimelines(); - - Data->Nodes[0] = IconNode.NodeId; - - AcceptedType = DragDropType.Everything; - Payload = new DragDropPayload(); - - // Use the fixed shadow struct for writing initial values if needed, - // though direct field access on the struct usually works for simple fields. - // However, to be safe with the VTable fix, we just set fields directly here - // as they are standard offsets, or use the pointer. - Component->AtkDragDropInterface.DragDropType = DragDropType.Everything; - Component->AtkDragDropInterface.DragDropReferenceIndex = 0; - - InitializeComponentEvents(); - - AddEvent(AtkEventType.DragDropBegin, DragDropBeginHandler); - AddEvent(AtkEventType.DragDropInsert, DragDropInsertHandler); - AddEvent(AtkEventType.DragDropDiscard, DragDropDiscardHandler); - AddEvent(AtkEventType.DragDropClick, DragDropClickHandler); - AddEvent(AtkEventType.DragDropRollOver, DragDropRollOverHandler); - AddEvent(AtkEventType.DragDropRollOut, DragDropRollOutHandler); - } - - private bool IsDragDropEndRegistered { get; set; } - - public Action? OnBegin { get; set; } - public Action? OnEnd { get; set; } - public Action? OnPayloadAccepted { get; set; } - public Action? OnDiscard { get; set; } - public Action? OnClicked { get; set; } - public Action? OnRollOver { get; set; } - public Action? OnRollOut { get; set; } - - public DragDropPayload Payload { get; set; } - - public uint IconId { - get => IconNode.IconId; - set { - IconNode.IconId = value; - IconNode.IsVisible = value != 0; - } - } - - public bool IsIconDisabled { - get => IconNode.IsIconDisabled; - set => IconNode.IsIconDisabled = value; - } - - public int Quantity { - get => int.Parse(Component->GetQuantityText().ToString()); - set => Component->SetQuantity(value); - } - - public string QuantityString { - get => Component->GetQuantityText().ToString(); - set => Component->SetQuantityText(value); - } - - public DragDropType AcceptedType { - get => Component->AcceptedType; - set => Component->AcceptedType = value; - } - - public AtkDragDropInterface.SoundEffectSuppression SoundEffectSuppression { - get => Component->AtkDragDropInterface.DragDropSoundEffectSuppression; - set => Component->AtkDragDropInterface.DragDropSoundEffectSuppression = value; - } - - public bool IsDraggable { - get => !Component->Flags.HasFlag(DragDropFlag.Locked); - set { - if (value) { - Component->Flags &= ~DragDropFlag.Locked; - } - else { - Component->Flags |= DragDropFlag.Locked; - } - } - } - - public bool IsClickable { - get => Component->Flags.HasFlag(DragDropFlag.Clickable); - set { - if (value) { - Component->Flags |= DragDropFlag.Clickable; - } - else { - Component->Flags &= ~DragDropFlag.Clickable; - } - } - } - - private void DragDropBeginHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) { - atkEvent->SetEventIsHandled(); - - // FIX: Use extension method to write payload using fixed VTable - Payload.ToFixedInterface(atkEventData->DragDropData.DragDropInterface); - - OnBegin?.Invoke(this); - - if (!IsDragDropEndRegistered) { - AddEvent(AtkEventType.DragDropEnd, DragDropEndHandler); - IsDragDropEndRegistered = true; - } - } - - private void DragDropInsertHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) { - atkEvent->SetEventIsHandled(); - - atkEvent->State.StateFlags |= AtkEventStateFlags.HasReturnFlags; - atkEvent->State.ReturnFlags = 1; - - // FIX: Use extension method to read payload using fixed VTable - var payload = DragDropPayloadExtensions.FromFixedInterface(atkEventData->DragDropData.DragDropInterface); - - Payload.Clear(); - IconId = 0; - - OnPayloadAccepted?.Invoke(this, payload); - } - - private void DragDropDiscardHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) { - atkEvent->SetEventIsHandled(); - - atkEvent->State.StateFlags |= AtkEventStateFlags.HasReturnFlags; - atkEvent->State.ReturnFlags = 1; - - OnDiscard?.Invoke(this); - } - - private void DragDropEndHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) { - atkEvent->SetEventIsHandled(); - - // FIX: Cast to shadow struct to call the correct GetPayloadContainer (Index 12) - var fixedInterface = (AtkDragDropInterfaceFixed*)atkEventData->DragDropData.DragDropInterface; - fixedInterface->GetPayloadContainer()->Clear(); - - OnEnd?.Invoke(this); - - if (IsDragDropEndRegistered) { - RemoveEvent(AtkEventType.DragDropEnd, DragDropEndHandler); - IsDragDropEndRegistered = false; - } - } - - private void DragDropClickHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) { - atkEvent->SetEventIsHandled(); - - atkEvent->State.StateFlags |= AtkEventStateFlags.HasReturnFlags; - atkEvent->State.ReturnFlags = 1; - - OnClicked?.Invoke(this); - } - - private void DragDropRollOverHandler() - => OnRollOver?.Invoke(this); - - private void DragDropRollOutHandler() - => OnRollOut?.Invoke(this); - - public void Clear() { - Payload.Clear(); - IconId = 0; - } - - public void ShowTooltip(AtkTooltipManager.AtkTooltipType type, ActionKind actionKind) { - if (AtkStage.Instance()->DragDropManager.IsDragging) return; - - // FIX: Explicitly use 'this.ResNode' and cast to (AtkResNode*) to avoid ambiguity with the class name - var addon = RaptureAtkUnitManager.Instance()->GetAddonByNode((AtkResNode*)this); - if (addon is null) return; - - var tooltipArgs = new AtkTooltipManager.AtkTooltipArgs(); - tooltipArgs.Ctor(); - tooltipArgs.ActionArgs.Id = Payload.Int2; - tooltipArgs.ActionArgs.Kind = (DetailKind)actionKind; - - AtkStage.Instance()->TooltipManager.ShowTooltip( - AtkTooltipManager.AtkTooltipType.Action, - addon->Id, - (AtkResNode*)this, // FIX: Explicit cast here as well - &tooltipArgs); - } - - private void LoadTimelines() { - AddTimeline(new TimelineBuilder() - .BeginFrameSet(1, 59) - .AddLabelPair(1, 10, 1) - .AddLabelPair(11, 19, 2) - .AddLabelPair(20, 29, 3) - .AddLabelPair(30, 39, 7) - .AddLabelPair(40, 49, 6) - .AddLabelPair(50, 59, 4) - .EndFrameSet() - .Build()); - } -} \ No newline at end of file diff --git a/AetherBags/Nodes/Input/LabeledDropdownNode.cs b/AetherBags/Nodes/Input/LabeledDropdownNode.cs index 216eb58..e2149c5 100644 --- a/AetherBags/Nodes/Input/LabeledDropdownNode.cs +++ b/AetherBags/Nodes/Input/LabeledDropdownNode.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Nodes; -namespace AetherBags.Nodes; +namespace AetherBags.Nodes.Input; public class LabeledDropdownNode : SimpleComponentNode { private readonly GridNode _gridNode; diff --git a/AetherBags/Nodes/Input/TextInputWithButtonNode.cs b/AetherBags/Nodes/Input/TextInputWithButtonNode.cs new file mode 100644 index 0000000..8b9d56f --- /dev/null +++ b/AetherBags/Nodes/Input/TextInputWithButtonNode.cs @@ -0,0 +1,55 @@ +using System; +using System.Numerics; +using KamiToolKit.Classes; +using KamiToolKit.Nodes; +using Lumina.Text.ReadOnly; + +namespace AetherBags.Nodes.Input; + +public class TextInputWithButtonNode : SimpleComponentNode { + private readonly TextInputNode _textInputNode; + private readonly CircleButtonNode _contextButton; + + public Action? OnButtonClicked { + get => _contextButton.OnClick; + set => _contextButton.OnClick = value; + } + + public TextInputWithButtonNode() { + _textInputNode = new TextInputNode { + PlaceholderString = "Search . . .", + }; + _textInputNode.AttachNode(this); + + _contextButton = new CircleButtonNode { + Icon = ButtonIcon.Filter, + Size = new Vector2(28f), + }; + _contextButton.AttachNode(this); + } + + public Vector3 HintAddColor { + get => _contextButton.AddColor; + set => _contextButton.AddColor = value; + } + + public required Action? OnInputReceived { + get => _textInputNode.OnInputReceived; + set => _textInputNode.OnInputReceived = value; + } + + protected override void OnSizeChanged() { + base.OnSizeChanged(); + + _contextButton.Size = new Vector2(Height, Height); + _contextButton.Position = new Vector2(Width - _contextButton.Width, 0.0f); + + _textInputNode.Size = new Vector2(Width - _contextButton.Width - 5.0f, Height); + _textInputNode.Position = new Vector2(0.0f, 0.0f); + } + + public ReadOnlySeString SearchString { + get => _textInputNode.SeString; + set => _textInputNode.SeString = value; + } +} \ No newline at end of file diff --git a/AetherBags/Nodes/Input/TextInputWithHintNode.cs b/AetherBags/Nodes/Input/TextInputWithHintNode.cs deleted file mode 100644 index 8d84ff2..0000000 --- a/AetherBags/Nodes/Input/TextInputWithHintNode.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Numerics; -using KamiToolKit.Nodes; -using Lumina.Text; -using Lumina.Text.ReadOnly; - -namespace AetherBags.Nodes.Input; - -public class TextInputWithHintNode : SimpleComponentNode { - private readonly TextInputNode _textInputNode; - private readonly ImageNode _helpNode; - - public TextInputWithHintNode() { - _textInputNode = new TextInputNode { - PlaceholderString = "Search . . .", - }; - _textInputNode.AttachNode(this); - - _helpNode = new SimpleImageNode { - TexturePath = "ui/uld/CircleButtons.tex", - TextureCoordinates = new Vector2(112.0f, 84.0f), - TextureSize = new Vector2(28.0f, 28.0f), - TextTooltip = new SeStringBuilder() - .Append("Supports Regex Search") - .AppendNewLine() - .Append("Start input with '$' to search by description") - .ToReadOnlySeString(), - }; - _helpNode.AttachNode(this); - } - - public required Action? OnInputReceived { - get => _textInputNode.OnInputReceived; - set => _textInputNode.OnInputReceived = value; - } - - protected override void OnSizeChanged() { - base.OnSizeChanged(); - - _helpNode.Size = new Vector2(Height, Height); - _helpNode.Position = new Vector2(Width - _helpNode.Width - 5.0f, 0.0f); - - _textInputNode.Size = new Vector2(Width - _helpNode.Width - 5.0f, Height); - _textInputNode.Position = new Vector2(0.0f, 0.0f); - } - - public ReadOnlySeString SearchString { - get => _textInputNode.SeString; - set => _textInputNode.SeString = value; - } -} \ No newline at end of file diff --git a/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs index 0d4c8d7..bf4cef5 100644 --- a/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs +++ b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs @@ -1,7 +1,10 @@ using System; using System.Numerics; using AetherBags.Helpers; +using AetherBags.Hooks; 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; @@ -11,15 +14,12 @@ using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Classes; using KamiToolKit.Nodes; -// TODO: Switch back to CS version when Dalamud Updated -using DragDropFixedNode = AetherBags.Nodes.DragDropNode; - namespace AetherBags.Nodes.Inventory; public class InventoryCategoryNode : SimpleComponentNode { private readonly TextNode _categoryNameTextNode; - private readonly HybridDirectionalFlexNode _itemGridNode; + private readonly HybridDirectionalFlexNode _itemGridNode; private const float FallbackItemSize = 46; private const float HeaderHeight = 16; @@ -33,6 +33,8 @@ public class InventoryCategoryNode : SimpleComponentNode private string _fullHeaderText = string.Empty; public event Action? HeaderHoverChanged; + public Action? OnRefreshRequested { get; set; } + public Action? OnDragEnd { get; set; } public InventoryCategoryNode() { @@ -51,7 +53,7 @@ public class InventoryCategoryNode : SimpleComponentNode _categoryNameTextNode.AddFlags(NodeFlags.EmitsEvents | NodeFlags.HasCollision); _categoryNameTextNode.AttachNode(this); - _itemGridNode = new HybridDirectionalFlexNode + _itemGridNode = new HybridDirectionalFlexNode { Position = new Vector2(0, HeaderHeight), Size = new Vector2(240, 92), @@ -218,26 +220,35 @@ public class InventoryCategoryNode : SimpleComponentNode private unsafe InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data) { InventoryItem item = data.Item; - InventoryMappedLocation location = data.VisualLocation; + InventoryMappedLocation visualLocation = data.VisualLocation; + + var visualInvType = InventoryType.GetInventoryTypeFromContainerId(visualLocation.Container); + int absoluteIndex = visualInvType.GetInventoryStartIndex + visualLocation.Slot; + + DragDropPayload nodePayload = new DragDropPayload + { + // Int1 is always the container ID, for Item DragDrop Int2 is only used as a fallback + // ReferenceIndex is the absolute index that's actually used + Type = DragDropType.Item, + Int1 = visualLocation.Container, + Int2 = visualLocation.Slot, + ReferenceIndex = (short)absoluteIndex + }; return new InventoryDragDropNode { Size = new Vector2(42, 46), - Alpha = data.IsEligibleForContext || data.IsSlotBlocked ? 1.0f : 0.4f, + Alpha = data.VisualAlpha, + AddColor = data.HighlightOverlayColor, + IsDraggable = !data.IsSlotBlocked, IsVisible = true, IconId = item.IconId, AcceptedType = DragDropType.Item, - IsDraggable = !data.IsSlotBlocked, - Payload = new DragDropPayload - { - Type = DragDropType.Item, - Int1 = location.Container, - Int2 = location.Slot, - }, + Payload = nodePayload, IsClickable = true, OnDiscard = node => OnDiscard(node, data), - OnEnd = _ => System.AddonInventoryWindow.ManualInventoryRefresh(), - OnPayloadAccepted = (node, payload) => OnPayloadAccepted(node, payload, data), + OnEnd = _ => OnDragEnd?.Invoke(), + OnPayloadAccepted = (node, acceptedPayload) => OnPayloadAccepted(node, acceptedPayload, data), OnRollOver = node => { BeginHeaderHover(); @@ -254,49 +265,63 @@ public class InventoryCategoryNode : SimpleComponentNode }; } + public void RefreshNodeVisuals() + { + foreach (var node in _itemGridNode.Nodes) + { + if (node is not InventoryDragDropNode itemNode || itemNode.ItemInfo == null) continue; + + itemNode.Alpha = itemNode.ItemInfo.VisualAlpha; + itemNode.AddColor = itemNode.ItemInfo.HighlightOverlayColor; + itemNode.IsDraggable = !itemNode.ItemInfo.IsSlotBlocked; + } + } + private unsafe void OnDiscard(DragDropNode node, ItemInfo item) { uint addonId = RaptureAtkUnitManager.Instance()->GetAddonByNode(node)->Id; AgentInventoryContext.Instance()->DiscardItem(item.Item.GetLinkedItem(), item.Item.Container, item.Item.Slot, addonId); } - private void OnPayloadAccepted(DragDropNode node, DragDropPayload payload, ItemInfo targetItemInfo) + private void OnPayloadAccepted(DragDropNode node, DragDropPayload acceptedPayload, ItemInfo targetItemInfo) { - InventoryItem item = targetItemInfo.Item; - if (!payload.IsValidInventoryPayload) + try { - Services.Logger.Warning($"[OnPayload] Invalid payload type: {payload.Type}"); - return; + // KTK clears node.Payload before invoking this, so setting it manually again + var nodePayload = new DragDropPayload + { + Type = DragDropType.Item, + Int1 = targetItemInfo.VisualLocation.Container, + Int2 = targetItemInfo.VisualLocation.Slot, + ReferenceIndex = (short)(targetItemInfo.Item.Container.GetInventoryStartIndex + targetItemInfo.VisualLocation.Slot) + }; + + Services.Logger.DebugOnly($"[OnPayload] ACCEPTED payload: Type={acceptedPayload.Type} Int1={acceptedPayload.Int1} Int2={acceptedPayload.Int2} Ref={acceptedPayload.ReferenceIndex}"); + Services.Logger.DebugOnly($"[OnPayload] NODE payload: Type={nodePayload.Type} Int1={nodePayload.Int1} Int2={nodePayload.Int2} Ref={nodePayload.ReferenceIndex}"); + + if (!acceptedPayload.IsValidInventoryPayload || !nodePayload.IsValidInventoryPayload) + { + Services.Logger.Warning($"[OnPayload] Invalid payload type: Accepted={acceptedPayload.Type} Node={nodePayload.Type}"); + return; + } + + if (acceptedPayload.IsSameBaseContainer(nodePayload)) + { + Services.Logger.DebugOnly("[OnPayload] Source and target are in the same base container, skipping move."); + node.IconId = targetItemInfo.IconId; + node.Payload = nodePayload; + return; + } + + var sourceCopy = acceptedPayload; + var targetCopy = nodePayload; + + InventoryMoveHelper.HandleItemMovePayload(sourceCopy, targetCopy); + OnRefreshRequested?.Invoke(); } - - InventoryLocation sourceLocation = payload.InventoryLocation; - - if (!sourceLocation.IsValid) + catch (Exception ex) { - Services.Logger.Warning($"[OnPayload] Could not resolve source from payload"); - return; + Services.Logger.Error(ex, "[OnPayload] Error handling payload acceptance"); } - - InventoryLocation targetLocation = new InventoryLocation( - item.Container, - (ushort)item.Slot - ); - - if (sourceLocation.Container.IsSameContainerGroup(targetLocation.Container)) - { - 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(); - return; - }; - - Services.Logger.Debug($"[OnPayload] Moving {sourceLocation} -> {targetLocation}"); - - InventoryMoveHelper.MoveItem( - sourceLocation.Container, sourceLocation.Slot, - targetLocation.Container, targetLocation.Slot - ); - System.AddonInventoryWindow.ManualInventoryRefresh(); } } \ No newline at end of file diff --git a/AetherBags/Nodes/Inventory/InventoryDragDropNode.cs b/AetherBags/Nodes/Inventory/InventoryDragDropNode.cs index a2435fb..285e68e 100644 --- a/AetherBags/Nodes/Inventory/InventoryDragDropNode.cs +++ b/AetherBags/Nodes/Inventory/InventoryDragDropNode.cs @@ -1,17 +1,16 @@ 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; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Classes; using KamiToolKit.Nodes; -// TODO: Switch back to CS version when Dalamud Updated -using DragDropFixedNode = AetherBags.Nodes.DragDropNode; namespace AetherBags.Nodes.Inventory; -public class InventoryDragDropNode : DragDropFixedNode +public class InventoryDragDropNode : DragDropNode { private readonly TextNode _quantityTextNode; public unsafe InventoryDragDropNode() diff --git a/AetherBags/Nodes/Inventory/InventoryFooterNode.cs b/AetherBags/Nodes/Inventory/InventoryFooterNode.cs index 27a9105..48024e3 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; @@ -24,7 +25,7 @@ public sealed class InventoryFooterNode : SimpleComponentNode FontType = FontType.MiedingerMed, TextFlags = TextFlags.Glare, TextColor = ColorHelper.GetColor(50), - TextOutlineColor = ColorHelper.GetColor(32) // Could also be Color 65 + TextOutlineColor = ColorHelper.GetColor(32) }; _slotAmountTextNode.AttachNode(this); @@ -46,8 +47,8 @@ public sealed class InventoryFooterNode : SimpleComponentNode IReadOnlyList currencyInfoList = InventoryState.GetCurrencyInfoList([1, 28, 0xFFFF_FFFE, 0xFFFF_FFFD]); _currencyListNode.SyncWithListDataByKey( dataList: currencyInfoList, - getKeyFromData: c => c.ItemId, - getKeyFromNode: n => n.Currency.ItemId, + getKeyFromData: currencyInfo => currencyInfo.ItemId, + getKeyFromNode: node => node.Currency.ItemId, updateNode: (node, data) => { node.Currency = data; diff --git a/AetherBags/Nodes/Inventory/InventoryNotificationNode.cs b/AetherBags/Nodes/Inventory/InventoryNotificationNode.cs index 02e578c..550dc72 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; @@ -87,7 +88,7 @@ public sealed class InventoryNotificationNode : SimpleComponentNode Timeline?.PlayAnimation(101); } - } + } = null!; // Future Zeff, this always goes on a parent private Timeline ParentLabels => new TimelineBuilder() diff --git a/AetherBags/Nodes/Inventory/SaddleBagFooterNode.cs b/AetherBags/Nodes/Inventory/SaddleBagFooterNode.cs new file mode 100644 index 0000000..1080390 --- /dev/null +++ b/AetherBags/Nodes/Inventory/SaddleBagFooterNode.cs @@ -0,0 +1,32 @@ +using System. Numerics; +using FFXIVClientStructs.FFXIV.Component.GUI; +using KamiToolKit.Classes; +using KamiToolKit.Nodes; + +namespace AetherBags.Nodes.Inventory; + +public class SaddleBagFooterNode : SimpleComponentNode +{ + private readonly TextNode _slotCounterNode; + + private const float Padding = 8f; + + public SaddleBagFooterNode() + { + _slotCounterNode = new TextNode + { + Position = new Vector2(Padding, 4f), + Size = new Vector2(100, 20), + AlignmentType = AlignmentType.Left, + TextColor = new Vector4(1f, 1f, 1f, 1f), + FontSize = 14, + }; + _slotCounterNode.AttachNode(this); + } + + public string SlotAmountText + { + get => _slotCounterNode.String; + set => _slotCounterNode.String = $"Slots: {value}"; + } +} \ No newline at end of file diff --git a/AetherBags/Plugin.cs b/AetherBags/Plugin.cs index 2be11b3..5199378 100644 --- a/AetherBags/Plugin.cs +++ b/AetherBags/Plugin.cs @@ -5,15 +5,18 @@ using AetherBags.Commands; using AetherBags.Helpers; using AetherBags.Hooks; using AetherBags.Inventory; +using AetherBags.Inventory.Context; +using AetherBags.Inventory.State; +using AetherBags.IPC; +using Dalamud.Game.Gui; using Dalamud.Plugin; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; using KamiToolKit; namespace AetherBags; public unsafe class Plugin : IDalamudPlugin { - private static string HelpDescription => "Opens your inventory."; - private readonly CommandHandler _commandHandler; private readonly InventoryHooks _inventoryHooks; private readonly InventoryLifecycles _inventoryLifecycles; @@ -22,19 +25,35 @@ public unsafe class Plugin : IDalamudPlugin { pluginInterface.Create(); + System.Config = Util.LoadConfigOrDefault(); + BackupHelper.DoConfigBackup(pluginInterface); KamiToolKitLibrary.Initialize(pluginInterface); - System.Config = Util.LoadConfigOrDefault(); + System.IPC = new IPCService(); System.AddonInventoryWindow = new AddonInventoryWindow { - InternalName = "AetherBags", + InternalName = "AetherBags_MainBags", Title = "AetherBags", Size = new Vector2(750, 750), }; + System.AddonSaddleBagWindow = new AddonSaddleBagWindow + { + InternalName = "AetherBags_SaddleBag", + Title = "AetherSaddlebag", + Size = new Vector2(750, 750), + }; + + System.AddonRetainerWindow = new AddonRetainerWindow + { + InternalName = "AetherBags_Retainer", + Title = "AetherRetainerbag", + Size = new Vector2(750, 750), + }; + System.AddonConfigurationWindow = new AddonConfigurationWindow { InternalName = "AetherBags Config", @@ -47,8 +66,6 @@ public unsafe class Plugin : IDalamudPlugin _commandHandler = new CommandHandler(); - // Services.GameInventory.InventoryChanged += InventoryState.OnRawItemAdded; - Services.ClientState.Login += OnLogin; Services.ClientState.Logout += OnLogout; @@ -62,22 +79,20 @@ public unsafe class Plugin : IDalamudPlugin public void Dispose() { - Util.SaveConfig(System.Config); - - // Services.GameInventory.InventoryChanged -= InventoryState.OnRawItemAdded; - - Services.ClientState.Login -= OnLogin; - Services.ClientState.Logout -= OnLogout; - - _commandHandler.Dispose(); - - System.AddonInventoryWindow.Dispose(); - System.AddonConfigurationWindow.Dispose(); - - KamiToolKitLibrary.Dispose(); - + InventoryAddonContextMenu.Close(); _inventoryHooks.Dispose(); _inventoryLifecycles.Dispose(); + + System.IPC.Dispose(); + HighlightState.ClearAll(); + + System.AddonInventoryWindow.Dispose(); + System.AddonSaddleBagWindow.Dispose(); + System.AddonRetainerWindow.Dispose(); + System.AddonConfigurationWindow.Dispose(); + + Util.SaveConfig(System.Config); + KamiToolKitLibrary.Dispose(); } private void OnLogin() @@ -96,6 +111,8 @@ public unsafe class Plugin : IDalamudPlugin Util.SaveConfig(System.Config); InventoryState.TrackLootedItems = false; System.AddonInventoryWindow.Close(); + System.AddonSaddleBagWindow.Close(); + System.AddonRetainerWindow.Close(); System.AddonConfigurationWindow.Close(); } } \ No newline at end of file diff --git a/AetherBags/Services.cs b/AetherBags/Services.cs index bf7a595..71fb89e 100644 --- a/AetherBags/Services.cs +++ b/AetherBags/Services.cs @@ -10,15 +10,17 @@ public class Services [PluginService] public static IChatGui ChatGui { get; set; } = null!; [PluginService] public static IClientState ClientState { get; private set; } = null!; [PluginService] public static ICommandManager CommandManager { get; private set; } = null!; + [PluginService] public static ICondition Condition { get; private set; } = null!; [PluginService] public static IDataManager DataManager { get; set; } = null!; [PluginService] public static IDalamudPluginInterface PluginInterface { get; private set; } = null!; [PluginService] public static IFramework Framework { get; private set; } = null!; [PluginService] public static IGameGui GameGui { get; private set; } = null!; [PluginService] public static IGameInventory GameInventory { get; set; } = null!; [PluginService] public static IKeyState KeyState { get; private set; } = null!; + [PluginService] public static IPlayerState PlayerState { get; private set; } = null!; [PluginService] public static IPluginLog Logger { get; private set; } = null!; [PluginService] public static INotificationManager NotificationManager { get; private set; } = null!; - // TODO: Remove cause temp + [PluginService] public static IObjectTable ObjectTable { get; private set; } = null!; [PluginService] public static ISigScanner SigScanner { get; private set; } = null!; [PluginService] public static IGameInteropProvider GameInteropProvider { get; private set; } = null!; } \ No newline at end of file diff --git a/AetherBags/System.cs b/AetherBags/System.cs index 05dee40..0821faa 100644 --- a/AetherBags/System.cs +++ b/AetherBags/System.cs @@ -1,11 +1,15 @@ using AetherBags.Addons; using AetherBags.Configuration; +using AetherBags.IPC; namespace AetherBags; public static class System { public static AddonInventoryWindow AddonInventoryWindow { get; set; } = null!; + public static AddonSaddleBagWindow AddonSaddleBagWindow { get; set; } = null!; + public static AddonRetainerWindow AddonRetainerWindow { get; set; } = null!; public static AddonConfigurationWindow AddonConfigurationWindow { get; set; } = null!; + public static IPCService IPC { get; set; } = null!; public static SystemConfiguration Config { get; set; } = null!; } \ No newline at end of file diff --git a/KamiToolKit b/KamiToolKit index 2122482..1d838e8 160000 --- a/KamiToolKit +++ b/KamiToolKit @@ -1 +1 @@ -Subproject commit 2122482f0dd453a74227965b4f0a6868866e21c1 +Subproject commit 1d838e8bfa973a88389318c88e0a24e136976253