diff --git a/AetherBags/Extensions/DragDropPayloadExtensions.cs b/AetherBags/Extensions/DragDropPayloadExtensions.cs new file mode 100644 index 0000000..62024dd --- /dev/null +++ b/AetherBags/Extensions/DragDropPayloadExtensions.cs @@ -0,0 +1,57 @@ +using AetherBags.Interop; +using FFXIVClientStructs.FFXIV.Component.GUI; +using KamiToolKit.Classes; +using Lumina.Text.ReadOnly; +using Lumina.Text; + +namespace AetherBags.Extensions; + +// TODO: Remove this when CS is merged into Dalamud. +public static unsafe class DragDropPayloadExtensions +{ + public static DragDropPayload FromFixedInterface(AtkDragDropInterface* dragDropInterface) + { + // Cast to our manual fixed struct + var fixedInterface = (AtkDragDropInterfaceFixed*)dragDropInterface; + + // Calls Index 12 + var payloadContainer = fixedInterface->GetPayloadContainer(); + + return new DragDropPayload + { + Type = fixedInterface->DragDropType, + ReferenceIndex = fixedInterface->DragDropReferenceIndex, + Int1 = payloadContainer->Int1, + Int2 = payloadContainer->Int2, + Text = new ReadOnlySeString(payloadContainer->Text), + }; + } + + public static void ToFixedInterface(this DragDropPayload payload, AtkDragDropInterface* dragDropInterface, bool writeToPayloadContainer = true) + { + var fixedInterface = (AtkDragDropInterfaceFixed*)dragDropInterface; + + fixedInterface->DragDropType = payload.Type; + fixedInterface->DragDropReferenceIndex = payload.ReferenceIndex; + + if (writeToPayloadContainer) + { + // Calls Index 12 + var payloadContainer = fixedInterface->GetPayloadContainer(); + + payloadContainer->Clear(); + payloadContainer->Int1 = payload.Int1; + payloadContainer->Int2 = payload.Int2; + + if (payload.Text.IsEmpty) + { + payloadContainer->Text.Clear(); + } + else + { + var stringBuilder = new SeStringBuilder().Append(payload.Text); + payloadContainer->Text.SetString(stringBuilder.GetViewAsSpan()); + } + } + } +} \ No newline at end of file diff --git a/AetherBags/Interop/AtkDragDropInterfaceFixed.cs b/AetherBags/Interop/AtkDragDropInterfaceFixed.cs new file mode 100644 index 0000000..34daf17 --- /dev/null +++ b/AetherBags/Interop/AtkDragDropInterfaceFixed.cs @@ -0,0 +1,69 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace AetherBags.Interop; + +// Size 0x30 (48) matches the original struct +[StructLayout(LayoutKind.Explicit, Size = 48)] +public unsafe struct AtkDragDropInterfaceFixed +{ + // Offset 0 is the Virtual Table Pointer (void**) + [FieldOffset(0)] public void** VirtualTable; + + // Map specific fields needed for Payload logic + [FieldOffset(36)] public DragDropType DragDropType; + [FieldOffset(40)] public short DragDropReferenceIndex; + + // Helper to get 'this' as a pointer + private AtkDragDropInterfaceFixed* ThisPtr => (AtkDragDropInterfaceFixed*)Unsafe.AsPointer(ref this); + + // [VirtualFunction(1)] + public void GetScreenPosition(float* screenX, float* screenY) + { + var fnPtr = (delegate* unmanaged)VirtualTable[1]; + fnPtr(ThisPtr, screenX, screenY); + } + + // [VirtualFunction(3)] + public AtkComponentNode* GetComponentNode() + { + var fnPtr = (delegate* unmanaged)VirtualTable[3]; + return fnPtr(ThisPtr); + } + + // [VirtualFunction(5)] + public void SetComponentNode(AtkComponentNode* node) + { + var fnPtr = (delegate* unmanaged)VirtualTable[5]; + fnPtr(ThisPtr, node); + } + + // [VirtualFunction(6)] + public AtkResNode* GetActiveNode() + { + var fnPtr = (delegate* unmanaged)VirtualTable[6]; + return fnPtr(ThisPtr); + } + + // [VirtualFunction(8)] + public AtkComponentBase* GetComponent() + { + var fnPtr = (delegate* unmanaged)VirtualTable[8]; + return fnPtr(ThisPtr); + } + + // [VirtualFunction(9)] + public bool HandleMouseUpEvent(AtkEventData.AtkMouseData* mouseData) + { + var fnPtr = (delegate* unmanaged)VirtualTable[9]; + return fnPtr(ThisPtr, mouseData) != 0; + } + + // [VirtualFunction(12)] + public AtkDragDropPayloadContainer* GetPayloadContainer() + { + var fnPtr = (delegate* unmanaged)VirtualTable[12]; + return fnPtr(ThisPtr); + } +} \ No newline at end of file diff --git a/AetherBags/Nodes/Configuration/GeneralScrollingAreaNode.cs b/AetherBags/Nodes/Configuration/GeneralScrollingAreaNode.cs index 6436640..1d2544c 100644 --- a/AetherBags/Nodes/Configuration/GeneralScrollingAreaNode.cs +++ b/AetherBags/Nodes/Configuration/GeneralScrollingAreaNode.cs @@ -12,7 +12,7 @@ public sealed class GeneralScrollingAreaNode : ScrollingAreaNode { + + // FIX: Manually expose the pointers that are 'internal' in KamiToolKit + // We access the raw AtkComponentNode* via 'this.ResNode' and cast from there. + private new AtkComponentDragDrop* Component => (AtkComponentDragDrop*)this.InternalComponentNode->Component; + private new AtkUldComponentDataDragDrop* Data => (AtkUldComponentDataDragDrop*)Component->UldManager.ComponentData; + + public readonly ImageNode DragDropBackgroundNode; + public readonly IconNode IconNode; + + public DragDropNode() { + SetInternalComponentType(ComponentType.DragDrop); + + DragDropBackgroundNode = new SimpleImageNode { + NodeId = 3, + Size = new Vector2(44.0f, 44.0f), + TexturePath = "ui/uld/DragTargetA.tex", + TextureCoordinates = new Vector2(0.0f, 0.0f), + TextureSize = new Vector2(44.0f, 44.0f), + WrapMode = WrapMode.Tile, + NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents, + }; + DragDropBackgroundNode.AttachNode(this); + + IconNode = new IconNode { + NodeId = 2, + Size = new Vector2(44.0f, 48.0f), + NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents, + }; + IconNode.AttachNode(this); + + LoadTimelines(); + + Data->Nodes[0] = IconNode.NodeId; + + AcceptedType = DragDropType.Everything; + Payload = new DragDropPayload(); + + // Use the fixed shadow struct for writing initial values if needed, + // though direct field access on the struct usually works for simple fields. + // However, to be safe with the VTable fix, we just set fields directly here + // as they are standard offsets, or use the pointer. + Component->AtkDragDropInterface.DragDropType = DragDropType.Everything; + Component->AtkDragDropInterface.DragDropReferenceIndex = 0; + + InitializeComponentEvents(); + + AddEvent(AtkEventType.DragDropBegin, DragDropBeginHandler); + AddEvent(AtkEventType.DragDropInsert, DragDropInsertHandler); + AddEvent(AtkEventType.DragDropDiscard, DragDropDiscardHandler); + AddEvent(AtkEventType.DragDropClick, DragDropClickHandler); + AddEvent(AtkEventType.DragDropRollOver, DragDropRollOverHandler); + AddEvent(AtkEventType.DragDropRollOut, DragDropRollOutHandler); + } + + private bool IsDragDropEndRegistered { get; set; } + + public Action? OnBegin { get; set; } + public Action? OnEnd { get; set; } + public Action? OnPayloadAccepted { get; set; } + public Action? OnDiscard { get; set; } + public Action? OnClicked { get; set; } + public Action? OnRollOver { get; set; } + public Action? OnRollOut { get; set; } + + public DragDropPayload Payload { get; set; } + + public uint IconId { + get => IconNode.IconId; + set { + IconNode.IconId = value; + IconNode.IsVisible = value != 0; + } + } + + public bool IsIconDisabled { + get => IconNode.IsIconDisabled; + set => IconNode.IsIconDisabled = value; + } + + public int Quantity { + get => int.Parse(Component->GetQuantityText().ToString()); + set => Component->SetQuantity(value); + } + + public string QuantityString { + get => Component->GetQuantityText().ToString(); + set => Component->SetQuantityText(value); + } + + public DragDropType AcceptedType { + get => Component->AcceptedType; + set => Component->AcceptedType = value; + } + + public AtkDragDropInterface.SoundEffectSuppression SoundEffectSuppression { + get => Component->AtkDragDropInterface.DragDropSoundEffectSuppression; + set => Component->AtkDragDropInterface.DragDropSoundEffectSuppression = value; + } + + public bool IsDraggable { + get => !Component->Flags.HasFlag(DragDropFlag.Locked); + set { + if (value) { + Component->Flags &= ~DragDropFlag.Locked; + } + else { + Component->Flags |= DragDropFlag.Locked; + } + } + } + + public bool IsClickable { + get => Component->Flags.HasFlag(DragDropFlag.Clickable); + set { + if (value) { + Component->Flags |= DragDropFlag.Clickable; + } + else { + Component->Flags &= ~DragDropFlag.Clickable; + } + } + } + + private void DragDropBeginHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) { + atkEvent->SetEventIsHandled(); + + // FIX: Use extension method to write payload using fixed VTable + Payload.ToFixedInterface(atkEventData->DragDropData.DragDropInterface); + + OnBegin?.Invoke(this); + + if (!IsDragDropEndRegistered) { + AddEvent(AtkEventType.DragDropEnd, DragDropEndHandler); + IsDragDropEndRegistered = true; + } + } + + 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(); + + atkEvent->State.StateFlags |= AtkEventStateFlags.HasReturnFlags; + atkEvent->State.ReturnFlags = 1; + + // FIX: Use extension method to read payload using fixed VTable + var payload = DragDropPayloadExtensions.FromFixedInterface(atkEventData->DragDropData.DragDropInterface); + + Payload.Clear(); + IconId = 0; + + OnPayloadAccepted?.Invoke(this, payload); + } + + private void DragDropDiscardHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) { + atkEvent->SetEventIsHandled(); + + atkEvent->State.StateFlags |= AtkEventStateFlags.HasReturnFlags; + atkEvent->State.ReturnFlags = 1; + + OnDiscard?.Invoke(this); + } + + private void DragDropEndHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) { + atkEvent->SetEventIsHandled(); + + // FIX: Cast to shadow struct to call the correct GetPayloadContainer (Index 12) + var fixedInterface = (AtkDragDropInterfaceFixed*)atkEventData->DragDropData.DragDropInterface; + fixedInterface->GetPayloadContainer()->Clear(); + + OnEnd?.Invoke(this); + + if (IsDragDropEndRegistered) { + RemoveEvent(AtkEventType.DragDropEnd, DragDropEndHandler); + IsDragDropEndRegistered = false; + } + } + + private void DragDropClickHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) { + atkEvent->SetEventIsHandled(); + + atkEvent->State.StateFlags |= AtkEventStateFlags.HasReturnFlags; + atkEvent->State.ReturnFlags = 1; + + OnClicked?.Invoke(this); + } + + private void DragDropRollOverHandler() + => OnRollOver?.Invoke(this); + + private void DragDropRollOutHandler() + => OnRollOut?.Invoke(this); + + public void Clear() { + Payload.Clear(); + IconId = 0; + } + + public void ShowTooltip(AtkTooltipManager.AtkTooltipType type, ActionKind actionKind) { + if (AtkStage.Instance()->DragDropManager.IsDragging) return; + + // FIX: Explicitly use 'this.ResNode' and cast to (AtkResNode*) to avoid ambiguity with the class name + var addon = RaptureAtkUnitManager.Instance()->GetAddonByNode((AtkResNode*)this); + if (addon is null) return; + + var tooltipArgs = new AtkTooltipManager.AtkTooltipArgs(); + tooltipArgs.Ctor(); + tooltipArgs.ActionArgs.Id = Payload.Int2; + tooltipArgs.ActionArgs.Kind = (DetailKind)actionKind; + + AtkStage.Instance()->TooltipManager.ShowTooltip( + AtkTooltipManager.AtkTooltipType.Action, + addon->Id, + (AtkResNode*)this, // FIX: Explicit cast here as well + &tooltipArgs); + } + + private void LoadTimelines() { + AddTimeline(new TimelineBuilder() + .BeginFrameSet(1, 59) + .AddLabelPair(1, 10, 1) + .AddLabelPair(11, 19, 2) + .AddLabelPair(20, 29, 3) + .AddLabelPair(30, 39, 7) + .AddLabelPair(40, 49, 6) + .AddLabelPair(50, 59, 4) + .EndFrameSet() + .Build()); + } + } \ No newline at end of file diff --git a/AetherBags/Nodes/InventoryCategoryNode.cs b/AetherBags/Nodes/InventoryCategoryNode.cs index 1e24bf4..77af556 100644 --- a/AetherBags/Nodes/InventoryCategoryNode.cs +++ b/AetherBags/Nodes/InventoryCategoryNode.cs @@ -8,12 +8,15 @@ using KamiToolKit.Nodes; using System; using System.Numerics; +// TODO: Switch back to CS version when Dalamud Updated +using DragDropFixedNode = AetherBags.Nodes.DragDropNode; + namespace AetherBags.Nodes; public class InventoryCategoryNode : SimpleComponentNode { private readonly TextNode _categoryNameTextNode; - private readonly HybridDirectionalFlexNode _itemGridNode; + private readonly HybridDirectionalFlexNode _itemGridNode; private const float FallbackItemSize = 46; private const float HeaderHeight = 16; @@ -48,7 +51,7 @@ public class InventoryCategoryNode : SimpleComponentNode _categoryNameTextNode.AddFlags(NodeFlags.EmitsEvents | NodeFlags.HasCollision); _categoryNameTextNode.AttachNode(this); - _itemGridNode = new HybridDirectionalFlexNode + _itemGridNode = new HybridDirectionalFlexNode { Position = new Vector2(0, HeaderHeight), Size = new Vector2(240, 92), @@ -249,16 +252,20 @@ public class InventoryCategoryNode : SimpleComponentNode Size = new Vector2(42, 46), IsVisible = true, IconId = item.IconId, - AcceptedType = DragDropType.Nothing, - IsDraggable = false, + AcceptedType = DragDropType.Item, + IsDraggable = true, Payload = new DragDropPayload { - Type = DragDropType.Item, - Int1 = (int)item.Container, - Int2 = (int)item.ItemId, + Type = DragDropType.Inventory_Item, + Int1 = (int)item.GetInventoryType(), + Int2 = item.Slot, }, IsClickable = true, - + OnEnd = _ => + { + System.AddonInventoryWindow.ManualInventoryRefresh(); + }, + OnPayloadAccepted = (n, p) => OnPayloadAccepted(n, p, data), OnRollOver = n => { BeginHeaderHover(); @@ -275,4 +282,14 @@ public class InventoryCategoryNode : SimpleComponentNode return node; } + + private unsafe void OnPayloadAccepted(DragDropNode node, DragDropPayload payload, ItemInfo itemInfo) + { + Services.Logger.Debug($"Inventory DragDropNode Payload Accepted: {payload.Type} Int1: {payload.Int1} Int2: {payload.Int2}"); + InventoryType inventoryType = (InventoryType)payload.Int1; + ushort sourceSlot = (ushort)payload.Int2; + System.AddonInventoryWindow.ManualInventoryRefresh(); + // Should work for swapping item but need a fake empty slot to put new items in probably. + InventoryManager.Instance()->MoveItemSlot(inventoryType, sourceSlot, itemInfo.Item.Container, itemInfo.Item.GetSlot()); + } } diff --git a/AetherBags/Nodes/InventoryDragDropNode.cs b/AetherBags/Nodes/InventoryDragDropNode.cs index 9df8e69..cc112a3 100644 --- a/AetherBags/Nodes/InventoryDragDropNode.cs +++ b/AetherBags/Nodes/InventoryDragDropNode.cs @@ -7,9 +7,12 @@ using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Classes; using KamiToolKit.Nodes; +// TODO: Switch back to CS version when Dalamud Updated +using DragDropFixedNode = AetherBags.Nodes.DragDropNode; + namespace AetherBags.Nodes; -public class InventoryDragDropNode : DragDropNode +public class InventoryDragDropNode : DragDropFixedNode { private readonly TextNode _quantityTextNode; public unsafe InventoryDragDropNode()