Release 1.0.5: Vendor Quick Sell, auto-confirm sell dialogs, README update

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-01-27 20:05:39 -05:00
parent 633cd487e4
commit d7df385239
10 changed files with 1484 additions and 1072 deletions
+299
View File
@@ -0,0 +1,299 @@
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;
}
}