fix(D.2b): Slice 2 — retail-exact doll pose, camera + heading (visual gate)

The paperdoll doll now matches retail: correct held pose, framing, and
facing. Three decomp-sourced fixes closed the visual gate.

Pose: cdb-confirmed m_didAnimation = 0x030003C0 (gmPaperDollUI), played
once + HELD (set_sequence_animation framerate=0, RedressCreature
0x004a3c22). Dumping the dat showed the 29-frame anim has only two
distinct keyframes — frame 0 (transitional, bent arm) and frames 1..28
(byte-identical: the settled stance, arms down + leg back) — so
ApplyPaperdollPose applies the LAST frame statically (no looping).

Camera: ported verbatim from UIElement_Viewport::SetCamera (decomp
0x004a5a39). position (0.12,-2.4,0.88); direction (0,0,0) => IDENTITY
view frame => look straight down +Y, ZERO yaw; FOV pi/4 (CreatureMode
ctor default 0x004543cf); ambient 0.3. The prior hand-tune aimed the
camera at mid-body, adding a ~2deg yaw that turned the doll's face away
— full-body framing comes from eye-height + FOV, not aiming.

Heading: retail Frame::set_heading(h) (0x00535e40) builds facing
(sin h, cos h); System.Numerics CreateFromAxisAngle(+Z, +h) rotates the
body's default +Y forward to (-sin h, cos h) — the X-lean was MIRRORED
(~22deg), the real cause of the turned-away face. Negate the angle to
land on retail's facing.

Wrap-up: stripped the temporary O/P pose-frame stepper + Slice2
diagnostics; divergence register AP-66 reworded, AP-67 (RTT doll render
vs in-cell CreatureMode::Render) + AP-68 (per-race UpdateForRace
unimpl) added; DollCameraTests pinned to the retail values + a zero-yaw
guard. tools/cdb/paperdoll-pose.cdb = the pose-DID capture script.
Build + full suite green (Core 1579 / Core.Net 343 / App 597 / UI 425).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-25 13:16:27 +02:00
parent 594942f127
commit 8fa66c23d5
8 changed files with 136 additions and 37 deletions

View file

