Fixed Retainer/SaddleBag drag

This commit is contained in:
Zeffuro
2025-12-31 20:50:46 +01:00
parent 1dec5b3183
commit b6084fb7e5
22 changed files with 280 additions and 161 deletions
@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using AetherBags.Configuration; using AetherBags.Configuration;
using AetherBags.Inventory;
using AetherBags.Nodes.Configuration.Category; using AetherBags.Nodes.Configuration.Category;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit; using KamiToolKit;
@@ -118,7 +119,7 @@ public class AddonCategoryConfigurationWindow : NativeAddon
listNode.AddOption(newWrapper); listNode.AddOption(newWrapper);
RefreshSelectionList(); RefreshSelectionList();
System.AddonInventoryWindow.ManualRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
} }
private void OnRemoveCategory(CategoryWrapper categoryWrapper) private void OnRemoveCategory(CategoryWrapper categoryWrapper)
@@ -134,7 +135,7 @@ public class AddonCategoryConfigurationWindow : NativeAddon
{ {
OnOptionChanged(null); OnOptionChanged(null);
} }
System.AddonInventoryWindow.ManualRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
} }
private void RefreshSelectionList() private void RefreshSelectionList()
+25 -15
View File
@@ -1,15 +1,16 @@
using System. Numerics; using System.Numerics;
using AetherBags.Inventory;
using AetherBags.Inventory.State; using AetherBags.Inventory.State;
using AetherBags. Nodes. Input; using AetherBags.Nodes.Input;
using AetherBags. Nodes. Inventory; using AetherBags.Nodes.Inventory;
using AetherBags. Nodes.Layout; using AetherBags.Nodes.Layout;
using Dalamud.Game. Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game. Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs. FFXIV. Component. GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes; using KamiToolKit.Classes;
using KamiToolKit. Nodes; using KamiToolKit.Nodes;
namespace AetherBags.Addons; namespace AetherBags.Addons;
@@ -25,12 +26,14 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
protected override bool HasFooter => false; protected override bool HasFooter => false;
protected override bool HasSlotCounter => true; protected override bool HasSlotCounter => true;
private readonly Vector3 _tintColor = new(8f / 255f, -8f / 255f, -4f / 255f);
protected override float MinWindowWidth => 400; protected override float MinWindowWidth => 400;
protected override float MaxWindowWidth => 700; protected override float MaxWindowWidth => 700;
protected override void OnSetup(AtkUnitBase* addon) protected override void OnSetup(AtkUnitBase* addon)
{ {
WindowNode?.AddColor = new Vector3(8f / 255f, -8f / 255f, -4f / 255f); WindowNode?.AddColor = _tintColor;
CategoriesNode = new WrappingGridNode<InventoryCategoryNode> CategoriesNode = new WrappingGridNode<InventoryCategoryNode>
{ {
@@ -43,7 +46,7 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
}; };
CategoriesNode.AttachNode(this); 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; var header = addon->WindowHeaderCollisionNode;
@@ -61,7 +64,7 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
Size = size, Size = size,
OnInputReceived = _ => RefreshCategoriesCore(autosize: false), OnInputReceived = _ => RefreshCategoriesCore(autosize: false),
}; };
SearchInputNode. AttachNode(this); SearchInputNode.AttachNode(this);
SettingsButtonNode = new CircleButtonNode SettingsButtonNode = new CircleButtonNode
{ {
@@ -70,9 +73,8 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
Icon = ButtonIcon.GearCog, Icon = ButtonIcon.GearCog,
OnClick = System.AddonConfigurationWindow.Toggle OnClick = System.AddonConfigurationWindow.Toggle
}; };
SettingsButtonNode. AttachNode(this); SettingsButtonNode.AttachNode(this);
// Retainer name display
_retainerNameNode = new TextNode _retainerNameNode = new TextNode
{ {
Position = new Vector2(8f, 0), Position = new Vector2(8f, 0),
@@ -88,6 +90,7 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
_entrustDuplicatesButton = new TextButtonNode _entrustDuplicatesButton = new TextButtonNode
{ {
Size = new Vector2(120, 28), Size = new Vector2(120, 28),
AddColor = _tintColor,
String = "Entrust Duplicates", String = "Entrust Duplicates",
OnClick = OnEntrustDuplicates, OnClick = OnEntrustDuplicates,
}; };
@@ -137,7 +140,7 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
float footerY = contentPos.Y + contentSize.Y - FooterHeight + 4f; 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 buttonWidth = _entrustDuplicatesButton.Width;
float buttonX = contentPos.X + (contentSize.X - buttonWidth) / 2f; float buttonX = contentPos.X + (contentSize.X - buttonWidth) / 2f;
@@ -158,7 +161,14 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
RefreshCategoriesCore(doAutosize); RefreshCategoriesCore(doAutosize);
} }
base. OnUpdate(addon); base.OnUpdate(addon);
}
protected override void OnShow(AtkUnitBase* addon)
{
base.OnShow(addon);
InventoryOrchestrator.RefreshAll(updateMaps: true);
} }
private void OnEntrustDuplicates() private void OnEntrustDuplicates()
+4 -1
View File
@@ -19,12 +19,14 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase
protected override bool HasFooter => false; protected override bool HasFooter => false;
protected override bool HasSlotCounter => true; protected override bool HasSlotCounter => true;
private readonly Vector3 _tintColor = new (-16f / 255f, -4f / 255f, 8f / 255f);
protected override float MinWindowWidth => 400; protected override float MinWindowWidth => 400;
protected override float MaxWindowWidth => 600; protected override float MaxWindowWidth => 600;
protected override void OnSetup(AtkUnitBase* addon) protected override void OnSetup(AtkUnitBase* addon)
{ {
WindowNode?.AddColor = new Vector3(-16f / 255f, -4f / 255f, 8f / 255f); WindowNode?.AddColor = _tintColor;
CategoriesNode = new WrappingGridNode<InventoryCategoryNode> CategoriesNode = new WrappingGridNode<InventoryCategoryNode>
{ {
@@ -61,6 +63,7 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase
{ {
Position = new Vector2(headerW - 48f, y), Position = new Vector2(headerW - 48f, y),
Size = new Vector2(28f), Size = new Vector2(28f),
AddColor = _tintColor,
Icon = ButtonIcon.GearCog, Icon = ButtonIcon.GearCog,
OnClick = System.AddonConfigurationWindow.Toggle OnClick = System.AddonConfigurationWindow.Toggle
}; };
+2 -11
View File
@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using AetherBags.Inventory;
using AetherBags.Inventory.Categories; using AetherBags.Inventory.Categories;
using AetherBags.Inventory.State; using AetherBags.Inventory.State;
using AetherBags.Nodes.Input; 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() public void RefreshFromLifecycle()
{ {
if (!_isSetupComplete) return; if (!_isSetupComplete) return;
@@ -144,7 +135,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon
{ {
Size = ContentSize with { Y = 120 }, Size = ContentSize with { Y = 120 },
OnRefreshRequested = ManualRefresh, OnRefreshRequested = ManualRefresh,
OnDragEnd = RefreshAllInventoryWindows, OnDragEnd = () => InventoryOrchestrator.RefreshAll(updateMaps: true),
}; };
} }
+4 -4
View File
@@ -59,7 +59,7 @@ public class CommandHandler : IDisposable
break; break;
case "refresh": case "refresh":
System.AddonInventoryWindow.ManualRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
PrintChat("Inventory refreshed."); PrintChat("Inventory refreshed.");
break; break;
@@ -69,7 +69,7 @@ public class CommandHandler : IDisposable
case "import-sk": case "import-sk":
ImportExportResetHelper.TryImportSortaKindaFromClipboard(true); ImportExportResetHelper.TryImportSortaKindaFromClipboard(true);
System.AddonInventoryWindow.ManualRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
break; break;
case "export": case "export":
@@ -78,12 +78,12 @@ public class CommandHandler : IDisposable
case "import": case "import":
ImportExportResetHelper.TryImportConfigFromClipboard(); ImportExportResetHelper.TryImportConfigFromClipboard();
System.AddonInventoryWindow.ManualRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
break; break;
case "reset": case "reset":
ImportExportResetHelper.TryResetConfig(); ImportExportResetHelper.TryResetConfig();
System.AddonInventoryWindow.ManualRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
break; break;
case "count": case "count":
@@ -1,6 +1,7 @@
using AetherBags.Inventory; using AetherBags.Inventory;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using InventoryManager = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager;
namespace AetherBags.Extensions; namespace AetherBags.Extensions;
@@ -107,17 +108,17 @@ public static unsafe class InventoryTypeExtensions
}; };
public int GetInventoryStartIndex => inventoryType switch { public int GetInventoryStartIndex => inventoryType switch {
InventoryType.Inventory2 => inventoryType.GetInventorySorter->ItemsPerPage, InventoryType.Inventory2 => inventoryType.UIPageSize,
InventoryType.Inventory3 => inventoryType.GetInventorySorter->ItemsPerPage * 2, InventoryType.Inventory3 => inventoryType.UIPageSize * 2,
InventoryType.Inventory4 => inventoryType.GetInventorySorter->ItemsPerPage * 3, InventoryType.Inventory4 => inventoryType.UIPageSize * 3,
InventoryType.SaddleBag2 => inventoryType.GetInventorySorter->ItemsPerPage, InventoryType.SaddleBag2 => inventoryType.UIPageSize,
InventoryType.PremiumSaddleBag2 => inventoryType.GetInventorySorter->ItemsPerPage, InventoryType.PremiumSaddleBag2 => inventoryType.UIPageSize,
InventoryType.RetainerPage2 => inventoryType.GetInventorySorter->ItemsPerPage, InventoryType.RetainerPage2 => inventoryType.UIPageSize,
InventoryType.RetainerPage3 => inventoryType.GetInventorySorter->ItemsPerPage * 2, InventoryType.RetainerPage3 => inventoryType.UIPageSize * 2,
InventoryType.RetainerPage4 => inventoryType.GetInventorySorter->ItemsPerPage * 3, InventoryType.RetainerPage4 => inventoryType.UIPageSize * 3,
InventoryType.RetainerPage5 => inventoryType.GetInventorySorter->ItemsPerPage * 4, InventoryType.RetainerPage5 => inventoryType.UIPageSize * 4,
InventoryType.RetainerPage6 => inventoryType.GetInventorySorter->ItemsPerPage * 5, InventoryType.RetainerPage6 => inventoryType.UIPageSize * 5,
InventoryType.RetainerPage7 => inventoryType.GetInventorySorter->ItemsPerPage * 6, InventoryType.RetainerPage7 => inventoryType.UIPageSize * 6,
_ => 0, _ => 0,
}; };
@@ -156,6 +157,14 @@ public static unsafe class InventoryTypeExtensions
InventoryType.RetainerPage6 or InventoryType.RetainerPage6 or
InventoryType.RetainerPage7; 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 public int ContainerGroup => inventoryType switch
{ {
_ when inventoryType.IsMainInventory => 1, _ when inventoryType.IsMainInventory => 1,
+24 -16
View File
@@ -1,4 +1,10 @@
using System;
using FFXIVClientStructs.FFXIV.Client.Game; 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; namespace AetherBags. Helpers;
@@ -12,33 +18,35 @@ public static unsafe class InventoryMoveHelper
Services.Framework.RunOnFrameworkThread(System.AddonInventoryWindow.ManualRefresh); Services.Framework.RunOnFrameworkThread(System.AddonInventoryWindow.ManualRefresh);
} }
/* public static void HandleItemMovePayload(DragDropPayload source, DragDropPayload target)
private static void MoveItemViaAgent(InventoryType sourceInventory, ushort sourceSlot, InventoryType destInventory, ushort destSlot)
{ {
uint sourceContainerId = sourceInventory.AgentItemContainerId; uint srcContainer = (uint)source.Int1;
uint destContainerId = destInventory.AgentItemContainerId; uint dstContainer = (uint)target.Int1;
if (sourceContainerId == 0 || destContainerId == 0) uint srcSlot = (uint)source.Int2;
{ uint dstSlot = (uint)target.Int2;
Services.Logger.Warning($"[MoveItemViaAgent] Invalid container IDs: src={sourceContainerId}, dst={destContainerId}");
return;
}
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]; var atkValues = stackalloc AtkValue[4];
for (var i = 0; i < 4; i++) for (var i = 0; i < 4; i++)
atkValues[i]. Type = ValueType.UInt; {
atkValues[i].Type = ValueType.UInt;
}
atkValues[0].SetUInt(sourceContainerId); atkValues[0].UInt = srcContainer;
atkValues[1].SetUInt(sourceSlot); atkValues[1].UInt = srcSlot;
atkValues[2].SetUInt(destContainerId); atkValues[2].UInt = dstContainer;
atkValues[3].SetUInt(destSlot); atkValues[3].UInt = dstSlot;
var retVal = stackalloc AtkValue[1]; var retVal = stackalloc AtkValue[1];
RaptureAtkModule* atkModule = RaptureAtkModule.Instance(); RaptureAtkModule* atkModule = RaptureAtkModule.Instance();
atkModule->HandleItemMove(retVal, atkValues, 4); atkModule->HandleItemMove(retVal, atkValues, 4);
} }
*/
} }
+4 -3
View File
@@ -93,10 +93,11 @@ public sealed unsafe class InventoryHooks : IDisposable
ushort dstSlot, ushort dstSlot,
bool unk) bool unk)
{ {
InventoryItem* sourceItem = InventoryManager.Instance()->GetInventorySlot(srcType, srcSlot); //InventoryItem* sourceItem = InventoryManager.Instance()->GetInventorySlot(srcType, srcSlot);
InventoryItem* destItem = InventoryManager.Instance()->GetInventorySlot(dstType, dstSlot); //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); return _moveItemSlotHook!.Original(manager, srcType, srcSlot, dstType, dstSlot, unk);
} }
@@ -10,16 +10,20 @@ public static unsafe class InventoryContextState
{ {
private static readonly HashSet<(int page, int slot)> EligibleSlots = new(); private static readonly HashSet<(int page, int slot)> EligibleSlots = new();
private static readonly HashSet<(InventoryType container, int slot)> BlockedSlots = new(); private static readonly HashSet<(InventoryType container, int slot)> BlockedSlots = new();
// map from real (containerId, slot) -> visual (containerId, slot)
private static readonly Dictionary<InventoryMappedLocation, InventoryMappedLocation> VisualLocationMap = new(); private static readonly Dictionary<InventoryMappedLocation, InventoryMappedLocation> VisualLocationMap = new();
private static readonly Dictionary<int, Dictionary<InventoryMappedLocation, InventoryMappedLocation>> GroupedLocationMaps = new();
private static uint _lastContextId; private static uint _lastContextId;
public static void RefreshMaps() public static void RefreshMaps()
{ {
EligibleSlots.Clear(); EligibleSlots.Clear();
VisualLocationMap.Clear(); VisualLocationMap.Clear();
GroupedLocationMaps.Clear();
var sorter = ItemOrderModule.Instance()->InventorySorter; var itemOrderModule = ItemOrderModule.Instance();
if (sorter == null) return; if (itemOrderModule == null) return;
var agentInventory = AgentInventory.Instance(); var agentInventory = AgentInventory.Instance();
bool hasContext = agentInventory != null && agentInventory->OpenTitleId != 0; bool hasContext = agentInventory != null && agentInventory->OpenTitleId != 0;
@@ -27,33 +31,81 @@ public static unsafe class InventoryContextState
var invArray = hasContext ? InventoryNumberArray.Instance() : null; var invArray = hasContext ? InventoryNumberArray.Instance() : null;
int itemsPerPage = sorter->ItemsPerPage; // Helper local to process any sorter
void ProcessSorter(ItemOrderModuleSorter* sorter)
{
if (sorter == null) return;
for (int displayIdx = 0; displayIdx < 140; displayIdx++) // 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; var entry = sorter->Items[displayIdx].Value;
if (entry == null) continue; if (entry == null) continue;
int realPage = entry->Page; var realContainer = (InventoryType)((int)baseInventoryType + entry->Page);
int realSlot = entry->Slot; int realSlot = entry->Slot;
int visualPage = displayIdx / itemsPerPage; int visualPage = displayIdx / itemsPerPage;
int visualSlot = displayIdx % itemsPerPage; int visualSlot = displayIdx % itemsPerPage;
int visualContainerId = 48 + visualPage; int visualContainerId = baseAgentId + visualPage;
VisualLocationMap[new InventoryMappedLocation(realPage, realSlot)] = new InventoryMappedLocation(visualContainerId, visualSlot); var realKey = new InventoryMappedLocation((int)realContainer, realSlot);
var visualValue = new InventoryMappedLocation(visualContainerId, visualSlot);
if (hasContext && invArray != null) VisualLocationMap[realKey] = visualValue;
if (hasContext && invArray != null && baseInventoryType.IsMainInventory)
{ {
var itemData = invArray->Items[displayIdx]; var itemData = invArray->Items[displayIdx];
if (itemData.IconId == 0) continue; if (itemData.IconId != 0)
{
bool eligible = itemData.ItemFlags.MirageFlag == 0; bool eligible = itemData.ItemFlags.MirageFlag == 0;
if (eligible) 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
{ {
EligibleSlots.Add((realPage, realSlot)); var activeRetainerSorter = itemOrderModule->GetActiveRetainerSorter();
} ProcessSorter(activeRetainerSorter);
} }
catch
{
// GetActiveRetainerSorter is a member function — guard just in case
} }
} }
@@ -85,6 +137,20 @@ public static unsafe class InventoryContextState
public static bool HasActiveContext public static bool HasActiveContext
=> _lastContextId != 0; => _lastContextId != 0;
public static InventoryMappedLocation GetVisualLocation(int page, int slot) public static InventoryMappedLocation GetVisualLocation(InventoryType realContainer, int slot)
=> VisualLocationMap.TryGetValue(new InventoryMappedLocation(page, slot), out var result) ? result : new InventoryMappedLocation(48 + page, 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);
}
} }
@@ -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();
});
}
}
+2 -4
View File
@@ -58,15 +58,13 @@ public sealed class ItemInfo : IEquatable<ItemInfo>
public bool IsHq => Item.Flags.HasFlag(InventoryItem.ItemFlags.HighQuality); public bool IsHq => Item.Flags.HasFlag(InventoryItem.ItemFlags.HighQuality);
public bool IsDesynthesizable => Row.Desynth > 0; 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 IsGlamourable => Row.IsGlamorous;
public bool IsSpiritbonded => Item.SpiritbondOrCollectability >= 10000; // 100% = 10000 public bool IsSpiritbonded => Item.SpiritbondOrCollectability >= 10000; // 100% = 10000
private string Description => _description ??= Row.Description.ToString(); private string Description => _description ??= Row.Description.ToString();
public InventoryMappedLocation VisualLocation => public InventoryMappedLocation VisualLocation => InventoryContextState.GetVisualLocation(Item.Container, Item.Slot);
IsMainInventory ? InventoryContextState.GetVisualLocation(InventoryPage, Item.Slot)
: new InventoryMappedLocation((int)Item.Container.AgentItemContainerId, Item.Slot);
public int InventoryPage => Item.Container switch public int InventoryPage => Item.Container switch
@@ -49,13 +49,13 @@ public static unsafe class InventoryScanner
// Backwards compatible TODO: Remove // Backwards compatible TODO: Remove
public static void ScanBags( public static void ScanBags(
InventoryManager* inventoryManager, FFXIVClientStructs.FFXIV.Client.Game.InventoryManager* inventoryManager,
InventoryStackMode stackMode, InventoryStackMode stackMode,
Dictionary<ulong, AggregatedItem> aggByKey) Dictionary<ulong, AggregatedItem> aggByKey)
=> ScanInventories(inventoryManager, stackMode, aggByKey, InventorySourceType.MainBags); => ScanInventories(inventoryManager, stackMode, aggByKey, InventorySourceType.MainBags);
public static void ScanInventories( public static void ScanInventories(
InventoryManager* inventoryManager, FFXIVClientStructs.FFXIV.Client.Game.InventoryManager* inventoryManager,
InventoryStackMode stackMode, InventoryStackMode stackMode,
Dictionary<ulong, AggregatedItem> aggByKey, Dictionary<ulong, AggregatedItem> aggByKey,
InventorySourceType source) InventorySourceType source)
@@ -173,7 +173,7 @@ public static unsafe class InventoryScanner
} }
public static InventoryContainer* GetInventoryContainer(InventoryType inventoryType) public static InventoryContainer* GetInventoryContainer(InventoryType inventoryType)
=> InventoryManager.Instance()->GetInventoryContainer(inventoryType); => FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance()->GetInventoryContainer(inventoryType);
// Backwards compability TODO: Remove // Backwards compability TODO: Remove
public static string GetEmptyItemSlotsString() public static string GetEmptyItemSlotsString()
@@ -184,7 +184,7 @@ public static unsafe class InventoryScanner
int total = InventorySourceDefinitions.GetTotalSlots(source); int total = InventorySourceDefinitions.GetTotalSlots(source);
uint empty = source switch uint empty = source switch
{ {
InventorySourceType.MainBags => InventoryManager.Instance()->GetEmptySlotsInBag(), InventorySourceType.MainBags => FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance()->GetEmptySlotsInBag(),
InventorySourceType.SaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.SaddleBag), InventorySourceType.SaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.SaddleBag),
InventorySourceType.PremiumSaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.PremiumSaddleBag), InventorySourceType.PremiumSaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.PremiumSaddleBag),
InventorySourceType.AllSaddleBags => GetEmptySlotsInContainer(InventorySourceDefinitions.AllSaddleBags), InventorySourceType.AllSaddleBags => GetEmptySlotsInContainer(InventorySourceDefinitions.AllSaddleBags),
@@ -198,7 +198,7 @@ public static unsafe class InventoryScanner
private static uint GetEmptySlotsInContainer(InventoryType[] inventories) private static uint GetEmptySlotsInContainer(InventoryType[] inventories)
{ {
uint empty = 0; uint empty = 0;
var inventoryManager = InventoryManager.Instance(); var inventoryManager = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance();
foreach (var inv in inventories) foreach (var inv in inventories)
{ {
var container = inventoryManager->GetInventoryContainer(inv); var container = inventoryManager->GetInventoryContainer(inv);
+3 -3
View File
@@ -14,7 +14,7 @@ namespace AetherBags.Inventory.State;
public static unsafe class InventoryState public static unsafe class InventoryState
{ {
public static IReadOnlyList<InventoryType> StandardInventories => InventoryScanner.StandardInventories; private static IReadOnlyList<InventoryType> StandardInventories => InventoryScanner.StandardInventories;
private static readonly Dictionary<ulong, AggregatedItem> AggByKey = new(capacity: 512); private static readonly Dictionary<ulong, AggregatedItem> AggByKey = new(capacity: 512);
private static readonly Dictionary<ulong, ItemInfo> ItemInfoByKey = new(capacity: 512); private static readonly Dictionary<ulong, ItemInfo> ItemInfoByKey = new(capacity: 512);
@@ -34,7 +34,7 @@ public static unsafe class InventoryState
public static void RefreshFromGame() public static void RefreshFromGame()
{ {
InventoryManager* inventoryManager = InventoryManager.Instance(); FFXIVClientStructs.FFXIV.Client.Game.InventoryManager* inventoryManager = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance();
if (inventoryManager == null) if (inventoryManager == null)
{ {
ClearAll(); ClearAll();
@@ -114,7 +114,7 @@ public static unsafe class InventoryState
totalQuantity += kvp.Value.ItemCount; totalQuantity += kvp.Value.ItemCount;
} }
uint emptySlots = InventoryManager.Instance()->GetEmptySlotsInBag(); uint emptySlots = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance()->GetEmptySlotsInBag();
const int totalSlots = 140; const int totalSlots = 140;
var categories = GetInventoryItemCategories(string.Empty); var categories = GetInventoryItemCategories(string.Empty);
@@ -25,7 +25,7 @@ public abstract class InventoryStateBase
public virtual unsafe void RefreshFromGame() public virtual unsafe void RefreshFromGame()
{ {
InventoryManager* inventoryManager = InventoryManager.Instance(); FFXIVClientStructs.FFXIV.Client.Game.InventoryManager* inventoryManager = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance();
if (inventoryManager == null) if (inventoryManager == null)
{ {
ClearAll(); ClearAll();
+1 -1
View File
@@ -54,7 +54,7 @@ public class RetainerState : InventoryStateBase
{ {
if (!IsRetainerActive) return false; if (!IsRetainerActive) return false;
var inventoryManager = InventoryManager.Instance(); var inventoryManager = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance();
if (inventoryManager == null) return false; if (inventoryManager == null) return false;
var container = inventoryManager->GetInventoryContainer(InventoryType.RetainerPage1); var container = inventoryManager->GetInventoryContainer(InventoryType.RetainerPage1);
+1
View File
@@ -94,4 +94,5 @@ public class ColorInputRow : HorizontalListNode
public Action<Vector4>? OnColorConfirmed { get; set; } public Action<Vector4>? OnColorConfirmed { get; set; }
public Action<Vector4>? OnColorCanceled { get; set; } public Action<Vector4>? OnColorCanceled { get; set; }
public Action<Vector4>? OnColorChange { get; set; } public Action<Vector4>? OnColorChange { get; set; }
public Action<Vector4>? OnColorPreviewed { get; set; }
} }
@@ -169,6 +169,11 @@ public sealed class CategoryDefinitionConfigurationNode : VerticalListNode
CategoryDefinition.Color = color; CategoryDefinition.Color = color;
NotifyChanged(); NotifyChanged();
}, },
OnColorPreviewed = color =>
{
CategoryDefinition.Color = color;
NotifyChanged();
}
}; };
AddNode(_colorInputNode); AddNode(_colorInputNode);
@@ -342,7 +347,7 @@ public sealed class CategoryDefinitionConfigurationNode : VerticalListNode
private static void NotifyChanged() private static void NotifyChanged()
{ {
System.AddonInventoryWindow.ManualRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
} }
private void NotifyCategoryPropertyChanged() private void NotifyCategoryPropertyChanged()
@@ -2,6 +2,7 @@ using System;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using AetherBags.Configuration; using AetherBags.Configuration;
using AetherBags.Inventory;
using AetherBags.Nodes.Input; using AetherBags.Nodes.Input;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Nodes; using KamiToolKit.Nodes;
@@ -82,7 +83,7 @@ internal sealed class FunctionalConfigurationNode : TabbedVerticalListNode
if (Enum.TryParse<InventoryStackMode>(selected, out var parsed)) if (Enum.TryParse<InventoryStackMode>(selected, out var parsed))
{ {
config.StackMode = parsed; config.StackMode = parsed;
System.AddonInventoryWindow.ManualRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
} }
} }
}; };
@@ -29,6 +29,4 @@ public sealed class GeneralScrollingAreaNode : ScrollingAreaNode<VerticalListNod
}; };
ContentNode.AddNode(_debugCheckboxNode); ContentNode.AddNode(_debugCheckboxNode);
} }
private void RefreshInventory() => System.AddonInventoryWindow.ManualRefresh();
} }
@@ -2,6 +2,7 @@
using KamiToolKit.Classes.Timelines; using KamiToolKit.Classes.Timelines;
using KamiToolKit.Nodes; using KamiToolKit.Nodes;
using System.Numerics; using System.Numerics;
using AetherBags.Inventory;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
namespace AetherBags.Nodes.Configuration.Layout; namespace AetherBags.Nodes.Configuration.Layout;
@@ -33,7 +34,7 @@ internal sealed class CompactLookaheadNode : SimpleComponentNode
OnValueUpdate = value => OnValueUpdate = value =>
{ {
config.CompactLookahead = value; config.CompactLookahead = value;
System.AddonInventoryWindow.ManualRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
} }
}; };
CompactLookahead.AttachNode(this); CompactLookahead.AttachNode(this);
@@ -1,5 +1,6 @@
using System.Numerics; using System.Numerics;
using AetherBags.Configuration; using AetherBags.Configuration;
using AetherBags.Inventory;
using KamiToolKit.Nodes; using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.Layout; namespace AetherBags.Nodes.Configuration.Layout;
@@ -32,7 +33,7 @@ internal class LayoutConfigurationNode : TabbedVerticalListNode
OnClick = isChecked => OnClick = isChecked =>
{ {
config.ShowCategoryItemCount = isChecked; config.ShowCategoryItemCount = isChecked;
System.AddonInventoryWindow.ManualRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
} }
}; };
AddNode(showCategoryItemAmountCheckboxNode); AddNode(showCategoryItemAmountCheckboxNode);
@@ -49,7 +50,7 @@ internal class LayoutConfigurationNode : TabbedVerticalListNode
_preferLargestFitCheckboxNode.IsEnabled = isChecked; _preferLargestFitCheckboxNode.IsEnabled = isChecked;
_useStableInsertCheckboxNode.IsEnabled = isChecked; _useStableInsertCheckboxNode.IsEnabled = isChecked;
_compactLookaheadNode.CompactLookahead.IsEnabled = isChecked; _compactLookaheadNode.CompactLookahead.IsEnabled = isChecked;
System.AddonInventoryWindow.ManualRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
} }
}; };
AddNode(compactPackingCheckboxNode); AddNode(compactPackingCheckboxNode);
@@ -65,7 +66,7 @@ internal class LayoutConfigurationNode : TabbedVerticalListNode
OnClick = isChecked => OnClick = isChecked =>
{ {
config.CompactPreferLargestFit = isChecked; config.CompactPreferLargestFit = isChecked;
System.AddonInventoryWindow.ManualRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
} }
}; };
AddNode(_preferLargestFitCheckboxNode); AddNode(_preferLargestFitCheckboxNode);
@@ -80,7 +81,7 @@ internal class LayoutConfigurationNode : TabbedVerticalListNode
OnClick = isChecked => OnClick = isChecked =>
{ {
config.CompactStableInsert = isChecked; config.CompactStableInsert = isChecked;
System.AddonInventoryWindow.ManualRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
} }
}; };
AddNode(_useStableInsertCheckboxNode); AddNode(_useStableInsertCheckboxNode);
@@ -1,6 +1,7 @@
using System; using System;
using System.Numerics; using System.Numerics;
using AetherBags.Helpers; using AetherBags.Helpers;
using AetherBags.Hooks;
using AetherBags.Inventory; using AetherBags.Inventory;
using AetherBags.Inventory.Categories; using AetherBags.Inventory.Categories;
using AetherBags.Inventory.Items; using AetherBags.Inventory.Items;
@@ -223,10 +224,19 @@ public class InventoryCategoryNode : SimpleComponentNode
InventoryItem item = data.Item; InventoryItem item = data.Item;
InventoryMappedLocation visualLocation = data.VisualLocation; InventoryMappedLocation visualLocation = data.VisualLocation;
int startIndex = item.Container.GetInventoryStartIndex; var visualInvType = InventoryType.GetInventoryTypeFromContainerId(visualLocation.Container);
int absoluteIndex = startIndex + visualLocation.Slot; 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; bool isSlotBlocked = item.Container.IsMainInventory && data.IsSlotBlocked;
float alpha = !isSlotBlocked && data.IsEligibleForContext ? 1.0f : 0.4f; float alpha = !isSlotBlocked && data.IsEligibleForContext ? 1.0f : 0.4f;
@@ -238,17 +248,11 @@ public class InventoryCategoryNode : SimpleComponentNode
IconId = item.IconId, IconId = item.IconId,
AcceptedType = DragDropType.Item, AcceptedType = DragDropType.Item,
IsDraggable = !data.IsSlotBlocked, IsDraggable = !data.IsSlotBlocked,
Payload = new DragDropPayload Payload = nodePayload,
{
Type = DragDropType.Item,
Int1 = useVisualLocation ? visualLocation.Container : (int)item.Container,
Int2 = useVisualLocation ? visualLocation.Slot : item.Slot,
ReferenceIndex = (short)absoluteIndex,
},
IsClickable = true, IsClickable = true,
OnDiscard = node => OnDiscard(node, data), OnDiscard = node => OnDiscard(node, data),
OnEnd = _ => OnDragEnd?.Invoke(), OnEnd = _ => OnDragEnd?.Invoke(),
OnPayloadAccepted = (node, payload) => OnPayloadAccepted(node, payload, data), OnPayloadAccepted = (node, acceptedPayload) => OnPayloadAccepted(node, acceptedPayload, data),
OnRollOver = node => OnRollOver = node =>
{ {
BeginHeaderHover(); BeginHeaderHover();
@@ -271,56 +275,37 @@ public class InventoryCategoryNode : SimpleComponentNode
AgentInventoryContext.Instance()->DiscardItem(item.Item.GetLinkedItem(), item.Item.Container, item.Item.Slot, addonId); 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; try
if (!payload.IsValidInventoryPayload)
{ {
Services.Logger.Warning($"[OnPayload] Invalid payload type: {payload.Type}"); // KTK clears node.Payload before invoking this, so setting it manually again
return; var nodePayload = new DragDropPayload
}
// Debug:log raw payload values
Services.Logger.Debug($"[OnPayload] Raw payload: Type={payload.Type} Int1={payload.Int1} Int2={payload.Int2} Ref={payload.ReferenceIndex}");
InventoryLocation sourceLocation = payload.InventoryLocation;
if (!sourceLocation.IsValid)
{ {
Services.Logger. Warning($"[OnPayload] Could not resolve source from payload"); Type = DragDropType.Item,
return; Int1 = targetItemInfo.VisualLocation.Container,
} Int2 = targetItemInfo.VisualLocation.Slot,
ReferenceIndex = (short)(targetItemInfo.Item.Container.GetInventoryStartIndex + targetItemInfo.VisualLocation.Slot)
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;
OnRefreshRequested?.Invoke();
return;
}; };
if (!sourceLocation.Container.IsLoaded || !targetLocation.Container.IsLoaded) 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}");
if (!acceptedPayload.IsValidInventoryPayload || !nodePayload.IsValidInventoryPayload)
{ {
Services.Logger.Debug($"[OnPayload] Source or target container is not loaded; cannot move"); Services.Logger.Warning($"[OnPayload] Invalid payload type: Accepted={acceptedPayload.Type} Node={nodePayload.Type}");
return; return;
} }
Services.Logger.Debug($"[OnPayload] Moving {sourceLocation} -> {targetLocation}"); var sourceCopy = acceptedPayload;
var targetCopy = nodePayload;
InventoryMoveHelper.MoveItem( InventoryMoveHelper.HandleItemMovePayload(sourceCopy, targetCopy);
sourceLocation.Container, sourceLocation.Slot,
targetLocation.Container, targetLocation.Slot
);
OnRefreshRequested?.Invoke(); OnRefreshRequested?.Invoke();
} }
catch (Exception ex)
{
Services.Logger.Error(ex, "[OnPayload] Error handling payload acceptance");
}
}
} }