Fixed Retainer/SaddleBag drag
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -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,33 +31,81 @@ public static unsafe class InventoryContextState
|
||||
|
||||
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;
|
||||
if (entry == null) continue;
|
||||
|
||||
int realPage = entry->Page;
|
||||
var realContainer = (InventoryType)((int)baseInventoryType + entry->Page);
|
||||
int realSlot = entry->Slot;
|
||||
|
||||
int visualPage = 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];
|
||||
if (itemData.IconId == 0) continue;
|
||||
|
||||
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
|
||||
{
|
||||
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
|
||||
=> _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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// 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)
|
||||
// KTK clears node.Payload before invoking this, so setting it manually again
|
||||
var nodePayload = new DragDropPayload
|
||||
{
|
||||
Services.Logger. Warning($"[OnPayload] Could not resolve source from payload");
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
Type = DragDropType.Item,
|
||||
Int1 = targetItemInfo.VisualLocation.Container,
|
||||
Int2 = targetItemInfo.VisualLocation.Slot,
|
||||
ReferenceIndex = (short)(targetItemInfo.Item.Container.GetInventoryStartIndex + targetItemInfo.VisualLocation.Slot)
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Services.Logger.Debug($"[OnPayload] Moving {sourceLocation} -> {targetLocation}");
|
||||
var sourceCopy = acceptedPayload;
|
||||
var targetCopy = nodePayload;
|
||||
|
||||
InventoryMoveHelper.MoveItem(
|
||||
sourceLocation.Container, sourceLocation.Slot,
|
||||
targetLocation.Container, targetLocation.Slot
|
||||
);
|
||||
InventoryMoveHelper.HandleItemMovePayload(sourceCopy, targetCopy);
|
||||
OnRefreshRequested?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Services.Logger.Error(ex, "[OnPayload] Error handling payload acceptance");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user