acdream/docs/research/2026-06-16-ui-panels-synthesis.md
Erik a5c5126e8d docs(D.5): action bar / inventory / paperdoll research drop
Five report-only deep-dives + synthesis for the next D.2b UI panels, built on the shipped widget toolkit. Confirms LayoutDesc ids (toolbar 0x21000016, inventory 0x21000023, backpack 0x21000022, paperdoll 0x21000024, 3ditems 0x21000021), the shared item-slot/item-list spine (UIElement_UIItem 0x10000032 / UIElement_ItemList 0x10000031), the 5-layer icon composite (IconData::RenderIcons @407524), the cross-panel wire catalog with acdream parse-status, and the dependency-ordered build plan.

Produced via a multi-agent research workflow; the spine agent died on a transient API error and was re-run as a focused follow-up with its decomp anchors verified against source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:04:57 +02:00

407 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

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