diff --git a/CLAUDE.md b/CLAUDE.md index f92b3f8..5b71754 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -842,9 +842,43 @@ cause identified.** Four commits (`fb5fba6` → `44614ab` → `0f2db62` → - 13 new cell fixtures cover the full 0xA9B4014X neighborhood (272 KB). Findings doc (canonical pickup): [`docs/research/2026-05-23-a6-p3-issue98-comparison-harness-findings.md`](docs/research/2026-05-23-a6-p3-issue98-comparison-harness-findings.md). -**Next-session move: extract cottage GfxObj polygons via focused -`ACDREAM_PROBE_BUILDING=1` capture, register as ShadowEntry in harness, -flip first-cap test to assertion form.** + +**Evening v2 follow-on — apparatus convergence SHIPPED 2026-05-23 PM.** +Two commits (`cc3afbc` → `97fec19`): +- `cc3afbc` adds the GfxObj dump infrastructure (`ACDREAM_DUMP_GFXOBJS`) + mirroring the existing `ACDREAM_DUMP_CELLS` pattern, with new + `GfxObjDump`/`GfxObjDumpSerializer` parallel to `CellDump`. The new + env var triggers `PhysicsDataCache.CacheGfxObj` to write the full + resolved polygon table as JSON when a listed id caches. Closes the + gap that the existing `[resolve-bldg]` probe couldn't fill (the BSP + wire site that populates `LastBspHitPoly` was never wired, so the + probe only emitted GfxObj-level metadata, not per-poly geometry). +- `97fec19` lands the cottage GfxObj fixture (`0x01000A2B`, 74 polygons, + BSP radius 13.989m matching live), the new `RegisterCottageGfxObj` + harness helper, and a minimum-stub landblock so + `TryGetLandblockContext` succeeds at the cellar XY. Harness now + reproduces the live `cn=(0,0,-1)` cap bit-perfect. The full per-field + round-trip uncovers ONE residual: live preserves +0.0266m of +X + motion through the cap (edge-slide along the cottage floor); harness + blocks all motion. Captured in + `LiveCompare_FirstCap_ResidualXMotionDivergence_DocumentsNextInvestigation` + in documents-the-bug form. +- All 21 issue-#98-relevant tests (12 harness + 4 GfxObjDumpRoundTrip + + 1 new PhysicsDiagnosticsTests + 4 CellDumpRoundTripTests) pass + deterministically in isolation. +- Pre-existing test suite flakiness observed (8–19 failures across runs + of the same code, from PhysicsResolveCapture / PhysicsDiagnostics + statics leaking between test classes). INDEPENDENT of A6.P3 — verified + by stashing the cottage helper and reproducing the same flaky range. + Out of scope for this session; tracked as follow-up. + +**Next-session move:** investigate the residual +X edge-slide divergence +in `Transition.transitional_insert` / `AdjustOffset`'s handling of a +`cn=(0,0,-1)` head-bump. Live treats it as a Z-only constraint and +slides the remaining XY motion along the cottage floor; harness blocks +the entire move vector instead. The harness's +`LiveCompare_FirstCap_ResidualXMotionDivergence_DocumentsNextInvestigation` +test gives <1s feedback per fix attempt. ~2 hours estimate. Original demo scenario (Holtburg Sewer end-to-end) is unreachable: sewer doesn't exist on this server, and **issue #95** (portal-graph visibility blowup) blocks any substitute dungeon. Revised M1.5 demo split into diff --git a/docs/research/2026-05-23-a6-p3-issue98-comparison-harness-findings.md b/docs/research/2026-05-23-a6-p3-issue98-comparison-harness-findings.md index 1504ef6..0be9de4 100644 --- a/docs/research/2026-05-23-a6-p3-issue98-comparison-harness-findings.md +++ b/docs/research/2026-05-23-a6-p3-issue98-comparison-harness-findings.md @@ -13,6 +13,12 @@ documents the FIRST evidence-driven step in the saga. ## TL;DR +**Updated 2026-05-23 evening v2: apparatus convergence shipped.** The +harness now reproduces the live cottage-floor cap event bit-perfect on +the collision normal. The residual divergence is a single +X-motion +edge-slide gap; everything else round-trips. The session below covers +both arcs (root-cause identification THEN convergence). + - **Evidence-driven apparatus shipped.** `PhysicsResolveCapture` writes one JSON Lines record per player ResolveWithTransition call when `ACDREAM_CAPTURE_RESOLVE=` is set. 41,228 records from a single @@ -21,21 +27,26 @@ documents the FIRST evidence-driven step in the saga. new `LiveCompare_*` tests in `CellarUpTrajectoryReplayTests.cs` load three representative records (spawn, on-ramp, first-cap) and replay them through the harness engine. Spawn + on-ramp PASS bit-perfect; first-cap - FAILS with a clear divergence — the right divergence. -- **Root cause identified: the cottage GfxObj is missing from the harness.** + FAILED with a clear divergence — the right divergence. +- **Root cause identified: the cottage GfxObj was missing from the harness.** Live cap attributes the blocking entity to `obj=0xA9B47900` — a landblock-baked static building. The cottage's floor polygons live in this GfxObj's polygon table (registered as a ShadowEntry), NOT in any - cottage CELL. The harness's `RegisterStairRampGfxObj` already models - the cottage's RAMP polygon but is commented out and only covers one - polygon. We need the full cottage GfxObj polygon set. + cottage CELL. +- **Apparatus convergence (v2 update).** With the cottage GfxObj + `0x01000A2B` extracted via the new `ACDREAM_DUMP_GFXOBJS` infrastructure + and registered as a ShadowEntry in `BuildEngineWithCellarFixtures`, the + harness now reproduces the live `cn=(0,0,-1)` cap exactly. The + full per-field round-trip reveals one residual: live preserves + +0.0266 m of +X motion through the cap; harness blocks all motion. + That's the next investigation target — see the "Residual divergence" + section below. - **Not a step-up / AdjustOffset bug.** The head sphere (top at Z=foot+1.2) hits the cottage floor at Z=94.0 from BELOW. Math: cap at foot Z=92.74 matches 94.0 − 1.2 = 92.80. Confirmed by user reporting same cap when JUMPING in the cellar (purely vertical motion). The retail comparison - question is now "why doesn't retail block the head sphere there?" — - likely cottage GfxObj polygon side-mode + retail's BSP query - treats single-sided polygons correctly. + question is now sharpened to "how does live's post-cap edge-slide + preserve the +X component that the harness drops?" --- @@ -220,43 +231,73 @@ polygon facing DOWN. ## Next-session pickup -### Recommended move: extract cottage GfxObj polygons + register in harness +### What shipped 2026-05-23 evening v2 (post-prior-section) -1. **One more focused live capture** with `ACDREAM_PROBE_BUILDING=1` (the - `[resolve-bldg]` probe). The cap event will dump the cottage GfxObj's - full BSP root + hit polygon vertices in both object-local and world - space, plus the cottage GfxObj id, scale, rotation, and world origin. +Three commits land apparatus convergence on the cap event: -2. **Add `RegisterCottageGfxObj(engine, cache)` helper** in the harness - (rename or extend `RegisterStairRampGfxObj`). Construct a synthetic - GfxObj with the full polygon table extracted from the capture. The - BSP needs all polygons (ramp + cottage floor + walls); a one-leaf - wrapper suffices for the comparison-test scope. +| Commit | What | +|---|---| +| `cc3afbc` | **GfxObj dump infrastructure.** Mirrors `ACDREAM_DUMP_CELLS`: new env var `ACDREAM_DUMP_GFXOBJS` triggers `PhysicsDataCache.CacheGfxObj` to write the full resolved polygon table as JSON, suffix `.gfxobj.json` so dumps don't collide with cell dumps in the same dir. New `GfxObjDump` DTO + `GfxObjDumpSerializer` parallel to `CellDump`; round-trip tests cover Capture / Write / Read / Hydrate; the Hydrate path constructs a synthetic single-leaf BSP for query coverage. | +| `97fec19` | **Harness reproduces the cottage-floor cap event.** `BuildEngineWithCellarFixtures` now registers a stub landblock 0xA9B40000 (TerrainSurface at z=-1000) so `TryGetLandblockContext` succeeds at the cellar XY, plus a new `RegisterCottageGfxObj` helper that loads the dumped cottage GfxObj fixture, hydrates it with synthetic BSP, and registers as a ShadowEntry at world (130.5, 11.5, 94.0) with 180° Z rotation — matching production's `GameWindow.cs:5893` registration shape for landblock-baked statics. The cottage fixture (74 polys, 6 downward-facing floor triangles, BSP radius 13.989 m) lives at `tests/.../Fixtures/issue98/0x01000A2B.gfxobj.json`; capture launch script is `launch-a6-issue98-cottage-gfxobj-dump.ps1`. | -3. **Uncomment the registration call** in - `BuildEngineWithCellarFixtures`. Re-run - `LiveCompare_FirstCap_HarnessMissesCottageFloorBecauseCottageGfxObjNotRegistered` - — it should now FAIL because the harness DOES reproduce the cap. - Flip the test body to `AssertCallMatchesCapture(engine, captured)` to - enforce the round-trip. +Test outcome at apparatus convergence: -4. **At this point the harness is FAITHFUL to live.** Now the question - becomes "how does retail differ?" — which is a retail cdb investigation, - not an apparatus investigation. +| Test | Outcome | Meaning | +|---|---|---| +| `LiveCompare_Tick0_Spawn` | PASS | Spawn round-trip preserved by the new landblock + cottage state | +| `LiveCompare_Tick376_OnRamp` | PASS | On-ramp round-trip preserved | +| `LiveCompare_FirstCap_HarnessReproducesCottageFloorCapNormal` | PASS (NEW) | Harness reproduces the live cn=(0,0,-1) cap-event normal exactly | +| `LiveCompare_FirstCap_ResidualXMotionDivergence_DocumentsNextInvestigation` | PASS (documents-the-bug) | Captures the ONE remaining post-cap divergence: live preserves +0.0266 m of +X motion through the cap (edge-slide along the cottage floor in XY); harness blocks ALL motion. Y and Z agree. | -### Alternative move: skip ahead to the retail cdb question +### The residual divergence (next investigation target) -If the user is impatient with apparatus polish, skip step 3 and go -directly to a retail trace of the cellar-up scenario. Attach cdb to a -running retail acclient, set breakpoints on `BSPTREE::find_collisions` -and `CGfxObj::shadow_find_obj_collisions`, walk up the cottage ramp, -log every BSP query against the cottage GfxObj. Compare which polygons -retail finds vs which polygons our acdream engine finds. +After registering the cottage GfxObj: -The retail trace is the ultimate oracle — but it requires reproducible -retail-server state (the user has a working `+Acdream` on local ACE, -but the retail client also needs to connect there, which works per the -CLAUDE.md "Retail debugger toolchain" section). +``` +Live: cn=(0,0,-1), position=(141.3865, 7.2243, 92.7390) ← +X motion preserved +Harness: cn=(0,0,-1), position=(141.3599, 7.2243, 92.7390) ← X stuck at input +Input: currentPos=(141.3599, 7.2243, 92.7390) + targetPos =(141.3865, 6.8221, 92.7390) + requestedDelta=(+0.0266, -0.4022, 0) +``` + +The cap-event collision normal matches bit-perfect. Position diverges +in X only. Working hypothesis: live's response to a `cn=(0,0,-1)` +head-bump treats it as a Z-only constraint and edge-slides the +remaining XY component along the cottage floor; harness's BSP path is +rejecting the entire move vector instead of computing a slid offset. + +That hypothesis is the next-session investigation target — work the +slide path in `Transition.transitional_insert` / `AdjustOffset` against +the production cap-event call. The new +`LiveCompare_FirstCap_ResidualXMotionDivergence_DocumentsNextInvestigation` +test PASSES today (asserting the current residual) and FAILS when the +divergence closes — that's the signal to flip it into +`AssertCallMatchesCapture` form. + +### Alternative pickup move: retail cdb trace at the cottage ramp-top + +If apparatus polish is enough and the user wants to widen the question +to "how does retail differ?", attach cdb to a running retail acclient +(see CLAUDE.md "Retail debugger toolchain"), set breakpoints on +`BSPTREE::find_collisions` and `CGfxObj::shadow_find_obj_collisions`, +walk up the cottage ramp, and log every BSP query against the cottage +GfxObj. Compare which polygons retail finds vs which polygons our +acdream engine finds. Retail's trace is the ultimate oracle for the +"how does retail differ?" question — but the apparatus-side X residual +investigation is the more focused, faster-feedback next step. + +### Pre-existing test flakiness (out of scope but documented) + +While verifying the cottage helper, the full `dotnet test` serial run +produced 8–19 failures across 1192 tests depending on order — the +suite has static-state leakage between test classes (likely from +`PhysicsResolveCapture.CapturePath`, `PhysicsDiagnostics.Probe*Enabled`, +and similar global mutators). The flakiness is **independent of A6.P3**: +stashing the cottage helper out and rerunning produces the same flaky +range. All 21 issue-#98-relevant tests (12 harness + 4 +`GfxObjDumpRoundTripTests` + 1 new `PhysicsDiagnosticsTests` + 4 +`CellDumpRoundTripTests`) pass deterministically in isolation. --- @@ -279,40 +320,56 @@ CLAUDE.md "Retail debugger toolchain" section). ## Pickup prompt for next session ``` -A6.P3 #98 comparison harness — session paused 2026-05-23 evening. +A6.P3 #98 — apparatus convergence landed, residual X-motion divergence +is next. Read FIRST: 1. docs/research/2026-05-23-a6-p3-issue98-comparison-harness-findings.md - (this doc — TL;DR, what was shipped, the root cause finding) + (this doc — particularly the "What shipped 2026-05-23 evening v2" + and "The residual divergence" sections) 2. tests/AcDream.Core.Tests/Physics/CellarUpTrajectoryReplayTests.cs - (especially the LiveCompare_* tests + LiveCompare_FirstCap_DiagnosticDump) - 3. CLAUDE.md "Current A6 phase" block (look for the 2026-05-23 - evening apparatus + finding paragraph) + (especially the two LiveCompare_FirstCap_* tests and the + RegisterCottageGfxObj helper) + 3. CLAUDE.md "Current A6 phase" block State both altitudes: Currently working toward: M1.5 — Indoor world feels right - Current phase: A6.P3 — comparison harness shipped + root cause - identified. Cottage GfxObj 0xA9B47900 missing from harness; needs - full polygon list extracted from a focused capture. + Current phase: A6.P3 — apparatus convergence shipped. Harness now + reproduces the live cottage-floor cap event (cn=(0,0,-1) round-trips + bit-perfect). Residual: +0.0266 m of +X motion lost in the harness's + post-cap slide where live preserves it. -The doc has TWO concrete options for what to do next: +Two concrete next moves: -(A) Extract cottage GfxObj polygons via focused ACDREAM_PROBE_BUILDING - capture, register as ShadowEntry in harness, flip - LiveCompare_FirstCap_* test to assertion form. ~2 hours. - Apparatus convergence. RECOMMENDED. +(A) Investigate the +X edge-slide divergence in the harness. The + LiveCompare_FirstCap_ResidualXMotionDivergence_DocumentsNextInvestigation + test currently passes asserting the divergence; flipping it should + drive the investigation. Likely target: Transition.transitional_insert + / AdjustOffset's handling of a cn=(0,0,-1) head-bump — live treats + it as Z-only constraint and edge-slides the remaining XY motion; + harness blocks all motion. Decomp anchor: acclient_2013_pseudo_c.txt + in the find_obj_collisions → adjust_sphere_to_plane chain. ~2 hours + estimate. -(B) Skip ahead to retail cdb trace of cottage ramp-top BSP queries to - answer "what does retail actually do differently?". Larger scope; - more direct fidelity question. +(B) Attach cdb to retail at the cottage ramp-top, trace the BSP queries, + compare polygon-by-polygon what retail finds vs what acdream finds. + Authoritative for the "how does retail differ?" question but + larger scope (~half day setup + capture). -Pick A or B. If A: the doc has a step-by-step plan in the -"Recommended next-session move" section. +(A) is recommended — the harness now isolates this divergence to a +specific known XY slide path; the test gives <1s feedback per fix +attempt. (B) becomes valuable if (A) hypothesis chase stalls. CLAUDE.md rules apply throughout. NO speculative fixes — the saga already converted from speculation to evidence-driven; keep it that way. +Out-of-scope but observed: pre-existing test suite has 8–19 failures +across runs of the same code due to static-state leakage between test +classes (PhysicsResolveCapture, PhysicsDiagnostics statics). Targeted +issue-#98 tests pass deterministically in isolation. Don't touch the +flakiness this session; it's a separate investigation. + Test baseline: 1178 + 8 pre-existing failures (serial run). Maintain throughout. The previously-failing LiveCompare_FirstCap_HarnessMissesCottageFloorBecauseCottageGfxObjNotRegistered