diff --git a/AetherBags/Addons/AddonCategoryConfigurationWindow.cs b/AetherBags/Addons/AddonCategoryConfigurationWindow.cs index 3585dab..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; @@ -118,7 +119,7 @@ public class AddonCategoryConfigurationWindow : NativeAddon listNode.AddOption(newWrapper); RefreshSelectionList(); - System.AddonInventoryWindow.ManualRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); } private void OnRemoveCategory(CategoryWrapper categoryWrapper) @@ -134,7 +135,7 @@ public class AddonCategoryConfigurationWindow : NativeAddon { OnOptionChanged(null); } - System.AddonInventoryWindow.ManualRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); } private void RefreshSelectionList() diff --git a/AetherBags/Addons/AddonRetainerWindow.cs b/AetherBags/Addons/AddonRetainerWindow.cs index 6d836f7..b235e4c 100644 --- a/AetherBags/Addons/AddonRetainerWindow.cs +++ b/AetherBags/Addons/AddonRetainerWindow.cs @@ -1,15 +1,16 @@ -using System. Numerics; +using System.Numerics; +using AetherBags.Inventory; 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 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 FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Classes; -using KamiToolKit. Nodes; +using KamiToolKit.Nodes; namespace AetherBags.Addons; @@ -25,12 +26,14 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase 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; protected override void OnSetup(AtkUnitBase* addon) { - WindowNode?.AddColor = new Vector3(8f / 255f, -8f / 255f, -4f / 255f); + WindowNode?.AddColor = _tintColor; CategoriesNode = new WrappingGridNode { @@ -43,7 +46,7 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase }; CategoriesNode.AttachNode(this); - var size = new Vector2(addon->Size. X / 2.0f, 28.0f); + var size = new Vector2(addon->Size.X / 2.0f, 28.0f); var header = addon->WindowHeaderCollisionNode; @@ -61,7 +64,7 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase Size = size, OnInputReceived = _ => RefreshCategoriesCore(autosize: false), }; - SearchInputNode. AttachNode(this); + SearchInputNode.AttachNode(this); SettingsButtonNode = new CircleButtonNode { @@ -70,9 +73,8 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase Icon = ButtonIcon.GearCog, OnClick = System.AddonConfigurationWindow.Toggle }; - SettingsButtonNode. AttachNode(this); + SettingsButtonNode.AttachNode(this); - // Retainer name display _retainerNameNode = new TextNode { Position = new Vector2(8f, 0), @@ -88,6 +90,7 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase _entrustDuplicatesButton = new TextButtonNode { Size = new Vector2(120, 28), + AddColor = _tintColor, String = "Entrust Duplicates", OnClick = OnEntrustDuplicates, }; @@ -137,7 +140,7 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase float footerY = contentPos.Y + contentSize.Y - FooterHeight + 4f; - _retainerNameNode. Position = new Vector2(contentPos.X + 8f, footerY); + _retainerNameNode.Position = new Vector2(contentPos.X + 8f, footerY); float buttonWidth = _entrustDuplicatesButton.Width; float buttonX = contentPos.X + (contentSize.X - buttonWidth) / 2f; @@ -158,7 +161,14 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase RefreshCategoriesCore(doAutosize); } - base. OnUpdate(addon); + base.OnUpdate(addon); + } + + protected override void OnShow(AtkUnitBase* addon) + { + base.OnShow(addon); + + InventoryOrchestrator.RefreshAll(updateMaps: true); } private void OnEntrustDuplicates() diff --git a/AetherBags/Addons/AddonSaddleBagWindow.cs b/AetherBags/Addons/AddonSaddleBagWindow.cs index f4ecc4d..9e9eae6 100644 --- a/AetherBags/Addons/AddonSaddleBagWindow.cs +++ b/AetherBags/Addons/AddonSaddleBagWindow.cs @@ -19,12 +19,14 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase 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) { - WindowNode?.AddColor = new Vector3(-16f / 255f, -4f / 255f, 8f / 255f); + WindowNode?.AddColor = _tintColor; CategoriesNode = new WrappingGridNode { @@ -61,6 +63,7 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase { Position = new Vector2(headerW - 48f, y), Size = new Vector2(28f), + AddColor = _tintColor, Icon = ButtonIcon.GearCog, OnClick = System.AddonConfigurationWindow.Toggle }; diff --git a/AetherBags/Addons/InventoryAddonBase.cs b/AetherBags/Addons/InventoryAddonBase.cs index 9c4cb02..8c38dd8 100644 --- a/AetherBags/Addons/InventoryAddonBase.cs +++ b/AetherBags/Addons/InventoryAddonBase.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Numerics; +using AetherBags.Inventory; using AetherBags.Inventory.Categories; using AetherBags.Inventory.State; using AetherBags.Nodes.Input; @@ -65,16 +66,6 @@ public abstract unsafe class InventoryAddonBase : NativeAddon } } - protected static void RefreshAllInventoryWindows() - { - Services.Framework.RunOnTick(() => - { - System.AddonInventoryWindow?.ManualRefresh(); - System.AddonSaddleBagWindow?.ManualRefresh(); - System.AddonRetainerWindow?.ManualRefresh(); - }, delayTicks: 2); - } - public void RefreshFromLifecycle() { if (!_isSetupComplete) return; @@ -144,7 +135,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon { Size = ContentSize with { Y = 120 }, OnRefreshRequested = ManualRefresh, - OnDragEnd = RefreshAllInventoryWindows, + OnDragEnd = () => InventoryOrchestrator.RefreshAll(updateMaps: true), }; } diff --git a/AetherBags/Commands/CommandHandler.cs b/AetherBags/Commands/CommandHandler.cs index 921f984..275f84d 100644 --- a/AetherBags/Commands/CommandHandler.cs +++ b/AetherBags/Commands/CommandHandler.cs @@ -59,7 +59,7 @@ public class CommandHandler : IDisposable break; case "refresh": - System.AddonInventoryWindow.ManualRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); PrintChat("Inventory refreshed."); break; @@ -69,7 +69,7 @@ public class CommandHandler : IDisposable case "import-sk": ImportExportResetHelper.TryImportSortaKindaFromClipboard(true); - System.AddonInventoryWindow.ManualRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); break; case "export": @@ -78,12 +78,12 @@ public class CommandHandler : IDisposable case "import": ImportExportResetHelper.TryImportConfigFromClipboard(); - System.AddonInventoryWindow.ManualRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); break; case "reset": ImportExportResetHelper.TryResetConfig(); - System.AddonInventoryWindow.ManualRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); break; case "count": diff --git a/AetherBags/Extensions/InventoryTypeExtensions.cs b/AetherBags/Extensions/InventoryTypeExtensions.cs index 809642c..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, diff --git a/AetherBags/Helpers/InventoryMoveHelper.cs b/AetherBags/Helpers/InventoryMoveHelper.cs index 8a568f1..9792c5c 100644 --- a/AetherBags/Helpers/InventoryMoveHelper.cs +++ b/AetherBags/Helpers/InventoryMoveHelper.cs @@ -1,4 +1,10 @@ +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; @@ -12,33 +18,35 @@ public static unsafe class InventoryMoveHelper 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.Debug($"[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..a0fe60e 100644 --- a/AetherBags/Hooks/InventoryHook.cs +++ b/AetherBags/Hooks/InventoryHook.cs @@ -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.Debug($"[MoveItemSlot Hook] Moving {srcType}@{srcSlot} -> {dstType}@{dstSlot} I Unk: {unk}"); + //Services.Logger.Debug($"[MoveItemSlot Hook] Moving {srcType}@{srcSlot} ID:{sourceItem->ItemId} -> {dstType}@{dstSlot} ID:{destItem->ItemId} Unk: {unk}"); return _moveItemSlotHook!.Original(manager, srcType, srcSlot, dstType, dstSlot, unk); } diff --git a/AetherBags/Inventory/Context/InventoryContextState.cs b/AetherBags/Inventory/Context/InventoryContextState.cs index 77337da..47e6181 100644 --- a/AetherBags/Inventory/Context/InventoryContextState.cs +++ b/AetherBags/Inventory/Context/InventoryContextState.cs @@ -10,16 +10,20 @@ 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 void RefreshMaps() { EligibleSlots.Clear(); VisualLocationMap.Clear(); + GroupedLocationMaps.Clear(); - var sorter = ItemOrderModule.Instance()->InventorySorter; - if (sorter == null) return; + var itemOrderModule = ItemOrderModule.Instance(); + if (itemOrderModule == null) return; var agentInventory = AgentInventory.Instance(); bool hasContext = agentInventory != null && agentInventory->OpenTitleId != 0; @@ -27,34 +31,82 @@ public static unsafe class InventoryContextState var invArray = hasContext ? InventoryNumberArray.Instance() : null; - int itemsPerPage = sorter->ItemsPerPage; - - for (int displayIdx = 0; displayIdx < 140; displayIdx++) + // Helper local to process any sorter + void ProcessSorter(ItemOrderModuleSorter* sorter) { - var entry = sorter->Items[displayIdx].Value; - if (entry == null) continue; + if (sorter == null) return; - int realPage = entry->Page; - int realSlot = entry->Slot; + // 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; - int visualPage = displayIdx / itemsPerPage; - int visualSlot = displayIdx % itemsPerPage; - int visualContainerId = 48 + visualPage; + // 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; - VisualLocationMap[new InventoryMappedLocation(realPage, realSlot)] = new InventoryMappedLocation(visualContainerId, visualSlot); + var baseAgentId = (int)baseInventoryType.AgentItemContainerId; + if (baseAgentId == 0) return; - if (hasContext && invArray != null) + long count = sorter->Items.LongCount; + for (int displayIdx = 0; displayIdx < count; displayIdx++) { - var itemData = invArray->Items[displayIdx]; - if (itemData.IconId == 0) continue; + var entry = sorter->Items[displayIdx].Value; + if (entry == null) continue; - bool eligible = itemData.ItemFlags.MirageFlag == 0; - if (eligible) + 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) { - EligibleSlots.Add((realPage, realSlot)); + 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() @@ -85,6 +137,20 @@ public static unsafe class InventoryContextState 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); + 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/InventoryOrchestrator.cs b/AetherBags/Inventory/InventoryOrchestrator.cs new file mode 100644 index 0000000..73f9203 --- /dev/null +++ b/AetherBags/Inventory/InventoryOrchestrator.cs @@ -0,0 +1,40 @@ +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) + { + // 1. Update the mapping data (Context menus / Visual slots) + if (updateMaps) + { + InventoryContextState.RefreshMaps(); + InventoryContextState.RefreshBlockedSlots(); + } + + // 2. Fetch the current context (Are we selling? Trading? Talking to a retainer?) + var agent = AgentInventory.Instance(); + var contextId = agent != null ? agent->OpenTitleId : 0; + var notification = NotificationState.GetNotificationInfo(contextId); + + // 3. Trigger UI refreshes + Services.Framework.RunOnTick(() => + { + if (System.AddonInventoryWindow.IsOpen) + { + System.AddonInventoryWindow.SetNotification(notification!); + System.AddonInventoryWindow.ManualRefresh(); + } + + if (System.AddonSaddleBagWindow.IsOpen) + System.AddonSaddleBagWindow.ManualRefresh(); + + if (System.AddonRetainerWindow.IsOpen) + System.AddonRetainerWindow.ManualRefresh(); + }); + } +} \ No newline at end of file diff --git a/AetherBags/Inventory/Items/ItemInfo.cs b/AetherBags/Inventory/Items/ItemInfo.cs index a6286e8..f58f25e 100644 --- a/AetherBags/Inventory/Items/ItemInfo.cs +++ b/AetherBags/Inventory/Items/ItemInfo.cs @@ -58,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 diff --git a/AetherBags/Inventory/Scanning/InventoryScanner.cs b/AetherBags/Inventory/Scanning/InventoryScanner.cs index ac77300..d4a819a 100644 --- a/AetherBags/Inventory/Scanning/InventoryScanner.cs +++ b/AetherBags/Inventory/Scanning/InventoryScanner.cs @@ -49,13 +49,13 @@ public static unsafe class InventoryScanner // Backwards compatible TODO: Remove public static void ScanBags( - InventoryManager* inventoryManager, + FFXIVClientStructs.FFXIV.Client.Game.InventoryManager* inventoryManager, InventoryStackMode stackMode, Dictionary aggByKey) => ScanInventories(inventoryManager, stackMode, aggByKey, InventorySourceType.MainBags); public static void ScanInventories( - InventoryManager* inventoryManager, + FFXIVClientStructs.FFXIV.Client.Game.InventoryManager* inventoryManager, InventoryStackMode stackMode, Dictionary aggByKey, InventorySourceType source) @@ -173,7 +173,7 @@ public static unsafe class InventoryScanner } public static InventoryContainer* GetInventoryContainer(InventoryType inventoryType) - => InventoryManager.Instance()->GetInventoryContainer(inventoryType); + => FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance()->GetInventoryContainer(inventoryType); // Backwards compability TODO: Remove public static string GetEmptyItemSlotsString() @@ -184,7 +184,7 @@ public static unsafe class InventoryScanner int total = InventorySourceDefinitions.GetTotalSlots(source); uint empty = source switch { - InventorySourceType.MainBags => InventoryManager.Instance()->GetEmptySlotsInBag(), + InventorySourceType.MainBags => FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance()->GetEmptySlotsInBag(), InventorySourceType.SaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.SaddleBag), InventorySourceType.PremiumSaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.PremiumSaddleBag), InventorySourceType.AllSaddleBags => GetEmptySlotsInContainer(InventorySourceDefinitions.AllSaddleBags), @@ -198,7 +198,7 @@ public static unsafe class InventoryScanner private static uint GetEmptySlotsInContainer(InventoryType[] inventories) { uint empty = 0; - var inventoryManager = InventoryManager.Instance(); + var inventoryManager = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance(); foreach (var inv in inventories) { var container = inventoryManager->GetInventoryContainer(inv); diff --git a/AetherBags/Inventory/State/InventoryState.cs b/AetherBags/Inventory/State/InventoryState.cs index 9886511..795e9dd 100644 --- a/AetherBags/Inventory/State/InventoryState.cs +++ b/AetherBags/Inventory/State/InventoryState.cs @@ -14,7 +14,7 @@ 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); @@ -34,7 +34,7 @@ public static unsafe class InventoryState public static void RefreshFromGame() { - InventoryManager* inventoryManager = InventoryManager.Instance(); + FFXIVClientStructs.FFXIV.Client.Game.InventoryManager* inventoryManager = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance(); if (inventoryManager == null) { ClearAll(); @@ -114,7 +114,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); diff --git a/AetherBags/Inventory/State/InventoryStateBase.cs b/AetherBags/Inventory/State/InventoryStateBase.cs index 3ecf792..e922bd4 100644 --- a/AetherBags/Inventory/State/InventoryStateBase.cs +++ b/AetherBags/Inventory/State/InventoryStateBase.cs @@ -25,7 +25,7 @@ public abstract class InventoryStateBase public virtual unsafe void RefreshFromGame() { - InventoryManager* inventoryManager = InventoryManager.Instance(); + FFXIVClientStructs.FFXIV.Client.Game.InventoryManager* inventoryManager = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance(); if (inventoryManager == null) { ClearAll(); diff --git a/AetherBags/Inventory/State/RetainerState.cs b/AetherBags/Inventory/State/RetainerState.cs index dfba24e..eabe183 100644 --- a/AetherBags/Inventory/State/RetainerState.cs +++ b/AetherBags/Inventory/State/RetainerState.cs @@ -54,7 +54,7 @@ public class RetainerState : InventoryStateBase { if (!IsRetainerActive) return false; - var inventoryManager = InventoryManager.Instance(); + var inventoryManager = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance(); if (inventoryManager == null) return false; var container = inventoryManager->GetInventoryContainer(InventoryType.RetainerPage1); diff --git a/AetherBags/Nodes/Color/ColorInputRow.cs b/AetherBags/Nodes/Color/ColorInputRow.cs index e4e8535..18e4cb8 100644 --- a/AetherBags/Nodes/Color/ColorInputRow.cs +++ b/AetherBags/Nodes/Color/ColorInputRow.cs @@ -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/CategoryDefinitionConfigurationNode.cs b/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs index a05ceca..c801073 100644 --- a/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs @@ -169,6 +169,11 @@ public sealed class CategoryDefinitionConfigurationNode : VerticalListNode CategoryDefinition.Color = color; NotifyChanged(); }, + OnColorPreviewed = color => + { + CategoryDefinition.Color = color; + NotifyChanged(); + } }; AddNode(_colorInputNode); @@ -342,7 +347,7 @@ public sealed class CategoryDefinitionConfigurationNode : VerticalListNode private static void NotifyChanged() { - System.AddonInventoryWindow.ManualRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); } private void NotifyCategoryPropertyChanged() diff --git a/AetherBags/Nodes/Configuration/General/FunctionalConfigurationNode.cs b/AetherBags/Nodes/Configuration/General/FunctionalConfigurationNode.cs index 2fb89a9..243d4bd 100644 --- a/AetherBags/Nodes/Configuration/General/FunctionalConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/General/FunctionalConfigurationNode.cs @@ -2,6 +2,7 @@ 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; @@ -82,7 +83,7 @@ internal sealed class FunctionalConfigurationNode : TabbedVerticalListNode if (Enum.TryParse(selected, out var parsed)) { config.StackMode = parsed; - System.AddonInventoryWindow.ManualRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); } } }; diff --git a/AetherBags/Nodes/Configuration/General/GeneralScrollingAreaNode.cs b/AetherBags/Nodes/Configuration/General/GeneralScrollingAreaNode.cs index f7f40b9..2ead348 100644 --- a/AetherBags/Nodes/Configuration/General/GeneralScrollingAreaNode.cs +++ b/AetherBags/Nodes/Configuration/General/GeneralScrollingAreaNode.cs @@ -29,6 +29,4 @@ public sealed class GeneralScrollingAreaNode : ScrollingAreaNode System.AddonInventoryWindow.ManualRefresh(); } \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/Layout/CompactLookaheadNode.cs b/AetherBags/Nodes/Configuration/Layout/CompactLookaheadNode.cs index 97d7ed6..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.ManualRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); } }; CompactLookahead.AttachNode(this); diff --git a/AetherBags/Nodes/Configuration/Layout/LayoutConfigurationNode.cs b/AetherBags/Nodes/Configuration/Layout/LayoutConfigurationNode.cs index 7640501..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.ManualRefresh(); + 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.ManualRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); } }; AddNode(compactPackingCheckboxNode); @@ -65,7 +66,7 @@ internal class LayoutConfigurationNode : TabbedVerticalListNode OnClick = isChecked => { config.CompactPreferLargestFit = isChecked; - System.AddonInventoryWindow.ManualRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); } }; AddNode(_preferLargestFitCheckboxNode); @@ -80,7 +81,7 @@ internal class LayoutConfigurationNode : TabbedVerticalListNode OnClick = isChecked => { config.CompactStableInsert = isChecked; - System.AddonInventoryWindow.ManualRefresh(); + InventoryOrchestrator.RefreshAll(updateMaps: true); } }; AddNode(_useStableInsertCheckboxNode); diff --git a/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs index 652ae77..9071b06 100644 --- a/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs +++ b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs @@ -1,6 +1,7 @@ using System; using System.Numerics; using AetherBags.Helpers; +using AetherBags.Hooks; using AetherBags.Inventory; using AetherBags.Inventory.Categories; using AetherBags.Inventory.Items; @@ -223,10 +224,19 @@ public class InventoryCategoryNode : SimpleComponentNode InventoryItem item = data.Item; InventoryMappedLocation visualLocation = data.VisualLocation; - int startIndex = item.Container.GetInventoryStartIndex; - int absoluteIndex = startIndex + visualLocation.Slot; + 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 + }; - bool useVisualLocation = true; bool isSlotBlocked = item.Container.IsMainInventory && data.IsSlotBlocked; float alpha = !isSlotBlocked && data.IsEligibleForContext ? 1.0f : 0.4f; @@ -238,17 +248,11 @@ public class InventoryCategoryNode : SimpleComponentNode IconId = item.IconId, AcceptedType = DragDropType.Item, IsDraggable = !data.IsSlotBlocked, - Payload = new DragDropPayload - { - Type = DragDropType.Item, - Int1 = useVisualLocation ? visualLocation.Container : (int)item.Container, - Int2 = useVisualLocation ? visualLocation.Slot : item.Slot, - ReferenceIndex = (short)absoluteIndex, - }, + Payload = nodePayload, IsClickable = true, OnDiscard = node => OnDiscard(node, data), OnEnd = _ => OnDragEnd?.Invoke(), - OnPayloadAccepted = (node, payload) => OnPayloadAccepted(node, payload, data), + OnPayloadAccepted = (node, acceptedPayload) => OnPayloadAccepted(node, acceptedPayload, data), OnRollOver = node => { BeginHeaderHover(); @@ -271,56 +275,37 @@ public class InventoryCategoryNode : SimpleComponentNode 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) + }; - // Debug:log raw payload values - Services.Logger.Debug($"[OnPayload] Raw payload: Type={payload.Type} Int1={payload.Int1} Int2={payload.Int2} Ref={payload.ReferenceIndex}"); + Services.Logger.Debug($"[OnPayload] ACCEPTED payload: Type={acceptedPayload.Type} Int1={acceptedPayload.Int1} Int2={acceptedPayload.Int2} Ref={acceptedPayload.ReferenceIndex}"); + Services.Logger.Debug($"[OnPayload] NODE payload: Type={nodePayload.Type} Int1={nodePayload.Int1} Int2={nodePayload.Int2} Ref={nodePayload.ReferenceIndex}"); - InventoryLocation sourceLocation = payload.InventoryLocation; + if (!acceptedPayload.IsValidInventoryPayload || !nodePayload.IsValidInventoryPayload) + { + Services.Logger.Warning($"[OnPayload] Invalid payload type: Accepted={acceptedPayload.Type} Node={nodePayload.Type}"); + return; + } - if (!sourceLocation.IsValid) - { - Services.Logger. Warning($"[OnPayload] Could not resolve source from payload"); - return; - } + var sourceCopy = acceptedPayload; + var targetCopy = nodePayload; - InventoryLocation targetLocation = new InventoryLocation( - item.Container, - (ushort)item.Slot - ); - - // Debug: log resolved locations - Services.Logger.Debug($"[OnPayload] Source: {sourceLocation. Container} @ {sourceLocation. Slot}"); - Services.Logger.Debug($"[OnPayload] Target: {targetLocation.Container} @ {targetLocation.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; + InventoryMoveHelper.HandleItemMovePayload(sourceCopy, targetCopy); OnRefreshRequested?.Invoke(); - return; - }; - - if (!sourceLocation.Container.IsLoaded || !targetLocation.Container.IsLoaded) - { - Services.Logger.Debug($"[OnPayload] Source or target container is not loaded; cannot move"); - return; } - - Services.Logger.Debug($"[OnPayload] Moving {sourceLocation} -> {targetLocation}"); - - InventoryMoveHelper.MoveItem( - sourceLocation.Container, sourceLocation.Slot, - targetLocation.Container, targetLocation.Slot - ); - OnRefreshRequested?.Invoke(); + catch (Exception ex) + { + Services.Logger.Error(ex, "[OnPayload] Error handling payload acceptance"); + } } } \ No newline at end of file