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:
parent
46a86d282e
commit
0442eadcec
2 changed files with 164 additions and 0 deletions
|
|
@ -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.)
|
||||
|
|
|
|||
|
|
@ -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.)");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue