acdream/docs/research/2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md
Erik a5c5126e8d docs(D.5): action bar / inventory / paperdoll research drop
Five report-only deep-dives + synthesis for the next D.2b UI panels, built on the shipped widget toolkit. Confirms LayoutDesc ids (toolbar 0x21000016, inventory 0x21000023, backpack 0x21000022, paperdoll 0x21000024, 3ditems 0x21000021), the shared item-slot/item-list spine (UIElement_UIItem 0x10000032 / UIElement_ItemList 0x10000031), the 5-layer icon composite (IconData::RenderIcons @407524), the cross-panel wire catalog with acdream parse-status, and the dependency-ordered build plan.

Produced via a multi-agent research workflow; the spine agent died on a transient API error and was re-run as a focused follow-up with its decomp anchors verified against source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:04:57 +02:00

37 KiB
Raw Blame History

UI item-slot SPINE — icon-composite render + widget-level drag-drop — deep dive

Date: 2026-06-16 Phase: D.2b retail-UI engine, "core panels" research arc. Report-only. Role: completes the workflow's MISSING 5th doc — the shared item-slot/icon/drag-drop spine that the action-bar, inventory, and paperdoll deep-dives all depend on. The spine agent died on a transient API error before writing anything; this doc is the recovery + the gap-fill. Deliverable: this doc only. No C# changed; no game run.

What this doc adds vs. the four existing docs

