diff --git a/docs/research/2026-06-23-paperdoll-slice2-handoff.md b/docs/research/2026-06-23-paperdoll-slice2-handoff.md new file mode 100644 index 00000000..952a6910 --- /dev/null +++ b/docs/research/2026-06-23-paperdoll-slice2-handoff.md @@ -0,0 +1,89 @@ +# 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) + +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::SetCamera` → `CreatureMode::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.