Some checks failed
Headless portability / portable-headless (ubuntu-latest) (push) Has been cancelled
Headless portability / portable-headless (windows-latest) (push) Has been cancelled
Headless portability / linux-graphical (push) Has been cancelled
Headless portability / linux-vulkan (push) Has been cancelled
The green ring is retail's own art: every UIItem cell carries an authored DragAccept child (catalog 0x21000037, child 0x1000045A), and the spell bar's drag-over handler (SpellCastSubMenu::OnItemListDragOver @0x004C5990) flips it to the Accept state (0x10000040 -> surface 0x060011F9) for any spell payload. Ported through a per-slot SetDragAcceptVisual seam + a catalog DragOverAcceptance hook; other lists are untouched (null acceptance = neutral). A polarity error in our older docs (Accept/Reject state ids swapped) was corrected against three independent sources; the shipped art was always right, only the labels lied. The ring shares ONE landing computation with the drop (FavoriteDropIndex) — and that requirement exposed a genuine #354 off-by-one: the empty-tail path double-applied the -1 adjustment (retail gates it on the lift's removal @0x004C7157), landing a reordered spell second-to-last instead of last. Fixed; discriminator-verified both ways. AP-172 narrowed + its false empty-tail claim corrected. Clean-room complete solution: 11,545 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
137 lines
6 KiB
Markdown
137 lines
6 KiB
Markdown
# 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 cell is not selected:
|
|
SetSelectedObject(cell.itemID, false)
|
|
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
|
|
```
|
|
|
|
## Destination feedback and source overlay order
|
|
|
|
`UIElement_ItemList::ItemList_DragOver @ 0x004e3400` distinguishes an ordinary
|
|
slot insertion from dropping into a container:
|
|
|
|
```text
|
|
if target list is a container selector
|
|
and target cell is occupied by a container
|
|
and that container has an empty item slot:
|
|
target.SetDragAcceptState(0x10000046) # ItemSlot_DragOver_DropIn
|
|
# 0x060011F7 green arrow
|
|
else if target accepts an ordinary item-list placement:
|
|
target.SetDragAcceptState(0x10000040) # ItemSlot_DragOver_Accept
|
|
# 0x060011F9 green circle
|
|
else:
|
|
target.SetDragAcceptState(0x10000041) # ItemSlot_DragOver_Reject
|
|
# 0x060011F8 reject
|
|
```
|
|
|
|
> **Correction 2026-08-08 (spell-bar drop-ring research):** the block above
|
|
> originally had the Accept/Reject numeric ids swapped (`0x10000041` labeled
|
|
> Accept, `0x10000040` labeled reject). Three primary sources agree the true
|
|
> mapping is `ItemSlot_DragOver_Accept = 0x10000040 → 0x060011F9` and
|
|
> `ItemSlot_DragOver_Reject = 0x10000041 → 0x060011F8`: DatReaderWriter's
|
|
> retail-derived `UIStateId` enum; the legal/illegal branches in
|
|
> `gmPaperDollUI::HandlePaperDollDragOver` (`AutoWearIsLegal` → 0x10000040
|
|
> @ 0x004A3AC9, else 0x10000041 @ 0x004A3AEB) and
|
|
> `VendorSellUI::OnItemListDragOver` (`DragItemAcceptable` → 0x10000040
|
|
> @ 0x004C2327, else 0x10000041 @ 0x004C2336); and the machine layout dump
|
|
> (`2026-06-25-retail-ui-layout-dump.json`: state 268435520 = 0x10000040 →
|
|
> image 0x060011F9, state 268435521 = 0x10000041 → 0x060011F8). The
|
|
> art-per-semantic mapping in the shipped code was always correct; only the
|
|
> numeric labels here were swapped.
|
|
|
|
Therefore the backpack contents grid uses the green circle; the side-bag column
|
|
and main-pack container cell use the green drop-in arrow. The selected/open
|
|
indicators remain visible while `m_elem_Icon_Ghosted` is active, so the
|
|
procedural leaf draws the waiting mesh before those persistent indicators and
|
|
keeps drag-accept feedback topmost.
|
|
|
|
## 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. It dispatches `DragBegin` before enabling the
|
|
source-active mesh, matching retail's select-then-waiting order. The inventory,
|
|
paperdoll, and toolbar handlers select an unselected dragged item synchronously.
|
|
`UiItemSlot` maps the 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`
|
|
- `InventoryControllerTests.OnDragLift_selectsItem_butKeepsItUntilServerConfirms`
|
|
- `PaperdollControllerTests.DragLift_selectsEquippedItem_beforeWaitingVisual`
|
|
- `DragDropSpineTests.ShortcutDrag_doesNotGhostSource`
|
|
- `UiItemSlotTests.DefaultWaitingSprite_isRetailGhostMesh`
|
|
- `InventoryControllerTests.Empty_sprites_and_drop_feedback_are_applied_per_list_role`
|
|
- Existing `IconComposerTests.TwoStageWithEffect_copiesTilePixelBeforeUnderlay`
|
|
locks the two-stage composition order.
|