docs(D.5): action bar / inventory / paperdoll research drop
Five report-only deep-dives + synthesis for the next D.2b UI panels, built on the shipped widget toolkit. Confirms LayoutDesc ids (toolbar 0x21000016, inventory 0x21000023, backpack 0x21000022, paperdoll 0x21000024, 3ditems 0x21000021), the shared item-slot/item-list spine (UIElement_UIItem 0x10000032 / UIElement_ItemList 0x10000031), the 5-layer icon composite (IconData::RenderIcons @407524), the cross-panel wire catalog with acdream parse-status, and the dependency-ordered build plan. Produced via a multi-agent research workflow; the spine agent died on a transient API error and was re-run as a focused follow-up with its decomp anchors verified against source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
78c91875b8
commit
a5c5126e8d
6 changed files with 2077 additions and 0 deletions
|
|
@ -0,0 +1,115 @@
|
|||
# Handoff — next UI phase: action bar / quick slots + inventory + equipment (paperdoll)
|
||||
|
||||
**Date:** 2026-06-16
|
||||
**From:** the session that landed the D.2b widget generalization (merged to `main` at `78c9187`).
|
||||
**Purpose:** kick off a **deep retail-faithful research phase** for the next three game panels, before any implementation. This doc + the new-session prompt at the bottom are the entry point.
|
||||
|
||||
---
|
||||
|
||||
## 1. Where we are (what you're building on)
|
||||
|
||||
The **D.2b retail-UI toolkit is complete and on `main`.** You have:
|
||||
|
||||
- **A generic, Type-registered widget toolkit** built by `DatWidgetFactory` from the dat `LayoutDesc`: `UiButton` (Type 1), `UiMenu` (6), `UiMeter` (7), `UiScrollbar` (11), `UiText` (12), plus `UiField` (editable, controller-placed) and `UiDatElement` (generic chrome/container fallback). All in `src/AcDream.App/UI/`.
|
||||
- **The assembly pattern**: a window is a dat `LayoutDesc` → `LayoutImporter.Import(...)` walks the `ElementDesc` tree → `DatWidgetFactory` builds each element generically → a thin **`gm*UI::PostInit`-style controller** finds widgets by id (`layout.FindElement(id) as UiX`) and binds live data/behavior. See `VitalsController` and `ChatWindowController` for the two worked examples.
|
||||
- **Key toolkit rules** (read `claude-memory/project_d2b_retail_ui.md` first — it's the START-HERE digest with the full DO-NOT-RETRY list):
|
||||
- `UiElement.ConsumesDatChildren` — behavioral widgets are **leaf** (the importer doesn't build their dat sub-elements; they reproduce them procedurally).
|
||||
- The base-chain Type resolution (`ElementReader.Merge`) already surfaces each element's real registered Type.
|
||||
- Type 3 is **chrome/container** in acdream's layouts (stays `UiDatElement`), NOT a factory-registered editable Field.
|
||||
|
||||
**This phase's job is to build the next real game panels on that toolkit** — but they're complex (live item state, drag-drop, wire messages, icon rendering), so we research first per the project's mandatory **grep named → decompile → cross-ref → pseudocode → port** workflow (CLAUDE.md).
|
||||
|
||||
These panels are the roadmap's **F.5 / D.5 "core panels"** (Attributes / Skills / Paperdoll / Inventory / Spellbook).
|
||||
|
||||
---
|
||||
|
||||
## 2. The three targets + their retail entry points (concrete anchors)
|
||||
|
||||
All confirmed from the named decomp (`docs/research/named-retail/acclient_2013_pseudo_c.txt`):
|
||||
|
||||
### A. Action bar / quick slots → `gmToolbarUI` (element class `0x10000007`)
|
||||
The retail "toolbar" is the shortcut bar. Confirmed methods (grep `gmToolbarUI::`):
|
||||
- `UseShortcut(slot)`, `AddShortcut`, `RemoveShortcut`, `RemoveShortcutInSlotNum`, `FlushShortcuts`, `CreateShortcutToItem`, `IsShortcutEligible(ACCWeenieObject*)`, `IsShortcutSlotAvailable`, `GetFirstEmptyShortcutToTheRightOf`, `RecvNotice_AddShortcut/RemoveShortcut/UseShortcut`.
|
||||
- Slots hold **`UIElement_UIItem`** widgets (`UIElement_UIItem::SetShortcutNum` / `SetDelayedShortcutNum`); the underlying object is an **`ACCWeenieObject`** with `SetShortcutNum`.
|
||||
- Spell shortcuts: `UIElement_ItemList::ItemList_InsertSpellShortcut`, `CM_Magic::SendNotice_AddSpellShortcut`.
|
||||
|
||||
### B. Inventory → `gmInventoryUI` (element class `0x10000023`), `gmBackpackUI` (`0x10000022`)
|
||||
The inventory window + nested backpacks/side-packs. Items are server-spawned **weenies** (`ACCWeenieObject`) — see `claude-memory/feedback_weenie_vs_static.md` (selectable/interactable items are server weenies, not dat-baked).
|
||||
|
||||
### C. Equipment / paperdoll → `gmPaperDollUI` (element class `0x10000024`), `gm3DItemsUI` (`0x10000021`)
|
||||
The paperdoll window shows equipped/wielded items + the character doll. `gm3DItemsUI` is likely the 3D doll viewport (a rotating character model with equipped gear).
|
||||
|
||||
### Shared building blocks (the toolkit pieces these need that we DON'T have yet)
|
||||
- **`UIElement_UIItem`** (element class `0x10000032`) — **the item-in-a-slot widget**: an icon (from the weenie's `IconDataID`), drag-drop (retail `UIElement_Field`'s `CatchDroppedItem` / `MouseOverTop` hooks — note our `UiField` already documents these as future drag-drop hooks), a shortcut number, a quantity/burden overlay, a selection/highlight. **This is the most important new generic widget** — all three panels are grids/lists of these.
|
||||
- **`UIElement_ItemList`** (`0x10000031`) — a scrollable list/grid of items (retail's ListBox-for-items). Maps to a `UiListBox` (Type 5, not yet built) or a `UiItemGrid`.
|
||||
- **The window manager** (the *other* deferred Plan-2 piece): open/close/z-order/persist, drag via Dragbar (Type 2), resize via Resizebar (Type 9). Inventory/paperdoll/toolbar are pop-up/dockable windows that need this.
|
||||
- **Drag-drop infrastructure**: item drag between inventory ↔ equip ↔ toolbar ↔ ground, with the wire messages it triggers.
|
||||
|
||||
---
|
||||
|
||||
## 3. The research questions (the deep-research scope)
|
||||
|
||||
Produce a research doc per panel (or one combined doc) under `docs/research/` answering these, each with a **retail anchor** (named `class::method` + decomp line, or `LayoutDesc`/element-class id) and cross-referenced against the references in §4.
|
||||
|
||||
**Common / cross-cutting (do this first — it unblocks all three):**
|
||||
1. **The dat `LayoutDesc` id** for each panel (`gmToolbarUI` / `gmInventoryUI` / `gmPaperDollUI`). The element-class ids above are the *registered class*; find the actual `LayoutDesc` (`0x21xxxxxx`) that builds each window. Use the `AcDream.Cli` layout-dump tools (`dump-vitals-layout <datdir> 0xId`, the `LayoutIndexDump`) and grep the decomp for the class's `Create`/`PostInit`.
|
||||
2. **`UIElement_UIItem`** (`0x10000032`) — full port spec: what Type does it resolve to in the dat? How does it render an item icon from the weenie's `IconDataID` (→ `RenderSurface`/`Icon` overlay — cross-ref WorldBuilder/ACViewer icon decode)? How are quantity, burden, wielded/selected states drawn? What's the drag-drop state machine (`MouseOverTop`/`CatchDroppedItem`)?
|
||||
3. **The item/container data model**: items are `ACCWeenieObject`s. How does the client learn container contents — which wire messages (CreateObject, the container/inventory messages), and how is the container hierarchy (main pack → backpacks → side-packs) represented? What does acdream already parse (cross-ref the wire-message catalog, §4)?
|
||||
|
||||
**Action bar (`gmToolbarUI`):**
|
||||
4. The shortcut slot model — how many slots/bars, item vs spell shortcuts, what a slot stores (`ShortcutNum`), `IsShortcutEligible`.
|
||||
5. Persistence + wire: is the toolbar server-persisted? What messages add/remove/use a shortcut (`RecvNotice_AddShortcut/RemoveShortcut/UseShortcut`) and what does activation send (use-item / cast-spell)?
|
||||
6. Drag-from-inventory-to-slot + drag-to-reorder (`CreateShortcutToItem`, `GetFirstEmptyShortcutToTheRightOf`).
|
||||
|
||||
**Inventory (`gmInventoryUI` / `gmBackpackUI`):**
|
||||
7. The window layout (the item grid, the side-pack tabs/list, burden/value summary).
|
||||
8. The full set of inventory wire messages (server → client item arrival + client → server actions: pick up, drop, give, move-between-containers, split-stack, merge-stack). Cross-ref ACE (what the server sends/validates) + holtburger (what the client sends).
|
||||
9. Icon rendering: weenie `IconDataID` → texture (+ any underlay/overlay/highlight for ID'd vs unidentified, wielded, selected).
|
||||
|
||||
**Equipment / paperdoll (`gmPaperDollUI` / `gm3DItemsUI`):**
|
||||
10. The equip/wield slots — the coverage/location enum (which slots exist, their screen positions on the doll).
|
||||
11. The wield/unwield wire messages (equip an item, the server's response, the resulting `ObjDesc` change on the character model).
|
||||
12. The paperdoll rendering — is it the 2D doll image + slot icons, or a 3D character viewport (`gm3DItemsUI`)? How does it assemble the equipped-character appearance (cross-ref ACViewer's ObjDesc/CreaturePalette + WorldBuilder for the model)?
|
||||
|
||||
---
|
||||
|
||||
## 4. References (the hierarchy — cross-reference at least two per question)
|
||||
|
||||
Per CLAUDE.md's reference table:
|
||||
- **Named retail decomp** (`docs/research/named-retail/acclient_2013_pseudo_c.txt` + `acclient.h` + `symbols.json`) — **primary oracle for the UI logic** (the `gm*UI` / `UIElement_UIItem` classes). Grep by `class::method`.
|
||||
- **holtburger** (`references/holtburger/`) — **primary oracle for client behavior + wire**: what a client sends/receives for inventory, equip, use-item, drag-drop. Look in `client/` + `session/`.
|
||||
- **ACE** (`references/ACE/Source/ACE.Server/`) — **server expectations**: the inventory/equip/move game-action handlers, what the server validates + broadcasts.
|
||||
- **WorldBuilder** + **ACViewer** — **icon + 3D-model rendering**: `IconDataID` → texture decode (ACViewer `TextureCache.IndexToColor` / WorldBuilder `TextureHelpers`); the equipped-character ObjDesc assembly (ACViewer `StaticObjectManager` / `CreaturePalette`).
|
||||
- **Chorizite.ACProtocol** (`references/Chorizite.ACProtocol/Types/*.cs`) — protocol field order for the inventory/equip messages.
|
||||
|
||||
**Existing acdream research/memory to read first (don't re-research what's done):**
|
||||
- `claude-memory/project_d2b_retail_ui.md` — the toolkit + the find-by-id controller pattern (START HERE).
|
||||
- `claude-memory/feedback_weenie_vs_static.md` — items are server weenies.
|
||||
- `claude-memory/project_interaction_pipeline.md` — the existing WorldPicker / Select / UseSelected interaction (B.4) — the use/pickup path partly exists.
|
||||
- `claude-memory/MEMORY.md` → the **wire-message catalog** research (`research/2026-06-04-wire-message-catalog.md`): 256 opcodes, what acdream parses vs stubs vs is-missing — the inventory/equip opcodes' parse status is in there.
|
||||
- `docs/research/2026-06-15-layoutdesc-format.md` — the `LayoutDesc`/`ElementDesc` format (for reading the panel layouts).
|
||||
- The `AcDream.Cli` layout-dump tools (`dump-vitals-layout`, `LayoutIndexDump`, `LayoutFixtureDump` from this session's Task 1) — for dumping any panel's `LayoutDesc`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Deliverable + approach
|
||||
|
||||
**Report-only research** — no implementation, no code changes (use the `/investigate` discipline, or just produce research docs). Output: one or more `docs/research/2026-06-NN-*-deep-dive.md` docs (mirroring the existing `2026-06-04-*-deep-dive.md` set), each with:
|
||||
- The retail anchors (class::method + decomp line; `LayoutDesc`/element-class ids).
|
||||
- The wire-message catalog for the panel (direction, trigger, field layout, ACE handler, acdream parse status).
|
||||
- The item/container model + the `UIElement_UIItem` port spec.
|
||||
- The drag-drop mechanics.
|
||||
- **A concrete "what the toolkit needs" list** — the new generic widgets (`UiItemSlot`/`UIElement_UIItem` port, `UiListBox`/item-grid, the window manager) + which `Type` they register at — so the *next* session can go straight to a brainstorm → spec → plan.
|
||||
- An end-of-doc `MEMORY.md` index line.
|
||||
|
||||
**Suggested approach:** this is broad (3 panels × decomp + 5 references + dat + wire). A **multi-agent research Workflow** fits well — e.g. one agent per panel for the class/LayoutDesc/wire scoping, plus one agent on the shared `UIElement_UIItem`/icon-rendering/drag-drop spine, then synthesize. (Ultracode authorizes this.) Or run the panels sequentially. Either way, finish with a synthesis that names the new toolkit widgets.
|
||||
|
||||
---
|
||||
|
||||
## 6. New-session prompt (paste this into a fresh session)
|
||||
|
||||
> Deep retail-faithful **research phase** (report-only, no code) for acdream's next three UI panels, building on the now-complete D.2b widget toolkit (merged to `main`). **Read the handoff first: `docs/research/2026-06-16-action-bar-inventory-equipment-handoff.md`**, then `claude-memory/project_d2b_retail_ui.md`.
|
||||
>
|
||||
> **Targets + confirmed retail entry points** (named decomp): action bar / quick slots = `gmToolbarUI` (element class `0x10000007`); inventory = `gmInventoryUI` (`0x10000023`) + `gmBackpackUI` (`0x10000022`); equipment/paperdoll = `gmPaperDollUI` (`0x10000024`) + `gm3DItemsUI` (`0x10000021`). Shared spine: `UIElement_UIItem` (`0x10000032`, the item-in-a-slot widget) + `UIElement_ItemList` (`0x10000031`). Items are server `ACCWeenieObject` weenies.
|
||||
>
|
||||
> **Produce** a `docs/research/` deep-dive per panel (+ a synthesis) answering the §3 research questions in the handoff — each with a retail anchor (class::method + decomp line / `LayoutDesc` + element-class id), the panel's wire-message catalog (cross-ref holtburger=client, ACE=server, Chorizite.ACProtocol=field order), the item/container model, the `UIElement_UIItem` port spec + icon rendering (cross-ref WorldBuilder/ACViewer for `IconDataID`→texture), and the drag-drop mechanics. **End with a concrete "new generic widgets the toolkit needs" list** (the item-slot widget, an item list/grid, the window manager) + the `Type` each registers at, so the following session can brainstorm → spec → plan the build. Use a multi-agent research Workflow (one agent per panel + one on the shared item-slot/icon/drag-drop spine) — Ultracode is authorized. Follow the mandatory grep-named→cross-ref→pseudocode workflow; do not write implementation code this phase.
|
||||
191
docs/research/2026-06-16-action-bar-toolbar-deep-dive.md
Normal file
191
docs/research/2026-06-16-action-bar-toolbar-deep-dive.md
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
# Action bar / quick slots (`gmToolbarUI`) — retail-faithful deep dive
|
||||
|
||||
**Date:** 2026-06-16
|
||||
**Panel:** action bar / shortcut bar — retail class `gmToolbarUI`, element class `0x10000007`, `LayoutDesc 0x21000016` (root element 300×122).
|
||||
**Scope:** handoff §3 Q1 (LayoutDesc/element map) + Q4 (shortcut slot model) + Q5 (wire + persistence) + Q6 (drag-drop / reorder). Report-only; no code written this phase.
|
||||
**Builds on:** the D.2b importer/widget toolkit (`src/AcDream.App/UI/` + `…/UI/Layout/`). The "spine" item-slot/icon doc referenced in the handoff prompt does **NOT exist** in this worktree (searched `**/*spine*`, `**/*item-slot*`, the named path — all NOT FOUND), so the `UIElement_UIItem` / `UIElement_ItemList` facts below are derived here directly from the decomp; a later synthesis should reconcile with the spine doc if it lands.
|
||||
|
||||
## 1. Summary + confidence legend
|
||||
|
||||
The retail toolbar is **one `gmToolbarUI` window** that contains **18 single-cell item slots** (two rows of 9: top `0x100001A7..AF`, bottom `0x100006B7..BF`), each slot a **`UIElement_ItemList` (element class `0x10000031`)** holding at most one **`UIElement_UIItem` (class `0x10000032`)**. A slot stores nothing but the item it currently holds; the persistent model is `ShortCutManager::shortCuts_[18]` (an array of `ShortCutData{ index_, objectID_, spellID_ }`) living on the `CPlayerModule`. Shortcuts are **server-persisted as a character option** — they arrive in the big `PlayerDescription` login message (the `CharacterOptionDataFlag::SHORTCUT` block, **already parsed by acdream**) and are mutated live by two C2S game actions: **`AddShortCut 0x019C`** and **`RemoveShortCut 0x019D`** (both already have outbound builders in acdream). Activation of a slot does **not** use a "use-shortcut" wire message — it routes through the ordinary **use-item** path (`ItemHolder::UseObject`), so it reuses acdream's existing B.4 interaction pipeline. Drag-from-inventory and drag-to-reorder are handled by `gmToolbarUI` being an `ItemListDragHandler` (multiple inheritance) whose `HandleDropRelease` resolves the target slot and calls `CreateShortcutToItem` / `AddShortcut` / `GetFirstEmptyShortcutToTheRightOf`.
|
||||
|
||||
The 2 Meters + 1 Scrollbar in the layout dump are **NOT** bar paging or extra vitals: they are the **selected-object Health & Mana meters** (`0x100001A1`/`0x100001A2`) and the **stack-size split slider** (`0x100001A4`), all inside the `m_pSelObjectField` sub-panel and **hidden by default** (`SetVisible(0)` in `PostInit`) — they appear only when you select an object / split a stack.
|
||||
|
||||
**Confidence legend** — **CONFIRMED** = quoted from named decomp or a reference file I opened; **LIKELY** = inferred from confirmed facts (source named); **UNVERIFIED** = educated guess, flagged.
|
||||
|
||||
## 2. LayoutDesc / element map (Q1) — CONFIRMED against `.layout-dumps/toolbar-0x21000016.txt`
|
||||
|
||||
`LayoutDesc 0x21000016` (Id 553648150). The dump's `Width=800 Height=600` is the LayoutDesc canvas; the **root element `0x10000191`** (ElementId 268435857, **Type `0x10000463` = the registered `gmToolbarUI` class type**) is **300×122** — that 300×122 matches the handoff's stated size and is the real window. The root's Type value `268435463 = 0x10000007`… correction: dump shows `Type = 268435463` which is `0x10000007` (the `gmToolbarUI` class id) — i.e. the root element registers as the panel class itself, exactly like `gmToolbarUI::GetUIElementType` returns `0x10000007` (decomp line 196707: `return 0x10000007;`). CONFIRMED.
|
||||
|
||||
### 2a. The 18 shortcut slots — element→slot-index map
|
||||
|
||||
`gmToolbarUI::InitShortcutArray` (decomp line 197051) wires the slots by walking `GetChildRecursive(this, <id>)` in order and `DynamicCast(0x10000031)` (= `UIElement_ItemList`), registering each with the drag handler and pushing into `m_shortcutSlots`. The push order **is** the slot index. The 18 ids extracted from the function body (decomp 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<Shortcut>` for the description.
|
||||
- **acdream already parses this**: `PlayerDescriptionParser.cs:345-356` reads `count` then `ShortcutEntry(Index, ObjectGuid, SpellId, Layer)` per entry, exposed on `Parsed.Shortcuts`.
|
||||
|
||||
Client-side restore: `gmToolbarUI::UpdateFromPlayerDesc` (decomp 198838) → `FlushShortcuts()`, gets the `CPlayerModule`'s `ShortCutManager`, then `for (i=0; i<0x12; i++) { objId = shortCuts_[i]->objectID_ (+8); if (objId) AddShortcut(this, objId, i, 0); }` (decomp 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`.
|
||||
416
docs/research/2026-06-16-equipment-paperdoll-deep-dive.md
Normal file
416
docs/research/2026-06-16-equipment-paperdoll-deep-dive.md
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
# Equipment / Paperdoll panel — retail-faithful deep-dive
|
||||
|
||||
**Date:** 2026-06-16
|
||||
**Scope:** D.2b "core panels" research phase, the equipment/paperdoll target from
|
||||
`docs/research/2026-06-16-action-bar-inventory-equipment-handoff.md` §3 Q1 + Q10/Q11/Q12.
|
||||
**Status:** REPORT-ONLY. No code changed. The deliverable is this doc.
|
||||
**Panels:** `gmPaperDollUI` (element class `0x10000024`, LayoutDesc `0x21000024`) and
|
||||
`gm3DItemsUI` (element class `0x10000021`, LayoutDesc `0x21000021`).
|
||||
|
||||
## 1. Summary + confidence legend
|
||||
|
||||
The retail **paperdoll** (`gmPaperDollUI`) is a **3D character viewport plus ~25
|
||||
single-cell equip slots**, NOT a 2D doll image. The window's element `0x100001D5`
|
||||
(Type `13` = `UIElement_Viewport`) hosts a live `CreatureMode` mini-scene; the
|
||||
character's `CPhysicsObj` is cloned from the player and re-dressed via the SAME
|
||||
ObjDesc machinery the in-world renderer uses (`DoObjDescChangesFromDefault`). Every
|
||||
equip slot is a **single-cell `UIElement_ItemList` (class `0x10000031`)**, one per
|
||||
`EquipMask` location, mapped element-id → coverage-mask by
|
||||
`gmPaperDollUI::GetLocationInfoFromElementID`. Equipping is the
|
||||
`GetAndWieldItem` game action (opcode `0x001A`, `item_guid + EquipMask`); the
|
||||
server's visible reply is `ObjDescEvent` (`0xF625`) which triggers
|
||||
`RedressCreature`. **acdream already parses `ObjDescEvent` (0xF625) and the full
|
||||
ObjDesc/ModelData block, and already has a complete per-instance animated-character
|
||||
render path** (`EntitySpawnAdapter` → `AnimatedEntityState` with palette/part/hidden-
|
||||
part overrides). The paperdoll viewport can REUSE that path — the gap is a
|
||||
**`UiViewport` (Type 0xD) widget** that renders a single entity into a UI rect (a
|
||||
scissored mini 3D pass), an **equip-slot variant of the item-slot widget**
|
||||
(`UIElement_ItemList` 0x10000031, single cell), and the **window manager**.
|
||||
`gm3DItemsUI` (0x21000021) is a SEPARATE "Contents of Backpack" pane (an
|
||||
`UIElement_ItemList` + a text label + a scrollbar), NOT the doll — it does not host
|
||||
a viewport.
|
||||
|
||||
`gm3DItemsUI` is misnamed for our purposes: despite "3DItems", its `PostInit` wires
|
||||
a `m_itemList` (`UIElement_ItemList`) and a `m_contentsText` and sets the text to
|
||||
"Contents of Backpack". It is an inventory contents list, addressed by the inventory
|
||||
deep-dive; included here only because the handoff paired it with the paperdoll.
|
||||
|
||||
**Confidence legend:**
|
||||
- **CONFIRMED** — quoted from a source I opened (decomp line / file:line).
|
||||
- **LIKELY** — inferred from confirmed facts; the inference is named.
|
||||
- **UNVERIFIED** — educated guess; flagged loudly.
|
||||
|
||||
**Note on a missing input:** the handoff promised a "spine agent" doc at
|
||||
`docs/research/2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md` and the
|
||||
START-HERE memory `claude-memory/project_d2b_retail_ui.md`. **Both are NOT FOUND in
|
||||
this worktree** (`Glob **/project_d2b_retail_ui.md` and `**/*spine*.md` returned
|
||||
nothing). I therefore re-derived the icon/item-model claims I needed from primary
|
||||
sources (decomp + acclient.h + ACE + ACViewer + acdream source) rather than citing a
|
||||
doc I could not open. Where this overlaps the spine's scope (icon decode, the
|
||||
`UIElement_UIItem` widget, container model) I keep it terse and defer to the spine
|
||||
doc once it lands.
|
||||
|
||||
## 2. LayoutDesc / element map
|
||||
|
||||
### 2a. Paperdoll `gmPaperDollUI` 0x10000024 → LayoutDesc 0x21000024 (224×214)
|
||||
|
||||
**CONFIRMED** registration: `gmPaperDollUI::Register` (decomp line 174445):
|
||||
`UIElement::RegisterElementClass(0x10000024, gmPaperDollUI::Create);`. Pre-dump
|
||||
`.layout-dumps/paperdoll-0x21000024.txt` root `0x100001D4` is 224×214, Type
|
||||
`268435492 = 0x10000024` (the gmPaperDollUI class). **CONFIRMED.**
|
||||
|
||||
Construction chain: `gmPaperDollUI::gmPaperDollUI` (line 174228) calls
|
||||
`UIElement_Field::UIElement_Field(this, ...)` — i.e. the paperdoll IS-A Field
|
||||
subclass (matters for drag-drop: it inherits Field's drop hooks). The slot/viewport
|
||||
wiring happens in the init routine that calls `GetChildRecursive` per id
|
||||
(lines 175480-175548) — the analog of a `PostInit`. **CONFIRMED.**
|
||||
|
||||
Key elements in 0x21000024 (from the pre-dump + the init routine):
|
||||
|
||||
| Element id | dump Type | Resolves to | Role | Anchor (cite) |
|
||||
|---|---|---|---|---|
|
||||
| `0x100001D4` | 0x10000024 | gmPaperDollUI (root) | window | dump:13 |
|
||||
| `0x100001D5` | **13** | `UIElement_Viewport` (0xD) | **the 3D character doll** | dump:125; `m_pPaperDoll = GetChildRecursive(this,0x100001d5)->DynamicCast(0xd)` line 175509-175517 |
|
||||
| `0x100001D6` | 0 → base 0x100002BF/0x21000080 | `m_paperDollDragMask` | doll click/drag mask region (100×214) | dump:157; line 175538 |
|
||||
| `0x1000046D` | 0 → base | `m_paperDollDragOverlay` | drag overlay sprite (32×32) | dump:173; line 175539 |
|
||||
| `0x10000595` | 0 → ItemList | `m_sigilOneItem` (SigilOne 0x10000000) | aetheria sigil slot, hidden by default | line 175540-175542 |
|
||||
| `0x10000596` | 0 → ItemList | `m_sigilTwoItem` (SigilTwo 0x20000000) | sigil slot | line 175543-175545 |
|
||||
| `0x10000597` | 0 → ItemList | `m_sigilThreeItem` (SigilThree 0x40000000) | sigil slot | line 175546-175548 |
|
||||
| `0x100005BE` | 0 → Button base 0x21000044 | a `UIElement_Button` | the close/expand button (120×14) | dump:349; line 175549 |
|
||||
| ~25 more `0x1000xxxx` ids | **0** → base `0x100001E4` | single-cell `UIElement_ItemList` (0x10000031) | the equip slots (§3) | dump:29-476 |
|
||||
|
||||
The shared equip-slot base chain (**CONFIRMED**):
|
||||
- Each slot element has `Type = 0`, `BaseElement = 268435940 = 0x100001E4`,
|
||||
`BaseLayoutId = 553648164 = 0x21000024` (dump e.g. lines 33,49,65…).
|
||||
- Element `0x100001E4` (dump:477) has `Type = 0`, `BaseElement = 268436281 =
|
||||
0x10000339`, `BaseLayoutId = 553648189 = 0x2100003D`.
|
||||
- `0x2100003D` root element `0x10000339` (`.layout-dumps/itemlist-0x2100003D.txt:16`)
|
||||
has `Type = 268435505 = 0x10000031` = `UIElement_ItemList`, 32×32.
|
||||
⇒ **every paperdoll equip slot resolves (via `ElementReader.Merge` zero-wins-base
|
||||
Type resolution) to `UIElement_ItemList` 0x10000031, a single 32×32 cell.**
|
||||
|
||||
The init routine confirms each is cast to ItemList and registered as a drag target,
|
||||
e.g. (line 175485-175496):
|
||||
```
|
||||
eax_66 = GetChildRecursive(this, 0x100005b2); // LowerLegArmor slot
|
||||
eax_67 = eax_66->vtable->DynamicCast(0x10000031); // → UIElement_ItemList
|
||||
this->m_lowerLegSlot = eax_67;
|
||||
UIElement_ItemList::RegisterItemListDragHandler(eax_67, &this->vtable);
|
||||
this->m_lowerLegSlot->vtable->SetVisible(0); // hidden until an item lands
|
||||
```
|
||||
**CONFIRMED.** Slots default invisible and are shown only when occupied (the empty
|
||||
slot shows the doll body behind it; an occupied slot shows the item icon).
|
||||
|
||||
### 2b. gm3DItemsUI 0x10000021 → LayoutDesc 0x21000021 (234×120) — NOT the doll
|
||||
|
||||
**CONFIRMED** registration: `gm3DItemsUI::Register` (line 176723):
|
||||
`UIElement::RegisterElementClass(0x10000021, gm3DItemsUI::Create);`.
|
||||
`gm3DItemsUI::PostInit` (line 176728-176745):
|
||||
```
|
||||
this->m_contentsText = UIElement::GetChildRecursive(this, 0x100001c5);
|
||||
eax_1 = UIElement::GetChildRecursive(this, 0x100001c6);
|
||||
this->m_itemList = eax_1->vtable->DynamicCast(0x10000031); // UIElement_ItemList
|
||||
... UIElement_Text::SetText(this->m_contentsText, u"Contents of Backpack");
|
||||
```
|
||||
Pre-dump `.layout-dumps/items3d-0x21000021.txt`: root `0x100001C4` (234×120, Type
|
||||
`268435489 = 0x10000021`), child `0x100001C5` (text, base 0x10000436/0x21000077),
|
||||
child `0x100001C6` (the ItemList grid, base 0x100002B9/0x2100003D — same ItemList
|
||||
base as the slots), child `0x100001C7` (a scrollbar-shaped 16×96, base
|
||||
0x100002C7/0x2100003E). **No Viewport element.** ⇒ gm3DItemsUI is a scrollable
|
||||
**item-contents list**, not a 3D doll. **CONFIRMED.** (The "3D" in the name is
|
||||
historical; it has no `UIElement_Viewport` and no `CreatureMode`.)
|
||||
|
||||
## 3. Equip-slot model + the coverage / location enum
|
||||
|
||||
### 3a. The element-id → EquipMask mapping (`GetLocationInfoFromElementID`)
|
||||
|
||||
`gmPaperDollUI::GetLocationInfoFromElementID(elementId, out uint mask, out UI_SLOT_SIDE side)`
|
||||
(decomp line 173620) is a giant switch. It is the SSOT for which slot is which. The
|
||||
mask values are exactly ACE's `EquipMask` (`ACE/Source/ACE.Entity/Enum/EquipMask.cs`).
|
||||
**CONFIRMED** — full table below (decomp line / mask / EquipMask name / SLOT_SIDE):
|
||||
|
||||
| Element id | mask (hex) | EquipMask name | SLOT_SIDE | decomp line |
|
||||
|---|---|---|---|---|
|
||||
| `0x100005AB` | `0x1` | HeadWear | NULL | 173723 |
|
||||
| `0x100001E2` | `0x2` | ChestWear | NULL | 173688 |
|
||||
| `0x100001E3` | `0x40` | UpperLegWear | NULL | 173694 |
|
||||
| `0x100005B0` | `0x20` | HandWear | NULL | 173753 |
|
||||
| `0x100005B3` | `0x100` | FootWear | NULL | 173771 |
|
||||
| `0x100005AC` | `0x200` | ChestArmor | NULL | 173729 |
|
||||
| `0x100005AD` | `0x400` | AbdomenArmor | NULL | 173735 |
|
||||
| `0x100005AE` | `0x800` | UpperArmArmor | NULL | 173741 |
|
||||
| `0x100005AF` | `0x1000` | LowerArmArmor | NULL | 173747 |
|
||||
| `0x100005B1` | `0x2000` | UpperLegArmor | NULL | 173759 |
|
||||
| `0x100005B2` | `0x4000` | LowerLegArmor | NULL | 173765 |
|
||||
| `0x100001DA` | `0x8000` | NeckWear | NULL | 173640 |
|
||||
| `0x100001DB` | `0x10000` | WristWearLeft | LEFT | 173646 |
|
||||
| `0x100001DD` | `0x20000` | WristWearRight | RIGHT | 173658 |
|
||||
| `0x100001DC` | `0x40000` | FingerWearLeft | LEFT | 173652 |
|
||||
| `0x100001DE` | `0x80000` | FingerWearRight | RIGHT | 173664 |
|
||||
| `0x100001E1` | `0x200000` | Shield | NULL | 173682 |
|
||||
| `0x100001E0` | `0x800000` | MissileAmmo | NULL | 173676* |
|
||||
| `0x100001DF` | `0x3500000` | (weapon composite — see 3b) | NULL | 173670 |
|
||||
| `0x100005E9` | `0x8000000` | Cloak | NULL | 173777 |
|
||||
| `0x10000595` | `0x10000000` | SigilOne | NULL | 173705 |
|
||||
| `0x10000596` | `0x20000000` | SigilTwo | NULL | 173711 |
|
||||
| `0x10000597` | `0x40000000` | SigilThree | NULL | 173717 |
|
||||
| `0x1000058E` | `0x4000000` | TrinketOne | NULL | 173630 |
|
||||
|
||||
\* **`0x100001E0`** — the decomp pseudo-C shows `*arg3 = "activation type (%s)…"`
|
||||
(a string-pointer artifact where the Binary Ninja lifter lost the immediate). The
|
||||
preceding/following cases are `0x200000` (Shield) and `0x200000`/`0x40`, and the only
|
||||
remaining ready-slot mask not otherwise assigned in this switch is `MissileAmmo
|
||||
(0x00800000)`. So **`0x100001E0` = MissileAmmo `0x800000` (LIKELY** — inferred from
|
||||
the EquipMask gap + neighbors; the literal value is corrupted in the decomp).
|
||||
|
||||
`UI_SLOT_SIDE` (CONFIRMED `acclient.h:4546`): `SLOT_SIDE_NULL=0, SLOT_SIDE_LEFT=1,
|
||||
SLOT_SIDE_RIGHT=2`. SIDE distinguishes the paired jewelry slots (left/right
|
||||
wrist + finger) that share the same wear concept but different physical sides.
|
||||
|
||||
### 3b. The weapon composite slot `0x3500000`
|
||||
|
||||
`0x100001DF → 0x3500000` = `MeleeWeapon(0x100000) | MissileWeapon(0x400000) |
|
||||
TwoHanded(0x2000000) | Held(0x1000000)` (= `0x3500000`). **CONFIRMED** by bit
|
||||
decomposition against EquipMask.cs. This is the single "weapon hand" doll slot that
|
||||
accepts any wieldable weapon. `OnItemListDragOver` has a special case at line 174302:
|
||||
`if (ecx_3 == 0x200000 && (eax_3 & 0x100000) != 0) eax_3 |= ecx_3;` — i.e. a
|
||||
melee-capable item may also drop into the Shield(0x200000) slot test. **CONFIRMED.**
|
||||
|
||||
### 3c. How the client knows what is equipped — `GetUpperInvObj(mask)`
|
||||
|
||||
`gmPaperDollUI::GetUpperInvObj(uint coverageMask)` (line 174565) is how the doll
|
||||
finds the item currently in a slot:
|
||||
```
|
||||
eax = ClientObjMaintSystem::GetWeenieObject(player_id);
|
||||
eax_3 = ACCWeenieObject::GetInvPlacementList(eax); // PackableList<InventoryPlacement>
|
||||
for (i = eax_3->head; i; i = i->next) {
|
||||
if (arg2 & i->data.loc_) // coverageMask & placement.loc_
|
||||
eax_5 = InventoryPlacement::DetermineHigherPriority(...);
|
||||
}
|
||||
return iid; // the equipped item's guid
|
||||
```
|
||||
`InventoryPlacement` (**CONFIRMED** `acclient.h:33178`):
|
||||
```cpp
|
||||
struct InventoryPlacement : PackObj { uint iid_; uint loc_; uint priority_; };
|
||||
```
|
||||
So the player weenie carries a **`PackableList<InventoryPlacement>`** where each
|
||||
node is `{itemGuid, locationMask (EquipMask), priority}`. `loc_` is the EquipMask
|
||||
slot; `priority_` resolves overlap (e.g. armor over clothing on the same body part —
|
||||
this is `CoverageMask` priority, `ACE/Source/ACE.Entity/Enum/CoverageMask.cs`).
|
||||
**CONFIRMED.** The paperdoll reads this list to populate each slot's icon and to
|
||||
drive part-selection lighting (`GetSelectionMaskFromObject`, line 174762, maps an
|
||||
item guid back to which doll body parts to highlight, via the same masks).
|
||||
|
||||
**Cross-ref ACE:** `EquipMask` (loc) and `CoverageMask` (priority) are documented in
|
||||
ACE as "sent as loc / in the priority field of the equipped-items list portion of the
|
||||
player description event F7B0-0013" (`EquipMask.cs:5-6`, `CoverageMask.cs:6-7`).
|
||||
**CONFIRMED** — this is the same `InventoryPlacement {iid, loc, priority}` triple the
|
||||
client stores, populated from PlayerDescription's equipped section.
|
||||
|
||||
**acdream parse status of the placement list:** PARTIAL. `PlayerDescriptionParser`
|
||||
(0x0013) "walks all sections through enchantments; the trailing options / inventory /
|
||||
**equipped** sections are partial" (`PlayerDescriptionParser.cs:70-77`). So acdream
|
||||
does NOT yet surface the equipped `InventoryPlacement` list. The per-item equip
|
||||
*state* is, however, available from `CreateObject`/`ObjDescEvent` ModelData
|
||||
(palette/part swaps already applied to the model). **CONFIRMED** (parser comment).
|
||||
|
||||
## 4. Wield / unwield wire + the ObjDesc change
|
||||
|
||||
### 4a. Wire table
|
||||
|
||||
| Opcode | Name | Dir | Trigger | ACE handler | Chorizite type | acdream parse status |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `0x001A` (GameAction) | GetAndWieldItem | C→S | drop an item onto an equip slot / doll (auto-wield) | `GameActionGetAndWieldItem.Handle` (`Actions/GameActionGetAndWieldItem.cs:7-14`) → `Player.HandleActionGetAndWieldItem(itemGuid, EquipMask)` | `Inventory_GetAndWieldItem` (`C2S/Actions/Inventory_GetAndWieldItem.generated.cs:14-42`: `uint ObjectId; EquipMask Slot`) | **MISSING** (no sender in acdream; `Grep GetAndWieldItem\|0x001A src` finds only the UI font-property 0x1A, unrelated) |
|
||||
| `0x0019` (GameAction) | PutItemInContainer / move-to-pack (un-wield) | C→S | drag a wielded item back into a pack | ACE `GameActionPutItemInContainer` | `Inventory_PutItemInContainer*` | MISSING (inventory deep-dive scope) |
|
||||
| `0xF625` | ObjDescEvent | S→C | server applies/removes the wielded item → appearance change | `GameMessageObjDescEvent` ctor → `worldObject.SerializeUpdateModelData` (`Messages/GameMessageObjDescEvent.cs:10-17`) | (ModelData block) | **PARSED** — `ObjDescEvent.cs:33-73` (opcode `0xF625`, `CreateObject.ReadModelData`) |
|
||||
| `0xF745`/`0x0024` (CreateObject) | CreateObject | S→C | the wielded item object itself arrives | ACE creation message | `Item_CreateObject` | PARSED — `CreateObject.cs` |
|
||||
| `0xF7B0`/`0x0013` (GameEvent) | PlayerDescription (equipped list) | S→C | full state incl. `InventoryPlacement` equipped section | `GameEventPlayerDescription.WriteEventBody` | `Login_PlayerDescription` | **PARTIAL** — `PlayerDescriptionParser.cs` (equipped section not surfaced) |
|
||||
|
||||
Wire payload of `GetAndWieldItem` (**CONFIRMED** both refs agree):
|
||||
- ACE reads `uint itemGuid; (EquipMask)int32 location` (`GameActionGetAndWieldItem.cs:10-11`).
|
||||
- Chorizite writes `uint ObjectId; (uint)EquipMask Slot` (`.generated.cs:38-41`).
|
||||
- holtburger sends `GetAndWieldItem { item_guid, equip_mask }`
|
||||
(`holtburger-core/src/client/commands.rs:808-814`):
|
||||
```rust
|
||||
self.send_game_action(GameAction::GetAndWieldItem(Box::new(
|
||||
GetAndWieldItemActionData { item_guid: item, equip_mask: target_mask })))
|
||||
```
|
||||
with `target_mask` resolved by `resolve_and_clear_slots(item, slot)` (line 799) —
|
||||
i.e. the client picks the EquipMask for the target slot, exactly like the doll's
|
||||
`GetLocationInfoFromElementID`. **CONFIRMED.**
|
||||
|
||||
`GameActionType.GetAndWieldItem = 0x001A` (**CONFIRMED**
|
||||
`ACE/Source/ACE.Server/Network/GameAction/GameActionType.cs:14`).
|
||||
|
||||
### 4b. The ObjDesc change on the model (`ObjDescEvent` → `RedressCreature`)
|
||||
|
||||
Server side: equipping changes the creature's `ObjDesc` (clothing base, sub-palettes,
|
||||
texture changes, anim-part swaps) and broadcasts `ObjDescEvent (0xF625)` carrying the
|
||||
FULL new appearance (ACE comment: "It contains the entire description of what they're
|
||||
wearing", `GameMessageObjDescEvent.cs:6-9`).
|
||||
|
||||
Client side: `gmPaperDollUI::RecvNotice_PlayerObjDescChanged` (line 174324) tail-calls
|
||||
`gmPaperDollUI::RedressCreature` (line 173990). **CONFIRMED.** RedressCreature:
|
||||
```
|
||||
if (m_pInventoryObject == 0 && smartbox->player != 0) { // first time:
|
||||
eax_5 = CPhysicsObj::makeObject(GetPhysicsObject(player_id)); // clone player obj
|
||||
this->m_pInventoryObject = eax_5;
|
||||
CPhysicsObj::set_heading(eax_5, 191.367905f, 1); // face ~191° (toward viewer)
|
||||
CPhysicsObj::set_sequence_animation(m_pInventoryObject, m_didAnimation.id, 1, 1, 0);
|
||||
CreatureMode::AddObject(&m_pPaperDoll->creature_mode_objects, m_pInventoryObject);
|
||||
}
|
||||
visualDesc = SmartBox::get_player_visualdesc(smartbox);
|
||||
CPhysicsObj::DoObjDescChangesFromDefault(this->m_pInventoryObject, visualDesc); // re-dress
|
||||
```
|
||||
**CONFIRMED** (lines 173997-174012). So the doll is a CLONE of the player's
|
||||
`CPhysicsObj`, and re-dressing is `CPhysicsObj::DoObjDescChangesFromDefault` applied
|
||||
to the cloned object using the player's current `VisualDesc` — **the same ObjDesc
|
||||
apply used for in-world creatures**. The ObjDesc fields (ACViewer
|
||||
`Entity/ObjDesc.cs:18-54`): `PaletteID`, `SubPalettes`, `TextureChanges`,
|
||||
`AnimPartChanges` — **all four already parsed by acdream's `CreateObject.ReadModelData`
|
||||
/ `ObjDescEvent`** (`CreateObject.cs:652-679`: subPalette/textureChange/animPartChange
|
||||
counts + entries). **CONFIRMED.**
|
||||
|
||||
## 5. Paperdoll 3D rendering + reuse analysis
|
||||
|
||||
### 5a. It is a 3D viewport, not a 2D image
|
||||
|
||||
**CONFIRMED.** The doll is `UIElement_Viewport` (Type `0xD`), element `0x100001D5`.
|
||||
`UIElement_Viewport::Create` (line 119029-119037) allocates the element + a
|
||||
`CreatureMode` sub-object at `+0x5f0`; `PostInit` calls
|
||||
`CreatureMode::InitializeScene` (line 119084). `SetCamera` forwards to
|
||||
`CreatureMode::SetCameraPosition/Direction` (line 119089-119094). `Register` ⇒
|
||||
`RegisterElementClass(0xd, …)` (line 119126). So a Viewport is a mini 3D scene
|
||||
embedded in a UI rect, with its own camera, lights, and an object list.
|
||||
|
||||
The paperdoll init (line 175517-175535) does, once:
|
||||
```
|
||||
m_pPaperDoll = GetChildRecursive(this, 0x100001d5)->DynamicCast(0xd); // the viewport
|
||||
UIElement_Viewport::SetCamera(m_pPaperDoll, &dir, &pos); // pos/dir vec3s
|
||||
UIElement_Viewport::SetLight(m_pPaperDoll, DISTANT_LIGHT, 2.0, &dir); // one distant light
|
||||
CreatureMode::UseSharpMode(&m_pPaperDoll->creature_mode_objects); // sharper mip bias
|
||||
gmPaperDollUI::RedressCreature(this); // build + dress the doll
|
||||
```
|
||||
**CONFIRMED.** `UpdateForRace` (line 174129) re-points the camera per body-type
|
||||
(case 6/7/8/9/0xC/0xD = the playable races/genders) and swaps `m_didAnimation` (the
|
||||
idle pose DID) via `DBObj::GetDIDByEnum`. **CONFIRMED.**
|
||||
|
||||
### 5b. The viewport render loop (`CreatureMode::Render`)
|
||||
|
||||
`CreatureMode::Render` (line 91665) is the per-frame doll draw. Walk-through
|
||||
(**CONFIRMED** lines 91665-91776):
|
||||
1. Enter "creature mode" (disables world LOD degrade so the doll is full detail).
|
||||
2. For each object in `creature_mode_objects`: `CPhysicsObj::update_position` (advance
|
||||
the idle animation).
|
||||
3. Set ambient color, sunlight, FOV (`Render::SetFOVRad`), push a frame.
|
||||
4. `Render::update_viewpoint(&creature_view_frame)`, `set_default_view()`.
|
||||
5. `RenderDevice::DrawObjCellForDummies(creature_cell)` — draw the object's private
|
||||
cell, then `D3DPolyRender::FlushAlphaList`.
|
||||
|
||||
i.e. the doll lives in its own tiny `creature_cell`, lit by one distant light, drawn
|
||||
with a dedicated camera into the viewport rect. `CreatureMode::AddObject` (line 94374)
|
||||
adds the cloned `CPhysicsObj` to that cell:
|
||||
`CPhysicsObj::AddObjectToSingleCell(obj, creature_cell); SetPlacementFrame(obj,0,1);`.
|
||||
**CONFIRMED.**
|
||||
|
||||
### 5c. Can acdream REUSE its existing character render path? — YES
|
||||
|
||||
**acdream already renders animated, equipped characters in-world.** The per-instance
|
||||
path is `EntitySpawnAdapter` (`src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs`):
|
||||
- `OnCreate(WorldEntity)` builds an `AnimatedEntityState(sequencer)` and applies
|
||||
`entity.HiddenPartsMask`, every `entity.PartOverrides` (`SetPartOverride(partIndex,
|
||||
gfxObjId)` — weapons/clothing/helmets that replace the Setup default), and
|
||||
pre-warms per-instance palette/texture decode via
|
||||
`GetOrUploadWithPaletteOverride(surfaceId, texOverride, paletteOverride)`.
|
||||
**CONFIRMED** `EntitySpawnAdapter.cs:100-168`.
|
||||
- `WorldEntity` carries `SourceGfxObjOrSetupId`, `MeshRefs`, `PaletteOverride`,
|
||||
`PartOverrides` (`record struct PartOverride(byte PartIndex, uint GfxObjId)`), and
|
||||
`HiddenPartsMask`. **CONFIRMED** `WorldEntity.cs:14,28,37,97,104,213`.
|
||||
|
||||
This is the EXACT data a re-dress produces: ObjDesc → base palette + sub-palettes
|
||||
(`PaletteOverride`), texture changes (`SurfaceOverrides`), anim-part swaps
|
||||
(`PartOverrides`). acdream already turns an `ObjDescEvent`/`CreateObject` ModelData
|
||||
into these fields. **So the paperdoll doll = "take the local player's WorldEntity (or
|
||||
a clone of it), feed it through the existing animated-character pipeline, and draw it
|
||||
with a fixed camera + one distant light into a UI rect."** This is the C# analog of
|
||||
`makeObject(player) + DoObjDescChangesFromDefault + CreatureMode::Render`.
|
||||
|
||||
### 5d. What a `UiViewport` (Type 0xD) widget needs to host the 3D render
|
||||
|
||||
The toolkit's `UiRenderContext` is a **2D** sprite/text submission bucket (see
|
||||
`UiElement.OnDraw(UiRenderContext)`). A 3D model render cannot go through it. A
|
||||
`UiViewport` widget therefore needs (LIKELY design — flagged):
|
||||
1. **A render-into-rect hook.** The widget's screen rect (`ScreenPosition` +
|
||||
Width/Height) defines a GL scissor + viewport. A 3D pass renders the single entity
|
||||
there, AFTER the world pass and BEFORE/INTERLEAVED with the 2D UI pass. The cleanest
|
||||
seam is a dedicated overlay callback the `UiHost`/`GameWindow` invokes for any
|
||||
`UiViewport` present, NOT a draw inside `OnDraw` (which only has a 2D context).
|
||||
**UNVERIFIED** — the exact integration point (a new `IUiViewportRenderer` Core
|
||||
interface implemented in App, per Code-Structure Rule 2) is a design call for the
|
||||
brainstorm/spec phase, not yet decided.
|
||||
2. **A private mini-scene** mirroring `CreatureMode`: one entity (`AnimatedEntityState`
|
||||
for the player clone), a fixed camera (position/direction vec3 like
|
||||
`SetCamera`, e.g. the retail values `dir.z=0.12, pos=(~-2.4, ~0.88)` floats from
|
||||
`UpdateForRace` — see the `0x3df5c28f / 0xc019999a / 0x3f6147ae` immediates at line
|
||||
175524-175526, which are little-endian floats ≈ 0.12, −2.4, 0.88; **LIKELY** —
|
||||
I read the hex but did not byte-convert each), one distant light, and an idle
|
||||
animation playing on the sequencer.
|
||||
3. **A heading toward the viewer** (`set_heading(191.37°)`, line 174001) and optional
|
||||
click-drag rotation (the doll spins under the mouse — that's
|
||||
`m_paperDollDragMask`/`CreateClickMap`, line 174636; **part-selection lighting** for
|
||||
"which armor piece is this?" highlight uses `ApplyPartSelectionLighting`, line
|
||||
174034, but that is a polish feature, not MVP).
|
||||
4. **Reuse `EntitySpawnAdapter`'s state** — feed it the player's `WorldEntity` so the
|
||||
doll automatically reflects equip changes when `ObjDescEvent` updates the player's
|
||||
ModelData. The re-dress is then "rebuild the player WorldEntity's PartOverrides/
|
||||
PaletteOverride from the new ObjDesc and refresh the viewport's entity state" — the
|
||||
C# analog of `RedressCreature`.
|
||||
|
||||
This is the single biggest new piece. The 3D machinery exists; the work is the
|
||||
**UI↔3D bridge** (a scissored single-entity pass driven by a UI rect).
|
||||
|
||||
## 6. New toolkit widgets this introduces
|
||||
|
||||
| Widget (proposed) | dat Type it registers at | leaf vs container | Purpose |
|
||||
|---|---|---|---|
|
||||
| **`UiViewport`** | **0xD** (`UIElement_Viewport`, reg line 119126) | **leaf** (`ConsumesDatChildren => true`) | Hosts a single 3D entity (the paperdoll character clone) rendered into the widget's screen rect via a scissored mini 3D pass. Owns a fixed camera + one distant light + an `AnimatedEntityState`; reuses `EntitySpawnAdapter`/`AnimatedEntityState` for the model. Needs a new render-into-rect seam (a Core `IUiViewportRenderer` interface implemented in App). **The biggest new piece.** |
|
||||
| **`UiItemSlot`** (equip-slot variant of the shared item-slot) | **0x10000031** (`UIElement_ItemList`, single 32×32 cell) | **leaf** (`ConsumesDatChildren => true`) | One equip slot. Renders the equipped item's icon (from the weenie `IconDataID`), is a drag-drop target keyed to its `EquipMask` (from `GetLocationInfoFromElementID`), shows/hides per occupancy. NOTE: this is the single-cell case of the shared `UIElement_UIItem`/`UIElement_ItemList` spine widget — the equipment panel is a fixed grid of ~25 of these, one per EquipMask, NOT a scrollable list. **Defer the shared icon/drag mechanics to the spine doc** (`2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md`, NOT FOUND yet); this panel only adds the EquipMask binding + the fixed-position-per-slot layout. |
|
||||
| **Window manager** (shared, not paperdoll-specific) | n/a (uses Dragbar Type 2 / Resizebar Type 9 already present on chrome) | n/a | Open/close/z-order/persist for the paperdoll window. `UiElement.Draggable/Resizable` already exist; the manager wires them + persistence. Shared with inventory/toolbar — same item the handoff §2 calls "the other deferred Plan-2 piece". |
|
||||
|
||||
`gm3DItemsUI`'s pane reuses `UiItemSlot`/the spine `UiItemList` + a `UiScrollbar`
|
||||
(Type 0xB, already built) + a `UiText` (already built) — no NEW widget. It is an
|
||||
inventory-contents list (inventory deep-dive scope), not a doll.
|
||||
|
||||
## 7. Open questions / UNVERIFIED
|
||||
|
||||
- **`0x100001E0` = MissileAmmo `0x800000`** — LIKELY (the decomp immediate is
|
||||
corrupted to a string pointer at line 173676; inferred from the EquipMask gap +
|
||||
neighbors). Re-dump element `0x100001E0`'s position vs the ammo doll slot, or
|
||||
re-decompile `0x004a388a` in Ghidra to recover the real immediate, to confirm.
|
||||
- **The exact viewport camera/light immediates** (lines 175524-175526, 174144-174146)
|
||||
— I read the hex but did not byte-convert all of them to floats; the paperdoll
|
||||
brainstorm should decode `0x3df5c28f≈0.12`, `0xc019999a≈−2.4`, `0xc0400000=−3.0`,
|
||||
`0xc059999a≈−3.4`, `0x3f6147ae≈0.88`, `0x3f800000=1.0` precisely for a faithful
|
||||
framing. **UNVERIFIED.**
|
||||
- **The UI↔3D render seam** (how a UI rect drives a scissored single-entity 3D pass,
|
||||
and whether it draws after the world pass or as a UI overlay) — DESIGN-OPEN, to be
|
||||
settled in brainstorm. Code-Structure Rule 2 means the seam is a Core interface
|
||||
implemented in App. **UNVERIFIED.**
|
||||
- **acdream's PlayerDescription equipped section** is not surfaced
|
||||
(`PlayerDescriptionParser.cs:70-77`). To populate slot icons at login (vs only
|
||||
reacting to later `ObjDescEvent`s), the parser must be extended to read the
|
||||
`InventoryPlacement` equipped list. Filed as a dependency, not yet an issue.
|
||||
- **Whether the doll clones the player `WorldEntity` or builds a fresh one** — retail
|
||||
clones the player `CPhysicsObj` (`makeObject(GetPhysicsObject(player_id))`, line
|
||||
173999). acdream has no player `CPhysicsObj`-as-renderable today (the local player
|
||||
isn't a `WorldEntity` in the per-instance adapter — it's the camera). LIKELY the
|
||||
paperdoll builds a dedicated `WorldEntity` from the local player's
|
||||
Setup+ObjDesc and feeds it to a private `EntitySpawnAdapter`-like host. **UNVERIFIED.**
|
||||
- **`gm3DItemsUI` true role** — its `m_itemList` + "Contents of Backpack" text is
|
||||
CONFIRMED, but whether retail ever shows 3D item models in it (the name suggests a
|
||||
historical 3D-preview) — NOT FOUND any Viewport in its layout; treated as a 2D
|
||||
contents list. If a 3D item preview surfaces elsewhere, revisit.
|
||||
|
||||
## 8. MEMORY.md index line
|
||||
|
||||
- [Equipment/Paperdoll panel deep-dive](research/2026-06-16-equipment-paperdoll-deep-dive.md) — gmPaperDollUI 0x10000024/LayoutDesc 0x21000024: doll = UIElement_Viewport (Type 0xD, elem 0x100001D5) hosting a CreatureMode clone re-dressed via DoObjDescChangesFromDefault; ~25 equip slots are single-cell UIElement_ItemList (0x10000031) mapped element-id→EquipMask by GetLocationInfoFromElementID; wield = GetAndWieldItem (0x001A, item+EquipMask, acdream-MISSING), appearance reply = ObjDescEvent 0xF625 (acdream-PARSED) → RedressCreature; acdream's EntitySpawnAdapter/AnimatedEntityState char path is reusable; new widgets = UiViewport (0xD, the UI↔3D bridge), UiItemSlot (0x10000031), window manager. gm3DItemsUI 0x21000021 is a "Contents of Backpack" list, NOT the doll.
|
||||
391
docs/research/2026-06-16-inventory-deep-dive.md
Normal file
391
docs/research/2026-06-16-inventory-deep-dive.md
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
# Inventory panel deep-dive — `gmInventoryUI` + `gmBackpackUI`
|
||||
|
||||
**Date:** 2026-06-16
|
||||
**Phase:** D.2b core-panels research (report-only). Sibling of the action-bar
|
||||
and paperdoll deep-dives; builds on the `UIElement_UIItem` / icon / drag-drop
|
||||
**spine** research (see §1 note). Answers handoff §3 questions **Q1** (this
|
||||
panel's `LayoutDesc`), **Q7** (window layout), **Q8** (full inventory
|
||||
wire-message set), **Q9** (icon rendering states).
|
||||
|
||||
## 1. Summary + confidence legend
|
||||
|
||||
The retail inventory window is two cooperating dat windows. **`gmInventoryUI`
|
||||
(class `0x10000023`, `LayoutDesc 0x21000023`, 300×362)** is the OUTER frame: a
|
||||
title bar, a chrome border, and three slots that host CHILD windows —
|
||||
`gmPaperDollUI` (the equipped-gear doll), `gmBackpackUI` (the pack list), and
|
||||
`gm3DItemsUI` (the 3D rotating-character viewport). **`gmBackpackUI` (class
|
||||
`0x10000022`, `LayoutDesc 0x21000022`, 61×339)** is the left strip: a burden
|
||||
**Meter** (Type 7) + a `%`-burden text label, the main-pack item grid
|
||||
(`UIElement_ItemList` `0x10000031`), and the side-pack tab column (a second
|
||||
`UIElement_ItemList`). Every cell in those grids is a `UIElement_UIItem`
|
||||
(class `0x10000032`) — the shared spine widget. Items are server-spawned
|
||||
**`ACCWeenieObject`** weenies; the client learns container contents from
|
||||
`CreateObject (0xF745)` + `PlayerDescription (0x0013)` at login and from the
|
||||
`0xF7B0` GameEvent family (`ViewContents 0x0196`, `InventoryPutObjInContainer
|
||||
0x0022`, `WieldObject 0x0023`, …) thereafter; it manipulates them with
|
||||
`0xF7B1` GameActions (`PutItemInContainer 0x0019`, `DropItem 0x001B`,
|
||||
`GetAndWieldItem 0x001A`, the `Stackable*` family, `GiveObjectRequest 0x00CD`).
|
||||
|
||||
acdream already has the outbound builders for most actions
|
||||
(`InventoryActions.cs`, `InteractRequests.cs`) and parsers for most inbound
|
||||
events (`GameEvents.cs`), plus a live `ItemRepository`. The gaps are concrete
|
||||
and enumerated in §4: a missing `DropItem`/`GetAndWieldItem`/`ViewContents`/
|
||||
`NoLongerViewingContents` parser-or-builder, a 4th field on
|
||||
`InventoryPutObjInContainer`, and `CreateObject` not yet extracting
|
||||
`IconId`/`WeenieClassId`/`StackSize`/capacities.
|
||||
|
||||
> **Spine dependency.** The handoff said the SPINE agent's doc would live at
|
||||
> `docs/research/2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md`.
|
||||
> At the time of writing **that file does NOT exist** (only the handoff
|
||||
> `2026-06-16-action-bar-inventory-equipment-handoff.md` is present — verified
|
||||
> by `Glob docs/research/2026-06-16-*.md`). I therefore derived the
|
||||
> inventory-relevant `UIElement_UIItem` facts FIRST-HAND from the decomp and
|
||||
> cite them here; where the spine doc later goes deeper (icon DBObj render,
|
||||
> drag state machine), this doc should be read as the inventory-specific layer
|
||||
> on top of it.
|
||||
|
||||
**Confidence legend:**
|
||||
- **CONFIRMED** — quoted from a source I opened (decomp `class::method` + line,
|
||||
or a real `file:line`).
|
||||
- **LIKELY** — inferred from a confirmed source; the inference is named.
|
||||
- **UNVERIFIED** — educated guess, flagged loudly; do not port without checking.
|
||||
|
||||
---
|
||||
|
||||
## 2. LayoutDesc / element map (Q1, Q7)
|
||||
|
||||
### 2.1 `gmInventoryUI` — outer frame, `LayoutDesc 0x21000023` (300×362)
|
||||
|
||||
**CONFIRMED Q1.** `gmInventoryUI::Register` registers element class `0x10000023`:
|
||||
> `gmInventoryUI::Register (decomp line 176285): UIElement::RegisterElementClass(0x10000023, gmInventoryUI::Create);`
|
||||
|
||||
The window is built from `LayoutDesc 0x21000023` (pre-dump
|
||||
`.layout-dumps/inventory-0x21000023.txt`). The root element `0x100001CC`
|
||||
(Type `268435491 = 0x10000023` = the gmInventoryUI class itself) is 300×362 at
|
||||
ZLevel 1000. `gmInventoryUI::PostInit` (decomp 176236) resolves its named
|
||||
children by id — these element ids match the dump 1:1, which is what confirms
|
||||
the map:
|
||||
|
||||
| Dump element | X,Y,W,H | Type (resolved) | PostInit binds to | Role |
|
||||
|---|---|---|---|---|
|
||||
| `0x100001CC` (root) | 0,0 300×362 | `0x10000023` gmInventoryUI | — | window root |
|
||||
| `0x100001CD` | 0,23 224×214 | `0x10000024` (base `0x21000024`) | `m_paperDollUI` (DynamicCast `0x10000024`) | nested **PaperDoll** window |
|
||||
| `0x100001CE` | 239,23 61×339 | `0x10000022` (base `0x21000022`) | `m_backpackUI` (DynamicCast `0x10000022`) | nested **Backpack** strip |
|
||||
| `0x100001CF` | 0,237 234×120 | `0x10000021` (base `0x21000021`) | `m_3DItemsUI` (DynamicCast `0x10000021`) | nested **3D items** viewport |
|
||||
| `0x100001D3` | 0,0 276×25 | base `0x21000191` | `m_titleText` (`GetChildRecursive`) | title bar ("Inventory of %s") |
|
||||
| `0x100001D2` | 276,0 24×23 | base `0x10000... 0x21000192` | (button: chrome) | close/X button (states Normal/pressed) |
|
||||
| `0x100001D1` | 0,361 300×1 | Type 3 (Field/chrome) | — | bottom rule line (sprite `0x06004D0B`) |
|
||||
| `0x100001D0` | 0,0 300×362 | Type 3 (Field/chrome) | — | full-window backdrop (`0x06004D0A`, Alphablend) ZLevel 100 |
|
||||
|
||||
PostInit excerpt (CONFIRMED):
|
||||
> `gmInventoryUI::PostInit (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.
|
||||
|
|
@ -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.
|
||||
407
docs/research/2026-06-16-ui-panels-synthesis.md
Normal file
407
docs/research/2026-06-16-ui-panels-synthesis.md
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
# D.2b core panels — SYNTHESIS (toolbar + inventory + paperdoll)
|
||||
|
||||
**Date:** 2026-06-16
|
||||
**Phase:** D.2b retail-UI engine, "core panels" research arc. Report-only synthesis.
|
||||
**Role:** synthesis lead reconciling the three panel deep-dives into one authoritative
|
||||
build plan. The deliverable is this doc; no code was written.
|
||||
**Inputs (all read in full):**
|
||||
- toolbar: [`2026-06-16-action-bar-toolbar-deep-dive.md`](2026-06-16-action-bar-toolbar-deep-dive.md)
|
||||
- inventory: [`2026-06-16-inventory-deep-dive.md`](2026-06-16-inventory-deep-dive.md)
|
||||
- paperdoll: [`2026-06-16-equipment-paperdoll-deep-dive.md`](2026-06-16-equipment-paperdoll-deep-dive.md)
|
||||
- handoff: [`2026-06-16-action-bar-inventory-equipment-handoff.md`](2026-06-16-action-bar-inventory-equipment-handoff.md)
|
||||
|
||||
> ## Note: the SPINE doc was completed in a follow-up pass
|
||||
> The handoff promised a "spine agent" doc covering the shared item-slot widget, icon
|
||||
> decode, and the full drag-drop state machine. During the original workflow run the
|
||||
> spine agent died on a transient API error, so this synthesis was first written against
|
||||
> a `null` spine digest. **The spine doc has since been written:**
|
||||
> [`2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md`](2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md).
|
||||
> It resolves the two items this synthesis had left open: (1) the **icon-composite
|
||||
> render path** — each icon LAYER is a `0x06` RenderSurface decoded DIRECTLY, but the
|
||||
> on-screen icon is a **5-layer runtime composite** blitted into one private 32×32
|
||||
> surface (`IconData::RenderIcons` decomp 407524), NOT a single texture and NOT
|
||||
> appraise-gated; (2) the item-cell **bound-object field `+0x5FC` = `UIElement_UIItem::itemID`**
|
||||
> (decomp 230230). The shared `UIElement_UIItem` / `UIElement_ItemList` identity facts
|
||||
> below were first-hand-derived + re-verified by the panel agents and remain sound.
|
||||
> §4 Step 0 and the §5 risks below have been updated to reflect the completed spine doc.
|
||||
|
||||
## 0. Summary + confidence legend
|
||||
|
||||
The three D.2b core panels are all built from the **same two reusable retail widgets**:
|
||||
the **item-slot** (`UIElement_UIItem`, class `0x10000032`) and the **item-list/grid**
|
||||
(`UIElement_ItemList`, class `0x10000031`). Every slot in every panel is one of these —
|
||||
the toolbar is 18 single-cell item-lists, the inventory is N-cell item-list grids, and
|
||||
the paperdoll is ~25 single-cell item-lists keyed to `EquipMask`. Build those two
|
||||
widgets once and all three panels fall out. The paperdoll adds one genuinely new piece:
|
||||
a **`UiViewport`** (`UIElement_Viewport`, Type `0xD`) that renders a live 3D character
|
||||
clone into a UI rect — the single biggest new engineering item. All three panels are
|
||||
pop-up windows, so they all need the deferred **window manager** (open/close/z-order/
|
||||
persist + Dragbar Type 2 + Resizebar Type 9 drag-resize). On the wire, acdream is in
|
||||
good shape: most C→S builders and S→C parsers already exist; the concrete gaps are a
|
||||
handful of missing builders (`DropItem`, `GetAndWieldItem`, `NoLongerViewingContents`),
|
||||
missing parsers (`ViewContents`, `SetStackSize`, `InventoryRemoveObject`), two
|
||||
incomplete parsers (a dropped 4th field on `0x0022`; a dropped error code on `0x00A0`),
|
||||
and `CreateObject` discarding `IconId`/`StackSize`/capacities the cells need.
|
||||
|
||||
**Confidence legend** (carried from the source docs, re-checked here):
|
||||
- **CONFIRMED** — quoted from a named-decomp `class::method` (with line) or a real
|
||||
`file:line` that I or a panel agent opened.
|
||||
- **LIKELY** — inferred from a confirmed source; the inference is named.
|
||||
- **UNVERIFIED** — educated guess, flagged loudly; needs a decomp/cdb follow-up before
|
||||
porting.
|
||||
|
||||
**Synthesis-lead re-verifications (opened first-hand, this session):**
|
||||
- `CreateObject.cs:515-516` — `_ = ReadPackedDword(...) // WeenieClassId; _ = ReadPackedDwordOfKnownType(..., IconTypePrefix);` → **IconId and WeenieClassId are discarded**. CONFIRMED.
|
||||
- `acclient_2013_pseudo_c.txt:135087-135088` — `UIElement_ItemList::Register();` and `UIElement_UIItem::Register();` are real adjacent symbols (`0x0047a483`/`0x0047a488`). CONFIRMED.
|
||||
- `acclient_2013_pseudo_c.txt:135130-135132` — `gmBackpackUI::Register / gmInventoryUI::Register / gmPaperDollUI::Register` all real. CONFIRMED.
|
||||
- `acclient_2013_pseudo_c.txt:175242-175508` — the ~25 paperdoll equip slots each `DynamicCast(0x10000031)` (`m_neckSlot, m_headSlot, m_weaponReadySlot, m_ammoReadySlot, …`) + `RegisterItemListDragHandler`. CONFIRMED.
|
||||
- `acclient_2013_pseudo_c.txt:229180-229413` — the `m_elem_Icon_*` family (`_Ghosted`, `_OpenContainer`, `_Selected`, `_DragAccept`) and its `SetState` reject/accept/neutral states (`0x10000040` / `0x10000041` / `0x1000003f`) are real on `UIElement_UIItem`. CONFIRMED (corroborates the inventory agent's first-hand derivation).
|
||||
|
||||
---
|
||||
|
||||
## 1. Confirmed class ids + LayoutDesc ids + sizes
|
||||
|
||||
All confirmed via `*::Register` (`RegisterElementClass`) in the decomp + the
|
||||
pre-dumped `.layout-dumps/` trees. The element-class id and the LayoutDesc id are
|
||||
distinct namespaces (`0x10000xxx` = element class registered in C++; `0x21000xxx` =
|
||||
the dat LayoutDesc that builds the window).
|
||||
|
||||
| Panel / widget | Element class id | LayoutDesc id | Root element | Size (W×H) | Register anchor |
|
||||
|---|---|---|---|---|---|
|
||||
| `gmToolbarUI` (action bar) | `0x10000007` | `0x21000016` | `0x10000191` | 300×122 | `gmToolbarUI::Register` (decomp 196897); `GetUIElementType`→`0x10000007` (196707) |
|
||||
| `gmInventoryUI` (frame) | `0x10000023` | `0x21000023` | `0x100001CC` | 300×362 | `gmInventoryUI::Register` (decomp 176285 / `0x004a6a60`) |
|
||||
| `gmBackpackUI` (pack strip) | `0x10000022` | `0x21000022` | `0x100001C8` | 61×339 | `gmBackpackUI::Register` (decomp 176531 / `0x004a6e80`) |
|
||||
| `gmPaperDollUI` (equip doll) | `0x10000024` | `0x21000024` | `0x100001D4` | 224×214 | `gmPaperDollUI::Register` (decomp 174445 / `0x004a4560`) |
|
||||
| `gm3DItemsUI` ("Contents of Backpack") | `0x10000021` | `0x21000021` | `0x100001C4` | 234×120 | `gm3DItemsUI::Register` (decomp 176723) |
|
||||
| `UIElement_UIItem` (item-slot, shared) | `0x10000032` | `0x21000037` (32×32 cell template) | — | 32×32 | `UIElement_UIItem::Register` (decomp 229339 / `0x0047a488`) |
|
||||
| `UIElement_ItemList` (item-list/grid, shared) | `0x10000031` | `0x2100003D` (single 32×32 cell) | `0x10000339` | 32×32 cell | `UIElement_ItemList::Register` (decomp / `0x0047a483`) |
|
||||
|
||||
**Nesting (CONFIRMED `gmInventoryUI::PostInit` 176236-176259):** the inventory FRAME
|
||||
(`0x21000023`) hosts three NESTED gm\*UI windows by id — `0x100001CD`→paperdoll
|
||||
(`DynamicCast 0x10000024`), `0x100001CE`→backpack (`DynamicCast 0x10000022`),
|
||||
`0x100001CF`→3D-items (`DynamicCast 0x10000021`). This "sub-window mount" (an element
|
||||
whose Type is a high `0x10000xxx` game class with its own `BaseLayoutId`) is a
|
||||
capability the importer does **not** have yet.
|
||||
|
||||
**Note on `gm3DItemsUI`:** despite the "3D" name it is a 2D "Contents of Backpack"
|
||||
item-list (`gm3DItemsUI::PostInit` 176728 sets `m_contentsText`→"Contents of Backpack",
|
||||
`m_itemList`→`DynamicCast(0x10000031)`; its layout has NO Viewport). The 3D character
|
||||
doll is in `gmPaperDollUI`, not here. CONFIRMED.
|
||||
|
||||
---
|
||||
|
||||
## 2. CONSOLIDATED new toolkit widgets (the single authoritative list)
|
||||
|
||||
This reconciles the four docs into one list. The shipped D.2b toolkit already has
|
||||
Button(1)/Dragbar(2)/Field(3)/Menu(6)/Meter(7)/Panel(8)/Scrollbar(0xB)/Text(0xC) plus
|
||||
`UiDatElement` for generic chrome — those are **reused**, not re-listed.
|
||||
|
||||
**Type-registration model:** the shipped numeric Type registry (1=Button … 0x12=Proto)
|
||||
is the toolkit's generic-widget dispatch. The item-slot / item-list / viewport are NOT
|
||||
in that numeric table — in retail they are **`UIElement` subclasses registered by a
|
||||
full class id** via `RegisterElementClass(0x10000xxx, …)`, and in the dat their
|
||||
elements have `Type=0` and inherit the real class id through the `BaseElement` chain
|
||||
(resolved by `ElementReader.Merge`'s zero-wins-base rule). So in acdream's
|
||||
`DatWidgetFactory` they are **new behavioral leaf widgets keyed off the resolved class
|
||||
id**, exactly the same pattern as the existing behavioral widgets — they just key off
|
||||
`0x10000031`/`0x10000032`/`0xD` rather than a small numeric Type. (The numeric Type
|
||||
that `0xD`=Viewport occupies in the confirmed registry IS a generic toolkit Type, so
|
||||
`UiViewport` can register at Type `0xD` directly; the item-slot/item-list register at
|
||||
their class ids.)
|
||||
|
||||
| Widget | Registers at | Leaf vs container | Panels that use it | Purpose |
|
||||
|---|---|---|---|---|
|
||||
| **`UiItemSlot`** (port of `UIElement_UIItem`, class `0x10000032`) | class id `0x10000032` (resolves to a `UIElement_Field` subclass ⇒ underlying **Type 3**). Behavioral leaf. | **LEAF** (`ConsumesDatChildren=>true`) — reproduces its icon + overlay sub-elements procedurally | **all three** (toolbar slots, inventory cells, paperdoll equip slots) | one item-in-a-slot: icon (underlay/base/effects-overlay) + quantity text + capacity/structure Type-7 bars + cooldown ring; holds the bound object id (retail `+0x5FC`); selection/ghost/drag-accept/open-container overlay states. **The spine widget — build once.** |
|
||||
| **`UiItemList` / `UiItemGrid`** (port of `UIElement_ItemList`, class `0x10000031`) | class id `0x10000031`. Behavioral widget. | **leaf wrt the importer** (manages its own `UiItemSlot` children procedurally) — but logically a **container** of slots | **all three** (toolbar = 1-cell instances; inventory = N-cell grids; paperdoll = 1-cell equip slots) | a 1-cell or N-cell grid of `UiItemSlot`s. Port `ItemList_AddItem/InsertItem/Flush/IsInList/GetNumUIItems/GetItem/OpenContainer/SetChildList/SetParentContainer/OpenFirstContainer`. Backpack uses **two** instances (own list `+0x604`, other-container list `+0x608`). |
|
||||
| **`UiViewport`** (port of `UIElement_Viewport`, Type `0xD`) | numeric Type **`0xD`** (confirmed registry; `UIElement_Viewport::Register`→`RegisterElementClass(0xd,…)` decomp 119126) | **LEAF** (`ConsumesDatChildren=>true`) | **paperdoll only** | hosts a single live 3D entity (the character clone) rendered into the widget's screen rect via a scissored mini 3D pass. Owns a fixed camera + one distant light + an `AnimatedEntityState`. **Needs a new Core→App render-into-rect seam (`IUiViewportRenderer`, Code-Structure Rule 2). The biggest new piece.** |
|
||||
| **Window manager** (shared infra; drives Dragbar Type 2 + Resizebar Type 9) | not a registered widget — infra that drives existing Type-2/Type-9 chrome + `UiElement.Draggable/Resizable` | n/a | **all three** (plus future pop-ups) | open/close/z-order/persist for pop-up windows + faithful grip/dragbar drag-resize. Today vitals/chat use whole-window drag (accepted IA-12 approximation). This is "the other deferred Plan-2 piece." |
|
||||
| **Sub-window mount** (LayoutImporter capability, not a widget) | n/a — an element whose Type is a high `0x10000xxx` game class WITH a non-zero `BaseLayoutId` | container | **inventory** (frame nests paperdoll + backpack + 3D-items) | lets `LayoutImporter` instantiate a NESTED `LayoutDesc` window inside a parent slot. The importer recurses generic children today but has never mounted another gm\*UI window. |
|
||||
| **Per-panel controllers** (`ToolbarController`, `InventoryController`, `PaperDollController`) | not widgets — controllers like `VitalsController`/`ChatWindowController` | n/a | one per panel | find-by-id binding + wire send/receive + model restore. The `gm*UI::PostInit` analogues. (Listed for completeness; each is panel-specific, not a shared widget.) |
|
||||
|
||||
### 2a. Reconciled disagreements between the agents
|
||||
|
||||
The four docs were **consistent** on the big-three widget identities; the differences
|
||||
were wording, not substance. Reconciled:
|
||||
|
||||
1. **Item-slot Type — no real conflict.** Toolbar + inventory + paperdoll all call it
|
||||
`UIElement_UIItem`, class **`0x10000032`**, a `UIElement_Field` subclass (underlying
|
||||
Type 3), built as a behavioral **leaf**. The paperdoll doc's widget table named its
|
||||
equip-slot variant "`UiItemSlot` registering at `0x10000031`" — that is the *equip
|
||||
slot* (a single-cell `UIElement_ItemList`), NOT the inner item-cell. **Reconciliation:
|
||||
`UIElement_ItemList` (`0x10000031`) is the slot/grid container; `UIElement_UIItem`
|
||||
(`0x10000032`) is the item-cell inside it.** The paperdoll equip slot is a 1-cell
|
||||
`UIElement_ItemList` that holds at most one `UIElement_UIItem` — same two-widget spine
|
||||
as everywhere else, just constrained to one cell. (CONFIRMED: every paperdoll slot is
|
||||
`DynamicCast(0x10000031)`, decomp 175242-175508; the inner cell is the
|
||||
`UIElement_UIItem` `0x10000032` per the inventory agent's `UIItem_Update`/`m_elem_Icon`
|
||||
citations, re-verified at 229180-229413.)
|
||||
|
||||
2. **Item-list "leaf vs container".** Toolbar + paperdoll said **leaf** (the importer
|
||||
doesn't build its dat children; it reproduces cells procedurally); inventory said
|
||||
**container** (it lays out an N-column grid). **Reconciliation: it is a behavioral
|
||||
LEAF to the importer** (`ConsumesDatChildren=>true`, the importer must NOT recurse its
|
||||
dat children) but it is logically a **container of `UiItemSlot`s at runtime** (it
|
||||
creates/destroys cells procedurally as items arrive). Both descriptions are correct
|
||||
at different layers; the binding rule for the factory is `ConsumesDatChildren=>true`.
|
||||
|
||||
3. **`UiViewport` Type.** Only the paperdoll doc introduced it; **Type `0xD`**,
|
||||
confirmed against the registry (`0xD`=Viewport) and `RegisterElementClass(0xd,…)`.
|
||||
No conflict.
|
||||
|
||||
4. **Window manager.** All three docs named it identically (shared, drives Dragbar
|
||||
Type 2 + Resizebar Type 9, open/close/z-order/persist). No conflict.
|
||||
|
||||
5. **`+0x5FC` (bound object id on the item-cell).** The toolbar doc anchors this by
|
||||
OFFSET only (UNVERIFIED symbolic name). The inventory/spine-territory render of the
|
||||
cell would have named it; since the spine doc is missing, **it stays UNVERIFIED** —
|
||||
carried to §5.
|
||||
|
||||
---
|
||||
|
||||
## 3. Cross-panel wire-message catalog (de-duplicated)
|
||||
|
||||
All C→S ride the `0xF7B1` GameAction envelope (`u32 0xF7B1; u32 seq; u32 subOpcode; …`);
|
||||
all S→C item events ride the `0xF7B0` GameEvent envelope (`u32 0xF7B0; u32 target;
|
||||
u32 seq; u32 eventOpcode; …`). De-duplicated across the three panels; the "Panels"
|
||||
column shows which panel(s) use each. acdream parse-status is the union of what the
|
||||
three agents found (each cross-checked against `src/AcDream.Core.Net/`).
|
||||
|
||||
### 3.1 Client → server (GameActions)
|
||||
|
||||
| Opcode | Name | Dir | Trigger | ACE handler | Chorizite type | acdream status | Panels |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `0x0019` | PutItemInContainer | C→S | drag item into pack / pick up (container=self) | `GameActionPutItemInContainer.Handle` | `Inventory_PutItemInContainer*` | **parsed** — `InteractRequests.BuildPickUp` (InteractRequests.cs:97) | inv, paperdoll (un-wield) |
|
||||
| `0x001A` | GetAndWieldItem | C→S | equip item from pack onto doll/equip slot | `GameActionGetAndWieldItem.Handle` (Actions/GameActionGetAndWieldItem.cs:7-14) → `Player.HandleActionGetAndWieldItem(itemGuid, EquipMask)` | `Inventory_GetAndWieldItem` (`uint ObjectId; EquipMask Slot`, generated.cs:14-42) | **MISSING** (no builder) | paperdoll, inv |
|
||||
| `0x001B` | DropItem | C→S | drop item on the ground | `GameActionDropItem.Handle` (1×u32 guid) | — | **MISSING** (no builder) | inv |
|
||||
| `0x0035` | UseWithTarget | C→S | use src item on target (toolbar target-mode / key→door) | (Interact) | — | **parsed** — `InteractRequests.BuildUseWithTarget` | toolbar, inv |
|
||||
| `0x0036` | UseItem | C→S | use/activate a single item (toolbar slot activation) | `GameActionUseItem` | — | **parsed** — `InteractRequests.BuildUse` | toolbar, inv |
|
||||
| `0x0054` | StackableMerge | C→S | drop stack A onto compatible stack B | `GameActionStackableMerge.Handle` (from,to,amount) | — | **parsed** — `InventoryActions.BuildStackableMerge` | inv |
|
||||
| `0x0055` | StackableSplitToContainer | C→S | split N off a stack into a pack slot | `GameActionStackableSplitToContainer.Handle` (stack,container,place,amount) | — | **parsed** — `InventoryActions.BuildStackableSplitToContainer` | inv |
|
||||
| `0x0056` | StackableSplitTo3D | C→S | split N off a stack onto the ground | `GameActionStackableSplitTo3D.Handle` (stack,amount) | — | **parsed** — `InventoryActions.BuildStackableSplitTo3D` | inv |
|
||||
| `0x019B` | StackableSplitToWield | C→S | split N off a stack into an equip slot (arrows) | `GameActionStackableSplitToWield` (stack,equipMask,amount) | — | **parsed** — `InventoryActions.BuildStackableSplitToWield` | inv, paperdoll |
|
||||
| `0x00CD` | GiveObjectRequest | C→S | give item/N-of-stack to NPC/player | `GameActionGiveObjectRequest.Handle` (target,item,amount) | — | **parsed** — `InventoryActions.BuildGiveObjectRequest` | inv |
|
||||
| `0x0195` | NoLongerViewingContents | C→S | close a side-pack / ground-container view | `GameActionType` 0x0195 (containerGuid) | — | **MISSING** (no builder) | inv |
|
||||
| `0x019C` | AddShortCut | C→S | pin item to toolbar slot (on drag-to-slot / add-selected) | `GameActionAddShortcut.Handle` → `Character.AddOrUpdateShortcut(Index,ObjectId)` | `Character_AddShortCut { ShortCutData Shortcut }` | **builder present** — `InventoryActions.BuildAddShortcut` *(fix field naming → Index/ObjectId/SpellId\|Layer)* | toolbar |
|
||||
| `0x019D` | RemoveShortCut | C→S | unpin / evict / overwrite a toolbar slot | `GameActionRemoveShortcut.Handle` → `Character.TryRemoveShortcut(index)` | `Character_RemoveShortCut { uint Index }` | **builder present** — `InventoryActions.BuildRemoveShortcut` | toolbar |
|
||||
|
||||
### 3.2 Server → client (GameEvents / GameMessages)
|
||||
|
||||
| Opcode | Name | Dir | Trigger | ACE handler | Chorizite type | acdream status | Panels |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `0x0022` | InventoryPutObjInContainer | S→C | confirm item in container at slot | `GameEventItemServerSaysContainId` (itemGuid,containerGuid,placement,**containerType**) | — | **parsed INCOMPLETE** — `GameEvents.ParsePutObjInContainer` reads 3 fields, **drops containerType**; wired (GameEventWiring.cs:239) | inv |
|
||||
| `0x0023` | WieldObject | S→C | confirm item equipped to slot | `GameEventWieldItem` (objectId, i32 equipMask) | — | **parsed + wired** — `GameEvents.ParseWieldObject`, GameEventWiring.cs:231 | paperdoll, inv |
|
||||
| `0x0052` | CloseGroundContainer | S→C | server closed a ground-container view | `GameEventCloseGroundContainer` (containerGuid) | — | **parsed UNWIRED** — `GameEvents.ParseCloseGroundContainer`, not in WireAll | inv |
|
||||
| `0x00A0` | InventoryServerSaveFailed | S→C | reject speculative client move (roll back) | `GameEventInventoryServerSaveFailed` (itemGuid, weenieError) | — | **parsed UNWIRED INCOMPLETE** — reads guid only, **drops error**; not in WireAll | inv (+toolbar/paperdoll rollback) |
|
||||
| `0x00C9` | IdentifyObjectResponse | S→C | appraise result (gates tooltip, not icon) | `GameEventIdentifyObjectResponse` (guid,flags,success,property tables) | — | **parsed + wired** — `AppraiseInfoParser` via GameEventWiring.cs:245 | inv, paperdoll, toolbar (tooltip) |
|
||||
| `0x0196` | ViewContents | S→C | full contents list of an opened container | `GameEventViewContents` (container,count,{guid,containerType}×n) | — | **MISSING** (no parser) | inv |
|
||||
| `0x0197` | SetStackSize | S→C | update a stack count+value after merge/split | `GameMessageSetStackSize` (seq,guid,stackSize,value) | — | **MISSING** (no parser) | inv, toolbar |
|
||||
| `0x019A` | InventoryPutObjectIn3D | S→C | confirm item dropped to world | `GameEventItemServerSaysMoveItem` (objectGuid) | — | **parsed UNWIRED** — `GameEvents.ParsePutObjectIn3D`, not in WireAll | inv |
|
||||
| `0xF625` | ObjDescEvent | S→C | wield/unwield → full new appearance broadcast → `RedressCreature` | `GameMessageObjDescEvent` → `SerializeUpdateModelData` (GameMessageObjDescEvent.cs:10-17) | (ModelData block) | **parsed** — `ObjDescEvent.cs:33-73` (`CreateObject.ReadModelData`) | paperdoll |
|
||||
| `0xF745` | CreateObject | S→C | spawn a weenie incl. a pack item (IconId/WeenieClassId/StackSize/Value/capacities) | `GameMessageCreateObject` → `WorldObject.SerializeCreateObject` | `Item_CreateObject` | **parsed INCOMPLETE** — `CreateObject.TryParse` **discards IconId (cs:516), WeenieClassId (cs:515), StackSize, Value, ItemCapacity, ContainerCapacity** | all (icon + quantity source) |
|
||||
| (UIQueue) | InventoryRemoveObject | S→C | remove item from inventory view (given/dropped/destroyed) | `GameMessageInventoryRemoveObject` (guid) | — | **MISSING** (no parser) | inv, toolbar |
|
||||
| `PlayerDescription` SHORTCUT block | persisted toolbar shortcut list | S→C | login (part of `0xF7B0`/0x0013 `PlayerDescription`) | `Player_Character.GetShortcuts()` | `ShortCutData` (Index,ObjectId,LayeredSpellId) | **parsed** — `PlayerDescriptionParser.cs:345-356` → `Parsed.Shortcuts` | toolbar |
|
||||
| `PlayerDescription` equipped `InventoryPlacement` list | persisted equipped-gear list | S→C | login | `GameEventPlayerDescription.WriteEventBody` | `Login_PlayerDescription` | **PARTIAL** — equipped section NOT surfaced (PlayerDescriptionParser.cs:70-77) | paperdoll |
|
||||
|
||||
**Shared-message note:** `CreateObject (0xF745)` and `IdentifyObjectResponse (0x00C9)`
|
||||
are used by all three panels and de-duplicated above. `GetAndWieldItem (0x001A)` is
|
||||
shared by inventory (equip-from-grid) and paperdoll (drop-on-doll); `UseItem (0x0036)`/
|
||||
`UseWithTarget (0x0035)` are shared by toolbar activation and inventory double-click.
|
||||
|
||||
### 3.3 acdream wire-gap TODO (the build session's concrete list)
|
||||
|
||||
- **Add C→S builders:** `GetAndWieldItem (0x001A)`, `DropItem (0x001B)`,
|
||||
`NoLongerViewingContents (0x0195)`.
|
||||
- **Add S→C parsers:** `ViewContents (0x0196)`, `SetStackSize (0x0197)`,
|
||||
`InventoryRemoveObject`.
|
||||
- **Fix incomplete parsers:** `ParsePutObjInContainer` (read the 4th `containerType`
|
||||
u32); `ParseInventoryServerSaveFailed` (read the `weenieError` u32).
|
||||
- **Wire (register in `GameEventWiring.WireAll`):** `ViewContents`,
|
||||
`InventoryPutObjectIn3D (0x019A)`, `CloseGroundContainer (0x0052)`,
|
||||
`InventoryServerSaveFailed (0x00A0)`.
|
||||
- **Extend `CreateObject.TryParse`** to capture `IconId`, `WeenieClassId`, `StackSize`,
|
||||
`Value`, `ItemCapacity`, `ContainerCapacity` (cells need icon + quantity +
|
||||
capacity bar). **Re-verified discarded at `CreateObject.cs:515-516`.**
|
||||
- **Extend `PlayerDescriptionParser`** to surface the equipped `InventoryPlacement
|
||||
{iid, loc, priority}` list (paperdoll slot icons at login).
|
||||
- **Fix `InventoryActions.BuildAddShortcut` field naming** (currently
|
||||
`slotIndex/objectType/targetId`; wire layout is correct for item shortcuts but
|
||||
semantics should be `Index/ObjectId/SpellId|Layer`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Recommended build order
|
||||
|
||||
Ordered by dependency so the next session can go straight to brainstorm → spec → plan.
|
||||
Each step states why it must come where it does.
|
||||
|
||||
**Step 0 — SPINE research (DONE — see the spine doc).**
|
||||
The spine doc is complete:
|
||||
[`2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md`](2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md).
|
||||
It specs the item-cell widget, the 5-layer icon composite (`IconData::RenderIcons`
|
||||
decomp 407524 — each layer a `0x06` RenderSurface decoded directly, blitted into one
|
||||
private 32×32 surface: type-default underlay / custom underlay / base `_iconID` / custom
|
||||
overlay+tint / effect overlay), `UIElement_UIItem::UIItem_Update`/`UIItem_SetIcon`
|
||||
(decomp 230226+), the overlay-state machine, and the widget-level drag-drop hooks
|
||||
(`UIElement_Field::MouseOverTop`/`CatchDroppedItem`). The `+0x5FC` field is resolved
|
||||
(`UIElement_UIItem::itemID`). Steps 2-7 can now proceed against a finished port spec;
|
||||
this is **no longer blocking**. (Note: WB `TextureHelpers.cs` / ACViewer `IndexToColor`
|
||||
are for WORLD textures — item icons take NO subpalette overlay at composite time; see
|
||||
the spine doc.)
|
||||
|
||||
**Step 1 — Window manager foundation.**
|
||||
*Why first among code:* all three panels are pop-up windows that must open/close, stack
|
||||
(z-order), persist position, and (faithfully) drag/resize via Dragbar (Type 2) +
|
||||
Resizebar (Type 9). Vitals/chat shipped with whole-window drag (accepted IA-12
|
||||
approximation); the panels need the real thing. Everything visible in Steps 5-7 mounts
|
||||
inside a managed window, so the manager is the substrate. It is independent of the wire
|
||||
work, so it can proceed in parallel with Step 0.
|
||||
|
||||
**Step 2 — `UiItemSlot` widget + icon pipeline (`UIElement_UIItem` 0x10000032).**
|
||||
*Why here:* it is the atom of all three panels. Depends on Step 0 (icon render) and on
|
||||
the §3.3 `CreateObject` extension (IconId/StackSize) for real data. Build the leaf
|
||||
widget: icon composite, quantity text, capacity/structure Type-7 bars, cooldown ring,
|
||||
and the selection/ghost/drag-accept/open-container overlay states.
|
||||
|
||||
**Step 3 — `UiItemList` / `UiItemGrid` widget (`UIElement_ItemList` 0x10000031).**
|
||||
*Why here:* it composes `UiItemSlot`s and is used by every panel (1-cell and N-cell).
|
||||
Depends on Step 2. Port `ItemList_AddItem/InsertItem/Flush/IsInList/GetNumUIItems/
|
||||
GetItem/OpenContainer/SetChildList/SetParentContainer`. Register as a behavioral leaf
|
||||
(`ConsumesDatChildren=>true`).
|
||||
|
||||
**Step 4 — Wire wiring (builders/parsers/wireup from §3.3).**
|
||||
*Why here:* the controllers in Steps 5-7 need the full send/receive surface, and this is
|
||||
independent of the widget rendering — it can run in parallel with Steps 1-3. Add the
|
||||
missing builders/parsers, fix the two incomplete parsers, register the unwired parsers,
|
||||
extend `CreateObject` + `PlayerDescriptionParser`, fix `BuildAddShortcut` naming. Each
|
||||
new deviation gets a divergence-register row in the same commit.
|
||||
|
||||
**Step 5 — `ToolbarController` + the action bar (simplest panel).**
|
||||
*Why before the others:* the toolbar is the simplest consumer (18 single-cell lists, no
|
||||
nested sub-windows, no viewport) and exercises the full spine + window manager + wire
|
||||
path end-to-end. acdream already parses the SHORTCUT block and has both shortcut
|
||||
builders, so it's the fastest path to a working, testable panel. Bind the 18 slots,
|
||||
the hidden selected-object meters + stack slider, the panel-launcher buttons; restore
|
||||
from `Parsed.Shortcuts`; wire `UseShortcut`/`AddShortcut`/`RemoveShortcut` +
|
||||
`HandleDropRelease`.
|
||||
|
||||
**Step 6 — `InventoryController` + the inventory/backpack panels + sub-window mount.**
|
||||
*Why here:* adds the N-cell grid (Step 3 at scale), the burden Meter (reuses Type-7
|
||||
`SetLoadLevel`→fill 0x69), the dual-ItemList container model (own `+0x604` / other
|
||||
`+0x608`), and the **sub-window mount** importer capability (frame nests paperdoll +
|
||||
backpack + 3D-items). The hardest 2D panel; depends on Steps 1-4 and the new sub-window
|
||||
mount.
|
||||
|
||||
**Step 7 — `UiViewport` + `PaperDollController` + the equipment doll (biggest new piece).**
|
||||
*Why last:* it depends on everything above (window manager, equip-slot `UiItemList`
|
||||
instances, `GetAndWieldItem` wire, `PlayerDescription` equipped list) AND introduces the
|
||||
single largest new engineering item: the **UI↔3D render seam** (`IUiViewportRenderer`
|
||||
Core interface, App impl, per Code-Structure Rule 2) that renders a re-dressed player
|
||||
clone into a scissored UI rect. It reuses acdream's existing `EntitySpawnAdapter`/
|
||||
`AnimatedEntityState` character path, but the rect-scissored single-entity pass is new.
|
||||
Doing it last lets the 2D panels validate the spine first, so a 3D-render bug is
|
||||
isolated.
|
||||
|
||||
**Parallelism summary:** Step 0 (spine research) + Step 1 (window manager) + Step 4
|
||||
(wire) can all proceed independently; Steps 2→3→5→6→7 are the dependent spine→panels
|
||||
chain.
|
||||
|
||||
---
|
||||
|
||||
## 5. Open risks / UNVERIFIED — resolve BEFORE implementation
|
||||
|
||||
Collated from all four docs; each needs a decomp or cdb follow-up before the cited step.
|
||||
|
||||
1. **SPINE doc — RESOLVED (no longer blocking).** Written:
|
||||
[`2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md`](2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md).
|
||||
Icon-composite render (`IconData::RenderIcons` 407524, 5 layers) + the widget-level
|
||||
drag-drop state machine (`UIElement_Field::MouseOverTop`/`CatchDroppedItem`, cell msgs
|
||||
`0x21`/`0x3e`/`0x15`, `InqDropIconInfo` flags) are now specced with anchors.
|
||||
2. **`UIElement_UIItem +0x5FC` bound-object-id field name — RESOLVED = `itemID`.**
|
||||
`UIElement_UIItem::itemID`, anchored at `UIItem_Update` decomp 230230
|
||||
(`uint32_t itemID = this->itemID; … GetWeenieObject(itemID)`), corroborated 230422/233107
|
||||
(companion field `spellID`). See the spine doc.
|
||||
3. **`CreateObject` IconId for CONTAINED (non-3D-visible) pack items — LIKELY.**
|
||||
Confirmed on the wire + currently discarded (`CreateObject.cs:516`), but not
|
||||
byte-traced that ACE sets IconId on a *contained* item's CreateObject vs. relying on
|
||||
PlayerDescription. Verify against a live capture before treating CreateObject as the
|
||||
sole icon source.
|
||||
4. **Use-item opcode `ItemHolder::UseObject` sends (0x0035 vs 0x0036) — UNVERIFIED.**
|
||||
Throttle (0.2 s) + dispatch CONFIRMED (decomp 402923); the precise opcode branch
|
||||
(`DetermineUseResult`) not traced to the send. Both opcodes exist in acdream
|
||||
`InteractRequests.cs`; reconcile when wiring toolbar/inventory activation (Step 5/6).
|
||||
5. **`UseShortcut` target-mode path — out of scope, file follow-up.**
|
||||
`ClientUISystem::ExecuteTargetModeForItem` (use-item-on-target) depends on the cursor
|
||||
target-mode subsystem; not part of the action-bar widget itself.
|
||||
6. **`SetDelayedShortcutNum` deferral — needs a re-bind state machine.** When a slot's
|
||||
weenie isn't loaded yet (`AddShortcut` decomp 196867), the slot must re-bind once
|
||||
`CreateObject` for that guid arrives. Detail in the `ToolbarController` port (Step 5).
|
||||
7. **Paperdoll `0x100001E0` = MissileAmmo `0x800000` — LIKELY only.** The decomp
|
||||
immediate is corrupted to a string-ptr (line 173676); inferred from the EquipMask gap
|
||||
+ neighbors. Re-decompile `0x004a388a` in Ghidra to recover the real value (Step 7).
|
||||
8. **Paperdoll viewport camera/light float immediates — UNVERIFIED (not byte-decoded).**
|
||||
Lines 175524-175526 / 174144-174146; the agent read the hex but did not convert all
|
||||
floats (`0x3df5c28f≈0.12`, `0xc019999a≈−2.4`, `0xc0400000=−3.0`, `0xc059999a≈−3.4`,
|
||||
`0x3f6147ae≈0.88`, `0x3f800000=1.0`). Decode precisely for faithful framing (Step 7).
|
||||
9. **UI↔3D render seam — DESIGN-OPEN.** How a UI rect drives a scissored single-entity
|
||||
3D pass (after the world pass vs. as a UI overlay), and the exact
|
||||
`IUiViewportRenderer` Core-interface shape (Code-Structure Rule 2). Brainstorm before
|
||||
Step 7.
|
||||
10. **Does the doll clone the player `WorldEntity` or build a fresh one? — UNVERIFIED.**
|
||||
Retail clones the player `CPhysicsObj` (`makeObject(GetPhysicsObject(player_id))`,
|
||||
line 173999); acdream has no player-as-renderable today (player = camera). LIKELY a
|
||||
dedicated `WorldEntity` from the local player's Setup+ObjDesc fed to a private
|
||||
viewport host. Settle in Step 7 brainstorm.
|
||||
11. **Inventory side-pack column `0x100001CB` (16×252, base `0x2100003E`) — UNVERIFIED.**
|
||||
Tabs (one per sub-bag) or a scrollbar gutter? Dump `0x2100003E` to settle (Step 6).
|
||||
12. **`UIElement_ItemList` grid geometry (column count, cell pitch) — LIKELY.** Cell
|
||||
template 36×36 (`0x100001C9`); `UIElement_UIItem` `0x21000037` is 32×32. Confirm the
|
||||
fixed-column wrap by reading `UIElement_ItemList::ItemList_AddItem` (Step 3).
|
||||
13. **Value/coin total NOT in the inventory window — UNVERIFIED home.** No value Meter/
|
||||
text in `0x21000022`/`0x21000023`; the window shows BURDEN only. Do NOT invent a
|
||||
value summary; find its real home before adding one.
|
||||
14. **Identified-vs-unidentified does NOT swap the icon — CONFIRMED (negative).** The
|
||||
spine doc confirms there is no appraise branch anywhere in the icon path
|
||||
(`UIItem_SetIcon` → `IconData::RenderIcons`); appraise gates `UpdateTooltip` only.
|
||||
15. **`InventoryActions.BuildAddShortcut` field-naming bug — CONFIRMED file contents,
|
||||
LIKELY latent bug.** Wire layout is correct for item shortcuts; the param names
|
||||
(`slotIndex/objectType/targetId`) are misleading. Fix to `Index/ObjectId/
|
||||
SpellId|Layer` + register a divergence row at port time (Step 4/5).
|
||||
|
||||
---
|
||||
|
||||
## 6. Proposed MEMORY.md index lines (for ALL 5 docs)
|
||||
|
||||
The parent will append these; I do NOT edit MEMORY.md.
|
||||
|
||||
- [UI core-panels SYNTHESIS (toolbar+inventory+paperdoll)](research/2026-06-16-ui-panels-synthesis.md) — D.2b build plan reconciling the 3 panel deep-dives. Big-three new widgets: `UiItemSlot`(`UIElement_UIItem` 0x10000032, shared leaf), `UiItemList/Grid`(`UIElement_ItemList` 0x10000031, shared leaf-to-importer), `UiViewport`(Type 0xD, paperdoll 3D doll), plus the shared **window manager** (Dragbar 2 + Resizebar 9) + sub-window-mount importer capability + per-panel controllers. De-duped cross-panel wire table; build order (window mgr → item-slot+icon → item-list → wire → toolbar → inventory → paperdoll; spine research DONE).
|
||||
- [UI item-slot SPINE — icon composite + drag-drop](research/2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md) — D.2b shared spine (completes the 5-doc arc): `UiItemSlot`(`UIElement_UIItem` 0x10000032, `+0x5FC` RESOLVED = `itemID`) inside `UiItemList`(0x10000031). ICON CRUX: each layer is a `0x06` RenderSurface decoded DIRECTLY, but the icon is a 5-layer runtime COMPOSITE (`IconData::RenderIcons` @407524; NOT one texture, NOT appraise-gated). Drag-drop = `Field::MouseOverTop`/`CatchDroppedItem` + cell msgs 0x21/0x3e/0x15 + `InqDropIconInfo` flags; `UiRoot` already has the chain, `UiField` only stubs the hooks; gap = `CreateObject` discards IconId (cs:516).
|
||||
- [Action bar / quick slots (`gmToolbarUI`) deep dive](research/2026-06-16-action-bar-toolbar-deep-dive.md) — 18 item slots (2 rows of 9, ids `0x100001A7-AF`+`0x100006B7-BF`) = `UIElement_ItemList`(0x10000031) of one `UIElement_UIItem`(0x10000032); model `ShortCutManager::shortCuts_[18]` persisted in `PlayerDescription`'s SHORTCUT block (acdream already parses it); live mutate via `AddShortCut 0x019C`/`RemoveShortCut 0x019D` (acdream builders present — fix `BuildAddShortcut` field naming); activation = ordinary use-item (`ItemHolder::UseObject`, no special wire); the 2 Meters + Scrollbar in `0x21000016` are the hidden selected-object Health/Mana bars + stack-split slider, NOT paging; drag-drop via `gmToolbarUI : ItemListDragHandler::HandleDropRelease`. New widgets: `UiItemSlot` + `UiItemList` + `ToolbarController`.
|
||||
- [Inventory panel deep-dive (gmInventoryUI/gmBackpackUI)](research/2026-06-16-inventory-deep-dive.md) — D.2b: LayoutDesc 0x21000023 (frame: title + 3 nested sub-windows) + 0x21000022 (backpack: burden Meter 0x100001D9 via SetLoadLevel→fill 0x69, main-pack ItemList 0x100001CA); full inventory wire catalog (C→S 0x0019/1A/1B/54/55/56/19B/CD/195, S→C 0x0022/23/196/19A/A0/52 + SetStackSize/InventoryRemoveObject) with acdream parse-status (gaps: DropItem/GetAndWieldItem/ViewContents builders, 0x0022 4th field, CreateObject IconId); new widgets UiItemSlot(0x10000032)/UiItemGrid(0x10000031)+sub-window mount+window manager.
|
||||
- [Equipment/Paperdoll panel deep-dive](research/2026-06-16-equipment-paperdoll-deep-dive.md) — gmPaperDollUI 0x10000024/LayoutDesc 0x21000024: doll = UIElement_Viewport (Type 0xD, elem 0x100001D5) hosting a CreatureMode clone re-dressed via DoObjDescChangesFromDefault; ~25 equip slots are single-cell UIElement_ItemList (0x10000031) mapped element-id→EquipMask by GetLocationInfoFromElementID; wield = GetAndWieldItem (0x001A, item+EquipMask, acdream-MISSING), appearance reply = ObjDescEvent 0xF625 (acdream-PARSED) → RedressCreature; acdream's EntitySpawnAdapter/AnimatedEntityState char path is reusable; new widgets = UiViewport (0xD, the UI↔3D bridge), UiItemSlot (0x10000031), window manager. gm3DItemsUI 0x21000021 is a "Contents of Backpack" list, NOT the doll.
|
||||
- [Action-bar/inventory/equipment research handoff](research/2026-06-16-action-bar-inventory-equipment-handoff.md) — the §3 question list (Q1-Q12) + agent assignment that drove the toolbar/inventory/paperdoll/spine deep-dives. (All 5 research docs delivered; the spine doc was completed in a follow-up pass after a transient agent failure.)
|
||||
|
||||
---
|
||||
|
||||
## 7. New toolkit widgets this introduces (recap)
|
||||
|
||||
| Widget | dat Type / class it registers at | leaf vs container | Purpose |
|
||||
|---|---|---|---|
|
||||
| `UiItemSlot` | class `0x10000032` (`UIElement_UIItem`) | leaf (`ConsumesDatChildren=>true`) | shared item-cell: icon + quantity + capacity/structure bars + overlay states + bound object id |
|
||||
| `UiItemList` / `UiItemGrid` | class `0x10000031` (`UIElement_ItemList`) | leaf to importer; container of slots at runtime | shared 1-cell/N-cell grid of `UiItemSlot`s |
|
||||
| `UiViewport` | numeric Type `0xD` (`UIElement_Viewport`) | leaf (`ConsumesDatChildren=>true`) | paperdoll 3D character doll via a scissored mini 3D pass; needs `IUiViewportRenderer` Core→App seam |
|
||||
| Window manager | infra (drives Dragbar Type 2 + Resizebar Type 9) | n/a | open/close/z-order/persist + faithful grip/dragbar drag-resize for all pop-up panels |
|
||||
| Sub-window mount | LayoutImporter capability (element whose Type is a high `0x10000xxx` class with a `BaseLayoutId`) | container | nest a `LayoutDesc` window inside a parent slot (inventory frame → paperdoll/backpack/3D-items) |
|
||||
|
||||
## 8. Open questions / UNVERIFIED (recap)
|
||||
|
||||
See §5 for the full collated list with anchors. The former blocking item — the spine
|
||||
doc — is now written (icon-composite render path + widget-level drag-drop state machine
|
||||
specced with anchors; `+0x5FC` = `itemID` resolved). Remaining items are per-step
|
||||
follow-ups (decode the paperdoll camera floats, recover `0x100001E0`, dump
|
||||
`0x2100003E`, byte-trace CreateObject IconId for contained items).
|
||||
|
||||
---
|
||||
|
||||
**Single MEMORY.md index line for THIS doc:**
|
||||
|
||||
- [UI core-panels SYNTHESIS (toolbar+inventory+paperdoll)](research/2026-06-16-ui-panels-synthesis.md) — D.2b build plan reconciling the 3 panel deep-dives: shared `UiItemSlot`(0x10000032)+`UiItemList`(0x10000031) spine, `UiViewport`(Type 0xD) for the paperdoll 3D doll, window manager (Dragbar 2 + Resizebar 9) + sub-window-mount; de-duped cross-panel wire table; build order window-mgr→item-slot+icon→item-list→wire→toolbar→inventory→paperdoll (spine research DONE — see the spine deep-dive).
|
||||
Loading…
Add table
Add a link
Reference in a new issue