test(phys): §2b corner-seal replay — camera-penetration hypothesis REFUTED (openings, not walls)

Replays the captured corner/exit "escape" sweeps (corner-seal-capture.log) through
the real ResolveWithTransition with the camera probe call shape. Geometry-map
diagnostic proves every zero-contact traversal runs through a REAL opening: the
0170 exit-door portal for the viewer-outdoor frames; the 0171↔0173↔0172 doorway
chain (0173 = 20 cm threshold cell) for the corner-press frames. Captured eyes land
inside door-opening rectangles; the containment walk shows no path point in solid
cell volume; 8,703/14,230 indoor sweeps in the same capture collided correctly
(pull-in up to 2.77 m) — camera collision works, nothing penetrates.

The user-visible "background at the corner" is therefore NOT collision — it is the
§2a edge-on portal-clip collapse with the eye hovering at the doorway plane. Both
§4 siblings converge on one render-side mechanism: edge-on clip collapse near
opening planes. Next per the 2026-06-08 handoff §5.3: read retail's edge-on clip
oracle (PView::GetClip :432344, PView::ClipPortals :433572, polyClipFinish :702749)
before designing the fix.

Assertion fact = characterization pinning the verified behavior (openings pass
clean); Diagnostic fact = dispatch trace + room map + containment apparatus.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-10 09:59:48 +02:00
parent df2ef7c598
commit b21bb28918

View file

