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>
391 lines
29 KiB
Markdown
391 lines
29 KiB
Markdown
# 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.
|