From 97fec19dbb2847f5637a34a4f747a640c8b67ea8 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 23 May 2026 20:44:50 +0200 Subject: [PATCH] =?UTF-8?q?test(phys):=20A6.P3=20#98=20=E2=80=94=20compari?= =?UTF-8?q?son=20harness=20reproduces=20cottage-floor=20cap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apparatus convergence. With the cottage GfxObj 0x01000A2B registered as a ShadowEntry in BuildEngineWithCellarFixtures, the harness now reproduces the live cap-event collision normal (cn=(0,0,-1)) exactly, ending the "harness doesn't reproduce" divergence the prior session's findings doc identified. Concretely: * Adds a minimum-stub landblock (TerrainSurface at z=-1000) so TryGetLandblockContext succeeds at the cellar XY — production's FindObjCollisions early-returns without a landblock and would skip the cottage shadow query. * Adds RegisterCottageGfxObj that loads the 74-polygon cottage fixture via GfxObjDumpSerializer.Hydrate, then registers it at the cottage's world transform (translation (130.5, 11.5, 94.0) + 180° around Z, derived from the cellar cell's WorldTransform), matching GameWindow.cs:5893's landblock-baked-static registration shape. * LiveCompare_FirstCap_HarnessMissesCottageFloorBecauseCottageGfxObjNotRegistered flips: the cap-normal reproduction is now enforced by LiveCompare_FirstCap_HarnessReproducesCottageFloorCapNormal. * The full per-field round-trip uncovered ONE residual divergence: live preserves +0.0266m of +X motion through the cap event (edge- slide along the floor in XY); harness blocks ALL motion at the cap. Captured by LiveCompare_FirstCap_ResidualXMotionDivergence_Docs... in documents-the-bug form so the next session has a concrete next target. Fixture: tests/AcDream.Core.Tests/Fixtures/issue98/0x01000A2B.gfxobj.json (74 polygons, 6 downward-facing cottage-floor triangles at object-local Z=0, BSP radius 13.989m matching the live [resolve-bldg] bspR=13.99). Captured via launch-a6-issue98-cottage-gfxobj-dump.ps1. In-isolation: all 12 CellarUpTrajectoryReplayTests + 4 GfxObjDumpRoundTripTests + 1 new PhysicsDiagnosticsTests pass. Note on full-suite baseline: the full xUnit serial run shows 8–19 failures depending on order (pre-existing test interaction with shared statics across PlayerMovementControllerTests, MotionInterpreterTests, PositionManagerTests, etc.). The flakiness is independent of this change — confirmed by stashing the harness changes and observing the same flaky range. Investigating the static-state isolation problem is out of scope for issue #98; tracked as a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) --- launch-a6-issue98-cottage-gfxobj-dump.ps1 | 46 + .../Fixtures/issue98/0x01000A2B.gfxobj.json | 2346 +++++++++++++++++ .../Physics/CellarUpTrajectoryReplayTests.cs | 326 ++- 3 files changed, 2586 insertions(+), 132 deletions(-) create mode 100644 launch-a6-issue98-cottage-gfxobj-dump.ps1 create mode 100644 tests/AcDream.Core.Tests/Fixtures/issue98/0x01000A2B.gfxobj.json diff --git a/launch-a6-issue98-cottage-gfxobj-dump.ps1 b/launch-a6-issue98-cottage-gfxobj-dump.ps1 new file mode 100644 index 0000000..9917d81 --- /dev/null +++ b/launch-a6-issue98-cottage-gfxobj-dump.ps1 @@ -0,0 +1,46 @@ +$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call" +$env:ACDREAM_LIVE = '1' +$env:ACDREAM_TEST_HOST = '127.0.0.1' +$env:ACDREAM_TEST_PORT = '9000' +$env:ACDREAM_TEST_USER = 'testaccount' +$env:ACDREAM_TEST_PASS = 'testpassword' + +# A6.P3 #98 (2026-05-23 evening v2) — focused capture of the cottage +# GfxObj 0x01000A2B's full polygon table. ACDREAM_DUMP_GFXOBJS triggers +# a one-shot JSON dump the first time PhysicsDataCache.CacheGfxObj fires +# for the listed id. The dump lands in the tests' fixture directory under +# the worktree, so the harness can load it without copying. +# +# Reproduction steps for the user: +# 1. Run this script (it launches in the foreground; log streams to the +# file path below). +# 2. Log into +Acdream. The cottage GfxObj caches when the streaming +# worker walks the cottage building's mesh — which happens as soon +# as the cottage landblock enters the streaming N1 (near) tier. +# Holtburg's cottage (the one with the cellar) is at the spawn area, +# so just being in-world is enough. +# 3. Watch the log for "[gfxobj-dump] wrote 0x01000A2B polys=N → ..." +# then close the client. +# +# After the dump file exists at +# tests/AcDream.Core.Tests/Fixtures/issue98/0x01000A2B.gfxobj.json +# come back to Claude to continue with the RegisterCottageGfxObj wiring. + +$env:ACDREAM_DUMP_GFXOBJS = '0x01000A2B' + +# Output dir is the relative fixture path; the dump infrastructure +# resolves it against the worktree current dir (Set-Location below). +$env:ACDREAM_DUMP_GFXOBJS_DIR = 'tests/AcDream.Core.Tests/Fixtures/issue98' + +# Keep the cell-transit probe on so the launch log shows when the player +# enters cells — helps correlate the dump event with player position. +$env:ACDREAM_PROBE_CELL = '1' + +$logPath = 'C:\Users\erikn\source\repos\acdream\.claude\worktrees\strange-albattani-3fc83c\a6-issue98-cottage-gfxobj-dump-launch.log' +Write-Host "Log path: $logPath" +Write-Host "Dump target: $env:ACDREAM_DUMP_GFXOBJS_DIR\0x01000A2B.gfxobj.json" +Write-Host '' +Write-Host 'After login, watch the log for [gfxobj-dump] then close the client.' + +Set-Location 'C:\Users\erikn\source\repos\acdream\.claude\worktrees\strange-albattani-3fc83c' +dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug *> $logPath diff --git a/tests/AcDream.Core.Tests/Fixtures/issue98/0x01000A2B.gfxobj.json b/tests/AcDream.Core.Tests/Fixtures/issue98/0x01000A2B.gfxobj.json new file mode 100644 index 0000000..42800dd --- /dev/null +++ b/tests/AcDream.Core.Tests/Fixtures/issue98/0x01000A2B.gfxobj.json @@ -0,0 +1,2346 @@ +{ + "GfxObjId": 16779819, + "BoundingSphereOrigin": { + "X": -3.25267, + "Y": -1.02984, + "Z": 1.11488 + }, + "BoundingSphereRadius": 13.9887, + "ResolvedPolygons": [ + { + "Id": 0, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 0, + "Z": -1 + }, + "D": -0 + }, + "Vertices": [ + { + "X": -5.8, + "Y": -8, + "Z": 0 + }, + { + "X": -12, + "Y": -8, + "Z": 0 + }, + { + "X": -12, + "Y": 8, + "Z": 0 + } + ] + }, + { + "Id": 1, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 0, + "Z": -1 + }, + "D": -0 + }, + "Vertices": [ + { + "X": -5.8, + "Y": 5, + "Z": 0 + }, + { + "X": 6, + "Y": 5, + "Z": 0 + }, + { + "X": 6, + "Y": -5.6, + "Z": 0 + } + ] + }, + { + "Id": 2, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 0.9805806, + "Z": 0.19611613 + }, + "D": -5.4912515 + }, + "Vertices": [ + { + "X": -5.8, + "Y": 5, + "Z": 3 + }, + { + "X": -8.0145, + "Y": 4, + "Z": 8 + }, + { + "X": 6, + "Y": 4, + "Z": 8 + } + ] + }, + { + "Id": 3, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 0, + "Z": -1 + }, + "D": -0 + }, + "Vertices": [ + { + "X": 6, + "Y": 1.8, + "Z": 0 + }, + { + "X": 8, + "Y": 1.8, + "Z": 0 + }, + { + "X": 8, + "Y": -2.4, + "Z": 0 + } + ] + }, + { + "Id": 4, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 1, + "Z": 0 + }, + "D": -8 + }, + "Vertices": [ + { + "X": -9.4, + "Y": 8, + "Z": 4 + }, + { + "X": -8.4, + "Y": 8, + "Z": 4 + }, + { + "X": -5.8, + "Y": 8, + "Z": 3 + }, + { + "X": -12, + "Y": 8, + "Z": 3 + } + ] + }, + { + "Id": 5, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 0, + "Z": -1 + }, + "D": -0 + }, + "Vertices": [ + { + "X": 8, + "Y": -2.4, + "Z": 0 + }, + { + "X": 6, + "Y": -2.4, + "Z": 0 + }, + { + "X": 6, + "Y": 1.8, + "Z": 0 + } + ] + }, + { + "Id": 6, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 0, + "Z": -1 + }, + "D": -0 + }, + "Vertices": [ + { + "X": 6, + "Y": -5.6, + "Z": 0 + }, + { + "X": -5.8, + "Y": -5.6, + "Z": 0 + }, + { + "X": -5.8, + "Y": 5, + "Z": 0 + } + ] + }, + { + "Id": 7, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 0, + "Z": -1 + }, + "D": -0 + }, + "Vertices": [ + { + "X": -12, + "Y": 8, + "Z": 0 + }, + { + "X": -5.8, + "Y": 8, + "Z": 0 + }, + { + "X": -5.8, + "Y": -8, + "Z": 0 + } + ] + }, + { + "Id": 8, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 1, + "Z": 0 + }, + "D": -8 + }, + "Vertices": [ + { + "X": -5.8, + "Y": 8, + "Z": 3 + }, + { + "X": -5.8, + "Y": 8, + "Z": 0 + }, + { + "X": -12, + "Y": 8, + "Z": 0 + }, + { + "X": -12, + "Y": 8, + "Z": 3 + } + ] + }, + { + "Id": 9, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -8 + }, + "Vertices": [ + { + "X": -5.8, + "Y": -8, + "Z": 3 + }, + { + "X": -8.9, + "Y": -8, + "Z": 10 + }, + { + "X": -12, + "Y": -8, + "Z": 3 + } + ] + }, + { + "Id": 10, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 0.9805807, + "Z": 0.19611613 + }, + "D": -5.4912515 + }, + "Vertices": [ + { + "X": 6, + "Y": 4, + "Z": 8 + }, + { + "X": 6, + "Y": 5, + "Z": 3 + }, + { + "X": -5.8, + "Y": 5, + "Z": 3 + } + ] + }, + { + "Id": 11, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 1, + "Y": 0, + "Z": 0 + }, + "D": 5.8 + }, + "Vertices": [ + { + "X": -5.8, + "Y": -8, + "Z": 0 + }, + { + "X": -5.8, + "Y": -5.6, + "Z": 0 + }, + { + "X": -5.8, + "Y": -5.6, + "Z": 3 + }, + { + "X": -5.8, + "Y": -8, + "Z": 3 + } + ] + }, + { + "Id": 12, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -0.84990263, + "Z": 0.52693963 + }, + "D": -6.340274 + }, + "Vertices": [ + { + "X": -8.0145, + "Y": -2.5, + "Z": 8 + }, + { + "X": -5.8, + "Y": -5.6, + "Z": 3 + }, + { + "X": 6, + "Y": -5.6, + "Z": 3 + }, + { + "X": 6, + "Y": -2.5, + "Z": 8 + } + ] + }, + { + "Id": 13, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 1, + "Z": 0 + }, + "D": -5 + }, + "Vertices": [ + { + "X": 6, + "Y": 5, + "Z": 0 + }, + { + "X": -5.8, + "Y": 5, + "Z": 0 + }, + { + "X": -5.8, + "Y": 5, + "Z": 3 + }, + { + "X": 6, + "Y": 5, + "Z": 3 + } + ] + }, + { + "Id": 14, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0.9143442, + "Y": 4.0675455E-05, + "Z": 0.40493774 + }, + "D": 4.0886106 + }, + "Vertices": [ + { + "X": -8.9, + "Y": -8, + "Z": 10 + }, + { + "X": -5.8, + "Y": -5.6, + "Z": 3 + }, + { + "X": -8.0145, + "Y": -2.5, + "Z": 8 + } + ] + }, + { + "Id": 15, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0.9143856, + "Y": 0, + "Z": 0.40484422 + }, + "D": 4.0895896 + }, + "Vertices": [ + { + "X": -8.0145, + "Y": -2.5, + "Z": 8 + }, + { + "X": -8.0145, + "Y": 4, + "Z": 8 + }, + { + "X": -8.9, + "Y": -8, + "Z": 10 + } + ] + }, + { + "Id": 16, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0.9143857, + "Y": 0, + "Z": 0.40484422 + }, + "D": 4.08959 + }, + "Vertices": [ + { + "X": -8.0145, + "Y": 4, + "Z": 8 + }, + { + "X": -8.9, + "Y": 8, + "Z": 10 + }, + { + "X": -8.9, + "Y": -8, + "Z": 10 + } + ] + }, + { + "Id": 17, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 0, + "Z": 1 + }, + "D": -8 + }, + "Vertices": [ + { + "X": 6, + "Y": -2.5, + "Z": 8 + }, + { + "X": 6, + "Y": -1.432, + "Z": 8 + }, + { + "X": -8.0145, + "Y": 4, + "Z": 8 + } + ] + }, + { + "Id": 18, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 0, + "Z": 1 + }, + "D": -8 + }, + "Vertices": [ + { + "X": 6, + "Y": -1.432, + "Z": 8 + }, + { + "X": 6, + "Y": 0.92, + "Z": 8 + }, + { + "X": -8.0145, + "Y": 4, + "Z": 8 + } + ] + }, + { + "Id": 19, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 0, + "Z": 1 + }, + "D": -8 + }, + "Vertices": [ + { + "X": 6, + "Y": 0.92, + "Z": 8 + }, + { + "X": 6, + "Y": 4, + "Z": 8 + }, + { + "X": -8.0145, + "Y": 4, + "Z": 8 + } + ] + }, + { + "Id": 20, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 0, + "Z": 1 + }, + "D": -8 + }, + "Vertices": [ + { + "X": -8.0145, + "Y": 4, + "Z": 8 + }, + { + "X": -8.0145, + "Y": -2.5, + "Z": 8 + }, + { + "X": 6, + "Y": -2.5, + "Z": 8 + } + ] + }, + { + "Id": 21, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 1, + "Y": 0, + "Z": 0 + }, + "D": -6 + }, + "Vertices": [ + { + "X": 6, + "Y": -2.4, + "Z": 3 + }, + { + "X": 6, + "Y": -1.432, + "Z": 8 + }, + { + "X": 6, + "Y": -2.5, + "Z": 8 + } + ] + }, + { + "Id": 22, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -2.4 + }, + "Vertices": [ + { + "X": 8, + "Y": -2.4, + "Z": 0 + }, + { + "X": 8, + "Y": -2.4, + "Z": 3 + }, + { + "X": 6, + "Y": -2.4, + "Z": 3 + }, + { + "X": 6, + "Y": -2.4, + "Z": 0 + } + ] + }, + { + "Id": 23, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 1, + "Z": 0 + }, + "D": -1.8 + }, + "Vertices": [ + { + "X": 6, + "Y": 1.8, + "Z": 0 + }, + { + "X": 6, + "Y": 1.8, + "Z": 3 + }, + { + "X": 8, + "Y": 1.8, + "Z": 3 + }, + { + "X": 8, + "Y": 1.8, + "Z": 0 + } + ] + }, + { + "Id": 24, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 1, + "Y": 0, + "Z": 0 + }, + "D": -8 + }, + "Vertices": [ + { + "X": 8, + "Y": 1.8, + "Z": 0 + }, + { + "X": 8, + "Y": 1.8, + "Z": 3 + }, + { + "X": 8, + "Y": -2.4, + "Z": 3 + }, + { + "X": 8, + "Y": -2.4, + "Z": 0 + } + ] + }, + { + "Id": 25, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": -1, + "Y": 0, + "Z": 0 + }, + "D": 6 + }, + "Vertices": [ + { + "X": 6, + "Y": -1.1856, + "Z": 10 + }, + { + "X": 6, + "Y": 0.696, + "Z": 10 + }, + { + "X": 6, + "Y": 0.92, + "Z": 8 + }, + { + "X": 6, + "Y": -1.432, + "Z": 8 + } + ] + }, + { + "Id": 26, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -0.98177034, + "Z": 0.19007075 + }, + "D": -2.9264612 + }, + "Vertices": [ + { + "X": 6, + "Y": -1.432, + "Z": 8 + }, + { + "X": 6, + "Y": -2.4, + "Z": 3 + }, + { + "X": 8, + "Y": -2.4, + "Z": 3 + }, + { + "X": 7.4, + "Y": -1.432, + "Z": 8 + } + ] + }, + { + "Id": 27, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 0.9848628, + "Z": 0.17333585 + }, + "D": -2.2927606 + }, + "Vertices": [ + { + "X": 8, + "Y": 1.8, + "Z": 3 + }, + { + "X": 6, + "Y": 1.8, + "Z": 3 + }, + { + "X": 6, + "Y": 0.92, + "Z": 8 + }, + { + "X": 7.4, + "Y": 0.92, + "Z": 8 + } + ] + }, + { + "Id": 28, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0.99287677, + "Y": 0, + "Z": 0.119145185 + }, + "D": -8.300449 + }, + "Vertices": [ + { + "X": 7.4, + "Y": -1.432, + "Z": 8 + }, + { + "X": 8, + "Y": -2.4, + "Z": 3 + }, + { + "X": 8, + "Y": 1.8, + "Z": 3 + }, + { + "X": 7.4, + "Y": 0.92, + "Z": 8 + } + ] + }, + { + "Id": 29, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -0.99249613, + "Z": 0.12227553 + }, + "D": -2.399459 + }, + "Vertices": [ + { + "X": 6, + "Y": -1.432, + "Z": 8 + }, + { + "X": 7.4, + "Y": -1.432, + "Z": 8 + }, + { + "X": 7.4, + "Y": -1.1856, + "Z": 10 + }, + { + "X": 6, + "Y": -1.1856, + "Z": 10 + } + ] + }, + { + "Id": 30, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 0.9937864, + "Z": 0.11130409 + }, + "D": -1.8047162 + }, + "Vertices": [ + { + "X": 7.4, + "Y": 0.696, + "Z": 10 + }, + { + "X": 7.4, + "Y": 0.92, + "Z": 8 + }, + { + "X": 6, + "Y": 0.92, + "Z": 8 + }, + { + "X": 6, + "Y": 0.696, + "Z": 10 + } + ] + }, + { + "Id": 31, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 1, + "Y": 0, + "Z": 0 + }, + "D": -7.4 + }, + "Vertices": [ + { + "X": 7.4, + "Y": -1.1856, + "Z": 10 + }, + { + "X": 7.4, + "Y": -1.432, + "Z": 8 + }, + { + "X": 7.4, + "Y": 0.92, + "Z": 8 + }, + { + "X": 7.4, + "Y": 0.696, + "Z": 10 + } + ] + }, + { + "Id": 32, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 0, + "Z": 1 + }, + "D": -10 + }, + "Vertices": [ + { + "X": 7.4, + "Y": 0.696, + "Z": 10 + }, + { + "X": 6, + "Y": 0.696, + "Z": 10 + }, + { + "X": 6, + "Y": -1.1856, + "Z": 10 + }, + { + "X": 7.4, + "Y": -1.1856, + "Z": 10 + } + ] + }, + { + "Id": 33, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 1, + "Y": 0, + "Z": 0 + }, + "D": -6 + }, + "Vertices": [ + { + "X": 6, + "Y": -2.5, + "Z": 8 + }, + { + "X": 6, + "Y": -5.6, + "Z": 3 + }, + { + "X": 6, + "Y": -2.4, + "Z": 3 + } + ] + }, + { + "Id": 34, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 1, + "Y": 0, + "Z": 0 + }, + "D": -6 + }, + "Vertices": [ + { + "X": 6, + "Y": -5.6, + "Z": 0 + }, + { + "X": 6, + "Y": -2.4, + "Z": 0 + }, + { + "X": 6, + "Y": -2.4, + "Z": 3 + }, + { + "X": 6, + "Y": -5.6, + "Z": 3 + } + ] + }, + { + "Id": 35, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 1, + "Y": 0, + "Z": 0 + }, + "D": -6 + }, + "Vertices": [ + { + "X": 6, + "Y": 4, + "Z": 8 + }, + { + "X": 6, + "Y": 0.92, + "Z": 8 + }, + { + "X": 6, + "Y": 1.8, + "Z": 3 + }, + { + "X": 6, + "Y": 5, + "Z": 3 + } + ] + }, + { + "Id": 36, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 1, + "Y": 0, + "Z": 0 + }, + "D": -6 + }, + "Vertices": [ + { + "X": 6, + "Y": 5, + "Z": 3 + }, + { + "X": 6, + "Y": 1.8, + "Z": 3 + }, + { + "X": 6, + "Y": 1.8, + "Z": 0 + }, + { + "X": 6, + "Y": 5, + "Z": 0 + } + ] + }, + { + "Id": 37, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -8 + }, + "Vertices": [ + { + "X": -12, + "Y": -8, + "Z": 3 + }, + { + "X": -12, + "Y": -8, + "Z": 0 + }, + { + "X": -11, + "Y": -8, + "Z": 0.85 + } + ] + }, + { + "Id": 38, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -8 + }, + "Vertices": [ + { + "X": -12, + "Y": -8, + "Z": 3 + }, + { + "X": -11, + "Y": -8, + "Z": 0.85 + }, + { + "X": -11, + "Y": -8, + "Z": 1.75 + } + ] + }, + { + "Id": 39, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -8 + }, + "Vertices": [ + { + "X": -12, + "Y": -8, + "Z": 3 + }, + { + "X": -11, + "Y": -8, + "Z": 1.75 + }, + { + "X": -9.95, + "Y": -8, + "Z": 2.2 + } + ] + }, + { + "Id": 40, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -8 + }, + "Vertices": [ + { + "X": -5.8, + "Y": -8, + "Z": 3 + }, + { + "X": -12, + "Y": -8, + "Z": 3 + }, + { + "X": -9.95, + "Y": -8, + "Z": 2.2 + } + ] + }, + { + "Id": 41, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -8 + }, + "Vertices": [ + { + "X": -11, + "Y": -8, + "Z": 0.85 + }, + { + "X": -12, + "Y": -8, + "Z": 0 + }, + { + "X": -5.8, + "Y": -8, + "Z": 0 + } + ] + }, + { + "Id": 42, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -8 + }, + "Vertices": [ + { + "X": -6.8, + "Y": -8, + "Z": 0.85 + }, + { + "X": -11, + "Y": -8, + "Z": 0.85 + }, + { + "X": -5.8, + "Y": -8, + "Z": 0 + } + ] + }, + { + "Id": 43, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -8 + }, + "Vertices": [ + { + "X": -6.8, + "Y": -8, + "Z": 0.85 + }, + { + "X": -5.8, + "Y": -8, + "Z": 0 + }, + { + "X": -5.8, + "Y": -8, + "Z": 3 + } + ] + }, + { + "Id": 44, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -8 + }, + "Vertices": [ + { + "X": -6.8, + "Y": -8, + "Z": 1.75 + }, + { + "X": -6.8, + "Y": -8, + "Z": 0.85 + }, + { + "X": -5.8, + "Y": -8, + "Z": 3 + } + ] + }, + { + "Id": 45, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -8 + }, + "Vertices": [ + { + "X": -7.85, + "Y": -8, + "Z": 2.2 + }, + { + "X": -6.8, + "Y": -8, + "Z": 1.75 + }, + { + "X": -5.8, + "Y": -8, + "Z": 3 + } + ] + }, + { + "Id": 46, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -8 + }, + "Vertices": [ + { + "X": -8.9, + "Y": -8, + "Z": 2.35 + }, + { + "X": -7.85, + "Y": -8, + "Z": 2.2 + }, + { + "X": -5.8, + "Y": -8, + "Z": 3 + } + ] + }, + { + "Id": 47, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -8 + }, + "Vertices": [ + { + "X": -9.95, + "Y": -8, + "Z": 2.2 + }, + { + "X": -8.9, + "Y": -8, + "Z": 2.35 + }, + { + "X": -5.8, + "Y": -8, + "Z": 3 + } + ] + }, + { + "Id": 48, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -5.6 + }, + "Vertices": [ + { + "X": 0.4, + "Y": -5.6, + "Z": 1 + }, + { + "X": 0.4, + "Y": -5.6, + "Z": 2.5 + }, + { + "X": -1.1, + "Y": -5.6, + "Z": 2.5 + } + ] + }, + { + "Id": 49, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -5.6 + }, + "Vertices": [ + { + "X": 0.4, + "Y": -5.6, + "Z": 1 + }, + { + "X": -1.1, + "Y": -5.6, + "Z": 2.5 + }, + { + "X": -1.1, + "Y": -5.6, + "Z": 0 + } + ] + }, + { + "Id": 50, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -5.6 + }, + "Vertices": [ + { + "X": -5.8, + "Y": -5.6, + "Z": 3 + }, + { + "X": -5.8, + "Y": -5.6, + "Z": 0 + }, + { + "X": -3, + "Y": -5.6, + "Z": 0 + } + ] + }, + { + "Id": 51, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -5.6 + }, + "Vertices": [ + { + "X": -5.8, + "Y": -5.6, + "Z": 3 + }, + { + "X": -3, + "Y": -5.6, + "Z": 0 + }, + { + "X": -3, + "Y": -5.6, + "Z": 2.5 + } + ] + }, + { + "Id": 52, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -5.6 + }, + "Vertices": [ + { + "X": 1.4, + "Y": -5.6, + "Z": 1 + }, + { + "X": 0.4, + "Y": -5.6, + "Z": 1 + }, + { + "X": -1.1, + "Y": -5.6, + "Z": 0 + } + ] + }, + { + "Id": 53, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -5.6 + }, + "Vertices": [ + { + "X": 1.4, + "Y": -5.6, + "Z": 1 + }, + { + "X": -1.1, + "Y": -5.6, + "Z": 0 + }, + { + "X": 6, + "Y": -5.6, + "Z": 0 + } + ] + }, + { + "Id": 54, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -5.6 + }, + "Vertices": [ + { + "X": 1.4, + "Y": -5.6, + "Z": 1 + }, + { + "X": 6, + "Y": -5.6, + "Z": 0 + }, + { + "X": 6, + "Y": -5.6, + "Z": 3 + } + ] + }, + { + "Id": 55, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -5.6 + }, + "Vertices": [ + { + "X": -5.8, + "Y": -5.6, + "Z": 3 + }, + { + "X": -3, + "Y": -5.6, + "Z": 2.5 + }, + { + "X": -1.1, + "Y": -5.6, + "Z": 2.5 + } + ] + }, + { + "Id": 56, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -5.6 + }, + "Vertices": [ + { + "X": 6, + "Y": -5.6, + "Z": 3 + }, + { + "X": -5.8, + "Y": -5.6, + "Z": 3 + }, + { + "X": -1.1, + "Y": -5.6, + "Z": 2.5 + } + ] + }, + { + "Id": 57, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -5.6 + }, + "Vertices": [ + { + "X": 1.4, + "Y": -5.6, + "Z": 2.5 + }, + { + "X": 1.4, + "Y": -5.6, + "Z": 1 + }, + { + "X": 6, + "Y": -5.6, + "Z": 3 + } + ] + }, + { + "Id": 58, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -5.6 + }, + "Vertices": [ + { + "X": 0.4, + "Y": -5.6, + "Z": 2.5 + }, + { + "X": 1.4, + "Y": -5.6, + "Z": 2.5 + }, + { + "X": 6, + "Y": -5.6, + "Z": 3 + } + ] + }, + { + "Id": 59, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": -1, + "Z": 0 + }, + "D": -5.6 + }, + "Vertices": [ + { + "X": -1.1, + "Y": -5.6, + "Z": 2.5 + }, + { + "X": 0.4, + "Y": -5.6, + "Z": 2.5 + }, + { + "X": 6, + "Y": -5.6, + "Z": 3 + } + ] + }, + { + "Id": 60, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": -1, + "Y": 0, + "Z": 0 + }, + "D": -12 + }, + "Vertices": [ + { + "X": -12, + "Y": 8, + "Z": 3 + }, + { + "X": -12, + "Y": 8, + "Z": 0 + }, + { + "X": -12, + "Y": -8, + "Z": 0 + }, + { + "X": -12, + "Y": -8, + "Z": 3 + } + ] + }, + { + "Id": 61, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": -0.9143494, + "Y": 0, + "Z": 0.4049262 + }, + "D": -12.186972 + }, + "Vertices": [ + { + "X": -12, + "Y": -8, + "Z": 3 + }, + { + "X": -8.9, + "Y": -8, + "Z": 10 + }, + { + "X": -8.9, + "Y": 8, + "Z": 10 + }, + { + "X": -12, + "Y": 8, + "Z": 3 + } + ] + }, + { + "Id": 62, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0.91433954, + "Y": -6.230709E-05, + "Z": 0.40494844 + }, + "D": 4.088636 + }, + "Vertices": [ + { + "X": -8.0145, + "Y": 4, + "Z": 8 + }, + { + "X": -5.8, + "Y": 5, + "Z": 3 + }, + { + "X": -8.9, + "Y": 8, + "Z": 10 + } + ] + }, + { + "Id": 63, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0.9143494, + "Y": 0, + "Z": 0.40492606 + }, + "D": 4.0884485 + }, + "Vertices": [ + { + "X": -8.9, + "Y": -8, + "Z": 10 + }, + { + "X": -5.8, + "Y": -8, + "Z": 3 + }, + { + "X": -5.8, + "Y": -5.6, + "Z": 3 + } + ] + }, + { + "Id": 64, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0.91434944, + "Y": 0, + "Z": 0.4049261 + }, + "D": 4.0884485 + }, + "Vertices": [ + { + "X": -5.8, + "Y": 5, + "Z": 3 + }, + { + "X": -5.8, + "Y": 8, + "Z": 3 + }, + { + "X": -8.9, + "Y": 8, + "Z": 10 + } + ] + }, + { + "Id": 65, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 1, + "Y": 0, + "Z": 0 + }, + "D": 5.8 + }, + "Vertices": [ + { + "X": -5.8, + "Y": 5, + "Z": 0 + }, + { + "X": -5.8, + "Y": 5.4, + "Z": 0 + }, + { + "X": -5.8, + "Y": 5.4, + "Z": 2.5 + }, + { + "X": -5.8, + "Y": 5, + "Z": 3 + } + ] + }, + { + "Id": 66, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 1, + "Y": 0, + "Z": 0 + }, + "D": 5.8000007 + }, + "Vertices": [ + { + "X": -5.8, + "Y": 5.4, + "Z": 2.5 + }, + { + "X": -5.8, + "Y": 7.3, + "Z": 2.5 + }, + { + "X": -5.8, + "Y": 8, + "Z": 3 + } + ] + }, + { + "Id": 67, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 1, + "Y": 0, + "Z": 0 + }, + "D": 5.8000007 + }, + "Vertices": [ + { + "X": -5.8, + "Y": 5.4, + "Z": 2.5 + }, + { + "X": -5.8, + "Y": 8, + "Z": 3 + }, + { + "X": -5.8, + "Y": 5, + "Z": 3 + } + ] + }, + { + "Id": 68, + "NumPoints": 4, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 1, + "Y": 0, + "Z": 0 + }, + "D": 5.8 + }, + "Vertices": [ + { + "X": -5.8, + "Y": 7.3, + "Z": 2.5 + }, + { + "X": -5.8, + "Y": 7.3, + "Z": 0 + }, + { + "X": -5.8, + "Y": 8, + "Z": 0 + }, + { + "X": -5.8, + "Y": 8, + "Z": 3 + } + ] + }, + { + "Id": 69, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 1, + "Z": 0 + }, + "D": -8 + }, + "Vertices": [ + { + "X": -5.8, + "Y": 8, + "Z": 3 + }, + { + "X": -8.4, + "Y": 8, + "Z": 4 + }, + { + "X": -8.4, + "Y": 8, + "Z": 5.5 + } + ] + }, + { + "Id": 70, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 1, + "Z": 0 + }, + "D": -8 + }, + "Vertices": [ + { + "X": -5.8, + "Y": 8, + "Z": 3 + }, + { + "X": -8.4, + "Y": 8, + "Z": 5.5 + }, + { + "X": -8.9, + "Y": 8, + "Z": 10 + } + ] + }, + { + "Id": 71, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 1, + "Z": 0 + }, + "D": -8 + }, + "Vertices": [ + { + "X": -8.4, + "Y": 8, + "Z": 5.5 + }, + { + "X": -9.4, + "Y": 8, + "Z": 5.5 + }, + { + "X": -8.9, + "Y": 8, + "Z": 10 + } + ] + }, + { + "Id": 72, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 1, + "Z": 0 + }, + "D": -8 + }, + "Vertices": [ + { + "X": -9.4, + "Y": 8, + "Z": 5.5 + }, + { + "X": -9.4, + "Y": 8, + "Z": 4 + }, + { + "X": -12, + "Y": 8, + "Z": 3 + } + ] + }, + { + "Id": 73, + "NumPoints": 3, + "SidesType": 0, + "Plane": { + "Normal": { + "X": 0, + "Y": 1, + "Z": 0 + }, + "D": -8 + }, + "Vertices": [ + { + "X": -9.4, + "Y": 8, + "Z": 5.5 + }, + { + "X": -12, + "Y": 8, + "Z": 3 + }, + { + "X": -8.9, + "Y": 8, + "Z": 10 + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/AcDream.Core.Tests/Physics/CellarUpTrajectoryReplayTests.cs b/tests/AcDream.Core.Tests/Physics/CellarUpTrajectoryReplayTests.cs index 3f8297b..c8519c4 100644 --- a/tests/AcDream.Core.Tests/Physics/CellarUpTrajectoryReplayTests.cs +++ b/tests/AcDream.Core.Tests/Physics/CellarUpTrajectoryReplayTests.cs @@ -484,31 +484,105 @@ public class CellarUpTrajectoryReplayTests /// First-cap event — the failing tick. Live engine reports cn=(0,0,-1), /// a downward-facing collision normal, capping the foot sphere at /// world Z=92.74. Math: head sphere TOP reaches Z=94.0 (the cottage - /// floor) when foot Z = 94.0 - sphereHeight = 92.80. So the head is - /// bumping the cottage floor from BELOW. + /// floor) when foot Z = 94.0 - sphereHeight = 92.80. The head bumps + /// the cottage floor from BELOW — NOT a step-up / AdjustOffset bug. /// /// - /// This is the actual #98 bug, NOT a step-up / AdjustOffset problem. - /// Live capture's [resolve] probe pinpoints the blocking + /// Live capture's [resolve] probe pinpointed the blocking /// entity: obj=0xA9B47900 — a landblock-baked static building - /// (the cottage GfxObj). The cottage's floor polygons live in this - /// GfxObj, registered as a ShadowEntry, NOT in any of the cottage's - /// cells. The harness's - /// loads cell fixtures but does NOT register the cottage GfxObj, so - /// the harness fails to reproduce the cap — DOCUMENTED here as the - /// divergence pattern. + /// (the cottage GfxObj 0x01000A2B). The cottage's floor polys + /// live in this GfxObj as a ShadowEntry, NOT in any cottage cell. /// /// /// - /// Documents-the-bug pattern: passes WHILE the harness lacks the - /// cottage GfxObj. When a future session adds the cottage GfxObj - /// (full polygon list extracted from the live [poly-dump] + - /// [resolve-bldg] probes), this test will start failing — - /// the signal to flip it from documenting-the-bug to enforcing-the-fix. + /// Apparatus-convergence form (2026-05-23 evening v2): with the + /// cottage GfxObj registered via , + /// the harness reproduces the live cn=(0,0,-1) cap event. This test + /// enforces THAT specific reproduction. The post-cap position + /// processing has a separate residual divergence — documented by + /// . /// /// [Fact] - public void LiveCompare_FirstCap_HarnessMissesCottageFloorBecauseCottageGfxObjNotRegistered() + public void LiveCompare_FirstCap_HarnessReproducesCottageFloorCapNormal() + { + var (engine, _) = BuildEngineWithCellarFixtures(); + var captured = LoadCapturedRecord(record => + record.Result.CollisionNormalValid + && record.Result.CollisionNormal.Z < -0.99f); + + // Live must have cn=(0,0,-1) at this point — sanity check. + Assert.True(captured.Result.CollisionNormalValid, + "Captured record must have collisionNormalValid=true."); + Assert.True(captured.Result.CollisionNormal.Z < -0.99f, + $"Captured record must have downward collision normal; got " + + $"{captured.Result.CollisionNormal}."); + + // Replay the call. + Assert.NotNull(captured.BodyBefore); + var body = SeedBodyFromSnapshot(captured.BodyBefore); + var harnessResult = engine.ResolveWithTransition( + currentPos: captured.Input.CurrentPos, + targetPos: captured.Input.TargetPos, + cellId: captured.Input.CellId, + sphereRadius: captured.Input.SphereRadius, + sphereHeight: captured.Input.SphereHeight, + stepUpHeight: captured.Input.StepUpHeight, + stepDownHeight: captured.Input.StepDownHeight, + isOnGround: captured.Input.IsOnGround, + body: body, + moverFlags: (ObjectInfoState)captured.Input.MoverFlags, + movingEntityId: captured.Input.MovingEntityId); + + // Apparatus convergence: harness reproduces the cap-event collision + // normal exactly. If this fails, the cottage GfxObj registration + // has regressed (RegisterCottageGfxObj is broken, the dump fixture + // is stale, or the harness wiring lost the landblock context). + Assert.True(harnessResult.CollisionNormalValid, + "Harness must reproduce the live collision-normal-valid signal " + + "now that the cottage GfxObj is registered."); + Assert.True(harnessResult.CollisionNormal.Z < -0.99f, + $"Harness must reproduce the live downward-facing cap normal " + + $"(live cn={captured.Result.CollisionNormal}, harness cn={harnessResult.CollisionNormal})."); + } + + /// + /// A6.P3 issue #98 (2026-05-23 evening v2) — documents-the-bug for the + /// residual divergence the apparatus surfaced. With the cottage GfxObj + /// registered, the harness reproduces the live cap-event collision + /// normal, but the POST-CAP POSITION processing diverges: + /// + /// Live: full +X motion preserved (sphere slides +0.0266 m in X + /// along the cottage floor before the Y motion is capped). + /// Harness: ZERO X motion (sphere stays at its input X position, + /// only the Y component is blocked). + /// + /// + /// + /// Both X positions agree on Y=7.2243 and Z=92.7390. Only X differs: + /// live X=141.3865 (requested target), harness X=141.3599 (current = no + /// move). The requested delta was (+0.0266, -0.4022, 0); live applied + /// the +X portion, harness applied nothing. + /// + /// + /// + /// Hypothesis (to investigate next session): live's response to a + /// cn=(0,0,-1) head-bump treats it as a Z-only constraint and lets the + /// XY component of the move complete via edge-slide. Harness's BSP path + /// is rejecting the WHOLE move vector when the cottage floor poly + /// intersects the head sphere, instead of computing a slid offset. + /// + /// + /// + /// Documents-the-bug: PASSES today on the asserted residual magnitude. + /// When a future session fixes the post-cap edge-slide, harness X will + /// match live X — this test FAILS at that point, signaling that the + /// X divergence is closed and the test should be folded back into the + /// strict path. + /// + /// + [Fact] + public void LiveCompare_FirstCap_ResidualXMotionDivergence_DocumentsNextInvestigation() { var (engine, _) = BuildEngineWithCellarFixtures(); var captured = LoadCapturedRecord(record => @@ -530,26 +604,20 @@ public class CellarUpTrajectoryReplayTests moverFlags: (ObjectInfoState)captured.Input.MoverFlags, movingEntityId: captured.Input.MovingEntityId); - // Live reported cn=(0,0,-1) blocking the climb at this point. - Assert.True(captured.Result.CollisionNormalValid, - "Captured record must have collisionNormalValid=true."); - Assert.True(captured.Result.CollisionNormal.Z < -0.99f, - $"Captured record must have downward collision normal; got " + - $"{captured.Result.CollisionNormal}."); + // Live preserved the full +X motion through the cap event; harness + // blocked it. Y and Z agree. + Assert.Equal(captured.Result.Position.Y, harnessResult.Position.Y, 4); + Assert.Equal(captured.Result.Position.Z, harnessResult.Position.Z, 4); - // Harness does NOT reproduce the live downward push because the - // cottage GfxObj is not registered — the blocking polygon lives - // in static obj 0xA9B47900, which BuildEngineWithCellarFixtures - // intentionally skips today (RegisterStairRampGfxObj is commented - // out). When the cottage GfxObj's full polygon set is added to - // the harness, this assertion will start to fail — flip the test - // to assert the live cn=(0,0,-1) round-trips at that point. - Assert.False( - harnessResult.CollisionNormalValid - && harnessResult.CollisionNormal.Z < -0.99f, - "Harness should NOT reproduce the cottage-floor cap yet — " + - "if it does, the cottage GfxObj has been added and this test " + - "needs to flip to AssertCallMatchesCapture(engine, captured)."); + float liveDeltaX = captured.Result.Position.X - captured.Input.CurrentPos.X; + float harnessDeltaX = harnessResult.Position.X - captured.Input.CurrentPos.X; + + Assert.True(liveDeltaX > 0.02f, + $"Live must show +X motion after cap (expected ~+0.0266 m, got {liveDeltaX:F4})."); + Assert.True(MathF.Abs(harnessDeltaX) < 0.001f, + $"Harness currently zeros X motion through the cap (expected ~0, got {harnessDeltaX:F4}). " + + "If this assertion starts failing because harness now preserves +X, the post-cap " + + "edge-slide divergence is closed — fold this back into AssertCallMatchesCapture."); } /// @@ -868,25 +936,44 @@ public class CellarUpTrajectoryReplayTests cache.RegisterCellStructForTest(cellId, cellWithBsp); } - // ── 2. NO landblock registered ────────────────────────────── - // Without a landblock, SampleTerrainWalkable returns null and - // FindEnvCollisions's outdoor-fallback path returns OK without - // running ValidateWalkable on stub terrain. This is the right - // shape for indoor-only tests — the cell's BSP would handle - // collision if hydrated, and falling through to stub terrain - // produces spurious (0,1,0) wall hits. FindObjCollisions also - // early-returns without landblock context (line 2153 of - // TransitionTypes.cs), so the synthetic stair GfxObj is also - // skipped — fine for the airborne-at-tick-1 isolation. + // ── 2. Minimum landblock context for FindObjCollisions ────── + // FindObjCollisions (TransitionTypes.cs:2153) early-returns + // TransitionState.OK when TryGetLandblockContext fails for the + // sphere XY. Without a landblock the harness can't query the + // cottage GfxObj's shadow entries — and that's where the + // first-cap collision actually lives (live capture confirmed + // obj=0xA9B47900 fires the cn=(0,0,-1) push). + // + // Register an EMPTY-terrain landblock 0xA9B40000 anchored at + // world origin (0,0). The landblock test + // (worldX >= 0 && worldX < 192) covers every harness sphere + // position (X≈141, Y≈7). TerrainSurface gets a flat far-below + // surface so SampleTerrainZ returns something the indoor BSP + // path never consults (FindEnvCollisions's indoor branch fires + // first when the cell has BSP). Outdoor-fallback queries are + // harmless because the cell's synthetic BSP returns Collided + // before terrain is checked. + var heights = new byte[81]; // 9x9 corners + var heightTable = new float[256]; + for (int i = 0; i < 256; i++) heightTable[i] = -1000f; // far below cellar + var stubTerrain = new TerrainSurface(heights, heightTable); + engine.AddLandblock( + landblockId: 0xA9B40000u, + terrain: stubTerrain, + cells: Array.Empty(), + portals: Array.Empty(), + worldOffsetX: 0f, + worldOffsetY: 0f); - // ── 3. Synthetic stair-piece GfxObj + ShadowEntry ────────── - // Temporarily disabled while debugging the airborne-at-tick-1 - // issue. Re-enable once the cell-BSP-is-null + landblock-stub - // interaction is understood, AND we have a way to register - // the stair without needing a landblock (e.g., extend - // FindObjCollisions to query cellScope-only shadows without - // landblock context). - // RegisterStairRampGfxObj(engine, cache); + // ── 3. Cottage GfxObj 0x01000A2B from dumped fixture ──────── + // Live capture (2026-05-23 PM v2) attributes the first-cap event + // to obj=0xA9B47900 (entity 0x00A9B479 partIdx=0) — a landblock- + // baked static building registered as a ShadowEntry. The full + // polygon table was extracted via ACDREAM_DUMP_GFXOBJS=0x01000A2B + // (issue #98 evening-v2 apparatus); 74 polygons including six + // downward-facing cottage-floor triangles at object-local Z=0 + // that the head sphere bumps from below at world Z=94. + RegisterCottageGfxObj(engine, cache); return (engine, cache); } @@ -939,100 +1026,75 @@ public class CellarUpTrajectoryReplayTests } /// - /// Constructs a synthetic GfxObj containing the cellar ramp polygon - /// in WORLD coordinates and registers it as a ShadowEntry scoped to - /// the cellar cell. The polygon's vertices + normal are reproduced - /// from the live capture's [poly-dump] data (commit pre-3f56915), - /// transformed to world frame so the GfxObj can sit at world origin - /// with identity rotation/scale (simplifies the - /// FindObjCollisions local-to-world transform). + /// A6.P3 issue #98 (2026-05-23 evening v2). Loads the cottage GfxObj + /// 0x01000A2B from the JSON fixture + /// (tests/AcDream.Core.Tests/Fixtures/issue98/0x01000A2B.gfxobj.json, + /// produced via the ACDREAM_DUMP_GFXOBJS capture infrastructure), + /// hydrates it as a with a synthetic + /// single-leaf BSP, and registers it as a ShadowEntry at the cottage's + /// world transform — the same shape production's GameWindow.cs:5893 + /// registration uses for landblock-baked statics. /// /// - /// Live capture's local polygon vertices (in building frame): - /// (0.8,-1.59,-1.5), (0.8,1.31,1.5), (-0.8,1.31,1.5), (-0.8,-1.59,-1.5). - /// Building's world transform: origin (141.5, 7.155, 92.455), 180° yaw - /// around Z. After applying yaw + translation, world vertices are: - /// (140.7, 8.745, 90.955), (140.7, 5.845, 93.955), - /// (142.3, 5.845, 93.955), (142.3, 8.745, 90.955). - /// World normal = (0, 0.719, 0.695), world d = -69.5035 — matches - /// the live cdb capture exactly. + /// Transform values come from two evidence sources: + /// + /// The cellar cell 0xA9B40147's WorldTransform has translation + /// (130.5, 11.5, 94.0) and a 3×3 with M11=M22=-1 / M33=+1 + /// (a 180° rotation around Z). The cottage GfxObj sits at the + /// SAME world transform (its building origin is also at + /// (130.5, 11.5, 94.0) per the existing [resolve-bldg] capture + /// entOrigin_lb=(130.5,11.5,94.0)). + /// BoundingSphere radius from the dump's + /// — 13.989 m. + /// Matches the live bspR=13.99 observed in the + /// [resolve-bldg] capture; cross-validation that the same + /// building is in play. + /// + /// + /// + /// + /// Entity id 0x00A9B479 mirrors the live capture's + /// obj=0xA9B47900 formula (entity.Id × 256 + partIdx=0). Using + /// the same id keeps any future probe correlation aligned with live + /// log conventions. /// /// - private static void RegisterStairRampGfxObj(PhysicsEngine engine, PhysicsDataCache cache) + private static void RegisterCottageGfxObj(PhysicsEngine engine, PhysicsDataCache cache) { - const ushort RampPolyId = 0x0008; - const uint StairGfxId = 0xDEADBEEFu; - const uint StairEntityId = 0xC0FFEE00u; + const uint CottageGfxId = 0x01000A2Bu; + const uint CottageEntityId = 0x00A9B479u; - // World-frame vertices (winding order preserved from live capture). - var v0 = new Vector3(140.7f, 8.745f, 90.955f); // ramp foot, X=-side - var v1 = new Vector3(140.7f, 5.845f, 93.955f); // ramp top, X=-side - var v2 = new Vector3(142.3f, 5.845f, 93.955f); // ramp top, X=+side - var v3 = new Vector3(142.3f, 8.745f, 90.955f); // ramp foot, X=+side - var verts = new[] { v0, v1, v2, v3 }; + var fixturePath = Path.Combine(FixtureDir, "0x01000A2B.gfxobj.json"); + Assert.True(File.Exists(fixturePath), + $"Cottage GfxObj fixture missing: {fixturePath}. Re-run live " + + $"capture with ACDREAM_DUMP_GFXOBJS=0x01000A2B."); - // Compute normal from cross(v1-v0, v3-v0). - var edge0 = v1 - v0; - var edge1 = v3 - v0; - var normal = Vector3.Normalize(Vector3.Cross(edge0, edge1)); - // Plane equation: N·p + d = 0 → d = -N·v0. - float d = -Vector3.Dot(normal, v0); + var dump = GfxObjDumpSerializer.Read(fixturePath); + var physics = GfxObjDumpSerializer.Hydrate(dump); + cache.RegisterGfxObjForTest(CottageGfxId, physics); - var resolved = new Dictionary - { - [RampPolyId] = new ResolvedPolygon - { - Vertices = verts, - Plane = new System.Numerics.Plane(normal, d), - NumPoints = 4, - SidesType = CullMode.Landblock, - }, - }; + // World transform from the cellar cell's WorldTransform: translation + // (130.5, 11.5, 94.0) + 180° rotation around Z. The cottage GfxObj + // shares this transform (it IS the cellar/cottage geometry). + var worldPos = new Vector3(130.5f, 11.5f, 94.0f); + var worldRot = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI); - // Minimal one-leaf BSP containing the ramp poly. Bounding sphere - // encompasses the polygon (center at poly centroid). - var leaf = new PhysicsBSPNode - { - Type = BSPNodeType.Leaf, - BoundingSphere = new Sphere - { - Origin = new Vector3(141.5f, 7.295f, 92.455f), - Radius = 3.0f, - }, - }; - leaf.Polygons.Add(RampPolyId); - - var bspTree = new PhysicsBSPTree { Root = leaf }; - - var gfxPhysics = new GfxObjPhysics - { - BSP = bspTree, - PhysicsPolygons = new Dictionary(), - Vertices = new VertexArray(), - Resolved = resolved, - BoundingSphere = leaf.BoundingSphere, - }; - - cache.RegisterGfxObjForTest(StairGfxId, gfxPhysics); - - // ShadowEntry: object at world origin (0,0,0), identity rotation, - // scale 1.0 — keeps the polygon's WORLD-frame vertices intact - // through the FindObjCollisions local-transform math. - // cellScope = CellarId so the entry is only queried when the sphere - // is in cellar cell (matches retail's per-cell shadow scoping for - // interior statics — Issue #91 family). engine.ShadowObjects.Register( - entityId: StairEntityId, - gfxObjId: StairGfxId, - worldPos: Vector3.Zero, - rotation: Quaternion.Identity, - radius: 5.0f, + entityId: CottageEntityId, + gfxObjId: CottageGfxId, + worldPos: worldPos, + rotation: worldRot, + radius: physics.BoundingSphere?.Radius ?? 14f, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: 0xA9B40000u, collisionType: ShadowCollisionType.BSP, scale: 1.0f, - cellScope: CellarId); + // Landblock-baked statics in production (GameWindow.cs:5899) use + // `entity.ParentCellId ?? 0u` — the cottage building has no + // ParentCellId (it's a top-level landblock static), so the + // scope is landblock-wide (cellScope=0). + cellScope: 0u); } ///