d7df385239
Co-authored-by: Cursor <cursoragent@cursor.com>
300 lines
9.6 KiB
C#
300 lines
9.6 KiB
C#
using System;
|
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
|
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
|
|
|
namespace QuickTransfer;
|
|
|
|
/// <summary>
|
|
/// Helper functions for parsing drag-drop interfaces from UI events.
|
|
/// </summary>
|
|
internal static unsafe class DragDropHelpers
|
|
{
|
|
// ArmouryBoard drag-drop payloads are not always (InventoryType, Slot).
|
|
// On some builds the payload's Int1 is a category index, and Int2 is the slot within that category.
|
|
// This mapping is best-effort and is only applied when we're sure the hover comes from the ArmouryBoard addon.
|
|
internal static readonly InventoryType[] ArmouryBoardIndexToType =
|
|
[
|
|
InventoryType.ArmoryMainHand,
|
|
InventoryType.ArmoryOffHand,
|
|
InventoryType.ArmoryHead,
|
|
InventoryType.ArmoryBody,
|
|
InventoryType.ArmoryHands,
|
|
InventoryType.ArmoryWaist,
|
|
InventoryType.ArmoryLegs,
|
|
InventoryType.ArmoryFeets,
|
|
InventoryType.ArmoryEar,
|
|
InventoryType.ArmoryNeck,
|
|
InventoryType.ArmoryWrist,
|
|
InventoryType.ArmoryRings,
|
|
InventoryType.ArmorySoulCrystal,
|
|
];
|
|
|
|
public static bool TryGetDragDropInterfaceFromReceiveEvent(
|
|
AddonArgs args,
|
|
AddonReceiveEventArgs recv,
|
|
AtkEventType eventType,
|
|
AtkEventData* eventData,
|
|
out uint addonId,
|
|
out AtkDragDropInterface* ddi)
|
|
{
|
|
addonId = 0;
|
|
ddi = null;
|
|
|
|
var addon = (AtkUnitBase*)args.Addon.Address;
|
|
if (addon == null)
|
|
return false;
|
|
addonId = addon->Id;
|
|
|
|
// List item events can provide a renderer directly.
|
|
if (eventData != null &&
|
|
eventType is AtkEventType.ListItemRollOver or AtkEventType.ListItemRollOut or AtkEventType.ListItemClick or
|
|
AtkEventType.ListItemDoubleClick or AtkEventType.ListItemSelect)
|
|
{
|
|
try
|
|
{
|
|
var r = eventData->ListItemData.ListItemRenderer;
|
|
if (r != null)
|
|
{
|
|
// Prefer the embedded DragDrop component if present.
|
|
if (r->DragDropComponent != null)
|
|
ddi = &r->DragDropComponent->AtkDragDropInterface;
|
|
else
|
|
{
|
|
try { ddi = &r->AtkDragDropInterface; } catch { /* ignore */ }
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
if (ddi != null)
|
|
return true;
|
|
|
|
static AtkDragDropInterface* TryGetDdiFromList(AtkComponentList* list)
|
|
{
|
|
if (list == null)
|
|
return null;
|
|
|
|
// The list tracks a hovered item index itself, which is much safer than trying to interpret eventParam.
|
|
// Prefer HoveredItemIndex, then fall back to other hover slots.
|
|
static AtkDragDropInterface* FromIndex(AtkComponentList* l, int idx)
|
|
{
|
|
if (idx < 0 || idx > 512)
|
|
return null;
|
|
try
|
|
{
|
|
var r = l->GetItemRenderer(idx);
|
|
return r != null ? &r->AtkDragDropInterface : null;
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
var ddi0 = FromIndex(list, list->HoveredItemIndex);
|
|
if (ddi0 != null)
|
|
return ddi0;
|
|
|
|
var ddi1 = FromIndex(list, list->HoveredItemIndex2);
|
|
if (ddi1 != null)
|
|
return ddi1;
|
|
|
|
var ddi2 = FromIndex(list, list->HoveredItemIndex3);
|
|
if (ddi2 != null)
|
|
return ddi2;
|
|
|
|
return null;
|
|
}
|
|
|
|
static AtkDragDropInterface* TryGetDdiFromComponent(AtkComponentBase* component)
|
|
{
|
|
if (component == null)
|
|
return null;
|
|
|
|
var t = component->GetComponentType();
|
|
return t switch
|
|
{
|
|
ComponentType.DragDrop => &((AtkComponentDragDrop*)component)->AtkDragDropInterface,
|
|
ComponentType.ListItemRenderer => &((AtkComponentListItemRenderer*)component)->AtkDragDropInterface,
|
|
ComponentType.List => TryGetDdiFromList((AtkComponentList*)component),
|
|
_ => null,
|
|
};
|
|
}
|
|
|
|
// Prefer the drag-drop interface directly from event data when present.
|
|
// IMPORTANT: only trust DragDropData for actual drag-drop event types; for MouseOver it can contain garbage.
|
|
var isDragDropEvent =
|
|
eventType is AtkEventType.DragDropBegin or
|
|
AtkEventType.DragDropCanAcceptCheck or
|
|
AtkEventType.DragDropClick or
|
|
AtkEventType.DragDropDiscard or
|
|
AtkEventType.DragDropEnd or
|
|
AtkEventType.DragDropInsert or
|
|
AtkEventType.DragDropInsertAttempt or
|
|
AtkEventType.DragDropRollOut or
|
|
AtkEventType.DragDropRollOver;
|
|
|
|
ddi = (isDragDropEvent && eventData != null) ? eventData->DragDropData.DragDropInterface : null;
|
|
|
|
// Some drag-drop events (notably DragDropRollOver) provide a ComponentNode but not a DragDropInterface.
|
|
// IMPORTANT: never read DragDropData.ComponentNode for non-dragdrop events (AtkEventData is a union).
|
|
if (ddi == null && isDragDropEvent && eventData != null && eventData->DragDropData.ComponentNode != null)
|
|
{
|
|
try
|
|
{
|
|
var compNode = eventData->DragDropData.ComponentNode;
|
|
var component = compNode->Component;
|
|
ddi = TryGetDdiFromComponent(component);
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
// Fallback: some event types provide MouseData, but the target is still a DragDrop component.
|
|
if (ddi == null)
|
|
{
|
|
var atkEvent = (AtkEvent*)recv.AtkEvent;
|
|
if (atkEvent != null && atkEvent->Node != null)
|
|
{
|
|
var node = atkEvent->Node;
|
|
var compNode = node->GetAsAtkComponentNode();
|
|
if (compNode != null)
|
|
{
|
|
var component = compNode->Component;
|
|
ddi = TryGetDdiFromComponent(component);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ddi == null)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
public static bool TryGetSlotFromDragDropInterface(
|
|
AtkDragDropInterface* ddi,
|
|
out InventoryType invType,
|
|
out int slot)
|
|
{
|
|
invType = default;
|
|
slot = -1;
|
|
if (ddi == null)
|
|
return false;
|
|
|
|
var payload = ddi->GetPayloadContainer();
|
|
if (payload == null)
|
|
return false;
|
|
|
|
invType = (InventoryType)payload->Int1;
|
|
slot = payload->Int2;
|
|
if (slot < 0 || slot > 500)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
public static bool TryGetSlotFromDragDropInterfaceForAddon(
|
|
AtkDragDropInterface* ddi,
|
|
string addonName,
|
|
uint addonId,
|
|
out InventoryType invType,
|
|
out int slot,
|
|
out int rawInt1,
|
|
out int rawInt2,
|
|
out uint rawFlags)
|
|
{
|
|
invType = default;
|
|
slot = -1;
|
|
rawInt1 = 0;
|
|
rawInt2 = 0;
|
|
rawFlags = 0;
|
|
|
|
if (ddi == null)
|
|
return false;
|
|
|
|
AtkDragDropPayloadContainer* payload;
|
|
try
|
|
{
|
|
payload = ddi->GetPayloadContainer();
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
if (payload == null)
|
|
return false;
|
|
|
|
rawInt1 = payload->Int1;
|
|
rawInt2 = payload->Int2;
|
|
rawFlags = payload->Flags;
|
|
|
|
// Default interpretation (most inventory add-ons): (InventoryType, Slot)
|
|
invType = (InventoryType)rawInt1;
|
|
slot = rawInt2;
|
|
|
|
// ArmouryBoard special-case: some builds use (CategoryIndex, Slot)
|
|
// and Int1 may look like Inventory1..Inventory4 (0..3), which is clearly wrong for ArmouryBoard.
|
|
if (!string.IsNullOrEmpty(addonName) &&
|
|
addonName.Equals("ArmouryBoard", StringComparison.OrdinalIgnoreCase) &&
|
|
InventoryHelpers.TryGetVisibleAddon("ArmouryBoard", out var ab) &&
|
|
ab != null &&
|
|
ab->Id == addonId)
|
|
{
|
|
if (rawInt1 >= 0 && rawInt1 < ArmouryBoardIndexToType.Length)
|
|
{
|
|
invType = ArmouryBoardIndexToType[rawInt1];
|
|
slot = rawInt2;
|
|
}
|
|
}
|
|
|
|
if (slot < 0 || slot > 500)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
public static int PickContextMenuSlot(InventoryType type, int preferredSlot)
|
|
{
|
|
try
|
|
{
|
|
var inv = InventoryManager.Instance();
|
|
if (inv == null)
|
|
return preferredSlot;
|
|
|
|
var c = inv->GetInventoryContainer(type);
|
|
if (c == null || !c->IsLoaded || c->Size <= 0)
|
|
return preferredSlot;
|
|
|
|
// Prefer the hovered slot when in range AND it contains an item.
|
|
if (preferredSlot >= 0 && preferredSlot < c->Size)
|
|
{
|
|
var it0 = c->GetInventorySlot(preferredSlot);
|
|
if (it0 != null && it0->ItemId != 0)
|
|
return preferredSlot;
|
|
}
|
|
|
|
// Fallback: find the first slot with an item.
|
|
for (var i = 0; i < c->Size; i++)
|
|
{
|
|
var it = c->GetInventorySlot(i);
|
|
if (it != null && it->ItemId != 0)
|
|
return i;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
return preferredSlot;
|
|
}
|
|
}
|