@ -164,7 +164,9 @@ accepted-divergence entries (#96, #49, #50).
| AP-63 | **Dual-wield-into-shield-slot special not implemented** — retail `OnItemListDragOver` (decomp 174302) lets a melee-capable item also drop on the Shield slot; acdream's `wieldMask = ValidLocations & slotMask` rejects it. | `src/AcDream.App/UI/Layout/PaperdollController.cs` (`HandleDropRelease`) | A dual-wielder cannot off-hand a melee weapon via the doll. | Dual-wield via drag-onto-shield-slot is blocked — functional gap for dual-wield characters. | `gmPaperDollUI::OnItemListDragOver` decomp 174302 |
| AP-64 | **Wield-reject rollback assumes `InventoryServerSaveFailed 0x00A0`** — an optimistic wield rolls back only if ACE emits `0x00A0` for a `GetAndWieldItem` rejection; otherwise corrected by the next authoritative message. Gate-verify via WireMCP. | `src/AcDream.Core.Net/GameEventWiring.cs` (0x00A0 handler) | If ACE uses a different reject opcode for wield, the optimistic state is left dangling until the next full update. | Rejected wield briefly shows the item as equipped in the doll — cosmetic flicker or stuck state if `0x00A0` is not the rejection path. | `InventoryServerSaveFailed 0x00A0`; WireMCP gate-verify |
| AP-65 | **PickupEvent (0xF74A) no longer evicts the weenie from `ClientObjectTable`** — only `DeleteObject (0xF747)` evicts, matching the retail `object_table`-vs-`weenie_object_table` split. An item another player picks up near you (only ever gets `PickupEvent`, never `DeleteObject`) lingers as a data-only entry (`ContainerId 0`, not in any view) until teleport/relog `Clear()`. | `src/AcDream.Core.Net/ObjectTableWiring.cs` (EntityDeleted handler); `src/AcDream.Core.Net/WorldSession.cs` (PickupEvent branch) | The weenie entry for nearby pickups is a harmless data ghost — no UI shows it (no container, not wielded). Retail evicts it from `weenie_object_table` when the object fully leaves interest range via `DeleteObject`; acdream defers to teleport/relog clear. | Slight memory growth in long sessions if many world items are picked up by other players near you; item data ghosts cannot cause functional issues because no UI queries `ContainerId 0`. | `CACObjectMaint::DeleteObject` / `SmartBox::HandleDeleteObject`; ACE `Player_Inventory.cs TryDequipObjectWithNetworking` |
| AP-66 | **Empty equip slots show a generic frame (Slice 1) instead of the retail doll-backed transparent look** — retail's slot-view leaves empty armor slots transparent with the live 3-D doll body behind them; Slice 1 has no doll yet, so empty slots render a visible frame (`0x06004D20`, the inventory grid's empty square) so every position is seen + usable. The "figure" in the paperdoll is the doll, NOT a per-slot silhouette. | `src/AcDream.App/UI/Layout/PaperdollController.cs` (`emptySlotSprite`); `src/AcDream.App/Rendering/GameWindow.cs` (PaperdollController.Bind) | Retired in Slice 2 when the doll `UiViewport` (`0x100001D5`) renders behind the slots and the empty slots flip to transparent (`EmptySprite=0`). | Empty equip slots look like plain framed squares instead of revealing the character body — cosmetic, pending Slice 2's doll viewport. | `gmPaperDollUI` init `SetVisible(0)` per slot; user retail screenshots (slot-view) |
| AP-66 | **Empty equip slots paint a frame sprite (`EmptySprite` 0x06004D20), faithfulness vs retail's empty-slot art unverified** — Slice 2 shipped the doll `UiViewport` (0x100001D5) + the Slots toggle (doll-view ↔ slot-view, decomp 175674-175706), so empty ARMOR slots are now hidden in doll-view (the 3-D body shows instead). The always-visible non-armor equip slots (and armor slots in slot-view) still paint a frame when empty. | `src/AcDream.App/UI/Layout/PaperdollController.cs:99` | The doll + toggle are faithful (decomp-cited); painting a slot frame on empty positions is plausibly retail (the outlines guide drops), but is not pixel-verified against retail's doll-view. | Empty equip slots may show a framed square where retail shows a different empty treatment — cosmetic. | `gmPaperDollUI::ListenToElementMessage` 175674-175706; user retail screenshots |
| AP-67 | **Paperdoll doll is rendered to an off-screen FBO and blitted as a 2-D sprite (RTT), not retail's in-cell `CreatureMode::Render`** — retail adds the doll to `m_pPaperDoll->creature_mode_objects` and draws it into the UI's creature cell during the frame; acdream renders the re-dressed clone into a private framebuffer (`PaperdollViewportRenderer`, sealed in a `GLStateScope`), then the `UiViewport` widget blits the color texture. | `src/AcDream.App/Rendering/PaperdollViewportRenderer.cs`; `src/AcDream.App/UI/UiViewport.cs` | Architectural adaptation: acdream's modern bindless/MDI world pipeline can't cheaply render one creature into a UI cell mid-pass; the RTT seam reuses `WbDrawDispatcher` with the retail camera/light/pose/heading so the pixels match. No visual difference intended. | An FBO-format / lighting-UBO-overwrite / blit-V-flip bug shows as a doll that's mis-lit, clipped, or upside-down vs retail — cosmetic, confined to the viewport. | retail `CreatureMode::Render` 0x004529d0; `RedressCreature` 0x004a3bc0 |
| AP-68 | **Per-race paperdoll pose (`gmPaperDollUI::UpdateForRace`) not implemented** — the ctor-default pose animation `0x030003C0` is applied for ALL body types; retail's `UpdateForRace` (decomp 174129) can override `m_didAnimation` per race. | `src/AcDream.App/Rendering/GameWindow.cs` (`ResolvePaperdollPoseDid`) | cdb-confirmed `UpdateForRace` does NOT fire for Horan, so the ctor default 0x030003C0 is authoritative for the gate character; other races/genders may pose differently. | A non-default-race character could hold the wrong stance in the doll — cosmetic. | `gmPaperDollUI::UpdateForRace` decomp 174129; cdb trace 2026-06-25 |
---