fix(ui): port retail item drag visuals

Keep retail's underlay-free m_pDragIcon separate from the full cell icon and reveal the authored ghost mesh on physical source cells for the complete drag lifecycle. This removes the backpack backing from the cursor and retires AP-47.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-13 09:14:24 +02:00
parent 3e84027885
commit ace5880fed
14 changed files with 307 additions and 44 deletions

View file

@ -41,6 +41,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
private readonly ClientObjectTable _objects;
private readonly Func<uint> _playerGuid;
private readonly Func<ItemType, uint, uint, uint, uint, uint> _iconIds;
private readonly Func<ItemType, uint, uint, uint, uint, uint>? _dragIconIds;
private readonly Func<int?> _strength;
private readonly Func<string>? _ownerName;
@ -74,6 +75,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
ClientObjectTable objects,
Func<uint> playerGuid,
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds,
Func<int?> strength,
SelectionState selection,
Func<string>? ownerName,
@ -94,6 +96,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
_objects = objects;
_playerGuid = playerGuid;
_iconIds = iconIds;
_dragIconIds = dragIconIds;
_strength = strength;
_ownerName = ownerName;
_sendUse = sendUse;
@ -214,8 +217,9 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
Action<uint, uint>? notifyMergeAttempt = null,
ItemInteractionController? itemInteraction = null,
Action? onClose = null,
StackSplitQuantityState? stackSplitQuantity = null)
=> new InventoryController(layout, objects, playerGuid, iconIds, strength, selection,
StackSplitQuantityState? stackSplitQuantity = null,
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null)
=> new InventoryController(layout, objects, playerGuid, iconIds, dragIconIds, strength, selection,
ownerName, datFont,
contentsEmptySprite, sideBagEmptySprite, mainPackEmptySprite,
sendUse, sendNoLongerViewing, sendPutItemInContainer,
@ -315,7 +319,11 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
const uint PlayerPackBaseIcon = 0x0600127Eu; // constant main-pack backpack (visual gate)
_topContainer.Flush();
var main = new UiItemSlot { SpriteResolve = _topContainer.SpriteResolve };
main.SetItem(p, _iconIds(ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u));
main.SetItem(
p,
_iconIds(ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u),
dragIconTexture: _dragIconIds?.Invoke(
ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u) ?? 0u);
main.DragAcceptSprite = 0x060011F7u; main.DragRejectSprite = 0x060011F8u;
main.Clicked = () =>
{
@ -351,8 +359,11 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
var item = _objects.Get(guid);
uint tex = item is null ? 0u
: _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
uint dragTex = item is null ? 0u
: _dragIconIds?.Invoke(
item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects) ?? 0u;
var cell = new UiItemSlot { SpriteResolve = list.SpriteResolve };
cell.SetItem(guid, tex);
cell.SetItem(guid, tex, dragIconTexture: dragTex);
cell.SlotIndex = list.GetNumUIItems(); // index it will occupy (== its slot in a packed list)
cell.DragAcceptSprite = 0x060011F7u; // green insert arrow (export-confirmed)
cell.DragRejectSprite = 0x060011F8u; // red circle
@ -400,7 +411,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
// ── IItemListDragHandler (B-Drag) — drop an item to move it (optimistic + wire) ──────────────
/// <summary>Inventory items do NOT lift-remove (unlike the toolbar): the item stays in its slot
/// until the server confirms the move. Retail dims the source; we leave it + the floating ghost.</summary>
/// while <see cref="UiItemSlot"/> reveals retail's waiting/ghosted mesh until release.</summary>
public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload) { }
/// <summary>Advisory neutral/accept/reject overlay. Shortcut aliases stay neutral; physical grid

View file

@ -78,6 +78,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
private readonly ClientObjectTable _objects;
private readonly Func<uint> _playerGuid;
private readonly Func<ItemType, uint, uint, uint, uint, uint> _iconIds;
private readonly Func<ItemType, uint, uint, uint, uint, uint>? _dragIconIds;
private readonly Action<uint, uint>? _sendWield; // (itemGuid, equipMask) → GetAndWieldItem 0x001A
private readonly ItemInteractionController? _itemInteraction;
private readonly SelectionState _selection;
@ -96,9 +97,11 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
Func<ItemType, uint, uint, uint, uint, uint> iconIds, SelectionState selection,
Action<uint, uint>? sendWield,
uint emptySlotSprite, UiDatFont? datFont, ItemInteractionController? itemInteraction,
PaperdollClickMap? clickMap)
PaperdollClickMap? clickMap,
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds)
{
_objects = objects; _playerGuid = playerGuid; _iconIds = iconIds; _sendWield = sendWield;
_dragIconIds = dragIconIds;
_itemInteraction = itemInteraction;
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
_clickMap = clickMap;
@ -195,10 +198,11 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
Action<uint, uint>? sendWield = null,
uint emptySlotSprite = 0u, UiDatFont? datFont = null,
ItemInteractionController? itemInteraction = null,
PaperdollClickMap? clickMap = null)
PaperdollClickMap? clickMap = null,
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null)
=> new PaperdollController(
layout, objects, playerGuid, iconIds, selection, sendWield, emptySlotSprite,
datFont, itemInteraction, clickMap);
datFont, itemInteraction, clickMap, dragIconIds);
private void HandleDollClick(int x, int y)
{
@ -256,7 +260,9 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
if (worn is null) { list.Cell.Clear(); continue; }
uint tex = _iconIds(worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects);
list.Cell.SetItem(worn.ObjectId, tex);
uint dragTex = _dragIconIds?.Invoke(
worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects) ?? 0u;
list.Cell.SetItem(worn.ObjectId, tex, dragIconTexture: dragTex);
}
ApplySelectionIndicators();
}

