# 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 `selected` ↔ `weenObj->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 `waiting` → `SetWaitingState` (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 `UiMeter`s, 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 `0xc`** — `RenderIcons` 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 (`CreateLocalSurface` → `Create(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 `Graphic` → `m_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_ItemList`s, the own list (`+0x604`) and the open-other- container list (`+0x608`) (inventory §2.2). The outbound verbs are the `ACCWeenieObject:: UIAttempt*` family — `UIAttemptWield` → `Event_GetAndWieldItem` (decomp 407763, with a stack-split-to-wield branch when `_stackSize>1`), `UIAttemptPutInContainer` → `Event_PutItemInContainer` (407797), `UIAttemptPutIn3D` → `Event_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` / `CatchDroppedItem` — `UiField` 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 `UiMeter`s + 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 `UiItemSlot`s; 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 resolution** — `GetByEnum(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 icons** — `GetOrUploadRenderSurface` 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](research/2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md) — 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.