Fix moving retainers, add InventoryNotificationNode

This commit is contained in:
Zeffuro
2025-12-27 03:54:47 +01:00
parent fc12b41f33
commit 9c68149d74
7 changed files with 260 additions and 105 deletions
@@ -69,6 +69,14 @@ public class AddonInventoryWindow : NativeAddon
float x = headerX + (headerW - size.X) * 0.5f; float x = headerX + (headerW - size.X) * 0.5f;
float y = headerY + (headerH - size.Y) * 0.5f; float y = headerY + (headerH - size.Y) * 0.5f;
InventoryNotificationNode notificationNode = new InventoryNotificationNode
{
Position = new Vector2(WindowNode!.X - 4f, WindowNode!.Y - 32f),
Size = new Vector2(headerW, 28f),
};
notificationNode.AttachNode(this);
//notificationNode.NotificationType = InventoryNotificationType.SaddleBag;
_searchInputNode = new TextInputWithHintNode _searchInputNode = new TextInputWithHintNode
{ {
Position = new Vector2(x, y), Position = new Vector2(x, y),
@@ -1,4 +1,6 @@
using AetherBags.Interop; using AetherBags.Interop;
using AetherBags.Inventory;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes; using KamiToolKit.Classes;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
@@ -6,7 +8,7 @@ using Lumina.Text;
namespace AetherBags.Extensions; namespace AetherBags.Extensions;
// TODO: Remove this when CS is merged into Dalamud. // TODO: Remove FixedInterface when CS is merged into Dalamud.
public static unsafe class DragDropPayloadExtensions public static unsafe class DragDropPayloadExtensions
{ {
public static DragDropPayload FromFixedInterface(AtkDragDropInterface* dragDropInterface) public static DragDropPayload FromFixedInterface(AtkDragDropInterface* dragDropInterface)
@@ -54,4 +56,59 @@ public static unsafe class DragDropPayloadExtensions
} }
} }
} }
extension(DragDropPayload payload)
{
public bool IsValidInventoryPayload =>
payload.Type is DragDropType.Inventory_Item
or DragDropType.Inventory_Crystal
or DragDropType.RemoteInventory_Item
or DragDropType.Item;
public InventoryLocation InventoryLocation
{
get
{
if (!payload.IsValidInventoryPayload) return default;
if (payload.Type == DragDropType.Inventory_Item)
{
return new InventoryLocation((InventoryType)payload.Int1, (ushort)payload.Int2);
}
int containerId = payload.Int1;
int uiSlot = payload.Int2;
InventoryType sourceContainer = InventoryType.GetInventoryTypeFromContainerId(containerId);
if (sourceContainer == 0)
return new InventoryLocation(0, 0);
// Retainers have special handling: UI has 5 tabs × 35 slots, data has 7 pages × 25 slots
if (sourceContainer.IsRetainer)
{
// Container IDs 52-56 = UI tabs 0-4
int uiTabIndex = containerId - 52;
// Convert to global data index
int globalDataIndex = (uiTabIndex * 35) + uiSlot;
// Calculate data page and slot
int dataPage = globalDataIndex / 25;
int dataSlot = globalDataIndex % 25;
InventoryType dataContainer = InventoryType.RetainerPage1 + (uint)dataPage;
// Now resolve through sorter for the actual storage location
var (realContainer, realSlot) = dataContainer.GetRealItemLocation(dataSlot);
return new InventoryLocation(realContainer, realSlot);
}
// For non-retainers, use the standard resolution
var (container, slot) = sourceContainer.GetRealItemLocation(uiSlot);
return new InventoryLocation(container, slot);
}
}
}
} }
@@ -1,4 +1,5 @@
using System; using System;
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;
@@ -17,6 +18,7 @@ public static unsafe class InventoryTypeExtensions
InventoryType.Inventory2 => 49, InventoryType.Inventory2 => 49,
InventoryType.Inventory3 => 50, InventoryType.Inventory3 => 50,
InventoryType.Inventory4 => 51, InventoryType.Inventory4 => 51,
// It's possible that these are actually UI IDs
InventoryType.RetainerPage1 => 52, InventoryType.RetainerPage1 => 52,
InventoryType.RetainerPage2 => 53, InventoryType.RetainerPage2 => 53,
InventoryType.RetainerPage3 => 54, InventoryType.RetainerPage3 => 54,
@@ -171,21 +173,21 @@ public static unsafe class InventoryTypeExtensions
/// Resolves the real container and slot for this inventory type using ItemOrderModule. /// Resolves the real container and slot for this inventory type using ItemOrderModule.
/// For sorted inventories, the visual slot differs from the actual storage slot. /// For sorted inventories, the visual slot differs from the actual storage slot.
/// </summary> /// </summary>
public (InventoryType Container, ushort Slot) GetRealItemLocation(int visualSlot) public InventoryLocation GetRealItemLocation(int visualSlot)
{ {
var sorter = inventoryType.GetInventorySorter; var sorter = inventoryType.GetInventorySorter;
if (sorter == null) if (sorter == null)
return (inventoryType, (ushort)visualSlot); return new InventoryLocation(inventoryType, (ushort)visualSlot);
int startIndex = inventoryType.GetInventoryStartIndex; int startIndex = inventoryType.GetInventoryStartIndex;
int sorterIndex = startIndex + visualSlot; int sorterIndex = startIndex + visualSlot;
if (sorterIndex < 0 || sorterIndex >= sorter->Items.LongCount) if (sorterIndex < 0 || sorterIndex >= sorter->Items.LongCount)
return (inventoryType, (ushort)visualSlot); return new InventoryLocation(inventoryType, (ushort)visualSlot);
var entry = sorter->Items[sorterIndex].Value; var entry = sorter->Items[sorterIndex].Value;
if (entry == null) if (entry == null)
return (inventoryType, (ushort)visualSlot); return new InventoryLocation(inventoryType, (ushort)visualSlot);
InventoryType baseType = inventoryType switch InventoryType baseType = inventoryType switch
{ {
@@ -200,45 +202,7 @@ public static unsafe class InventoryTypeExtensions
InventoryType realContainer = baseType + entry->Page; InventoryType realContainer = baseType + entry->Page;
ushort realSlot = entry->Slot; ushort realSlot = entry->Slot;
return (realContainer, realSlot); return new InventoryLocation(realContainer, realSlot);
}
public int GetVisualSlotFromReal(int realSlot)
{
var sorter = inventoryType.GetInventorySorter;
if (sorter == null)
return realSlot;
int startIndex = inventoryType.GetInventoryStartIndex;
long itemCount = sorter->Items.LongCount;
// Search through the sorter to find which visual index maps to this real slot
for (int visualIdx = 0; visualIdx < itemCount; visualIdx++)
{
var entry = sorter->Items[visualIdx]. Value;
if (entry == null) continue;
// Calculate what container this entry belongs to
InventoryType baseType = inventoryType switch
{
_ when inventoryType.IsMainInventory => InventoryType. Inventory1,
_ when inventoryType.IsSaddleBag => inventoryType is InventoryType.SaddleBag1 or InventoryType.SaddleBag2
? InventoryType. SaddleBag1
: InventoryType.PremiumSaddleBag1,
_ when inventoryType.IsRetainer => InventoryType.RetainerPage1,
_ => inventoryType,
};
InventoryType entryContainer = baseType + entry->Page;
if (entryContainer == inventoryType && entry->Slot == realSlot)
{
// Found it! Return visual index relative to the container's start
return visualIdx - startIndex;
}
}
return realSlot; // Fallback
} }
} }
} }
+5 -12
View File
@@ -8,24 +8,16 @@ namespace AetherBags. Helpers;
public static unsafe class InventoryMoveHelper public static unsafe class InventoryMoveHelper
{ {
// Requires the visual UI slots instead of actual slots.
public static void MoveItem(InventoryType sourceContainer, ushort sourceSlot, InventoryType destContainer, ushort destSlot) public static void MoveItem(InventoryType sourceContainer, ushort sourceSlot, InventoryType destContainer, ushort destSlot)
{ {
Services.Logger.Debug($"[MoveItem] {sourceContainer}@{sourceSlot} -> {destContainer}@{destSlot}"); Services.Logger.Debug($"[MoveItem] {sourceContainer}@{sourceSlot} -> {destContainer}@{destSlot}");
InventoryManager.Instance()->MoveItemSlot(sourceContainer, sourceSlot, destContainer, destSlot, true); InventoryManager.Instance()->MoveItemSlot(sourceContainer, sourceSlot, destContainer, destSlot, true);
System.AddonInventoryWindow.ManualInventoryRefresh(); Services.Framework.DelayTicks(2);
return; Services.Framework.RunOnFrameworkThread(System.AddonInventoryWindow.ManualInventoryRefresh);
bool isCrossContainerMove = ! sourceContainer.IsSameContainerGroup(destContainer);
if (isCrossContainerMove)
{
MoveItemViaAgent(sourceContainer, sourceSlot, destContainer, destSlot);
}
else
{
InventoryManager.Instance()->MoveItemSlot(sourceContainer, sourceSlot, destContainer, destSlot, true);
}
} }
/*
private static void MoveItemViaAgent(InventoryType sourceInventory, ushort sourceSlot, InventoryType destInventory, ushort destSlot) private static void MoveItemViaAgent(InventoryType sourceInventory, ushort sourceSlot, InventoryType destInventory, ushort destSlot)
{ {
uint sourceContainerId = sourceInventory.AgentItemContainerId; uint sourceContainerId = sourceInventory.AgentItemContainerId;
@@ -53,4 +45,5 @@ public static unsafe class InventoryMoveHelper
RaptureAtkModule* atkModule = RaptureAtkModule.Instance(); RaptureAtkModule* atkModule = RaptureAtkModule.Instance();
atkModule->HandleItemMove(retVal, atkValues, 4); atkModule->HandleItemMove(retVal, atkValues, 4);
} }
*/
} }
+12
View File
@@ -0,0 +1,12 @@
using FFXIVClientStructs.FFXIV.Client.Game;
namespace AetherBags.Inventory;
public readonly record struct InventoryLocation(InventoryType Container, ushort Slot)
{
public static readonly InventoryLocation Invalid = new(0, 0);
public bool IsValid => Container != 0;
public override string ToString() => $"{Container}@{Slot}";
}
@@ -243,65 +243,24 @@ public class InventoryCategoryNode : SimpleComponentNode
}; };
} }
private void OnPayloadAccepted(DragDropNode node, DragDropPayload payload, ItemInfo targetItemInfo) private void OnPayloadAccepted(DragDropNode _, DragDropPayload payload, ItemInfo targetItemInfo)
{ {
if (payload.Type != DragDropType.Item && payload.Type != DragDropType.Inventory_Item) Services.Logger.Debug($"[OnPayload] Received payload of type {payload.Type}, Int1={payload.Int1}, Int2={payload.Int2}, RefIndex={payload.ReferenceIndex}, Text={payload.Text}");
if (!payload.IsValidInventoryPayload)
return; return;
Services.Logger.Debug($"[OnPayload] Received payload of type {payload.Type}, Int1={payload.Int1}, Int2={payload.Int2}, RefIndex={payload.ReferenceIndex}, Text={payload.Text}"); InventoryLocation sourceLocation = payload.InventoryLocation;
var (sourceContainer, sourceSlot) = ResolveSourceFromPayload(payload);
if (sourceContainer == 0) if (!sourceLocation.IsValid)
{ {
Services.Logger.Warning($"[OnPayload] Could not resolve source from payload"); Services.Logger.Warning($"[OnPayload] Could not resolve source from payload");
return; return;
} }
InventoryType targetContainer = targetItemInfo.Item.Container; InventoryLocation targetLocation = new InventoryLocation(targetItemInfo.Item.Container, (ushort)targetItemInfo.Item.Slot);
ushort targetSlot = (ushort)targetItemInfo.Item.Slot;
Services.Logger.Debug($"[OnPayload] Moving {sourceContainer}@{sourceSlot} -> {targetContainer}@{targetSlot}"); Services.Logger.Debug($"[OnPayload] Moving {sourceLocation.ToString()} -> {targetLocation.ToString()}");
InventoryMoveHelper.MoveItem(sourceContainer, sourceSlot, targetContainer, targetSlot); InventoryMoveHelper.MoveItem(sourceLocation.Container, sourceLocation.Slot, targetLocation.Container, targetLocation.Slot);
}
private static (InventoryType Container, ushort Slot) ResolveSourceFromPayload(DragDropPayload payload)
{
if (payload.Type == DragDropType.Inventory_Item)
{
return ((InventoryType)payload.Int1, (ushort)payload.Int2);
}
int containerId = payload.Int1;
int uiSlot = payload.Int2;
InventoryType sourceContainer = InventoryType.GetInventoryTypeFromContainerId(containerId);
if (sourceContainer == 0)
return (0, 0);
// Retainers have special handling: UI has 5 tabs × 35 slots, data has 7 pages × 25 slots
if (sourceContainer. IsRetainer)
{
// Container IDs 52-56 = UI tabs 0-4
int uiTabIndex = containerId - 52;
// Convert to global data index
int globalDataIndex = (uiTabIndex * 35) + uiSlot;
// Calculate data page and slot
int dataPage = globalDataIndex / 25;
int dataSlot = globalDataIndex % 25;
InventoryType dataContainer = InventoryType.RetainerPage1 + (uint)dataPage;
// Now resolve through sorter for the actual storage location
var (realContainer, realSlot) = dataContainer.GetRealItemLocation(dataSlot);
return (realContainer, realSlot);
}
// For non-retainers, use the standard resolution
var (realContainerOther, realSlotOther) = sourceContainer.GetRealItemLocation(uiSlot);
return (realContainerOther, realSlotOther);
} }
} }
@@ -0,0 +1,162 @@
using System.Collections.Generic;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit;
using KamiToolKit.Classes;
using KamiToolKit.Classes.Timelines;
using KamiToolKit.Nodes;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Lumina.Text;
using Lumina.Text.ReadOnly;
namespace AetherBags.Nodes.Inventory;
public sealed class InventoryNotificationNode : SimpleComponentNode
{
private readonly SimpleNineGridNode glowNode;
private readonly TextNode titleTextNode;
private readonly TextNode messageTextNode;
private Dictionary<InventoryNotificationType, InventoryNotificationInfo> notificationCache = null!;
public InventoryNotificationNode()
{
PopulateNotificationCache();
AddTimeline(ParentLabels);
glowNode = new SimpleNineGridNode {
TexturePath = "ui/uld/Inventory.tex",
TextureSize = new Vector2(56.0f, 56.0f),
TextureCoordinates = new Vector2(88.0f, 0.0f),
TopOffset = 10,
BottomOffset = 10,
LeftOffset = 26,
RightOffset = 26,
};
glowNode.AttachNode(this);
glowNode.AddTimeline(GlowKeyFrames);
titleTextNode = new TextNode
{
Position = new Vector2(0, 10f),
FontType = FontType.MiedingerMed,
FontSize = 18,
TextColor = ColorHelper.GetColor(50),
TextOutlineColor = ColorHelper.GetColor(37),
TextFlags = TextFlags.Edge,
AlignmentType = AlignmentType.Center,
};
titleTextNode.AttachNode(this);
titleTextNode.AddTimeline(TextKeyFrames);
messageTextNode = new TextNode
{
Position = new Vector2(0, -10f),
FontType = FontType.Axis,
FontSize = 14,
TextColor = ColorHelper.GetColor(50),
TextOutlineColor = ColorHelper.GetColor(37),
TextFlags = TextFlags.Edge,
AlignmentType = AlignmentType.Center,
};
messageTextNode.AttachNode(this);
messageTextNode.AddTimeline(TextKeyFrames);
Timeline?.PlayAnimation(17);
}
protected override void OnSizeChanged()
{
base.OnSizeChanged();
glowNode.Size = Size with { Y = 40 };
titleTextNode.Size = Size with { Y = 20 };
messageTextNode.Size = Size with { Y = 16 };
}
private void PopulateNotificationCache()
{
ExcelSheet<Addon> addonSheet = Services.DataManager.GetExcelSheet<Addon>();
notificationCache = new Dictionary<InventoryNotificationType, InventoryNotificationInfo>
{
{
InventoryNotificationType.SaddleBag,
new InventoryNotificationInfo(addonSheet.GetRow(891).Text, addonSheet.GetRow(892).Text)
},
{
InventoryNotificationType.RetainerEntrust,
new InventoryNotificationInfo(addonSheet.GetRow(910).Text, addonSheet.GetRow(3573).Text)
},
{
InventoryNotificationType.RetainerEquip,
new InventoryNotificationInfo(addonSheet.GetRow(910).Text, addonSheet.GetRow(3585).Text)
}
};
}
public InventoryNotificationType NotificationType
{
get;
set
{
field = value;
if (value == InventoryNotificationType.None)
{
titleTextNode.String = string.Empty;
messageTextNode.String = string.Empty;
Timeline?.PlayAnimation(17); // Hide
}
else if (notificationCache.TryGetValue(value, out var texts))
{
titleTextNode.SeString = texts.Title;
messageTextNode.SeString = texts.Message;
Timeline?.PlayAnimation(101); // Show
}
}
} = InventoryNotificationType.None;
// Future Zeff, this always goes on a parent
private Timeline ParentLabels => new TimelineBuilder()
.BeginFrameSet(1, 59)
.AddLabel(1, 17, AtkTimelineJumpBehavior.PlayOnce, 0)
.AddLabel(10, 101, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(25, 102, AtkTimelineJumpBehavior.Start, 0)
.AddLabel(59, 0, AtkTimelineJumpBehavior.LoopForever, 102)
.EndFrameSet()
.Build();
// Future Zeff, this always goes on a child
private Timeline GlowKeyFrames => new TimelineBuilder().BeginFrameSet(15, 59)
.AddFrame(10, scale: new Vector2(1.4f, 1.0f), alpha: 0, addColor: new Vector3(128, 128, 128))
.AddFrame(15, scale: new Vector2(1.0f, 1.0f), alpha: 255, addColor: new Vector3(128, 128, 128))
.AddFrame(21, scale: new Vector2(1.0f, 1.0f), alpha: 255, addColor: new Vector3(0, 0, 0))
.AddFrame(40, scale: new Vector2(1.0f, 1.0f), alpha: 255, addColor: new Vector3(0, 0, 0))
.AddFrame(46, scale: new Vector2(1.0f, 1.0f), alpha: 255, addColor: new Vector3(10, 10, 10))
.AddFrame(59, scale: new Vector2(1.0f, 1.0f), alpha: 255, addColor: new Vector3(0, 0, 0))
.EndFrameSet()
.Build();
// Future Zeff, this always goes on a child
private Timeline TextKeyFrames => new TimelineBuilder().BeginFrameSet(15, 59)
.AddFrame(15, alpha: 0, addColor: new Vector3(128, 128, 128))
.AddFrame(18, alpha: 255, addColor: new Vector3(64, 64, 64))
.AddFrame(25, alpha: 255, addColor: new Vector3(0, 0, 0))
.AddFrame(40, alpha: 255, addColor: new Vector3(0, 0, 0))
.AddFrame(46, alpha: 255, addColor: new Vector3(64, 64, 64))
.AddFrame(59, alpha: 255, addColor: new Vector3(0, 0, 0))
.EndFrameSet()
.Build();
private record InventoryNotificationInfo(ReadOnlySeString Title, ReadOnlySeString Message);
}
public enum InventoryNotificationType
{
None,
SaddleBag,
RetainerEntrust,
RetainerEquip
}