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>
557 lines
37 KiB
Markdown
557 lines
37 KiB
Markdown
# 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.
|