docs: A6.P3 #98 — record apparatus convergence + residual X-motion

The findings doc gets an evening-v2 follow-on documenting:
  - GfxObj dump infrastructure shipped (cc3afbc)
  - Harness reproduces cap-event collision normal (97fec19)
  - Residual +0.0266m X-motion divergence — the new investigation target
  - Pre-existing test suite flakiness (out of scope, tracked separately)

CLAUDE.md's "Current A6 phase" block points at the residual divergence
as the next concrete move with the test that gives <1s feedback per fix
attempt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-23 20:47:17 +02:00
parent 97fec19dbb
commit 7729bdcf98
2 changed files with 150 additions and 59 deletions

View file

@ -842,9 +842,43 @@ cause identified.** Four commits (`fb5fba6` → `44614ab` → `0f2db62` →
- 13 new cell fixtures cover the full 0xA9B4014X neighborhood (272 KB). - 13 new cell fixtures cover the full 0xA9B4014X neighborhood (272 KB).
Findings doc (canonical pickup): 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). [`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, **Evening v2 follow-on — apparatus convergence SHIPPED 2026-05-23 PM.**
flip first-cap test to assertion form.** 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 (819 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 Original demo scenario (Holtburg Sewer end-to-end) is unreachable: sewer
doesn't exist on this server, and **issue #95** (portal-graph visibility doesn't exist on this server, and **issue #95** (portal-graph visibility
blowup) blocks any substitute dungeon. Revised M1.5 demo split into blowup) blocks any substitute dungeon. Revised M1.5 demo split into

View file

@ -13,6 +13,12 @@ documents the FIRST evidence-driven step in the saga.
## TL;DR ## 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 - **Evidence-driven apparatus shipped.** `PhysicsResolveCapture` writes one
JSON Lines record per player ResolveWithTransition call when JSON Lines record per player ResolveWithTransition call when
`ACDREAM_CAPTURE_RESOLVE=<path>` is set. 41,228 records from a single `ACDREAM_CAPTURE_RESOLVE=<path>` 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 new `LiveCompare_*` tests in `CellarUpTrajectoryReplayTests.cs` load three
representative records (spawn, on-ramp, first-cap) and replay them representative records (spawn, on-ramp, first-cap) and replay them
through the harness engine. Spawn + on-ramp PASS bit-perfect; first-cap through the harness engine. Spawn + on-ramp PASS bit-perfect; first-cap
FAILS with a clear divergence — the right divergence. FAILED with a clear divergence — the right divergence.
- **Root cause identified: the cottage GfxObj is missing from the harness.** - **Root cause identified: the cottage GfxObj was missing from the harness.**
Live cap attributes the blocking entity to `obj=0xA9B47900` — a Live cap attributes the blocking entity to `obj=0xA9B47900` — a
landblock-baked static building. The cottage's floor polygons live in landblock-baked static building. The cottage's floor polygons live in
this GfxObj's polygon table (registered as a ShadowEntry), NOT in any this GfxObj's polygon table (registered as a ShadowEntry), NOT in any
cottage CELL. The harness's `RegisterStairRampGfxObj` already models cottage CELL.
the cottage's RAMP polygon but is commented out and only covers one - **Apparatus convergence (v2 update).** With the cottage GfxObj
polygon. We need the full cottage GfxObj polygon set. `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) - **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 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 matches 94.0 1.2 = 92.80. Confirmed by user reporting same cap when
JUMPING in the cellar (purely vertical motion). The retail comparison JUMPING in the cellar (purely vertical motion). The retail comparison
question is now "why doesn't retail block the head sphere there?" — question is now sharpened to "how does live's post-cap edge-slide
likely cottage GfxObj polygon side-mode + retail's BSP query preserve the +X component that the harness drops?"
treats single-sided polygons correctly.
--- ---
@ -220,43 +231,73 @@ polygon facing DOWN.
## Next-session pickup ## 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 Three commits land apparatus convergence on the cap event:
`[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.
2. **Add `RegisterCottageGfxObj(engine, cache)` helper** in the harness | Commit | What |
(rename or extend `RegisterStairRampGfxObj`). Construct a synthetic |---|---|
GfxObj with the full polygon table extracted from the capture. The | `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. |
BSP needs all polygons (ramp + cottage floor + walls); a one-leaf | `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`. |
wrapper suffices for the comparison-test scope.
3. **Uncomment the registration call** in Test outcome at apparatus convergence:
`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.
4. **At this point the harness is FAITHFUL to live.** Now the question | Test | Outcome | Meaning |
becomes "how does retail differ?" — which is a retail cdb investigation, |---|---|---|
not an apparatus investigation. | `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 After registering the cottage GfxObj:
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.
The retail trace is the ultimate oracle — but it requires reproducible ```
retail-server state (the user has a working `+Acdream` on local ACE, Live: cn=(0,0,-1), position=(141.3865, 7.2243, 92.7390) ← +X motion preserved
but the retail client also needs to connect there, which works per the Harness: cn=(0,0,-1), position=(141.3599, 7.2243, 92.7390) ← X stuck at input
CLAUDE.md "Retail debugger toolchain" section). 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 819 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 ## 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: Read FIRST:
1. docs/research/2026-05-23-a6-p3-issue98-comparison-harness-findings.md 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 2. tests/AcDream.Core.Tests/Physics/CellarUpTrajectoryReplayTests.cs
(especially the LiveCompare_* tests + LiveCompare_FirstCap_DiagnosticDump) (especially the two LiveCompare_FirstCap_* tests and the
3. CLAUDE.md "Current A6 phase" block (look for the 2026-05-23 RegisterCottageGfxObj helper)
evening apparatus + finding paragraph) 3. CLAUDE.md "Current A6 phase" block
State both altitudes: State both altitudes:
Currently working toward: M1.5 — Indoor world feels right Currently working toward: M1.5 — Indoor world feels right
Current phase: A6.P3 — comparison harness shipped + root cause Current phase: A6.P3 — apparatus convergence shipped. Harness now
identified. Cottage GfxObj 0xA9B47900 missing from harness; needs reproduces the live cottage-floor cap event (cn=(0,0,-1) round-trips
full polygon list extracted from a focused capture. 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 (A) Investigate the +X edge-slide divergence in the harness. The
capture, register as ShadowEntry in harness, flip LiveCompare_FirstCap_ResidualXMotionDivergence_DocumentsNextInvestigation
LiveCompare_FirstCap_* test to assertion form. ~2 hours. test currently passes asserting the divergence; flipping it should
Apparatus convergence. RECOMMENDED. 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 (B) Attach cdb to retail at the cottage ramp-top, trace the BSP queries,
answer "what does retail actually do differently?". Larger scope; compare polygon-by-polygon what retail finds vs what acdream finds.
more direct fidelity question. 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 (A) is recommended — the harness now isolates this divergence to a
"Recommended next-session move" section. 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 CLAUDE.md rules apply throughout. NO speculative fixes — the saga
already converted from speculation to evidence-driven; keep it that already converted from speculation to evidence-driven; keep it that
way. way.
Out-of-scope but observed: pre-existing test suite has 819 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). Test baseline: 1178 + 8 pre-existing failures (serial run).
Maintain throughout. The previously-failing Maintain throughout. The previously-failing
LiveCompare_FirstCap_HarnessMissesCottageFloorBecauseCottageGfxObjNotRegistered LiveCompare_FirstCap_HarnessMissesCottageFloorBecauseCottageGfxObjNotRegistered