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>
28 KiB
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) hasType = 0,BaseElement = 268436281 = 0x10000339,BaseLayoutId = 553648189 = 0x2100003D. 0x2100003Droot element0x10000339(.layout-dumps/itemlist-0x2100003D.txt:16) hasType = 268435505 = 0x10000031=UIElement_ItemList, 32×32. ⇒ every paperdoll equip slot resolves (viaElementReader.Mergezero-wins-base Type resolution) toUIElement_ItemList0x10000031, 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):
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):
withself.send_game_action(GameAction::GetAndWieldItem(Box::new( GetAndWieldItemActionData { item_guid: item, equip_mask: target_mask })))target_maskresolved byresolve_and_clear_slots(item, slot)(line 799) — i.e. the client picks the EquipMask for the target slot, exactly like the doll'sGetLocationInfoFromElementID. 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):
- Enter "creature mode" (disables world LOD degrade so the doll is full detail).
- For each object in
creature_mode_objects:CPhysicsObj::update_position(advance the idle animation). - Set ambient color, sunlight, FOV (
Render::SetFOVRad), push a frame. Render::update_viewpoint(&creature_view_frame),set_default_view().RenderDevice::DrawObjCellForDummies(creature_cell)— draw the object's private cell, thenD3DPolyRender::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 anAnimatedEntityState(sequencer)and appliesentity.HiddenPartsMask, everyentity.PartOverrides(SetPartOverride(partIndex, gfxObjId)— weapons/clothing/helmets that replace the Setup default), and pre-warms per-instance palette/texture decode viaGetOrUploadWithPaletteOverride(surfaceId, texOverride, paletteOverride). CONFIRMEDEntitySpawnAdapter.cs:100-168.WorldEntitycarriesSourceGfxObjOrSetupId,MeshRefs,PaletteOverride,PartOverrides(record struct PartOverride(byte PartIndex, uint GfxObjId)), andHiddenPartsMask. CONFIRMEDWorldEntity.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):
- 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 theUiHost/GameWindowinvokes for anyUiViewportpresent, NOT a draw insideOnDraw(which only has a 2D context). UNVERIFIED — the exact integration point (a newIUiViewportRendererCore interface implemented in App, per Code-Structure Rule 2) is a design call for the brainstorm/spec phase, not yet decided. - A private mini-scene mirroring
CreatureMode: one entity (AnimatedEntityStatefor the player clone), a fixed camera (position/direction vec3 likeSetCamera, e.g. the retail valuesdir.z=0.12, pos=(~-2.4, ~0.88)floats fromUpdateForRace— see the0x3df5c28f / 0xc019999a / 0x3f6147aeimmediates 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. - A heading toward the viewer (
set_heading(191.37°), line 174001) and optional click-drag rotation (the doll spins under the mouse — that'sm_paperDollDragMask/CreateClickMap, line 174636; part-selection lighting for "which armor piece is this?" highlight usesApplyPartSelectionLighting, line 174034, but that is a polish feature, not MVP). - Reuse
EntitySpawnAdapter's state — feed it the player'sWorldEntityso the doll automatically reflects equip changes whenObjDescEventupdates 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 ofRedressCreature.
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= MissileAmmo0x800000— LIKELY (the decomp immediate is corrupted to a string pointer at line 173676; inferred from the EquipMask gap + neighbors). Re-dump element0x100001E0's position vs the ammo doll slot, or re-decompile0x004a388ain 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.0precisely 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 laterObjDescEvents), the parser must be extended to read theInventoryPlacementequipped list. Filed as a dependency, not yet an issue. - Whether the doll clones the player
WorldEntityor builds a fresh one — retail clones the playerCPhysicsObj(makeObject(GetPhysicsObject(player_id)), line 173999). acdream has no playerCPhysicsObj-as-renderable today (the local player isn't aWorldEntityin the per-instance adapter — it's the camera). LIKELY the paperdoll builds a dedicatedWorldEntityfrom the local player's Setup+ObjDesc and feeds it to a privateEntitySpawnAdapter-like host. UNVERIFIED. gm3DItemsUItrue role — itsm_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 — 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.