test(p1): production-path membership conformance — divergence CONFIRMED (0/11), not a probe artifact

Replays the golden indoor 0170<->0171 segments through the real
PhysicsEngine.ResolveWithTransition (engine builds the global sphere + sweeps;
cells loaded from dats with real BSP). Result: 0/11 match retail. Every segment
restPos==target (the sweep completes the move) but CellId stays on the SOURCE
cell — acdream moves the body across the doorway yet NEVER advances curr_cell.
So the 'probe artifact' hypothesis is FALSIFIED: production membership genuinely
lags retail.

Refined mechanism: both retail and acdream PICK with center-only point_in_cell
(architect's radius-aware-pick hypothesis falsified, confirmed by reading
CEnvCell::point_in_cell -> BSPTREE::point_inside_cell_bsp). The gap is retail's
curr_cell ADVANCES across the portal mid-sweep (swept crossing / leading sphere
point) while acdream's swept advance keeps the source cell. P1 ports that advance.

ProductionPath_IndoorCrossings_DivergeFromRetail_PendingP1 is the RED gate the P1
fix must turn GREEN. Conformance 60 pass / 1 skip / 0 fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-03 15:56:52 +02:00
parent 46a86d282e
commit 0442eadcec
2 changed files with 164 additions and 0 deletions

View file