The three panel agents + the synthesis already recovered the identity facts of the two spine widgets first-hand and re-verified them (synthesis §0 re-verifications, §1 table, §2). I do not re-derive those — I cite and extend them. My NEW, spine-owned contributions are the three things the panel docs explicitly deferred:

  1. The icon-composite render port spec (synthesis §4 Step 0, §5 risk #1) — the full IconData::RenderIcons blit pipeline, and the definitive answer to the direct-RenderSurface-vs-Icon-composite decode question.
  2. The widget-level drag-drop state machine (synthesis §5 risk #1, §8) — the UIElement_Field/UIElement_UIItem hooks every cell inherits, below the per-panel HandleDropRelease the panel docs covered.
  3. The consolidated, authoritative UIElement_UIItem port spec with the resolved field names — including the +0x5FC resolution (synthesis §5 risk #2): it is UIElement_UIItem::itemID.

Obsoletes in the synthesis (the parent should patch these now that the spine exists): the ⚠ banner (synthesis lines 13-31), §4 Step 0's "re-do / complete the spine research (blocking)", §5 risk #1 (spine never written), §5 risk #2's "stays UNVERIFIED", §6's "⚠ the SPINE doc was never written", §8's blocking note, and the two panel-doc index lines' "spine still owed" caveats. Details in the closing summary.

1. Summary + confidence legend

Every item-bearing slot in all three D.2b panels is the same pair of retail widgets: the item-cell UIElement_UIItem (element class 0x10000032) sits inside a slot/grid UIElement_ItemList (element class 0x10000031). The cell holds a bound object id (itemID), resolves it to an ACCWeenieObject, and draws a composited 32×32 icon plus a stack of overlay sub-elements (quantity text, capacity/structure Type-7 meters, a 10-step cooldown ring, selected/ghosted/open-container/drag-accept/sell/trade overlays). The icon itself is composited at runtime from up to five 0x06xx RenderSurfaces (base + custom-underlay + custom-overlay + item-type-default-underlay + spell-effect-overlay) blitted into one private 32×32 surface — NOT a single texture. Drag-drop is a generic chain inherited from UIElement_Field: the cell is both a drag-SOURCE (ItemList_BeginDrag on left-press-and-move) and a drop-TARGET (MouseOverTop rollover → accept/reject state, CatchDroppedItem on release → HandleDropRelease), with InqDropIconInfo extracting the dragged object id + flags that tell a fresh-from-inventory drag (flags&0xE==0) from a within-list reorder (flags&4).

acdream is well-positioned: ItemInstance already models IconId/IconUnderlayId/ IconOverlayId/StackSize/ContainerId/ContainerSlot; TextureCache. GetOrUploadRenderSurface already decodes a 0x06 id directly; UiRoot already has a real drag-drop state machine (DragSource/DragPayload/BeginDrag/UpdateDragHover/ FinishDrag, even commented with the retail 0x15→0x21→0x1C→0x3E event chain). The concrete gaps: CreateObject discards IconId; there is no multi-layer icon-compositor; UiField names the CatchDroppedItem/MouseOverTop hooks in a doc-comment but does not implement them yet.

Confidence legend:

  • CONFIRMED — quoted from a named-decomp class::method (with line) or a real file:line I opened this session.
  • LIKELY — inferred from a CONFIRMED source; the inference is named.
  • UNVERIFIED — educated guess, flagged loudly.

2. UIElement_UIItem port spec (consolidated + authoritative)

2.1 Identity + the resolved struct (+0x5FC = itemID)

UIElement_UIItem::Register (decomp 229339): UIElement::RegisterElementClass(0x10000032, UIElement_UIItem::Create); — class 0x10000032. It is a UIElement_Field subclass: the destructor chains UIElement_Field::~UIElement_Field(this) (decomp 229326), and Field::Register is RegisterElementClass(3, …) (decomp 126190) ⇒ the underlying generic Type is 3. CONFIRMED.

+0x5FC RESOLVED — it is UIElement_UIItem::itemID. The toolbar doc anchored the bound object id by raw offset +0x5FC only (toolbar §3, UNVERIFIED name). The named decomp resolves it: UIItem_Update reads uint32_t itemID = this->itemID; (decomp 230230) and this->weenObj = ClientObjMaintSystem::GetWeenieObject(itemID) (decomp 230235). HandleTargetedUseLeftClick reads uint32_t itemID = arg2->itemID; (decomp 230422). ItemList_AddItem's rebuild loop tests eax_2->itemID == arg2 (decomp 233107). So the field the toolbar's RemoveShortcutInSlotNum read at +0x5FC is itemID — the bound weenie/object guid. CONFIRMED. (The companion spell-shortcut id is this->spellID, decomp 230239/230414.)

Resolved instance fields (all CONFIRMED from UIItem_Update 230226-230393, UIItem_SetIcon 230143, PostInit 229668, SetShortcutNum 229465, the setters 229190-229286, and the acclient.h IconData/PublicWeenieDesc structs):

Field Meaning Anchor
itemID bound object/weenie guid (the retail +0x5FC) 230230
spellID spell-shortcut id (0 for an item) 230239, 230414
weenObj cached ACCWeenieObject* from GetWeenieObject(itemID) 230235
selected mirror of weenObj->selected 230269
effects mirror of weenObj->pwd._effects 230293
waiting mirror of weenObj->waiting (the pending/ghost flag) 230336
isOpenable/isContainer/isContainerHolder container-capability flags from _bitfield/_itemsCapacity/_containersCapacity 230298-230331
m_quantity stack count to display 229285
m_selectable whether selection is allowed 229266
unghostable suppress the ghost overlay 229199
m_shortcutNum / m_shortcutGhosted / m_delayedShortcutNum toolbar slot index + deferred-bind sentinel 0xFFFFFFFF 229542-229543, 230344-230349
m_sellState / m_tradeState vendor-sell / trade-window markers 230362, 230377
m_dragIcon translucent drag-ghost copy (created in PostInit, id 0x10000345) 229738

2.2 Sub-element id map (from PostInit, decomp 229672-229733) — all CONFIRMED

PostInit binds each overlay/feature sub-element by GetChildRecursive(this, id). These ids live in the cell template LayoutDesc 0x21000037; the importer must reproduce them procedurally (the cell is a behavioral leaf). The dump .layout-dumps/uiitem-0x21000037.txt gives the per-state sprite ids (column 3 below).

Member Element id Type Role Dump sprite(s) (state → 0x06id)
m_elem_Icon 0x1000033B 3 the composited icon, AND the empty-slot bg ItemSlot_Empty → 0x060074CF (dump:45)
m_elem_Icon_Overlays 0x1000033C enchantment/effect overlay layer (state-driven; see §3)
m_elem_Text 0x10000344 12 (Text) spell name / label text
m_elem_Icon_CapacityBar 0x10000347 7 (Meter) container fill (numContained/itemsCapacity) DirectState 0x06004D22+0x06004D23 (dump:693,710)
m_elem_Icon_StructureBar 0x10000348 7 (Meter) structure/charges fill DirectState 0x06004D24+0x06004D25 (dump:727,744)
m_elem_Icon_Selected 0x10000342 3 selection highlight 0x06001A97 / 0x06001396 / 0x060067D2 per variant (dump:95,311,541)
m_elem_Icon_Ghosted 0x10000349 3 greyed "pending server confirm" overlay DirectState 0x0600109A (dump:761)
m_elem_Icon_ShortcutNum 0x1000034A 3 the slot-number badge (toolbar) media set at runtime via SetMediaImage (229508)
m_elem_Icon_SellState 0x10000437 3 vendor-sell marker
m_elem_Icon_TradeState 0x10000438 3 trade-window marker
m_elem_Icon_OpenContainer 0x10000450 3 "this container is open" frame DirectState 0x06005D9C Alphablend (dump:2232)
m_elem_Icon_DragAccept 0x1000045A 3 drag-rollover accept/reject frame ItemSlot_DragOver_Accept → 0x060011F9, _Reject → 0x060011F8, _DropIn → 0x060011F7 (dump:1174-1175,1258-1260)
m_elem_Icon_Quantity 0x100004F5 12 (Text) the stack-count number
m_elem_Icon_Cooldown_10..100 0x1000054F..0x10000558 3 10-step radial cooldown ring DirectState 0x0600109D / 0x060012D9 / 0x06001DAE / 0x060067CF..D1 … (dump:778-863)
m_dragIcon 0x10000345 (created) translucent drag-ghost created via CreateChildElement(this, dbobj, 0x10000345), SetVisible(0) (229738-229740)

The four named LayoutDesc states that drive m_elem_Icon / m_elem_Icon_DragAccept (from the dump): ItemSlot_Empty (the empty-slot background sprite, default 0x060074CF), ItemSlot_DragOver_Accept (0x060011F9), ItemSlot_DragOver_Reject (0x060011F8), ItemSlot_DragOver_DropIn (0x060011F7). The DragAccept neutral/reset UIStateId is 0x1000003f; the inventory agent's 0x10000040(reject)/0x10000041 (accept) SetState ids (synthesis §0 re-verification, decomp 229180-229413) are the internal element states SetDragAcceptState writes — both are real; the LayoutDesc named states and the 0x1000003x/4x UIStateIds are the same overlay seen from the dat side vs. the C++ side. CONFIRMED.

2.3 Key methods + the update pass (UIItem_Update, decomp 230226)

UIItem_Update is the per-change refresh; the controller calls it whenever the bound weenie or its display state changes. Walk-through (CONFIRMED 230226-230392):

  1. Resolve weenObj = GetWeenieObject(itemID) (230235). If null & has a spellID → UIItem_SetState(0x1000001d) + UIItem_SetIcon; if null & no spell → UIItem_SetState(0x1000001c) (= empty) + ClearTooltip. (230232-230250)
  2. Set m_elem_Icon / m_elem_Text / m_elem_Icon_Overlays to state 0x1000001d (= occupied). (230256-230265)
  3. UIItem_SetIcon(this) — (re)build the composited icon (§3). (230268)
  4. Sync selectedweenObj->selected, toggling m_elem_Icon_Selected visibility (gated on m_selectable). (230269-230290)
  5. Recompute isOpenable/isContainer/isContainerHolder from _bitfield/_itemsCapacity/_containersCapacity (the player's own cell is always openable). (230298-230331)
  6. UpdateCapacityDisplay (Type-7 meter = numContained/itemsCapacity, decomp 229554-), UpdateStructureDisplay, UpdateQuantityDisplay, UpdateCooldownDisplay. (230332-230335)
  7. Sync waitingSetWaitingState (toggles m_elem_Icon_Ghosted). (230336-230342)
  8. Apply any deferred m_delayedShortcutNum (re-bind once the weenie loaded). (230344-230350)
  9. Sync m_shortcutNum/m_shortcutGhosted (230352-230360), m_sellState/m_tradeState overlays (230362-230389), then UpdateTooltip. (230392)

Companion methods (CONFIRMED): UIItem_SetIcon 230143 (§3); SetShortcutNum(slot, ghosted) 229465 (writes the slot badge via SetMediaImage, mirrors into ACCWeenieObject::SetShortcutNum); SetDelayedShortcutNum 229238; SetWaitingState 229190; SetSelectedState 229243; SetSelectableState 229263; SetDragAcceptState 229271; SetOpenContainerState 229216; SetQuantity 229282; UpdateCapacityDisplay 229554.

2.4 acdream item-cell port = UiItemSlot

A behavioral leaf widget (ConsumesDatChildren => true) keyed off resolved class 0x10000032, exactly like the shipped behavioral widgets. It binds an ItemInstance (by itemID), draws the composited icon (§3), the quantity UiText, the capacity/ structure UiMeters, the cooldown ring, and the overlay states; it is a drag source + drop target (§5). This aligns with the synthesis §2 row (no correction). The retail sub-element ids in §2.2 become the named child slots the controller toggles.


3. Icon rendering pipeline — THE CRUX

3.1 The decode question, answered definitively

Both halves of the synthesis's question are true, layered: each icon LAYER is a 0x06xx RenderSurface decoded directly (the D.2b memory's GetOrUploadRenderSurface path), but the on-screen icon is a runtime COMPOSITE of up to five of those layers blitted into one private 32×32 surface. It is NOT a single weenie texture, and it is NOT an "Icon DBObj type that references other surfaces" — there is no Icon DBObj; the composite logic lives entirely in client code (IconData::RenderIcons), and every input id is a plain RenderSurface.

Proof chain (all CONFIRMED):

  • UIElement_UIItem::UIItem_SetIcon (decomp 230171) sets the cell's image from ACCWeenieObject::GetIcon(weenObj): eax_15 = Graphic::Graphic(eax_13, ACCWeenieObject::GetIcon(eax_12)); … UIRegion::SetImage(this->m_elem_Icon, eax_15);
  • ACCWeenieObject::GetIcon (decomp 408999): return ACCWeenieObject::GetIconData(this)->m_pIcon;
  • ACCWeenieObject::GetIconData (decomp 408224) caches a per-object IconData (hash by guid), constructing one via IconData::IconData(eax_4, this, this->id) (408253) on first use; IconData::IconData calls IconData::RenderIcons(this, arg2) (407957).
  • The IconData struct (acclient.h:54112, verbatim): m_idIcon, m_idCustomOverlay, m_idCustomUnderlay, m_itemType, m_effects, Graphic *m_pIcon, Graphic *m_pDragIcon.

The base id is the weenie's _iconID: ACCWeenieObject::InqIconID (decomp 406951) returns this->pwd._iconID.id. _iconID/_iconOverlayID/_iconUnderlayID are all IDClass<_tagDataID,32,0> in PublicWeenieDesc (acclient.h:37168-37170). CONFIRMED.

Every layer is DBObj type 0xcRenderIcons fetches each with DBObj::Get(QualifiedDataID(&v, id, 0xc)) (decomp 407587/407589/407592). DBObj type 0xc = DB_TYPE_RENDERSURFACE = Texture in ACE's DatFileType enum, id range 0x06000000-0x07FFFFFF (references/.../ACE.DatLoader/DatFileType.cs:127-128). So all five ids are 0x06xx RenderSurfaces — decode each via TextureCache.GetOrUploadRenderSurface per the D.2b memory gotcha, NOT GetOrUpload (feeding a 0x06 id to GetOrUpload walks the Surface→SurfaceTexture chain and returns 1×1 magenta — TextureCache.cs:112-128, project_d2b_retail_ui.md "Dat sprites — the decode path"). CONFIRMED.

3.2 The composite — IconData::RenderIcons (decomp 407524), CONFIRMED

RenderIcons builds TWO graphics: m_pDragIcon (the drag-ghost, no underlay) and m_pIcon (the full slot icon). Field captures first (407528-407532):

m_idIcon          = InqIconID()                 # = pwd._iconID  (base)
m_idCustomOverlay = pwd._iconOverlayID          # server "enchanted" overlay
m_idCustomUnderlay= pwd._iconUnderlayID         # server "magic" underlay
m_itemType        = InqType()
m_effects         = pwd._effects

Player special-case (407546-407549): if IsThePlayer(), m_idIcon = GetDIDByEnum(0x10000004, 7) (the player container icon) and m_itemType = TYPE_CONTAINER.

Two enum-resolved layers (407552-407584):

  • type-default underlay eax_11 = DBObj::GetByEnum(LowestSetBit(m_itemType)+1, …) with enum 0x10000004 (the SkillTable DID-mapper namespace reused as the icon-type table); if m_itemType has no bits, index 0x21. (407555-407564)
  • effect overlay arg2 = DBObj::GetByEnum(LowestSetBit(m_effects)+1, …) with enum 0x10000005; if null, fall back to index 0x21 of the same enum. (407568-407584)

Then it resolves the three direct ids as DBObjs (407587-407592): eax_19 = m_idCustomUnderlay, ebp = m_idIcon (base), edi_1/var_38 = m_idCustomOverlay.

Drag-icon surface (m_pDragIcon, 407594-407625): a 32×32 local surface (CreateLocalSurfaceCreate(0x20, 0x20, GetUISurfaceFormat, 1)); blit base ebp Blit_Normal, then custom-overlay var_38 Blit_4Alpha; ReplaceColor(..., &pwd._iconOverlayID) applies the overlay tint; wrapped in a Graphic.

Full slot icon (m_pIcon, 407626-407647): a second 32×32 surface; blit type-default underlay eax_11 Blit_Normal, then custom-underlay eax_19 Blit_3Alpha, then the drag-icon surface eax_26 Blit_3Alpha on top (base + overlay already baked into it). Wrapped in a Graphicm_pIcon.

Net composite (bottom → top):

  1. item-type default underlay (GetByEnum(0x10000004, lsb(itemType)+1)) — Normal
  2. server custom underlay (pwd._iconUnderlayID) — 3Alpha
  3. base icon (pwd._iconID) — Normal (baked into the drag layer first)
  4. server custom overlay (pwd._iconOverlayID) + its tint — 4Alpha
  5. spell-effect overlay (GetByEnum(0x10000005, lsb(effects)+1)) — (captured arg2; note: in the 2013 BN lifting the effect-overlay capture lands but I did not see its explicit Blit in the slot-surface block; it feeds the same path. LIKELY blitted as part of the overlay stage — flagged, see §7.)

Cache invalidation: IconData::UpdateIcons (407962) re-renders only when InqIconID(), _iconOverlayID, _iconUnderlayID, InqType(), or _effects changed (407968-407976); ACCWeenieObject::IconDataChanged (408201) drives it on a property update.

3.3 The decode pipeline acdream should use

  1. On CreateObject (and ObjDescEvent/property-update), capture IconId (_iconID), IconUnderlayId (_iconUnderlayID), IconOverlayId (_iconOverlayID), _effects, and ItemType into the ItemInstance (the model already has the first three fields; _effects needs adding). Gap: CreateObject.TryParse discards IconId — re-verified at CreateObject.cs:516 (_ = ReadPackedDwordOfKnownType(body, ref pos, IconTypePrefix); // IconId) and :515 (_ = ReadPackedDword(...) // WeenieClassId). CONFIRMED.
  2. For each of the up-to-five layer ids, decode the 0x06xx RenderSurface directly via TextureCache.GetOrUploadRenderSurface (per the D.2b gotcha).
  3. Composite into one 32×32 RGBA target in the order of §3.2. Two faithful options: (a) a CPU compositor matching retail's blit modes (Normal = src-over opaque, 3Alpha/4Alpha = the AC alpha blits — see ACViewer ImgTex/RenderSurface decode for the per-format alpha handling), uploaded as one cached GL texture keyed by the (iconId, underlay, overlay, effects, itemType) tuple; or (b) draw the layers as stacked sprites at the cell rect each frame. Retail does (a) (one m_pIcon surface), and caching matches retail's IconData per-object cache + UpdateIcons dirty check — recommend (a).
  4. The type-default underlay (GetByEnum(0x10000004, lsb(itemType)+1)) and effect overlay (GetByEnum(0x10000005, lsb(effects)+1)) require resolving the retail icon-type / effect DID-mapper enums to concrete 0x06 ids. These map through the dat DidMapper/EnumMapper tables (DatFileType 38/36). For MVP, the base _iconID alone is the dominant visual (most items have no custom underlay/overlay and no effects); the underlay/overlay/effect layers are the "magic/enchanted/glow" polish. LIKELY-safe to ship base-only first, then layer in the composite. (synthesis §5 risk #3 — verify IconId is set on a CONTAINED item's CreateObject against a live capture before treating it as the sole source.)

Palette note (cross-ref). Item icons are pre-rendered 0x06 RenderSurfaces; they do NOT take a creature/clothing subpalette overlay at icon-composite time (the composite only blits + tints with _iconOverlayID). ACViewer's TextureCache.cs::IndexToColor subpalette-overlay is for paletted INDEX16/P8 world textures — the canonical reference for THAT path, but the icon path uses the surfaces as-decoded. acdream's WB TextureHelpers.cs (in-tree) is the decode reference for the 0x06 formats themselves (BGRA/DXT/P8/INDEX16). CONFIRMED the composite has no subpalette step; LIKELY a paletted UI icon would need a palette (today GetOrUploadRenderSurface passes palette: null → magenta on a paletted sprite, TextureCache.cs:135 — flagged §7).

3.4 Identified-vs-unidentified does NOT swap the icon (synthesis §5 risk #14)

CONFIRMED in the negative: UIItem_Update/UIItem_SetIcon/RenderIcons derive the icon purely from server-sent weenie props (_iconID/_iconUnderlayID/_iconOverlayID/ _effects/InqType) — there is no appraise/identified branch anywhere in the icon path. Appraise (IdentifyObjectResponse 0x00C9) gates the TOOLTIP detail (UpdateTooltip, 230392), not the icon. So a slot shows the same icon before and after appraise. The inventory agent's risk #14 LIKELY is now CONFIRMED.


4. Item / container data model + acdream gap analysis

4.1 Items are ACCWeenieObject weenies

The cell never holds item data — it holds an itemID and resolves it live via ClientObjMaintSystem::GetWeenieObject(itemID) (decomp 230235). This matches claude-memory/feedback_weenie_vs_static.md (interactable items are server-spawned weenies, not dat-baked). The data the cell binds to:

Cell display Source field (PublicWeenieDesc, acclient.h:37163+)
base icon _iconID (37168)
magic underlay _iconUnderlayID (37170)
enchanted overlay _iconOverlayID (37169)
effect glow _effects (37183)
stack count _stackSize / _maxStackSize (37188-37189)
capacity bar _itemsCapacity / _containersCapacity (37176-37177)
structure bar _structure / _maxStructure (37186-37187)
value/burden _value (37179) / _burden (37193)
container membership _containerID / _wielderID / _location / _priority (37171-37175)

4.2 How the client learns container contents

  • Login: PlayerDescription (0x0013) carries the full inventory + equipped lists.
  • Per-item spawn: CreateObject (0xF745) for each weenie (incl. a pack item) with the WeenieHeader fields above.
  • Open a container: ViewContents (0x0196) lists {guid, containerType} per slot → UIElement_ItemList::ItemList_OpenContainer builds a UIElement_UIItem per entry.
  • Live moves: ACCWeenieObject::ServerSaysMoveItem (decomp 408086) is the client's per-weenie relocation: it updates _containerID/_wielderID/_location, re-parents in the local content lists (RemoveContent/AddContent), sets current_state (IN_CONTAINER/IN_3D_VIEW), and clears the waiting ghost. This is driven by the 0x0022/0x0023/0x019A GameEvents. CONFIRMED.

Hierarchy is 2-deep (main pack → side-packs; a side-pack holds no side-pack) — the backpack hosts two UIElement_ItemLists, the own list (+0x604) and the open-other- container list (+0x608) (inventory §2.2). The outbound verbs are the ACCWeenieObject:: UIAttempt* family — UIAttemptWieldEvent_GetAndWieldItem (decomp 407763, with a stack-split-to-wield branch when _stackSize>1), UIAttemptPutInContainerEvent_PutItemInContainer (407797), UIAttemptPutIn3DEvent_DropItem (407821), UIAttemptMerge/UIAttemptSplitToContainer/UIAttemptSplitTo3D/UIAttemptGive (407840-407897, 407780). Each records a prevRequest for the speculative-then-confirm rollback. CONFIRMED.

4.3 acdream model status (focus: what the cell binds to)

  • ItemInstance.cs (verified): already has IconId (cs:136), IconUnderlayId (137), IconOverlayId (138), StackSize/StackSizeMax (139-140), Burden (141), Value (142), ContainerId (143), ContainerSlot (144), ValidLocations/ CurrentlyEquippedLocation (134-135). Missing for the icon composite: _effects (effect glow) and an ItemType already present (Type, 133). The synthesis §0 claim is CONFIRMED.
  • ItemRepository.cs (verified): already models the container map, the move events (WieldObject/InventoryPutObjInContainer/InventoryPutObjectIn3D/ViewContents/ CloseGroundContainer, cs:23-27) and the InventoryServerSaveFailed speculative- revert (cs:28-31). CONFIRMED.
  • CreateObject.cs (verified): discards IconId (cs:516) + WeenieClassId (cs:515) + StackSize/Value/capacities — the cell's icon + quantity + capacity-bar source. CONFIRMED gap.
  • The full wire-gap TODO is the synthesis §3.3 — not duplicated here; the data-model-binding subset is: extend CreateObject to capture IconId/WeenieClassId/StackSize/Value/ItemCapacity/ContainerCapacity (+ _effects), and add _effects to ItemInstance.

5. Drag-drop spine — the WIDGET-LEVEL state machine

The per-panel docs covered the panel-class HandleDropRelease (e.g. gmToolbarUI : ItemListDragHandler). THIS is the shared lower layer every item-cell inherits.

5.1 The retail event chain on the cell (UIElement_UIItem::ListenToElementMessage, decomp 229344)

The cell handles four element messages (CONFIRMED 229347-229418):

  • 0x21 = begin-drag (left-press-and-move on an occupied cell): walk to the parent UIElement_ItemList (GetParent()->DynamicCast(0x10000031)) and call ItemList_BeginDrag(list, ptWindow.x, ptWindow.y) (229357-229360). The list spawns the m_dragIcon ghost and arms the drag.
  • 0x3e = drag-over, with two sub-cases keyed on dwParam1:
    • dwParam1 == 0 (drag left this cell): reset DragAccept to neutral SetState(0x1000003f) (229381-229387).
    • else (drag hovering): if a global drag is active (UIElementManager::s_pInstance-> m_dragElement != 0), forward to ItemList_DragOver(list, target, dragElement) (229390-229406); the list decides accept/reject and flips the DragAccept overlay.
  • 0x15 = drop/release: clear the weenie's waiting flag and hide m_elem_Icon_Ghosted (229363-229379). (The retail event-id sequence is 0x15→0x21→0x1C→0x3E, which acdream's UiRoot already cites verbatim — UiRoot.cs:448.)

5.2 The drop-TARGET rollover (UIElement_Field::MouseOverTop, decomp 126098)

Every cell inherits Field's drop-target rollover. When a drag is in progress (UIElementManager::s_pInstance->m_dragElement != 0) and this field has the CatchDroppedItem attribute (GetAttribute_Bool(0x36), plus 0x70/0x38), it calls m_dragDropCallback(m_dragElement, this) to test acceptance and sets element state 9 (accept) or 0xa (reject), saving the old state for restore on leave (126124-126153). UIElement_Field::CatchDroppedItem (decomp 126159) restores the rollover state then chains UIElement::CatchDroppedItem (the real drop handler). CONFIRMED.

The 0x36 attribute (CatchDroppedItem flag) is exactly what UIElement_UIItem::PostInit sets true on every cell (decomp 229744: SetPropertyName(0x36); …(1); SetProperty), with 0x3a and 0x39 set false (229755/229766). So every item-cell is a drop target by construction. CONFIRMED.

5.3 InqDropIconInfo — what the drop carries (decomp 230533)

UIElement_ItemList::InqDropIconInfo(dragElement, &objId, &containerId, &flags) reads the dragged element's properties via InqProperty(0x1000000f..0x10000014) and assembles the flag word (230595-230617): flags = (bit8 from 0x10000014) | (bit2 from 0x10000013) | (bit4 from 0x10000012) | (bit1 from var_39/0x10000011). The synthesis flag semantics hold: flags & 0xE == 0 ⇒ fresh drag from inventory (place-new); flags & 4 ⇒ within-list reorder (the source slot is m_lastShortcutNumDragged). objId = the dragged object guid; containerId = its source container. CONFIRMED (the bit→source mapping is the toolbar/inventory docs' HandleDropRelease).

5.4 The drag handler interface (ItemListDragHandler + RegisterItemListDragHandler)

UIElement_ItemList::RegisterItemListDragHandler(list, handler) stores this->m_dragHandler = handler (decomp 230461-230464). Each panel registers ITSELF as the handler on every slot list (toolbar §5, paperdoll §2a). On a drop, the list routes to the handler's HandleDropRelease, which resolves the target slot + the InqDropIconInfo payload and issues the wire action (the per-panel docs). The shared contract the spine defines is: ItemListDragHandler { OnItemListDragOver(list, target, drag); HandleDropRelease(msg) } + RegisterItemListDragHandler(handler).

5.5 Drag-ghost / cursor lifecycle

m_dragIcon (id 0x10000345) is created in PostInit from a DBObj and kept hidden (SetVisible(0), decomp 229738-229740); on begin-drag the list makes the global m_dragElement track the cursor (the translucent icon copy), and on drop it is hidden again. The drag-ghost graphic is the SAME m_pDragIcon the icon compositor built (§3.2) — base + overlay, no underlay. CONFIRMED.

5.6 What acdream's UiRoot already has vs. needs

Already there (verified UiRoot.cs): DragSource/DragPayload (cs:71-73), BeginDrag (cs:450), UpdateDragHover emitting DragOver/DragEnter/DragLeave (cs:458-482), FinishDrag emitting DropReleased with an accepted flag (cs:484-496), the 3-pixel DragDistanceThreshold promote-on-move (cs:84,183-189), and the retail 0x15→0x21→0x1C→0x3E chain noted in the comment (cs:448). CapturesPointerDrag on UiElement distinguishes interior-drag from window-move.

Needs to grow: a per-cell accept test hook (the retail m_dragDropCallback / CatchDroppedItemUiField only NAMES these in its doc-comment, it does NOT implement them: UiField.cs:7-11 "Carries retail Field's drag-drop hooks (CatchDroppedItem/MouseOverTop) as stubs for future item-window use" — there is no such method body in the class). So the spine adds: (1) an OnDragOver→accept/reject result on UiItemSlot that flips its DragAccept overlay state, (2) an OnDrop that calls the panel's drag handler with the resolved {objId, srcContainer, flags}, and (3) the m_dragIcon translucent ghost as the drag visual. CONFIRMED gap.

5.7 Generic pick-up → drag → drop → dispatch (pseudocode)

on left-press over an OCCUPIED UiItemSlot:        # retail msg 0x21 path
    UiRoot.Captured = slot; _dragCandidate = true
on mouse-move while captured & moved > 3px:
    UiRoot.BeginDrag(slot, payload = { objId = slot.itemID,
                                       srcContainer = weenie._containerID,
                                       srcSlotIndex = slot.shortcutNum })
    show slot.m_dragIcon tracking the cursor       # retail m_dragElement
on drag-over a target UiItemSlot/UiItemList:       # retail msg 0x3e / MouseOverTop
    accepted = targetHandler.OnDragOver(target, payload)   # m_dragDropCallback
    target.SetDragAccept(accepted ? Accept(0x10000041) : Reject(0x10000040))
on drag leaving the target:
    target.SetDragAccept(Neutral 0x1000003f)
on release over target:                            # retail msg 0x15 / CatchDroppedItem
    info = InqDropIconInfo(payload)                # objId, srcContainer, flags
    targetHandler.HandleDropRelease(target, info)  # per-panel: picks the opcode:
        # toolbar slot  : flags&0xE==0 -> CreateShortcutToItem ; flags&4 -> reorder
        # pack slot      : PutItemInContainer 0x0019
        # equip slot     : GetAndWieldItem 0x001A (target's EquipMask)
        # ground         : DropItem 0x001B
        # compatible stack: StackableMerge 0x0054 / split dialog -> Stackable*Split*
        # NPC            : GiveObjectRequest 0x00CD
    slot.SetWaitingState(true)                      # speculative ghost until server confirm
    hide drag ghost; clear DragSource
on server reply (move event) or rollback (InventoryServerSaveFailed 0x00A0):
    slot.SetWaitingState(false); UIItem_Update(...)  # confirm or revert

The opcode-selection table is the per-panel docs' job (already covered); the spine owns the pick-up → ghost → accept-test → release → InqDropIconInfo → dispatch-to-handler chain above.


6. New toolkit widgets this spine introduces

Widget Registers at Leaf vs container Purpose
UiItemSlot (port of UIElement_UIItem, class 0x10000032) resolved class id 0x10000032 (resolves to a UIElement_Field subclass ⇒ underlying Type 3); a behavioral leaf in DatWidgetFactory keyed off the resolved class id LEAF (ConsumesDatChildren=>true) — reproduces the icon + §2.2 overlay sub-elements procedurally one item-in-a-slot: composited icon (§3) + quantity UiText + capacity/structure UiMeters + 10-step cooldown ring + selected/ghosted/open-container/drag-accept/sell/trade overlay states; binds itemID (retail +0x5FC). The spine widget — build once.
UiItemList / UiItemGrid (port of UIElement_ItemList, class 0x10000031) resolved class id 0x10000031 (dump root 0x10000339, Type 268435505, 32×32 — CONFIRMED itemlist-0x2100003D.txt:13-23) leaf to the importer (ConsumesDatChildren=>true; manages its own UiItemSlot children procedurally) — logically a container of slots at runtime a 1-cell (toolbar/equip) or N-cell (inventory) grid of UiItemSlots; owns the drag handler registration. Port ItemList_AddItem/InsertItem/Flush/IsInList/GetNumUIItems/GetItem/OpenContainer/SetChildList/SetParentContainer/BeginDrag/DragOver/InqDropIconInfo/RegisterItemListDragHandler.

These exactly match the synthesis §2 / §7 rows — no correction. The UiViewport (Type 0xD), window manager, and sub-window-mount are NOT spine widgets (paperdoll / shared-infra; out of scope here). One precision the spine adds: the UiField Type-3 drag hooks are documented-but-unimplemented (§5.6) — the UiItemSlot is where they get a body, not the generic UiField.


7. Open questions / UNVERIFIED — resolved + carried forward

Resolved by this doc (synthesis §5 risks → now CONFIRMED):

  • #1 icon-composite render — RESOLVED. Each layer is a 0x06 RenderSurface decoded directly; the icon is a 5-layer composite (IconData::RenderIcons 407524). §3.
  • #2 +0x5FC field name — RESOLVED. It is UIElement_UIItem::itemID (decomp 230230). §2.1.
  • #14 identified-vs-unidentified does NOT swap the icon — CONFIRMED in the negative (no appraise branch in the icon path). §3.4.

Carried forward (still need a follow-up):

  • Effect-overlay blit into the slot surface (§3.2 layer 5) — the effect DBObj (GetByEnum(0x10000005, lsb(effects)+1)) is captured (arg2, 407575) but I did not see its explicit Blit into the m_pIcon surface in the 2013 BN lifting (the visible blits are type-default-underlay, custom-underlay, and the base+overlay drag layer). LIKELY it blits as part of the overlay stage; confirm with a Ghidra decompile of 0x0058d180 or a cdb trace before relying on the exact effect layering. UNVERIFIED.
  • Type-default underlay + effect-overlay enum→DID resolutionGetByEnum(0x10000004, …) / GetByEnum(0x10000005, …) resolve through the dat DidMapper/EnumMapper tables; the concrete 0x06 ids per item-type / effect were not enumerated. MVP can ship base-_iconID-only. §3.3. UNVERIFIED.
  • Paletted UI iconsGetOrUploadRenderSurface passes palette: null (TextureCache.cs:135), returning magenta on a paletted (INDEX16/P8) icon. Most item icons are pre-baked BGRA/DXT, but verify no item icon is paletted before shipping; if one is, wire a UI palette (the D.2b memory flags this as a known TODO). UNVERIFIED.
  • CreateObject IconId on a CONTAINED item (synthesis §5 risk #3) — byte-trace a live capture that ACE sets IconId on a non-3D-visible pack item's CreateObject vs. relying on PlayerDescription. LIKELY present; verify. (WireMCP capture of 0xF745.)
  • m_dragDropCallback shape — retail's per-field accept callback signature (callback(dragElement, this) -> bool, decomp 126124) is confirmed; the acdream binding (a delegate on UiItemSlot/the handler) is a design call for the build spec.

8. MEMORY.md index line

  • UI item-slot SPINE — icon composite + drag-drop — D.2b shared spine (completes the 5-doc arc): UiItemSlot(UIElement_UIItem 0x10000032, the +0x5FC bound id RESOLVED = itemID) inside UiItemList(0x10000031). ICON CRUX RESOLVED: each layer is a 0x06 RenderSurface decoded DIRECTLY via GetOrUploadRenderSurface, but the on-screen icon is a 5-layer runtime COMPOSITE (IconData::RenderIcons @407524: type-default underlay + _iconUnderlayID + _iconID base + _iconOverlayID+tint + effect overlay, blitted into one 32×32 surface; NOT a single texture, NOT appraise-gated). Drag-drop state machine: cell inherits UIElement_Field::MouseOverTop/CatchDroppedItem (drop-target rollover, attr 0x36) + ListenToElementMessage msgs 0x21 begin-drag/0x3e drag-over/0x15 drop; InqDropIconInfo flags 0xE==0 fresh-drag, &4 reorder; UiRoot already has the drag chain (0x15→0x21→0x1C→0x3E), UiField only STUBS the hooks. acdream gap: CreateObject discards IconId (cs:516). Sub-element id map + named states (ItemSlot_Empty 0x060074CF, DragOver Accept/Reject/DropIn 0x060011F9/F8/F7) included.