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.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()
+25 -15
View File
@@ -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<InventoryCategoryNode>
{
@@ -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()
+4 -1
View File
@@ -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<InventoryCategoryNode>
{
@@ -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
};
+2 -11
View File
@@ -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),
};
}
+4 -4
View File
@@ -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":
@@ -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,
+24 -16
View File
@@ -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);
}
*/
}
+4 -3
View File
@@ -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);
}
@@ -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<InventoryMappedLocation, InventoryMappedLocation> VisualLocationMap = new();
private static readonly Dictionary<int, Dictionary<InventoryMappedLocation, InventoryMappedLocation>> 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);
}
}
@@ -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 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
@@ -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<ulong, AggregatedItem> aggByKey)
=> ScanInventories(inventoryManager, stackMode, aggByKey, InventorySourceType.MainBags);
public static void ScanInventories(
InventoryManager* inventoryManager,
FFXIVClientStructs.FFXIV.Client.Game.InventoryManager* inventoryManager,
InventoryStackMode stackMode,
Dictionary<ulong, AggregatedItem> 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);
+3 -3
View File
@@ -14,7 +14,7 @@ namespace AetherBags.Inventory.State;
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, ItemInfo> 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);
@@ -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();
+1 -1
View File
@@ -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);
+1
View File
@@ -94,4 +94,5 @@ public class ColorInputRow : HorizontalListNode
public Action<Vector4>? OnColorConfirmed { get; set; }
public Action<Vector4>? OnColorCanceled { 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;
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()
@@ -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<InventoryStackMode>(selected, out var parsed))
{
config.StackMode = parsed;
System.AddonInventoryWindow.ManualRefresh();
InventoryOrchestrator.RefreshAll(updateMaps: true);
}
}
};
@@ -29,6 +29,4 @@ public sealed class GeneralScrollingAreaNode : ScrollingAreaNode<VerticalListNod
};
ContentNode.AddNode(_debugCheckboxNode);
}
private void RefreshInventory() => System.AddonInventoryWindow.ManualRefresh();
}
@@ -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);
@@ -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);
@@ -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");
}
}
}