@ -167,6 +167,29 @@ evidence. **Do NOT design or code a P1 membership "fix" before the production-pa
RED/GREEN is read.** The P0 `..._DivergesFromRetail_PendingP1` test is a UNIT-level pin only, NOT
evidence of a production divergence.
## ✅ RESOLVED — the production path DIVERGES (the "probe artifact" hypothesis is FALSIFIED)
Built `ThresholdPortalCrossingReplayTests.ProductionPath_IndoorCrossings_DivergeFromRetail_PendingP1`
(replays the golden indoor `0170↔0171` segments through the REAL `ResolveWithTransition` — engine
builds the global sphere + sweeps; cells loaded from dats with real BSP). Result: **0/11 match
retail.** Every segment: `restPos == target` (the sweep COMPLETES the move cleanly) but `CellId`
stays on the SOURCE cell — acdream moves the body across the doorway yet **never advances
`curr_cell`**. So production membership genuinely lags; the P0 finding is REAL, not a probe artifact.
**Refined mechanism (supersedes the "portal-crossing vs point-in-cell criterion" framing).** Both
retail and acdream PICK with center-only `point_in_cell`. The divergence is that retail's `curr_cell`
ADVANCES to the neighbour during the sweep (the swept sphere crossing the doorway polygon, and/or a
sphere point that leads the foot into the room), so by the time the foot rests at the captured
position the membership has already advanced. acdream's swept advance does NOT promote the neighbour —
at the end-position its tested sphere center is still inside the source cell's BSP, so the pick keeps
the source cell. **P1's job: port how retail advances `curr_cell` across the portal mid-sweep.** The
open decomp questions for P1: (1) how `global_sphere[0]` local origin relates to `m_position` (does
retail's sphere point lead the foot?); (2) whether `curr_cell` advances via `find_transit_cells`'
swept crossing in `transitional_insert`/`validate_transition` BEFORE the `find_cell_list` pick, vs the
pick alone. Anchors: `CTransition::transitional_insert @ 0x50aa70 pc:272547`,
`CTransition::validate_transition`, `CPhysicsObj::SetPositionInternal @ 0x515330 pc:283399`. The RED
production-path test is the gate the P1 fix must turn GREEN.
## P0 status / P1-entry checklist — COMPLETE
**Apparatus: COMPLETE + GREEN.** (Conformance suite 59 pass / 1 skip / 0 fail.)

View file

@ -0,0 +1,141 @@
using System;
using System.IO;
using System.Numerics;
using AcDream.Core.Physics;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.Core.Tests.Conformance;
/// <summary>
/// P1 decisive evidence — the PRODUCTION-PATH membership conformance. Replays the
/// captured retail doorway golden through the REAL <see cref="PhysicsEngine.ResolveWithTransition"/>
/// (which builds the global sphere and SWEEPS it), then checks whether the swept
/// <c>CellId</c> matches retail's committed cell. This is the integration test the
/// P0 bare-<c>FindCellList</c> probe could NOT be: the probe fed the resting foot
/// origin with no sweep, so it always reported the cell the foot stood in. Here the
/// engine sweeps prev→curr, so the sphere crosses the doorway exactly as retail's did.
///
/// Diagnostic-first: prints MATCH/DIVERGE per segment so we read the real production
/// behaviour before pinning an assertion. Outcomes (notes
/// docs/research/2026-06-03-p0-conformance-apparatus-notes.md §CORRECTION): production
/// MATCHES → the P0 divergence was a probe artifact; DIVERGES → a real bug to design from.
///
/// Scope here = the INDOOR segments (vestibule 0170 ↔ room 0171), which need only the
/// building-cell cache. The outdoor-involving segments (0031↔0170) need the landcell +
/// building portal and are a follow-up.
/// </summary>
public class ThresholdPortalCrossingReplayTests
{
private readonly ITestOutputHelper _out;
public ThresholdPortalCrossingReplayTests(ITestOutputHelper output) => _out = output;
// The doorway floor is flat at the captured pz across all transitions.
private const float FloorZ = 94.005f;
private const float SphereRadius = 0.4f;
private const float SphereHeight = 1.2f;
private const float StepUpHeight = 0.4f;
private const float StepDownHeight = 0.1f;
private static (PhysicsEngine, PhysicsDataCache) BuildBuildingEngine(DatCollection dats)
{
var cache = new PhysicsDataCache();
var engine = new PhysicsEngine { DataCache = cache };
for (uint low = 0x016Fu; low <= 0x0175u; low++)
ConformanceDats.LoadEnvCell(dats, cache, ConformanceDats.HoltburgLandblock | low);
// Stub landblock for FindObjCollisions context (flat far-below terrain; the
// indoor BSP path fires first, terrain is never consulted). Matches the
// CellarUpTrajectoryReplay harness pattern.
var heights = new byte[81];
var heightTable = new float[256];
for (int i = 0; i < 256; i++) heightTable[i] = -1000f;
engine.AddLandblock(0xA9B40000u, new TerrainSurface(heights, heightTable),
Array.Empty<CellSurface>(), Array.Empty<PortalPlane>(), 0f, 0f);
return (engine, cache);
}
private static PhysicsBody GroundedBodyAt(Vector3 pos, uint cellId) => new()
{
Position = pos,
Orientation = Quaternion.Identity,
ContactPlaneValid = true,
ContactPlane = new Plane(0f, 0f, 1f, -FloorZ),
ContactPlaneCellId = cellId,
WalkablePolygonValid = true,
WalkablePlane = new Plane(0f, 0f, 1f, -FloorZ),
WalkableVertices = new[]
{
new Vector3(150f, 5f, FloorZ), new Vector3(150f, 20f, FloorZ),
new Vector3(165f, 20f, FloorZ), new Vector3(165f, 5f, FloorZ),
},
WalkableUp = Vector3.UnitZ,
TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable,
};
private static uint Low(uint id) => id & 0xFFFFu;
/// <summary>
/// Documents-the-bug (GREEN while acdream diverges; FAILS when P1 lands → rewrite to
/// per-segment Assert.Equal). The swept production path DIVERGES from retail on the
/// indoor doorway crossings: <c>ResolveWithTransition</c> completes the move
/// (<c>restPos == target</c>) but leaves <c>CellId</c> on the SOURCE cell — it never
/// advances <c>curr_cell</c> across the portal the way retail's <c>change_cell</c> golden
/// does. So the P0 finding is NOT a probe artifact: production membership genuinely lags.
/// P1 must port retail's swept-crossing <c>curr_cell</c> advance (how the sphere crossing
/// the doorway polygon / the leading sphere point promotes the neighbour to the membership
/// answer mid-sweep), then this flips to all-match.
/// </summary>
[Fact]
public void ProductionPath_IndoorCrossings_DivergeFromRetail_PendingP1()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
var fixturePath = Path.Combine(ConformanceDats.FixturesDir, "find-cell-list-threshold.log");
if (!File.Exists(fixturePath)) { _out.WriteLine("SKIP: capture pending"); return; }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var (engine, _) = BuildBuildingEngine(dats);
var picks = RetailTrace.ParseAll(File.ReadAllLines(fixturePath));
int match = 0, total = 0;
for (int i = 0; i + 1 < picks.Count; i++)
{
uint fromCell = picks[i].PickedCellId;
uint toCell = picks[i + 1].PickedCellId;
if (Low(fromCell) < 0x100 || Low(toCell) < 0x100) continue; // indoor segments only
var body = GroundedBodyAt(picks[i].Position, fromCell);
var result = engine.ResolveWithTransition(
currentPos: picks[i].Position,
targetPos: picks[i + 1].Position,
cellId: fromCell,
sphereRadius: SphereRadius,
sphereHeight: SphereHeight,
stepUpHeight: StepUpHeight,
stepDownHeight: StepDownHeight,
isOnGround: true,
body: body,
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
movingEntityId: 0);
bool ok = result.CellId == toCell;
if (ok) match++;
total++;
_out.WriteLine(
$"seg 0x{Low(fromCell):X4}->0x{Low(toCell):X4} " +
$"pos=({picks[i].Position.X:F2},{picks[i].Position.Y:F2})->({picks[i + 1].Position.X:F2},{picks[i + 1].Position.Y:F2}) " +
$"acdream=0x{Low(result.CellId):X4} restPos=({result.Position.X:F2},{result.Position.Y:F2},{result.Position.Z:F2}) " +
$"{(ok ? "MATCH" : "DIVERGE")}");
}
_out.WriteLine($"=== production-path indoor crossings: {match}/{total} match retail ===");
Assert.True(total > 0, "no indoor doorway segments found in the golden");
Assert.True(match < total,
$"acdream's swept ResolveWithTransition now reproduces {match}/{total} retail indoor " +
"doorway crossings. If match == total, P1's curr_cell swept-advance port landed -> " +
"rewrite this to Assert.Equal(toCell, result.CellId) per segment. (Retail truth = the golden.)");
}
}