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

@ -0,0 +1,90 @@
# Retail item drag visuals — pseudocode
Date: 2026-07-13
Scope: the cursor graphic and source-cell visual while dragging an item from an
item list. This is a focused continuation of
`2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md`.
## Retail anchors
- `IconData::RenderIcons` @ `0x0058d180`
(`named-retail/acclient_2013_pseudo_c.txt:407524`)
- `UIElement_ItemList::PrepareDragIcon` @ `0x004e2a50`
(`named-retail/acclient_2013_pseudo_c.txt:230765`)
- `UIElement_ItemList::ItemList_BeginDrag` @ `0x004e32d0`
(`named-retail/acclient_2013_pseudo_c.txt:231350`)
- `UIElement_UIItem::SetWaitingState` @ `0x004e11b0`
(`named-retail/acclient_2013_pseudo_c.txt:229190`)
- `ACCWeenieObject::SetWaitingState` @ `0x0058c0d0`
(`named-retail/acclient_2013_pseudo_c.txt:406260`)
- `UIElement_UIItem` layout child `m_elem_Icon_Ghosted = 0x10000349`;
the shared UIItem catalog `0x21000037` gives it DirectState surface
`0x0600109A`.
The live DAT catalog and the named retail code agree. The in-tree WorldBuilder
reference has no retained UI/item-list implementation to compare for this path.
## Literal pseudocode
```text
IconData.RenderIcons(item):
destroy old m_pIcon and m_pDragIcon
dragSurface = transparent 32 x 32
blit item.baseIcon onto dragSurface using normal blit
blit item.customOverlay onto dragSurface using alpha blit
replace pure-white dragSurface pixels from the effect-color surface
m_pDragIcon = Graphic(dragSurface)
cellSurface = transparent 32 x 32
blit typeDefaultUnderlay onto cellSurface using normal blit
blit item.customUnderlay onto cellSurface using alpha blit
blit dragSurface onto cellSurface using alpha blit
m_pIcon = Graphic(cellSurface)
ItemList.PrepareDragIcon(cell):
clear cell.m_dragIcon image
object = GetWeenieObject(cell.itemID)
set cell.m_dragIcon image to object.GetDragIcon() # m_pDragIcon, not m_pIcon
ItemList.ItemList_BeginDrag(cell):
if PrepareDragIcon(cell) failed:
return
if list is not vendor, salvage, or shortcut list:
cell.SetWaitingState(true)
StartDragAndDrop(cell.m_dragIcon, hotspot = 16,16)
UIItem.SetWaitingState(waiting):
object.SetWaitingState(waiting)
if cell is not unghostable:
set m_elem_Icon_Ghosted visible = waiting
on drag release/cancel:
clear source waiting state
hide m_elem_Icon_Ghosted
```
## Port mapping
- `IconComposer` caches the underlay-free composite separately and exposes
`GetDragIcon`; the ordinary `GetIcon` still layers the two underlays beneath
the same cached drag pixels.
- `UiItemSlot` stores `IconTexture` and `DragIconTexture` separately.
`GetDragGhost` returns the latter, while `OnDraw` keeps the former in the
source cell.
- `UiRoot` owns the generic source-active lifecycle, including release outside
UI and source-subtree removal. `UiItemSlot` maps that lifecycle to the retail
waiting visual for physical lists; shortcut aliases remain unghosted.
- `UiItemSlot.WaitingSprite` is the authored `0x0600109A` mesh, not a procedural
tint.
## Conformance coverage
- `DragDropSpineTests.GetDragGhost_prefersDedicatedUnderlayFreeTexture`
- `DragDropSpineTests.InventoryDrag_ghostsSourceUntilRelease`
- `DragDropSpineTests.ShortcutDrag_doesNotGhostSource`
- `UiItemSlotTests.DefaultWaitingSprite_isRetailGhostMesh`
- Existing `IconComposerTests.TwoStageWithEffect_copiesTilePixelBeforeUnderlay`
locks the two-stage composition order.