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