From 0ba5c0698e7d2395c5c21ef8f193008d01e92240 Mon Sep 17 00:00:00 2001 From: Zeffuro Date: Wed, 31 Dec 2025 21:45:41 +0100 Subject: [PATCH] Add dragging between windows on background --- AetherBags/Addons/AddonInventoryWindow.cs | 2 + AetherBags/Addons/AddonRetainerWindow.cs | 2 + AetherBags/Addons/AddonSaddleBagWindow.cs | 2 + AetherBags/Addons/InventoryAddonBase.cs | 64 +++++++++++++++++++ .../Context/InventoryContextState.cs | 9 +-- AetherBags/Inventory/Items/ItemInfo.cs | 32 ++++++++-- .../Inventory/Scanning/InventoryScanner.cs | 26 +++++++- .../Inventory/Scanning/InventorySource.cs | 20 ++++-- .../CurrencyGeneralConfigurationNode.cs | 6 +- AetherBags/Nodes/Currency/CurrencyNode.cs | 2 +- KamiToolKit | 2 +- 11 files changed, 144 insertions(+), 23 deletions(-) diff --git a/AetherBags/Addons/AddonInventoryWindow.cs b/AetherBags/Addons/AddonInventoryWindow.cs index 82c2313..f894f64 100644 --- a/AetherBags/Addons/AddonInventoryWindow.cs +++ b/AetherBags/Addons/AddonInventoryWindow.cs @@ -22,6 +22,8 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase protected override void OnSetup(AtkUnitBase* addon) { + InitializeBackgroundDropTarget(); + CategoriesNode = new WrappingGridNode { Position = ContentStartPosition, diff --git a/AetherBags/Addons/AddonRetainerWindow.cs b/AetherBags/Addons/AddonRetainerWindow.cs index b235e4c..6db4251 100644 --- a/AetherBags/Addons/AddonRetainerWindow.cs +++ b/AetherBags/Addons/AddonRetainerWindow.cs @@ -33,6 +33,8 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase protected override void OnSetup(AtkUnitBase* addon) { + InitializeBackgroundDropTarget(); + WindowNode?.AddColor = _tintColor; CategoriesNode = new WrappingGridNode diff --git a/AetherBags/Addons/AddonSaddleBagWindow.cs b/AetherBags/Addons/AddonSaddleBagWindow.cs index 9e9eae6..58d5481 100644 --- a/AetherBags/Addons/AddonSaddleBagWindow.cs +++ b/AetherBags/Addons/AddonSaddleBagWindow.cs @@ -26,6 +26,8 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase protected override void OnSetup(AtkUnitBase* addon) { + InitializeBackgroundDropTarget(); + WindowNode?.AddColor = _tintColor; CategoriesNode = new WrappingGridNode diff --git a/AetherBags/Addons/InventoryAddonBase.cs b/AetherBags/Addons/InventoryAddonBase.cs index 8c38dd8..f628f7b 100644 --- a/AetherBags/Addons/InventoryAddonBase.cs +++ b/AetherBags/Addons/InventoryAddonBase.cs @@ -1,14 +1,19 @@ using System; using System.Collections.Generic; using System.Numerics; +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.Nodes; namespace AetherBags.Addons; @@ -19,6 +24,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon protected readonly InventoryCategoryPinCoordinator PinCoordinator = new(); protected readonly HashSet HoverSubscribed = new(); + protected DragDropNode BackgroundDropTarget = null!; protected WrappingGridNode CategoriesNode = null!; protected TextInputWithHintNode SearchInputNode = null!; protected InventoryFooterNode FooterNode = null!; @@ -129,6 +135,26 @@ public abstract unsafe class InventoryAddonBase : NativeAddon } } + 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 @@ -139,6 +165,38 @@ public abstract unsafe class InventoryAddonBase : NativeAddon }; } + 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.Debug($"[BackgroundDrop] Target: {emptyLocation} -> Visual: {visualLocation} (Ref: {absoluteIndex})"); + + InventoryMoveHelper.HandleItemMovePayload(acceptedPayload, targetPayload); + + ManualRefresh(); + } + protected void WireHoverHandlers() { var nodes = CategoriesNode.Nodes; @@ -233,6 +291,12 @@ public abstract unsafe class InventoryAddonBase : NativeAddon protected void ResizeWindow(float width, float height, bool recalcLayout) { SetWindowSize(width, height); + + if (BackgroundDropTarget != null) + { + BackgroundDropTarget.Size = ContentSize; + } + LayoutContent(); if (recalcLayout) diff --git a/AetherBags/Inventory/Context/InventoryContextState.cs b/AetherBags/Inventory/Context/InventoryContextState.cs index 47e6181..1f1e8d6 100644 --- a/AetherBags/Inventory/Context/InventoryContextState.cs +++ b/AetherBags/Inventory/Context/InventoryContextState.cs @@ -10,12 +10,16 @@ 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(); - // map from real (containerId, slot) -> visual (containerId, slot) + 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(); @@ -134,9 +138,6 @@ public static unsafe class InventoryContextState public static bool IsSlotBlocked(InventoryType container, int slot) => BlockedSlots.Contains((container, slot)); - public static bool HasActiveContext - => _lastContextId != 0; - public static InventoryMappedLocation GetVisualLocation(InventoryType realContainer, int slot) { var key = new InventoryMappedLocation((int)realContainer, slot); diff --git a/AetherBags/Inventory/Items/ItemInfo.cs b/AetherBags/Inventory/Items/ItemInfo.cs index f58f25e..8a0ffe6 100644 --- a/AetherBags/Inventory/Items/ItemInfo.cs +++ b/AetherBags/Inventory/Items/ItemInfo.cs @@ -82,13 +82,35 @@ public sealed class ItemInfo : IEquatable { get { - if (!InventoryContextState.HasActiveContext) - return true; + uint contextId = InventoryContextState.ActiveContextId; + if (contextId == 0) return true; - if (!IsMainInventory) - return true; + bool isRetainerContext = contextId == 4; + bool isSaddlebagContext = contextId == 29; + bool isMainContext = !isRetainerContext && isSaddlebagContext == false; - return InventoryContextState.IsEligible(InventoryPage, Item.Slot); + if (IsMainInventory) + { + if (!isMainContext) return true; + + return InventoryContextState.IsEligible(InventoryPage, Item.Slot); + } + + if (Item.Container.IsRetainer) + { + // ...but the context isn't for Retainers, don't dim it. + if (!isRetainerContext) + return true; + } + + // 3. If we are looking at a Saddlebag item... + if (Item.Container.IsSaddleBag) + { + if (!isSaddlebagContext) + return true; + } + + return true; } } diff --git a/AetherBags/Inventory/Scanning/InventoryScanner.cs b/AetherBags/Inventory/Scanning/InventoryScanner.cs index d4a819a..43a8858 100644 --- a/AetherBags/Inventory/Scanning/InventoryScanner.cs +++ b/AetherBags/Inventory/Scanning/InventoryScanner.cs @@ -173,7 +173,27 @@ public static unsafe class InventoryScanner } public static InventoryContainer* GetInventoryContainer(InventoryType inventoryType) - => FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance()->GetInventoryContainer(inventoryType); + => InventoryManager.Instance()->GetInventoryContainer(inventoryType); + + public static InventoryLocation GetFirstEmptySlot(InventorySourceType source) + { + var manager = InventoryManager.Instance(); + var containers = InventorySourceDefinitions.GetContainersForSource(source); + + 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; + } // Backwards compability TODO: Remove public static string GetEmptyItemSlotsString() @@ -184,7 +204,7 @@ public static unsafe class InventoryScanner int total = InventorySourceDefinitions.GetTotalSlots(source); uint empty = source switch { - InventorySourceType.MainBags => FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance()->GetEmptySlotsInBag(), + InventorySourceType.MainBags => InventoryManager.Instance()->GetEmptySlotsInBag(), InventorySourceType.SaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.SaddleBag), InventorySourceType.PremiumSaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.PremiumSaddleBag), InventorySourceType.AllSaddleBags => GetEmptySlotsInContainer(InventorySourceDefinitions.AllSaddleBags), @@ -198,7 +218,7 @@ public static unsafe class InventoryScanner private static uint GetEmptySlotsInContainer(InventoryType[] inventories) { uint empty = 0; - var inventoryManager = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance(); + var inventoryManager = InventoryManager.Instance(); foreach (var inv in inventories) { var container = inventoryManager->GetInventoryContainer(inv); diff --git a/AetherBags/Inventory/Scanning/InventorySource.cs b/AetherBags/Inventory/Scanning/InventorySource.cs index 213d6ee..027d641 100644 --- a/AetherBags/Inventory/Scanning/InventorySource.cs +++ b/AetherBags/Inventory/Scanning/InventorySource.cs @@ -60,13 +60,23 @@ public static class InventorySourceDefinitions _ => 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 => 175, // 7 * 25 + InventorySourceType.MainBags => 140, // 4 * 35 + InventorySourceType.SaddleBag => 70, // 2 * 35 + InventorySourceType.PremiumSaddleBag => 70, // 2 * 35 + InventorySourceType.AllSaddleBags => 140, // 2 * 35 + InventorySourceType.Retainer => Retainer.Length * 35, // 7 * 25 _ => 140, }; } \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Currency/CurrencyGeneralConfigurationNode.cs b/AetherBags/Nodes/Configuration/Currency/CurrencyGeneralConfigurationNode.cs index f8c1006..363f90b 100644 --- a/AetherBags/Nodes/Configuration/Currency/CurrencyGeneralConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/Currency/CurrencyGeneralConfigurationNode.cs @@ -51,8 +51,6 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode }; AddNode(defaultCurrencyColorNode); - AddNode(); - CheckboxNode cappedEnabledCheckbox = new CheckboxNode { Size = Size with { Y = 18 }, @@ -89,7 +87,7 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode { Size = Size with { Y = 18 }, IsVisible = true, - String = "Color Weekly Limit", + String = "Limited Currency Color", IsChecked = config.ColorWhenLimited, OnClick = isChecked => { @@ -103,7 +101,7 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode ColorInputRow limitCurrencyColorNode = new ColorInputRow { - Label = "Limit Currency Color", + Label = "Color Weekly Limit", Size = new Vector2(300, 24), CurrentColor = config.LimitColor, DefaultColor = new CurrencySettings().LimitColor, 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/KamiToolKit b/KamiToolKit index 2122482..ac0f811 160000 --- a/KamiToolKit +++ b/KamiToolKit @@ -1 +1 @@ -Subproject commit 2122482f0dd453a74227965b4f0a6868866e21c1 +Subproject commit ac0f8116f693e5a962306f0b9f917012b7ff556c