using System;
using System.IO;
using System.Numerics;
using AcDream.Core.Physics;
using Xunit;
namespace AcDream.Core.Tests.Physics;
///
/// A6.P3 issue #98 (2026-05-23) — deterministic replay harness for the
/// cellar-ascent failure. Loads the three real-geometry cell fixtures
/// captured from the live client (commit
/// 3f56915 — cellar/cottage cell fixtures from live capture) and
/// drives the failing-frame sphere through the same nearest-walkable
/// algorithm that the production
/// Transition.LogNearestWalkableCandidate diagnostic uses.
///
///
/// The failing frame is anchored at sphere world position
/// (141.7164, 8.3937, 92.0093) with radius 0.4800 — the
/// position the live client reports immediately before
/// stepup: FAILED — sliding back along normal. Equivalent
/// log lines are in
/// a6-issue98-negpoly-20260523-135032.out.log around line 11338
/// ([walkable-nearest]) + 11339 ([issue98-walkable-detail]).
///
///
///
/// What this test asserts is the CURRENT (failing) behavior — it
/// documents the bug. The retail comparison (Step 4 of the plan) will
/// add assertions that name what retail does at the same sphere
/// position; those assertions will fail until the underlying
/// predicate / transform divergence is fixed.
///
///
public class Issue98CellarUpReplayTests
{
// ── Failing-frame anchor data (from the negpoly capture log) ──
private static readonly Vector3 FailingFrameSphereWorld =
new(141.7164f, 8.3937f, 92.0093f);
private const float FailingFrameSphereRadius = 0.4800f;
private const float WalkableAllowance = 0.6642f; // PhysicsGlobals.FloorZ
private const float StepSearch = 0.6000f;
private const uint CellarId = 0xA9B40147u;
private const uint CottageNeighborA = 0xA9B40143u;
private const uint CottageNeighborB = 0xA9B40146u;
private static readonly string FixtureDir =
Path.Combine(SolutionRoot(), "tests", "AcDream.Core.Tests",
"Fixtures", "issue98");
[Fact]
public void Fixtures_AllThreeCellsLoadAndShareOrigin()
{
var cells = LoadAllThreeCells();
Assert.Equal(3, cells.Length);
foreach (var (id, cell) in cells)
{
Assert.NotEmpty(cell.Resolved);
// All three cells should sit at world origin (130.5, 11.5, 94.0).
var origin = Vector3.Transform(Vector3.Zero, cell.WorldTransform);
Assert.InRange(origin.X, 130.49f, 130.51f);
Assert.InRange(origin.Y, 11.49f, 11.51f);
Assert.InRange(origin.Z, 93.99f, 94.01f);
}
}
[Fact]
public void Cellar_HasMostPolygons_CottageNeighborBIsSparse()
{
var cellar = LoadCell(CellarId);
var nbrA = LoadCell(CottageNeighborA);
var nbrB = LoadCell(CottageNeighborB);
// Cellar is the densest cell — runtime captured 37 physics polys.
Assert.InRange(cellar.Resolved.Count, 30, 50);
// Cottage neighbor A is mid-density (~14 polys).
Assert.InRange(nbrA.Resolved.Count, 10, 20);
// Cottage neighbor B is sparse (~4 polys) — slice 5 handoff's
// "cottage main floor cell 0xA9B40146" inference was wrong;
// 0x0146 is a transition cell with no real floor coverage.
Assert.InRange(nbrB.Resolved.Count, 1, 8);
}
[Fact]
public void FailingFrame_CellarPrimary_HasCellarRampAsNearestWalkable()
{
var cell = LoadCell(CellarId);
var local = ToCellLocal(FailingFrameSphereWorld, cell);
var nearest = FindNearestWalkable(cell, local, FailingFrameSphereRadius);
// We are physically on the cellar ramp. The nearest walkable
// should be the ramp polygon itself (poly 0x0008 in retail's
// numbering — verified via the polydump capture). Normal Z
// ≈ 0.695 → walkable per FloorZ=0.6642.
Assert.True(nearest.Found,
"Cellar should always have at least one walkable candidate " +
"at the failing frame; the ramp's foot is right under the sphere.");
Assert.True(nearest.Polygon!.Plane.Normal.Z > WalkableAllowance,
"Nearest walkable normal must be above WalkableAllowance.");
}
///
/// The cellar-up failure: the step-up probe looks for a walkable
/// polygon in the cottage neighbour cells that the player can step
/// up onto. In 0xA9B40143 the nearest candidate is poly
/// 0x0004 (a flat Z=0 triangle = world Z=94, i.e. cottage
/// floor). At the failing-frame sphere position our predicate
/// reports:
/// - insideEdges = false (sphere XY is beyond the triangle
/// edge)
/// - overlapsSphere = false (sphere is too far below the
/// plane)
/// → no walkable accepted → step-up fails.
///
[Fact]
public void FailingFrame_CottageNeighborA_NearestWalkableIsOutsideSphereAndEdges()
{
var cell = LoadCell(CottageNeighborA);
var local = ToCellLocal(FailingFrameSphereWorld, cell);
var nearest = FindNearestWalkable(cell, local, FailingFrameSphereRadius);
Assert.True(nearest.Found,
"0xA9B40143 must have at least one walkable candidate " +
"(otherwise the nearest-walkable diagnostic itself would be wrong).");
// The CURRENT (failing) behavior — exposed precisely.
Assert.False(nearest.OverlapsSphere,
"Failing frame: sphere center is too far below the cottage floor " +
"plane in 0xA9B40143 — overlapsSphere is false.");
Assert.False(nearest.InsideEdges,
"Failing frame: sphere XY is beyond the cottage floor triangle's " +
"edge in 0xA9B40143 — insideEdges is false.");
}
///
/// The slice 5 handoff named 0xA9B40146 as "cottage main
/// floor cell" but the live capture shows it has only ~4 polygons —
/// far too sparse to be a real interior floor. At the failing-frame
/// sphere, the nearest-walkable query returns NO candidate at all.
///
[Fact]
public void FailingFrame_CottageNeighborB_HasNoWalkableCandidate()
{
var cell = LoadCell(CottageNeighborB);
var local = ToCellLocal(FailingFrameSphereWorld, cell);
var nearest = FindNearestWalkable(cell, local, FailingFrameSphereRadius);
// The CURRENT (failing) behavior — this cell offers nothing.
// Step 4's retail comparison will tell us whether retail also
// sees nothing in 0x0146, or whether retail's walkable query
// reaches a polygon ours doesn't.
Assert.False(nearest.Found,
"0xA9B40146 has no walkable polygon close enough to the failing-frame " +
"sphere for the nearest-walkable diagnostic to select.");
}
///
/// Composite assertion: across the two cottage neighbour cells,
/// NO walkable is accepted at the failing-frame sphere. This is the
/// bug. Step-up runs out of options → the player gets stuck on the
/// cellar ramp top.
///
[Fact]
public void FailingFrame_NoCottageNeighbourYieldsAcceptedWalkable()
{
var nbrA = LoadCell(CottageNeighborA);
var nbrB = LoadCell(CottageNeighborB);
var localA = ToCellLocal(FailingFrameSphereWorld, nbrA);
var localB = ToCellLocal(FailingFrameSphereWorld, nbrB);
var resultA = FindNearestWalkable(nbrA, localA, FailingFrameSphereRadius);
var resultB = FindNearestWalkable(nbrB, localB, FailingFrameSphereRadius);
// Either cell's nearest walkable could in principle be accepted
// (insideEdges + overlapsSphere). At the failing frame, neither
// is.
bool nbrAAccepted = resultA.Found && resultA.InsideEdges && resultA.OverlapsSphere;
bool nbrBAccepted = resultB.Found && resultB.InsideEdges && resultB.OverlapsSphere;
Assert.False(nbrAAccepted || nbrBAccepted,
"Failing frame: no cottage neighbour cell yields a walkable " +
"that passes both insideEdges and overlapsSphere — this is the " +
"issue #98 reproduction. Step 4's retail capture will tell us " +
"which polygon retail accepts in this scenario.");
}
///
/// Surface the EXACT geometry we're testing against — this assertion
/// makes the test self-documenting and will fail loudly if a future
/// fixture re-capture changes the polygon set.
///
[Fact]
public void FailingFrame_CottageNeighborA_Poly0x0004_HasExpectedShape()
{
var cell = LoadCell(CottageNeighborA);
Assert.True(cell.Resolved.TryGetValue(0x0004, out var poly),
"Poly 0x0004 must exist in 0xA9B40143 — it's the nearest-walkable " +
"candidate per the negpoly capture.");
Assert.NotNull(poly);
Assert.Equal(3, poly!.NumPoints);
Assert.Equal(0f, poly.Plane.Normal.X, precision: 4);
Assert.Equal(0f, poly.Plane.Normal.Y, precision: 4);
Assert.Equal(1f, poly.Plane.Normal.Z, precision: 4);
Assert.Equal(0f, poly.Plane.D, precision: 4);
}
// ─────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────
private static (uint Id, CellPhysics Cell)[] LoadAllThreeCells() => new[]
{
(CellarId, LoadCell(CellarId)),
(CottageNeighborA, LoadCell(CottageNeighborA)),
(CottageNeighborB, LoadCell(CottageNeighborB)),
};
private static CellPhysics LoadCell(uint cellId)
{
var path = Path.Combine(FixtureDir, $"0x{cellId:X8}.json");
Assert.True(File.Exists(path),
$"Fixture missing: {path}. Re-run cell-dump capture (Step 2 of plan).");
var dump = CellDumpSerializer.Read(path);
return CellDumpSerializer.Hydrate(dump);
}
private static Vector3 ToCellLocal(Vector3 worldPos, CellPhysics cell)
=> Vector3.Transform(worldPos, cell.InverseWorldTransform);
///
/// Replay of Transition.LogNearestWalkableCandidate's
/// algorithm. Returns the nearest candidate by absolute distance to
/// the polygon plane, along with the diagnostic fields the live
/// probe emits.
///
private static NearestWalkableResult FindNearestWalkable(
CellPhysics cell, Vector3 localCenter, float sphereRadius)
{
ResolvedPolygon? nearest = null;
ushort nearestId = 0;
float nearestAbs = float.MaxValue;
float nearestSigned = 0f;
bool overlaps = false;
bool insideEdges = false;
foreach (var (id, poly) in cell.Resolved)
{
float normalDotUp = Vector3.Dot(Vector3.UnitZ, poly.Plane.Normal);
if (normalDotUp <= WalkableAllowance)
continue;
float signed = Vector3.Dot(poly.Plane.Normal, localCenter) + poly.Plane.D;
float abs = MathF.Abs(signed);
if (abs >= nearestAbs)
continue;
nearest = poly;
nearestId = id;
nearestAbs = abs;
nearestSigned = signed;
// PhysicsGlobals.EPSILON ≈ 0.0001f; the production code subtracts it.
overlaps = abs <= sphereRadius - 1e-4f;
insideEdges = !BSPQuery.FindCrossedEdge(
poly.Plane, poly.Vertices, localCenter, Vector3.UnitZ, out _);
}
return new NearestWalkableResult
{
Found = nearest is not null,
Polygon = nearest,
PolygonId = nearestId,
SignedDistance = nearestSigned,
AbsDistance = nearestAbs,
OverlapsSphere = overlaps,
InsideEdges = insideEdges,
};
}
private sealed class NearestWalkableResult
{
public bool Found { get; init; }
public ResolvedPolygon? Polygon { get; init; }
public ushort PolygonId { get; init; }
public float SignedDistance { get; init; }
public float AbsDistance { get; init; }
public bool OverlapsSphere { get; init; }
public bool InsideEdges { get; init; }
}
private static string SolutionRoot()
{
// Walk up from the test binary until we find AcDream.slnx.
var dir = AppContext.BaseDirectory;
while (!string.IsNullOrEmpty(dir))
{
if (File.Exists(Path.Combine(dir, "AcDream.slnx")))
return dir;
dir = Path.GetDirectoryName(dir);
}
throw new InvalidOperationException(
"Could not locate AcDream.slnx from " + AppContext.BaseDirectory);
}
}