View file

@ -64,6 +64,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
private readonly CombatState? _combatState;
private readonly Func<IReadOnlyList<ShortcutEntry>> _shortcuts;
private readonly Func<ItemType, uint, uint, uint, uint, uint> _iconIds; // (itemType, icon, underlay, overlay, effects) → GL tex
private readonly Func<ItemType, uint, uint, uint, uint, uint>? _dragIconIds;
private readonly Action<uint> _useItem; // guid → fire UseObject
private readonly ShortcutStore _store = new();
private bool _storeLoaded;
@ -107,12 +108,14 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
Func<uint>? selectedObjectId = null,
Func<uint>? playerGuid = null,
Action<uint, uint, int>? sendPutItemInContainer = null,
UiDatFont? ammoFont = null)
UiDatFont? ammoFont = null,
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null)
{
_repo = repo;
_combatState = combatState;
_shortcuts = shortcuts;
_iconIds = iconIds;
_dragIconIds = dragIconIds;
_useItem = useItem;
_peaceDigits = peaceDigits;
_warDigits = warDigits;
@ -322,12 +325,14 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
Func<uint>? selectedObjectId = null,
Func<uint>? playerGuid = null,
Action<uint, uint, int>? sendPutItemInContainer = null,
UiDatFont? ammoFont = null)
UiDatFont? ammoFont = null,
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null)
{
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState,
peaceDigits, warDigits, emptyDigits, itemInteraction,
sendAddShortcut, sendRemoveShortcut, toggleCombat, selectItem,
selectedObjectId, playerGuid, sendPutItemInContainer, ammoFont);
selectedObjectId, playerGuid, sendPutItemInContainer, ammoFont,
dragIconIds);
c.Populate();
return c;
}
@ -401,7 +406,9 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
var item = _repo.Get(guid);
if (item is null) continue; // deferred: ObjectAdded re-calls Populate
uint tex = _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
list.Cell.SetItem(guid, tex, entry);
uint dragTex = _dragIconIds?.Invoke(
item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects) ?? 0u;
list.Cell.SetItem(guid, tex, entry, dragTex);
}
// Re-stamp slot number labels after any item change.