acdream/docs/research/2026-06-23-paperdoll-slice2-handoff.md
Erik c88bc5c8eb docs(D.2b): Slice 2 handoff — paperdoll 3D doll viewport + the Slots toggle
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>
2026-06-23 07:56:54 +02:00

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 + mergedmain 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)

  1. This doc.
  2. 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.
  3. 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.
  4. docs/architecture/code-structure.md Rule 2 — Core defines interfaces, App implements; never the reverse. Governs the IUiViewportRenderer seam.
  5. src/AcDream.App/UI/Layout/PaperdollController.cs — Slice 1's controller (the 21 slot bindings + wield/unwield drag handler you'll extend, not rewrite).
  6. 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 (decomp 0x004a65c0) builds clothingPriorityMask from the mask 0x8007fff (line 176016) — clothing/jewelry/misc; the armor slots are the *Armor EquipMask bits (ChestArmor 0x200 … LowerLegArmor 0x4000). Confirm the exact partition from GetLocationInfoFromElementID (the §3a table) + the toggle handler.
  • READ THE TOGGLE HANDLER FIRST: gmPaperDollUI::ListenToElementMessage (decomp 0x004a5c30, ~line 175593) — the idMessage == 1 branch (button click, ~line 175628+, NOT yet read this session) is the show/hide logic. The init does SetAttribute_Bool(m_SlotCheckbox, 0xe, 0) (line 175585) = start unchecked (doll view). Port the exact SetVisible toggling it does on the armor slots + the viewport.
  • In acdream: PaperdollController keeps refs to the armor slots (toggle-hidden in doll view) + the viewport widget; the button's click flips their Visible. 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 IUiViewportRenderer in AcDream.Core (or the UI.Abstractions layer), implement in AcDream.App (it has GL + EntitySpawnAdapter). The widget's OnDraw only has a 2-D UiRenderContext; the 3-D pass needs a dedicated overlay hook the UiHost/GameWindow invokes for any UiViewport present (NOT inside OnDraw). 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 WorldEntity from the local player's Setup + current ObjDesc, feed it through EntitySpawnAdapter/AnimatedEntityState (palette/part/hidden-part overrides), draw it with a fixed camera + one distant light into the rect. Re-dress on ObjDescEvent 0xF625 (already PARSED) = rebuild the entity's overrides — the C# analog of retail RedressCreature (decomp 0x004a5c90?/line 173990 / 175535).
  • Camera/light/heading immediates (retail gmPaperDollUI::PostInit, decomp ~175509-535; raw hex read this session, decode + VERIFY):
    • SetCamera(viewport, &dir, &pos) with dir ≈ (0x3df5c28f=0.12, 0xc019999a=-2.4, 0x3f6147ae=0.88), pos = (0,0,0). (Arg order pos-vs-dir UNVERIFIED — UIElement_Viewport::SetCameraCreatureMode::SetCameraPosition/Direction.)
    • SetLight(viewport, DISTANT_LIGHT, 2.0f, &dir) with dir ≈ (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 via m_didAnimation (UpdateForRace 0x004a…/line 174129 swaps the idle DID per body-type via DBObj::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 renderable WorldEntity (it's the camera), so LIKELY build a dedicated WorldEntity from 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→EquipMask map. Extend it with the armor/non-armor partition + the viewport ref + the toggle.
  • The mount + import — the gmPaperDollUI subtree (0x21000024) is imported under 0x100001CD in the inventory frame; the viewport element 0x100001D5 is in the tree (currently skipped — DatWidgetFactory Type 12→skip; Type 0xD is NOT registered yet → register it to UiViewport).
  • EntitySpawnAdapter / AnimatedEntityState / WorldEntity — the per-instance animated-character render path (palette/part/hidden-part overrides). The doll's model pipeline.
  • ObjDescEvent 0xF625 parse + CreateObject.ReadModelData (palette/sub-palette/texture/anim-part) — the re-dress input.
  • ClientObjectTable + the wield wire (GetAndWieldItem, WieldObject 0x0023 + ConfirmMove, PickupEvent two-table fix) — all shipped in Slice 1.

Open questions / UNVERIFIED (settle in the brainstorm)

  1. Overlay vs replace — confirmed REPLACE (toggle) per the user, but verify the exact SetVisible set the ListenToElementMessage idMessage==1 handler toggles (armor slots + viewport; do the non-armor slots stay shown in slot-view?).
  2. The IUiViewportRenderer integration 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.
  3. Camera/light immediates — decode + verify the floats above; recover the corrupted 3rd light component from Ghidra.
  4. Player-clone vs fresh WorldEntity — acdream has no player WorldEntity; build one from Setup+ObjDesc. Confirm the data is available client-side.
  5. 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.
  6. 0x100001E0 = MissileAmmo 0x800000 still 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/RedressCreature at 175509-535, m_SlotCheckbox = 0x100005BE at 175557).
  • gmPaperDollUI::ListenToElementMessage (~175593; read the idMessage==1 branch ~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 IUiViewportRenderer seam 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; main is 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 in claude-memory/project_d2b_retail_ui.md (the CORRECTED paperdoll model) and the deep-dive docs/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 — read gmPaperDollUI::ListenToElementMessage's idMessage==1 branch (~decomp 175628+) for the exact SetVisible show/hide of the armor slots + viewport; partition armor vs non-armor slots (armor = the *Armor EquipMask bits; clothing/jewelry/weapon = non-armor, cf. clothingPriorityMask 0x8007fff). Then (B) the 3-D doll UiViewport (register dat Type 0xD, element 0x100001D5): a Core IUiViewportRenderer seam (Code-Structure Rule 2 — Core defines, App implements) driving a scissored single-entity 3-D pass keyed to the widget rect; reuse EntitySpawnAdapter/AnimatedEntityState by building a WorldEntity from the local player's Setup + current ObjDesc, re-dressed on ObjDescEvent 0xF625 (the C# analog of RedressCreature); fixed camera + one distant light + set_heading(191.37°) + idle anim (decode the PostInit immediates ~175509-535 + verify). Extend the shipped PaperdollController — 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 plain dotnet run; if a rebuild is locked, ask the user to close it.