The P1 "doorway membership lags retail" premise is FALSIFIED. acdream's swept ResolveWithTransition already matches retail's true per-frame curr_cell: the production gate ProductionPath_IndoorCrossings reads 9/9 on the indoor 0170<->0171 crossings with NO code change, once fed an aligned retail golden. Root cause of the false 0/11: CPhysicsObj::SetPositionInternal calls change_cell (acclient_2013_pseudo_c.txt:283456) BEFORE set_frame writes m_position (:283458), so the original golden (find-cell-list-capture.cdb, read at the change_cell BP) paired each frame's NEW cell with the PREVIOUS frame's position — a one-frame skew. Verified 3 ways: the decomp ordering; golden_picked[i] == geom(golden_position[i+1]) for all 22 rows; acdream's static pick == golden_picked[i-1] for all rows. Both retail and acdream pick with center-only point_in_cell on global_sphere[0] (no XY lead; cache_global_sphere @ pc:274196). curr_cell commits via validate_transition (@ pc:272608, curr_cell = check_cell) = the find_cell_list pick, structurally identical to acdream's RunCheckOtherCellsAndAdvance -> FindCellSet -> SetCheckPos. There was nothing to port; a swept advance would make membership LEAD by a frame. - tools/cdb/find-cell-list-capture-aligned.cdb: re-capture reads the committed position from the set_frame that follows change_cell (cell+position same instant). - Fixtures/find-cell-list-threshold.log: replaced with the aligned capture. - ThresholdPortalCrossingReplayTests / FindCellListConformanceTests: rewritten from documents-the-bug to assert retail truth (per-segment / per-indoor-pick equality). - handoff + notes + README + memory: banners correcting the disproven premise. Still open (NOT indoor membership, which is DONE): outdoor->indoor 0031<->0170 entry conformance (needs landcell + building stab in the gate cache); master-plan cleanups (delete CheckBuildingTransit, unify find_env_collisions, demote ResolveCellId) refactor working retail-faithful code -> need explicit user approval. Conformance 60 pass / 1 skip / 0 fail; full Core 1309 pass / 5 fail (pre-existing 2 BSPStepUp + 3 door-collision = P2) / 1 skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
158 lines
8.2 KiB
C#
158 lines
8.2 KiB
C#
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>
|
|
/// P1 conformance gate. Replays the captured retail doorway golden's INDOOR
|
|
/// (<c>0170↔0171</c>) crossings through the REAL
|
|
/// <see cref="PhysicsEngine.ResolveWithTransition"/> and asserts acdream's swept
|
|
/// <c>CellId</c> equals retail's committed cell on every crossing.
|
|
///
|
|
/// HISTORY (2026-06-03). With the FIRST capture (find-cell-list-capture.cdb) this read
|
|
/// 0/11 and was believed to prove a "membership lags retail" bug needing a swept
|
|
/// <c>curr_cell</c>-advance port. That was a CAPTURE ARTIFACT, not a real divergence:
|
|
/// <c>CPhysicsObj::SetPositionInternal</c> calls <c>change_cell</c>
|
|
/// (acclient_2013_pseudo_c.txt:283456) BEFORE <c>set_frame</c> updates <c>m_position</c>
|
|
/// (:283458), so the cdb golden paired THIS frame's new cell with the PREVIOUS frame's
|
|
/// position — a deterministic one-frame skew (golden_picked[i] == geom(position[i+1])).
|
|
/// Re-capturing with the committed position read from the following <c>set_frame</c>
|
|
/// (tools/cdb/find-cell-list-capture-aligned.cdb) makes cell+position align at the same
|
|
/// instant, and acdream then matches retail 9/9 with NO code change: acdream's center-only
|
|
/// <c>point_in_cell</c> pick at the swept rest position IS retail's true per-frame
|
|
/// membership. Both pick with center-only <c>point_in_cell</c> on <c>global_sphere[0]</c>
|
|
/// (find_cell_list @ pc:308788-308825); the foot sphere does not lead the foot in XY
|
|
/// (cache_global_sphere @ pc:274196). See
|
|
/// docs/research/2026-06-03-p1-membership-swept-advance-handoff.md.
|
|
///
|
|
/// Scope = INDOOR segments (the building-cell cache models these fully). The
|
|
/// outdoor-involving <c>0031↔0170</c> segments need the landcell + building stab loaded
|
|
/// (separate fixture work — verifies the outdoor→indoor entry path).
|
|
/// </summary>
|
|
[Fact]
|
|
public void ProductionPath_IndoorCrossings_MatchRetail()
|
|
{
|
|
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;
|
|
var failures = new System.Collections.Generic.List<string>();
|
|
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++;
|
|
else failures.Add(System.FormattableString.Invariant(
|
|
$"0x{Low(fromCell):X4}->0x{Low(toCell):X4}@({picks[i + 1].Position.X:F2},{picks[i + 1].Position.Y:F2}): acdream=0x{Low(result.CellId):X4}"));
|
|
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(failures.Count == 0,
|
|
$"acdream's swept membership diverged on {failures.Count}/{total} indoor doorway " +
|
|
$"crossings (retail truth = the aligned golden): {string.Join("; ", failures)}");
|
|
}
|
|
}
|