acdream/docs/research/2026-06-16-equipment-paperdoll-deep-dive.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

416 lines
28 KiB
Markdown
Raw Permalink 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.

# Equipment / Paperdoll panel — retail-faithful deep-dive
**Date:** 2026-06-16
**Scope:** D.2b "core panels" research phase, the equipment/paperdoll target from
`docs/research/2026-06-16-action-bar-inventory-equipment-handoff.md` §3 Q1 + Q10/Q11/Q12.
**Status:** REPORT-ONLY. No code changed. The deliverable is this doc.
**Panels:** `gmPaperDollUI` (element class `0x10000024`, LayoutDesc `0x21000024`) and
`gm3DItemsUI` (element class `0x10000021`, LayoutDesc `0x21000021`).
## 1. Summary + confidence legend
The retail **paperdoll** (`gmPaperDollUI`) is a **3D character viewport plus ~25
single-cell equip slots**, NOT a 2D doll image. The window's element `0x100001D5`
(Type `13` = `UIElement_Viewport`) hosts a live `CreatureMode` mini-scene; the
character's `CPhysicsObj` is cloned from the player and re-dressed via the SAME
ObjDesc machinery the in-world renderer uses (`DoObjDescChangesFromDefault`). Every
equip slot is a **single-cell `UIElement_ItemList` (class `0x10000031`)**, one per
`EquipMask` location, mapped element-id → coverage-mask by
`gmPaperDollUI::GetLocationInfoFromElementID`. Equipping is the
`GetAndWieldItem` game action (opcode `0x001A`, `item_guid + EquipMask`); the
server's visible reply is `ObjDescEvent` (`0xF625`) which triggers
`RedressCreature`. **acdream already parses `ObjDescEvent` (0xF625) and the full
ObjDesc/ModelData block, and already has a complete per-instance animated-character
render path** (`EntitySpawnAdapter``AnimatedEntityState` with palette/part/hidden-
part overrides). The paperdoll viewport can REUSE that path — the gap is a
**`UiViewport` (Type 0xD) widget** that renders a single entity into a UI rect (a
scissored mini 3D pass), an **equip-slot variant of the item-slot widget**
(`UIElement_ItemList` 0x10000031, single cell), and the **window manager**.
`gm3DItemsUI` (0x21000021) is a SEPARATE "Contents of Backpack" pane (an
`UIElement_ItemList` + a text label + a scrollbar), NOT the doll — it does not host
a viewport.
`gm3DItemsUI` is misnamed for our purposes: despite "3DItems", its `PostInit` wires
a `m_itemList` (`UIElement_ItemList`) and a `m_contentsText` and sets the text to
"Contents of Backpack". It is an inventory contents list, addressed by the inventory
deep-dive; included here only because the handoff paired it with the paperdoll.
**Confidence legend:**
- **CONFIRMED** — quoted from a source I opened (decomp line / file:line).
- **LIKELY** — inferred from confirmed facts; the inference is named.
- **UNVERIFIED** — educated guess; flagged loudly.
**Note on a missing input:** the handoff promised a "spine agent" doc at
`docs/research/2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md` and the
START-HERE memory `claude-memory/project_d2b_retail_ui.md`. **Both are NOT FOUND in
this worktree** (`Glob **/project_d2b_retail_ui.md` and `**/*spine*.md` returned
nothing). I therefore re-derived the icon/item-model claims I needed from primary
sources (decomp + acclient.h + ACE + ACViewer + acdream source) rather than citing a
doc I could not open. Where this overlaps the spine's scope (icon decode, the
`UIElement_UIItem` widget, container model) I keep it terse and defer to the spine
doc once it lands.
## 2. LayoutDesc / element map
### 2a. Paperdoll `gmPaperDollUI` 0x10000024 → LayoutDesc 0x21000024 (224×214)
**CONFIRMED** registration: `gmPaperDollUI::Register` (decomp line 174445):
`UIElement::RegisterElementClass(0x10000024, gmPaperDollUI::Create);`. Pre-dump
`.layout-dumps/paperdoll-0x21000024.txt` root `0x100001D4` is 224×214, Type
`268435492 = 0x10000024` (the gmPaperDollUI class). **CONFIRMED.**
Construction chain: `gmPaperDollUI::gmPaperDollUI` (line 174228) calls
`UIElement_Field::UIElement_Field(this, ...)` — i.e. the paperdoll IS-A Field
subclass (matters for drag-drop: it inherits Field's drop hooks). The slot/viewport
wiring happens in the init routine that calls `GetChildRecursive` per id
(lines 175480-175548) — the analog of a `PostInit`. **CONFIRMED.**
Key elements in 0x21000024 (from the pre-dump + the init routine):
| Element id | dump Type | Resolves to | Role | Anchor (cite) |
|---|---|---|---|---|
| `0x100001D4` | 0x10000024 | gmPaperDollUI (root) | window | dump:13 |
| `0x100001D5` | **13** | `UIElement_Viewport` (0xD) | **the 3D character doll** | dump:125; `m_pPaperDoll = GetChildRecursive(this,0x100001d5)->DynamicCast(0xd)` line 175509-175517 |
| `0x100001D6` | 0 → base 0x100002BF/0x21000080 | `m_paperDollDragMask` | doll click/drag mask region (100×214) | dump:157; line 175538 |
| `0x1000046D` | 0 → base | `m_paperDollDragOverlay` | drag overlay sprite (32×32) | dump:173; line 175539 |
| `0x10000595` | 0 → ItemList | `m_sigilOneItem` (SigilOne 0x10000000) | aetheria sigil slot, hidden by default | line 175540-175542 |
| `0x10000596` | 0 → ItemList | `m_sigilTwoItem` (SigilTwo 0x20000000) | sigil slot | line 175543-175545 |
| `0x10000597` | 0 → ItemList | `m_sigilThreeItem` (SigilThree 0x40000000) | sigil slot | line 175546-175548 |
| `0x100005BE` | 0 → Button base 0x21000044 | a `UIElement_Button` | the close/expand button (120×14) | dump:349; line 175549 |
| ~25 more `0x1000xxxx` ids | **0** → base `0x100001E4` | single-cell `UIElement_ItemList` (0x10000031) | the equip slots (§3) | dump:29-476 |
The shared equip-slot base chain (**CONFIRMED**):
- Each slot element has `Type = 0`, `BaseElement = 268435940 = 0x100001E4`,
`BaseLayoutId = 553648164 = 0x21000024` (dump e.g. lines 33,49,65…).
- Element `0x100001E4` (dump:477) has `Type = 0`, `BaseElement = 268436281 =
0x10000339`, `BaseLayoutId = 553648189 = 0x2100003D`.
- `0x2100003D` root element `0x10000339` (`.layout-dumps/itemlist-0x2100003D.txt:16`)
has `Type = 268435505 = 0x10000031` = `UIElement_ItemList`, 32×32.
⇒ **every paperdoll equip slot resolves (via `ElementReader.Merge` zero-wins-base
Type resolution) to `UIElement_ItemList` 0x10000031, a single 32×32 cell.**
The init routine confirms each is cast to ItemList and registered as a drag target,
e.g. (line 175485-175496):
```
eax_66 = GetChildRecursive(this, 0x100005b2); // LowerLegArmor slot
eax_67 = eax_66->vtable->DynamicCast(0x10000031); // → UIElement_ItemList
this->m_lowerLegSlot = eax_67;
UIElement_ItemList::RegisterItemListDragHandler(eax_67, &this->vtable);
this->m_lowerLegSlot->vtable->SetVisible(0); // hidden until an item lands
```
**CONFIRMED.** Slots default invisible and are shown only when occupied (the empty
slot shows the doll body behind it; an occupied slot shows the item icon).
### 2b. gm3DItemsUI 0x10000021 → LayoutDesc 0x21000021 (234×120) — NOT the doll
**CONFIRMED** registration: `gm3DItemsUI::Register` (line 176723):
`UIElement::RegisterElementClass(0x10000021, gm3DItemsUI::Create);`.
`gm3DItemsUI::PostInit` (line 176728-176745):
```
this->m_contentsText = UIElement::GetChildRecursive(this, 0x100001c5);
eax_1 = UIElement::GetChildRecursive(this, 0x100001c6);
this->m_itemList = eax_1->vtable->DynamicCast(0x10000031); // UIElement_ItemList
... UIElement_Text::SetText(this->m_contentsText, u"Contents of Backpack");
```
Pre-dump `.layout-dumps/items3d-0x21000021.txt`: root `0x100001C4` (234×120, Type
`268435489 = 0x10000021`), child `0x100001C5` (text, base 0x10000436/0x21000077),
child `0x100001C6` (the ItemList grid, base 0x100002B9/0x2100003D — same ItemList
base as the slots), child `0x100001C7` (a scrollbar-shaped 16×96, base
0x100002C7/0x2100003E). **No Viewport element.** ⇒ gm3DItemsUI is a scrollable
**item-contents list**, not a 3D doll. **CONFIRMED.** (The "3D" in the name is
historical; it has no `UIElement_Viewport` and no `CreatureMode`.)
## 3. Equip-slot model + the coverage / location enum
### 3a. The element-id → EquipMask mapping (`GetLocationInfoFromElementID`)
`gmPaperDollUI::GetLocationInfoFromElementID(elementId, out uint mask, out UI_SLOT_SIDE side)`
(decomp line 173620) is a giant switch. It is the SSOT for which slot is which. The
mask values are exactly ACE's `EquipMask` (`ACE/Source/ACE.Entity/Enum/EquipMask.cs`).
**CONFIRMED** — full table below (decomp line / mask / EquipMask name / SLOT_SIDE):
| Element id | mask (hex) | EquipMask name | SLOT_SIDE | decomp line |
|---|---|---|---|---|
| `0x100005AB` | `0x1` | HeadWear | NULL | 173723 |
| `0x100001E2` | `0x2` | ChestWear | NULL | 173688 |
| `0x100001E3` | `0x40` | UpperLegWear | NULL | 173694 |
| `0x100005B0` | `0x20` | HandWear | NULL | 173753 |
| `0x100005B3` | `0x100` | FootWear | NULL | 173771 |
| `0x100005AC` | `0x200` | ChestArmor | NULL | 173729 |
| `0x100005AD` | `0x400` | AbdomenArmor | NULL | 173735 |
| `0x100005AE` | `0x800` | UpperArmArmor | NULL | 173741 |
| `0x100005AF` | `0x1000` | LowerArmArmor | NULL | 173747 |
| `0x100005B1` | `0x2000` | UpperLegArmor | NULL | 173759 |
| `0x100005B2` | `0x4000` | LowerLegArmor | NULL | 173765 |
| `0x100001DA` | `0x8000` | NeckWear | NULL | 173640 |
| `0x100001DB` | `0x10000` | WristWearLeft | LEFT | 173646 |
| `0x100001DD` | `0x20000` | WristWearRight | RIGHT | 173658 |
| `0x100001DC` | `0x40000` | FingerWearLeft | LEFT | 173652 |
| `0x100001DE` | `0x80000` | FingerWearRight | RIGHT | 173664 |
| `0x100001E1` | `0x200000` | Shield | NULL | 173682 |
| `0x100001E0` | `0x800000` | MissileAmmo | NULL | 173676* |
| `0x100001DF` | `0x3500000` | (weapon composite — see 3b) | NULL | 173670 |
| `0x100005E9` | `0x8000000` | Cloak | NULL | 173777 |
| `0x10000595` | `0x10000000` | SigilOne | NULL | 173705 |
| `0x10000596` | `0x20000000` | SigilTwo | NULL | 173711 |
| `0x10000597` | `0x40000000` | SigilThree | NULL | 173717 |
| `0x1000058E` | `0x4000000` | TrinketOne | NULL | 173630 |
\* **`0x100001E0`** — the decomp pseudo-C shows `*arg3 = "activation type (%s)…"`
(a string-pointer artifact where the Binary Ninja lifter lost the immediate). The
preceding/following cases are `0x200000` (Shield) and `0x200000`/`0x40`, and the only
remaining ready-slot mask not otherwise assigned in this switch is `MissileAmmo
(0x00800000)`. So **`0x100001E0` = MissileAmmo `0x800000` (LIKELY** — inferred from
the EquipMask gap + neighbors; the literal value is corrupted in the decomp).
`UI_SLOT_SIDE` (CONFIRMED `acclient.h:4546`): `SLOT_SIDE_NULL=0, SLOT_SIDE_LEFT=1,
SLOT_SIDE_RIGHT=2`. SIDE distinguishes the paired jewelry slots (left/right
wrist + finger) that share the same wear concept but different physical sides.
### 3b. The weapon composite slot `0x3500000`
`0x100001DF → 0x3500000` = `MeleeWeapon(0x100000) | MissileWeapon(0x400000) |
TwoHanded(0x2000000) | Held(0x1000000)` (= `0x3500000`). **CONFIRMED** by bit
decomposition against EquipMask.cs. This is the single "weapon hand" doll slot that
accepts any wieldable weapon. `OnItemListDragOver` has a special case at line 174302:
`if (ecx_3 == 0x200000 && (eax_3 & 0x100000) != 0) eax_3 |= ecx_3;` — i.e. a
melee-capable item may also drop into the Shield(0x200000) slot test. **CONFIRMED.**
### 3c. How the client knows what is equipped — `GetUpperInvObj(mask)`
`gmPaperDollUI::GetUpperInvObj(uint coverageMask)` (line 174565) is how the doll
finds the item currently in a slot:
```
eax = ClientObjMaintSystem::GetWeenieObject(player_id);
eax_3 = ACCWeenieObject::GetInvPlacementList(eax); // PackableList<InventoryPlacement>
for (i = eax_3->head; i; i = i->next) {
if (arg2 & i->data.loc_) // coverageMask & placement.loc_
eax_5 = InventoryPlacement::DetermineHigherPriority(...);
}
return iid; // the equipped item's guid
```
`InventoryPlacement` (**CONFIRMED** `acclient.h:33178`):
```cpp
struct InventoryPlacement : PackObj { uint iid_; uint loc_; uint priority_; };
```
So the player weenie carries a **`PackableList<InventoryPlacement>`** where each
node is `{itemGuid, locationMask (EquipMask), priority}`. `loc_` is the EquipMask
slot; `priority_` resolves overlap (e.g. armor over clothing on the same body part —
this is `CoverageMask` priority, `ACE/Source/ACE.Entity/Enum/CoverageMask.cs`).
**CONFIRMED.** The paperdoll reads this list to populate each slot's icon and to
drive part-selection lighting (`GetSelectionMaskFromObject`, line 174762, maps an
item guid back to which doll body parts to highlight, via the same masks).
**Cross-ref ACE:** `EquipMask` (loc) and `CoverageMask` (priority) are documented in
ACE as "sent as loc / in the priority field of the equipped-items list portion of the
player description event F7B0-0013" (`EquipMask.cs:5-6`, `CoverageMask.cs:6-7`).
**CONFIRMED** — this is the same `InventoryPlacement {iid, loc, priority}` triple the
client stores, populated from PlayerDescription's equipped section.
**acdream parse status of the placement list:** PARTIAL. `PlayerDescriptionParser`
(0x0013) "walks all sections through enchantments; the trailing options / inventory /
**equipped** sections are partial" (`PlayerDescriptionParser.cs:70-77`). So acdream
does NOT yet surface the equipped `InventoryPlacement` list. The per-item equip
*state* is, however, available from `CreateObject`/`ObjDescEvent` ModelData
(palette/part swaps already applied to the model). **CONFIRMED** (parser comment).
## 4. Wield / unwield wire + the ObjDesc change
### 4a. Wire table
| Opcode | Name | Dir | Trigger | ACE handler | Chorizite type | acdream parse status |
|---|---|---|---|---|---|---|
| `0x001A` (GameAction) | GetAndWieldItem | C→S | drop an item onto an equip slot / doll (auto-wield) | `GameActionGetAndWieldItem.Handle` (`Actions/GameActionGetAndWieldItem.cs:7-14`) → `Player.HandleActionGetAndWieldItem(itemGuid, EquipMask)` | `Inventory_GetAndWieldItem` (`C2S/Actions/Inventory_GetAndWieldItem.generated.cs:14-42`: `uint ObjectId; EquipMask Slot`) | **MISSING** (no sender in acdream; `Grep GetAndWieldItem\|0x001A src` finds only the UI font-property 0x1A, unrelated) |
| `0x0019` (GameAction) | PutItemInContainer / move-to-pack (un-wield) | C→S | drag a wielded item back into a pack | ACE `GameActionPutItemInContainer` | `Inventory_PutItemInContainer*` | MISSING (inventory deep-dive scope) |
| `0xF625` | ObjDescEvent | S→C | server applies/removes the wielded item → appearance change | `GameMessageObjDescEvent` ctor → `worldObject.SerializeUpdateModelData` (`Messages/GameMessageObjDescEvent.cs:10-17`) | (ModelData block) | **PARSED** — `ObjDescEvent.cs:33-73` (opcode `0xF625`, `CreateObject.ReadModelData`) |
| `0xF745`/`0x0024` (CreateObject) | CreateObject | S→C | the wielded item object itself arrives | ACE creation message | `Item_CreateObject` | PARSED — `CreateObject.cs` |
| `0xF7B0`/`0x0013` (GameEvent) | PlayerDescription (equipped list) | S→C | full state incl. `InventoryPlacement` equipped section | `GameEventPlayerDescription.WriteEventBody` | `Login_PlayerDescription` | **PARTIAL** — `PlayerDescriptionParser.cs` (equipped section not surfaced) |
Wire payload of `GetAndWieldItem` (**CONFIRMED** both refs agree):
- ACE reads `uint itemGuid; (EquipMask)int32 location` (`GameActionGetAndWieldItem.cs:10-11`).
- Chorizite writes `uint ObjectId; (uint)EquipMask Slot` (`.generated.cs:38-41`).
- holtburger sends `GetAndWieldItem { item_guid, equip_mask }`
(`holtburger-core/src/client/commands.rs:808-814`):
```rust
self.send_game_action(GameAction::GetAndWieldItem(Box::new(
GetAndWieldItemActionData { item_guid: item, equip_mask: target_mask })))
```
with `target_mask` resolved by `resolve_and_clear_slots(item, slot)` (line 799) —
i.e. the client picks the EquipMask for the target slot, exactly like the doll's
`GetLocationInfoFromElementID`. **CONFIRMED.**
`GameActionType.GetAndWieldItem = 0x001A` (**CONFIRMED**
`ACE/Source/ACE.Server/Network/GameAction/GameActionType.cs:14`).
### 4b. The ObjDesc change on the model (`ObjDescEvent` → `RedressCreature`)
Server side: equipping changes the creature's `ObjDesc` (clothing base, sub-palettes,
texture changes, anim-part swaps) and broadcasts `ObjDescEvent (0xF625)` carrying the
FULL new appearance (ACE comment: "It contains the entire description of what they're
wearing", `GameMessageObjDescEvent.cs:6-9`).
Client side: `gmPaperDollUI::RecvNotice_PlayerObjDescChanged` (line 174324) tail-calls
`gmPaperDollUI::RedressCreature` (line 173990). **CONFIRMED.** RedressCreature:
```
if (m_pInventoryObject == 0 && smartbox->player != 0) { // first time:
eax_5 = CPhysicsObj::makeObject(GetPhysicsObject(player_id)); // clone player obj
this->m_pInventoryObject = eax_5;
CPhysicsObj::set_heading(eax_5, 191.367905f, 1); // face ~191° (toward viewer)
CPhysicsObj::set_sequence_animation(m_pInventoryObject, m_didAnimation.id, 1, 1, 0);
CreatureMode::AddObject(&m_pPaperDoll->creature_mode_objects, m_pInventoryObject);
}
visualDesc = SmartBox::get_player_visualdesc(smartbox);
CPhysicsObj::DoObjDescChangesFromDefault(this->m_pInventoryObject, visualDesc); // re-dress
```
**CONFIRMED** (lines 173997-174012). So the doll is a CLONE of the player's
`CPhysicsObj`, and re-dressing is `CPhysicsObj::DoObjDescChangesFromDefault` applied
to the cloned object using the player's current `VisualDesc` — **the same ObjDesc
apply used for in-world creatures**. The ObjDesc fields (ACViewer
`Entity/ObjDesc.cs:18-54`): `PaletteID`, `SubPalettes`, `TextureChanges`,
`AnimPartChanges` — **all four already parsed by acdream's `CreateObject.ReadModelData`
/ `ObjDescEvent`** (`CreateObject.cs:652-679`: subPalette/textureChange/animPartChange
counts + entries). **CONFIRMED.**
## 5. Paperdoll 3D rendering + reuse analysis
### 5a. It is a 3D viewport, not a 2D image
**CONFIRMED.** The doll is `UIElement_Viewport` (Type `0xD`), element `0x100001D5`.
`UIElement_Viewport::Create` (line 119029-119037) allocates the element + a
`CreatureMode` sub-object at `+0x5f0`; `PostInit` calls
`CreatureMode::InitializeScene` (line 119084). `SetCamera` forwards to
`CreatureMode::SetCameraPosition/Direction` (line 119089-119094). `Register` ⇒
`RegisterElementClass(0xd, …)` (line 119126). So a Viewport is a mini 3D scene
embedded in a UI rect, with its own camera, lights, and an object list.
The paperdoll init (line 175517-175535) does, once:
```
m_pPaperDoll = GetChildRecursive(this, 0x100001d5)->DynamicCast(0xd); // the viewport
UIElement_Viewport::SetCamera(m_pPaperDoll, &dir, &pos); // pos/dir vec3s
UIElement_Viewport::SetLight(m_pPaperDoll, DISTANT_LIGHT, 2.0, &dir); // one distant light
CreatureMode::UseSharpMode(&m_pPaperDoll->creature_mode_objects); // sharper mip bias
gmPaperDollUI::RedressCreature(this); // build + dress the doll
```
**CONFIRMED.** `UpdateForRace` (line 174129) re-points the camera per body-type
(case 6/7/8/9/0xC/0xD = the playable races/genders) and swaps `m_didAnimation` (the
idle pose DID) via `DBObj::GetDIDByEnum`. **CONFIRMED.**
### 5b. The viewport render loop (`CreatureMode::Render`)
`CreatureMode::Render` (line 91665) is the per-frame doll draw. Walk-through
(**CONFIRMED** lines 91665-91776):
1. Enter "creature mode" (disables world LOD degrade so the doll is full detail).
2. For each object in `creature_mode_objects`: `CPhysicsObj::update_position` (advance
the idle animation).
3. Set ambient color, sunlight, FOV (`Render::SetFOVRad`), push a frame.
4. `Render::update_viewpoint(&creature_view_frame)`, `set_default_view()`.
5. `RenderDevice::DrawObjCellForDummies(creature_cell)` — draw the object's private
cell, then `D3DPolyRender::FlushAlphaList`.
i.e. the doll lives in its own tiny `creature_cell`, lit by one distant light, drawn
with a dedicated camera into the viewport rect. `CreatureMode::AddObject` (line 94374)
adds the cloned `CPhysicsObj` to that cell:
`CPhysicsObj::AddObjectToSingleCell(obj, creature_cell); SetPlacementFrame(obj,0,1);`.
**CONFIRMED.**
### 5c. Can acdream REUSE its existing character render path? — YES
**acdream already renders animated, equipped characters in-world.** The per-instance
path is `EntitySpawnAdapter` (`src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs`):
- `OnCreate(WorldEntity)` builds an `AnimatedEntityState(sequencer)` and applies
`entity.HiddenPartsMask`, every `entity.PartOverrides` (`SetPartOverride(partIndex,
gfxObjId)` — weapons/clothing/helmets that replace the Setup default), and
pre-warms per-instance palette/texture decode via
`GetOrUploadWithPaletteOverride(surfaceId, texOverride, paletteOverride)`.
**CONFIRMED** `EntitySpawnAdapter.cs:100-168`.
- `WorldEntity` carries `SourceGfxObjOrSetupId`, `MeshRefs`, `PaletteOverride`,
`PartOverrides` (`record struct PartOverride(byte PartIndex, uint GfxObjId)`), and
`HiddenPartsMask`. **CONFIRMED** `WorldEntity.cs:14,28,37,97,104,213`.
This is the EXACT data a re-dress produces: ObjDesc → base palette + sub-palettes
(`PaletteOverride`), texture changes (`SurfaceOverrides`), anim-part swaps
(`PartOverrides`). acdream already turns an `ObjDescEvent`/`CreateObject` ModelData
into these fields. **So the paperdoll doll = "take the local player's WorldEntity (or
a clone of it), feed it through the existing animated-character pipeline, and draw it
with a fixed camera + one distant light into a UI rect."** This is the C# analog of
`makeObject(player) + DoObjDescChangesFromDefault + CreatureMode::Render`.
### 5d. What a `UiViewport` (Type 0xD) widget needs to host the 3D render
The toolkit's `UiRenderContext` is a **2D** sprite/text submission bucket (see
`UiElement.OnDraw(UiRenderContext)`). A 3D model render cannot go through it. A
`UiViewport` widget therefore needs (LIKELY design — flagged):
1. **A render-into-rect hook.** The widget's screen rect (`ScreenPosition` +
Width/Height) defines a GL scissor + viewport. A 3D pass renders the single entity
there, AFTER the world pass and BEFORE/INTERLEAVED with the 2D UI pass. The cleanest
seam is a dedicated overlay callback the `UiHost`/`GameWindow` invokes for any
`UiViewport` present, NOT a draw inside `OnDraw` (which only has a 2D context).
**UNVERIFIED** — the exact integration point (a new `IUiViewportRenderer` Core
interface implemented in App, per Code-Structure Rule 2) is a design call for the
brainstorm/spec phase, not yet decided.
2. **A private mini-scene** mirroring `CreatureMode`: one entity (`AnimatedEntityState`
for the player clone), a fixed camera (position/direction vec3 like
`SetCamera`, e.g. the retail values `dir.z=0.12, pos=(~-2.4, ~0.88)` floats from
`UpdateForRace` — see the `0x3df5c28f / 0xc019999a / 0x3f6147ae` immediates at line
175524-175526, which are little-endian floats ≈ 0.12, 2.4, 0.88; **LIKELY** —
I read the hex but did not byte-convert each), one distant light, and an idle
animation playing on the sequencer.
3. **A heading toward the viewer** (`set_heading(191.37°)`, line 174001) and optional
click-drag rotation (the doll spins under the mouse — that's
`m_paperDollDragMask`/`CreateClickMap`, line 174636; **part-selection lighting** for
"which armor piece is this?" highlight uses `ApplyPartSelectionLighting`, line
174034, but that is a polish feature, not MVP).
4. **Reuse `EntitySpawnAdapter`'s state** — feed it the player's `WorldEntity` so the
doll automatically reflects equip changes when `ObjDescEvent` updates the player's
ModelData. The re-dress is then "rebuild the player WorldEntity's PartOverrides/
PaletteOverride from the new ObjDesc and refresh the viewport's entity state" — the
C# analog of `RedressCreature`.
This is the single biggest new piece. The 3D machinery exists; the work is the
**UI↔3D bridge** (a scissored single-entity pass driven by a UI rect).
## 6. New toolkit widgets this introduces
| Widget (proposed) | dat Type it registers at | leaf vs container | Purpose |
|---|---|---|---|
| **`UiViewport`** | **0xD** (`UIElement_Viewport`, reg line 119126) | **leaf** (`ConsumesDatChildren => true`) | Hosts a single 3D entity (the paperdoll character clone) rendered into the widget's screen rect via a scissored mini 3D pass. Owns a fixed camera + one distant light + an `AnimatedEntityState`; reuses `EntitySpawnAdapter`/`AnimatedEntityState` for the model. Needs a new render-into-rect seam (a Core `IUiViewportRenderer` interface implemented in App). **The biggest new piece.** |
| **`UiItemSlot`** (equip-slot variant of the shared item-slot) | **0x10000031** (`UIElement_ItemList`, single 32×32 cell) | **leaf** (`ConsumesDatChildren => true`) | One equip slot. Renders the equipped item's icon (from the weenie `IconDataID`), is a drag-drop target keyed to its `EquipMask` (from `GetLocationInfoFromElementID`), shows/hides per occupancy. NOTE: this is the single-cell case of the shared `UIElement_UIItem`/`UIElement_ItemList` spine widget — the equipment panel is a fixed grid of ~25 of these, one per EquipMask, NOT a scrollable list. **Defer the shared icon/drag mechanics to the spine doc** (`2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md`, NOT FOUND yet); this panel only adds the EquipMask binding + the fixed-position-per-slot layout. |
| **Window manager** (shared, not paperdoll-specific) | n/a (uses Dragbar Type 2 / Resizebar Type 9 already present on chrome) | n/a | Open/close/z-order/persist for the paperdoll window. `UiElement.Draggable/Resizable` already exist; the manager wires them + persistence. Shared with inventory/toolbar — same item the handoff §2 calls "the other deferred Plan-2 piece". |
`gm3DItemsUI`'s pane reuses `UiItemSlot`/the spine `UiItemList` + a `UiScrollbar`
(Type 0xB, already built) + a `UiText` (already built) — no NEW widget. It is an
inventory-contents list (inventory deep-dive scope), not a doll.
## 7. Open questions / UNVERIFIED
- **`0x100001E0` = MissileAmmo `0x800000`** — LIKELY (the decomp immediate is
corrupted to a string pointer at line 173676; inferred from the EquipMask gap +
neighbors). Re-dump element `0x100001E0`'s position vs the ammo doll slot, or
re-decompile `0x004a388a` in Ghidra to recover the real immediate, to confirm.
- **The exact viewport camera/light immediates** (lines 175524-175526, 174144-174146)
— I read the hex but did not byte-convert all of them to floats; the paperdoll
brainstorm should decode `0x3df5c28f≈0.12`, `0xc019999a≈2.4`, `0xc0400000=3.0`,
`0xc059999a≈3.4`, `0x3f6147ae≈0.88`, `0x3f800000=1.0` precisely for a faithful
framing. **UNVERIFIED.**
- **The UI↔3D render seam** (how a UI rect drives a scissored single-entity 3D pass,
and whether it draws after the world pass or as a UI overlay) — DESIGN-OPEN, to be
settled in brainstorm. Code-Structure Rule 2 means the seam is a Core interface
implemented in App. **UNVERIFIED.**
- **acdream's PlayerDescription equipped section** is not surfaced
(`PlayerDescriptionParser.cs:70-77`). To populate slot icons at login (vs only
reacting to later `ObjDescEvent`s), the parser must be extended to read the
`InventoryPlacement` equipped list. Filed as a dependency, not yet an issue.
- **Whether the doll clones the player `WorldEntity` or builds a fresh one** — retail
clones the player `CPhysicsObj` (`makeObject(GetPhysicsObject(player_id))`, line
173999). acdream has no player `CPhysicsObj`-as-renderable today (the local player
isn't a `WorldEntity` in the per-instance adapter — it's the camera). LIKELY the
paperdoll builds a dedicated `WorldEntity` from the local player's
Setup+ObjDesc and feeds it to a private `EntitySpawnAdapter`-like host. **UNVERIFIED.**
- **`gm3DItemsUI` true role** — its `m_itemList` + "Contents of Backpack" text is
CONFIRMED, but whether retail ever shows 3D item models in it (the name suggests a
historical 3D-preview) — NOT FOUND any Viewport in its layout; treated as a 2D
contents list. If a 3D item preview surfaces elsewhere, revisit.
## 8. MEMORY.md index line
- [Equipment/Paperdoll panel deep-dive](research/2026-06-16-equipment-paperdoll-deep-dive.md) — gmPaperDollUI 0x10000024/LayoutDesc 0x21000024: doll = UIElement_Viewport (Type 0xD, elem 0x100001D5) hosting a CreatureMode clone re-dressed via DoObjDescChangesFromDefault; ~25 equip slots are single-cell UIElement_ItemList (0x10000031) mapped element-id→EquipMask by GetLocationInfoFromElementID; wield = GetAndWieldItem (0x001A, item+EquipMask, acdream-MISSING), appearance reply = ObjDescEvent 0xF625 (acdream-PARSED) → RedressCreature; acdream's EntitySpawnAdapter/AnimatedEntityState char path is reusable; new widgets = UiViewport (0xD, the UI↔3D bridge), UiItemSlot (0x10000031), window manager. gm3DItemsUI 0x21000021 is a "Contents of Backpack" list, NOT the doll.