From a5c5126e8d431a5bb13dfe4da11a5ac3902ef9e9 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 16 Jun 2026 21:04:57 +0200 Subject: [PATCH] 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) --- ...-action-bar-inventory-equipment-handoff.md | 115 ++++ ...2026-06-16-action-bar-toolbar-deep-dive.md | 191 ++++++ ...026-06-16-equipment-paperdoll-deep-dive.md | 416 +++++++++++++ .../2026-06-16-inventory-deep-dive.md | 391 ++++++++++++ ...item-slot-icon-dragdrop-spine-deep-dive.md | 557 ++++++++++++++++++ .../2026-06-16-ui-panels-synthesis.md | 407 +++++++++++++ 6 files changed, 2077 insertions(+) create mode 100644 docs/research/2026-06-16-action-bar-inventory-equipment-handoff.md create mode 100644 docs/research/2026-06-16-action-bar-toolbar-deep-dive.md create mode 100644 docs/research/2026-06-16-equipment-paperdoll-deep-dive.md create mode 100644 docs/research/2026-06-16-inventory-deep-dive.md create mode 100644 docs/research/2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md create mode 100644 docs/research/2026-06-16-ui-panels-synthesis.md diff --git a/docs/research/2026-06-16-action-bar-inventory-equipment-handoff.md b/docs/research/2026-06-16-action-bar-inventory-equipment-handoff.md new file mode 100644 index 00000000..3834ba94 --- /dev/null +++ b/docs/research/2026-06-16-action-bar-inventory-equipment-handoff.md @@ -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 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. diff --git a/docs/research/2026-06-16-action-bar-toolbar-deep-dive.md b/docs/research/2026-06-16-action-bar-toolbar-deep-dive.md new file mode 100644 index 00000000..de3e30de --- /dev/null +++ b/docs/research/2026-06-16-action-bar-toolbar-deep-dive.md @@ -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, )` 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 197054–197560): + +| 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 **0–11**, **use** (arg3=1). (decomp 197576–197591) +- `0x1000004E..0x10000059` → `UseShortcut(this, msg-0x1000004E, 0)` → slots **0–11**, **select** (arg3=0). (decomp 197592–197606) +- `0x10000132..0x10000137` → `UseShortcut(this, 0xC..0x11, 1)` → slots **12–17**, **use**. (decomp 197616–197645) +- `0x10000138..0x1000013D` → `UseShortcut(this, 0xC..0x11, 0)` → slots **12–17**, **select**. (decomp 197646–197674) + +The slot count `18` is independently confirmed by the header struct `ShortCutManager::shortCuts_[18]` (`acclient.h` line 36492–36494: `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 198307–198310) 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 198179–198303). `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 196268–196300): +- 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 196412–196421) +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` 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 198879–198893). 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 197974–197976) +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 198031–198056) — 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 198007–198018) + - **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 198020–198027) + +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 196928–196930). Else (slot unspecified) it scans for a home (the loop at 196954+, with a "no empty slot" `DisplayStringInfo` notice when full, decomp 196945–196949). 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 196836–196848). Then `ItemList_Flush(slot); ItemList_AddItem(slot, objId); SetShortcutNum(weenie, slot)` (or `SetDelayedShortcutNum` if the weenie isn't loaded yet, decomp 196861–196867). If `send`, build `CShortCutData(slot, objId, 0)` → `Event_AddShortCut` (wire) + `PlayerModule::AddShortCut` (local model) (decomp 196873–196876). +- **`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 196471–196496) +- **`RemoveShortcutInSlotNum(slot, send)`** (decomp 196502): read the `UIItem` objId at `+0x5FC`, `RemoveShortcut(objId, send)`, return the evicted objId. (decomp 196519–196524) +- **`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 196539–196569) +- **`FlushShortcuts()`** (decomp 196442): `ItemList_Flush` every slot (visual clear; does NOT touch the server). Used by login restore. (decomp 196451–196457) + +## 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`. diff --git a/docs/research/2026-06-16-equipment-paperdoll-deep-dive.md b/docs/research/2026-06-16-equipment-paperdoll-deep-dive.md new file mode 100644 index 00000000..1d6f5a56 --- /dev/null +++ b/docs/research/2026-06-16-equipment-paperdoll-deep-dive.md @@ -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 +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`** 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. diff --git a/docs/research/2026-06-16-inventory-deep-dive.md b/docs/research/2026-06-16-inventory-deep-dive.md new file mode 100644 index 00000000..614932d4 --- /dev/null +++ b/docs/research/2026-06-16-inventory-deep-dive.md @@ -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 (176240–176259): 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 (176600–176629): 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 (176565–176573): 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 176576–176583: +`floor(arg2 * 300.0)` → `SetText(m_burdenText, "%d%%")`). So the bar is FULL +at 100% load and the number reads 0–300% (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:405–432`). [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 176269–176277)** +— 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, 980257–980562) 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:13–76` — +`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:10–13` 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:13–26` writes `Guid, count, {guid, + containerType}×n`; holtburger `events.rs:20` (+ fixture `events.rs:195`). +- `0x0023`: `GameEventWieldItem.cs:11–12` writes `objectId, (int)newLocation`. +- `0x019A`: `GameEventItemServerSaysMoveItem.cs:11` writes only `Guid`. +- `0x00A0`: `GameEventInventoryServerSaveFailed.cs` (error code present; + holtburger reads it). +- `SetStackSize`: `GameMessageSetStackSize.cs:12–15` (`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` 229271–229277, 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` 229738–229740 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` 229190–229208 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 `…54f–558`) 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. diff --git a/docs/research/2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md b/docs/research/2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md new file mode 100644 index 00000000..ba815855 --- /dev/null +++ b/docs/research/2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md @@ -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. diff --git a/docs/research/2026-06-16-ui-panels-synthesis.md b/docs/research/2026-06-16-ui-panels-synthesis.md new file mode 100644 index 00000000..3dd77de1 --- /dev/null +++ b/docs/research/2026-06-16-ui-panels-synthesis.md @@ -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).