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

28 KiB
Raw Permalink Blame History

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 (EntitySpawnAdapterAnimatedEntityState 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):

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) PARSEDObjDescEvent.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 PARTIALPlayerDescriptionParser.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):
    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 (ObjDescEventRedressCreature)

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 VisualDescthe same ObjDesc apply used for in-world creatures. The ObjDesc fields (ACViewer Entity/ObjDesc.cs:18-54): PaletteID, SubPalettes, TextureChanges, AnimPartChangesall 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). RegisterRegisterElementClass(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 ObjDescEvents), 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 — 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.