diff --git a/AetherBags.sln.DotSettings.user b/AetherBags.sln.DotSettings.user
index f39de2b..6ef2c30 100644
--- a/AetherBags.sln.DotSettings.user
+++ b/AetherBags.sln.DotSettings.user
@@ -1,6 +1,10 @@
+ ForceIncluded
ForceIncluded
+ ForceIncluded
ForceIncluded
ForceIncluded
+ ForceIncluded
ForceIncluded
+ ForceIncluded
ForceIncluded
\ No newline at end of file
diff --git a/AetherBags/Addons/AddonInventoryWindow.cs b/AetherBags/Addons/AddonInventoryWindow.cs
index 2cf5597..6ab3fd7 100644
--- a/AetherBags/Addons/AddonInventoryWindow.cs
+++ b/AetherBags/Addons/AddonInventoryWindow.cs
@@ -9,6 +9,7 @@ using AetherBags.Nodes.Inventory;
using AetherBags.Nodes.Layout;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Game.Gui;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit;
@@ -22,6 +23,7 @@ public class AddonInventoryWindow : NativeAddon
private readonly InventoryCategoryHoverCoordinator _hoverCoordinator = new();
private readonly HashSet _hoverSubscribed = new();
+ private InventoryNotificationNode _notificationNode = null!;
private WrappingGridNode _categoriesNode = null!;
private TextInputWithHintNode _searchInputNode = null!;
private CircleButtonNode _settingsButtonNode = null!;
@@ -69,6 +71,13 @@ public class AddonInventoryWindow : NativeAddon
float x = headerX + (headerW - size.X) * 0.5f;
float y = headerY + (headerH - size.Y) * 0.5f;
+ _notificationNode = new InventoryNotificationNode
+ {
+ Position = new Vector2(WindowNode!.X - 4f, WindowNode!.Y - 32f),
+ Size = new Vector2(headerW, 28f),
+ };
+ _notificationNode.AttachNode(this);
+
_searchInputNode = new TextInputWithHintNode
{
Position = new Vector2(x, y),
@@ -105,7 +114,6 @@ public class AddonInventoryWindow : NativeAddon
base.OnSetup(addon);
}
-
protected override unsafe void OnUpdate(AtkUnitBase* addon)
{
if (_refreshQueued)
@@ -117,6 +125,9 @@ public class AddonInventoryWindow : NativeAddon
RefreshCategoriesCore(doAutosize);
}
+ InventoryNotificationType currentNotificationType = (InventoryNotificationType) AgentInventory.Instance()->OpenTitleId;
+ if(currentNotificationType != _notificationNode.NotificationType) _notificationNode.NotificationType = currentNotificationType;
+
base.OnUpdate(addon);
}
diff --git a/AetherBags/Extensions/DragDropPayloadExtensions.cs b/AetherBags/Extensions/DragDropPayloadExtensions.cs
index 62024dd..a8cd0dd 100644
--- a/AetherBags/Extensions/DragDropPayloadExtensions.cs
+++ b/AetherBags/Extensions/DragDropPayloadExtensions.cs
@@ -1,4 +1,6 @@
using AetherBags.Interop;
+using AetherBags.Inventory;
+using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using Lumina.Text.ReadOnly;
@@ -6,7 +8,7 @@ using Lumina.Text;
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 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);
+ }
+ }
+ }
+
}
\ No newline at end of file
diff --git a/AetherBags/Extensions/InventoryTypeExtensions.cs b/AetherBags/Extensions/InventoryTypeExtensions.cs
index 4c28d8f..df91f0b 100644
--- a/AetherBags/Extensions/InventoryTypeExtensions.cs
+++ b/AetherBags/Extensions/InventoryTypeExtensions.cs
@@ -1,4 +1,5 @@
using System;
+using AetherBags.Inventory;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
@@ -17,6 +18,12 @@ public static unsafe class InventoryTypeExtensions
InventoryType.Inventory2 => 49,
InventoryType.Inventory3 => 50,
InventoryType.Inventory4 => 51,
+ // It's possible that these are actually UI IDs
+ InventoryType.RetainerPage1 => 52,
+ InventoryType.RetainerPage2 => 53,
+ InventoryType.RetainerPage3 => 54,
+ InventoryType.RetainerPage4 => 55,
+ InventoryType.RetainerPage5 => 56,
InventoryType.ArmoryMainHand => 57,
InventoryType.ArmoryHead => 58,
InventoryType.ArmoryBody => 59,
@@ -45,6 +52,11 @@ public static unsafe class InventoryTypeExtensions
49 => InventoryType.Inventory2,
50 => InventoryType.Inventory3,
51 => InventoryType.Inventory4,
+ 52 => InventoryType.RetainerPage1,
+ 53 => InventoryType.RetainerPage2,
+ 54 => InventoryType.RetainerPage3,
+ 55 => InventoryType.RetainerPage4,
+ 56 => InventoryType.RetainerPage5,
57 => InventoryType.ArmoryMainHand,
58 => InventoryType.ArmoryHead,
59 => InventoryType.ArmoryBody,
@@ -85,6 +97,13 @@ public static unsafe class InventoryTypeExtensions
InventoryType.SaddleBag2 => ItemOrderModule.Instance()->SaddleBagSorter,
InventoryType.PremiumSaddleBag1 => ItemOrderModule.Instance()->PremiumSaddleBagSorter,
InventoryType.PremiumSaddleBag2 => ItemOrderModule.Instance()->PremiumSaddleBagSorter,
+ InventoryType.RetainerPage1 => ItemOrderModule.Instance()->GetActiveRetainerSorter(),
+ InventoryType.RetainerPage2 => ItemOrderModule.Instance()->GetActiveRetainerSorter(),
+ InventoryType.RetainerPage3 => ItemOrderModule.Instance()->GetActiveRetainerSorter(),
+ InventoryType.RetainerPage4 => ItemOrderModule.Instance()->GetActiveRetainerSorter(),
+ InventoryType.RetainerPage5 => ItemOrderModule.Instance()->GetActiveRetainerSorter(),
+ InventoryType.RetainerPage6 => ItemOrderModule.Instance()->GetActiveRetainerSorter(),
+ InventoryType.RetainerPage7 => ItemOrderModule.Instance()->GetActiveRetainerSorter(),
_ => null,
};
@@ -94,6 +113,12 @@ public static unsafe class InventoryTypeExtensions
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,
_ => 0,
};
@@ -123,11 +148,21 @@ public static unsafe class InventoryTypeExtensions
InventoryType.ArmoryRings or
InventoryType.ArmorySoulCrystal;
+ public bool IsRetainer => inventoryType is
+ InventoryType.RetainerPage1 or
+ InventoryType.RetainerPage2 or
+ InventoryType.RetainerPage3 or
+ InventoryType.RetainerPage4 or
+ InventoryType.RetainerPage5 or
+ InventoryType.RetainerPage6 or
+ InventoryType.RetainerPage7;
+
public int ContainerGroup => inventoryType switch
{
_ when inventoryType.IsMainInventory => 1,
_ when inventoryType.IsSaddleBag => 2,
_ when inventoryType.IsArmory => 3,
+ _ when inventoryType.IsRetainer => 4,
_ => 0,
};
@@ -138,35 +173,36 @@ public static unsafe class InventoryTypeExtensions
/// Resolves the real container and slot for this inventory type using ItemOrderModule.
/// For sorted inventories, the visual slot differs from the actual storage slot.
///
- public (InventoryType Container, ushort Slot) GetRealItemLocation(int visualSlot)
+ public InventoryLocation GetRealItemLocation(int visualSlot)
{
var sorter = inventoryType.GetInventorySorter;
if (sorter == null)
- return (inventoryType, (ushort)visualSlot);
+ return new InventoryLocation(inventoryType, (ushort)visualSlot);
int startIndex = inventoryType.GetInventoryStartIndex;
int sorterIndex = startIndex + visualSlot;
if (sorterIndex < 0 || sorterIndex >= sorter->Items.LongCount)
- return (inventoryType, (ushort)visualSlot);
+ return new InventoryLocation(inventoryType, (ushort)visualSlot);
var entry = sorter->Items[sorterIndex].Value;
if (entry == null)
- return (inventoryType, (ushort)visualSlot);
+ return new InventoryLocation(inventoryType, (ushort)visualSlot);
InventoryType baseType = inventoryType switch
{
_ when inventoryType.IsMainInventory => InventoryType.Inventory1,
- _ when inventoryType.IsSaddleBag => inventoryType is InventoryType. SaddleBag1 or InventoryType.SaddleBag2
- ? InventoryType. SaddleBag1
+ _ when inventoryType.IsSaddleBag => inventoryType is InventoryType.SaddleBag1 or InventoryType.SaddleBag2
+ ? InventoryType.SaddleBag1
: InventoryType.PremiumSaddleBag1,
+ _ when inventoryType.IsRetainer => InventoryType.RetainerPage1,
_ => inventoryType,
};
InventoryType realContainer = baseType + entry->Page;
ushort realSlot = entry->Slot;
- return (realContainer, realSlot);
+ return new InventoryLocation(realContainer, realSlot);
}
}
}
\ No newline at end of file
diff --git a/AetherBags/Helpers/InventoryMoveHelper.cs b/AetherBags/Helpers/InventoryMoveHelper.cs
index ad15f7c..1763765 100644
--- a/AetherBags/Helpers/InventoryMoveHelper.cs
+++ b/AetherBags/Helpers/InventoryMoveHelper.cs
@@ -8,20 +8,16 @@ namespace AetherBags. Helpers;
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)
{
- bool isCrossContainerMove = ! sourceContainer.IsSameContainerGroup(destContainer);
-
- if (isCrossContainerMove)
- {
- MoveItemViaAgent(sourceContainer, sourceSlot, destContainer, destSlot);
- }
- else
- {
- InventoryManager.Instance()->MoveItemSlot(sourceContainer, sourceSlot, destContainer, destSlot, true);
- }
+ Services.Logger.Debug($"[MoveItem] {sourceContainer}@{sourceSlot} -> {destContainer}@{destSlot}");
+ InventoryManager.Instance()->MoveItemSlot(sourceContainer, sourceSlot, destContainer, destSlot, true);
+ Services.Framework.DelayTicks(2);
+ Services.Framework.RunOnFrameworkThread(System.AddonInventoryWindow.ManualInventoryRefresh);
}
+ /*
private static void MoveItemViaAgent(InventoryType sourceInventory, ushort sourceSlot, InventoryType destInventory, ushort destSlot)
{
uint sourceContainerId = sourceInventory.AgentItemContainerId;
@@ -49,4 +45,5 @@ public static unsafe class InventoryMoveHelper
RaptureAtkModule* atkModule = RaptureAtkModule.Instance();
atkModule->HandleItemMove(retVal, atkValues, 4);
}
+ */
}
\ No newline at end of file
diff --git a/AetherBags/Hooks/InventoryHook.cs b/AetherBags/Hooks/InventoryHook.cs
index 5e821fc..dd2fa48 100644
--- a/AetherBags/Hooks/InventoryHook.cs
+++ b/AetherBags/Hooks/InventoryHook.cs
@@ -46,7 +46,7 @@ public sealed unsafe class InventoryHooks : IDisposable
InventoryItem* sourceItem = InventoryManager.Instance()->GetInventorySlot(srcType, srcSlot);
InventoryItem* destItem = InventoryManager.Instance()->GetInventorySlot(dstType, dstSlot);
- Services.Logger.Debug($"[MoveItemSlot] Moving {srcType}@{srcSlot} ID:{sourceItem->ItemId} -> {dstType}@{dstSlot} ID:{destItem->ItemId} Unk: {unk}");
+ Services.Logger.Debug($"[MoveItemSlot Hook] Moving {srcType}@{srcSlot} ID:{sourceItem->ItemId} -> {dstType}@{dstSlot} ID:{destItem->ItemId} Unk: {unk}");
return _moveItemSlotHook!.Original(manager, srcType, srcSlot, dstType, dstSlot, unk);
}
diff --git a/AetherBags/Inventory/InventoryLocation.cs b/AetherBags/Inventory/InventoryLocation.cs
new file mode 100644
index 0000000..13b36d1
--- /dev/null
+++ b/AetherBags/Inventory/InventoryLocation.cs
@@ -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}";
+}
\ No newline at end of file
diff --git a/AetherBags/Inventory/InventoryNotificationState.cs b/AetherBags/Inventory/InventoryNotificationState.cs
new file mode 100644
index 0000000..065fdf5
--- /dev/null
+++ b/AetherBags/Inventory/InventoryNotificationState.cs
@@ -0,0 +1,99 @@
+using System.Collections.Generic;
+using Lumina.Excel;
+using Lumina.Excel.Sheets;
+using Lumina.Text.ReadOnly;
+
+namespace AetherBags.Inventory;
+
+public class InventoryNotificationState
+{
+ private readonly Dictionary notificationCache;
+
+ public InventoryNotificationState()
+ {
+ var addonSheet = Services.DataManager.GetExcelSheet();
+ notificationCache = new Dictionary
+ {
+ { InventoryNotificationType.Sell, new InventoryNotificationInfo(addonSheet.GetRow(530).Text, addonSheet.GetRow(3576).Text) },
+ { InventoryNotificationType.Trade, new InventoryNotificationInfo(addonSheet.GetRow(531).Text, addonSheet.GetRow(3572).Text) },
+ { InventoryNotificationType.Letters, new InventoryNotificationInfo(addonSheet.GetRow(549).Text, addonSheet.GetRow(3575).Text) },
+ { InventoryNotificationType.Retainer, new InventoryNotificationInfo(addonSheet.GetRow(532).Text, addonSheet.GetRow(3573).Text) },
+ { InventoryNotificationType.RetainerEquip, new InventoryNotificationInfo(addonSheet.GetRow(778).Text, addonSheet.GetRow(3585).Text) },
+ { InventoryNotificationType.Equip, new InventoryNotificationInfo(addonSheet.GetRow(538).Text, addonSheet.GetRow(3577).Text) },
+ { InventoryNotificationType.Armory, new InventoryNotificationInfo(addonSheet.GetRow(775).Text, addonSheet.GetRow(3578).Text) },
+ { InventoryNotificationType.Markets, new InventoryNotificationInfo(addonSheet.GetRow(548).Text, addonSheet.GetRow(3574).Text) },
+ { InventoryNotificationType.Trade2, new InventoryNotificationInfo(addonSheet.GetRow(531).Text, addonSheet.GetRow(3572).Text) },
+ { InventoryNotificationType.CompanyChest, new InventoryNotificationInfo(addonSheet.GetRow(776).Text, addonSheet.GetRow(3579).Text) },
+ { InventoryNotificationType.Exterior, new InventoryNotificationInfo(addonSheet.GetRow(3583).Text, addonSheet.GetRow(3581).Text) },
+ { InventoryNotificationType.Interior, new InventoryNotificationInfo(addonSheet.GetRow(3584).Text, addonSheet.GetRow(3582).Text) },
+ { InventoryNotificationType.Layout, new InventoryNotificationInfo(addonSheet.GetRow(6237).Text, addonSheet.GetRow(3580).Text) },
+ { InventoryNotificationType.Plant, new InventoryNotificationInfo(addonSheet.GetRow(6416).Text, addonSheet.GetRow(6418).Text) },
+ { InventoryNotificationType.Fertilize, new InventoryNotificationInfo(addonSheet.GetRow(6417).Text, addonSheet.GetRow(6419).Text) },
+ { InventoryNotificationType.Transmutation, new InventoryNotificationInfo(addonSheet.GetRow(3911).Text, addonSheet.GetRow(3901).Text) },
+ { InventoryNotificationType.Reward, new InventoryNotificationInfo(addonSheet.GetRow(6503).Text, addonSheet.GetRow(6502).Text) },
+ { InventoryNotificationType.Feed, new InventoryNotificationInfo(addonSheet.GetRow(6519).Text, addonSheet.GetRow(6518).Text) },
+ { InventoryNotificationType.Charge, new InventoryNotificationInfo(addonSheet.GetRow(8638).Text, addonSheet.GetRow(8637).Text) },
+ { InventoryNotificationType.Convert, new InventoryNotificationInfo(addonSheet.GetRow(8647).Text, addonSheet.GetRow(8646).Text) },
+ { InventoryNotificationType.Covering, new InventoryNotificationInfo(addonSheet.GetRow(9029).Text, addonSheet.GetRow(9028).Text) },
+ { InventoryNotificationType.Feed2, new InventoryNotificationInfo(addonSheet.GetRow(9041).Text, addonSheet.GetRow(9040).Text) },
+ { InventoryNotificationType.Manual, new InventoryNotificationInfo(addonSheet.GetRow(9044).Text, addonSheet.GetRow(9043).Text) },
+ { InventoryNotificationType.Chocobo, new InventoryNotificationInfo(addonSheet.GetRow(9073).Text, addonSheet.GetRow(9072).Text) },
+ { InventoryNotificationType.Outfit, new InventoryNotificationInfo(addonSheet.GetRow(6578).Text, addonSheet.GetRow(6579).Text) },
+ { InventoryNotificationType.Outfit2, new InventoryNotificationInfo(addonSheet.GetRow(6578).Text, addonSheet.GetRow(6579).Text) },
+ { InventoryNotificationType.Plant2, new InventoryNotificationInfo(addonSheet.GetRow(6416).Text, addonSheet.GetRow(6418).Text) },
+ { InventoryNotificationType.Aquarium, new InventoryNotificationInfo(addonSheet.GetRow(6808).Text, addonSheet.GetRow(6807).Text) },
+ { InventoryNotificationType.SaddleBag, new InventoryNotificationInfo(addonSheet.GetRow(891).Text, addonSheet.GetRow(892).Text) },
+ { InventoryNotificationType.Donate, new InventoryNotificationInfo(addonSheet.GetRow(11595).Text, addonSheet.GetRow(11596).Text) },
+ { InventoryNotificationType.Trade3, new InventoryNotificationInfo(addonSheet.GetRow(531).Text, addonSheet.GetRow(3572).Text) },
+ { InventoryNotificationType.Trade4, new InventoryNotificationInfo(addonSheet.GetRow(531).Text, addonSheet.GetRow(3572).Text) },
+ { InventoryNotificationType.Exterior2, new InventoryNotificationInfo(addonSheet.GetRow(3583).Text, addonSheet.GetRow(3581).Text) },
+ { InventoryNotificationType.Interior2, new InventoryNotificationInfo(addonSheet.GetRow(6237).Text, addonSheet.GetRow(3580).Text) },
+ };
+ }
+
+ public InventoryNotificationInfo? GetNotificationInfo(uint openTitleId)
+ {
+ return notificationCache.GetValueOrDefault((InventoryNotificationType)openTitleId);
+ }
+
+ public record InventoryNotificationInfo(ReadOnlySeString Title, ReadOnlySeString Message);
+}
+
+public enum InventoryNotificationType : uint
+{
+ None = 0,
+ Sell = 1,
+ Trade = 2,
+ Letters = 3,
+ Retainer = 4,
+ RetainerEquip = 5,
+ Equip = 6,
+ Armory = 7,
+ Markets = 8,
+ Trade2 = 9,
+ CompanyChest = 10,
+ Exterior = 11,
+ Interior = 12,
+ Layout = 13,
+ Plant = 14,
+ Fertilize = 15,
+ Transmutation = 16,
+ Reward = 17,
+ Feed = 18,
+ Charge = 19,
+ Convert = 20,
+ Covering = 21,
+ Feed2 = 22,
+ Manual = 23,
+ Chocobo = 24,
+ Outfit = 25,
+ Outfit2 = 26,
+ Plant2 = 27,
+ Aquarium = 28,
+ SaddleBag = 29,
+ Donate = 30,
+ Trade3 = 31,
+ Trade4 = 32,
+ Exterior2 = 33,
+ Interior2 = 34
+}
\ No newline at end of file
diff --git a/AetherBags/Nodes/DragDropNode.cs b/AetherBags/Nodes/DragDropNode.cs
index 7eaf992..b7c6bba 100644
--- a/AetherBags/Nodes/DragDropNode.cs
+++ b/AetherBags/Nodes/DragDropNode.cs
@@ -151,28 +151,6 @@
}
}
- public override ReadOnlySeString? Tooltip {
- get;
- set {
- field = value;
- switch (value) {
- case { IsEmpty: false } when !TooltipRegistered:
- AddEvent(AtkEventType.DragDropRollOver, ShowTooltip);
- AddEvent(AtkEventType.DragDropRollOut, HideTooltip);
-
- TooltipRegistered = true;
- break;
-
- case null when TooltipRegistered:
- RemoveEvent(AtkEventType.DragDropRollOver, ShowTooltip);
- RemoveEvent(AtkEventType.DragDropRollOut, HideTooltip);
-
- TooltipRegistered = false;
- break;
- }
- }
- }
-
private void DragDropInsertHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
atkEvent->SetEventIsHandled();
diff --git a/AetherBags/Nodes/Input/TextInputWithHintNode.cs b/AetherBags/Nodes/Input/TextInputWithHintNode.cs
index 5b7af51..8d84ff2 100644
--- a/AetherBags/Nodes/Input/TextInputWithHintNode.cs
+++ b/AetherBags/Nodes/Input/TextInputWithHintNode.cs
@@ -20,7 +20,7 @@ public class TextInputWithHintNode : SimpleComponentNode {
TexturePath = "ui/uld/CircleButtons.tex",
TextureCoordinates = new Vector2(112.0f, 84.0f),
TextureSize = new Vector2(28.0f, 28.0f),
- Tooltip = new SeStringBuilder()
+ TextTooltip = new SeStringBuilder()
.Append("Supports Regex Search")
.AppendNewLine()
.Append("Start input with '$' to search by description")
diff --git a/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs
index f8fbd36..a30c507 100644
--- a/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs
+++ b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs
@@ -5,6 +5,7 @@ using AetherBags.Helpers;
using AetherBags.Inventory;
using AetherBags.Nodes.Layout;
using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
@@ -74,7 +75,7 @@ public class InventoryCategoryNode : SimpleComponentNode
_categoryNameTextNode.String = _fullHeaderText;
_categoryNameTextNode.TextColor = value.Category.Color;
- _categoryNameTextNode.TooltipString = value.Category.Description;
+ _categoryNameTextNode.TextTooltip = value.Category.Description;
UpdateItemGrid();
RecalculateSize();
@@ -209,7 +210,7 @@ public class InventoryCategoryNode : SimpleComponentNode
CreateInventoryDragDropNode);
}
- private InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data)
+ private unsafe InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data)
{
InventoryItem item = data.Item;
@@ -228,65 +229,41 @@ public class InventoryCategoryNode : SimpleComponentNode
},
IsClickable = true,
OnEnd = _ => System.AddonInventoryWindow.ManualInventoryRefresh(),
- OnPayloadAccepted = (n, p) => OnPayloadAccepted(n, p, data),
- OnRollOver = n =>
+ OnPayloadAccepted = (node, payload) => OnPayloadAccepted(node, payload, data),
+ OnRollOver = node =>
{
BeginHeaderHover();
- n.ShowInventoryItemTooltip(item.Container, item.Slot);
+ node.ShowInventoryItemTooltip(item.Container, item.Slot);
},
- OnRollOut = n =>
+ OnRollOut = node =>
{
EndHeaderHover();
- n.HideTooltip();
+
+ ushort addonId = RaptureAtkUnitManager.Instance()->GetAddonByNode(node)->Id;
+ AtkStage.Instance()->TooltipManager.HideTooltip(addonId);
},
ItemInfo = data
};
}
- 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;
- var (sourceContainer, sourceSlot) = ResolveSourceFromPayload(payload);
+ InventoryLocation sourceLocation = payload.InventoryLocation;
- if (sourceContainer == 0)
+ if (!sourceLocation.IsValid)
{
Services.Logger.Warning($"[OnPayload] Could not resolve source from payload");
return;
}
- InventoryType targetContainer = targetItemInfo.Item.Container;
- ushort targetSlot = (ushort)targetItemInfo.Item.Slot;
+ InventoryLocation targetLocation = new InventoryLocation(targetItemInfo.Item.Container, (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);
- }
-
- 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 slotIndex = payload.Int2;
-
- InventoryType sourceContainer = InventoryType.GetInventoryTypeFromContainerId(containerId);
-
- if (sourceContainer == 0)
- return (0, 0);
-
- // For main inventory, resolve the real slot via ItemOrderModule
- if (sourceContainer.IsMainInventory)
- {
- var (realContainer, realSlot) = sourceContainer.GetRealItemLocation(slotIndex);
- return (realContainer, realSlot);
- }
-
- // For other containers (saddlebags, armory, etc.), use the slot directly
- return (sourceContainer, (ushort)slotIndex);
+ InventoryMoveHelper.MoveItem(sourceLocation.Container, sourceLocation.Slot, targetLocation.Container, targetLocation.Slot);
}
}
\ No newline at end of file
diff --git a/AetherBags/Nodes/Inventory/InventoryNotificationNode.cs b/AetherBags/Nodes/Inventory/InventoryNotificationNode.cs
new file mode 100644
index 0000000..a3d260d
--- /dev/null
+++ b/AetherBags/Nodes/Inventory/InventoryNotificationNode.cs
@@ -0,0 +1,134 @@
+using System.Collections.Generic;
+using System.Numerics;
+using AetherBags.Inventory;
+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 static readonly InventoryNotificationState NotificationState = new();
+
+ public InventoryNotificationNode()
+ {
+ 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 };
+ }
+
+ public InventoryNotificationType NotificationType
+ {
+ get;
+ set
+ {
+ field = value;
+ if (value == InventoryNotificationType.None)
+ {
+ titleTextNode.String = string.Empty;
+ messageTextNode.String = string.Empty;
+ Timeline?.PlayAnimation(17); // Hide
+ }
+ else
+ {
+ var info = NotificationState.GetNotificationInfo((uint)value);
+ if (info != null)
+ {
+ titleTextNode.SeString = info.Title;
+ messageTextNode.SeString = info.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();
+}
\ No newline at end of file
diff --git a/AetherBags/Services.cs b/AetherBags/Services.cs
index 00eeddf..1569de2 100644
--- a/AetherBags/Services.cs
+++ b/AetherBags/Services.cs
@@ -13,6 +13,7 @@ public class Services
[PluginService] public static IDataManager DataManager { get; set; } = null!;
[PluginService] public static IDalamudPluginInterface PluginInterface { get; private set; } = null!;
[PluginService] public static IFramework Framework { get; private set; } = null!;
+ [PluginService] public static IGameGui GameGui { get; private set; } = null!;
[PluginService] public static IKeyState KeyState { get; private set; } = null!;
[PluginService] public static IPluginLog Logger { get; private set; } = null!;
[PluginService] public static INotificationManager NotificationManager { get; private set; } = null!;
diff --git a/KamiToolKit b/KamiToolKit
index f16338c..9519b07 160000
--- a/KamiToolKit
+++ b/KamiToolKit
@@ -1 +1 @@
-Subproject commit f16338cf173eca3a0fcca389533a8ada1c4ef624
+Subproject commit 9519b07c8db287ef75b7153a5e97c24574e800f2