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