Carries the corrected paperdoll model forward (figure = live doll, NOT silhouettes; the Slots button toggles doll-view vs armor-slot-view — REPLACE, not overlay). Scopes Slice 2 = (A) the toggle (read ListenToElementMessage idMessage==1) + (B) the UiViewport Type 0xD doll via a Core->App IUiViewportRenderer seam reusing EntitySpawnAdapter. Includes the decomp anchors, camera/light immediates to decode, open questions, and the new-session prompt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
13 KiB
Handoff — D.2b Sub-phase C, Slice 2: the paperdoll 3-D doll + the "Slots" toggle
Date: 2026-06-23 (end of the Slice 1 session)
Branch: claude/hopeful-maxwell-214a12. Slice 1 SHIPPED + merged — main is fast-forwarded to the branch tip (main == branch at 6897148). Slice 2 continues on this branch.
Status: SCOPED, not started. Run the full brainstorm → spec → plan → subagent-driven → visual-gate flow.
Line numbers drift — grep the symbol.
Read first (in this order)
- This doc.
claude-memory/project_d2b_retail_ui.md— the D.2b SSOT. The "SLICE 1 … SHIPPED 2026-06-23" entry holds the CORRECTED paperdoll model + the full DO-NOT-RETRY. It supersedes the visual/look claims in the older docs below.docs/research/2026-06-16-equipment-paperdoll-deep-dive.md— the AUTHORITATIVE element-id / wire / viewport research: §5 (the viewport mechanism, CONFIRMED), §5c (reuse analysis), §5d + §7 (camera/light immediates + the UNVERIFIED list). ⚠ Caveat: its §2a "empty slot shows the doll body behind it" and the 2026-06-22 handoff's "transparent / per-slot silhouettes" framing were WRONG about the LOOK — see the corrected model below. The element ids, opcodes, and viewport plumbing in it are still good.docs/architecture/code-structure.mdRule 2 — Core defines interfaces, App implements; never the reverse. Governs theIUiViewportRendererseam.src/AcDream.App/UI/Layout/PaperdollController.cs— Slice 1's controller (the 21 slot bindings + wield/unwield drag handler you'll extend, not rewrite).memory/reference_modern_rendering_pipeline.md+EntitySpawnAdapter/AnimatedEntityState— the in-world animated-character path the doll reuses.
The CORRECTED paperdoll model (USER AXIOM — confirmed against his retail screenshots, 2026-06-23)
There are NO per-slot silhouette sprites. The "figure" in the paperdoll IS the live 3-D character (the doll — it can be naked if nothing is equipped). The panel has two views toggled by the "Slots" button (0x100005BE = m_SlotCheckbox, a UIElement_Button):
- Button OFF (default) — "doll view": show the 3-D character + the non-armor slots (under-clothes / weapon / wand / shield / jewelry). The armor slots are hidden.
- Button ON — "slot view": the character disappears and the armor slots become visible (the grid).
It is a TOGGLE (mutually exclusive: doll XOR armor-slots), NOT slots-overlaid-on-the-doll. Verify the exact show/hide split against the decomp (below) + the user's screenshots before coding — do not re-derive the look from the old deep-dive prose.
What Slice 1 already did (don't redo): bound all ~21 equip slots (PaperdollController, element-id→EquipMask map), wield (GetAndWieldItem 0x001A, optimistic + rollback) + unwield (the inventory-grid path + the PickupEvent two-table fix), and gave empty slots a visible frame (emptySlotSprite = 0x06004D20) so all positions are usable. Slice 1 shows all slots at once (no toggle, no doll). Slice 2 adds the doll + the toggle on top.
Slice 2 — two pieces (each its own spec → plan → implement; the viewport is the crux)
A. The "Slots" toggle (the lighter piece — do FIRST, it de-risks the layout)
Wire the m_SlotCheckbox button (0x100005BE) to switch between doll-view and slot-view:
- Map each slot as ARMOR vs NON-ARMOR. Retail's
RemakeCharacterInventory(decomp0x004a65c0) buildsclothingPriorityMaskfrom the mask0x8007fff(line 176016) — clothing/jewelry/misc; the armor slots are the*ArmorEquipMaskbits (ChestArmor 0x200 … LowerLegArmor 0x4000). Confirm the exact partition fromGetLocationInfoFromElementID(the §3a table) + the toggle handler. - READ THE TOGGLE HANDLER FIRST:
gmPaperDollUI::ListenToElementMessage(decomp0x004a5c30, ~line 175593) — theidMessage == 1branch (button click, ~line 175628+, NOT yet read this session) is the show/hide logic. The init doesSetAttribute_Bool(m_SlotCheckbox, 0xe, 0)(line 175585) = start unchecked (doll view). Port the exactSetVisibletoggling it does on the armor slots + the viewport. - In acdream:
PaperdollControllerkeeps refs to the armor slots (toggle-hidden in doll view) + the viewport widget; the button's click flips theirVisible. The non-armor slots stay visible in both views (verify). - Slice-1 caveat to revisit: AP-66 (empty slots show a frame) — decide whether the frame stays in slot-view (likely yes) and what an empty NON-ARMOR slot shows in doll-view. The "frames flip to transparent / doll-through" idea in AP-66 was written under the overlay assumption; the TOGGLE model means there is no doll behind the armor slots in slot-view, so the frame likely stays. Re-word AP-66 once the toggle behavior is confirmed.
B. The 3-D doll UiViewport (the heavy piece — the UI↔3D bridge, the real crux)
A UiViewport widget (registers at dat Type 0xD, element 0x100001D5) that renders a re-dressed clone of the local player into the widget's screen rect (a scissored single-entity 3-D pass), shown in doll-view.
- The Core→App seam (Code-Structure Rule 2): define
IUiViewportRendererinAcDream.Core(or the UI.Abstractions layer), implement inAcDream.App(it has GL +EntitySpawnAdapter). The widget'sOnDrawonly has a 2-DUiRenderContext; the 3-D pass needs a dedicated overlay hook theUiHost/GameWindowinvokes for anyUiViewportpresent (NOT insideOnDraw). The exact integration point (after the world pass vs a UI overlay) is DESIGN-OPEN — settle it in the brainstorm (deep-dive §5d/§7). - Reuse the existing char path (deep-dive §5c, CONFIRMED): build a
WorldEntityfrom the local player's Setup + current ObjDesc, feed it throughEntitySpawnAdapter/AnimatedEntityState(palette/part/hidden-part overrides), draw it with a fixed camera + one distant light into the rect. Re-dress onObjDescEvent 0xF625(already PARSED) = rebuild the entity's overrides — the C# analog of retailRedressCreature(decomp0x004a5c90?/line 173990 / 175535). - Camera/light/heading immediates (retail
gmPaperDollUI::PostInit, decomp ~175509-535; raw hex read this session, decode + VERIFY):SetCamera(viewport, &dir, &pos)withdir ≈ (0x3df5c28f=0.12, 0xc019999a=-2.4, 0x3f6147ae=0.88),pos = (0,0,0). (Arg order pos-vs-dir UNVERIFIED —UIElement_Viewport::SetCamera→CreatureMode::SetCameraPosition/Direction.)SetLight(viewport, DISTANT_LIGHT, 2.0f, &dir)withdir ≈ (0x3e99999a=0.3, 0x3ff33333=1.9, <3rd component is a strncpy/lifter artifact — recover from Ghidra/re-decompile>).set_heading(191.367905°)so the doll faces the viewer; idle animation viam_didAnimation(UpdateForRace0x004a…/line 174129 swaps the idle DID per body-type viaDBObj::GetDIDByEnum);CreatureMode::UseSharpMode(sharper mip bias).
- Player-clone vs fresh WorldEntity (UNVERIFIED, deep-dive §7): retail clones the player
CPhysicsObj(makeObject(GetPhysicsObject(player_id))). acdream's local player is NOT a renderableWorldEntity(it's the camera), so LIKELY build a dedicatedWorldEntityfrom the player's Setup + ObjDesc and host it in a private viewport scene. Confirm the player's Setup id + current ObjDesc are available client-side (PlayerDescription / CreateObject / the equipped ObjDescEvents). - Polish, NOT MVP: part-selection lighting (
ApplyPartSelectionLighting/GetSelectionMaskFromObject/CreateClickMap, lines 174034/174762/174636) — the "which armor piece is this?" highlight + the doll click-map; the Aetheria sigil slots (0x10000595/96/97,SetVisible(0)by default). Defer.
What's already in place (reuse, don't rebuild)
PaperdollController— the slot bindings + wield/unwield + the element-id→EquipMaskmap. Extend it with the armor/non-armor partition + the viewport ref + the toggle.- The mount + import — the gmPaperDollUI subtree (
0x21000024) is imported under0x100001CDin the inventory frame; the viewport element0x100001D5is in the tree (currently skipped —DatWidgetFactoryType 12→skip; Type0xDis NOT registered yet → register it toUiViewport). EntitySpawnAdapter/AnimatedEntityState/WorldEntity— the per-instance animated-character render path (palette/part/hidden-part overrides). The doll's model pipeline.ObjDescEvent 0xF625parse +CreateObject.ReadModelData(palette/sub-palette/texture/anim-part) — the re-dress input.ClientObjectTable+ the wield wire (GetAndWieldItem,WieldObject 0x0023+ConfirmMove,PickupEventtwo-table fix) — all shipped in Slice 1.
Open questions / UNVERIFIED (settle in the brainstorm)
- Overlay vs replace — confirmed REPLACE (toggle) per the user, but verify the exact
SetVisibleset theListenToElementMessage idMessage==1handler toggles (armor slots + viewport; do the non-armor slots stay shown in slot-view?). - The
IUiViewportRendererintegration point — after the world pass, or a dedicated UI-overlay 3-D pass? Scissor + viewport from the widget's screen rect. Code-Structure Rule 2 = Core interface, App impl. - Camera/light immediates — decode + verify the floats above; recover the corrupted 3rd light component from Ghidra.
- Player-clone vs fresh
WorldEntity— acdream has no playerWorldEntity; build one from Setup+ObjDesc. Confirm the data is available client-side. - AP-66 fate — does the empty-slot frame stay in slot-view (likely) or change? Re-word the register row once the toggle behavior is confirmed.
0x100001E0 = MissileAmmo 0x800000still LIKELY (AP-62) — gate-verify the ammo slot.
Decomp anchors (named-retail acclient_2013_pseudo_c.txt)
gmPaperDollUI::PostInit(~175232; the slot/viewport/button init I read this session —SetVisible(0)per slot,SetCamera/SetLight/UseSharpMode/RedressCreatureat 175509-535,m_SlotCheckbox = 0x100005BEat 175557).gmPaperDollUI::ListenToElementMessage(~175593; read theidMessage==1branch ~175628+ for the toggle).gmPaperDollUI::RedressCreature(173990 / 175535; clone +set_heading(191.37°)+set_sequence_animation+DoObjDescChangesFromDefault).gmPaperDollUI::UpdateForRace(174129; per-body-type camera + idle DID).gmPaperDollUI::RemakeCharacterInventory(175983) +SetUIItemIntoLocation(175713/175950; populate equipped items — Slice 1 does the equivalent).UIElement_Viewport::Create/SetCamera/SetLight/PostInit+CreatureMode::Render(deep-dive §5a/§5b, lines 119029/91665).
Acceptance (Slice 2)
- The "Slots" button toggles: doll-view (3-D character + non-armor slots) ↔ slot-view (armor slots). Matches the user's two screenshots.
- The doll renders the re-dressed local player (correct race/gender/equipped gear; naked if nothing equipped), faces the viewer, idles; updates live on equip/unequip via
ObjDescEvent 0xF625. - Build + full suite green; visual gate (user compares to retail). Divergence rows for any approximation; the
IUiViewportRendererseam respects Code-Structure Rule 2.
New-session prompt
Continue acdream's D.2b retail-UI inventory arc on branch
claude/hopeful-maxwell-214a12(Slice 1 shipped;mainis ff-merged to the tip). READ FIRST:docs/research/2026-06-23-paperdoll-slice2-handoff.md, then its "Read first" list — especially the Slice 1 entry inclaude-memory/project_d2b_retail_ui.md(the CORRECTED paperdoll model) and the deep-divedocs/research/2026-06-16-equipment-paperdoll-deep-dive.md§5.NEXT: Sub-phase C — the paperdoll, Slice 2. The corrected model (user axiom): the paperdoll "figure" IS the live 3-D character (not silhouettes); the "Slots" button (
0x100005BE) TOGGLES between doll-view (3-D character + non-armor slots) and slot-view (armor slots) — mutually exclusive, NOT overlaid. Build (A) the toggle first — readgmPaperDollUI::ListenToElementMessage'sidMessage==1branch (~decomp 175628+) for the exactSetVisibleshow/hide of the armor slots + viewport; partition armor vs non-armor slots (armor = the*ArmorEquipMask bits; clothing/jewelry/weapon = non-armor, cf.clothingPriorityMask0x8007fff). Then (B) the 3-D dollUiViewport(register dat Type0xD, element0x100001D5): a CoreIUiViewportRendererseam (Code-Structure Rule 2 — Core defines, App implements) driving a scissored single-entity 3-D pass keyed to the widget rect; reuseEntitySpawnAdapter/AnimatedEntityStateby building aWorldEntityfrom the local player's Setup + current ObjDesc, re-dressed onObjDescEvent 0xF625(the C# analog ofRedressCreature); fixed camera + one distant light +set_heading(191.37°)+ idle anim (decode thePostInitimmediates ~175509-535 + verify). Extend the shippedPaperdollController— do NOT rewrite the slot bindings/wield/unwield. The UI↔3-D integration point is DESIGN-OPEN — settle it in the brainstorm. Run the full brainstorm → spec → plan → subagent-driven → visual-gate flow. DO NOT auto-kill the running client — launch with plaindotnet run; if a rebuild is locked, ask the user to close it.