docs(D.5): action bar / inventory / paperdoll research drop

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-16 21:04:57 +02:00
parent 78c91875b8
commit a5c5126e8d
6 changed files with 2077 additions and 0 deletions

View file

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