@ -0,0 +1,293 @@
using System;
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Core.Tests.Conformance;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.Core.Tests.Physics;
/// <summary>
/// §2b corner camera-seal replay (2026-06-10) — INVESTIGATION RESULT: the
/// "camera penetrates the wall at corners" hypothesis is REFUTED. Replays the
/// captured "escape" camera sweeps from <c>corner-seal-capture.log</c> through the
/// REAL <see cref="PhysicsEngine.ResolveWithTransition"/> with the camera's exact
/// call shape (<see cref="ObjectInfoState.IsViewer"/> | PathClipped | FreeRotate |
/// PerfectClip, single 0.3 m sphere — mirrors
/// src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.SweepEye).
///
/// WHAT THE REPLAY + GEOMETRY MAP PROVED (Diagnostic fact below):
/// every captured zero-contact traversal runs through a REAL OPENING, not a wall —
/// the building's exit-door portal (0170 → 0xFFFF) for the viewer-outdoor frames, and
/// the 0171↔0173↔0172 doorway chain (0173 is a 20 cm threshold cell — two parallel
/// portal planes at local x=4.10/3.90) for the corner-press frames. The captured eyes
/// land INSIDE the door openings' rectangles; the containment walk shows no path point
/// inside solid cell volume. 8,703 of 14,230 indoor sweeps in the same capture DID
/// collide (pull-in up to 2.77 m) — camera collision works; nothing penetrates.
/// The user-visible "background at the corner" is therefore NOT a collision bug: it is
/// the §2a edge-on portal-clip collapse with the eye hovering at the doorway plane
/// (render-side; see docs/research/2026-06-10 corner-seal handoff).
///
/// The assertion fact below is a CHARACTERIZATION pinning the verified-correct
/// behavior: viewer sweeps along these captured opening paths must pass WITHOUT
/// collision (a future change that makes doorway air solid would break the camera).
///
/// Capture provenance (worktree root, untracked): corner-seal-capture.log
/// L20104 / L20224 / L20386 (door-exit) + L23035 / L81340 / L87958 (corner press);
/// pivot = player + (0,0,1.5) per RetailChaseCamera.PivotHeight.
/// </summary>
public class CameraCornerSealReplayTests
{
private readonly ITestOutputHelper _out;
public CameraCornerSealReplayTests(ITestOutputHelper output) => _out = output;
private const float ViewerSphereRadius = 0.3f; // retail viewer_sphere (acclient :93314)
private const float PivotHeight = 1.5f; // RetailChaseCamera.PivotHeight
// Captured corner-escape samples: (label, start cellId, player feet position,
// desired eye = the [flap-sweep] in= value). Player from the same frame's
// [pv-input]; S1 has both the render-interpolated and raw physics player (1 cm
// apart) — both must stop at the wall, so both are exercised.
private static readonly (string Label, uint CellId, Vector3 Player, Vector3 Eye)[] Samples =
{
("S1-render 0170", 0xA9B40170u, new Vector3(154.934845f, 16.451527f, 94.000000f),
new Vector3(154.797028f, 19.320539f, 96.248833f)),
("S1-raw 0170", 0xA9B40170u, new Vector3(154.945374f, 16.451527f, 94.000000f),
new Vector3(154.797028f, 19.320539f, 96.248833f)),
("S2 0171", 0xA9B40171u, new Vector3(155.164307f, 14.392493f, 94.000000f),
new Vector3(154.804489f, 18.434128f, 96.248833f)),
("S3 0171", 0xA9B40171u, new Vector3(155.475723f, 11.463923f, 94.000000f),
new Vector3(154.990143f, 16.249460f, 96.248833f)),
};
private static (PhysicsEngine, PhysicsDataCache,
System.Collections.Generic.Dictionary<uint, AcDream.Core.World.Cells.EnvCell>)
BuildBuildingEngine(DatCollection dats)
{
// Same fixture as ThresholdPortalCrossingReplayTests.BuildBuildingEngine:
// the full Holtburg building cell range + a stub far-below landblock.
var cache = new PhysicsDataCache();
var engine = new PhysicsEngine { DataCache = cache };
var envCells = new System.Collections.Generic.Dictionary<uint, AcDream.Core.World.Cells.EnvCell>();
for (uint low = 0x016Fu; low <= 0x0175u; low++)
{
uint id = ConformanceDats.HoltburgLandblock | low;
envCells[id] = ConformanceDats.LoadEnvCell(dats, cache, id);
}
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, envCells);
}
/// <summary>
/// Mirror of PhysicsCameraCollisionProbe.SweepEye's transition call (the App-layer
/// probe is not referencable from Core tests; the call shape is duplicated here and
/// kept in sync by the file-header provenance note).
/// </summary>
private static ResolveResult SweepViewer(PhysicsEngine engine, Vector3 pivot, Vector3 desiredEye, uint cellId)
{
uint startCell = cellId;
if ((cellId & 0xFFFFu) >= 0x0100u)
{
var (pivotCell, found) = engine.AdjustPosition(cellId, pivot);
if (found) startCell = pivotCell;
}
// InitPath sphere-center convention: shift down by the radius (ToSpherePath).
Vector3 begin = pivot - new Vector3(0f, 0f, ViewerSphereRadius);
Vector3 end = desiredEye - new Vector3(0f, 0f, ViewerSphereRadius);
return engine.ResolveWithTransition(
currentPos: begin,
targetPos: end,
cellId: startCell,
sphereRadius: ViewerSphereRadius,
sphereHeight: 0f,
stepUpHeight: 0f,
stepDownHeight: 0f,
isOnGround: false,
body: null,
moverFlags: ObjectInfoState.IsViewer | ObjectInfoState.PathClipped
| ObjectInfoState.FreeRotate | ObjectInfoState.PerfectClip,
movingEntityId: 0);
}
/// <summary>
/// Diagnostic (no assertions): per-sweep BSP dispatch trace via the A6.P1
/// [push-back] probe family. Compares the LEAKING camera path (S1) against two
/// controls into the same north boundary: a horizontal viewer-style sweep at
/// chest height and a player-style grounded sweep. Outcome decides whether the
/// room's exterior boundary is sealed in the cell physics BSP at all (controls
/// collide ⇒ path/angle-specific miss) or genuinely open (nothing collides ⇒
/// the building-shell GfxObj is the only enclosure and the live isViewer
/// shadow-query path is the half that failed).
/// </summary>
[Fact]
public void Diagnostic_DispatchTrace_LeakPath_vs_Controls()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var (engine, cache, envCells) = BuildBuildingEngine(dats);
var s = Samples[0]; // S1-render 0170
Vector3 pivot = s.Player + new Vector3(0f, 0f, PivotHeight);
RunTraced(engine, "LEAK viewer pivot->eye", () =>
SweepViewer(engine, pivot, s.Eye, s.CellId));
RunTraced(engine, "CTRL-H viewer horizontal +3Y", () =>
SweepViewer(engine, pivot, pivot + new Vector3(0f, 3f, 0f), s.CellId));
RunTraced(engine, "CTRL-P player grounded +3Y", () =>
engine.ResolveWithTransition(
currentPos: s.Player,
targetPos: s.Player + new Vector3(0f, 3f, 0f),
cellId: s.CellId,
sphereRadius: 0.4f,
sphereHeight: 1.2f,
stepUpHeight: 0.4f,
stepDownHeight: 0.1f,
isOnGround: true,
body: null,
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
movingEntityId: 0));
// ── Geometry map: what does the leak path actually cross? ──────────────
_out.WriteLine("=== containment along LEAK path (pivot -> eye, 11 steps) ===");
for (int i = 0; i <= 10; i++)
{
Vector3 p = Vector3.Lerp(pivot, s.Eye, i / 10f);
string inCells = "";
foreach (var kv in envCells)
if (kv.Value.PointInCell(p))
inCells += $" 0x{kv.Key & 0xFFFFu:X4}";
if (inCells.Length == 0) inCells = " (none)";
_out.WriteLine(System.FormattableString.Invariant(
$" t={i / 10f:F1} ({p.X:F2},{p.Y:F2},{p.Z:F2}) ->{inCells}"));
}
_out.WriteLine("=== full room map: origin + portals (cell-LOCAL polygon bounds) ===");
foreach (uint low in new uint[] { 0x016Fu, 0x0170u, 0x0171u, 0x0172u, 0x0173u, 0x0174u, 0x0175u })
{
var cp = cache.GetCellStruct(ConformanceDats.HoltburgLandblock | low);
if (cp is null) { _out.WriteLine($" 0x{low:X4}: no CellPhysics"); continue; }
var o = cp.WorldTransform.Translation;
_out.WriteLine(System.FormattableString.Invariant(
$" 0x{low:X4} origin=({o.X:F2},{o.Y:F2},{o.Z:F2}) portals={cp.Portals.Count}"));
foreach (var portal in cp.Portals)
{
string bounds = "(polygon missing)";
if (cp.PortalPolygons is not null
&& cp.PortalPolygons.TryGetValue(portal.PolygonId, out var poly))
{
Vector3 min = new(float.MaxValue), max = new(float.MinValue);
foreach (var v in poly.Vertices)
{
min = Vector3.Min(min, v);
max = Vector3.Max(max, v);
}
bounds = System.FormattableString.Invariant(
$"x[{min.X:F2},{max.X:F2}] y[{min.Y:F2},{max.Y:F2}] z[{min.Z:F2},{max.Z:F2}]");
}
_out.WriteLine(System.FormattableString.Invariant(
$" -> other=0x{portal.OtherCellId:X4} poly={portal.PolygonId} {bounds}"));
}
}
// ── Containment for the live 0172->0171 corner frames (captured eye points
// + their pivots). The corner press lived in room 0172; the viewer classified
// into 0171 (2,963 frames). Which cell volume actually contains those points?
_out.WriteLine("=== containment: corner-press eyes + pivots (player=0172 viewer=0171 frames) ===");
var cornerPoints = new (string Label, Vector3 P)[]
{
("eyeA (L23035)", new Vector3(154.607880f, 11.154252f, 96.248787f)),
("eyeB (L81340)", new Vector3(157.477249f, 7.912723f, 96.248863f)),
("eyeC (L87958)", new Vector3(157.452667f, 7.914233f, 96.248856f)),
("pivotA", new Vector3(157.976959f, 8.622595f, 95.500000f)),
("pivotB/C", new Vector3(159.936676f, 7.701012f, 95.500000f)),
};
foreach (var (label, p) in cornerPoints)
{
string inCells = "";
foreach (var kv in envCells)
if (kv.Value.PointInCell(p))
inCells += $" 0x{kv.Key & 0xFFFFu:X4}";
if (inCells.Length == 0) inCells = " (none)";
_out.WriteLine(System.FormattableString.Invariant(
$" {label} ({p.X:F2},{p.Y:F2},{p.Z:F2}) ->{inCells}"));
}
}
private void RunTraced(PhysicsEngine engine, string label, Func<ResolveResult> sweep)
{
var sw = new StringWriter();
var prev = Console.Out;
ResolveResult r;
try
{
Console.SetOut(sw);
PhysicsDiagnostics.ProbePushBackEnabled = true;
r = sweep();
}
finally
{
PhysicsDiagnostics.ProbePushBackEnabled = false;
Console.SetOut(prev);
}
var lines = sw.ToString().Split('\n', StringSplitOptions.RemoveEmptyEntries);
_out.WriteLine(System.FormattableString.Invariant(
$"=== {label}: end=({r.Position.X:F3},{r.Position.Y:F3},{r.Position.Z:F3}) cell=0x{r.CellId:X8} collNorm={r.CollisionNormalValid} ok={r.Ok} probeLines={lines.Length} ==="));
for (int i = 0; i < lines.Length && i < 40; i++)
_out.WriteLine(" " + lines[i].TrimEnd());
if (lines.Length > 40)
_out.WriteLine($" ... +{lines.Length - 40} more");
}
[Fact]
public void ViewerSweep_ThroughOpenings_PassesWithoutCollision()
{
var datDir = ConformanceDats.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var (engine, _, _) = BuildBuildingEngine(dats);
var failures = new System.Collections.Generic.List<string>();
foreach (var s in Samples)
{
Vector3 pivot = s.Player + new Vector3(0f, 0f, PivotHeight);
float desiredBack = Vector3.Distance(pivot, s.Eye);
var r = SweepViewer(engine, pivot, s.Eye, s.CellId);
Vector3 eyeOut = r.Position + new Vector3(0f, 0f, ViewerSphereRadius);
float eyeBack = Vector3.Distance(pivot, eyeOut);
float pulledIn = desiredBack - eyeBack;
// Characterization (see file header): these captured paths run through
// real openings (exit door / doorway-threshold chain). The geometry-map
// diagnostic verified no solid is crossed, so the sweep must pass clean —
// no collision, full traversal.
bool passedClean = !r.CollisionNormalValid && pulledIn < 0.10f && r.Ok;
_out.WriteLine(System.FormattableString.Invariant(
$"{s.Label}: ok={r.Ok} collNorm={r.CollisionNormalValid} desiredBack={desiredBack:F2} eyeBack={eyeBack:F2} pulledIn={pulledIn:F2} endCell=0x{r.CellId:X8} {(passedClean ? "CLEAN" : "OBSTRUCTED")}"));
if (!passedClean)
failures.Add(s.Label);
}
Assert.True(failures.Count == 0,
"Viewer sweep through a verified-open doorway path was obstructed or cut short " +
"(camera would wrongly pull in at openings) for: " + string.Join(", ", failures));
}
}