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>
This commit is contained in:
Erik 2026-06-16 21:04:57 +02:00
parent 78c91875b8
commit a5c5126e8d
6 changed files with 2077 additions and 0 deletions

View file

@ -0,0 +1,115 @@
# Handoff — next UI phase: action bar / quick slots + inventory + equipment (paperdoll)
**Date:** 2026-06-16
**From:** the session that landed the D.2b widget generalization (merged to `main` at `78c9187`).
**Purpose:** kick off a **deep retail-faithful research phase** for the next three game panels, before any implementation. This doc + the new-session prompt at the bottom are the entry point.
---
## 1. Where we are (what you're building on)
The **D.2b retail-UI toolkit is complete and on `main`.** You have:
- **A generic, Type-registered widget toolkit** built by `DatWidgetFactory` from the dat `LayoutDesc`: `UiButton` (Type 1), `UiMenu` (6), `UiMeter` (7), `UiScrollbar` (11), `UiText` (12), plus `UiField` (editable, controller-placed) and `UiDatElement` (generic chrome/container fallback). All in `src/AcDream.App/UI/`.
- **The assembly pattern**: a window is a dat `LayoutDesc``LayoutImporter.Import(...)` walks the `ElementDesc` tree → `DatWidgetFactory` builds each element generically → a thin **`gm*UI::PostInit`-style controller** finds widgets by id (`layout.FindElement(id) as UiX`) and binds live data/behavior. See `VitalsController` and `ChatWindowController` for the two worked examples.
- **Key toolkit rules** (read `claude-memory/project_d2b_retail_ui.md` first — it's the START-HERE digest with the full DO-NOT-RETRY list):
- `UiElement.ConsumesDatChildren` — behavioral widgets are **leaf** (the importer doesn't build their dat sub-elements; they reproduce them procedurally).
- The base-chain Type resolution (`ElementReader.Merge`) already surfaces each element's real registered Type.
- Type 3 is **chrome/container** in acdream's layouts (stays `UiDatElement`), NOT a factory-registered editable Field.
**This phase's job is to build the next real game panels on that toolkit** — but they're complex (live item state, drag-drop, wire messages, icon rendering), so we research first per the project's mandatory **grep named → decompile → cross-ref → pseudocode → port** workflow (CLAUDE.md).
These panels are the roadmap's **F.5 / D.5 "core panels"** (Attributes / Skills / Paperdoll / Inventory / Spellbook).
---
## 2. The three targets + their retail entry points (concrete anchors)
All confirmed from the named decomp (`docs/research/named-retail/acclient_2013_pseudo_c.txt`):
### A. Action bar / quick slots → `gmToolbarUI` (element class `0x10000007`)
The retail "toolbar" is the shortcut bar. Confirmed methods (grep `gmToolbarUI::`):
- `UseShortcut(slot)`, `AddShortcut`, `RemoveShortcut`, `RemoveShortcutInSlotNum`, `FlushShortcuts`, `CreateShortcutToItem`, `IsShortcutEligible(ACCWeenieObject*)`, `IsShortcutSlotAvailable`, `GetFirstEmptyShortcutToTheRightOf`, `RecvNotice_AddShortcut/RemoveShortcut/UseShortcut`.
- Slots hold **`UIElement_UIItem`** widgets (`UIElement_UIItem::SetShortcutNum` / `SetDelayedShortcutNum`); the underlying object is an **`ACCWeenieObject`** with `SetShortcutNum`.
- Spell shortcuts: `UIElement_ItemList::ItemList_InsertSpellShortcut`, `CM_Magic::SendNotice_AddSpellShortcut`.
### B. Inventory → `gmInventoryUI` (element class `0x10000023`), `gmBackpackUI` (`0x10000022`)
The inventory window + nested backpacks/side-packs. Items are server-spawned **weenies** (`ACCWeenieObject`) — see `claude-memory/feedback_weenie_vs_static.md` (selectable/interactable items are server weenies, not dat-baked).
### C. Equipment / paperdoll → `gmPaperDollUI` (element class `0x10000024`), `gm3DItemsUI` (`0x10000021`)
The paperdoll window shows equipped/wielded items + the character doll. `gm3DItemsUI` is likely the 3D doll viewport (a rotating character model with equipped gear).
### Shared building blocks (the toolkit pieces these need that we DON'T have yet)
- **`UIElement_UIItem`** (element class `0x10000032`) — **the item-in-a-slot widget**: an icon (from the weenie's `IconDataID`), drag-drop (retail `UIElement_Field`'s `CatchDroppedItem` / `MouseOverTop` hooks — note our `UiField` already documents these as future drag-drop hooks), a shortcut number, a quantity/burden overlay, a selection/highlight. **This is the most important new generic widget** — all three panels are grids/lists of these.
- **`UIElement_ItemList`** (`0x10000031`) — a scrollable list/grid of items (retail's ListBox-for-items). Maps to a `UiListBox` (Type 5, not yet built) or a `UiItemGrid`.
- **The window manager** (the *other* deferred Plan-2 piece): open/close/z-order/persist, drag via Dragbar (Type 2), resize via Resizebar (Type 9). Inventory/paperdoll/toolbar are pop-up/dockable windows that need this.
- **Drag-drop infrastructure**: item drag between inventory ↔ equip ↔ toolbar ↔ ground, with the wire messages it triggers.
---
## 3. The research questions (the deep-research scope)
Produce a research doc per panel (or one combined doc) under `docs/research/` answering these, each with a **retail anchor** (named `class::method` + decomp line, or `LayoutDesc`/element-class id) and cross-referenced against the references in §4.
**Common / cross-cutting (do this first — it unblocks all three):**
1. **The dat `LayoutDesc` id** for each panel (`gmToolbarUI` / `gmInventoryUI` / `gmPaperDollUI`). The element-class ids above are the *registered class*; find the actual `LayoutDesc` (`0x21xxxxxx`) that builds each window. Use the `AcDream.Cli` layout-dump tools (`dump-vitals-layout <datdir> 0xId`, the `LayoutIndexDump`) and grep the decomp for the class's `Create`/`PostInit`.
2. **`UIElement_UIItem`** (`0x10000032`) — full port spec: what Type does it resolve to in the dat? How does it render an item icon from the weenie's `IconDataID` (→ `RenderSurface`/`Icon` overlay — cross-ref WorldBuilder/ACViewer icon decode)? How are quantity, burden, wielded/selected states drawn? What's the drag-drop state machine (`MouseOverTop`/`CatchDroppedItem`)?
3. **The item/container data model**: items are `ACCWeenieObject`s. How does the client learn container contents — which wire messages (CreateObject, the container/inventory messages), and how is the container hierarchy (main pack → backpacks → side-packs) represented? What does acdream already parse (cross-ref the wire-message catalog, §4)?
**Action bar (`gmToolbarUI`):**
4. The shortcut slot model — how many slots/bars, item vs spell shortcuts, what a slot stores (`ShortcutNum`), `IsShortcutEligible`.
5. Persistence + wire: is the toolbar server-persisted? What messages add/remove/use a shortcut (`RecvNotice_AddShortcut/RemoveShortcut/UseShortcut`) and what does activation send (use-item / cast-spell)?
6. Drag-from-inventory-to-slot + drag-to-reorder (`CreateShortcutToItem`, `GetFirstEmptyShortcutToTheRightOf`).
**Inventory (`gmInventoryUI` / `gmBackpackUI`):**
7. The window layout (the item grid, the side-pack tabs/list, burden/value summary).
8. The full set of inventory wire messages (server → client item arrival + client → server actions: pick up, drop, give, move-between-containers, split-stack, merge-stack). Cross-ref ACE (what the server sends/validates) + holtburger (what the client sends).
9. Icon rendering: weenie `IconDataID` → texture (+ any underlay/overlay/highlight for ID'd vs unidentified, wielded, selected).
**Equipment / paperdoll (`gmPaperDollUI` / `gm3DItemsUI`):**
10. The equip/wield slots — the coverage/location enum (which slots exist, their screen positions on the doll).
11. The wield/unwield wire messages (equip an item, the server's response, the resulting `ObjDesc` change on the character model).
12. The paperdoll rendering — is it the 2D doll image + slot icons, or a 3D character viewport (`gm3DItemsUI`)? How does it assemble the equipped-character appearance (cross-ref ACViewer's ObjDesc/CreaturePalette + WorldBuilder for the model)?
---
## 4. References (the hierarchy — cross-reference at least two per question)
Per CLAUDE.md's reference table:
- **Named retail decomp** (`docs/research/named-retail/acclient_2013_pseudo_c.txt` + `acclient.h` + `symbols.json`) — **primary oracle for the UI logic** (the `gm*UI` / `UIElement_UIItem` classes). Grep by `class::method`.
- **holtburger** (`references/holtburger/`) — **primary oracle for client behavior + wire**: what a client sends/receives for inventory, equip, use-item, drag-drop. Look in `client/` + `session/`.
- **ACE** (`references/ACE/Source/ACE.Server/`) — **server expectations**: the inventory/equip/move game-action handlers, what the server validates + broadcasts.
- **WorldBuilder** + **ACViewer****icon + 3D-model rendering**: `IconDataID` → texture decode (ACViewer `TextureCache.IndexToColor` / WorldBuilder `TextureHelpers`); the equipped-character ObjDesc assembly (ACViewer `StaticObjectManager` / `CreaturePalette`).
- **Chorizite.ACProtocol** (`references/Chorizite.ACProtocol/Types/*.cs`) — protocol field order for the inventory/equip messages.
**Existing acdream research/memory to read first (don't re-research what's done):**
- `claude-memory/project_d2b_retail_ui.md` — the toolkit + the find-by-id controller pattern (START HERE).
- `claude-memory/feedback_weenie_vs_static.md` — items are server weenies.
- `claude-memory/project_interaction_pipeline.md` — the existing WorldPicker / Select / UseSelected interaction (B.4) — the use/pickup path partly exists.
- `claude-memory/MEMORY.md` → the **wire-message catalog** research (`research/2026-06-04-wire-message-catalog.md`): 256 opcodes, what acdream parses vs stubs vs is-missing — the inventory/equip opcodes' parse status is in there.
- `docs/research/2026-06-15-layoutdesc-format.md` — the `LayoutDesc`/`ElementDesc` format (for reading the panel layouts).
- The `AcDream.Cli` layout-dump tools (`dump-vitals-layout`, `LayoutIndexDump`, `LayoutFixtureDump` from this session's Task 1) — for dumping any panel's `LayoutDesc`.
---
## 5. Deliverable + approach
**Report-only research** — no implementation, no code changes (use the `/investigate` discipline, or just produce research docs). Output: one or more `docs/research/2026-06-NN-*-deep-dive.md` docs (mirroring the existing `2026-06-04-*-deep-dive.md` set), each with:
- The retail anchors (class::method + decomp line; `LayoutDesc`/element-class ids).
- The wire-message catalog for the panel (direction, trigger, field layout, ACE handler, acdream parse status).
- The item/container model + the `UIElement_UIItem` port spec.
- The drag-drop mechanics.
- **A concrete "what the toolkit needs" list** — the new generic widgets (`UiItemSlot`/`UIElement_UIItem` port, `UiListBox`/item-grid, the window manager) + which `Type` they register at — so the *next* session can go straight to a brainstorm → spec → plan.
- An end-of-doc `MEMORY.md` index line.
**Suggested approach:** this is broad (3 panels × decomp + 5 references + dat + wire). A **multi-agent research Workflow** fits well — e.g. one agent per panel for the class/LayoutDesc/wire scoping, plus one agent on the shared `UIElement_UIItem`/icon-rendering/drag-drop spine, then synthesize. (Ultracode authorizes this.) Or run the panels sequentially. Either way, finish with a synthesis that names the new toolkit widgets.
---
## 6. New-session prompt (paste this into a fresh session)
> Deep retail-faithful **research phase** (report-only, no code) for acdream's next three UI panels, building on the now-complete D.2b widget toolkit (merged to `main`). **Read the handoff first: `docs/research/2026-06-16-action-bar-inventory-equipment-handoff.md`**, then `claude-memory/project_d2b_retail_ui.md`.
>
> **Targets + confirmed retail entry points** (named decomp): action bar / quick slots = `gmToolbarUI` (element class `0x10000007`); inventory = `gmInventoryUI` (`0x10000023`) + `gmBackpackUI` (`0x10000022`); equipment/paperdoll = `gmPaperDollUI` (`0x10000024`) + `gm3DItemsUI` (`0x10000021`). Shared spine: `UIElement_UIItem` (`0x10000032`, the item-in-a-slot widget) + `UIElement_ItemList` (`0x10000031`). Items are server `ACCWeenieObject` weenies.
>
> **Produce** a `docs/research/` deep-dive per panel (+ a synthesis) answering the §3 research questions in the handoff — each with a retail anchor (class::method + decomp line / `LayoutDesc` + element-class id), the panel's wire-message catalog (cross-ref holtburger=client, ACE=server, Chorizite.ACProtocol=field order), the item/container model, the `UIElement_UIItem` port spec + icon rendering (cross-ref WorldBuilder/ACViewer for `IconDataID`→texture), and the drag-drop mechanics. **End with a concrete "new generic widgets the toolkit needs" list** (the item-slot widget, an item list/grid, the window manager) + the `Type` each registers at, so the following session can brainstorm → spec → plan the build. Use a multi-agent research Workflow (one agent per panel + one on the shared item-slot/icon/drag-drop spine) — Ultracode is authorized. Follow the mandatory grep-named→cross-ref→pseudocode workflow; do not write implementation code this phase.

View file

@ -0,0 +1,191 @@
# Action bar / quick slots (`gmToolbarUI`) — retail-faithful deep dive
**Date:** 2026-06-16
**Panel:** action bar / shortcut bar — retail class `gmToolbarUI`, element class `0x10000007`, `LayoutDesc 0x21000016` (root element 300×122).
**Scope:** handoff §3 Q1 (LayoutDesc/element map) + Q4 (shortcut slot model) + Q5 (wire + persistence) + Q6 (drag-drop / reorder). Report-only; no code written this phase.
**Builds on:** the D.2b importer/widget toolkit (`src/AcDream.App/UI/` + `…/UI/Layout/`). The "spine" item-slot/icon doc referenced in the handoff prompt does **NOT exist** in this worktree (searched `**/*spine*`, `**/*item-slot*`, the named path — all NOT FOUND), so the `UIElement_UIItem` / `UIElement_ItemList` facts below are derived here directly from the decomp; a later synthesis should reconcile with the spine doc if it lands.
## 1. Summary + confidence legend
The retail toolbar is **one `gmToolbarUI` window** that contains **18 single-cell item slots** (two rows of 9: top `0x100001A7..AF`, bottom `0x100006B7..BF`), each slot a **`UIElement_ItemList` (element class `0x10000031`)** holding at most one **`UIElement_UIItem` (class `0x10000032`)**. A slot stores nothing but the item it currently holds; the persistent model is `ShortCutManager::shortCuts_[18]` (an array of `ShortCutData{ index_, objectID_, spellID_ }`) living on the `CPlayerModule`. Shortcuts are **server-persisted as a character option** — they arrive in the big `PlayerDescription` login message (the `CharacterOptionDataFlag::SHORTCUT` block, **already parsed by acdream**) and are mutated live by two C2S game actions: **`AddShortCut 0x019C`** and **`RemoveShortCut 0x019D`** (both already have outbound builders in acdream). Activation of a slot does **not** use a "use-shortcut" wire message — it routes through the ordinary **use-item** path (`ItemHolder::UseObject`), so it reuses acdream's existing B.4 interaction pipeline. Drag-from-inventory and drag-to-reorder are handled by `gmToolbarUI` being an `ItemListDragHandler` (multiple inheritance) whose `HandleDropRelease` resolves the target slot and calls `CreateShortcutToItem` / `AddShortcut` / `GetFirstEmptyShortcutToTheRightOf`.
The 2 Meters + 1 Scrollbar in the layout dump are **NOT** bar paging or extra vitals: they are the **selected-object Health & Mana meters** (`0x100001A1`/`0x100001A2`) and the **stack-size split slider** (`0x100001A4`), all inside the `m_pSelObjectField` sub-panel and **hidden by default** (`SetVisible(0)` in `PostInit`) — they appear only when you select an object / split a stack.
**Confidence legend** — **CONFIRMED** = quoted from named decomp or a reference file I opened; **LIKELY** = inferred from confirmed facts (source named); **UNVERIFIED** = educated guess, flagged.
## 2. LayoutDesc / element map (Q1) — CONFIRMED against `.layout-dumps/toolbar-0x21000016.txt`
`LayoutDesc 0x21000016` (Id 553648150). The dump's `Width=800 Height=600` is the LayoutDesc canvas; the **root element `0x10000191`** (ElementId 268435857, **Type `0x10000463` = the registered `gmToolbarUI` class type**) is **300×122** — that 300×122 matches the handoff's stated size and is the real window. The root's Type value `268435463 = 0x10000007`… correction: dump shows `Type = 268435463` which is `0x10000007` (the `gmToolbarUI` class id) — i.e. the root element registers as the panel class itself, exactly like `gmToolbarUI::GetUIElementType` returns `0x10000007` (decomp line 196707: `return 0x10000007;`). CONFIRMED.
### 2a. The 18 shortcut slots — element→slot-index map
`gmToolbarUI::InitShortcutArray` (decomp line 197051) wires the slots by walking `GetChildRecursive(this, <id>)` in order and `DynamicCast(0x10000031)` (= `UIElement_ItemList`), registering each with the drag handler and pushing into `m_shortcutSlots`. The push order **is** the slot index. The 18 ids extracted from the function body (decomp 197054197560):
| Slot # | Element id | Row | Dump X,Y (W×H) | Hotkey msg (use / select) |
|---|---|---|---|---|
| 0 | `0x100001A7` | top | 6,58 (32×32) | `0x10000042` / `0x1000004E` |
| 1 | `0x100001A8` | top | 38,58 | `0x10000043` / `0x1000004F` |
| 2 | `0x100001A9` | top | 70,58 | `0x10000044` / `0x10000050` |
| 3 | `0x100001AA` | top | 102,58 | `0x10000045` / `0x10000051` |
| 4 | `0x100001AB` | top | 134,58 | `0x10000046` / `0x10000052` |
| 5 | `0x100001AC` | top | 166,58 | `0x10000047` / `0x10000053` |
| 6 | `0x100001AD` | top | 198,58 | `0x10000048` / `0x10000054` |
| 7 | `0x100001AE` | top | 230,58 | `0x10000049` / `0x10000055` |
| 8 | `0x100001AF` | top | 262,58 | `0x1000004A` / `0x10000056` |
| 9 | `0x100006B7` | bottom | 6,90 | `0x1000004B` / `0x10000057` |
| 10 | `0x100006B8` | bottom | 38,90 | `0x1000004C` / `0x10000058` |
| 11 | `0x100006B9` | bottom | 70,90 | `0x1000004D` / `0x10000059` |
| 12 | `0x100006BA` | bottom | 102,90 | `0x10000132` / `0x10000138` |
| 13 | `0x100006BB` | bottom | 134,90 | `0x10000133` / `0x10000139` |
| 14 | `0x100006BC` | bottom | 166,90 | `0x10000134` / `0x1000013A` |
| 15 | `0x100006BD` | bottom | 198,90 | `0x10000135` / `0x1000013B` |
| 16 | `0x100006BE` | bottom | 230,90 | `0x10000136` / `0x1000013C` |
| 17 | `0x100006BF` | bottom | 262,90 | `0x10000137` / `0x1000013D` |
CONFIRMED — slot ids from `InitShortcutArray`; X/Y from the dump; hotkey msg ids from `gmToolbarUI::ListenToGlobalMessage` (decomp 197564). The hotkey routing:
- `0x10000042..0x1000004D``UseShortcut(this, msg-0x10000042, 1)` → slots **011**, **use** (arg3=1). (decomp 197576197591)
- `0x1000004E..0x10000059``UseShortcut(this, msg-0x1000004E, 0)` → slots **011**, **select** (arg3=0). (decomp 197592197606)
- `0x10000132..0x10000137``UseShortcut(this, 0xC..0x11, 1)` → slots **1217**, **use**. (decomp 197616197645)
- `0x10000138..0x1000013D``UseShortcut(this, 0xC..0x11, 0)` → slots **1217**, **select**. (decomp 197646197674)
The slot count `18` is independently confirmed by the header struct `ShortCutManager::shortCuts_[18]` (`acclient.h` line 3649236494: `struct __cppobj ShortCutManager : PackObj { ShortCutData *shortCuts_[18]; };`) and the login-restore loop `for (i=0; i<0x12; i++)` in `UpdateFromPlayerDesc` (decomp 198879). ACE's comment corroborates the UX: *"there are two rows. The top row is 1-9, the bottom row has no hotkeys"* (`Player_Character.cs:250`).
Slot template: each slot's `ElementDesc` has `BaseElement=0x100001B2` / `BaseLayoutId=553648150` (the dump's last element, `0x100001B2`, ElementId 268435890, which itself inherits `BaseElement=268436281 BaseLayoutId=553648189`). `0x100001B2` is the slot **prototype** (W=32 H=32) — i.e. the 18 slot elements are clones of one `UIElement_ItemList` prototype. LIKELY (from the dump's BaseElement chain; the resolved Type would surface 0x10000031 via `ElementReader.Merge`, exactly as the toolkit memory describes for Type-0 inheritance).
### 2b. The selected-object sub-panel + the "extra" widgets (resolves the prompt's "2 Meters / 1 Scrollbar = ?")
From `gmToolbarUI::PostInit` (decomp 198119) — all `GetChildRecursive` + `DynamicCast`:
| Element id | Field | DynamicCast Type | Dump location | Purpose |
|---|---|---|---|---|
| `0x1000019D` | `m_pUseObjectButton` | (button) | 55,27 (23×31) | the **Use** button (sprite `0x06001129`, Ghosted `0x0600120E`) |
| `0x100001A5` | `m_pExamineObjectButton` | (button) | 218,27 (22×31) | the **Examine/Appraise** button (sprite `0x06001127`) |
| `0x1000019E` | `m_pSelObjectField` | (Type 3 container) | 78,27 (140×31) | the selected-object info sub-panel (dump `0x1000019E`) |
| `0x1000019F` | `m_pSelObjectName` | `DynamicCast(0xC)` Text | child of A field | selected object's **name** |
| **`0x100001A1`** | `m_pSelObjectHealthMeter` | `DynamicCast(7)` **Meter** | child | **Meter #1 = target Health bar** |
| **`0x100001A2`** | `m_pSelObjectManaMeter` | `DynamicCast(7)` **Meter** | child | **Meter #2 = target Mana bar** |
| `0x100001A3` | `m_pStackSizeEntryBox` | `DynamicCast(0xC)` Text | child | the **stack-split number entry** (gets `NumberInputFilter`) |
| **`0x100001A4`** | `m_pStackSizeSlider` | `DynamicCast(0xB)` **Scrollbar** | 50,13 (90×14), Type 11 | **Scrollbar = the stack-split slider** |
`PostInit` ends (decomp 198307198310) by hiding all four: `m_pSelObjectHealthMeter/ManaMeter/StackSizeEntryBox/StackSizeSlider → SetVisible(0)`. **So the 2 Meters and the Scrollbar are NOT toolbar paging or persistent vitals — they are the on-demand "selected object" readout + the stack-split slider, hidden until needed.** CONFIRMED.
Panel-launcher buttons (open inventory/spellbook/etc.) wired into `m_buttonInfoArray` with a `panelID` attribute (`0x10000029`): `0x10000197, 0x10000198, 0x10000199, 0x1000055A, 0x1000019A, 0x1000019B, 0x100001B1` (decomp 198179198303). `0x100001B1` (X=238 W=63, sprite `0x06004CF7` Alphablend, with child `0x1000046C` = `m_pInventoryButtonDragOverlay`) is the **inventory button that also serves as a "drop item into your pack" target** (see §5). The `0x1000019C/0x10000196` Type-3 elements (sprites `0x0600112B/0x0600112C`) are decorative dividers; the `0x10000194` element drives `UpdateAmmoNumber` (the ammo-count readout, decomp 198081). Text0x34 in the pre-dump label = the 0x34 (52) text/field/image sub-elements across this whole tree (chrome + the above); they are NOT 52 slots.
## 3. Shortcut slot model (Q4) — CONFIRMED
**A slot holds an item, the player module holds the model.** Each `m_shortcutSlots[i]` is a `UIElement_ItemList`; `UseShortcut`/`RemoveShortcutInSlotNum` read the item via `UIElement_ListBox::GetItem(slot, 0)` then `DynamicCast(0x10000032)` (= `UIElement_UIItem`) and read the **object id at field offset `+0x5FC`** on the `UIItem` (decomp 196415, 196519, 196811: `*(uint32_t*)((char*)eax_1 + 0x5fc)`). That `+0x5FC` is the weenie/object id the slot points at. UNVERIFIED exact field name (offset only); LIKELY the `UIItem`'s bound object id.
**`ShortCutData` (the persistent unit)** — verbatim header (`acclient.h:36484`):
```c
struct __cppobj ShortCutData : PackObj {
int index_; // slot number (0..17)
unsigned int objectID_;// item guid (0 if spell shortcut)
unsigned int spellID_; // spell id (0 for item shortcut)
};
```
Constructed `CShortCutData(&var_10, index, objectID, spellID)` (decomp 489341: `index_=arg2; objectID_=arg3; spellID_=arg4`). For an **item** shortcut the toolbar always passes `spellID=0` (`CShortCutData(&var_10, i_1, arg2, 0)` in `AddShortcut`, decomp 196874).
**Number of slots / bars:** 18 slots in 2 visible rows of 9 (top row = hotkeys 1-9, bottom = no hotkeys but addressable via `UseShortcut(0xC..0x11)`). There is **no separate "bar paging"** — all 18 are always present; the layout just stacks two rows. CONFIRMED (§2a).
**Item vs spell shortcuts.** The data model has a `spellID_` slot, **but in practice the toolbar holds only items.** Confirmation from three angles:
1. The toolbar's add paths only ever construct item shortcuts (`AddShortcut`/`CreateShortcutToItem` pass `spellID=0`).
2. Spell shortcuts live in a **different** list — the spellbook's `m_spellList` via `UIElement_ItemList::ItemList_InsertSpellShortcut` (decomp 232294) and the spell-bar hotbars (the `SpellLists8` / `hotbar_spells` block, separate from `SHORTCUT`). `CM_Magic::SendNotice_AddSpellShortcut` (decomp 682275) is a **local UI notice** (dispatched via `gmGlobalEventHandler` to notice handlers), **not** a wire send and **not** routed to `gmToolbarUI`.
3. Chorizite's own comment on `ShortCutData.SpellId`: *"May not have been used in prod? … I don't think you could put spells in shortcut spot…"* (`ShortCutData.generated.cs:34`). CONFIRMED — the toolbar is item-only; the `spellID_`/spell-bar machinery is a separate spellbook concern (out of scope for the action-bar widget).
**`IsShortcutEligible(ACCWeenieObject*)`** (decomp 196261, `__stdcall`): returns true unless the object is null, **OR** it's the player itself / a creature you don't own, **OR** it's currently inside the open vendor's container. Logic (decomp 196268196300):
- if `(pwd._bitfield & 4) == 0` (not "owned"?) and not a player → fall through; else require `IsPlayer()`.
- then `if ((InqType() & 0x10) != 0)` (Creature type bit) require `IsPlayer()` to continue;
- then read `pwd._containerID`; eligible (`return 1`) **iff** `_containerID == 0` OR `_containerID != UISystem->vendorID` — i.e. anything not sitting in the currently-open vendor window is eligible. CONFIRMED (paraphrase of the branch tree).
**`IsShortcutSlotAvailable(slot)`** (decomp 196575): `slot` in range AND `UIElement_ItemList::GetNumUIItems(slot)==0` (empty). CONFIRMED.
**Activation — `UseShortcut(slot, useFlag)`** (decomp 196395):
1. Get the `UIItem` in the slot; read its object id from `+0x5FC`.
2. If a **target mode** is active (`UISystem->targetMode != TARGET_MODE_NONE`, e.g. a spell awaiting a target): `ClientUISystem::ExecuteTargetModeForItem(objId, targetMode)` then clear target mode. (decomp 196412196421)
3. Else if `useFlag != 0`: `ItemHolder::UseObject(objId, 0, 0)` — the **standard use-item** action. (decomp 196429)
4. Else (`useFlag==0`): `ACCWeenieObject::SetSelectedObject(objId, 0)` — just select it. (decomp 196433)
So **toolbar activation is the ordinary use-item path**, not a bespoke message. `ItemHolder::UseObject` (decomp 402923) has a **0.2 s throttle** (`m_timeLastUsed + 0.2`, decomp 402933) and then dispatches the use via the inventory-request path (`DetermineUseResult` → 0x0036 "Use" or 0x0035 "UseWithTarget"). LIKELY (the exact 0x0035/0x0036 branch is deep in `UseObject`; the throttle + dispatch are CONFIRMED, the opcode selection is inferred from acdream's existing `InteractRequests.cs` opcodes 0x0035/0x0036).
## 4. Wire + persistence (Q5)
### 4a. Persistence = a character option in `PlayerDescription` (login restore)
Shortcuts are saved server-side (ACE: `CharacterPropertiesShortcutBar`, `Player_Character.cs:235`) and shipped to the client **inside the `PlayerDescription` login message** in the `CharacterOptionDataFlag::SHORTCUT` (0x1) block — `count:u32` then `count × ShortCutData`. CONFIRMED in three refs:
- holtburger `events.rs:514-524` (`PlayerDescriptionEventData.shortcuts`, *"List of user-defined shortcuts for the action bar"* line 124).
- ACE `Player_Character.cs:238 GetShortcuts()` reads `Character.GetShortcuts(...)``List<Shortcut>` for the description.
- **acdream already parses this**: `PlayerDescriptionParser.cs:345-356` reads `count` then `ShortcutEntry(Index, ObjectGuid, SpellId, Layer)` per entry, exposed on `Parsed.Shortcuts`.
Client-side restore: `gmToolbarUI::UpdateFromPlayerDesc` (decomp 198838) → `FlushShortcuts()`, gets the `CPlayerModule`'s `ShortCutManager`, then `for (i=0; i<0x12; i++) { objId = shortCuts_[i]->objectID_ (+8); if (objId) AddShortcut(this, objId, i, 0); }` (decomp 198879198893). The `0` final arg = **do NOT echo to server** (it's already persisted). CONFIRMED.
### 4b. Live mutation — two C2S game actions
| Opcode | Name | Dir | Trigger | ACE handler | Chorizite type | acdream parse status |
|---|---|---|---|---|---|---|
| `0x019C` | AddShortCut | C→S | `AddShortcut(…, send=1)` builds `CShortCutData(slot,objId,0)``CM_Character::Event_AddShortCut` | `GameActionAddShortcut.Handle``Player.HandleActionAddShortcut(shortcut)``Character.AddOrUpdateShortcut(Index,ObjectId)` | `Character_AddShortCut { ShortCutData Shortcut }` | **builder present** (outbound `InventoryActions.BuildAddShortcut`, see note) |
| `0x019D` | RemoveShortCut | C→S | `RemoveShortcut(…, send=1)``CM_Character::Event_RemoveShortCut(slotIndex)` | `GameActionRemoveShortcut.Handle``Player.HandleActionRemoveShortcut(index)``Character.TryRemoveShortcut(index)` | `Character_RemoveShortCut { uint Index }` | **builder present** (`InventoryActions.BuildRemoveShortcut`) |
| (—) | shortcut list | S→C | login | part of `PlayerDescription` `SHORTCUT` block | `ShortCutData` in description | **parsed** (`PlayerDescriptionParser.cs:345`) |
Opcode values triple-confirmed: decomp `Event_AddShortCut` packs `*(uint32_t*)var_c = 0x19c` (decomp 679733) and `Event_RemoveShortCut` packs `0x19d` (decomp 680332); ACE `GameActionType.cs:77-78` (`AddShortCut=0x019C, RemoveShortCut=0x019D`); holtburger `opcodes.rs:371-374` (commented, same values).
**Wire field order — `ShortCutData` payload (16 bytes), CONFIRMED across 3 refs:**
```
Index : u32 (slot 0..17)
ObjectId : u32 (item guid; 0 for spell)
SpellId : u16 (LayeredSpell.id; 0 for item)
Layer : u16 (LayeredSpell.layer; 0 for item)
```
- Chorizite `ShortCutData.generated.cs:41-46` (`Index`, `ObjectId`, then `LayeredSpellId.Read` = u16 id + u16 layer).
- ACE `Shortcut.cs:33-42` `ReadShortcut` (`Index`, `ObjectId`, `ReadLayeredSpell`).
- holtburger `shortcuts.rs:13-34` (`index u32`, `object_id Guid`, `spell_id u16`, `layer u16`).
RemoveShortCut payload = just `Index:u32` (Chorizite `Character_RemoveShortCut.generated.cs:33`; ACE `GameActionRemoveShortcut.cs:9`; decomp packs `*(uint32_t*)eax_3 = arg1` at 680335).
**⚠ acdream builder field-naming bug to fix at port time (not a wire bug).** `InventoryActions.BuildAddShortcut(seq, slotIndex, objectType, targetId)` (`InventoryActions.cs:99-110`) writes 24 bytes = 8-byte envelope (`0xF7B1` + seq) + `slotIndex`(u32) + `objectType`(u32) + `targetId`(u32). The **byte layout is correct for item shortcuts** (slot, then guid, then a final dword that for items is `0` = SpellId|Layer), but the parameter names are wrong/misleading: the 2nd field is `Index`, the 3rd is `ObjectId`, and the 4th dword is `SpellId(u16)|Layer(u16)` — there is no separate "objectType". A faithful builder should take `(seq, uint index, uint objectGuid, ushort spellId, ushort layer)` and pack the spell as two u16s. For the toolbar's item-only use, callers must pass `objectGuid` as the 3rd arg and `0` as the 4th. LIKELY a latent bug if anyone wired a "objectType" semantic; flag in the divergence register when the toolbar lands. (CONFIRMED file contents; the "bug" judgment is mine.)
**ACE's reorder note (important UX contract):** *"When a shortcut is added on top of an existing item, the client automatically sends the RemoveShortcut command for that existing item first, then will add the new item, and re-add the existing item to the appropriate place."* (`Player_Character.cs:254`). This is exactly the `HandleDropRelease` sequence in §5. CONFIRMED.
## 5. Drag-drop for the toolbar (Q6) — CONFIRMED
`gmToolbarUI` multiply-inherits `ItemListDragHandler` (constructor sets the `ItemListDragHandler::vftable`, decomp 196680) and registers itself as the drag handler on **every** slot's `UIElement_ItemList` in `InitShortcutArray` (`RegisterItemListDragHandler(slot, &this->vtable)`, decomp 197069 etc.). Drops land in **`gmToolbarUI::HandleDropRelease`** (decomp 197971):
1. Read source `UIItem` (`ebp = msg.dwParam1+8`) and drop-target element (`ebx = msg.dwParam1+0x10`). (decomp 197974197976)
2. **If the target is the inventory button** (`ebx->m_desc.m_elementID == 0x100001B1`): this is "drop item into my pack." `InqDropIconInfo` extracts the dragged object id; then if owned by player → `CPlayerSystem::PlaceInBackpack(objId, 0)`, else → `ItemHolder::AttemptToPlaceInContainer(objId, playerId, …)`. (decomp 198031198056) — i.e. dropping on the inventory button moves the *real item* into your pack, it does not create a shortcut.
3. **Else (target is a shortcut slot):** find which slot `i` is the ancestor of the drop target (`IsAncestorOfMe(ebx, m_shortcutSlots[i])`, decomp 197991), `InqDropIconInfo(ebp, &objId, &var_4, &flags)`. Then on `objId != 0`:
- **drop flags `(flags & 0xE) == 0`** (a fresh drag from inventory, not a within-bar move): `RemoveShortcutInSlotNum(i, 1)` (evict whatever was there, returns its objId `eax_13`), `CreateShortcutToItem(objId, i, 1, 0)` (place the dragged item in slot `i`, send=1). If the evicted `eax_13` was a different item, `GetFirstEmptyShortcutToTheRightOf(i)` and `AddShortcut(eax_13, thatSlot, 1)` to relocate it. (decomp 198007198018)
- **else if `(flags & 4) != 0`** (a within-bar reorder, `m_lastShortcutNumDragged` is the source slot): `RemoveShortcutInSlotNum(i, 1)``AddShortcut(objId, i, 1)`; if an item was displaced and `IsShortcutSlotAvailable(m_lastShortcutNumDragged)`, put the displaced item back into the **vacated source slot** (`AddShortcut(eax_15, m_lastShortcutNumDragged, 1)`). (decomp 198020198027)
This is precisely ACE's "remove the existing one, add the new one, re-add the existing item to the appropriate place." CONFIRMED.
**Slot-resolution helpers (Q6 core):**
- **`CreateShortcutToItem(objId, slotOrNeg1, send, fromServer)`** (decomp 196905): null-check; get `ACCWeenieObject`; if `IsShortcutEligible`. If `slot != 0xFFFFFFFF``RemoveShortcut(objId,1); AddShortcut(objId, slot, 1)` (decomp 196928196930). Else (slot unspecified) it scans for a home (the loop at 196954+, with a "no empty slot" `DisplayStringInfo` notice when full, decomp 196945196949). This is the entry called by `RecvNotice_AddShortcut` and the keyboard "add selected to toolbar" (`0x1000010D``CreateShortcutToItem(selectedID, 0xFFFFFFFF, 1, 0)`, decomp 197613).
- **`AddShortcut(objId, slot, send)`** (decomp 196825): if `slot` out of range, find the **first empty** slot (linear scan, decomp 196836196848). Then `ItemList_Flush(slot); ItemList_AddItem(slot, objId); SetShortcutNum(weenie, slot)` (or `SetDelayedShortcutNum` if the weenie isn't loaded yet, decomp 196861196867). If `send`, build `CShortCutData(slot, objId, 0)``Event_AddShortCut` (wire) + `PlayerModule::AddShortCut` (local model) (decomp 196873196876).
- **`RemoveShortcut(objId, send)`** (decomp 196462): scan slots for the one containing `objId` (`ItemList_IsInList`), `ItemList_Flush`, `SetShortcutNum(weenie, 0xFFFFFFFF)`; if `send`, `Event_RemoveShortCut(slotIndex)` + `PlayerModule::RemoveShortCut(slotIndex)`; returns the slot index (or `0xFFFFFFFF`). (decomp 196471196496)
- **`RemoveShortcutInSlotNum(slot, send)`** (decomp 196502): read the `UIItem` objId at `+0x5FC`, `RemoveShortcut(objId, send)`, return the evicted objId. (decomp 196519196524)
- **`GetFirstEmptyShortcutToTheRightOf(slot)`** (decomp 196536): scan `slot+1 .. end` for an empty `ItemList` (`GetNumUIItems==0`); if none, wrap-scan `0 .. slot`; return `0xFFFFFFFF` if the bar is full. (decomp 196539196569)
- **`FlushShortcuts()`** (decomp 196442): `ItemList_Flush` every slot (visual clear; does NOT touch the server). Used by login restore. (decomp 196451196457)
## 6. New toolkit widgets this introduces
The toolbar needs the same item-slot spine the inventory/paperdoll need; it adds the slot-grid + drag-handler concept on top.
| Widget | dat Type it registers at | leaf vs container | Purpose |
|---|---|---|---|
| **`UiItemSlot`** (port of `UIElement_UIItem`, class `0x10000032`) | resolves to a class id, not a numeric toolkit Type (it's a `UIElement` subclass `0x10000032`, registered via `RegisterElementClass`, not Types 1-0x12); in acdream's factory this is a **new behavioral leaf widget** | **leaf** (`ConsumesDatChildren=>true`) | the item-in-a-slot: icon from weenie `IconId` (+ underlay/overlay/highlight), stack-size + selection state, holds the bound object id (retail `+0x5FC`). **Shared with inventory + paperdoll** — build once. |
| **`UiItemList`** (port of `UIElement_ItemList` / `UIElement_ListBox`, class `0x10000031`) | new behavioral widget at class `0x10000031` (the dump shows it as the slot prototype `0x100001B2`'s resolved class; Type-5 `ListBox` is the generic relative but item lists are the specialized `0x10000031`) | **leaf** wrt the importer (it manages its own `UIItem` children procedurally) | a 1-cell (toolbar) or N-cell (inventory) container of `UiItemSlot`s; exposes `AddItem/Flush/IsInList/GetNumUIItems/GetItem`. **Shared.** |
| **`ToolbarController`** (the `gmToolbarUI::PostInit`-style binder) | not a widget — a controller (like `VitalsController`/`ChatWindowController`) | n/a | finds the 18 slots by id, the use/examine buttons, the selected-object meters/name, the stack slider; binds `UseShortcut`/`AddShortcut`/`RemoveShortcut`; restores from `Parsed.Shortcuts`; sends 0x019C/0x019D. |
| **drag-handler seam** | n/a (an interface on `UiItemList` + the controller) | n/a | port of `ItemListDragHandler``OnItemListDragOver` / `HandleDropRelease` (slot resolution from §5). The toolkit's `UiRoot` already has drag-drop input plumbing (per the d2b memory: *"UiRoot already has full input (focus/capture/drag-drop/tooltip/click)"*), so this is a binding, not new infra. |
**Reuses (no new widget needed):** `UiMeter` (Type 7) for the two selected-object bars; `UiText`/`UiField` (Type 12 / the controller-placed editable) for the name + stack-size box; `UiScrollbar` (Type 11) for the stack slider; `UiButton` (Type 1) for Use/Examine/panel-launchers; `UiDatElement` for chrome. The window-manager (open/close/z-order/persist + grip/dragbar drag from D.2b Plan-2) is needed for show/hide + persisting position, same as inventory/paperdoll — it is **not toolbar-specific**.
## 7. Open questions / UNVERIFIED
- **`UIElement_UIItem +0x5FC` field name** — confirmed as the bound object id by offset only; the symbolic field name is UNVERIFIED. Cross-check against the spine doc's `UIItem` port if/when it exists, or grep `UIElement_UIItem::SetShortcutNum`/`UIItem_GetState`.
- **Exact use-item opcode `UseObject` sends (0x0035 vs 0x0036)**`ItemHolder::UseObject` throttle + dispatch CONFIRMED; the precise opcode branch (`DetermineUseResult`) was not traced to the send. acdream's `InteractRequests.cs` already has both (0x0035 UseWithTarget, 0x0036 Use); reconcile when wiring activation.
- **`UseShortcut` target-mode path** — `ClientUISystem::ExecuteTargetModeForItem` (for "use item on a target", e.g. a healing kit) is out of scope for the action-bar widget itself; it depends on the target-mode subsystem (cursor target picking). File as a follow-up.
- **`SetDelayedShortcutNum`** — the "weenie not loaded yet" deferral path (`AddShortcut` decomp 196867) needs a small state machine on the slot to re-bind once `CreateObject` for that guid arrives. Note for the controller port; not yet detailed here.
- **Root element Type value** — the dump prints the root's `Type = 268435463` (=`0x10000007`) for `0x10000191` but some other top-level dump fields print `Type = 268435463` ambiguously; I read it as the panel class id, consistent with `GetUIElementType`. LIKELY; verify with `ElementReader.Merge` when the importer runs over `0x21000016`.
- **Spell-on-toolbar** — declared dead (Chorizite + the toolbar's item-only add paths). If a future server/ACE variant DOES persist a spell shortcut (`spellID_!=0`), the `UiItemSlot` would need a spell-icon branch. Low priority; the wire field exists so parsing already handles it.
## 8. MEMORY.md index line
- [Action bar / quick slots (`gmToolbarUI`) deep dive](research/2026-06-16-action-bar-toolbar-deep-dive.md) — 18 item slots (2 rows of 9, ids `0x100001A7-AF`+`0x100006B7-BF`) = `UIElement_ItemList`(0x10000031) of one `UIElement_UIItem`(0x10000032); model `ShortCutManager::shortCuts_[18]` persisted in `PlayerDescription`'s SHORTCUT block (acdream already parses it); live mutate via `AddShortCut 0x019C`/`RemoveShortCut 0x019D` (acdream builders present — fix `BuildAddShortcut` field naming); activation = ordinary use-item (`ItemHolder::UseObject`, no special wire); the 2 Meters + Scrollbar in `0x21000016` are the hidden selected-object Health/Mana bars + the stack-split slider, NOT paging; drag-drop via `gmToolbarUI : ItemListDragHandler::HandleDropRelease` (`CreateShortcutToItem`/`GetFirstEmptyShortcutToTheRightOf`). New toolkit widgets: `UiItemSlot` + `UiItemList` (shared spine) + `ToolbarController`.

View file

@ -0,0 +1,416 @@
# Equipment / Paperdoll panel — retail-faithful deep-dive
**Date:** 2026-06-16
**Scope:** D.2b "core panels" research phase, the equipment/paperdoll target from
`docs/research/2026-06-16-action-bar-inventory-equipment-handoff.md` §3 Q1 + Q10/Q11/Q12.
**Status:** REPORT-ONLY. No code changed. The deliverable is this doc.
**Panels:** `gmPaperDollUI` (element class `0x10000024`, LayoutDesc `0x21000024`) and
`gm3DItemsUI` (element class `0x10000021`, LayoutDesc `0x21000021`).
## 1. Summary + confidence legend
The retail **paperdoll** (`gmPaperDollUI`) is a **3D character viewport plus ~25
single-cell equip slots**, NOT a 2D doll image. The window's element `0x100001D5`
(Type `13` = `UIElement_Viewport`) hosts a live `CreatureMode` mini-scene; the
character's `CPhysicsObj` is cloned from the player and re-dressed via the SAME
ObjDesc machinery the in-world renderer uses (`DoObjDescChangesFromDefault`). Every
equip slot is a **single-cell `UIElement_ItemList` (class `0x10000031`)**, one per
`EquipMask` location, mapped element-id → coverage-mask by
`gmPaperDollUI::GetLocationInfoFromElementID`. Equipping is the
`GetAndWieldItem` game action (opcode `0x001A`, `item_guid + EquipMask`); the
server's visible reply is `ObjDescEvent` (`0xF625`) which triggers
`RedressCreature`. **acdream already parses `ObjDescEvent` (0xF625) and the full
ObjDesc/ModelData block, and already has a complete per-instance animated-character
render path** (`EntitySpawnAdapter``AnimatedEntityState` with palette/part/hidden-
part overrides). The paperdoll viewport can REUSE that path — the gap is a
**`UiViewport` (Type 0xD) widget** that renders a single entity into a UI rect (a
scissored mini 3D pass), an **equip-slot variant of the item-slot widget**
(`UIElement_ItemList` 0x10000031, single cell), and the **window manager**.
`gm3DItemsUI` (0x21000021) is a SEPARATE "Contents of Backpack" pane (an
`UIElement_ItemList` + a text label + a scrollbar), NOT the doll — it does not host
a viewport.
`gm3DItemsUI` is misnamed for our purposes: despite "3DItems", its `PostInit` wires
a `m_itemList` (`UIElement_ItemList`) and a `m_contentsText` and sets the text to
"Contents of Backpack". It is an inventory contents list, addressed by the inventory
deep-dive; included here only because the handoff paired it with the paperdoll.
**Confidence legend:**
- **CONFIRMED** — quoted from a source I opened (decomp line / file:line).
- **LIKELY** — inferred from confirmed facts; the inference is named.
- **UNVERIFIED** — educated guess; flagged loudly.
**Note on a missing input:** the handoff promised a "spine agent" doc at
`docs/research/2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md` and the
START-HERE memory `claude-memory/project_d2b_retail_ui.md`. **Both are NOT FOUND in
this worktree** (`Glob **/project_d2b_retail_ui.md` and `**/*spine*.md` returned
nothing). I therefore re-derived the icon/item-model claims I needed from primary
sources (decomp + acclient.h + ACE + ACViewer + acdream source) rather than citing a
doc I could not open. Where this overlaps the spine's scope (icon decode, the
`UIElement_UIItem` widget, container model) I keep it terse and defer to the spine
doc once it lands.
## 2. LayoutDesc / element map
### 2a. Paperdoll `gmPaperDollUI` 0x10000024 → LayoutDesc 0x21000024 (224×214)
**CONFIRMED** registration: `gmPaperDollUI::Register` (decomp line 174445):
`UIElement::RegisterElementClass(0x10000024, gmPaperDollUI::Create);`. Pre-dump
`.layout-dumps/paperdoll-0x21000024.txt` root `0x100001D4` is 224×214, Type
`268435492 = 0x10000024` (the gmPaperDollUI class). **CONFIRMED.**
Construction chain: `gmPaperDollUI::gmPaperDollUI` (line 174228) calls
`UIElement_Field::UIElement_Field(this, ...)` — i.e. the paperdoll IS-A Field
subclass (matters for drag-drop: it inherits Field's drop hooks). The slot/viewport
wiring happens in the init routine that calls `GetChildRecursive` per id
(lines 175480-175548) — the analog of a `PostInit`. **CONFIRMED.**
Key elements in 0x21000024 (from the pre-dump + the init routine):
| Element id | dump Type | Resolves to | Role | Anchor (cite) |
|---|---|---|---|---|
| `0x100001D4` | 0x10000024 | gmPaperDollUI (root) | window | dump:13 |
| `0x100001D5` | **13** | `UIElement_Viewport` (0xD) | **the 3D character doll** | dump:125; `m_pPaperDoll = GetChildRecursive(this,0x100001d5)->DynamicCast(0xd)` line 175509-175517 |
| `0x100001D6` | 0 → base 0x100002BF/0x21000080 | `m_paperDollDragMask` | doll click/drag mask region (100×214) | dump:157; line 175538 |
| `0x1000046D` | 0 → base | `m_paperDollDragOverlay` | drag overlay sprite (32×32) | dump:173; line 175539 |
| `0x10000595` | 0 → ItemList | `m_sigilOneItem` (SigilOne 0x10000000) | aetheria sigil slot, hidden by default | line 175540-175542 |
| `0x10000596` | 0 → ItemList | `m_sigilTwoItem` (SigilTwo 0x20000000) | sigil slot | line 175543-175545 |
| `0x10000597` | 0 → ItemList | `m_sigilThreeItem` (SigilThree 0x40000000) | sigil slot | line 175546-175548 |
| `0x100005BE` | 0 → Button base 0x21000044 | a `UIElement_Button` | the close/expand button (120×14) | dump:349; line 175549 |
| ~25 more `0x1000xxxx` ids | **0** → base `0x100001E4` | single-cell `UIElement_ItemList` (0x10000031) | the equip slots (§3) | dump:29-476 |
The shared equip-slot base chain (**CONFIRMED**):
- Each slot element has `Type = 0`, `BaseElement = 268435940 = 0x100001E4`,
`BaseLayoutId = 553648164 = 0x21000024` (dump e.g. lines 33,49,65…).
- Element `0x100001E4` (dump:477) has `Type = 0`, `BaseElement = 268436281 =
0x10000339`, `BaseLayoutId = 553648189 = 0x2100003D`.
- `0x2100003D` root element `0x10000339` (`.layout-dumps/itemlist-0x2100003D.txt:16`)
has `Type = 268435505 = 0x10000031` = `UIElement_ItemList`, 32×32.
⇒ **every paperdoll equip slot resolves (via `ElementReader.Merge` zero-wins-base
Type resolution) to `UIElement_ItemList` 0x10000031, a single 32×32 cell.**
The init routine confirms each is cast to ItemList and registered as a drag target,
e.g. (line 175485-175496):
```
eax_66 = GetChildRecursive(this, 0x100005b2); // LowerLegArmor slot
eax_67 = eax_66->vtable->DynamicCast(0x10000031); // → UIElement_ItemList
this->m_lowerLegSlot = eax_67;
UIElement_ItemList::RegisterItemListDragHandler(eax_67, &this->vtable);
this->m_lowerLegSlot->vtable->SetVisible(0); // hidden until an item lands
```
**CONFIRMED.** Slots default invisible and are shown only when occupied (the empty
slot shows the doll body behind it; an occupied slot shows the item icon).
### 2b. gm3DItemsUI 0x10000021 → LayoutDesc 0x21000021 (234×120) — NOT the doll
**CONFIRMED** registration: `gm3DItemsUI::Register` (line 176723):
`UIElement::RegisterElementClass(0x10000021, gm3DItemsUI::Create);`.
`gm3DItemsUI::PostInit` (line 176728-176745):
```
this->m_contentsText = UIElement::GetChildRecursive(this, 0x100001c5);
eax_1 = UIElement::GetChildRecursive(this, 0x100001c6);
this->m_itemList = eax_1->vtable->DynamicCast(0x10000031); // UIElement_ItemList
... UIElement_Text::SetText(this->m_contentsText, u"Contents of Backpack");
```
Pre-dump `.layout-dumps/items3d-0x21000021.txt`: root `0x100001C4` (234×120, Type
`268435489 = 0x10000021`), child `0x100001C5` (text, base 0x10000436/0x21000077),
child `0x100001C6` (the ItemList grid, base 0x100002B9/0x2100003D — same ItemList
base as the slots), child `0x100001C7` (a scrollbar-shaped 16×96, base
0x100002C7/0x2100003E). **No Viewport element.** ⇒ gm3DItemsUI is a scrollable
**item-contents list**, not a 3D doll. **CONFIRMED.** (The "3D" in the name is
historical; it has no `UIElement_Viewport` and no `CreatureMode`.)
## 3. Equip-slot model + the coverage / location enum
### 3a. The element-id → EquipMask mapping (`GetLocationInfoFromElementID`)
`gmPaperDollUI::GetLocationInfoFromElementID(elementId, out uint mask, out UI_SLOT_SIDE side)`
(decomp line 173620) is a giant switch. It is the SSOT for which slot is which. The
mask values are exactly ACE's `EquipMask` (`ACE/Source/ACE.Entity/Enum/EquipMask.cs`).
**CONFIRMED** — full table below (decomp line / mask / EquipMask name / SLOT_SIDE):
| Element id | mask (hex) | EquipMask name | SLOT_SIDE | decomp line |
|---|---|---|---|---|
| `0x100005AB` | `0x1` | HeadWear | NULL | 173723 |
| `0x100001E2` | `0x2` | ChestWear | NULL | 173688 |
| `0x100001E3` | `0x40` | UpperLegWear | NULL | 173694 |
| `0x100005B0` | `0x20` | HandWear | NULL | 173753 |
| `0x100005B3` | `0x100` | FootWear | NULL | 173771 |
| `0x100005AC` | `0x200` | ChestArmor | NULL | 173729 |
| `0x100005AD` | `0x400` | AbdomenArmor | NULL | 173735 |
| `0x100005AE` | `0x800` | UpperArmArmor | NULL | 173741 |
| `0x100005AF` | `0x1000` | LowerArmArmor | NULL | 173747 |
| `0x100005B1` | `0x2000` | UpperLegArmor | NULL | 173759 |
| `0x100005B2` | `0x4000` | LowerLegArmor | NULL | 173765 |
| `0x100001DA` | `0x8000` | NeckWear | NULL | 173640 |
| `0x100001DB` | `0x10000` | WristWearLeft | LEFT | 173646 |
| `0x100001DD` | `0x20000` | WristWearRight | RIGHT | 173658 |
| `0x100001DC` | `0x40000` | FingerWearLeft | LEFT | 173652 |
| `0x100001DE` | `0x80000` | FingerWearRight | RIGHT | 173664 |
| `0x100001E1` | `0x200000` | Shield | NULL | 173682 |
| `0x100001E0` | `0x800000` | MissileAmmo | NULL | 173676* |
| `0x100001DF` | `0x3500000` | (weapon composite — see 3b) | NULL | 173670 |
| `0x100005E9` | `0x8000000` | Cloak | NULL | 173777 |
| `0x10000595` | `0x10000000` | SigilOne | NULL | 173705 |
| `0x10000596` | `0x20000000` | SigilTwo | NULL | 173711 |
| `0x10000597` | `0x40000000` | SigilThree | NULL | 173717 |
| `0x1000058E` | `0x4000000` | TrinketOne | NULL | 173630 |
\* **`0x100001E0`** — the decomp pseudo-C shows `*arg3 = "activation type (%s)…"`
(a string-pointer artifact where the Binary Ninja lifter lost the immediate). The
preceding/following cases are `0x200000` (Shield) and `0x200000`/`0x40`, and the only
remaining ready-slot mask not otherwise assigned in this switch is `MissileAmmo
(0x00800000)`. So **`0x100001E0` = MissileAmmo `0x800000` (LIKELY** — inferred from
the EquipMask gap + neighbors; the literal value is corrupted in the decomp).
`UI_SLOT_SIDE` (CONFIRMED `acclient.h:4546`): `SLOT_SIDE_NULL=0, SLOT_SIDE_LEFT=1,
SLOT_SIDE_RIGHT=2`. SIDE distinguishes the paired jewelry slots (left/right
wrist + finger) that share the same wear concept but different physical sides.
### 3b. The weapon composite slot `0x3500000`
`0x100001DF → 0x3500000` = `MeleeWeapon(0x100000) | MissileWeapon(0x400000) |
TwoHanded(0x2000000) | Held(0x1000000)` (= `0x3500000`). **CONFIRMED** by bit
decomposition against EquipMask.cs. This is the single "weapon hand" doll slot that
accepts any wieldable weapon. `OnItemListDragOver` has a special case at line 174302:
`if (ecx_3 == 0x200000 && (eax_3 & 0x100000) != 0) eax_3 |= ecx_3;` — i.e. a
melee-capable item may also drop into the Shield(0x200000) slot test. **CONFIRMED.**
### 3c. How the client knows what is equipped — `GetUpperInvObj(mask)`
`gmPaperDollUI::GetUpperInvObj(uint coverageMask)` (line 174565) is how the doll
finds the item currently in a slot:
```
eax = ClientObjMaintSystem::GetWeenieObject(player_id);
eax_3 = ACCWeenieObject::GetInvPlacementList(eax); // PackableList<InventoryPlacement>
for (i = eax_3->head; i; i = i->next) {
if (arg2 & i->data.loc_) // coverageMask & placement.loc_
eax_5 = InventoryPlacement::DetermineHigherPriority(...);
}
return iid; // the equipped item's guid
```
`InventoryPlacement` (**CONFIRMED** `acclient.h:33178`):
```cpp
struct InventoryPlacement : PackObj { uint iid_; uint loc_; uint priority_; };
```
So the player weenie carries a **`PackableList<InventoryPlacement>`** where each
node is `{itemGuid, locationMask (EquipMask), priority}`. `loc_` is the EquipMask
slot; `priority_` resolves overlap (e.g. armor over clothing on the same body part —
this is `CoverageMask` priority, `ACE/Source/ACE.Entity/Enum/CoverageMask.cs`).
**CONFIRMED.** The paperdoll reads this list to populate each slot's icon and to
drive part-selection lighting (`GetSelectionMaskFromObject`, line 174762, maps an
item guid back to which doll body parts to highlight, via the same masks).
**Cross-ref ACE:** `EquipMask` (loc) and `CoverageMask` (priority) are documented in
ACE as "sent as loc / in the priority field of the equipped-items list portion of the
player description event F7B0-0013" (`EquipMask.cs:5-6`, `CoverageMask.cs:6-7`).
**CONFIRMED** — this is the same `InventoryPlacement {iid, loc, priority}` triple the
client stores, populated from PlayerDescription's equipped section.
**acdream parse status of the placement list:** PARTIAL. `PlayerDescriptionParser`
(0x0013) "walks all sections through enchantments; the trailing options / inventory /
**equipped** sections are partial" (`PlayerDescriptionParser.cs:70-77`). So acdream
does NOT yet surface the equipped `InventoryPlacement` list. The per-item equip
*state* is, however, available from `CreateObject`/`ObjDescEvent` ModelData
(palette/part swaps already applied to the model). **CONFIRMED** (parser comment).
## 4. Wield / unwield wire + the ObjDesc change
### 4a. Wire table
| Opcode | Name | Dir | Trigger | ACE handler | Chorizite type | acdream parse status |
|---|---|---|---|---|---|---|
| `0x001A` (GameAction) | GetAndWieldItem | C→S | drop an item onto an equip slot / doll (auto-wield) | `GameActionGetAndWieldItem.Handle` (`Actions/GameActionGetAndWieldItem.cs:7-14`) → `Player.HandleActionGetAndWieldItem(itemGuid, EquipMask)` | `Inventory_GetAndWieldItem` (`C2S/Actions/Inventory_GetAndWieldItem.generated.cs:14-42`: `uint ObjectId; EquipMask Slot`) | **MISSING** (no sender in acdream; `Grep GetAndWieldItem\|0x001A src` finds only the UI font-property 0x1A, unrelated) |
| `0x0019` (GameAction) | PutItemInContainer / move-to-pack (un-wield) | C→S | drag a wielded item back into a pack | ACE `GameActionPutItemInContainer` | `Inventory_PutItemInContainer*` | MISSING (inventory deep-dive scope) |
| `0xF625` | ObjDescEvent | S→C | server applies/removes the wielded item → appearance change | `GameMessageObjDescEvent` ctor → `worldObject.SerializeUpdateModelData` (`Messages/GameMessageObjDescEvent.cs:10-17`) | (ModelData block) | **PARSED**`ObjDescEvent.cs:33-73` (opcode `0xF625`, `CreateObject.ReadModelData`) |
| `0xF745`/`0x0024` (CreateObject) | CreateObject | S→C | the wielded item object itself arrives | ACE creation message | `Item_CreateObject` | PARSED — `CreateObject.cs` |
| `0xF7B0`/`0x0013` (GameEvent) | PlayerDescription (equipped list) | S→C | full state incl. `InventoryPlacement` equipped section | `GameEventPlayerDescription.WriteEventBody` | `Login_PlayerDescription` | **PARTIAL**`PlayerDescriptionParser.cs` (equipped section not surfaced) |
Wire payload of `GetAndWieldItem` (**CONFIRMED** both refs agree):
- ACE reads `uint itemGuid; (EquipMask)int32 location` (`GameActionGetAndWieldItem.cs:10-11`).
- Chorizite writes `uint ObjectId; (uint)EquipMask Slot` (`.generated.cs:38-41`).
- holtburger sends `GetAndWieldItem { item_guid, equip_mask }`
(`holtburger-core/src/client/commands.rs:808-814`):
```rust
self.send_game_action(GameAction::GetAndWieldItem(Box::new(
GetAndWieldItemActionData { item_guid: item, equip_mask: target_mask })))
```
with `target_mask` resolved by `resolve_and_clear_slots(item, slot)` (line 799) —
i.e. the client picks the EquipMask for the target slot, exactly like the doll's
`GetLocationInfoFromElementID`. **CONFIRMED.**
`GameActionType.GetAndWieldItem = 0x001A` (**CONFIRMED**
`ACE/Source/ACE.Server/Network/GameAction/GameActionType.cs:14`).
### 4b. The ObjDesc change on the model (`ObjDescEvent``RedressCreature`)
Server side: equipping changes the creature's `ObjDesc` (clothing base, sub-palettes,
texture changes, anim-part swaps) and broadcasts `ObjDescEvent (0xF625)` carrying the
FULL new appearance (ACE comment: "It contains the entire description of what they're
wearing", `GameMessageObjDescEvent.cs:6-9`).
Client side: `gmPaperDollUI::RecvNotice_PlayerObjDescChanged` (line 174324) tail-calls
`gmPaperDollUI::RedressCreature` (line 173990). **CONFIRMED.** RedressCreature:
```
if (m_pInventoryObject == 0 && smartbox->player != 0) { // first time:
eax_5 = CPhysicsObj::makeObject(GetPhysicsObject(player_id)); // clone player obj
this->m_pInventoryObject = eax_5;
CPhysicsObj::set_heading(eax_5, 191.367905f, 1); // face ~191° (toward viewer)
CPhysicsObj::set_sequence_animation(m_pInventoryObject, m_didAnimation.id, 1, 1, 0);
CreatureMode::AddObject(&m_pPaperDoll->creature_mode_objects, m_pInventoryObject);
}
visualDesc = SmartBox::get_player_visualdesc(smartbox);
CPhysicsObj::DoObjDescChangesFromDefault(this->m_pInventoryObject, visualDesc); // re-dress
```
**CONFIRMED** (lines 173997-174012). So the doll is a CLONE of the player's
`CPhysicsObj`, and re-dressing is `CPhysicsObj::DoObjDescChangesFromDefault` applied
to the cloned object using the player's current `VisualDesc` — **the same ObjDesc
apply used for in-world creatures**. The ObjDesc fields (ACViewer
`Entity/ObjDesc.cs:18-54`): `PaletteID`, `SubPalettes`, `TextureChanges`,
`AnimPartChanges` — **all four already parsed by acdream's `CreateObject.ReadModelData`
/ `ObjDescEvent`** (`CreateObject.cs:652-679`: subPalette/textureChange/animPartChange
counts + entries). **CONFIRMED.**
## 5. Paperdoll 3D rendering + reuse analysis
### 5a. It is a 3D viewport, not a 2D image
**CONFIRMED.** The doll is `UIElement_Viewport` (Type `0xD`), element `0x100001D5`.
`UIElement_Viewport::Create` (line 119029-119037) allocates the element + a
`CreatureMode` sub-object at `+0x5f0`; `PostInit` calls
`CreatureMode::InitializeScene` (line 119084). `SetCamera` forwards to
`CreatureMode::SetCameraPosition/Direction` (line 119089-119094). `Register`
`RegisterElementClass(0xd, …)` (line 119126). So a Viewport is a mini 3D scene
embedded in a UI rect, with its own camera, lights, and an object list.
The paperdoll init (line 175517-175535) does, once:
```
m_pPaperDoll = GetChildRecursive(this, 0x100001d5)->DynamicCast(0xd); // the viewport
UIElement_Viewport::SetCamera(m_pPaperDoll, &dir, &pos); // pos/dir vec3s
UIElement_Viewport::SetLight(m_pPaperDoll, DISTANT_LIGHT, 2.0, &dir); // one distant light
CreatureMode::UseSharpMode(&m_pPaperDoll->creature_mode_objects); // sharper mip bias
gmPaperDollUI::RedressCreature(this); // build + dress the doll
```
**CONFIRMED.** `UpdateForRace` (line 174129) re-points the camera per body-type
(case 6/7/8/9/0xC/0xD = the playable races/genders) and swaps `m_didAnimation` (the
idle pose DID) via `DBObj::GetDIDByEnum`. **CONFIRMED.**
### 5b. The viewport render loop (`CreatureMode::Render`)
`CreatureMode::Render` (line 91665) is the per-frame doll draw. Walk-through
(**CONFIRMED** lines 91665-91776):
1. Enter "creature mode" (disables world LOD degrade so the doll is full detail).
2. For each object in `creature_mode_objects`: `CPhysicsObj::update_position` (advance
the idle animation).
3. Set ambient color, sunlight, FOV (`Render::SetFOVRad`), push a frame.
4. `Render::update_viewpoint(&creature_view_frame)`, `set_default_view()`.
5. `RenderDevice::DrawObjCellForDummies(creature_cell)` — draw the object's private
cell, then `D3DPolyRender::FlushAlphaList`.
i.e. the doll lives in its own tiny `creature_cell`, lit by one distant light, drawn
with a dedicated camera into the viewport rect. `CreatureMode::AddObject` (line 94374)
adds the cloned `CPhysicsObj` to that cell:
`CPhysicsObj::AddObjectToSingleCell(obj, creature_cell); SetPlacementFrame(obj,0,1);`.
**CONFIRMED.**
### 5c. Can acdream REUSE its existing character render path? — YES
**acdream already renders animated, equipped characters in-world.** The per-instance
path is `EntitySpawnAdapter` (`src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs`):
- `OnCreate(WorldEntity)` builds an `AnimatedEntityState(sequencer)` and applies
`entity.HiddenPartsMask`, every `entity.PartOverrides` (`SetPartOverride(partIndex,
gfxObjId)` — weapons/clothing/helmets that replace the Setup default), and
pre-warms per-instance palette/texture decode via
`GetOrUploadWithPaletteOverride(surfaceId, texOverride, paletteOverride)`.
**CONFIRMED** `EntitySpawnAdapter.cs:100-168`.
- `WorldEntity` carries `SourceGfxObjOrSetupId`, `MeshRefs`, `PaletteOverride`,
`PartOverrides` (`record struct PartOverride(byte PartIndex, uint GfxObjId)`), and
`HiddenPartsMask`. **CONFIRMED** `WorldEntity.cs:14,28,37,97,104,213`.
This is the EXACT data a re-dress produces: ObjDesc → base palette + sub-palettes
(`PaletteOverride`), texture changes (`SurfaceOverrides`), anim-part swaps
(`PartOverrides`). acdream already turns an `ObjDescEvent`/`CreateObject` ModelData
into these fields. **So the paperdoll doll = "take the local player's WorldEntity (or
a clone of it), feed it through the existing animated-character pipeline, and draw it
with a fixed camera + one distant light into a UI rect."** This is the C# analog of
`makeObject(player) + DoObjDescChangesFromDefault + CreatureMode::Render`.
### 5d. What a `UiViewport` (Type 0xD) widget needs to host the 3D render
The toolkit's `UiRenderContext` is a **2D** sprite/text submission bucket (see
`UiElement.OnDraw(UiRenderContext)`). A 3D model render cannot go through it. A
`UiViewport` widget therefore needs (LIKELY design — flagged):
1. **A render-into-rect hook.** The widget's screen rect (`ScreenPosition` +
Width/Height) defines a GL scissor + viewport. A 3D pass renders the single entity
there, AFTER the world pass and BEFORE/INTERLEAVED with the 2D UI pass. The cleanest
seam is a dedicated overlay callback the `UiHost`/`GameWindow` invokes for any
`UiViewport` present, NOT a draw inside `OnDraw` (which only has a 2D context).
**UNVERIFIED** — the exact integration point (a new `IUiViewportRenderer` Core
interface implemented in App, per Code-Structure Rule 2) is a design call for the
brainstorm/spec phase, not yet decided.
2. **A private mini-scene** mirroring `CreatureMode`: one entity (`AnimatedEntityState`
for the player clone), a fixed camera (position/direction vec3 like
`SetCamera`, e.g. the retail values `dir.z=0.12, pos=(~-2.4, ~0.88)` floats from
`UpdateForRace` — see the `0x3df5c28f / 0xc019999a / 0x3f6147ae` immediates at line
175524-175526, which are little-endian floats ≈ 0.12, 2.4, 0.88; **LIKELY**
I read the hex but did not byte-convert each), one distant light, and an idle
animation playing on the sequencer.
3. **A heading toward the viewer** (`set_heading(191.37°)`, line 174001) and optional
click-drag rotation (the doll spins under the mouse — that's
`m_paperDollDragMask`/`CreateClickMap`, line 174636; **part-selection lighting** for
"which armor piece is this?" highlight uses `ApplyPartSelectionLighting`, line
174034, but that is a polish feature, not MVP).
4. **Reuse `EntitySpawnAdapter`'s state** — feed it the player's `WorldEntity` so the
doll automatically reflects equip changes when `ObjDescEvent` updates the player's
ModelData. The re-dress is then "rebuild the player WorldEntity's PartOverrides/
PaletteOverride from the new ObjDesc and refresh the viewport's entity state" — the
C# analog of `RedressCreature`.
This is the single biggest new piece. The 3D machinery exists; the work is the
**UI↔3D bridge** (a scissored single-entity pass driven by a UI rect).
## 6. New toolkit widgets this introduces
| Widget (proposed) | dat Type it registers at | leaf vs container | Purpose |
|---|---|---|---|
| **`UiViewport`** | **0xD** (`UIElement_Viewport`, reg line 119126) | **leaf** (`ConsumesDatChildren => true`) | Hosts a single 3D entity (the paperdoll character clone) rendered into the widget's screen rect via a scissored mini 3D pass. Owns a fixed camera + one distant light + an `AnimatedEntityState`; reuses `EntitySpawnAdapter`/`AnimatedEntityState` for the model. Needs a new render-into-rect seam (a Core `IUiViewportRenderer` interface implemented in App). **The biggest new piece.** |
| **`UiItemSlot`** (equip-slot variant of the shared item-slot) | **0x10000031** (`UIElement_ItemList`, single 32×32 cell) | **leaf** (`ConsumesDatChildren => true`) | One equip slot. Renders the equipped item's icon (from the weenie `IconDataID`), is a drag-drop target keyed to its `EquipMask` (from `GetLocationInfoFromElementID`), shows/hides per occupancy. NOTE: this is the single-cell case of the shared `UIElement_UIItem`/`UIElement_ItemList` spine widget — the equipment panel is a fixed grid of ~25 of these, one per EquipMask, NOT a scrollable list. **Defer the shared icon/drag mechanics to the spine doc** (`2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md`, NOT FOUND yet); this panel only adds the EquipMask binding + the fixed-position-per-slot layout. |
| **Window manager** (shared, not paperdoll-specific) | n/a (uses Dragbar Type 2 / Resizebar Type 9 already present on chrome) | n/a | Open/close/z-order/persist for the paperdoll window. `UiElement.Draggable/Resizable` already exist; the manager wires them + persistence. Shared with inventory/toolbar — same item the handoff §2 calls "the other deferred Plan-2 piece". |
`gm3DItemsUI`'s pane reuses `UiItemSlot`/the spine `UiItemList` + a `UiScrollbar`
(Type 0xB, already built) + a `UiText` (already built) — no NEW widget. It is an
inventory-contents list (inventory deep-dive scope), not a doll.
## 7. Open questions / UNVERIFIED
- **`0x100001E0` = MissileAmmo `0x800000`** — LIKELY (the decomp immediate is
corrupted to a string pointer at line 173676; inferred from the EquipMask gap +
neighbors). Re-dump element `0x100001E0`'s position vs the ammo doll slot, or
re-decompile `0x004a388a` in Ghidra to recover the real immediate, to confirm.
- **The exact viewport camera/light immediates** (lines 175524-175526, 174144-174146)
— I read the hex but did not byte-convert all of them to floats; the paperdoll
brainstorm should decode `0x3df5c28f≈0.12`, `0xc019999a≈2.4`, `0xc0400000=3.0`,
`0xc059999a≈3.4`, `0x3f6147ae≈0.88`, `0x3f800000=1.0` precisely for a faithful
framing. **UNVERIFIED.**
- **The UI↔3D render seam** (how a UI rect drives a scissored single-entity 3D pass,
and whether it draws after the world pass or as a UI overlay) — DESIGN-OPEN, to be
settled in brainstorm. Code-Structure Rule 2 means the seam is a Core interface
implemented in App. **UNVERIFIED.**
- **acdream's PlayerDescription equipped section** is not surfaced
(`PlayerDescriptionParser.cs:70-77`). To populate slot icons at login (vs only
reacting to later `ObjDescEvent`s), the parser must be extended to read the
`InventoryPlacement` equipped list. Filed as a dependency, not yet an issue.
- **Whether the doll clones the player `WorldEntity` or builds a fresh one** — retail
clones the player `CPhysicsObj` (`makeObject(GetPhysicsObject(player_id))`, line
173999). acdream has no player `CPhysicsObj`-as-renderable today (the local player
isn't a `WorldEntity` in the per-instance adapter — it's the camera). LIKELY the
paperdoll builds a dedicated `WorldEntity` from the local player's
Setup+ObjDesc and feeds it to a private `EntitySpawnAdapter`-like host. **UNVERIFIED.**
- **`gm3DItemsUI` true role** — its `m_itemList` + "Contents of Backpack" text is
CONFIRMED, but whether retail ever shows 3D item models in it (the name suggests a
historical 3D-preview) — NOT FOUND any Viewport in its layout; treated as a 2D
contents list. If a 3D item preview surfaces elsewhere, revisit.
## 8. MEMORY.md index line
- [Equipment/Paperdoll panel deep-dive](research/2026-06-16-equipment-paperdoll-deep-dive.md) — gmPaperDollUI 0x10000024/LayoutDesc 0x21000024: doll = UIElement_Viewport (Type 0xD, elem 0x100001D5) hosting a CreatureMode clone re-dressed via DoObjDescChangesFromDefault; ~25 equip slots are single-cell UIElement_ItemList (0x10000031) mapped element-id→EquipMask by GetLocationInfoFromElementID; wield = GetAndWieldItem (0x001A, item+EquipMask, acdream-MISSING), appearance reply = ObjDescEvent 0xF625 (acdream-PARSED) → RedressCreature; acdream's EntitySpawnAdapter/AnimatedEntityState char path is reusable; new widgets = UiViewport (0xD, the UI↔3D bridge), UiItemSlot (0x10000031), window manager. gm3DItemsUI 0x21000021 is a "Contents of Backpack" list, NOT the doll.

View file

@ -0,0 +1,391 @@
# Inventory panel deep-dive — `gmInventoryUI` + `gmBackpackUI`
**Date:** 2026-06-16
**Phase:** D.2b core-panels research (report-only). Sibling of the action-bar
and paperdoll deep-dives; builds on the `UIElement_UIItem` / icon / drag-drop
**spine** research (see §1 note). Answers handoff §3 questions **Q1** (this
panel's `LayoutDesc`), **Q7** (window layout), **Q8** (full inventory
wire-message set), **Q9** (icon rendering states).
## 1. Summary + confidence legend
The retail inventory window is two cooperating dat windows. **`gmInventoryUI`
(class `0x10000023`, `LayoutDesc 0x21000023`, 300×362)** is the OUTER frame: a
title bar, a chrome border, and three slots that host CHILD windows —
`gmPaperDollUI` (the equipped-gear doll), `gmBackpackUI` (the pack list), and
`gm3DItemsUI` (the 3D rotating-character viewport). **`gmBackpackUI` (class
`0x10000022`, `LayoutDesc 0x21000022`, 61×339)** is the left strip: a burden
**Meter** (Type 7) + a `%`-burden text label, the main-pack item grid
(`UIElement_ItemList` `0x10000031`), and the side-pack tab column (a second
`UIElement_ItemList`). Every cell in those grids is a `UIElement_UIItem`
(class `0x10000032`) — the shared spine widget. Items are server-spawned
**`ACCWeenieObject`** weenies; the client learns container contents from
`CreateObject (0xF745)` + `PlayerDescription (0x0013)` at login and from the
`0xF7B0` GameEvent family (`ViewContents 0x0196`, `InventoryPutObjInContainer
0x0022`, `WieldObject 0x0023`, …) thereafter; it manipulates them with
`0xF7B1` GameActions (`PutItemInContainer 0x0019`, `DropItem 0x001B`,
`GetAndWieldItem 0x001A`, the `Stackable*` family, `GiveObjectRequest 0x00CD`).
acdream already has the outbound builders for most actions
(`InventoryActions.cs`, `InteractRequests.cs`) and parsers for most inbound
events (`GameEvents.cs`), plus a live `ItemRepository`. The gaps are concrete
and enumerated in §4: a missing `DropItem`/`GetAndWieldItem`/`ViewContents`/
`NoLongerViewingContents` parser-or-builder, a 4th field on
`InventoryPutObjInContainer`, and `CreateObject` not yet extracting
`IconId`/`WeenieClassId`/`StackSize`/capacities.
> **Spine dependency.** The handoff said the SPINE agent's doc would live at
> `docs/research/2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md`.
> At the time of writing **that file does NOT exist** (only the handoff
> `2026-06-16-action-bar-inventory-equipment-handoff.md` is present — verified
> by `Glob docs/research/2026-06-16-*.md`). I therefore derived the
> inventory-relevant `UIElement_UIItem` facts FIRST-HAND from the decomp and
> cite them here; where the spine doc later goes deeper (icon DBObj render,
> drag state machine), this doc should be read as the inventory-specific layer
> on top of it.
**Confidence legend:**
- **CONFIRMED** — quoted from a source I opened (decomp `class::method` + line,
or a real `file:line`).
- **LIKELY** — inferred from a confirmed source; the inference is named.
- **UNVERIFIED** — educated guess, flagged loudly; do not port without checking.
---
## 2. LayoutDesc / element map (Q1, Q7)
### 2.1 `gmInventoryUI` — outer frame, `LayoutDesc 0x21000023` (300×362)
**CONFIRMED Q1.** `gmInventoryUI::Register` registers element class `0x10000023`:
> `gmInventoryUI::Register (decomp line 176285): UIElement::RegisterElementClass(0x10000023, gmInventoryUI::Create);`
The window is built from `LayoutDesc 0x21000023` (pre-dump
`.layout-dumps/inventory-0x21000023.txt`). The root element `0x100001CC`
(Type `268435491 = 0x10000023` = the gmInventoryUI class itself) is 300×362 at
ZLevel 1000. `gmInventoryUI::PostInit` (decomp 176236) resolves its named
children by id — these element ids match the dump 1:1, which is what confirms
the map:
| Dump element | X,Y,W,H | Type (resolved) | PostInit binds to | Role |
|---|---|---|---|---|
| `0x100001CC` (root) | 0,0 300×362 | `0x10000023` gmInventoryUI | — | window root |
| `0x100001CD` | 0,23 224×214 | `0x10000024` (base `0x21000024`) | `m_paperDollUI` (DynamicCast `0x10000024`) | nested **PaperDoll** window |
| `0x100001CE` | 239,23 61×339 | `0x10000022` (base `0x21000022`) | `m_backpackUI` (DynamicCast `0x10000022`) | nested **Backpack** strip |
| `0x100001CF` | 0,237 234×120 | `0x10000021` (base `0x21000021`) | `m_3DItemsUI` (DynamicCast `0x10000021`) | nested **3D items** viewport |
| `0x100001D3` | 0,0 276×25 | base `0x21000191` | `m_titleText` (`GetChildRecursive`) | title bar ("Inventory of %s") |
| `0x100001D2` | 276,0 24×23 | base `0x10000... 0x21000192` | (button: chrome) | close/X button (states Normal/pressed) |
| `0x100001D1` | 0,361 300×1 | Type 3 (Field/chrome) | — | bottom rule line (sprite `0x06004D0B`) |
| `0x100001D0` | 0,0 300×362 | Type 3 (Field/chrome) | — | full-window backdrop (`0x06004D0A`, Alphablend) ZLevel 100 |
PostInit excerpt (CONFIRMED):
> `gmInventoryUI::PostInit (176240176259): m_titleText = GetChildRecursive(this, 0x100001d3); … = GetChildRecursive(this, 0x100001cd)->DynamicCast(0x10000024) [paperdoll]; … 0x100001ce ->DynamicCast(0x10000022) [backpack]; … 0x100001cf ->DynamicCast(0x10000021) [3DItems];`
**Implication for the toolkit (LIKELY):** the inventory frame is mostly chrome
+ a title `UIElement_Text` + an X button — the real work is delegated to three
NESTED `LayoutDesc` windows. The importer already recurses generic containers,
but it has never instantiated a *nested gm\*UI window* (an element whose Type is
a high `0x10000xxx` game class with its own `BaseLayoutId`). This is the
"sub-window mount" gap (§6).
### 2.2 `gmBackpackUI` — pack strip, `LayoutDesc 0x21000022` (61×339)
**CONFIRMED Q1.** `gmBackpackUI::Register` (decomp 176531):
> `UIElement::RegisterElementClass(0x10000022, gmBackpackUI::Create);`
Built from `LayoutDesc 0x21000022` (pre-dump `.layout-dumps/backpack-0x21000022.txt`).
Root `0x100001C8` (Type `268435490 = 0x10000022`) is 61×339. `gmBackpackUI::PostInit`
(decomp 176596) binds the children — again matching the dump exactly:
| Dump element | X,Y,W,H | Type | PostInit binds to | Role |
|---|---|---|---|---|
| `0x100001C8` (root) | 0,0 61×339 | `0x10000022` gmBackpackUI | — | window root |
| `0x100001D7` | 0,7 36×15 | base `0x10000376`/`0x2100003F` | — | "Burden" caption text |
| `0x100001D8` | 0,18 36×15 | base `0x10000376`/`0x2100003F` | `m_burdenText` | the `%`-load number text |
| `0x100001D9` | 44,8 11×58 | **7 (Meter)** | `m_burdenMeter` (DynamicCast 7) | **the burden bar** (vertical) |
| `0x100001C9` | 6,32 36×36 | `0x10000031` ItemList | `m_topContainer` (DynamicCast `0x10000031`) | main-pack first cell / list head |
| `0x100001CA` | 6,73 36×252 | `0x10000031` ItemList | `m_containerList` (DynamicCast `0x10000031`) | the **item grid** (main pack) |
| `0x100001CB` | 41,73 16×252 | base `0x10000... 0x2100003E` | — | side-pack tab column / scrollbar gutter |
PostInit excerpt (CONFIRMED):
> `gmBackpackUI::PostInit (176600176629): m_burdenText = GetChildRecursive(this, 0x100001d8); m_burdenMeter = GetChildRecursive(0x100001d9)->DynamicCast(7); … m_topContainer = GetChildRecursive(0x100001c9)->DynamicCast(0x10000031); m_containerList = GetChildRecursive(0x100001ca)->DynamicCast(0x10000031);`
**The burden Meter (Q7 answer).** Element `0x100001D9` is the Type-7 meter the
backpack dump shows with back sprite `0x0600121C` (grandchild `0x00000002`) +
fill sprite `0x0600121D`. It is a VERTICAL 11×58 bar (the only meter in the
window) — confirmed by `gmBackpackUI::SetLoadLevel` writing it:
> `gmBackpackUI::SetLoadLevel (176565176573): m_burdenMeter; …(float)arg2; var_10 = 0x69; UIElement::SetAttribute_Float();`
That is the SAME meter-fill mechanism as vitals (property `0x69` = fill ratio,
pushed at runtime — see `2026-06-15-layoutdesc-format.md §3`). The fill value
is `load × 0.3333…` clamped to [0,1] (CONFIRMED 176542:
`x87_r7_1 = arg2 * 0.33333333333333331`), and the text is formatted `%d%%`
from `floor(load × 300)` (CONFIRMED 176576176583:
`floor(arg2 * 300.0)``SetText(m_burdenText, "%d%%")`). So the bar is FULL
at 100% load and the number reads 0300% (retail's encumbrance scale: 100% =
your computed max burden, you can carry up to 300%).
> **Where is the VALUE total / coin total?** NOT in `gmBackpackUI` — there is
> no value Meter or value text element in `0x21000022`. The inventory window
> shows BURDEN only; the pyreal/coin total is the player's Coin Value displayed
> elsewhere (UNVERIFIED — likely a separate stat readout; the panel dump has
> no value field). Do not invent a value summary for this window.
**The side-pack list.** `m_containerList` (`0x100001CA`) is the main item grid;
`0x100001CB` is the narrow 16-wide column to its right (scrollbar gutter / tab
strip). The retail "side packs" (sub-bags) are opened as ADDITIONAL container
views — `gmInventoryUI::RecvNotice_OpenContainedContainer` (decomp 176290)
routes a contained-container open into a second `UIElement_ItemList`:
> `RecvNotice_OpenContainedContainer (176318): UIElement_ItemList::ItemList_OpenContainer(*(…+0x608), arg2, 1);`
> (offset `+0x604` = the main/own list; `+0x608` = the secondary/other-container list)
The two `UIElement_ItemList`s at member offsets `+0x604` and `+0x608` are the
"my main pack" list and the "currently-open other container" list — CONFIRMED
by the dual flush/open pattern in `RecvNotice_SetDisplayInventory`
(176114/176123/176141) and `RecvNotice_PlayerDescReceived` (176374/176375
`ItemList_SetChildList(+0x604, …); ItemList_SetChildList(+0x608, …)`).
---
## 3. Container model for this panel (Q3 / cross-cutting, inventory slice)
**Items are server weenies (`ACCWeenieObject`).** CONFIRMED throughout the
inventory code: `ClientObjMaintSystem::GetWeenieObject(itemID)` is the only way
the panel resolves an item id to its data (e.g. `UIItem_Update` 230235,
`RecvNotice_OpenContainedContainer` 176293). This matches
`claude-memory/feedback_weenie_vs_static.md` (interactable items are
server-spawned weenies). [CONFIRMED]
**Container hierarchy = 2-deep.** A character has a main pack (capacity ~102) +
N side-packs (sub-bags); a side-pack cannot hold another side-pack. acdream's
`Container` model already encodes this (`ItemInstance.cs:154` `Container` with
`SidePacks` + `IsSidePack => SideCapacity == 0`). [CONFIRMED in acdream; the
2-deep rule is retail-standard and matches ACE]
**How the client learns contents:**
1. **At login**`PlayerDescription (0x0013)` carries the player's full
inventory + equipped lists; acdream already registers both into
`ItemRepository` (`GameEventWiring.cs:405432`). [CONFIRMED]
2. **Per-item spawn**`CreateObject (0xF745)` for each visible weenie; for an
item in your pack the server sends the weenie (with `IconId`, capacities,
stack size in the WeenieHeader). acdream's `CreateObject.TryParse` extracts
guid/name/itemType but **discards IconId, WeenieClassId, StackSize, Value,
ItemCapacity, ContainerCapacity** (it `_ =`-skips the IconId at
`CreateObject.cs:516` and never reads StackSize/Value). [CONFIRMED gap]
3. **Open a container**`ViewContents (0x0196)` lists `{guid, containerType}`
per slot; `gmInventoryUI` / `UIElement_ItemList` insert a `UIElement_UIItem`
per entry. [CONFIRMED on ACE/holtburger side; acdream has NO ViewContents
parser]
4. **Live moves**`InventoryPutObjInContainer (0x0022)`, `WieldObject
(0x0023)`, `InventoryPutObjectIn3D (0x019A)` relocate one weenie;
`gmInventoryUI::RecvNotice_ServerSaysMoveItem` (176175) + the
`UIElement_ItemList` rebuild the affected cells. [CONFIRMED]
**The notice ids `gmInventoryUI::PostInit` registers (CONFIRMED 176269176277)**
— these are the internal client notice opcodes (NOT wire opcodes) the window
listens to: `0x4dd1f0, 0x4dd1f1, 0x4dd1f2, 0x4dd1f6, 0x4dd266, 0x186ab,
0x186a8, 0x4dd25b, 0x4dd25d`. They map (via the vftable, 980257980562) to
`RecvNotice_ItemAttributesChanged / ServerSaysMoveItem / EndPendingInPlayer /
ShowPendingInPlayer / OpenContainedContainer / NewParentContainer /
PlayerDescReceived / SetDisplayInventory / UpdateCharacterInformation`. These
are the controller hooks acdream's `InventoryController` (new, §6) must expose
to drive the live grid.
---
## 4. Wire-message catalog (Q8)
All client→server ride the `0xF7B1` GameAction envelope (`u32 0xF7B1; u32 seq;
u32 subOpcode; …`); all server→client item events ride the `0xF7B0` GameEvent
envelope (`u32 0xF7B0; u32 target; u32 seq; u32 eventOpcode; …`).
**ACE handler** = the file under
`ACE/Source/ACE.Server/Network/GameAction/Actions/` (C→S) or
`…/GameEvent/Events/` (S→C). **Chorizite/holtburger** field order verified;
where I cite holtburger it is `inventory/actions.rs` or `inventory/events.rs`
(both opened, with hex pack/unpack fixtures).
### 4.1 Client → server (GameActions, `0xF7B1`)
| Opcode | Name | Dir | Trigger | ACE handler | Field order (holtburger/ACE) | acdream parse status |
|---|---|---|---|---|---|---|
| `0x0019` | PutItemInContainer | C→S | drag item into pack / pick up ground item (container = self) | `GameActionPutItemInContainer.Handle` | `u32 itemGuid, u32 containerGuid, i32 placement` | **parsed**`InteractRequests.BuildPickUp` (`InteractRequests.cs:97`) |
| `0x001A` | GetAndWieldItem | C→S | equip an item from inventory onto the doll | (`GameActionType` 0x001A; handler `Player_Inventory`) | `u32 itemGuid, u32 equipMask` (holtburger `actions.rs:8` `GetAndWieldItemActionData`) | **MISSING** (no builder) |
| `0x001B` | DropItem | C→S | drop an item on the ground | `GameActionDropItem.Handle` | `u32 itemGuid` (holtburger `actions.rs:140`) | **MISSING** (no builder; acdream reuses 0x0019 for moves only) |
| `0x0035` | UseWithTarget | C→S | use src item on target (key→door) | (Interact) | `u32 sourceGuid, u32 targetGuid` | **parsed**`InteractRequests.BuildUseWithTarget` |
| `0x0036` | UseItem | C→S | use/equip-by-doubleclick a single item | `GameActionUseItem` | `u32 targetGuid` | **parsed**`InteractRequests.BuildUse` |
| `0x0054` | StackableMerge | C→S | drop stack A onto compatible stack B | `GameActionStackableMerge.Handle` | `u32 mergeFromGuid, u32 mergeToGuid, i32 amount` | **parsed**`InventoryActions.BuildStackableMerge` |
| `0x0055` | StackableSplitToContainer | C→S | split N off a stack into a pack slot | `GameActionStackableSplitToContainer.Handle` | `u32 stackGuid, u32 containerGuid, i32 place, i32 amount` | **parsed**`InventoryActions.BuildStackableSplitToContainer` |
| `0x0056` | StackableSplitTo3D | C→S | split N off a stack onto the ground | `GameActionStackableSplitTo3D.Handle` | `u32 stackGuid, i32 amount` | **parsed**`InventoryActions.BuildStackableSplitTo3D` |
| `0x019B` | StackableSplitToWield | C→S | split N off a stack into an equip slot (e.g. arrows) | `GameActionStackableSplitToWield` | `u32 stackGuid, u32 equipMask, i32 amount` | **parsed**`InventoryActions.BuildStackableSplitToWield` |
| `0x00CD` | GiveObjectRequest | C→S | give item (or N of a stack) to an NPC/player | `GameActionGiveObjectRequest.Handle` | `u32 targetGuid, u32 itemGuid, i32 amount` | **parsed**`InventoryActions.BuildGiveObjectRequest` |
| `0x0195` | NoLongerViewingContents | C→S | close a side-pack / ground-container view | (`GameActionType` 0x0195) | `u32 containerGuid` (holtburger `actions.rs:280`) | **MISSING** (no builder) |
| `0x019C` | AddShortcut | C→S | pin to quickbar (toolbar phase, listed for completeness) | (`GameActionType`) | `u32 slot, u32 objType, u32 targetId` | **parsed**`InventoryActions.BuildAddShortcut` |
| `0x019D` | RemoveShortcut | C→S | unpin quickbar slot | (`GameActionType`) | `u32 slot` | **parsed**`InventoryActions.BuildRemoveShortcut` |
**Opcode source (CONFIRMED):** `ACE/.../GameAction/GameActionType.cs:1376`
`PutItemInContainer=0x0019, GetAndWieldItem=0x001A, DropItem=0x001B,
UseWithTarget=0x0035, StackableMerge=0x0054, StackableSplitToContainer=0x0055,
StackableSplitTo3D=0x0056, GiveObjectRequest=0x00CD, NoLongerViewingContents=0x0195,
StackableSplitToWield=0x019B`. ACE handler field order CONFIRMED by reading each
`GameAction*.Handle` (DropItem reads 1 u32; PutItemInContainer reads 3;
GiveObjectRequest reads 3; StackableMerge reads 3; SplitToContainer reads 4;
SplitTo3D reads 2). holtburger hex fixtures (`actions.rs` test module)
independently confirm every field layout.
> **acdream byte-order note:** `InteractRequests.BuildPickUp` writes `placement`
> as `i32` (`InteractRequests.cs:106`), matching ACE's `ReadInt32()`. The split
> builders write `amount`/`placement` as `u32` — on the wire identical bytes,
> but ACE reads them as `i32` (negative split amounts can't occur, so this is
> safe). [CONFIRMED, harmless]
### 4.2 Server → client (GameEvents, `0xF7B0`)
| Opcode | Name | Dir | Trigger | ACE handler | Field order | acdream parse status |
|---|---|---|---|---|---|---|
| `0x0022` | InventoryPutObjInContainer | S→C | server confirms item now in container at slot | `GameEventItemServerSaysContainId` | `u32 itemGuid, u32 containerGuid, u32 placement, u32 containerType` | **parsed (INCOMPLETE)**`GameEvents.ParsePutObjInContainer` reads only 3 fields, **drops `containerType`** |
| `0x0023` | WieldObject | S→C | server confirms item equipped to slot | `GameEventWieldItem` | `u32 objectId, i32 equipMask` | **parsed + wired**`GameEvents.ParseWieldObject`, `GameEventWiring.cs:231` |
| `0x0196` | ViewContents | S→C | full contents list of a container you opened | `GameEventViewContents` | `u32 containerGuid, u32 count, [u32 guid, u32 containerType]×count` | **MISSING** (no parser) |
| `0x019A` | InventoryPutObjectIn3D | S→C | server confirms item dropped to world | `GameEventItemServerSaysMoveItem` | `u32 objectGuid` | **parsed (UNWIRED)**`GameEvents.ParsePutObjectIn3D` exists, not in `WireAll` |
| `0x00A0` | InventoryServerSaveFailed | S→C | reject a speculative client move (roll back) | `GameEventInventoryServerSaveFailed` | `u32 itemGuid, u32 weenieError` | **parsed (UNWIRED, INCOMPLETE)**`GameEvents.ParseInventoryServerSaveFailed` reads only the guid, drops error (holtburger reads both: `events.rs:147`) |
| `0x0052` | CloseGroundContainer | S→C | server closed a ground-container view | `GameEventCloseGroundContainer` | `u32 containerGuid` | **parsed (UNWIRED)**`GameEvents.ParseCloseGroundContainer` exists, not in `WireAll` |
| `0x00C9` | IdentifyObjectResponse | S→C | appraise result (full property bundle) | `GameEventIdentifyObjectResponse` | `u32 guid, u32 flags, u32 success, …property tables…` | **parsed + wired**`AppraiseInfoParser` via `GameEventWiring.cs:245` |
| `0xF745` | CreateObject (GameMessage, not GameEvent) | S→C | spawn a weenie (incl. an item in your pack) | `GameMessageCreateObject``WorldObject.SerializeCreateObject` | weenie header (Name, WeenieClassId, **IconId**, ItemType, …) + ModelData + PhysicsData | **parsed (INCOMPLETE)**`CreateObject.TryParse` skips IconId/WeenieClassId/StackSize/Value/capacities |
| `SetStackSize` (`0x0197`/UIQueue) | SetStackSize | S→C | update a stack's count + value after merge/split | `GameMessageSetStackSize` | `u32 seq, u32 guid, u32 stackSize, u32 value` | **MISSING** (no parser) |
| `InventoryRemoveObject` (UIQueue) | InventoryRemoveObject | S→C | remove an item from inventory view (given/dropped/destroyed) | `GameMessageInventoryRemoveObject` | `u32 guid` | **MISSING** (no parser) |
**Opcode + field-order sources (CONFIRMED):**
- `0x0022` four fields: `GameEventItemServerSaysContainId.cs:1013` writes
`itemGuid, containerGuid, PlacementPosition, ContainerType`; holtburger
`events.rs:65` reads `item_guid, container_guid, slot, container_type`
(+ hex fixture `events.rs:217` slot=3 type=1). acdream's parser
(`GameEvents.cs:352`) stops after 3 u32s — `containerType` is dropped.
- `0x0196` shape: `GameEventViewContents.cs:1326` writes `Guid, count, {guid,
containerType}×n`; holtburger `events.rs:20` (+ fixture `events.rs:195`).
- `0x0023`: `GameEventWieldItem.cs:1112` writes `objectId, (int)newLocation`.
- `0x019A`: `GameEventItemServerSaysMoveItem.cs:11` writes only `Guid`.
- `0x00A0`: `GameEventInventoryServerSaveFailed.cs` (error code present;
holtburger reads it).
- `SetStackSize`: `GameMessageSetStackSize.cs:1215` (`seq, guid, stackSize,
value`).
- `InventoryRemoveObject`: `GameMessageInventoryRemoveObject.cs:11` (`guid`).
### 4.3 acdream wire gaps (concrete TODO list for the build session)
- **Add C→S builders:** `DropItem (0x001B)`, `GetAndWieldItem (0x001A)`,
`NoLongerViewingContents (0x0195)`. (Equip + drop are core inventory verbs.)
- **Add S→C parsers:** `ViewContents (0x0196)`, `SetStackSize`,
`InventoryRemoveObject`.
- **Fix `ParsePutObjInContainer`** to read the 4th `containerType` u32.
- **Fix `ParseInventoryServerSaveFailed`** to read the `weenieError` u32.
- **Wire (register in `GameEventWiring.WireAll`):** `ViewContents`,
`InventoryPutObjectIn3D`, `CloseGroundContainer`, `InventoryServerSaveFailed`
(parsers exist or will, but `WireAll` doesn't register them today —
CONFIRMED `GameEventWiring.cs` registers only `WieldObject`,
`InventoryPutObjInContainer`, `IdentifyObjectResponse`, `PlayerDescription`).
- **Extend `CreateObject.TryParse`** to capture `IconId` (already in the wire,
currently `_`-discarded at `CreateObject.cs:516`), `WeenieClassId`,
`StackSize`, `Value`, `ItemCapacity`, `ContainerCapacity` — the inventory
cell needs all of these to draw an icon + quantity + capacity bar.
---
## 5. Drag-drop for inventory (Q5, this panel's slice)
The drag-drop machinery lives on `UIElement_UIItem` (the spine widget). The
inventory-relevant parts I confirmed first-hand:
- **A slot accepts a drop** via `UIElement_UIItem::SetDragAcceptState(state)`,
toggling the `m_elem_Icon_DragAccept` sub-element's STATE
(`0x10000040` = reject / `0x10000041` = accept; CONFIRMED
`SetDragAcceptState` 229271229277, and call sites at 174307/174313,
201327/201333 flip between the two). [CONFIRMED]
- **A drag in progress** uses `m_dragIcon` (a translucent copy of the icon,
created in `PostInit` 229738229740 via `UIElementManager::CreateChildElement`
with id `0x10000345`, `SetVisible(0)` until a drag starts). [CONFIRMED]
- **The drop RESULT is a wire action**, chosen by source→destination:
inventory→pack slot = `PutItemInContainer (0x0019)`; inventory→doll =
`GetAndWieldItem (0x001A)`; inventory→ground = `DropItem (0x001B)`;
stack→compatible stack = `StackableMerge (0x0054)`; partial-stack drag =
one of the `StackableSplit*` (the count picker dialog supplies `amount`);
item→NPC = `GiveObjectRequest (0x00CD)`. [LIKELY — inferred from the action
set in §4 + the ACE handler names; the exact source/dest→opcode table is the
spine doc's job, but these are the inventory verbs]
- **Speculative-then-confirm:** the client may move the cell locally and wait;
if the server rejects, `InventoryServerSaveFailed (0x00A0)` rolls it back
(the slot's pending/ghost state is `SetWaitingState``m_elem_Icon_Ghosted`
greys it; CONFIRMED `SetWaitingState` 229190229208 toggles
`m_elem_Icon_Ghosted` visibility). acdream's `ItemRepository` already
documents this revert path (`ItemRepository.cs:30`). [CONFIRMED mechanism]
For acdream's toolkit, the drop target is a `UiItemSlot` (§6) that reports a
drop to the `InventoryController`, which picks the opcode and sends it via
`LiveCommandBus` + the builders in §4 — mirroring the existing interaction
pipeline (`claude-memory/project_interaction_pipeline.md`, B.4
WorldPicker→Use). The `UiRoot` already has drag-drop input plumbing
(per `project_d2b_retail_ui.md`: "UiRoot already has full input
(focus/capture/drag-drop/tooltip/click) — dormant until wired").
---
## 6. New toolkit widgets this introduces
The inventory panel needs four new pieces beyond the shipped spine widgets
(Button/Menu/Meter/Scrollbar/Text/Field/UiDatElement):
| Widget | dat Type it registers at | Leaf or container | Purpose |
|---|---|---|---|
| **`UiItemSlot`** (port of `UIElement_UIItem`) | **`0x10000032`** (`UIElement_UIItem::Register` line 229339); resolves to a `UIElement_Field` subclass ⇒ underlying **Type 3** | **leaf** (`ConsumesDatChildren=>true`) — it owns the icon + all overlay sub-elements (`m_elem_Icon` `0x1000033b`, `m_elem_Icon_Overlays` `…33c`, `m_elem_Icon_Selected` `…342`, `m_elem_Icon_Ghosted` `…349`, `m_elem_Icon_Quantity` `…4f5`, `m_elem_Icon_CapacityBar` `…347`/`StructureBar` `…348` Type-7 meters, cooldown ring `…54f558`) and reproduces them procedurally | one item-in-a-slot: icon + quantity + capacity/structure bars + selection/ghost/drag-accept/open-container overlays. **Shared by all 3 panels.** *(This is the spine widget; named here for the inventory's needs.)* |
| **`UiItemList` / `UiItemGrid`** (port of `UIElement_ItemList`) | **`0x10000031`** (`UIElement_ItemList`; the backpack root element is itself this class) | **container** of `UiItemSlot`s (it lays out an N-column grid + scroll) | the main-pack grid + the side-pack list. Methods to port: `ItemList_AddItem`, `ItemList_InsertItem`, `ItemList_Flush`, `ItemList_OpenContainer`, `ItemList_SetChildList`, `ItemList_SetParentContainer`, `ItemList_OpenFirstContainer` (all CONFIRMED as called from `gmInventoryUI`/`gmBackpackUI`). Two instances per backpack (own list `+0x604`, other-container list `+0x608`). |
| **Sub-window mount** (importer capability, not a widget per se) | element whose Type is a high `0x10000xxx` game class WITH a non-zero `BaseLayoutId` (e.g. `0x100001CD`→paperdoll `0x21000024`) | container | lets `LayoutImporter` instantiate a NESTED `LayoutDesc` window inside a parent slot (paperdoll + backpack + 3DItems inside the inventory frame). The importer recurses generic children today but has never mounted another gm\*UI window. |
| **Window manager** (the deferred Plan-2 piece) | drives Dragbar (Type 2) + Resizebar (Type 9) + open/close/z-order/persist | infra | inventory/paperdoll/toolbar are pop-up windows; needs the faithful grip/dragbar drag (today vitals/chat use whole-window drag, accepted IA-12 approximation). |
Plus a thin **`InventoryController`** (the `gmInventoryUI::PostInit` analogue):
find-by-id binds `m_titleText`/`m_paperDollUI`/`m_backpackUI`/`m_3DItemsUI`,
subscribes to `ItemRepository` events, and exposes the notice hooks
(`ServerSaysMoveItem`, `SetDisplayInventory`, `OpenContainedContainer`,
`PlayerDescReceived`) — exactly mirroring `VitalsController`/`ChatWindowController`.
---
## 7. Open questions / UNVERIFIED
1. **Value/coin total in the window.** No value Meter or value text exists in
`0x21000022` or `0x21000023`. Retail likely shows pyreals elsewhere (the
coin readout). **UNVERIFIED** — do not add a value summary to this window
without finding its real home.
2. **Side-pack tabs vs. a single scrolling list.** Element `0x100001CB` (16×252,
base `0x2100003E`) is the narrow column right of the grid. Whether it renders
side-pack TABS (one per sub-bag) or a SCROLLBAR is **UNVERIFIED** — I read the
geometry + the dual-ItemList open pattern but did not decode `0x2100003E`.
Dump `0x2100003E` to settle it.
3. **`UIElement_ItemList` grid geometry** (columns, cell pitch). The cell
template is 36×36 (from `0x100001C9`); UIElement_UIItem `0x21000037` is 32×32
per the handoff. The exact column count + wrap is in `ItemList_AddItem` /
`ItemList_SetChildList` (not fully read here). **LIKELY** a fixed-column grid;
confirm by reading `UIElement_ItemList::ItemList_AddItem`.
4. **`CreateObject` IconId for pack items.** I confirmed the IconId is on the
wire and currently discarded, but did not byte-trace that ACE actually sets
IconId on a *contained* (non-visible-in-3D) item's CreateObject vs. relying on
PlayerDescription. **LIKELY** present (the spine icon path needs it); verify
against a live capture before trusting it as the sole icon source.
5. **The icon composite layering** (underlay/base/effects-overlay) — I anchored
it from `IconData::IconData` (407532+) and the cache key (408842): underlay =
`pwd._iconUnderlayID` OR type-default `GetByEnum(0x10000004,
LowestSetBit(itemType)+1)`; base = `m_idIcon`; effects overlay =
`GetByEnum(0x10000005, LowestSetBit(_effects)+1)` (default `0x21`). The exact
blend/DBObj-render is the **spine doc's** territory — treat my §5/§6 citations
as the inventory-state hooks, not the full render port. [CONFIRMED anchors,
render detail deferred to spine]
6. **Q9 identified-vs-unidentified state.** Retail does NOT gate the icon on
appraise-state; the underlay/overlay come from the weenie's own
`_iconUnderlayID`/`_iconOverlayID`/`_effects` (server-sent), and "unidentified"
shows the same icon (the tooltip detail is what's gated by appraise, via
`IdentifyObjectResponse`). **LIKELY** (no identified→icon-swap code seen in
`UIItem_Update`); the only icon-affecting client states are
selected/waiting(ghost)/open-container/drag-accept (all §5). Confirm there's
no appraise-gated icon variant before claiming it.
---
## 8. MEMORY.md index line
- [Inventory panel deep-dive (gmInventoryUI/gmBackpackUI)](research/2026-06-16-inventory-deep-dive.md) — D.2b: LayoutDesc 0x21000023 (frame: title + 3 nested sub-windows) + 0x21000022 (backpack: burden Meter 0x100001D9 via SetLoadLevel→fill 0x69, main-pack ItemList 0x100001CA); full inventory wire catalog (C→S 0x0019/1A/1B/54/55/56/19B/CD/195, S→C 0x0022/23/196/19A/A0/52 + SetStackSize/InventoryRemoveObject) with acdream parse-status (gaps: DropItem/GetAndWieldItem/ViewContents builders, 0x0022 4th field, CreateObject IconId); new widgets UiItemSlot(0x10000032)/UiItemGrid(0x10000031)+sub-window mount+window manager.

View file

@ -0,0 +1,557 @@
# 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.

View file

@ -0,0 +1,407 @@
# D.2b core panels — SYNTHESIS (toolbar + inventory + paperdoll)
**Date:** 2026-06-16
**Phase:** D.2b retail-UI engine, "core panels" research arc. Report-only synthesis.
**Role:** synthesis lead reconciling the three panel deep-dives into one authoritative
build plan. The deliverable is this doc; no code was written.
**Inputs (all read in full):**
- toolbar: [`2026-06-16-action-bar-toolbar-deep-dive.md`](2026-06-16-action-bar-toolbar-deep-dive.md)
- inventory: [`2026-06-16-inventory-deep-dive.md`](2026-06-16-inventory-deep-dive.md)
- paperdoll: [`2026-06-16-equipment-paperdoll-deep-dive.md`](2026-06-16-equipment-paperdoll-deep-dive.md)
- handoff: [`2026-06-16-action-bar-inventory-equipment-handoff.md`](2026-06-16-action-bar-inventory-equipment-handoff.md)
> ## Note: the SPINE doc was completed in a follow-up pass
> The handoff promised a "spine agent" doc covering the shared item-slot widget, icon
> decode, and the full drag-drop state machine. During the original workflow run the
> spine agent died on a transient API error, so this synthesis was first written against
> a `null` spine digest. **The spine doc has since been written:**
> [`2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md`](2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md).
> It resolves the two items this synthesis had left open: (1) the **icon-composite
> render path** — each icon LAYER is a `0x06` RenderSurface decoded DIRECTLY, but the
> on-screen icon is a **5-layer runtime composite** blitted into one private 32×32
> surface (`IconData::RenderIcons` decomp 407524), NOT a single texture and NOT
> appraise-gated; (2) the item-cell **bound-object field `+0x5FC` = `UIElement_UIItem::itemID`**
> (decomp 230230). The shared `UIElement_UIItem` / `UIElement_ItemList` identity facts
> below were first-hand-derived + re-verified by the panel agents and remain sound.
> §4 Step 0 and the §5 risks below have been updated to reflect the completed spine doc.
## 0. Summary + confidence legend
The three D.2b core panels are all built from the **same two reusable retail widgets**:
the **item-slot** (`UIElement_UIItem`, class `0x10000032`) and the **item-list/grid**
(`UIElement_ItemList`, class `0x10000031`). Every slot in every panel is one of these —
the toolbar is 18 single-cell item-lists, the inventory is N-cell item-list grids, and
the paperdoll is ~25 single-cell item-lists keyed to `EquipMask`. Build those two
widgets once and all three panels fall out. The paperdoll adds one genuinely new piece:
a **`UiViewport`** (`UIElement_Viewport`, Type `0xD`) that renders a live 3D character
clone into a UI rect — the single biggest new engineering item. All three panels are
pop-up windows, so they all need the deferred **window manager** (open/close/z-order/
persist + Dragbar Type 2 + Resizebar Type 9 drag-resize). On the wire, acdream is in
good shape: most C→S builders and S→C parsers already exist; the concrete gaps are a
handful of missing builders (`DropItem`, `GetAndWieldItem`, `NoLongerViewingContents`),
missing parsers (`ViewContents`, `SetStackSize`, `InventoryRemoveObject`), two
incomplete parsers (a dropped 4th field on `0x0022`; a dropped error code on `0x00A0`),
and `CreateObject` discarding `IconId`/`StackSize`/capacities the cells need.
**Confidence legend** (carried from the source docs, re-checked here):
- **CONFIRMED** — quoted from a named-decomp `class::method` (with line) or a real
`file:line` that I or a panel agent opened.
- **LIKELY** — inferred from a confirmed source; the inference is named.
- **UNVERIFIED** — educated guess, flagged loudly; needs a decomp/cdb follow-up before
porting.
**Synthesis-lead re-verifications (opened first-hand, this session):**
- `CreateObject.cs:515-516``_ = ReadPackedDword(...) // WeenieClassId; _ = ReadPackedDwordOfKnownType(..., IconTypePrefix);`**IconId and WeenieClassId are discarded**. CONFIRMED.
- `acclient_2013_pseudo_c.txt:135087-135088``UIElement_ItemList::Register();` and `UIElement_UIItem::Register();` are real adjacent symbols (`0x0047a483`/`0x0047a488`). CONFIRMED.
- `acclient_2013_pseudo_c.txt:135130-135132``gmBackpackUI::Register / gmInventoryUI::Register / gmPaperDollUI::Register` all real. CONFIRMED.
- `acclient_2013_pseudo_c.txt:175242-175508` — the ~25 paperdoll equip slots each `DynamicCast(0x10000031)` (`m_neckSlot, m_headSlot, m_weaponReadySlot, m_ammoReadySlot, …`) + `RegisterItemListDragHandler`. CONFIRMED.
- `acclient_2013_pseudo_c.txt:229180-229413` — the `m_elem_Icon_*` family (`_Ghosted`, `_OpenContainer`, `_Selected`, `_DragAccept`) and its `SetState` reject/accept/neutral states (`0x10000040` / `0x10000041` / `0x1000003f`) are real on `UIElement_UIItem`. CONFIRMED (corroborates the inventory agent's first-hand derivation).
---
## 1. Confirmed class ids + LayoutDesc ids + sizes
All confirmed via `*::Register` (`RegisterElementClass`) in the decomp + the
pre-dumped `.layout-dumps/` trees. The element-class id and the LayoutDesc id are
distinct namespaces (`0x10000xxx` = element class registered in C++; `0x21000xxx` =
the dat LayoutDesc that builds the window).
| Panel / widget | Element class id | LayoutDesc id | Root element | Size (W×H) | Register anchor |
|---|---|---|---|---|---|
| `gmToolbarUI` (action bar) | `0x10000007` | `0x21000016` | `0x10000191` | 300×122 | `gmToolbarUI::Register` (decomp 196897); `GetUIElementType``0x10000007` (196707) |
| `gmInventoryUI` (frame) | `0x10000023` | `0x21000023` | `0x100001CC` | 300×362 | `gmInventoryUI::Register` (decomp 176285 / `0x004a6a60`) |
| `gmBackpackUI` (pack strip) | `0x10000022` | `0x21000022` | `0x100001C8` | 61×339 | `gmBackpackUI::Register` (decomp 176531 / `0x004a6e80`) |
| `gmPaperDollUI` (equip doll) | `0x10000024` | `0x21000024` | `0x100001D4` | 224×214 | `gmPaperDollUI::Register` (decomp 174445 / `0x004a4560`) |
| `gm3DItemsUI` ("Contents of Backpack") | `0x10000021` | `0x21000021` | `0x100001C4` | 234×120 | `gm3DItemsUI::Register` (decomp 176723) |
| `UIElement_UIItem` (item-slot, shared) | `0x10000032` | `0x21000037` (32×32 cell template) | — | 32×32 | `UIElement_UIItem::Register` (decomp 229339 / `0x0047a488`) |
| `UIElement_ItemList` (item-list/grid, shared) | `0x10000031` | `0x2100003D` (single 32×32 cell) | `0x10000339` | 32×32 cell | `UIElement_ItemList::Register` (decomp / `0x0047a483`) |
**Nesting (CONFIRMED `gmInventoryUI::PostInit` 176236-176259):** the inventory FRAME
(`0x21000023`) hosts three NESTED gm\*UI windows by id — `0x100001CD`→paperdoll
(`DynamicCast 0x10000024`), `0x100001CE`→backpack (`DynamicCast 0x10000022`),
`0x100001CF`→3D-items (`DynamicCast 0x10000021`). This "sub-window mount" (an element
whose Type is a high `0x10000xxx` game class with its own `BaseLayoutId`) is a
capability the importer does **not** have yet.
**Note on `gm3DItemsUI`:** despite the "3D" name it is a 2D "Contents of Backpack"
item-list (`gm3DItemsUI::PostInit` 176728 sets `m_contentsText`→"Contents of Backpack",
`m_itemList``DynamicCast(0x10000031)`; its layout has NO Viewport). The 3D character
doll is in `gmPaperDollUI`, not here. CONFIRMED.
---
## 2. CONSOLIDATED new toolkit widgets (the single authoritative list)
This reconciles the four docs into one list. The shipped D.2b toolkit already has
Button(1)/Dragbar(2)/Field(3)/Menu(6)/Meter(7)/Panel(8)/Scrollbar(0xB)/Text(0xC) plus
`UiDatElement` for generic chrome — those are **reused**, not re-listed.
**Type-registration model:** the shipped numeric Type registry (1=Button … 0x12=Proto)
is the toolkit's generic-widget dispatch. The item-slot / item-list / viewport are NOT
in that numeric table — in retail they are **`UIElement` subclasses registered by a
full class id** via `RegisterElementClass(0x10000xxx, …)`, and in the dat their
elements have `Type=0` and inherit the real class id through the `BaseElement` chain
(resolved by `ElementReader.Merge`'s zero-wins-base rule). So in acdream's
`DatWidgetFactory` they are **new behavioral leaf widgets keyed off the resolved class
id**, exactly the same pattern as the existing behavioral widgets — they just key off
`0x10000031`/`0x10000032`/`0xD` rather than a small numeric Type. (The numeric Type
that `0xD`=Viewport occupies in the confirmed registry IS a generic toolkit Type, so
`UiViewport` can register at Type `0xD` directly; the item-slot/item-list register at
their class ids.)
| Widget | Registers at | Leaf vs container | Panels that use it | Purpose |
|---|---|---|---|---|
| **`UiItemSlot`** (port of `UIElement_UIItem`, class `0x10000032`) | class id `0x10000032` (resolves to a `UIElement_Field` subclass ⇒ underlying **Type 3**). Behavioral leaf. | **LEAF** (`ConsumesDatChildren=>true`) — reproduces its icon + overlay sub-elements procedurally | **all three** (toolbar slots, inventory cells, paperdoll equip slots) | one item-in-a-slot: icon (underlay/base/effects-overlay) + quantity text + capacity/structure Type-7 bars + cooldown ring; holds the bound object id (retail `+0x5FC`); selection/ghost/drag-accept/open-container overlay states. **The spine widget — build once.** |
| **`UiItemList` / `UiItemGrid`** (port of `UIElement_ItemList`, class `0x10000031`) | class id `0x10000031`. Behavioral widget. | **leaf wrt the importer** (manages its own `UiItemSlot` children procedurally) — but logically a **container** of slots | **all three** (toolbar = 1-cell instances; inventory = N-cell grids; paperdoll = 1-cell equip slots) | a 1-cell or N-cell grid of `UiItemSlot`s. Port `ItemList_AddItem/InsertItem/Flush/IsInList/GetNumUIItems/GetItem/OpenContainer/SetChildList/SetParentContainer/OpenFirstContainer`. Backpack uses **two** instances (own list `+0x604`, other-container list `+0x608`). |
| **`UiViewport`** (port of `UIElement_Viewport`, Type `0xD`) | numeric Type **`0xD`** (confirmed registry; `UIElement_Viewport::Register``RegisterElementClass(0xd,…)` decomp 119126) | **LEAF** (`ConsumesDatChildren=>true`) | **paperdoll only** | hosts a single live 3D entity (the character clone) rendered into the widget's screen rect via a scissored mini 3D pass. Owns a fixed camera + one distant light + an `AnimatedEntityState`. **Needs a new Core→App render-into-rect seam (`IUiViewportRenderer`, Code-Structure Rule 2). The biggest new piece.** |
| **Window manager** (shared infra; drives Dragbar Type 2 + Resizebar Type 9) | not a registered widget — infra that drives existing Type-2/Type-9 chrome + `UiElement.Draggable/Resizable` | n/a | **all three** (plus future pop-ups) | open/close/z-order/persist for pop-up windows + faithful grip/dragbar drag-resize. Today vitals/chat use whole-window drag (accepted IA-12 approximation). This is "the other deferred Plan-2 piece." |
| **Sub-window mount** (LayoutImporter capability, not a widget) | n/a — an element whose Type is a high `0x10000xxx` game class WITH a non-zero `BaseLayoutId` | container | **inventory** (frame nests paperdoll + backpack + 3D-items) | lets `LayoutImporter` instantiate a NESTED `LayoutDesc` window inside a parent slot. The importer recurses generic children today but has never mounted another gm\*UI window. |
| **Per-panel controllers** (`ToolbarController`, `InventoryController`, `PaperDollController`) | not widgets — controllers like `VitalsController`/`ChatWindowController` | n/a | one per panel | find-by-id binding + wire send/receive + model restore. The `gm*UI::PostInit` analogues. (Listed for completeness; each is panel-specific, not a shared widget.) |
### 2a. Reconciled disagreements between the agents
The four docs were **consistent** on the big-three widget identities; the differences
were wording, not substance. Reconciled:
1. **Item-slot Type — no real conflict.** Toolbar + inventory + paperdoll all call it
`UIElement_UIItem`, class **`0x10000032`**, a `UIElement_Field` subclass (underlying
Type 3), built as a behavioral **leaf**. The paperdoll doc's widget table named its
equip-slot variant "`UiItemSlot` registering at `0x10000031`" — that is the *equip
slot* (a single-cell `UIElement_ItemList`), NOT the inner item-cell. **Reconciliation:
`UIElement_ItemList` (`0x10000031`) is the slot/grid container; `UIElement_UIItem`
(`0x10000032`) is the item-cell inside it.** The paperdoll equip slot is a 1-cell
`UIElement_ItemList` that holds at most one `UIElement_UIItem` — same two-widget spine
as everywhere else, just constrained to one cell. (CONFIRMED: every paperdoll slot is
`DynamicCast(0x10000031)`, decomp 175242-175508; the inner cell is the
`UIElement_UIItem` `0x10000032` per the inventory agent's `UIItem_Update`/`m_elem_Icon`
citations, re-verified at 229180-229413.)
2. **Item-list "leaf vs container".** Toolbar + paperdoll said **leaf** (the importer
doesn't build its dat children; it reproduces cells procedurally); inventory said
**container** (it lays out an N-column grid). **Reconciliation: it is a behavioral
LEAF to the importer** (`ConsumesDatChildren=>true`, the importer must NOT recurse its
dat children) but it is logically a **container of `UiItemSlot`s at runtime** (it
creates/destroys cells procedurally as items arrive). Both descriptions are correct
at different layers; the binding rule for the factory is `ConsumesDatChildren=>true`.
3. **`UiViewport` Type.** Only the paperdoll doc introduced it; **Type `0xD`**,
confirmed against the registry (`0xD`=Viewport) and `RegisterElementClass(0xd,…)`.
No conflict.
4. **Window manager.** All three docs named it identically (shared, drives Dragbar
Type 2 + Resizebar Type 9, open/close/z-order/persist). No conflict.
5. **`+0x5FC` (bound object id on the item-cell).** The toolbar doc anchors this by
OFFSET only (UNVERIFIED symbolic name). The inventory/spine-territory render of the
cell would have named it; since the spine doc is missing, **it stays UNVERIFIED**
carried to §5.
---
## 3. Cross-panel wire-message catalog (de-duplicated)
All C→S ride the `0xF7B1` GameAction envelope (`u32 0xF7B1; u32 seq; u32 subOpcode; …`);
all S→C item events ride the `0xF7B0` GameEvent envelope (`u32 0xF7B0; u32 target;
u32 seq; u32 eventOpcode; …`). De-duplicated across the three panels; the "Panels"
column shows which panel(s) use each. acdream parse-status is the union of what the
three agents found (each cross-checked against `src/AcDream.Core.Net/`).
### 3.1 Client → server (GameActions)
| Opcode | Name | Dir | Trigger | ACE handler | Chorizite type | acdream status | Panels |
|---|---|---|---|---|---|---|---|
| `0x0019` | PutItemInContainer | C→S | drag item into pack / pick up (container=self) | `GameActionPutItemInContainer.Handle` | `Inventory_PutItemInContainer*` | **parsed**`InteractRequests.BuildPickUp` (InteractRequests.cs:97) | inv, paperdoll (un-wield) |
| `0x001A` | GetAndWieldItem | C→S | equip item from pack onto doll/equip slot | `GameActionGetAndWieldItem.Handle` (Actions/GameActionGetAndWieldItem.cs:7-14) → `Player.HandleActionGetAndWieldItem(itemGuid, EquipMask)` | `Inventory_GetAndWieldItem` (`uint ObjectId; EquipMask Slot`, generated.cs:14-42) | **MISSING** (no builder) | paperdoll, inv |
| `0x001B` | DropItem | C→S | drop item on the ground | `GameActionDropItem.Handle` (1×u32 guid) | — | **MISSING** (no builder) | inv |
| `0x0035` | UseWithTarget | C→S | use src item on target (toolbar target-mode / key→door) | (Interact) | — | **parsed**`InteractRequests.BuildUseWithTarget` | toolbar, inv |
| `0x0036` | UseItem | C→S | use/activate a single item (toolbar slot activation) | `GameActionUseItem` | — | **parsed**`InteractRequests.BuildUse` | toolbar, inv |
| `0x0054` | StackableMerge | C→S | drop stack A onto compatible stack B | `GameActionStackableMerge.Handle` (from,to,amount) | — | **parsed**`InventoryActions.BuildStackableMerge` | inv |
| `0x0055` | StackableSplitToContainer | C→S | split N off a stack into a pack slot | `GameActionStackableSplitToContainer.Handle` (stack,container,place,amount) | — | **parsed**`InventoryActions.BuildStackableSplitToContainer` | inv |
| `0x0056` | StackableSplitTo3D | C→S | split N off a stack onto the ground | `GameActionStackableSplitTo3D.Handle` (stack,amount) | — | **parsed**`InventoryActions.BuildStackableSplitTo3D` | inv |
| `0x019B` | StackableSplitToWield | C→S | split N off a stack into an equip slot (arrows) | `GameActionStackableSplitToWield` (stack,equipMask,amount) | — | **parsed**`InventoryActions.BuildStackableSplitToWield` | inv, paperdoll |
| `0x00CD` | GiveObjectRequest | C→S | give item/N-of-stack to NPC/player | `GameActionGiveObjectRequest.Handle` (target,item,amount) | — | **parsed**`InventoryActions.BuildGiveObjectRequest` | inv |
| `0x0195` | NoLongerViewingContents | C→S | close a side-pack / ground-container view | `GameActionType` 0x0195 (containerGuid) | — | **MISSING** (no builder) | inv |
| `0x019C` | AddShortCut | C→S | pin item to toolbar slot (on drag-to-slot / add-selected) | `GameActionAddShortcut.Handle``Character.AddOrUpdateShortcut(Index,ObjectId)` | `Character_AddShortCut { ShortCutData Shortcut }` | **builder present**`InventoryActions.BuildAddShortcut` *(fix field naming → Index/ObjectId/SpellId\|Layer)* | toolbar |
| `0x019D` | RemoveShortCut | C→S | unpin / evict / overwrite a toolbar slot | `GameActionRemoveShortcut.Handle``Character.TryRemoveShortcut(index)` | `Character_RemoveShortCut { uint Index }` | **builder present**`InventoryActions.BuildRemoveShortcut` | toolbar |
### 3.2 Server → client (GameEvents / GameMessages)
| Opcode | Name | Dir | Trigger | ACE handler | Chorizite type | acdream status | Panels |
|---|---|---|---|---|---|---|---|
| `0x0022` | InventoryPutObjInContainer | S→C | confirm item in container at slot | `GameEventItemServerSaysContainId` (itemGuid,containerGuid,placement,**containerType**) | — | **parsed INCOMPLETE**`GameEvents.ParsePutObjInContainer` reads 3 fields, **drops containerType**; wired (GameEventWiring.cs:239) | inv |
| `0x0023` | WieldObject | S→C | confirm item equipped to slot | `GameEventWieldItem` (objectId, i32 equipMask) | — | **parsed + wired**`GameEvents.ParseWieldObject`, GameEventWiring.cs:231 | paperdoll, inv |
| `0x0052` | CloseGroundContainer | S→C | server closed a ground-container view | `GameEventCloseGroundContainer` (containerGuid) | — | **parsed UNWIRED**`GameEvents.ParseCloseGroundContainer`, not in WireAll | inv |
| `0x00A0` | InventoryServerSaveFailed | S→C | reject speculative client move (roll back) | `GameEventInventoryServerSaveFailed` (itemGuid, weenieError) | — | **parsed UNWIRED INCOMPLETE** — reads guid only, **drops error**; not in WireAll | inv (+toolbar/paperdoll rollback) |
| `0x00C9` | IdentifyObjectResponse | S→C | appraise result (gates tooltip, not icon) | `GameEventIdentifyObjectResponse` (guid,flags,success,property tables) | — | **parsed + wired**`AppraiseInfoParser` via GameEventWiring.cs:245 | inv, paperdoll, toolbar (tooltip) |
| `0x0196` | ViewContents | S→C | full contents list of an opened container | `GameEventViewContents` (container,count,{guid,containerType}×n) | — | **MISSING** (no parser) | inv |
| `0x0197` | SetStackSize | S→C | update a stack count+value after merge/split | `GameMessageSetStackSize` (seq,guid,stackSize,value) | — | **MISSING** (no parser) | inv, toolbar |
| `0x019A` | InventoryPutObjectIn3D | S→C | confirm item dropped to world | `GameEventItemServerSaysMoveItem` (objectGuid) | — | **parsed UNWIRED**`GameEvents.ParsePutObjectIn3D`, not in WireAll | inv |
| `0xF625` | ObjDescEvent | S→C | wield/unwield → full new appearance broadcast → `RedressCreature` | `GameMessageObjDescEvent``SerializeUpdateModelData` (GameMessageObjDescEvent.cs:10-17) | (ModelData block) | **parsed**`ObjDescEvent.cs:33-73` (`CreateObject.ReadModelData`) | paperdoll |
| `0xF745` | CreateObject | S→C | spawn a weenie incl. a pack item (IconId/WeenieClassId/StackSize/Value/capacities) | `GameMessageCreateObject``WorldObject.SerializeCreateObject` | `Item_CreateObject` | **parsed INCOMPLETE**`CreateObject.TryParse` **discards IconId (cs:516), WeenieClassId (cs:515), StackSize, Value, ItemCapacity, ContainerCapacity** | all (icon + quantity source) |
| (UIQueue) | InventoryRemoveObject | S→C | remove item from inventory view (given/dropped/destroyed) | `GameMessageInventoryRemoveObject` (guid) | — | **MISSING** (no parser) | inv, toolbar |
| `PlayerDescription` SHORTCUT block | persisted toolbar shortcut list | S→C | login (part of `0xF7B0`/0x0013 `PlayerDescription`) | `Player_Character.GetShortcuts()` | `ShortCutData` (Index,ObjectId,LayeredSpellId) | **parsed**`PlayerDescriptionParser.cs:345-356``Parsed.Shortcuts` | toolbar |
| `PlayerDescription` equipped `InventoryPlacement` list | persisted equipped-gear list | S→C | login | `GameEventPlayerDescription.WriteEventBody` | `Login_PlayerDescription` | **PARTIAL** — equipped section NOT surfaced (PlayerDescriptionParser.cs:70-77) | paperdoll |
**Shared-message note:** `CreateObject (0xF745)` and `IdentifyObjectResponse (0x00C9)`
are used by all three panels and de-duplicated above. `GetAndWieldItem (0x001A)` is
shared by inventory (equip-from-grid) and paperdoll (drop-on-doll); `UseItem (0x0036)`/
`UseWithTarget (0x0035)` are shared by toolbar activation and inventory double-click.
### 3.3 acdream wire-gap TODO (the build session's concrete list)
- **Add C→S builders:** `GetAndWieldItem (0x001A)`, `DropItem (0x001B)`,
`NoLongerViewingContents (0x0195)`.
- **Add S→C parsers:** `ViewContents (0x0196)`, `SetStackSize (0x0197)`,
`InventoryRemoveObject`.
- **Fix incomplete parsers:** `ParsePutObjInContainer` (read the 4th `containerType`
u32); `ParseInventoryServerSaveFailed` (read the `weenieError` u32).
- **Wire (register in `GameEventWiring.WireAll`):** `ViewContents`,
`InventoryPutObjectIn3D (0x019A)`, `CloseGroundContainer (0x0052)`,
`InventoryServerSaveFailed (0x00A0)`.
- **Extend `CreateObject.TryParse`** to capture `IconId`, `WeenieClassId`, `StackSize`,
`Value`, `ItemCapacity`, `ContainerCapacity` (cells need icon + quantity +
capacity bar). **Re-verified discarded at `CreateObject.cs:515-516`.**
- **Extend `PlayerDescriptionParser`** to surface the equipped `InventoryPlacement
{iid, loc, priority}` list (paperdoll slot icons at login).
- **Fix `InventoryActions.BuildAddShortcut` field naming** (currently
`slotIndex/objectType/targetId`; wire layout is correct for item shortcuts but
semantics should be `Index/ObjectId/SpellId|Layer`).
---
## 4. Recommended build order
Ordered by dependency so the next session can go straight to brainstorm → spec → plan.
Each step states why it must come where it does.
**Step 0 — SPINE research (DONE — see the spine doc).**
The spine doc is complete:
[`2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md`](2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md).
It specs the item-cell widget, the 5-layer icon composite (`IconData::RenderIcons`
decomp 407524 — each layer a `0x06` RenderSurface decoded directly, blitted into one
private 32×32 surface: type-default underlay / custom underlay / base `_iconID` / custom
overlay+tint / effect overlay), `UIElement_UIItem::UIItem_Update`/`UIItem_SetIcon`
(decomp 230226+), the overlay-state machine, and the widget-level drag-drop hooks
(`UIElement_Field::MouseOverTop`/`CatchDroppedItem`). The `+0x5FC` field is resolved
(`UIElement_UIItem::itemID`). Steps 2-7 can now proceed against a finished port spec;
this is **no longer blocking**. (Note: WB `TextureHelpers.cs` / ACViewer `IndexToColor`
are for WORLD textures — item icons take NO subpalette overlay at composite time; see
the spine doc.)
**Step 1 — Window manager foundation.**
*Why first among code:* all three panels are pop-up windows that must open/close, stack
(z-order), persist position, and (faithfully) drag/resize via Dragbar (Type 2) +
Resizebar (Type 9). Vitals/chat shipped with whole-window drag (accepted IA-12
approximation); the panels need the real thing. Everything visible in Steps 5-7 mounts
inside a managed window, so the manager is the substrate. It is independent of the wire
work, so it can proceed in parallel with Step 0.
**Step 2 — `UiItemSlot` widget + icon pipeline (`UIElement_UIItem` 0x10000032).**
*Why here:* it is the atom of all three panels. Depends on Step 0 (icon render) and on
the §3.3 `CreateObject` extension (IconId/StackSize) for real data. Build the leaf
widget: icon composite, quantity text, capacity/structure Type-7 bars, cooldown ring,
and the selection/ghost/drag-accept/open-container overlay states.
**Step 3 — `UiItemList` / `UiItemGrid` widget (`UIElement_ItemList` 0x10000031).**
*Why here:* it composes `UiItemSlot`s and is used by every panel (1-cell and N-cell).
Depends on Step 2. Port `ItemList_AddItem/InsertItem/Flush/IsInList/GetNumUIItems/
GetItem/OpenContainer/SetChildList/SetParentContainer`. Register as a behavioral leaf
(`ConsumesDatChildren=>true`).
**Step 4 — Wire wiring (builders/parsers/wireup from §3.3).**
*Why here:* the controllers in Steps 5-7 need the full send/receive surface, and this is
independent of the widget rendering — it can run in parallel with Steps 1-3. Add the
missing builders/parsers, fix the two incomplete parsers, register the unwired parsers,
extend `CreateObject` + `PlayerDescriptionParser`, fix `BuildAddShortcut` naming. Each
new deviation gets a divergence-register row in the same commit.
**Step 5 — `ToolbarController` + the action bar (simplest panel).**
*Why before the others:* the toolbar is the simplest consumer (18 single-cell lists, no
nested sub-windows, no viewport) and exercises the full spine + window manager + wire
path end-to-end. acdream already parses the SHORTCUT block and has both shortcut
builders, so it's the fastest path to a working, testable panel. Bind the 18 slots,
the hidden selected-object meters + stack slider, the panel-launcher buttons; restore
from `Parsed.Shortcuts`; wire `UseShortcut`/`AddShortcut`/`RemoveShortcut` +
`HandleDropRelease`.
**Step 6 — `InventoryController` + the inventory/backpack panels + sub-window mount.**
*Why here:* adds the N-cell grid (Step 3 at scale), the burden Meter (reuses Type-7
`SetLoadLevel`→fill 0x69), the dual-ItemList container model (own `+0x604` / other
`+0x608`), and the **sub-window mount** importer capability (frame nests paperdoll +
backpack + 3D-items). The hardest 2D panel; depends on Steps 1-4 and the new sub-window
mount.
**Step 7 — `UiViewport` + `PaperDollController` + the equipment doll (biggest new piece).**
*Why last:* it depends on everything above (window manager, equip-slot `UiItemList`
instances, `GetAndWieldItem` wire, `PlayerDescription` equipped list) AND introduces the
single largest new engineering item: the **UI↔3D render seam** (`IUiViewportRenderer`
Core interface, App impl, per Code-Structure Rule 2) that renders a re-dressed player
clone into a scissored UI rect. It reuses acdream's existing `EntitySpawnAdapter`/
`AnimatedEntityState` character path, but the rect-scissored single-entity pass is new.
Doing it last lets the 2D panels validate the spine first, so a 3D-render bug is
isolated.
**Parallelism summary:** Step 0 (spine research) + Step 1 (window manager) + Step 4
(wire) can all proceed independently; Steps 2→3→5→6→7 are the dependent spine→panels
chain.
---
## 5. Open risks / UNVERIFIED — resolve BEFORE implementation
Collated from all four docs; each needs a decomp or cdb follow-up before the cited step.
1. **SPINE doc — RESOLVED (no longer blocking).** Written:
[`2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md`](2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md).
Icon-composite render (`IconData::RenderIcons` 407524, 5 layers) + the widget-level
drag-drop state machine (`UIElement_Field::MouseOverTop`/`CatchDroppedItem`, cell msgs
`0x21`/`0x3e`/`0x15`, `InqDropIconInfo` flags) are now specced with anchors.
2. **`UIElement_UIItem +0x5FC` bound-object-id field name — RESOLVED = `itemID`.**
`UIElement_UIItem::itemID`, anchored at `UIItem_Update` decomp 230230
(`uint32_t itemID = this->itemID; … GetWeenieObject(itemID)`), corroborated 230422/233107
(companion field `spellID`). See the spine doc.
3. **`CreateObject` IconId for CONTAINED (non-3D-visible) pack items — LIKELY.**
Confirmed on the wire + currently discarded (`CreateObject.cs:516`), but not
byte-traced that ACE sets IconId on a *contained* item's CreateObject vs. relying on
PlayerDescription. Verify against a live capture before treating CreateObject as the
sole icon source.
4. **Use-item opcode `ItemHolder::UseObject` sends (0x0035 vs 0x0036) — UNVERIFIED.**
Throttle (0.2 s) + dispatch CONFIRMED (decomp 402923); the precise opcode branch
(`DetermineUseResult`) not traced to the send. Both opcodes exist in acdream
`InteractRequests.cs`; reconcile when wiring toolbar/inventory activation (Step 5/6).
5. **`UseShortcut` target-mode path — out of scope, file follow-up.**
`ClientUISystem::ExecuteTargetModeForItem` (use-item-on-target) depends on the cursor
target-mode subsystem; not part of the action-bar widget itself.
6. **`SetDelayedShortcutNum` deferral — needs a re-bind state machine.** When a slot's
weenie isn't loaded yet (`AddShortcut` decomp 196867), the slot must re-bind once
`CreateObject` for that guid arrives. Detail in the `ToolbarController` port (Step 5).
7. **Paperdoll `0x100001E0` = MissileAmmo `0x800000` — LIKELY only.** The decomp
immediate is corrupted to a string-ptr (line 173676); inferred from the EquipMask gap
+ neighbors. Re-decompile `0x004a388a` in Ghidra to recover the real value (Step 7).
8. **Paperdoll viewport camera/light float immediates — UNVERIFIED (not byte-decoded).**
Lines 175524-175526 / 174144-174146; the agent read the hex but did not convert all
floats (`0x3df5c28f≈0.12`, `0xc019999a≈2.4`, `0xc0400000=3.0`, `0xc059999a≈3.4`,
`0x3f6147ae≈0.88`, `0x3f800000=1.0`). Decode precisely for faithful framing (Step 7).
9. **UI↔3D render seam — DESIGN-OPEN.** How a UI rect drives a scissored single-entity
3D pass (after the world pass vs. as a UI overlay), and the exact
`IUiViewportRenderer` Core-interface shape (Code-Structure Rule 2). Brainstorm before
Step 7.
10. **Does the doll clone the player `WorldEntity` or build a fresh one? — UNVERIFIED.**
Retail clones the player `CPhysicsObj` (`makeObject(GetPhysicsObject(player_id))`,
line 173999); acdream has no player-as-renderable today (player = camera). LIKELY a
dedicated `WorldEntity` from the local player's Setup+ObjDesc fed to a private
viewport host. Settle in Step 7 brainstorm.
11. **Inventory side-pack column `0x100001CB` (16×252, base `0x2100003E`) — UNVERIFIED.**
Tabs (one per sub-bag) or a scrollbar gutter? Dump `0x2100003E` to settle (Step 6).
12. **`UIElement_ItemList` grid geometry (column count, cell pitch) — LIKELY.** Cell
template 36×36 (`0x100001C9`); `UIElement_UIItem` `0x21000037` is 32×32. Confirm the
fixed-column wrap by reading `UIElement_ItemList::ItemList_AddItem` (Step 3).
13. **Value/coin total NOT in the inventory window — UNVERIFIED home.** No value Meter/
text in `0x21000022`/`0x21000023`; the window shows BURDEN only. Do NOT invent a
value summary; find its real home before adding one.
14. **Identified-vs-unidentified does NOT swap the icon — CONFIRMED (negative).** The
spine doc confirms there is no appraise branch anywhere in the icon path
(`UIItem_SetIcon``IconData::RenderIcons`); appraise gates `UpdateTooltip` only.
15. **`InventoryActions.BuildAddShortcut` field-naming bug — CONFIRMED file contents,
LIKELY latent bug.** Wire layout is correct for item shortcuts; the param names
(`slotIndex/objectType/targetId`) are misleading. Fix to `Index/ObjectId/
SpellId|Layer` + register a divergence row at port time (Step 4/5).
---
## 6. Proposed MEMORY.md index lines (for ALL 5 docs)
The parent will append these; I do NOT edit MEMORY.md.
- [UI core-panels SYNTHESIS (toolbar+inventory+paperdoll)](research/2026-06-16-ui-panels-synthesis.md) — D.2b build plan reconciling the 3 panel deep-dives. Big-three new widgets: `UiItemSlot`(`UIElement_UIItem` 0x10000032, shared leaf), `UiItemList/Grid`(`UIElement_ItemList` 0x10000031, shared leaf-to-importer), `UiViewport`(Type 0xD, paperdoll 3D doll), plus the shared **window manager** (Dragbar 2 + Resizebar 9) + sub-window-mount importer capability + per-panel controllers. De-duped cross-panel wire table; build order (window mgr → item-slot+icon → item-list → wire → toolbar → inventory → paperdoll; spine research DONE).
- [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, `+0x5FC` RESOLVED = `itemID`) inside `UiItemList`(0x10000031). ICON CRUX: each layer is a `0x06` RenderSurface decoded DIRECTLY, but the icon is a 5-layer runtime COMPOSITE (`IconData::RenderIcons` @407524; NOT one texture, NOT appraise-gated). Drag-drop = `Field::MouseOverTop`/`CatchDroppedItem` + cell msgs 0x21/0x3e/0x15 + `InqDropIconInfo` flags; `UiRoot` already has the chain, `UiField` only stubs the hooks; gap = `CreateObject` discards IconId (cs:516).
- [Action bar / quick slots (`gmToolbarUI`) deep dive](research/2026-06-16-action-bar-toolbar-deep-dive.md) — 18 item slots (2 rows of 9, ids `0x100001A7-AF`+`0x100006B7-BF`) = `UIElement_ItemList`(0x10000031) of one `UIElement_UIItem`(0x10000032); model `ShortCutManager::shortCuts_[18]` persisted in `PlayerDescription`'s SHORTCUT block (acdream already parses it); live mutate via `AddShortCut 0x019C`/`RemoveShortCut 0x019D` (acdream builders present — fix `BuildAddShortcut` field naming); activation = ordinary use-item (`ItemHolder::UseObject`, no special wire); the 2 Meters + Scrollbar in `0x21000016` are the hidden selected-object Health/Mana bars + stack-split slider, NOT paging; drag-drop via `gmToolbarUI : ItemListDragHandler::HandleDropRelease`. New widgets: `UiItemSlot` + `UiItemList` + `ToolbarController`.
- [Inventory panel deep-dive (gmInventoryUI/gmBackpackUI)](research/2026-06-16-inventory-deep-dive.md) — D.2b: LayoutDesc 0x21000023 (frame: title + 3 nested sub-windows) + 0x21000022 (backpack: burden Meter 0x100001D9 via SetLoadLevel→fill 0x69, main-pack ItemList 0x100001CA); full inventory wire catalog (C→S 0x0019/1A/1B/54/55/56/19B/CD/195, S→C 0x0022/23/196/19A/A0/52 + SetStackSize/InventoryRemoveObject) with acdream parse-status (gaps: DropItem/GetAndWieldItem/ViewContents builders, 0x0022 4th field, CreateObject IconId); new widgets UiItemSlot(0x10000032)/UiItemGrid(0x10000031)+sub-window mount+window manager.
- [Equipment/Paperdoll panel deep-dive](research/2026-06-16-equipment-paperdoll-deep-dive.md) — gmPaperDollUI 0x10000024/LayoutDesc 0x21000024: doll = UIElement_Viewport (Type 0xD, elem 0x100001D5) hosting a CreatureMode clone re-dressed via DoObjDescChangesFromDefault; ~25 equip slots are single-cell UIElement_ItemList (0x10000031) mapped element-id→EquipMask by GetLocationInfoFromElementID; wield = GetAndWieldItem (0x001A, item+EquipMask, acdream-MISSING), appearance reply = ObjDescEvent 0xF625 (acdream-PARSED) → RedressCreature; acdream's EntitySpawnAdapter/AnimatedEntityState char path is reusable; new widgets = UiViewport (0xD, the UI↔3D bridge), UiItemSlot (0x10000031), window manager. gm3DItemsUI 0x21000021 is a "Contents of Backpack" list, NOT the doll.
- [Action-bar/inventory/equipment research handoff](research/2026-06-16-action-bar-inventory-equipment-handoff.md) — the §3 question list (Q1-Q12) + agent assignment that drove the toolbar/inventory/paperdoll/spine deep-dives. (All 5 research docs delivered; the spine doc was completed in a follow-up pass after a transient agent failure.)
---
## 7. New toolkit widgets this introduces (recap)
| Widget | dat Type / class it registers at | leaf vs container | Purpose |
|---|---|---|---|
| `UiItemSlot` | class `0x10000032` (`UIElement_UIItem`) | leaf (`ConsumesDatChildren=>true`) | shared item-cell: icon + quantity + capacity/structure bars + overlay states + bound object id |
| `UiItemList` / `UiItemGrid` | class `0x10000031` (`UIElement_ItemList`) | leaf to importer; container of slots at runtime | shared 1-cell/N-cell grid of `UiItemSlot`s |
| `UiViewport` | numeric Type `0xD` (`UIElement_Viewport`) | leaf (`ConsumesDatChildren=>true`) | paperdoll 3D character doll via a scissored mini 3D pass; needs `IUiViewportRenderer` Core→App seam |
| Window manager | infra (drives Dragbar Type 2 + Resizebar Type 9) | n/a | open/close/z-order/persist + faithful grip/dragbar drag-resize for all pop-up panels |
| Sub-window mount | LayoutImporter capability (element whose Type is a high `0x10000xxx` class with a `BaseLayoutId`) | container | nest a `LayoutDesc` window inside a parent slot (inventory frame → paperdoll/backpack/3D-items) |
## 8. Open questions / UNVERIFIED (recap)
See §5 for the full collated list with anchors. The former blocking item — the spine
doc — is now written (icon-composite render path + widget-level drag-drop state machine
specced with anchors; `+0x5FC` = `itemID` resolved). Remaining items are per-step
follow-ups (decode the paperdoll camera floats, recover `0x100001E0`, dump
`0x2100003E`, byte-trace CreateObject IconId for contained items).
---
**Single MEMORY.md index line for THIS doc:**
- [UI core-panels SYNTHESIS (toolbar+inventory+paperdoll)](research/2026-06-16-ui-panels-synthesis.md) — D.2b build plan reconciling the 3 panel deep-dives: shared `UiItemSlot`(0x10000032)+`UiItemList`(0x10000031) spine, `UiViewport`(Type 0xD) for the paperdoll 3D doll, window manager (Dragbar 2 + Resizebar 9) + sub-window-mount; de-duped cross-panel wire table; build order window-mgr→item-slot+icon→item-list→wire→toolbar→inventory→paperdoll (spine research DONE — see the spine deep-dive).