From 42afec61419f9072a38f009f18b768b961acdf42 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 5 Sep 2026 12:35:45 +0200 Subject: [PATCH] test(overhaul): land reviewed C1c replay correction and retire AD-118 --- docs/ISSUES.md | 37 ++- .../retail-divergence-register.md | 4 +- ...-09-01-campaign-overhaul-world-solidity.md | 7 + .../s5-consumers-material-closeout-packet.md | 38 +++ .../Rendering/Walk/WalkOracleTrace.cs | 30 +- .../Rendering/Walk/WalkOracleTraceTests.cs | 30 +- .../Walk/WalkProjectionDiagnosticTests.cs | 123 ++++++++ ...ceConformanceTests.AlphaDepthTranscript.cs | 6 +- .../Walk/WalkTraceConformanceTests.cs | 290 ++++++++++-------- .../Rendering/Walk/WalkTraceReplay.cs | 103 +++++-- 10 files changed, 492 insertions(+), 176 deletions(-) create mode 100644 tests/AcDream.App.Tests/Rendering/Walk/WalkProjectionDiagnosticTests.cs diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 54b9ea03..b87c55fd 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -218,8 +218,10 @@ remaining closeout gates. No merge main. ## #472 — Stair-arch walk fixture cannot distinguish retail e229 residency from block rejection -**Status:** IN-PROGRESS — capture complete; §40's diagnostic resolves the -mismatch. Permanent default replay correction and review remain before closure. +**Status:** DONE 2026-09-05 — C1c made the captured matrix path, explicit +next-marker pose, and live-view EC multiplicity the permanent shared replay +defaults. The exact 25-frame Signature8 proof passes; no renderer defect was +established. Geometry/membership closeout remains a separate C1a chunk. **Severity:** EVIDENCE BLOCKER (no production renderer defect established) **Component:** InstalledDat walk-conformance harness / retail capture corpus @@ -266,8 +268,9 @@ next-marker pose, and per-live-view EC recording reproduce all four stair frames exactly (2158/2158), with e229 Outside and no LC/SC. All six OH walk fixtures plus the new capture are exact (25/25 complete frames). The historical capture/acceptance paragraphs above are superseded; this is a replay-harness -defect, not an accepted x87 renderer divergence. Packet §40 contracts the -permanent correction, regression tests and reviews. #472 is not closed yet. +defect, not an accepted x87 renderer divergence. C1c permanently corrected the +shared replay/parser/recorder and added discriminating regression tests; #472 +is closed. This does not claim universal bit-identical D3D/x87 projection. ## #471 — Production walk-ordered building detail was classified but never rendered @@ -633,23 +636,24 @@ so this class is caught on every gate, not only when the lead remembers the env ## #458 — Walk admits land block `a9c9` at holtburg-doorway-still that retail does not draw -**Status:** REOPENED 2026-09-05 — the former x87-only attribution below is -disproven by S5 packet §40's diagnostic. Corrected matrix rays/projection and -next-marker pose reproduce all four doorway frames exactly (1185/1185). -Permanent default replay correction, KnownFailure removal and review remain. -The 2026-09-03 investigation/resolution below is retained as superseded history. +**Status:** DONE 2026-09-05 — C1c permanently corrected the shared replay to +the captured matrix ray/projection path and explicit next-marker pose. All four +doorway frames reproduce exactly (1185/1185); the ordinary frame-1 row passes +and its KnownFailure trait is removed. The 2026-09-03 investigation/resolution +below is retained as superseded history. **Severity:** MEDIUM (retail-order exactness; one far land cell drawn that retail skips — no known visible artifact) **Component:** `WalkLandscape.CheckBlocks` / `LandCellCheck` block-level visibility at a LOD-ring boundary -**Symptom:** replaying `docs/research/2026-09-01-overhaul/oh-capture/holtburg-doorway-still.walk.log` +**Superseded symptom:** replaying `docs/research/2026-09-01-overhaul/oh-capture/holtburg-doorway-still.walk.log` frame 2 (camera cell `a9b4013f`) through `RetailFrameWalk`, the signature diverges at token 165: retail `…LC:a8c90001|SC:a8c90001|LC:95c60001…` vs acdream `…LC:a8c90001|SC:a8c90001|LC:a9c90001|SC:a9c90001|LC:95c60001…` — acdream admits the side-1 (ring ≥ 5) block `a9c9` through the interior root's exit views; retail's `LScape::draw_check_blocks` @0x00505f80 / `landcell_check` @0x005050a0 do not. The other three kit poses (terrace-edge, cathedral-arrival, foundry-deep) reproduce exactly. -`WalkTraceConformanceTests.Oh_doorway_still_first_frame_diff` carries the row tagged -`Status=KnownFailure` with the position documented. +`WalkTraceConformanceTests.Oh_doorway_still_first_frame_diff` formerly carried +the row tagged `Status=KnownFailure`; C1c removed the trait after the ordinary +shared-path frame-1 equality passed. **Lead experiment (2026-09-03):** with `DegradeMultiplier = 0f` (the FW0 sibling's cdb-depressed value) the row diverges at the SAME token 165 — the degrade level is NOT the @@ -695,7 +699,7 @@ pose against the decomp (the per-view union, the block MinZ/MaxZ clip heights, t exit-view edge planes, not the cell test. Resolve in the S3 review round or an S3 chunk 5; remove the KnownFailure trait in the same commit. -**Resolution (2026-09-03 afternoon, lead + owner, two live cdb captures on the paired 2013 +**Superseded resolution (2026-09-03 afternoon, lead + owner, two live cdb captures on the paired 2013 client at a NEARBY doorway pose — `docs/research/2026-09-01-overhaul/oh-capture/ holtburg-doorway-still.blockset.log` and `.blockcheck.log`, templates `tools/walk-oracle/oh/oh-capture-blockset.cdb.template` / `oh-capture-blockcheck.cdb.template`):** @@ -718,8 +722,11 @@ no `LC`. One far LOD block, one corner, one plane, at a boundary. The morning z- structural notes above stand (slab and loop shape match). No bit-exact fix exists short of reproducing D3D's x87 projection and `copy_view`'s float rounding; the conformance row stays `Status=KnownFailure` with its doc comment rewritten to this finding, and the register -carries it as AD-118. The two look-in-flood divergences (2)/(3) above are the pre-existing -FW1 near-win gate state and remain tracked by `WalkLookInGateSweepTests` (skipped). +carries it as AD-118. C1c later disproved that attribution: the capture used the +matrix branch and next-marker pose, and the corrected shared replay reproduces +the literal transcript. The two historical look-in-flood observations likewise +disappear under the corrected association; the text above remains history, not +an accepted current failure. ## #457 — Two App tests pass only in Release (IL-offset pins fail under Debug) diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 995bfe14..89bf176d 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -67,7 +67,7 @@ accepted-divergence entries (#96, #49, #50). --- -## 2. Adaptation (AD) — 92 active rows (AD-120 filed 2026-09-04 at the S4-c2 fix round 1 (M3) — a translucent building-shell instance under building detail draws immediately at its own walk-stream alpha-submission mark rather than "in place" mid-mesh-call, since acdream's opaque instances are stream-batched and retail's mesh call has no equivalent; AD-119 filed 2026-09-03 at Campaign OVERHAUL v2 S4 chunk 1 (S4-c1 C2) — the portal-depth color path substitutes a `ColorWrite=false` write mask for retail's zero-source-alpha `SRCALPHA`/`INVSRCALPHA` blend, a provably pixel-identical no-op either way; AD-117 filed 2026-09-03 at the Campaign OVERHAUL S2 review fix round — three residual Contract A/B approximations the S2 retail-lens review named (visual-AABB circumsphere cheap reject, part rows published into unloaded neighbour cells, the unported `state & 0x1000` particle branch) — its original item 1, the render-only destination-cell move rule, was VERIFIED the same night as retail's own zero-sphere `CObjCell::find_cell_list` 0x0052b4e0 mechanism and is not a deviation; AD-116 filed 2026-09-03 at Campaign OVERHAUL S2 chunk 5 — `WalkProductionWorldData`'s borrowed per-cell view contributes NO cell for an entity the registry has flooded but the presentation scene cannot resolve yet (the deleted parent-cell/root-position fallbacks are gone), counted once per distinct entity per frame in `UnregisteredRenderMembershipCount`; AD-115 filed 2026-08-25 at Campaign AS slice AS2 review fix round (F16) — `BuildCharacterTitleDisplay` clears the Profession element (`0x10000151`) when neither Int 261 CharacterTitleId nor String 5 Template resolves, where retail never clears `0x10000150`/`51`/`52` anywhere and would instead show the PREVIOUS target's stale title; AD-114 filed 2026-08-25 at Campaign AS slice AS2, owner-ruled ("we animate it, and I like it") — the examination window's preview clone tracks the assessed creature's live current animated pose every frame, where retail's clone plays its own private `CreatureMode` cycle decoupled from the live target's actual motion; AD-113 filed 2026-08-25 at Campaign CT slice CT-GF1 — `UiMenu`'s inline-drawn popup opts out of the new client-wide ancestor-clip default (`ExpandsClipForPopup`), standing in for retail's separate top-level popup region; AD-112 filed 2026-08-23 with the sky default-script port — camera-anchored synthetic script owners instead of retail's sky-cell physics objects; AD-110 filed 2026-08-17 at the entry/exit presentation round — the in-world logoff's single confirmed-echo handoff edge versus retail's two independent ExecuteLogOff/CharacterList edges, and the Tunnel-hold tail; AD-74 RETIRED 2026-08-17 at the same round — the Exit to Character Selection "behaves as Exit Game" adaptation is deleted: the confirmed grounded exit now runs the REAL retail flow (0xF653 request, server LogOut motion, 3 s hold, reverse wormhole, return to the live-connection character-select screen via LiveSessionController.CompleteCharacterLogOff), and the previously-missing indicator-bar grounded gate now runs retail's shared three-way branch; AD-109 filed 2026-08-17 at the entry/exit presentation round — the click-armed login tunnel: the wormhole presentation + enter cue now begin at the character-select Enter click instead of retail's black CreatePlayer wait, USER-DIRECTED; AD-108 filed 2026-08-17 at the night-round review fix round (F9), mechanism REPLACED same day at the overnight round's final fix — the Map tab's player/house icons, swallowed as `UiButton` dat children by `m_pMap`'s own Type-1 authoring, are now found in the panel-slot resolve's own info tree and rebuilt via `MapPageController.Bindings.IconBuilder` (the original standalone re-import resolved nothing on the live DAT); AD-107 RETIRED 2026-08-17 at the night-round review fix round (F2) — HouseQuery now fires once at the canonical local-player first-placement-completion edge (the same "initial session bootstrap" moment `GameActionLoginComplete`'s non-portal send sites already use), matching the byte-decoded retail truth that `CM_House::Event_QueryHouse @0x006aaa00` is tail-called, unconditionally, from the END of `CPlayerSystem::InitializePlayer @0x00563570` — the ONE-TIME-per-session function `AttemptSendLoginCompleteNotification` also lives in, guarded by the same `player_initialized` flag — right after that notification, not from any tab-open UI event; the invented tab-open trigger this row described is deleted outright, not merely narrowed; AD-106 filed 2026-08-16 at #409 (client-wide retail tooltip system) — RetailTooltipPresenter mounts the popup as an ordinary UiRoot sibling and keeps it topmost via its own per-tick BringToFront, scheduled after both RetailDialogFactory.Tick and Host.Tick, rather than porting retail's separate always-on-top presentation layer (m_pTooltipElement) — same adaptation shape AP-229 already accepted for dialogs-vs-screens, extended one layer further; AD-105 filed 2026-08-16 at Campaign CC gate round 1 re-test 3, finding R4-3 — the Skills info-box description-pane Height clamp to the SIBLING gold frame's own authored bottom edge, since retail's `ShowSkillsText` has no code relationship between the pane and the frame to cite directly. AD-104 filed 2026-08-16 at Campaign CC gate round 1 re-test 2, finding R3-3 — the Skills info-box title/description VerticalJustify page-scoped override, ISSUES.md #410 tracks the shared client-wide VJustify-default fix this compensates for. F12 correction, Campaign CC gate round 1 closeout, 2026-08-16: this header undercounted by 2 — a direct count of the physical `| AD-` rows below found 79, not the 77 this header carried; corrected to the counted total, matching AP-213's own row-count reconciliation the same closeout. AD-103 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch C fix (GF-4a) — the swallowed Type-12 value child (`0x100002f1`/`0x100002f3` under the avail/health/stamina/mana/credits badge buttons) is now surfaced as its OWN addressable `UiButton.ValueLabel`/`ValueBox`/`ValueFont`/`ValueColor` slot, built from the child's OWN authored rect/font/color (`DatWidgetFactory.BuildButton`) — closing both the container-Label-substitution shape AND F5's unmeasured-pixel-equivalence concern outright, since the value now renders at the child's own dat-local geometry instead of discarding it for the button's own Label font/rect; AD-101 RETIRED 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Heritage-page auto-gender-select interim default is deleted outright now that the Appearance page's real gender buttons (`0x100003a7`/`0x100003a8`) exist; AD-102/AD-103 filed 2026-08-15 at Campaign CC slice CC4 — the Viamontian/Sanamar ToD-account-ownership gate omission, and the avail/health/stamina/mana/credits-meter UiButton-Label substitution for retail's swallowed Text-child overlays; AD-100 filed 2026-08-15 at the Campaign CC CC2 review (F2) — an unrequested `0xF643` CharGenVerificationResponse is DROPPED with a once-per-session log, where retail's handler has no armed-request gate and processes whatever arrives; AD-99 filed 2026-08-15 at Campaign LA gate round 2 finding 1 — the char-select Exit-confirmed close routes through the existing graceful window-close seam instead of retail's post-confirm `gmEpilogueUI` transition; AD-98 filed 2026-08-15 at Campaign LA gate round 2, COMPLETED same day — the char-select screen keeps its authored 800x600 root and the whole tree (widgets, glyphs, art, dialogs) stretches as one canvas via `UiRoot.FixedCanvasSize` scaling every quad at `TextRenderer.AppendQuad` with inverse mouse mapping, substituting one stage earlier for retail's fixed-canvas-stretched-at-presentation mechanism (the first resize-the-root substitution was deleted at 73041d70); AD-95 RETIRED same-day 2026-08-14 at trade gate round 3 — ID_SecureTrade_TotalItemsLabel probe-verified token-free (fragments ["Total Items: ", ""], one ITEMS variable) and now composed via ResolveTemplate; AD-94 filed 2026-08-14 at the secure-trade feature — the ACE-discarded AcceptTrade echo's zero-count item lists; AD-93 filed 2026-08-13 at social gate round 2 item 5 — the refused-drop notice port's two narrow gaps: wire-guid-match instead of retail's latched-guid preference, and no Move/Wield latch kinds; AD-85 NARROWED + AD-81 AMENDED 2026-08-13 at social gate round 2 — the five confirmation-dialog templates now compose exactly via the new `DatStringResolver.ResolveTemplate` port of `StringTable::GetString @0x004300D0`'s token-free fragment/PLAYER interleave; AD-85 keeps only its numeric-field item, AD-81 keeps the meta-token engine + `FormatName`; AD-92 filed 2026-08-13 at the #376/#388 fix round — highest-refresh-for-WxH selection + refuse-and-log invalid fullscreen requests, versus retail's pass-through-and-error `ForceDisplayResolution`; AD-91 filed 2026-08-13 at the #390 port — the display-change clamp covers floating chats too, which retail leaves unclamped/strandable; AD-90 filed 2026-08-13 at the #389 fix round — retail's smartbox aspect runs through the `Render.AspectRatio` preference (`ComputeAspectForViewport @0x0054f150`), exactly raw w/h at its default, which is what acdream assumes; AD-89 RETIRED same-day 2026-08-13 — the SmartboxFOV port landed (#389): `RetailFieldOfView` + `CameraController.SetGameFov` now apply retail's `gameFOV/(aspect−0.1)` law with the 90°-degrees option semantics, and the invented 60° camera constants are deleted; AD-88 RETIRED 2026-08-28 — #386's named-retail message trace confirmed the vendor popup is content-sized and installed-DAT property 0x79 hides its disabled scrollbar; both behaviors are now ported; AD-87 filed 2026-08-12 at Campaign FA slice FA6 — the allegiance-swear half of the two-bot headless gate is written+wired but `AllegianceGateEnabled=false` (disabled by default), unverified end-to-end over the wire because ACE returns nothing to the `0x001D` swear (ISSUES #384); the FELLOWSHIP two-session gate passed live and ships as FA6's automated proof; AD-86 filed 2026-08-12 at Campaign FA slice FA5, item 4 — ACE's deliberate zeroing of officers/officer titles/MOTD/MOTD-set-by/name-last-set-time/lock/approved-vassal/timeOnline/allegianceAge, dropped past acdream's own parse layer to match retail's own no-widget presentation; AD-85 filed 2026-08-12 at Campaign FA slice FA5 — the Allegiance page's numeric-only fields and its three local confirmation dialogs' unsubstituted-verbatim-or-bare-name text, the same unported `StringInfo` gap AD-81 filed for Fellowship; AD-84 filed 2026-08-12 at Campaign FA slice FA5 — the Swear button's missing "target is a player" gate, the same class as AD-83's Recruit-button gap; AD-83 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 5) — the Recruit button's missing "target is a player" gate, previously an inline comment not a row; AD-82 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 4/5) — the invented leader-tint/selection-tint colors, the name-text-only row click target, and the page-local (not generic-`UiTemplateListBox`) world→panel selection sync; AD-81 filed 2026-08-12 at Campaign FA slice FA4 — the fellowship roster/create-flow text-composition gap (unported `StringInfo` variable substitution + `ACCharGenData::FormatName`); AD-80 filed 2026-08-12 at Campaign FA slice FA4, D5 — the panel's retail-exact XP-share percentage display versus the currently-targeted ACE server's slightly different actual grant; AD-79 filed 2026-08-12 at Campaign FA slice FA3, D1 — the social panel's Friends/Squelch page action buttons (add/remove friend, appear offline, squelch add/remove/clear) are honest INERT, no wire implemented this campaign; AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice; AD-111 (renumbered from a parallel-round AD-109 collision) filed 2026-08-17 at the systemic escape-normalization round — the appraisal report's wire-domain literal- +## 2. Adaptation (AD) — 91 active rows (AD-120 filed 2026-09-04 at the S4-c2 fix round 1 (M3) — a translucent building-shell instance under building detail draws immediately at its own walk-stream alpha-submission mark rather than "in place" mid-mesh-call, since acdream's opaque instances are stream-batched and retail's mesh call has no equivalent; AD-119 filed 2026-09-03 at Campaign OVERHAUL v2 S4 chunk 1 (S4-c1 C2) — the portal-depth color path substitutes a `ColorWrite=false` write mask for retail's zero-source-alpha `SRCALPHA`/`INVSRCALPHA` blend, a provably pixel-identical no-op either way; AD-117 filed 2026-09-03 at the Campaign OVERHAUL S2 review fix round — three residual Contract A/B approximations the S2 retail-lens review named (visual-AABB circumsphere cheap reject, part rows published into unloaded neighbour cells, the unported `state & 0x1000` particle branch) — its original item 1, the render-only destination-cell move rule, was VERIFIED the same night as retail's own zero-sphere `CObjCell::find_cell_list` 0x0052b4e0 mechanism and is not a deviation; AD-116 filed 2026-09-03 at Campaign OVERHAUL S2 chunk 5 — `WalkProductionWorldData`'s borrowed per-cell view contributes NO cell for an entity the registry has flooded but the presentation scene cannot resolve yet (the deleted parent-cell/root-position fallbacks are gone), counted once per distinct entity per frame in `UnregisteredRenderMembershipCount`; AD-115 filed 2026-08-25 at Campaign AS slice AS2 review fix round (F16) — `BuildCharacterTitleDisplay` clears the Profession element (`0x10000151`) when neither Int 261 CharacterTitleId nor String 5 Template resolves, where retail never clears `0x10000150`/`51`/`52` anywhere and would instead show the PREVIOUS target's stale title; AD-114 filed 2026-08-25 at Campaign AS slice AS2, owner-ruled ("we animate it, and I like it") — the examination window's preview clone tracks the assessed creature's live current animated pose every frame, where retail's clone plays its own private `CreatureMode` cycle decoupled from the live target's actual motion; AD-113 filed 2026-08-25 at Campaign CT slice CT-GF1 — `UiMenu`'s inline-drawn popup opts out of the new client-wide ancestor-clip default (`ExpandsClipForPopup`), standing in for retail's separate top-level popup region; AD-112 filed 2026-08-23 with the sky default-script port — camera-anchored synthetic script owners instead of retail's sky-cell physics objects; AD-110 filed 2026-08-17 at the entry/exit presentation round — the in-world logoff's single confirmed-echo handoff edge versus retail's two independent ExecuteLogOff/CharacterList edges, and the Tunnel-hold tail; AD-74 RETIRED 2026-08-17 at the same round — the Exit to Character Selection "behaves as Exit Game" adaptation is deleted: the confirmed grounded exit now runs the REAL retail flow (0xF653 request, server LogOut motion, 3 s hold, reverse wormhole, return to the live-connection character-select screen via LiveSessionController.CompleteCharacterLogOff), and the previously-missing indicator-bar grounded gate now runs retail's shared three-way branch; AD-109 filed 2026-08-17 at the entry/exit presentation round — the click-armed login tunnel: the wormhole presentation + enter cue now begin at the character-select Enter click instead of retail's black CreatePlayer wait, USER-DIRECTED; AD-108 filed 2026-08-17 at the night-round review fix round (F9), mechanism REPLACED same day at the overnight round's final fix — the Map tab's player/house icons, swallowed as `UiButton` dat children by `m_pMap`'s own Type-1 authoring, are now found in the panel-slot resolve's own info tree and rebuilt via `MapPageController.Bindings.IconBuilder` (the original standalone re-import resolved nothing on the live DAT); AD-107 RETIRED 2026-08-17 at the night-round review fix round (F2) — HouseQuery now fires once at the canonical local-player first-placement-completion edge (the same "initial session bootstrap" moment `GameActionLoginComplete`'s non-portal send sites already use), matching the byte-decoded retail truth that `CM_House::Event_QueryHouse @0x006aaa00` is tail-called, unconditionally, from the END of `CPlayerSystem::InitializePlayer @0x00563570` — the ONE-TIME-per-session function `AttemptSendLoginCompleteNotification` also lives in, guarded by the same `player_initialized` flag — right after that notification, not from any tab-open UI event; the invented tab-open trigger this row described is deleted outright, not merely narrowed; AD-106 filed 2026-08-16 at #409 (client-wide retail tooltip system) — RetailTooltipPresenter mounts the popup as an ordinary UiRoot sibling and keeps it topmost via its own per-tick BringToFront, scheduled after both RetailDialogFactory.Tick and Host.Tick, rather than porting retail's separate always-on-top presentation layer (m_pTooltipElement) — same adaptation shape AP-229 already accepted for dialogs-vs-screens, extended one layer further; AD-105 filed 2026-08-16 at Campaign CC gate round 1 re-test 3, finding R4-3 — the Skills info-box description-pane Height clamp to the SIBLING gold frame's own authored bottom edge, since retail's `ShowSkillsText` has no code relationship between the pane and the frame to cite directly. AD-104 filed 2026-08-16 at Campaign CC gate round 1 re-test 2, finding R3-3 — the Skills info-box title/description VerticalJustify page-scoped override, ISSUES.md #410 tracks the shared client-wide VJustify-default fix this compensates for. F12 correction, Campaign CC gate round 1 closeout, 2026-08-16: this header undercounted by 2 — a direct count of the physical `| AD-` rows below found 79, not the 77 this header carried; corrected to the counted total, matching AP-213's own row-count reconciliation the same closeout. AD-103 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch C fix (GF-4a) — the swallowed Type-12 value child (`0x100002f1`/`0x100002f3` under the avail/health/stamina/mana/credits badge buttons) is now surfaced as its OWN addressable `UiButton.ValueLabel`/`ValueBox`/`ValueFont`/`ValueColor` slot, built from the child's OWN authored rect/font/color (`DatWidgetFactory.BuildButton`) — closing both the container-Label-substitution shape AND F5's unmeasured-pixel-equivalence concern outright, since the value now renders at the child's own dat-local geometry instead of discarding it for the button's own Label font/rect; AD-101 RETIRED 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Heritage-page auto-gender-select interim default is deleted outright now that the Appearance page's real gender buttons (`0x100003a7`/`0x100003a8`) exist; AD-102/AD-103 filed 2026-08-15 at Campaign CC slice CC4 — the Viamontian/Sanamar ToD-account-ownership gate omission, and the avail/health/stamina/mana/credits-meter UiButton-Label substitution for retail's swallowed Text-child overlays; AD-100 filed 2026-08-15 at the Campaign CC CC2 review (F2) — an unrequested `0xF643` CharGenVerificationResponse is DROPPED with a once-per-session log, where retail's handler has no armed-request gate and processes whatever arrives; AD-99 filed 2026-08-15 at Campaign LA gate round 2 finding 1 — the char-select Exit-confirmed close routes through the existing graceful window-close seam instead of retail's post-confirm `gmEpilogueUI` transition; AD-98 filed 2026-08-15 at Campaign LA gate round 2, COMPLETED same day — the char-select screen keeps its authored 800x600 root and the whole tree (widgets, glyphs, art, dialogs) stretches as one canvas via `UiRoot.FixedCanvasSize` scaling every quad at `TextRenderer.AppendQuad` with inverse mouse mapping, substituting one stage earlier for retail's fixed-canvas-stretched-at-presentation mechanism (the first resize-the-root substitution was deleted at 73041d70); AD-95 RETIRED same-day 2026-08-14 at trade gate round 3 — ID_SecureTrade_TotalItemsLabel probe-verified token-free (fragments ["Total Items: ", ""], one ITEMS variable) and now composed via ResolveTemplate; AD-94 filed 2026-08-14 at the secure-trade feature — the ACE-discarded AcceptTrade echo's zero-count item lists; AD-93 filed 2026-08-13 at social gate round 2 item 5 — the refused-drop notice port's two narrow gaps: wire-guid-match instead of retail's latched-guid preference, and no Move/Wield latch kinds; AD-85 NARROWED + AD-81 AMENDED 2026-08-13 at social gate round 2 — the five confirmation-dialog templates now compose exactly via the new `DatStringResolver.ResolveTemplate` port of `StringTable::GetString @0x004300D0`'s token-free fragment/PLAYER interleave; AD-85 keeps only its numeric-field item, AD-81 keeps the meta-token engine + `FormatName`; AD-92 filed 2026-08-13 at the #376/#388 fix round — highest-refresh-for-WxH selection + refuse-and-log invalid fullscreen requests, versus retail's pass-through-and-error `ForceDisplayResolution`; AD-91 filed 2026-08-13 at the #390 port — the display-change clamp covers floating chats too, which retail leaves unclamped/strandable; AD-90 filed 2026-08-13 at the #389 fix round — retail's smartbox aspect runs through the `Render.AspectRatio` preference (`ComputeAspectForViewport @0x0054f150`), exactly raw w/h at its default, which is what acdream assumes; AD-89 RETIRED same-day 2026-08-13 — the SmartboxFOV port landed (#389): `RetailFieldOfView` + `CameraController.SetGameFov` now apply retail's `gameFOV/(aspect−0.1)` law with the 90°-degrees option semantics, and the invented 60° camera constants are deleted; AD-88 RETIRED 2026-08-28 — #386's named-retail message trace confirmed the vendor popup is content-sized and installed-DAT property 0x79 hides its disabled scrollbar; both behaviors are now ported; AD-87 filed 2026-08-12 at Campaign FA slice FA6 — the allegiance-swear half of the two-bot headless gate is written+wired but `AllegianceGateEnabled=false` (disabled by default), unverified end-to-end over the wire because ACE returns nothing to the `0x001D` swear (ISSUES #384); the FELLOWSHIP two-session gate passed live and ships as FA6's automated proof; AD-86 filed 2026-08-12 at Campaign FA slice FA5, item 4 — ACE's deliberate zeroing of officers/officer titles/MOTD/MOTD-set-by/name-last-set-time/lock/approved-vassal/timeOnline/allegianceAge, dropped past acdream's own parse layer to match retail's own no-widget presentation; AD-85 filed 2026-08-12 at Campaign FA slice FA5 — the Allegiance page's numeric-only fields and its three local confirmation dialogs' unsubstituted-verbatim-or-bare-name text, the same unported `StringInfo` gap AD-81 filed for Fellowship; AD-84 filed 2026-08-12 at Campaign FA slice FA5 — the Swear button's missing "target is a player" gate, the same class as AD-83's Recruit-button gap; AD-83 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 5) — the Recruit button's missing "target is a player" gate, previously an inline comment not a row; AD-82 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 4/5) — the invented leader-tint/selection-tint colors, the name-text-only row click target, and the page-local (not generic-`UiTemplateListBox`) world→panel selection sync; AD-81 filed 2026-08-12 at Campaign FA slice FA4 — the fellowship roster/create-flow text-composition gap (unported `StringInfo` variable substitution + `ACCharGenData::FormatName`); AD-80 filed 2026-08-12 at Campaign FA slice FA4, D5 — the panel's retail-exact XP-share percentage display versus the currently-targeted ACE server's slightly different actual grant; AD-79 filed 2026-08-12 at Campaign FA slice FA3, D1 — the social panel's Friends/Squelch page action buttons (add/remove friend, appear offline, squelch add/remove/clear) are honest INERT, no wire implemented this campaign; AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice; AD-111 (renumbered from a parallel-round AD-109 collision) filed 2026-08-17 at the systemic escape-normalization round — the appraisal report's wire-domain literal- -to-line-break shaping, which retail's `ItemExamineUI::AddItemInfo @0x004AC050` does not do (wire text appends verbatim; the escape decode retail runs at `StringInfo` resolution now lives at our string source, `DatStringResolver` → `RetailStringEscapes`); AD-108 filed 2026-08-17 at the night-round review fix round (F9), mechanism REPLACED same day at the overnight round's final fix — the Map tab's player/house icons, swallowed as `UiButton` dat children by `m_pMap`'s own Type-1 authoring, are now found in the panel-slot resolve's own info tree and rebuilt via `MapPageController.Bindings.IconBuilder` (the original standalone re-import resolved nothing on the live DAT); AD-107 RETIRED 2026-08-17 at the night-round review fix round (F2) — HouseQuery now fires once at the canonical local-player first-placement-completion edge (the same "initial session bootstrap" moment `GameActionLoginComplete`'s non-portal send sites already use), matching the byte-decoded retail truth that `CM_House::Event_QueryHouse @0x006aaa00` is tail-called, unconditionally, from the END of `CPlayerSystem::InitializePlayer @0x00563570` — the ONE-TIME-per-session function `AttemptSendLoginCompleteNotification` also lives in, guarded by the same `player_initialized` flag — right after that notification, not from any tab-open UI event; the invented tab-open trigger this row described is deleted outright, not merely narrowed; AD-106 filed 2026-08-16 at #409 (client-wide retail tooltip system) — RetailTooltipPresenter mounts the popup as an ordinary UiRoot sibling and keeps it topmost via its own per-tick BringToFront, scheduled after both RetailDialogFactory.Tick and Host.Tick, rather than porting retail's separate always-on-top presentation layer (m_pTooltipElement) — same adaptation shape AP-229 already accepted for dialogs-vs-screens, extended one layer further; AD-105 filed 2026-08-16 at Campaign CC gate round 1 re-test 3, finding R4-3 — the Skills info-box description-pane Height clamp to the SIBLING gold frame's own authored bottom edge, since retail's `ShowSkillsText` has no code relationship between the pane and the frame to cite directly. AD-104 filed 2026-08-16 at Campaign CC gate round 1 re-test 2, finding R3-3 — the Skills info-box title/description VerticalJustify page-scoped override, ISSUES.md #410 tracks the shared client-wide VJustify-default fix this compensates for. F12 correction, Campaign CC gate round 1 closeout, 2026-08-16: this header undercounted by 2 — a direct count of the physical `| AD-` rows below found 79, not the 77 this header carried; corrected to the counted total, matching AP-213's own row-count reconciliation the same closeout. AD-103 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch C fix (GF-4a) — the swallowed Type-12 value child (`0x100002f1`/`0x100002f3` under the avail/health/stamina/mana/credits badge buttons) is now surfaced as its OWN addressable `UiButton.ValueLabel`/`ValueBox`/`ValueFont`/`ValueColor` slot, built from the child's OWN authored rect/font/color (`DatWidgetFactory.BuildButton`) — closing both the container-Label-substitution shape AND F5's unmeasured-pixel-equivalence concern outright, since the value now renders at the child's own dat-local geometry instead of discarding it for the button's own Label font/rect; AD-101 RETIRED 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Heritage-page auto-gender-select interim default is deleted outright now that the Appearance page's real gender buttons (`0x100003a7`/`0x100003a8`) exist; AD-102/AD-103 filed 2026-08-15 at Campaign CC slice CC4 — the Viamontian/Sanamar ToD-account-ownership gate omission, and the avail/health/stamina/mana/credits-meter UiButton-Label substitution for retail's swallowed Text-child overlays; AD-100 filed 2026-08-15 at the Campaign CC CC2 review (F2) — an unrequested `0xF643` CharGenVerificationResponse is DROPPED with a once-per-session log, where retail's handler has no armed-request gate and processes whatever arrives; AD-99 filed 2026-08-15 at Campaign LA gate round 2 finding 1 — the char-select Exit-confirmed close routes through the existing graceful window-close seam instead of retail's post-confirm `gmEpilogueUI` transition; AD-98 filed 2026-08-15 at Campaign LA gate round 2, COMPLETED same day — the char-select screen keeps its authored 800x600 root and the whole tree (widgets, glyphs, art, dialogs) stretches as one canvas via `UiRoot.FixedCanvasSize` scaling every quad at `TextRenderer.AppendQuad` with inverse mouse mapping, substituting one stage earlier for retail's fixed-canvas-stretched-at-presentation mechanism (the first resize-the-root substitution was deleted at 73041d70); AD-95 RETIRED same-day 2026-08-14 at trade gate round 3 — ID_SecureTrade_TotalItemsLabel probe-verified token-free (fragments ["Total Items: ", ""], one ITEMS variable) and now composed via ResolveTemplate; AD-94 filed 2026-08-14 at the secure-trade feature — the ACE-discarded AcceptTrade echo's zero-count item lists; AD-93 filed 2026-08-13 at social gate round 2 item 5 — the refused-drop notice port's two narrow gaps: wire-guid-match instead of retail's latched-guid preference, and no Move/Wield latch kinds; AD-85 NARROWED + AD-81 AMENDED 2026-08-13 at social gate round 2 — the five confirmation-dialog templates now compose exactly via the new `DatStringResolver.ResolveTemplate` port of `StringTable::GetString @0x004300D0`'s token-free fragment/PLAYER interleave; AD-85 keeps only its numeric-field item, AD-81 keeps the meta-token engine + `FormatName`; AD-92 filed 2026-08-13 at the #376/#388 fix round — highest-refresh-for-WxH selection + refuse-and-log invalid fullscreen requests, versus retail's pass-through-and-error `ForceDisplayResolution`; AD-91 filed 2026-08-13 at the #390 port — the display-change clamp covers floating chats too, which retail leaves unclamped/strandable; AD-90 filed 2026-08-13 at the #389 fix round — retail's smartbox aspect runs through the `Render.AspectRatio` preference (`ComputeAspectForViewport @0x0054f150`), exactly raw w/h at its default, which is what acdream assumes; AD-89 RETIRED same-day 2026-08-13 — the SmartboxFOV port landed (#389): `RetailFieldOfView` + `CameraController.SetGameFov` now apply retail's `gameFOV/(aspect−0.1)` law with the 90°-degrees option semantics, and the invented 60° camera constants are deleted; AD-88 filed 2026-08-13 at the #385 dropdown fix — the vendor category dropdown keeps G5's fixed 6-row scrollable window although its authored popup ListBox is edge-docked, the condition that arms retail's `RecalculatePopupSize` size-to-content resize; classification UNCLEAR pending a retail side-by-side (ISSUES #386); AD-87 filed 2026-08-12 at Campaign FA slice FA6 — the allegiance-swear half of the two-bot headless gate is written+wired but `AllegianceGateEnabled=false` (disabled by default), unverified end-to-end over the wire because ACE returns nothing to the `0x001D` swear (ISSUES #384); the FELLOWSHIP two-session gate passed live and ships as FA6's automated proof; AD-86 filed 2026-08-12 at Campaign FA slice FA5, item 4 — ACE's deliberate zeroing of officers/officer titles/MOTD/MOTD-set-by/name-last-set-time/lock/approved-vassal/timeOnline/allegianceAge, dropped past acdream's own parse layer to match retail's own no-widget presentation; AD-85 filed 2026-08-12 at Campaign FA slice FA5 — the Allegiance page's numeric-only fields and its three local confirmation dialogs' unsubstituted-verbatim-or-bare-name text, the same unported `StringInfo` gap AD-81 filed for Fellowship; AD-84 filed 2026-08-12 at Campaign FA slice FA5 — the Swear button's missing "target is a player" gate, the same class as AD-83's Recruit-button gap; AD-83 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 5) — the Recruit button's missing "target is a player" gate, previously an inline comment not a row; AD-82 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 4/5) — the invented leader-tint/selection-tint colors, the name-text-only row click target, and the page-local (not generic-`UiTemplateListBox`) world→panel selection sync; AD-81 filed 2026-08-12 at Campaign FA slice FA4 — the fellowship roster/create-flow text-composition gap (unported `StringInfo` variable substitution + `ACCharGenData::FormatName`); AD-80 filed 2026-08-12 at Campaign FA slice FA4, D5 — the panel's retail-exact XP-share percentage display versus the currently-targeted ACE server's slightly different actual grant; AD-79 filed 2026-08-12 at Campaign FA slice FA3, D1 — the social panel's Friends/Squelch page action buttons (add/remove friend, appear offline, squelch add/remove/clear) are honest INERT, no wire implemented this campaign; AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice) Recent retirements: AD-3/AD-4 retired 2026-07-31 by exact active/per-candidate @@ -216,7 +216,7 @@ readiness/requeue adaptation. See | AD-102 | **Filed 2026-08-15 at Campaign CC slice CC4 (the Heritage page's Viamontian button and the Town page's Sanamar button).** Retail gates BOTH controls behind `CPlayerSystem::AccountHasThroneOfDestiny`: `gmCGHeritagePage::ListenToElementMessage @ 0x00483860` shows `MakeToDWarningDialog` instead of selecting Viamontian (element `0x100003c3`) for a non-ToD account, and `gmCGTownPage::ListenToElementMessage @ 0x0047c480` does the same for Sanamar (element `0x1000040b`, `startArea` index 3 — also the reason `CharGenState::RandomizeStartArea`'s ToD-aware `RandInt(3 or 4)` bound exists). acdream's `ChargenOptions` (CC1) carries no account/DLC-ownership signal anywhere in the model, so both controls ship WITHOUT the gate — every installed heritage/town in `Options.HeritagesById`/`Options.StarterAreas` is always selectable, matching what a ToD-owning account would see. | `src/AcDream.App/UI/Layout/CharacterCreationHeritagePage.cs` (`HeritageByButtonId[0x100003C3u]`); `src/AcDream.App/UI/Layout/CharacterCreationTownPage.cs` (`StartAreaByButtonId[0x1000040Bu]`, `Randomize`) | ACE's server-side `CharacterCreate` handler never checks ToD ownership either (the field is purely a retail-client UI gate), so accepting the selection unconditionally never produces a request the emulator would reject; adding an account-ownership model to CC1's DAT-only `ChargenOptions` is out of this slice's scope and would need its own design (where does the "ToD owned" bit come from — account service, launcher config, a new env flag?). | None observable against ACE. A future retail-parity gate that specifically checks "does a non-ToD account get warned off Viamontian/Sanamar" will fail until an account-ownership signal exists to gate on. | `gmCGHeritagePage::ListenToElementMessage @ 0x00483860`; `gmCGTownPage::ListenToElementMessage @ 0x0047c480`; `gmCGTownPage::SetTown @ 0x0047c360`; `CharGenState::RandomizeStartArea` (DoRandom case 4, `RandInt(hasToD ? 4 : 3)`) | | AD-99 | **Filed 2026-08-15 at Campaign LA gate round 2 finding 1 (character-select Exit button).** On a confirmed Exit, acdream closes the client through the existing graceful window-close path (`d.Window.Close`, the same seam `GameplayInputCommandController`'s in-world Escape fallback already uses) instead of retail's real post-confirm behavior: `RecvNotice_CloseDialog`'s case-1 arm queues UI mode `0x10000009`, which `gmEpilogueUI::Register` claims — a brief epilogue/farewell screen — before the process actually terminates. The confirmation dialog itself (`MakeConfirmExitDialog`, its exact `ID_CharacterManagement_ConfirmExit` text, and the `m_confirmExitDialogContext != 0` re-entry guard) IS ported faithfully; only the post-confirm destination differs, the same shape as AD-74's Options-panel exit. | `src/AcDream.App/UI/Layout/CharacterManagementUiController.cs` (`RequestExit`); `src/AcDream.App/UI/RetailUiRuntime.cs` (`CharacterSelectionRuntimeBindings.RequestExit`); `src/AcDream.App/Composition/InteractionRetainedUiComposition.cs` (`d.Window.Close` binding) | acdream has no `gmEpilogueUI` port (out of scope this round); reusing the ONE existing graceful-shutdown seam keeps `disconnected`/`exited` status events firing through `GameWindow.OnClosing` → `CompleteShutdown` rather than inventing a second shutdown path, per explicit direction for this finding. | A user confirming Exit sees the window close immediately instead of retail's brief epilogue screen; a future feature wanting to reproduce that screen (or an intermediate "logged off, returned to character select" state) has no seam yet — same gap class as AD-44. | `gmCharacterManagementUI::MakeConfirmExitDialog @0x004ed250`; `RecvNotice_CloseDialog @0x004ed760` case 1; `gmEpilogueUI::Register(0x10000009)` @0x0047a680; `gmCharacterManagementUI::OnAction @0x004ed410` (Escape key, unported — button-only this round) | | AD-113 | **Filed 2026-08-25 at Campaign CT slice CT-GF1 (client-wide retained-UI ancestor clip).** Porting retail's `UIRegion::DrawHere @0x0069FA30` ancestor-clip intersection (an element's screen rect is intersected against the FULL inherited clip-rect chain and the subtree is skipped when the intersection is empty — the `var_24` gate @0x0069FB8E) as `UiElement.ClipsChildren`'s new client-wide default (true, threaded through the pre-existing `UiRenderContext.PushClip`/`PopClip`) needed one deliberate opt-out: retail spawns a menu's dropdown popup as a SEPARATE top-level region (`UIElement_Menu::MakePopup`), clipped only by the screen, while acdream's `UiMenu` draws its popup INLINE from the owning button in a second traversal (`OnDrawOverlay`, pre-existing, "regardless of this element's position in the tree" by its own doc comment). Without an escape, the new ancestor clip would wrongly cut off a popup that legitimately extends outside its own (possibly short) owning window — e.g. a channel dropdown opened upward past a short chat window's top edge. `UiElement.ExpandsClipForPopup` (default false) resets the accumulated clip to the full CANVAS rect (0,0,ScreenSize) — SCREEN-clipped, not truly unbounded, matching retail's own popup region (`UIElement_Menu::MakePopup` spawns a top-level region bounded by the screen) — for exactly the `OnDrawOverlay` call of an opted-in element (`UiRenderContext.PushClipUnbounded`, sharing the existing clip stack; corrected from an earlier `null`/unbounded clip at the CT-GF1 fix round); `UiMenu` overrides it true, paired with `ClipsChildren => false` so its own out-of-bounds `OnHitTest` union (the popup occupies `ly < 0` or `ly >= Height` depending on open direction) stays reachable through the same early-bounds gate that now defaults on for every other element. | `src/AcDream.App/UI/UiElement.cs` (`ClipsChildren`, `ExpandsClipForPopup`, `DrawOverlays`); `src/AcDream.App/UI/UiRenderContext.cs` (`PushClipUnbounded`); `src/AcDream.App/UI/UiMenu.cs` (the two overrides) | The popup is the ONLY overlay-drawing widget in the tree today (grep-confirmed: exactly one `OnDrawOverlay` override client-wide), and it already renders on top of the whole UI by construction (the overlay pass beats even rect backgrounds), so exempting it from the ancestor clip matches its existing "regardless of tree position" contract rather than introducing new behavior. | A future `OnDrawOverlay` override that is NOT a screen-anchored popup (e.g. an in-place highlight meant to stay window-clipped) would silently escape every ancestor's clip if it left `ExpandsClipForPopup` at its default; the opt-in default direction makes that the exception rather than the rule, but a widget that WANTS window-clipped overlay content has no dedicated seam beyond simply not overriding the escape. | `UIRegion::DrawHere @0x0069FA30`; `UIElement_Menu::MakePopup`; the register's own AP-201 retirement note (the FIRST `ClipsChildren`/`PushClip` port, for `UiScrollablePanel`'s viewport) | -| AD-118 | **DIAGNOSTICALLY DISPROVEN 2026-09-05; permanent test correction/review pending (S5 packet §40).** The observed #458 doorway and #472 stair differences came from the replay's legacy rays paired with a different forward projection; frame-entry pose timing also mattered. Captured newmethod=1 selects the matrix branch. The corrected matrix plus next-marker pose reproduces all 25 complete OH/capture frames exactly; the old x87-only attribution is superseded. | `tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs`; `WalkOracleTrace.cs`; shared conformance Recorder | No production deviation has been established by these fixtures. Retire this row's acceptance rationale with C1c's permanent discriminating proof; ordinary floating-point differences are not permission to accept a transcript mismatch. | #458's KnownFailure remains only until the permanent default replay row passes; no new failure accepted. The historical evidence remains in docs/ISSUES.md, explicitly superseded. | `Render::copy_view @0054dfc0`; `PrimD3DRender::ScreenToViewTransform @0059aa40`; `SmartBox::RenderNormalMode @00453aa0`; packet §§39–40; `s5-c1b-projection-diagnostic.txt` | +| ~~AD-118~~ | **RETIRED 2026-09-05 by S5 C1c.** The observed #458 doorway and #472 stair differences were replay-harness defects: legacy rays were paired with a different forward projection, frame events used the stale same-marker pose, and the recorder emitted one EC per cell instead of one per live view. Captured `newmethod=1` selects the matrix branch. The permanent shared matrix path, explicit next-marker replay pose, and live-view recorder reproduce all 25 complete OH/capture frames exactly; the old x87-only attribution is superseded. | `tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs`; `WalkOracleTrace.cs`; shared conformance Recorder | No production deviation was established by these fixtures. The five restored mutations discriminate each corrected boundary. Ordinary floating-point differences are not permission to accept a transcript mismatch, and retirement does not promise universal bit-identical D3D/x87 projection. | #458's KnownFailure is removed after its ordinary frame-1 row passes; #458 and #472 are closed. Historical evidence remains in docs/ISSUES.md, explicitly superseded. | `Render::copy_view @0054dfc0`; `PrimD3DRender::ScreenToViewTransform @0059aa40`; `SmartBox::RenderNormalMode @00453aa0`; packet §§39–40; `s5-c1b-projection-diagnostic.txt` | --- diff --git a/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md b/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md index 4517b350..8cfaba7b 100644 --- a/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md +++ b/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md @@ -743,6 +743,13 @@ Rynthid Recall6150→3032001C corroborates owner generation4; historical route labels are not location evidence (479-rynthid-location-evidence.md). #479 transform cause remains unproven. No change to G4 FAIL or the open obligations. +C1c current (packet§53): reviewed nine-file test/doc return now lands from +scratch963728612; all17 source/TRX hashes rechecked, prior lead68/68 and both +review lenses closed. Six test blobs unchanged; production/fixtures untouched. +#458/#472 closed as harness defects, AD-118 retired, counted104/13/91. +Clean post-landing Release/canonical conformance is next, then §45 activation. +The later476F1 AD-121 must preserve this retirement and produce92 active rows. + ## 14. Risk register (condensed) - **R1 second scene graph.** Graph records own only topology/membership facts; diff --git a/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md b/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md index 0b369094..faf8bc87 100644 --- a/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md +++ b/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md @@ -3763,6 +3763,25 @@ AcDream.App.dll copies remained SHA-256 ACE remains UDP 9000/PID 13340. Do not build production or launch another graphical client beside the owner's running client. +**C1c permanent result (uncommitted implementer return, 2026-09-05):** the +ordinary replay now uses the coherent captured `newmethod=1` matrix path; the +parser retains literal P(N) while exposing events(N)/P(N+1), including the +last raw marker; and the shared Recorder emits EC once per live `TopView` +before OC once per cell. The permanent proof consumes all six historical OH +walks plus the new capture: 25/25 complete frames match at full ordered +Signature8, the doorway frame-1 row passes without KnownFailure, the stair +pins four EC turns/two adjacent duplicate pairs and no LC/SC:e2290001, and +the focused restored lanes pass 29/29 parser/projection, 27/27 conformance, +and 12/12 production-world context invariance. The five required mutations +were run separately and restored: legacy rays fail the center round trip at +0.7078837 px; one-EC-per-cell fails the stair pin (expected 4, actual 2); +same-marker association fails the synthetic final-marker ID and doorway +frame 1 at token 165; changing matrix word 0 fails the captured-word binding +(1065698052 vs 1065698053); redirecting the sixth fixture fails on the absent +canonical `.walk.log`. No fixture or production byte changed, and this result +does not claim universal bit-identical D3D/x87 math. Geometry/membership C1a +remains separate. + ### 40.2 C1c scope — permanent test-only correction One OpenAI implementer uses the existing scratch, fast-forwarding this @@ -4776,3 +4795,22 @@ requirement (AD-121 at landing), actual-command/coherence witnesses and mutation No global allocator expansion; unchanged phase/lifetime review stays closed. Prepared route-476-capture-current-frame.txt preserves first request plus two real resizes, then settled hand-item views; NOT RUN. No new client launched. + +## 53. C1c accepted test-only return landed + +While #476 F1 is implemented, root landed the already-reviewed nine-file C1c +return from §43. Scratch96372861249a092998dd0dc6deed6d1d4196eb8a preserves +the exact six reviewed test blobs; root rechecked all17 source/TRX hashes in +artifacts/s5-c1c-02219318a/README.txt before committing. Both prior reviews and +root68/68 proof in§43 remain applicable; no new code finding or review round. +The three documentation hunks merge without replacing later campaign evidence. +#458/#472 close as replay-harness defects; AD-118 retired in this same commit. +Fresh root physical-row recount104 total/13 retired/91 active AD rows. + +No production, fixture, package or graphical change. Exact current production +remains #477 + #480; #476 is still unintegrated. Post-landing clean Release and +canonical conformance checks are next, classifying every nonpassing identity. +Only after those checks may §45's separately bounded geometry/membership witness +activate on a new clean base. F1's later AD-121 addition must preserve retired +AD-118 and make the combined AD count92, not restore the stale pre-C1c header. +No new graphical run; G4 FAIL, remaining owner issues/full lanes/C2 still open. diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkOracleTrace.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkOracleTrace.cs index e71328cc..1bb6bb1d 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkOracleTrace.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkOracleTrace.cs @@ -13,7 +13,10 @@ namespace AcDream.App.Tests.Rendering.Walk; /// /// The final frame of every capture is dropped: the harness detaches at /// the frame MARKER, so the last frame never records its draws and would -/// read as a false "outdoor, no landscape" frame. +/// read as a false "outdoor, no landscape" frame. Its raw P record is +/// nevertheless retained as the preceding complete frame's ReplayPose: +/// RenderNormalMode's entry marker is prior-frame state, so events(N) pair +/// with P(N+1), not their literal P(N) marker. /// public static class WalkOracleTrace { @@ -23,6 +26,7 @@ public static class WalkOracleTrace List? current = null; int currentNumber = 0; WalkOraclePose? currentPose = null; + int frameAwaitingReplayPose = -1; foreach (string line in lines) { @@ -30,7 +34,10 @@ public static class WalkOracleTrace if (frameMatch.Success) { if (current is not null) + { frames.Add(new WalkOracleFrame(currentNumber, current, currentPose)); + frameAwaitingReplayPose = frames.Count - 1; + } currentNumber = int.Parse( frameMatch.Groups[1].Value, CultureInfo.InvariantCulture); current = new List(); @@ -60,6 +67,17 @@ public static class WalkOracleTrace BitConverter.Int32BitsToSingle((int)raw[5]), BitConverter.Int32BitsToSingle((int)raw[6]), BitConverter.Int32BitsToSingle((int)raw[7])); + // RenderNormalMode's entry marker observes the PREVIOUS + // rendered frame: events(N) therefore use marker P(N+1). + // Assign it explicitly to the already-complete prior frame. + // This also consumes the last raw P marker even though its + // own event-incomplete frame remains deliberately dropped. + if (frameAwaitingReplayPose >= 0) + { + frames[frameAwaitingReplayPose] = frames[frameAwaitingReplayPose] + with { ReplayPose = currentPose }; + frameAwaitingReplayPose = -1; + } continue; } @@ -180,7 +198,7 @@ public static class WalkOracleTrace private static readonly Regex ObjectCellTurnPattern = new(@"^OC ([0-9a-f]{8})\s*$", RegexOptions.Compiled); } -/// The camera pose dumped at the frame marker (raw dwords from +/// The literal camera pose dumped at the frame marker (raw dwords from /// Render::viewer_pos @0x0081ef00): the camera's cell id, world /// origin, and the Frame quaternion's four raw components (q0..q3 in /// storage order — axis convention resolved by the replay runner). @@ -196,8 +214,14 @@ public sealed record WalkOraclePose( public sealed record WalkOracleFrame( int Number, IReadOnlyList Events, - WalkOraclePose? Pose = null) + WalkOraclePose? Pose = null, + WalkOraclePose? ReplayPose = null) { + /// is the literal P record after this frame's + /// marker. is the following marker's P record, + /// the camera state this frame's events actually rendered with. The + /// parser fills it from the raw final marker without creating a fake + /// event frame. /// The frame's root: the first DrawInside cell, or null when outdoor-rooted. public uint? InteriorRootCell => Events.FirstOrDefault(e => e.Kind == WalkOracleEventKind.DrawInside)?.CellId; diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkOracleTraceTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkOracleTraceTests.cs index 71e1d240..396382d7 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkOracleTraceTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkOracleTraceTests.cs @@ -20,7 +20,7 @@ public sealed class WalkOracleTraceTests private const string OhRoot = "docs/research/2026-09-01-overhaul/oh-capture"; /// (root, fixture name) pairs. Every FW0 still/posed fixture - /// keeps its old bare name under ; the five OH + /// keeps its old bare name under ; the six OH /// walk captures (§11.2 B3) join under with the /// .walk filename-half suffix. public static readonly TheoryData AllFixtures = new() @@ -40,6 +40,7 @@ public sealed class WalkOracleTraceTests { OhRoot, "cathedral-arrival.walk" }, { OhRoot, "foundry-deep.walk" }, { OhRoot, "cathedral-leak.walk" }, + { OhRoot, "cathedral-stair-arch.walk" }, }; [Theory] @@ -55,6 +56,33 @@ public sealed class WalkOracleTraceTests Assert.All(frames, f => Assert.NotEmpty(f.Events)); } + [Fact] + public void Replay_pose_uses_next_marker_including_the_dropped_final_frame() + { + string[] lines = + [ + "F 1", + "P 11110100 3f800000 40000000 40400000 3f800000 00000000 00000000 00000000", + "LS", + "F 2", + "P 22220200 40800000 40a00000 40c00000 00000000 3f800000 00000000 00000000", + "LS", + "F 3", + "P 33330300 40e00000 41000000 41100000 00000000 00000000 3f800000 00000000", + ]; + + IReadOnlyList frames = WalkOracleTrace.Parse(lines); + + Assert.Equal(2, frames.Count); // F3 remains an incomplete event frame. + Assert.Equal(0x11110100u, frames[0].Pose!.CellId); + Assert.Equal(0x22220200u, frames[0].ReplayPose!.CellId); + Assert.Equal(new System.Numerics.Vector3(4f, 5f, 6f), frames[0].ReplayPose!.Origin); + Assert.Equal(0x22220200u, frames[1].Pose!.CellId); + Assert.Equal(0x33330300u, frames[1].ReplayPose!.CellId); + Assert.Equal(new System.Numerics.Vector3(7f, 8f, 9f), frames[1].ReplayPose!.Origin); + Assert.DoesNotContain(frames, frame => frame.Number == 3); + } + /// S3 chunk 1 fix round 1 (G8/G10): the "assert separately" /// half of 's /// own doc comment — that method's derivation never produces the diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkProjectionDiagnosticTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkProjectionDiagnosticTests.cs new file mode 100644 index 00000000..13f08f14 --- /dev/null +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkProjectionDiagnosticTests.cs @@ -0,0 +1,123 @@ +using System.Globalization; +using System.Numerics; +using AcDream.App.Rendering.Walk; + +namespace AcDream.App.Tests.Rendering.Walk; + +public sealed class WalkReplayProjectionTests +{ + private const string OhRoot = + "docs/research/2026-09-01-overhaul/oh-capture"; + + [Fact] + public void Captured_matrix_screen_ray_screen_round_trips_without_legacy_half_pixel() + { + WalkOracleFrame captured = Assert.Single(WalkOracleTrace.Load( + OhRoot, + "cathedral-stair-arch-e229.combined-r1")); + WalkOraclePose pose = Assert.IsType(captured.ReplayPose); + var context = new WalkTraceReplayContext( + pose, + new Dictionary()); + Vector2[] points = + [ + new(512f, 360f), + new(0f, 0f), + new(1024f, 0f), + new(1024f, 720f), + new(0f, 720f), + new(127.25f, 83.75f), + new(817.5f, 611.125f), + ]; + + foreach (Vector2 point in points) + { + Vector3 ray = context.Rays.RayThrough(point.X, point.Y); + Vector2 roundTrip = context.ProjectWorldToScreen( + pose.Origin + ray * 100f); + float error = Vector2.Distance(point, roundTrip); + Assert.True( + error <= 0.001f, + string.Create( + CultureInfo.InvariantCulture, + $"matrix round-trip error {error:R} at {point}")); + } + + Vector3 legacyCenterRay = LegacyRayThrough(pose, 512f, 360f); + Vector2 legacyCenter = context.ProjectWorldToScreen( + pose.Origin + legacyCenterRay * 100f); + Assert.InRange(MathF.Abs(legacyCenter.X - 512f), 0.45f, 0.55f); + Assert.InRange(MathF.Abs(legacyCenter.Y - 360f), 0.45f, 0.55f); + } + + [Fact] + public void Configured_projection_is_bound_to_captured_newmethod_matrix_words() + { + string[] lines = File.ReadAllLines(FixturePath( + "cathedral-stair-arch-e229.projection-mode")); + int newMethod = Array.FindIndex(lines, line => line.Trim() == "NEWMETHOD"); + Assert.True(newMethod >= 0); + Assert.Matches(@"\s00000001\s*$", lines[newMethod + 1]); + int heading = Array.FindIndex(lines, line => line.Trim() == "VIEW_TO_CLIP"); + Assert.True(heading >= 0); + uint[] captured = lines.Skip(heading + 1) + .Take(4) + .SelectMany(ParseDumpRow) + .ToArray(); + + Matrix4x4 configured = WalkTraceReplayContext.CapturedViewToClip(); + uint[] actual = + [ + Bits(configured.M11), Bits(configured.M12), Bits(configured.M13), Bits(configured.M14), + Bits(configured.M21), Bits(configured.M22), Bits(configured.M23), Bits(configured.M24), + Bits(configured.M31), Bits(configured.M32), Bits(configured.M33), Bits(configured.M34), + Bits(configured.M41), Bits(configured.M42), Bits(configured.M43), Bits(configured.M44), + ]; + + Assert.Equal(captured, actual); + } + + private static Vector3 LegacyRayThrough( + WalkOraclePose pose, + float screenX, + float screenY) + { + var rotation = new Quaternion(pose.Q1, pose.Q2, pose.Q3, pose.Q0); + Vector3 right = Vector3.Transform(Vector3.UnitX, rotation); + Vector3 forward = Vector3.Transform(Vector3.UnitY, rotation); + Vector3 up = Vector3.Transform(Vector3.UnitZ, rotation); + float u = screenX * 0.00025f - 0.127875f; + float w = screenY * 0.00025f - 0.089875f; + return right * u + forward * 0.1330766976f - up * w; + } + + private static IEnumerable ParseDumpRow(string line) + { + string[] fields = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(5, fields.Length); + return fields.Skip(1).Select(value => uint.Parse( + value, + NumberStyles.HexNumber, + CultureInfo.InvariantCulture)); + } + + private static uint Bits(float value) => + BitConverter.SingleToUInt32Bits(value); + + private static string FixturePath(string fixtureName) => Path.Combine( + FindRepositoryRoot(), + Path.Combine(OhRoot.Split('/')), + fixtureName + ".log"); + + private static string FindRepositoryRoot() + { + DirectoryInfo? directory = new(AppContext.BaseDirectory); + while (directory is not null) + { + if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx"))) + return directory.FullName; + directory = directory.Parent; + } + throw new InvalidOperationException("AcDream.slnx not found."); + } +} diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.AlphaDepthTranscript.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.AlphaDepthTranscript.cs index c3f9b89a..81591fc2 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.AlphaDepthTranscript.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.AlphaDepthTranscript.cs @@ -308,8 +308,7 @@ public sealed partial class WalkTraceConformanceTests Assert.NotEmpty(poseFrames); Assert.True(poseFrames.Count > 1, $"{fixtureName}: need a captured frame 2."); WalkOracleFrame poseFrame = poseFrames[1]; - Assert.NotNull(poseFrame.Pose); - WalkOraclePose pose = poseFrame.Pose!; + WalkOraclePose pose = Assert.IsType(poseFrame.ReplayPose); IReadOnlyList depthFrames = WalkAlphaDepthTrace.Load(OhCaptureRoot, fixtureName); Assert.True(depthFrames.Count > 1, $"{fixtureName}: need a captured frame 2's PM/PC content."); @@ -394,8 +393,7 @@ public sealed partial class WalkTraceConformanceTests Assert.NotEmpty(poseFrames); Assert.True(poseFrames.Count > 1, $"{fixtureName}: need a captured frame 2."); WalkOracleFrame poseFrame = poseFrames[1]; - Assert.NotNull(poseFrame.Pose); - WalkOraclePose pose = poseFrame.Pose!; + WalkOraclePose pose = Assert.IsType(poseFrame.ReplayPose); IReadOnlyList depthFrames = WalkAlphaDepthTrace.Load(OhCaptureRoot, fixtureName); Assert.True(depthFrames.Count > 1, $"{fixtureName}: need a captured frame 2's AM/FL content."); diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs index 4b78c5f3..a047357d 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs @@ -34,19 +34,18 @@ public sealed partial class WalkTraceConformanceTests /// ORACLE side () /// moved off derivation onto its own literal captured EC/OC lines. /// - /// KNOWN LIMIT (S3 review fix round 1, F4a): the production - /// transcript prints EC once per LIVE VIEW of a flood cell - /// (retail's setup_view/DrawEnvCell loop, - /// PView::DrawCells @0x005a4ab1–0x005a4acc), but this replay - /// side's still - /// derives ONE EC per cell — the sink event carries no per-cell - /// view count. Every fixture passes only because no captured flood cell - /// holds more than one live view at DrawCells time. A fixture with a - /// two-view flood cell would fail here on the second EC; S5's - /// transcript closeout carries the per-cell view count through the - /// sink event and removes this note. + /// C1c: the recorder borrows the real cell map and reads each + /// cell's live TopView.ViewCount while the flood callback still + /// owns it. That reproduces retail's setup_view/DrawEnvCell loop at + /// PView::DrawCells @0x005a4ab1–0x005a4acc without changing a + /// production event or deriving oracle EC/OC from DC. private sealed class Recorder : IWalkEventSink { + private readonly IReadOnlyDictionary _cells; + + public Recorder(IReadOnlyDictionary cells) => + _cells = cells; + public readonly List Events = new(); public readonly List Tokens = new(); @@ -78,7 +77,7 @@ public sealed partial class WalkTraceConformanceTests // CellStatic, _lookIn still false) defers to // OnInteriorFloodDrawTurn below — see that method. if (_lookIn == true) - WalkTraceReplayContext.AppendFloodTurns(Tokens, walkEvent.Cells); + WalkTraceReplayContext.AppendFloodTurns(Tokens, walkEvent.Cells, _cells); break; } } @@ -94,7 +93,7 @@ public sealed partial class WalkTraceConformanceTests public void OnBuildingTurn(WalkBuilding building) => _lookIn = true; public void OnInteriorFloodDrawTurn(IReadOnlyList cells, int outsideViewCount) - => WalkTraceReplayContext.AppendFloodTurns(Tokens, cells); + => WalkTraceReplayContext.AppendFloodTurns(Tokens, cells, _cells); /// S3 review fix round 1 (F4b): retail's trailing per-frame /// weather turn (GameSky::Draw(sky,1) @0x00506ff0, fired by @@ -132,17 +131,17 @@ public sealed partial class WalkTraceConformanceTests Assert.NotEmpty(frames); using DatCollection dats = OpenDats(); WalkOracleFrame frame = frames[1]; // frame 2: pose settled (marker timing) - Assert.NotNull(frame.Pose); + WalkOraclePose pose = Assert.IsType(frame.ReplayPose); WalkLandscapeDatBuilder.BuiltWorld world = - WalkLandscapeDatBuilder.Build(dats, frame.Pose!.CellId, frame.Pose.Origin); - var ctx = new WalkTraceReplayContext(frame.Pose, world.Cells) + WalkLandscapeDatBuilder.Build(dats, pose.CellId, pose.Origin); + var ctx = new WalkTraceReplayContext(pose, world.Cells) { Buildings = world.Buildings, }; var walk = new RetailFrameWalk(100f, 0.99f); - var recorder = new Recorder(); + var recorder = new Recorder(world.Cells); - walk.WalkFrame(frame.Pose.CellId, null, world.Landscape, ctx, recorder); + walk.WalkFrame(pose.CellId, null, world.Landscape, ctx, recorder); string expected = WalkTraceReplayContext.Signature(frame); string actual = WalkTraceReplayContext.Signature(recorder.Events); @@ -162,14 +161,14 @@ public sealed partial class WalkTraceConformanceTests Assert.NotEmpty(frames); using DatCollection dats = OpenDats(); WalkOracleFrame frame = frames[1]; - Assert.NotNull(frame.Pose); + WalkOraclePose pose = Assert.IsType(frame.ReplayPose); WalkLandscapeDatBuilder.BuiltWorld world = - WalkLandscapeDatBuilder.Build(dats, frame.Pose!.CellId, frame.Pose.Origin); - var ctx = new WalkTraceReplayContext(frame.Pose, world.Cells) + WalkLandscapeDatBuilder.Build(dats, pose.CellId, pose.Origin); + var ctx = new WalkTraceReplayContext(pose, world.Cells) { Buildings = world.Buildings, }; - WalkCell camera = Assert.Contains(frame.Pose.CellId, world.Cells); + WalkCell camera = Assert.Contains(pose.CellId, world.Cells); // Render::deg_mul is DYNAMIC (auto-tuned by frame load), and this // capture ran right after the heavy terrace-edge capture with the // multiplier depressed: at mul ≤ 0 the degrade thresholds sit at or @@ -179,9 +178,9 @@ public sealed partial class WalkTraceConformanceTests // max). The recon session's live dump read −0.99 under the same cdb // load. Re-dump deg_mul per capture at the next retail session. var walk = new RetailFrameWalk(100f, 0f); - var recorder = new Recorder(); + var recorder = new Recorder(world.Cells); - walk.WalkFrame(frame.Pose.CellId, camera, world.Landscape, ctx, recorder); + walk.WalkFrame(pose.CellId, camera, world.Landscape, ctx, recorder); string expected = WalkTraceReplayContext.Signature(frame); string actual = WalkTraceReplayContext.Signature(recorder.Events); @@ -191,48 +190,16 @@ public sealed partial class WalkTraceConformanceTests } /// - /// PINNED RED 2026-09-03 (S3 chunk 1 fix round 1, G10 — un-skip with the - /// fix): the eight-kind signature is the FIRST comparison ever run at - /// LC/SC placement/content against a DAT-built world for this fixture - /// (the old four-kind Signature filtered LC/SC out entirely — how the - /// G7 SC-ordering regression shipped green). One real divergence - /// survives after fixing the LandblockId=0 fixture bug this same round - /// uncovered (WalkLandscapeDatBuilder.cs — a genuinely separate, - /// already-fixed defect): at token index 165, the replay draws ONE - /// EXTRA land-cell pair retail never does — - /// …SC:a8c90001|LC:a8c90001|SC:a8c90001| retail jumps straight to - /// LC:95c60001; the replay inserts LC:a9c90001|SC:a9c90001 - /// first. A single spurious block admission at a ring-2/ring-3 LOD - /// boundary near this pose — a real WalkLandscape.CheckBlocks - /// visibility question, not a print-site or ordering bug, and out of - /// this round's print-only scope (§11.4) — needs the retail oracle - /// (decomp/cdb) to resolve. Filed as docs/ISSUES.md #458. - /// terrace-edge/cathedral-arrival/foundry-deep all reproduce EXACTLY at - /// this same eight-kind level; only this row's specific pose hits the - /// boundary. S3 chunk 1 fix round 2 (§11.6 H3) switched the ORACLE side - /// of the eight-kind signature from a derived EC/OC to retail's own - /// LITERAL captured EC/OC lines — re-verified 2026-09-03 that this - /// divergence stays at the SAME token index 165 (an LC/SC-only mismatch, - /// upstream of any EC/OC content) under the new comparison; #458's - /// position is unchanged. - /// - /// RESOLVED 2026-09-03 (lead, two live cdb captures — see - /// docs/ISSUES.md #458 and register row AD-118): it is a PRECISION - /// BOUNDARY, not a structural walk divergence. At this fixture pose the - /// replay's block test for a9c9 (ring 21 north, z slab 75..330) - /// has three of the four block corners OUTSIDE the doorway's fourth - /// edge plane and the south-west corner just INSIDE it, so - /// block_plane_check says PartiallyInside and the block draws; - /// retail's plane sits about 0.5% away (measured at a nearby pose: - /// retail's clip heights 300.4/310.2 m vs the replay's 298.8/308.5 m - /// for the same block, with the SAME sentinel pattern and the SAME - /// verdicts 1 then 0 in the two views) and puts that last corner - /// outside too. The row stays KnownFailure on purpose: the fixture is - /// retail's literal frame, and no bit-exact fix exists short of - /// reproducing D3D's x87 projection. + /// C1c supersedes the former #458 KnownFailure. The literal eight-kind + /// fixture exposed LC/SC:a9c90001 only because the replay used + /// Render::copy_view's inactive legacy-ray branch and paired the + /// events with the stale same-frame marker. Retail's captured + /// newmethod=1 matrix path plus the explicit next-marker replay + /// pose reproduces the complete frame exactly. The historical numeric + /// investigation remains in docs/ISSUES.md; it is not a current + /// production divergence or permission to relax this equality. /// [Fact] - [Trait("Status", "KnownFailure")] public void Oh_doorway_still_first_frame_diff() { // S3 chunk 1 (§11.2 B3): the OH kit pose's own doorway-still capture @@ -248,19 +215,19 @@ public sealed partial class WalkTraceConformanceTests "docs/research/2026-09-01-overhaul/oh-capture", "holtburg-doorway-still.walk"); Assert.NotEmpty(frames); using DatCollection dats = OpenDats(); - WalkOracleFrame frame = frames[1]; - Assert.NotNull(frame.Pose); + WalkOracleFrame frame = frames[0]; + WalkOraclePose pose = Assert.IsType(frame.ReplayPose); WalkLandscapeDatBuilder.BuiltWorld world = - WalkLandscapeDatBuilder.Build(dats, frame.Pose!.CellId, frame.Pose.Origin); - var ctx = new WalkTraceReplayContext(frame.Pose, world.Cells) + WalkLandscapeDatBuilder.Build(dats, pose.CellId, pose.Origin); + var ctx = new WalkTraceReplayContext(pose, world.Cells) { Buildings = world.Buildings, }; - WalkCell camera = Assert.Contains(frame.Pose.CellId, world.Cells); + WalkCell camera = Assert.Contains(pose.CellId, world.Cells); var walk = new RetailFrameWalk(100f, 0.99f); - var recorder = new Recorder(); + var recorder = new Recorder(world.Cells); - walk.WalkFrame(frame.Pose.CellId, camera, world.Landscape, ctx, recorder); + walk.WalkFrame(pose.CellId, camera, world.Landscape, ctx, recorder); // S3 chunk 1 fix round 1 (G10): the OH kit pose carries LC/SC/EC/OC // — compare at the full eight-kind level (see Signature8's own doc @@ -272,6 +239,93 @@ public sealed partial class WalkTraceConformanceTests $"walk diverged from retail\nEXPECTED: {expected}\nACTUAL: {actual}"); } + [Fact] + public void Oh_walk_corpus_reproduces_all_25_complete_frames_at_full_signature8() + { + string[] fixtures = + [ + "holtburg-doorway-still.walk", + "terrace-edge.walk", + "cathedral-arrival.walk", + "foundry-deep.walk", + "cathedral-leak.walk", + "cathedral-stair-arch.walk", + "cathedral-stair-arch-e229.combined-r1", + ]; + + using DatCollection dats = OpenDats(); + int comparedFrames = 0; + foreach (string fixture in fixtures) + { + IReadOnlyList frames = + WalkOracleTrace.Load(OhCaptureRoot, fixture); + Assert.Equal( + fixture == "cathedral-stair-arch-e229.combined-r1" ? 1 : 4, + frames.Count); + WalkOraclePose anchor = Assert.IsType(frames[0].ReplayPose); + WalkLandscapeDatBuilder.BuiltWorld world = + WalkLandscapeDatBuilder.Build(dats, anchor.CellId, anchor.Origin); + var walk = new RetailFrameWalk(100f, 0.99f); + + foreach (WalkOracleFrame frame in frames) + { + WalkOraclePose pose = Assert.IsType(frame.ReplayPose); + WalkLandscapeDatBuilder.SetViewer( + world.Landscape, pose.CellId, pose.Origin); + var context = new WalkTraceReplayContext(pose, world.Cells) + { + Buildings = world.Buildings, + }; + WalkCell? camera = null; + if ((pose.CellId & 0xFFFFu) >= 0x100u) + { + camera = Assert.Contains(pose.CellId, world.Cells); + } + var recorder = new Recorder(world.Cells); + + walk.WalkFrame( + pose.CellId, camera, world.Landscape, context, recorder); + + if (fixture == "cathedral-stair-arch.walk") + { + Assert.Equal(4, recorder.Tokens.Count(t => t == "EC:f4180106")); + Assert.Equal(2, recorder.Tokens.Zip(recorder.Tokens.Skip(1)) + .Count(pair => pair.First == "EC:f4180106" + && pair.Second == "EC:f4180106")); + Assert.DoesNotContain("LC:e2290001", recorder.Tokens); + Assert.DoesNotContain("SC:e2290001", recorder.Tokens); + } + + string expected = WalkTraceReplayContext.Signature8(frame); + string actual = WalkTraceReplayContext.Signature8(recorder.Tokens); + Assert.True( + expected == actual, + $"{fixture} frame {frame.Number} diverged at token " + + $"{FirstMismatch(expected, actual)}\n" + + $"EXPECTED: {expected}\nACTUAL: {actual}"); + comparedFrames++; + } + } + Assert.Equal(25, comparedFrames); + } + + private static string FirstMismatch(string expected, string actual) + { + string[] expectedTokens = expected.Split('|'); + string[] actualTokens = actual.Split('|'); + int common = Math.Min(expectedTokens.Length, actualTokens.Length); + int index = 0; + while (index < common && expectedTokens[index] == actualTokens[index]) + index++; + string expectedToken = index < expectedTokens.Length + ? expectedTokens[index] + : ""; + string actualToken = index < actualTokens.Length + ? actualTokens[index] + : ""; + return $"{index}: expected {expectedToken}, actual {actualToken}"; + } + private const string FwOracleRoot = "docs/research/2026-08-30-fw-walk-oracle"; /// S3 chunk 1 (§11.2 B3): the OH capture directory — its OWN @@ -294,20 +348,20 @@ public sealed partial class WalkTraceConformanceTests Assert.NotEmpty(frames); using DatCollection dats = OpenDats(); WalkOracleFrame frame = frames[1]; - Assert.NotNull(frame.Pose); + WalkOraclePose pose = Assert.IsType(frame.ReplayPose); WalkLandscapeDatBuilder.BuiltWorld world = - WalkLandscapeDatBuilder.Build(dats, frame.Pose!.CellId, frame.Pose.Origin); - var ctx = new WalkTraceReplayContext(frame.Pose, world.Cells) + WalkLandscapeDatBuilder.Build(dats, pose.CellId, pose.Origin); + var ctx = new WalkTraceReplayContext(pose, world.Cells) { Buildings = world.Buildings, }; - WalkCell? camera = (frame.Pose.CellId & 0xFFFFu) >= 0x100 - ? Assert.Contains(frame.Pose.CellId, world.Cells) + WalkCell? camera = (pose.CellId & 0xFFFFu) >= 0x100 + ? Assert.Contains(pose.CellId, world.Cells) : null; var walk = new RetailFrameWalk(100f, 0.99f); - var recorder = new Recorder(); + var recorder = new Recorder(world.Cells); - walk.WalkFrame(frame.Pose.CellId, camera, world.Landscape, ctx, recorder); + walk.WalkFrame(pose.CellId, camera, world.Landscape, ctx, recorder); // S3 chunk 1 fix round 1 (G10): the OH kit-pose rows carry // LC/SC/EC/OC — compare THOSE two rows at the full eight-kind level @@ -360,51 +414,39 @@ public sealed partial class WalkTraceConformanceTests IReadOnlyList frames = WalkOracleTrace.Load(fixture); Assert.True(frames.Count >= 3); using DatCollection dats = OpenDats(); - WalkOraclePose anchor = frames[1].Pose!; + WalkOraclePose anchor = Assert.IsType(frames[1].ReplayPose); WalkLandscapeDatBuilder.BuiltWorld world = WalkLandscapeDatBuilder.Build(dats, anchor.CellId, anchor.Origin); var walk = new RetailFrameWalk(100f, 0.99f); - for (int n = 1; n < frames.Count - 1; n++) + for (int n = 1; n < frames.Count; n++) { WalkOracleFrame frame = frames[n]; if (frame.Number >= stopBeforeFrame) break; string expected = WalkTraceReplayContext.Signature(frame); - // The marker dumps the PREVIOUS frame's camera, so frame N's - // true camera state lies between pose(N) and pose(N+1) — a - // capture artifact, not a port ambiguity. A frame passes when - // the replay matches under either adjacent pose. - string? firstActual = null; - bool matched = false; - foreach (WalkOraclePose pose in new[] { frames[n + 1].Pose!, frame.Pose! }) + // The parser pairs this event frame explicitly with P(N+1), + // including the final raw marker. Never try the stale same-frame + // marker as a fallback. + WalkOraclePose pose = Assert.IsType(frame.ReplayPose); + WalkLandscapeDatBuilder.SetViewer(world.Landscape, pose.CellId, pose.Origin); + var ctx = new WalkTraceReplayContext(pose, world.Cells) { - Assert.NotNull(pose); - WalkLandscapeDatBuilder.SetViewer(world.Landscape, pose.CellId, pose.Origin); - var ctx = new WalkTraceReplayContext(pose, world.Cells) - { - Buildings = world.Buildings, - }; - WalkCell? camera = null; - if ((pose.CellId & 0xFFFFu) >= 0x100) - { - Assert.True( - world.Cells.TryGetValue(pose.CellId, out camera), - $"frame {frame.Number}: interior camera cell {pose.CellId:x8} not loaded"); - } - var recorder = new Recorder(); - walk.WalkFrame(pose.CellId, camera, world.Landscape, ctx, recorder); - string actual = WalkTraceReplayContext.Signature(recorder.Events); - firstActual ??= actual; - if (actual == expected) - { - matched = true; - break; - } + Buildings = world.Buildings, + }; + WalkCell? camera = null; + if ((pose.CellId & 0xFFFFu) >= 0x100) + { + Assert.True( + world.Cells.TryGetValue(pose.CellId, out camera), + $"frame {frame.Number}: interior camera cell {pose.CellId:x8} not loaded"); } + var recorder = new Recorder(world.Cells); + walk.WalkFrame(pose.CellId, camera, world.Landscape, ctx, recorder); + string actual = WalkTraceReplayContext.Signature(recorder.Events); Assert.True( - matched, - $"frame {frame.Number} diverged under both adjacent poses ({fixture})\n" - + $"EXPECTED: {expected}\nACTUAL: {firstActual}"); + actual == expected, + $"frame {frame.Number} diverged at its explicit next-marker pose ({fixture})\n" + + $"EXPECTED: {expected}\nACTUAL: {actual}"); } } @@ -420,13 +462,13 @@ public sealed partial class WalkTraceConformanceTests foreach (WalkOracleFrame frame in frames) { - Assert.NotNull(frame.Pose); - WalkCell camera = Assert.Contains(frame.Pose!.CellId, cells); - var ctx = new WalkTraceReplayContext(frame.Pose, cells); + WalkOraclePose pose = Assert.IsType(frame.ReplayPose); + WalkCell camera = Assert.Contains(pose.CellId, cells); + var ctx = new WalkTraceReplayContext(pose, cells); var walk = new RetailFrameWalk(100f, 0.99f); - var recorder = new Recorder(); + var recorder = new Recorder(cells); - walk.WalkFrame(frame.Pose.CellId, camera, landscape, ctx, recorder); + walk.WalkFrame(pose.CellId, camera, landscape, ctx, recorder); Assert.Equal( WalkTraceReplayContext.Signature(frame), @@ -455,22 +497,22 @@ public sealed partial class WalkTraceConformanceTests "docs/research/2026-09-01-overhaul/oh-capture", "foundry-deep.walk"); Assert.NotEmpty(frames); using DatCollection dats = OpenDats(); - Assert.NotNull(frames[0].Pose); + WalkOraclePose anchor = Assert.IsType(frames[0].ReplayPose); WalkLandscapeDatBuilder.BuiltWorld world = WalkLandscapeDatBuilder.Build( - dats, frames[0].Pose!.CellId, frames[0].Pose!.Origin); + dats, anchor.CellId, anchor.Origin); foreach (WalkOracleFrame frame in frames) { - Assert.NotNull(frame.Pose); - WalkCell camera = Assert.Contains(frame.Pose!.CellId, world.Cells); - var ctx = new WalkTraceReplayContext(frame.Pose, world.Cells) + WalkOraclePose pose = Assert.IsType(frame.ReplayPose); + WalkCell camera = Assert.Contains(pose.CellId, world.Cells); + var ctx = new WalkTraceReplayContext(pose, world.Cells) { Buildings = world.Buildings, }; var walk = new RetailFrameWalk(100f, 0.99f); - var recorder = new Recorder(); + var recorder = new Recorder(world.Cells); - walk.WalkFrame(frame.Pose.CellId, camera, world.Landscape, ctx, recorder); + walk.WalkFrame(pose.CellId, camera, world.Landscape, ctx, recorder); // S3 chunk 1 fix round 1 (G10): full eight-kind comparison — // see Signature8's own doc comment. diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs index 588c8671..6dc553c4 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs @@ -19,28 +19,30 @@ namespace AcDream.App.Tests.Rendering.Walk; /// public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalkContext { - // Retail projection globals, dumped live from the capture client - // (recon 2026-08-30 evening: Render::bw/bh/xinvscale/yinvscale/tx/ty/vdst). + // The declared capture configuration. The active retail path is + // Render::copy_view @0054dfc0's newmethod=1 arm, which calls + // PrimD3DRender::ScreenToViewTransform @0059aa40 with the actual + // viewport and the inverse WorldToView/ViewToClip matrices. The old + // scalar xinvscale/tx ray is the other branch and is not equivalent: + // it shifts this configuration's center by about half a pixel. public const float RetailViewportWidth = 1024f; public const float RetailViewportHeight = 720f; - public const float RetailXInvScale = 0.00025f; - public const float RetailYInvScale = 0.00025f; - public const float RetailTx = 0.127875f; - public const float RetailTy = 0.089875f; - public const float RetailVdst = 0.1330766976f; - private sealed class RetailRayCaster( - Vector3 right, Vector3 forward, Vector3 up) : IWalkRayCaster + private sealed class CapturedD3dRayCaster( + Matrix4x4 inverseWorldToView, + Matrix4x4 viewToClip, + float viewportWidth, + float viewportHeight) : IWalkRayCaster { - // Retail's unproject (copy_view's ray path; equal to - // ScreenToViewTransform for these globals): - // u = sx·xinvscale − tx; w = sy·yinvscale − ty - // ray = Xaxis·u + Yaxis·vdst − Zaxis·w public Vector3 RayThrough(float screenX, float screenY) { - float u = screenX * RetailXInvScale - RetailTx; - float w = screenY * RetailYInvScale - RetailTy; - return right * u + forward * RetailVdst - up * w; + float ndcX = screenX * 2f / viewportWidth - 1f; + float ndcY = 1f - screenY * 2f / viewportHeight; + var view = new Vector3( + ndcX / viewToClip.M11, + ndcY / viewToClip.M22, + 1f); + return Vector3.TransformNormal(view, inverseWorldToView); } } @@ -73,21 +75,57 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk Vector3 up = Vector3.Transform(Vector3.UnitZ, rotation); Vector3 right = Vector3.Transform(Vector3.UnitX, rotation); - // The exact retail frustum: tan(halfFovY) = ty/vdst, aspect = tx/ty. - float fovY = 2f * MathF.Atan(RetailTy / RetailVdst); - Matrix4x4 view = Matrix4x4.CreateLookAt(pose.Origin, pose.Origin + forward, up); - Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView( - fovY, RetailTx / RetailTy, 0.1f, 5000f); + Matrix4x4 view = CreateLeftHandedWorldToView( + pose.Origin, right, up, forward); + Matrix4x4 projection = CapturedViewToClip(); _viewProjection = view * projection; ViewportWidth = RetailViewportWidth; ViewportHeight = RetailViewportHeight; - _rays = new RetailRayCaster(right, forward, up); + Assert.True(Matrix4x4.Invert(view, out Matrix4x4 inverseView)); + _rays = new CapturedD3dRayCaster( + inverseView, + projection, + ViewportWidth, + ViewportHeight); // The retail CY near plane: N = forward, d = −dot(eye, forward) − znear. CyPlane = new WalkPlane(forward, -Vector3.Dot(pose.Origin, forward) - 0.1f); } + internal Vector2 ProjectWorldToScreen(Vector3 point) + { + WalkScreenPoint screen = WalkScreenClip.TransformToScreen( + point, + _viewProjection, + ViewportWidth, + ViewportHeight); + return new Vector2(screen.X / screen.W, screen.Y / screen.W); + } + + private static Matrix4x4 CreateLeftHandedWorldToView( + Vector3 eye, + Vector3 right, + Vector3 up, + Vector3 forward) => new( + right.X, up.X, forward.X, 0f, + right.Y, up.Y, forward.Y, 0f, + right.Z, up.Z, forward.Z, 0f, + -Vector3.Dot(right, eye), + -Vector3.Dot(up, eye), + -Vector3.Dot(forward, eye), + 1f); + + internal static Matrix4x4 CapturedViewToClip() + { + static float F(uint bits) => BitConverter.UInt32BitsToSingle(bits); + return new Matrix4x4( + F(0x3F854304), 0f, 0f, 0f, + 0f, F(0x3FBD8723), 0f, 0f, + 0f, 0f, F(0x3F8000D2), F(0x3F800000), + 0f, 0f, F(0xBDCCCE1D), 0f); + } + /// Building placements (camera-block-local) for the landscape /// fixtures; empty for the interior-only ones. public Dictionary Buildings { get; set; } @@ -297,9 +335,12 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk } /// PView::DrawCells's two complete reverse loops: every - /// EnvCell shell far-to-near, THEN every object-list turn far-to-near — - /// the SAME reversed cell order twice (not one reversed EC/OC pair per - /// cell). S3 chunk 1 fix round 2 (§11.6 H3): this is now ONLY the + /// live view's EnvCell entry far-to-near, THEN every cell's object-list + /// turn far-to-near — the SAME reversed cell order twice, with EC + /// repeated by the cell's live TopView.ViewCount and OC once. + /// This reads the borrowed live cells while the flood callback still + /// owns their view stacks, matching @005a4ab1–005a4acc and + /// @005a4ade–005a4b1a. S3 chunk 1 fix round 2 (§11.6 H3): this is ONLY the /// REPLAY side's derivation — 's /// Recorder calls this at the exact hook that fires for each /// flood (a look-in's own DrawCells immediately; the interior @@ -310,10 +351,18 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk /// this — the ORACLE side reads its own captured EC/OC lines literally /// instead of re-deriving them (see that method's own doc comment for /// why: a derivation compared to itself proves nothing). - internal static void AppendFloodTurns(List tokens, IReadOnlyList cells) + internal static void AppendFloodTurns( + List tokens, + IReadOnlyList cells, + IReadOnlyDictionary liveCells) { for (int i = cells.Count - 1; i >= 0; i--) - tokens.Add($"EC:{cells[i]:x8}"); + { + WalkCell cell = liveCells[cells[i]]; + int liveViewCount = cell.NumView > 0 ? cell.TopView.ViewCount : 0; + for (int view = 0; view < liveViewCount; view++) + tokens.Add($"EC:{cells[i]:x8}"); + } for (int i = cells.Count - 1; i >= 0; i--) tokens.Add($"OC:{cells[i]:x8}"); }