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