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

557 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.