using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; using AcDream.App.Rendering; using AcDream.Core.Physics; using AcDream.Core.Rendering; using DatReaderWriter; using DatReaderWriter.Options; using DatEnvCell = DatReaderWriter.DBObjs.EnvCell; using DatEnvironment = DatReaderWriter.DBObjs.Environment; using Xunit; using Xunit.Abstractions; namespace AcDream.App.Tests.Rendering; /// /// #177 decisive full-chain replay: drive the REAL /// + REAL down the Facility Hub /// staircase against the REAL BSP, then feed the swept (ViewerCellId, eye, /// View*Projection) into — exactly /// the production coupling (GameWindow ~9055-9065 + RetailPViewRenderer.DrawInside). /// /// The earlier Issue176177 replays fed SYNTHETIC (root, eye) pairs: Scenario A /// admitted the stairs fine from a coherent corridor root, Scenario E collapsed /// the flood 12->1 but only because it HELD the root at the old cell while the /// eye crossed a portal plane. This test removes the assumption: the root IS the /// swept camera cell, the eye IS the swept camera eye, both from the same sweep. /// If the stairs still collapse, #177 is a production root/eye coupling defect /// and the printed per-portal side-test D names the failing gate. If they stay /// admitted, the flood is exonerated and the artifact lives downstream. /// public class Issue177StairDescentCameraFloodTests { private const uint FacilityHub = 0x8A020000u; private readonly ITestOutputHelper _out; public Issue177StairDescentCameraFloodTests(ITestOutputHelper output) => _out = output; private static string? ResolveDatDir() { var fromEnv = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR"); if (!string.IsNullOrWhiteSpace(fromEnv) && Directory.Exists(fromEnv)) return fromEnv; var def = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Documents", "Asheron's Call"); return Directory.Exists(def) ? def : null; } private static PhysicsEngine BuildHubEngine(DatCollection dats) { var cache = new PhysicsDataCache(); var engine = new PhysicsEngine { DataCache = cache }; for (uint low = 0x0100u; low <= 0x01FFu; low++) { uint id = FacilityHub | low; var datCell = dats.Get(id); if (datCell is null) continue; var environment = dats.Get(0x0D000000u | datCell.EnvironmentId); if (environment is null) continue; if (!environment.Cells.TryGetValue(datCell.CellStructure, out var cellStruct) || cellStruct is null) continue; var world = Matrix4x4.CreateFromQuaternion(datCell.Position.Orientation) * Matrix4x4.CreateTranslation(datCell.Position.Origin); cache.CacheCellStruct(id, datCell, cellStruct, world); } var heights = new byte[81]; var heightTable = new float[256]; for (int i = 0; i < 256; i++) heightTable[i] = -1000f; engine.AddLandblock(FacilityHub, new TerrainSurface(heights, heightTable), Array.Empty(), Array.Empty(), 0f, 0f); return engine; } // Analytic ramp floor from Scenario B: flat -6 (x<95), descends to -9 across // x 95..98.333, then flat -9. private static float FloorZ(float x) => x < 95f ? -6f : x < 98.333f ? -6f - 3f * (x - 95f) / 3.333f : -9f; // Replicated side test (PortalVisibilityBuilder.CameraOnInteriorSide is private). private const float PortalSideEpsilon = 0.01f; private static bool EyeInteriorSide(LoadedCell cell, int i, Vector3 eye) { if (i >= cell.ClipPlanes.Count) return true; var pl = cell.ClipPlanes[i]; if (pl.Normal.LengthSquared() < 1e-8f) return true; var local = Vector3.Transform(eye, cell.InverseWorldTransform); float dot = Vector3.Dot(pl.Normal, local) + pl.D; return pl.InsideSide == 0 ? dot >= -PortalSideEpsilon : dot <= PortalSideEpsilon; } private static float SideD(LoadedCell cell, int i, Vector3 eye) { if (i >= cell.ClipPlanes.Count) return float.NaN; var pl = cell.ClipPlanes[i]; if (pl.Normal.LengthSquared() < 1e-8f) return float.NaN; var local = Vector3.Transform(eye, cell.InverseWorldTransform); return Vector3.Dot(pl.Normal, local) + pl.D; } private static string CellSet(IEnumerable ids) => string.Join(" ", ids.Select(id => $"{id & 0xFFFFu:X4}")); [Fact] public void StairCellComposition_ShellVsStatics() { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); foreach (uint low in new uint[] { 0x01C8, 0x01C4, 0x01C9, 0x0210, 0x020E, 0x01C1, 0x01C0 }) { uint id = FacilityHub | low; var envCell = dats.Get(id); if (envCell is null) { _out.WriteLine($"0x{low:X4}: (no dat cell)"); continue; } var environment = dats.Get(0x0D000000u | envCell.EnvironmentId); environment!.Cells.TryGetValue(envCell.CellStructure, out var cs); int polys = cs?.Polygons.Count ?? -1; int verts = cs?.VertexArray.Vertices.Count ?? -1; var mn = new Vector3(float.MaxValue); var mx = new Vector3(float.MinValue); if (cs is not null) foreach (var kv in cs.VertexArray.Vertices) { var o = kv.Value.Origin; var v = new Vector3(o.X, o.Y, o.Z); mn = Vector3.Min(mn, v); mx = Vector3.Max(mx, v); } int statics = envCell.StaticObjects?.Count ?? 0; bool seenOut = (envCell.Flags & DatReaderWriter.Enums.EnvCellFlags.SeenOutside) != 0; _out.WriteLine(FormattableString.Invariant( $"0x{low:X4} env=0x{envCell.EnvironmentId:X4} struct={envCell.CellStructure} shellPolys={polys} shellVerts={verts} bounds=[{mn.X:F1},{mn.Y:F1},{mn.Z:F1}]..[{mx.X:F1},{mx.Y:F1},{mx.Z:F1}] portals={envCell.CellPortals.Count} statics={statics} seenOut={seenOut}")); if (envCell.StaticObjects is not null) foreach (var stab in envCell.StaticObjects) { var o = stab.Frame.Origin; _out.WriteLine(FormattableString.Invariant( $" static Id=0x{stab.Id:X8} kind={(stab.Id >> 24 == 0x02 ? "Setup" : stab.Id >> 24 == 0x01 ? "GfxObj" : "?")} pos=({o.X:F2},{o.Y:F2},{o.Z:F2})")); } // Portal normals (local space) — horizontal (|Nz|~1) = FLOOR/CEILING portal // (the edge-on-clip suspect); vertical (|Nz|~0) = wall/doorway portal. var lc = renderLookup(id); if (lc is not null) for (int i = 0; i < lc.Portals.Count; i++) { var n = i < lc.ClipPlanes.Count ? lc.ClipPlanes[i].Normal : Vector3.Zero; string kind = MathF.Abs(n.Z) > 0.7f ? "FLOOR/CEIL" : MathF.Abs(n.Z) < 0.3f ? "wall" : "slope"; _out.WriteLine(FormattableString.Invariant( $" portal[{i}]->0x{lc.Portals[i].OtherCellId:X4} N=({n.X:F2},{n.Y:F2},{n.Z:F2}) {kind} inside={lc.ClipPlanes[i].InsideSide}")); } } LoadedCell? renderLookup(uint id) { try { return CornerFloodReplayTests.LoadCell(dats, id); } catch (InvalidOperationException) { return null; } } } /// /// #177 re-diagnosis (fix#1 failed the visual gate): the vanish flips on a SLIGHT turn + /// depends on zoom, so it is a gaze/eye knife-edge, not the eye-squarely-in-opening case /// fix#1 handled. Drive the REAL camera at a fixed player spot on the 015F stairs, sweep /// yaw FINELY at two zooms, and print the viewer cell + stair-cell admission each step to /// locate the knife-edge and which cell flips. /// [Fact] public void RealStaircase_FineYawZoomSweep_FindKnifeEdge() { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var renderCells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub); var engine = BuildHubEngine(dats); Func lookup = id => renderCells.TryGetValue(id, out var c) ? c : null; bool sC = CameraDiagnostics.CollideCamera; bool sA = CameraDiagnostics.AlignToSlope; try { CameraDiagnostics.CollideCamera = true; CameraDiagnostics.AlignToSlope = true; CameraDiagnostics.TranslationStiffness = 0.45f; CameraDiagnostics.RotationStiffness = 0.45f; uint[] stair = { 0x015Eu, 0x015Fu, 0x01C1u, 0x01C0u, 0x020Fu }; // Player standing mid-015F stairs, eye near the ceiling boundary (per the sweep char). foreach (var playerPos in new[] { new Vector3(60.5f, -46f, -2.5f), new Vector3(60.5f, -47f, -3.0f) }) foreach (float dist in new[] { 2.61f, 12.0f }) { _out.WriteLine(FormattableString.Invariant( $"=== player=({playerPos.X:F1},{playerPos.Y:F1},{playerPos.Z:F1}) zoom={dist} ===")); var cam = new RetailChaseCamera { CollisionProbe = new PhysicsCameraCollisionProbe(engine), FovY = MathF.PI / 3f, Aspect = 16f / 9f, Distance = dist, }; uint prevViewer = 0; string prevAdmit = ""; for (float yawDeg = 0f; yawDeg < 360f; yawDeg += 6f) { float yaw = yawDeg * MathF.PI / 180f; var (pcell, _) = engine.AdjustPosition(FacilityHub | 0x015Fu, playerPos + new Vector3(0, 0, 0.1f)); for (int i = 0; i < 160; i++) cam.Update(playerPos, yaw, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f, pcell, 0x5000000A); var eye = cam.Position; uint viewer = cam.ViewerCellId; string admit = "?"; if (viewer != 0u && renderCells.TryGetValue(viewer, out var rc)) { var f = PortalVisibilityBuilder.Build(rc, eye, lookup, cam.View * cam.Projection, null, PortalVisibilityBuilder.ShellDrawLiftZ); var s = f.OrderedVisibleCells; admit = string.Concat(stair.Select(low => (s.Contains(FacilityHub | low) ? "1" : "0"))); } // Print only when viewer or the admit pattern CHANGES (knife-edges). if (viewer != prevViewer || admit != prevAdmit) { _out.WriteLine(FormattableString.Invariant( $" yaw={yawDeg,3:F0} eye=({eye.X,6:F2},{eye.Y,6:F2},{eye.Z,6:F2}) viewer=0x{viewer & 0xFFFF:X4} stair[5E,5F,C1,C0,0F]={admit}")); prevViewer = viewer; prevAdmit = admit; } } } } finally { CameraDiagnostics.CollideCamera = sC; CameraDiagnostics.AlignToSlope = sA; } } /// /// #177 retail-vs-us flood DEPTH: retail's cdb capture shows PView cell_draw_num reaching 26 /// cells from camera cell 015E (avg 14.3) looking down the spiral shaft. Sweep OUR real camera /// from 015E over many gaze/zoom poses and report the max/avg OrderedVisibleCells — if ours /// caps well below retail's 26, our flood truncates the spiral (the vanish). /// [Fact] public void FloodDepthFrom015E_VsRetail26() { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var renderCells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub); var engine = BuildHubEngine(dats); Func lookup = id => renderCells.TryGetValue(id, out var c) ? c : null; bool sC = CameraDiagnostics.CollideCamera; bool sA = CameraDiagnostics.AlignToSlope; try { CameraDiagnostics.CollideCamera = true; CameraDiagnostics.AlignToSlope = true; CameraDiagnostics.TranslationStiffness = 0.45f; CameraDiagnostics.RotationStiffness = 0.45f; int maxCells = 0; uint bestViewer = 0; double sum = 0; int n = 0; string bestSet = ""; foreach (var playerPos in new[] { new Vector3(61f, -42f, -4f), new Vector3(60f, -41f, -5f), new Vector3(62f, -43f, -4f) }) foreach (float dist in new[] { 2.61f, 4f, 8f }) foreach (float pitch in new[] { 0.291f, 0.7f, -0.2f }) { var cam = new RetailChaseCamera { CollisionProbe = new PhysicsCameraCollisionProbe(engine), FovY = MathF.PI / 3f, Aspect = 16f / 9f, Distance = dist, Pitch = pitch }; for (float yawDeg = 0f; yawDeg < 360f; yawDeg += 10f) { float yaw = yawDeg * MathF.PI / 180f; var (pcell, _) = engine.AdjustPosition(FacilityHub | 0x015Eu, playerPos + new Vector3(0, 0, 0.1f)); for (int i = 0; i < 140; i++) cam.Update(playerPos, yaw, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f, pcell, 0x5000000A); uint viewer = cam.ViewerCellId; if (viewer == 0u || !renderCells.TryGetValue(viewer, out var rc)) continue; var f = PortalVisibilityBuilder.Build(rc, cam.Position, lookup, cam.View * cam.Projection, null, PortalVisibilityBuilder.ShellDrawLiftZ); int c = f.OrderedVisibleCells.Count; sum += c; n++; if (c > maxCells) { maxCells = c; bestViewer = viewer; bestSet = CellSet(f.OrderedVisibleCells.OrderBy(v => v)); } } } _out.WriteLine(FormattableString.Invariant( $"OUR flood from 015E-area: maxCells={maxCells} (retail max=26) avg={sum / Math.Max(1, n):F1} (retail avg=14.3) bestViewer=0x{bestViewer & 0xFFFF:X4}")); _out.WriteLine($" best set: [{bestSet}]"); } finally { CameraDiagnostics.CollideCamera = sC; CameraDiagnostics.AlignToSlope = sA; } } private static Matrix4x4 ViewProj(Vector3 eye, Vector3 dir) => Matrix4x4.CreateLookAt(eye, eye + Vector3.Normalize(dir), Vector3.UnitZ) * Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 16f / 9f, 0.1f, 5000f); // Signed distance of a WORLD point to a render cell's portal[i] plane (local-space plane). private static float PortalPlaneDistance(LoadedCell cell, int i, Vector3 worldPt) { var pl = cell.ClipPlanes[i]; var local = Vector3.Transform(worldPt, cell.InverseWorldTransform); return Vector3.Dot(pl.Normal, local) + pl.D; } /// /// #177 THE decisive experiment. Reproduce the captured failing frame — camera in /// cell 0x015F with the eye sitting IN the ceiling-portal plane (production [flap]: /// root=015F eye=(60.47,-46.57,-0.00), p->01C1 clip=0) — and confirm the upper /// stair cell 01C1 drops from the flood. Then move the SAME eye 0.4 m off that /// plane (into the cell interior) and confirm 01C1 is re-admitted. If the contrast /// holds, #177's fix is a viewpoint-off-the-portal-plane rule, and the load-bearing /// clip (#119/#181) is correct as-is. /// [Fact] public void EdgeOnCeilingPortal_DropsUpperCell_OffPlaneReadmits() { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub); Func lookup = id => cells.TryGetValue(id, out var c) ? c : null; var root = cells[FacilityHub | 0x015Fu]; uint upperId = FacilityHub | 0x01C1u; // Find 015F's portal to 01C1 (the ceiling/up portal) + report its normal. int upIdx = -1; for (int i = 0; i < root.Portals.Count; i++) if (root.Portals[i].OtherCellId == 0x01C1u) { upIdx = i; break; } Assert.True(upIdx >= 0, "015F has no portal to 01C1"); var upN = root.ClipPlanes[upIdx].Normal; _out.WriteLine(FormattableString.Invariant( $"015F portal[{upIdx}]->01C1 localN=({upN.X:F2},{upN.Y:F2},{upN.Z:F2})")); // Look UP the stairs so the ceiling portal + 01C1 land on screen. var gaze = new Vector3(0f, -0.3f, 1f); // Captured FAILING eye — in the ceiling-portal plane (world z ~0). var eyeEdgeOn = new Vector3(60.47f, -46.57f, -0.00f); // Same eye, pulled 0.4 m DOWN (off the ceiling portal, into 015F's interior). var eyeOffPlane = eyeEdgeOn + new Vector3(0f, 0f, -0.40f); foreach (var (label, eye) in new[] { ("edge-on(captured)", eyeEdgeOn), ("off-plane(-0.4z)", eyeOffPlane) }) { float d = PortalPlaneDistance(root, upIdx, eye); var frame = PortalVisibilityBuilder.Build( root, eye, lookup, ViewProj(eye, gaze), buildingMembership: null, drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ); var vis = frame.OrderedVisibleCells.OrderBy(v => v).ToList(); bool up = vis.Contains(upperId); string admit = up ? "ADMITTED" : "DROPPED"; _out.WriteLine(FormattableString.Invariant( $"{label,-18} eye=({eye.X,6:F2},{eye.Y,6:F2},{eye.Z,6:F2}) D->01C1={d,6:F3} 01C1={admit} flood={vis.Count} [{CellSet(vis)}]")); } // #177 CHARACTERIZATION (fix#1 reverted — this edge-on-in-plane case was NOT the // production mechanism; the real vanish is a grazing/sliver flood collapse in the // spiral, root 0x01C8, portals off-screen/sliver, camera NOT collision-jammed). // Kept as a mechanism pin: an eye exactly in the ceiling-portal plane drops the upper // cell (edge-on → <3 clip), an eye 0.4 m off admits it via the normal clip. var fEdge = PortalVisibilityBuilder.Build(root, eyeEdgeOn, lookup, ViewProj(eyeEdgeOn, gaze), null, PortalVisibilityBuilder.ShellDrawLiftZ); var fOff = PortalVisibilityBuilder.Build(root, eyeOffPlane, lookup, ViewProj(eyeOffPlane, gaze), null, PortalVisibilityBuilder.ShellDrawLiftZ); Assert.DoesNotContain(upperId, fEdge.OrderedVisibleCells); // edge-on drops (mechanism) Assert.Contains(upperId, fOff.OrderedVisibleCells); // off-plane admits (control) } /// /// #177 sweep characterization: place the player on the 015F staircase and drive the /// REAL chase camera + collision sweep, reporting where the eye lands relative to the /// 015F ceiling-portal plane (world z=0). If the eye rests AT the plane (|D|~0) the /// flood collapses (see EdgeOnCeilingPortal test); this shows whether that is a swept /// contact leaving the eye on the surface vs free-space boom geometry. /// [Fact] public void StaircaseSweep_EyeClearanceFromCeilingPortal() { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var renderCells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub); var engine = BuildHubEngine(dats); var root015F = renderCells[FacilityHub | 0x015Fu]; int upIdx = 0; // portal[0]->01C1 (ceiling) bool sC = CameraDiagnostics.CollideCamera; bool sA = CameraDiagnostics.AlignToSlope; try { CameraDiagnostics.CollideCamera = true; CameraDiagnostics.AlignToSlope = true; CameraDiagnostics.TranslationStiffness = 0.45f; CameraDiagnostics.RotationStiffness = 0.45f; // Player on the 015F stairs (world y~-46, ceiling portal at world z~0), swept // at a range of heights + facings (up-stairs = -Y, down-stairs = +Y). foreach (float pz in new[] { -1.0f, -1.5f, -2.0f, -2.5f, -3.0f, -3.5f }) foreach (var (fLabel, yaw) in new (string, float)[] { ("up(-Y)", -MathF.PI / 2f), ("down(+Y)", MathF.PI / 2f) }) { var playerPos = new Vector3(60.5f, -46f, pz); var (pcell, _) = engine.AdjustPosition(FacilityHub | 0x015Fu, playerPos + new Vector3(0, 0, 0.1f)); var cam = new RetailChaseCamera { CollisionProbe = new PhysicsCameraCollisionProbe(engine), FovY = MathF.PI / 3f, Aspect = 16f / 9f, }; for (int i = 0; i < 300; i++) cam.Update(playerPos, yaw, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f, pcell, 0x5000000A); var eye = cam.Position; uint viewer = cam.ViewerCellId; // D from the eye to 015F's ceiling-portal plane (only meaningful if viewer==015F). float dCeil = PortalPlaneDistance(root015F, upIdx, eye); _out.WriteLine(FormattableString.Invariant( $"playerZ={pz,5:F1} face={fLabel,-8} pcell=0x{pcell & 0xFFFF:X4} eye=({eye.X,6:F2},{eye.Y,6:F2},{eye.Z,6:F2}) viewer=0x{viewer & 0xFFFF:X4} D->ceilPortal={dCeil,6:F3}")); } } finally { CameraDiagnostics.CollideCamera = sC; CameraDiagnostics.AlignToSlope = sA; } } [Theory] // spot label, player x, player cell, description [InlineData(90.0f, 0x0178u, "TOP corridor 0178 (approach view)")] [InlineData(100.0f, 0x0183u, "BOTTOM room 0183 (look-back view)")] public void ParkedYawZoomSweep_StairAdmission(float px, uint pcell, string label) { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var renderCells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub); var engine = BuildHubEngine(dats); Func lookup = id => renderCells.TryGetValue(id, out var c) ? c : null; bool sC = CameraDiagnostics.CollideCamera; bool sA = CameraDiagnostics.AlignToSlope; float sT = CameraDiagnostics.TranslationStiffness, sR = CameraDiagnostics.RotationStiffness; try { CameraDiagnostics.CollideCamera = true; CameraDiagnostics.AlignToSlope = true; CameraDiagnostics.TranslationStiffness = 0.45f; CameraDiagnostics.RotationStiffness = 0.45f; var playerPos = new Vector3(px, -40f, FloorZ(px)); uint playerCell = FacilityHub | pcell; _out.WriteLine($"=== {label} @ ({px:F1},-40,{FloorZ(px):F2}) ==="); foreach (float dist in new[] { 2.61f, 8.0f }) { _out.WriteLine($" -- zoom Distance={dist} --"); var cam = new RetailChaseCamera { CollisionProbe = new PhysicsCameraCollisionProbe(engine), FovY = MathF.PI / 3f, Aspect = 16f / 9f, Distance = dist, }; for (float yawDeg = 0f; yawDeg < 360f; yawDeg += 30f) { float yaw = yawDeg * MathF.PI / 180f; // Settle at this yaw (static player). for (int i = 0; i < 200; i++) cam.Update(playerPos, yaw, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f, playerCell, 0x5000000A); Vector3 eye = cam.Position; uint viewer = cam.ViewerCellId; var vp = cam.View * cam.Projection; if (viewer != 0u && renderCells.TryGetValue(viewer, out var rootCell)) { var f = PortalVisibilityBuilder.Build(rootCell, eye, lookup, vp, null, PortalVisibilityBuilder.ShellDrawLiftZ); var vis = f.OrderedVisibleCells.OrderBy(v => v).ToList(); string flags = $"0178={(vis.Contains(FacilityHub | 0x0178u) ? "Y" : "-")} 0182={(vis.Contains(FacilityHub | 0x0182u) ? "Y" : "-")} 0183={(vis.Contains(FacilityHub | 0x0183u) ? "Y" : "-")} 0181={(vis.Contains(FacilityHub | 0x0181u) ? "Y" : "-")}"; _out.WriteLine(FormattableString.Invariant( $" yaw={yawDeg,3:F0} eye=({eye.X,6:F2},{eye.Y,6:F2},{eye.Z,6:F2}) viewer=0x{viewer & 0xFFFF:X4} {flags} flood={vis.Count} [{CellSet(vis)}]")); } else _out.WriteLine(FormattableString.Invariant($" yaw={yawDeg,3:F0} eye=({eye.X,6:F2},{eye.Y,6:F2},{eye.Z,6:F2}) viewer=0x{viewer:X8} ROOT-MISSING")); } } } finally { CameraDiagnostics.CollideCamera = sC; CameraDiagnostics.AlignToSlope = sA; CameraDiagnostics.TranslationStiffness = sT; CameraDiagnostics.RotationStiffness = sR; } } [Fact] public void Descent_RealCameraSweep_StairCellRetention() { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var renderCells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub); var engine = BuildHubEngine(dats); Func lookup = id => renderCells.TryGetValue(id, out var c) ? c : null; bool savedColl = CameraDiagnostics.CollideCamera; bool savedAlign = CameraDiagnostics.AlignToSlope; float savedT = CameraDiagnostics.TranslationStiffness; float savedR = CameraDiagnostics.RotationStiffness; try { CameraDiagnostics.CollideCamera = true; CameraDiagnostics.AlignToSlope = true; CameraDiagnostics.TranslationStiffness = 0.45f; CameraDiagnostics.RotationStiffness = 0.45f; var cam = new RetailChaseCamera { CollisionProbe = new PhysicsCameraCollisionProbe(engine), FovY = MathF.PI / 3f, Aspect = 16f / 9f, }; const float yaw = 0f; // facing +X, descending const float speed = 3.0f; // m/s run const float dt = 1f / 60f; float startX = 88f, endX = 101.5f; // Seat the player + settle the camera at the start pose. uint playerCell = FacilityHub | 0x0178u; Vector3 P(float x) => new Vector3(x, -40f, FloorZ(x)); for (int i = 0; i < 400; i++) cam.Update(P(startX), yaw, Vector3.Zero, true, Vector3.UnitZ, dt, playerCell, 0x5000000A); _out.WriteLine("x playerCell eye=(x,y,z) viewerCell coh flood stairs rootPortalsPastPlane"); float x = startX; uint prevViewer = cam.ViewerCellId; int collapses = 0, incoherentFrames = 0; while (x <= endX) { var playerPos = P(x); // Track player membership like production: AdjustPosition carry-forward. var (pc, found) = engine.AdjustPosition(playerCell, playerPos + new Vector3(0, 0, 0.1f)); if (found) playerCell = pc; cam.Update(playerPos, yaw, new Vector3(speed, 0, 0), true, Vector3.UnitZ, dt, playerCell, 0x5000000A); Vector3 eye = cam.Position; uint viewer = cam.ViewerCellId; var viewProj = cam.View * cam.Projection; string floodStr, stairs, coh, past; if (viewer != 0u && renderCells.TryGetValue(viewer, out var rootCell)) { var frame = PortalVisibilityBuilder.Build( rootCell, eye, lookup, viewProj, buildingMembership: null, drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ); var vis = frame.OrderedVisibleCells.OrderBy(v => v).ToList(); floodStr = $"{vis.Count,2} [{CellSet(vis)}]"; bool ramp = vis.Contains(FacilityHub | 0x0182u); bool lower = vis.Contains(FacilityHub | 0x0183u); stairs = $"r{(ramp ? "Y" : "-")}l{(lower ? "Y" : "-")}"; // Coherence: is the eye on the interior side of ALL of root's portals? // If not, the eye is outside root -> the crossed portal's subtree drops. var pastList = new List(); bool coherent = true; for (int i = 0; i < rootCell.Portals.Count && i < rootCell.ClipPlanes.Count; i++) { if (!EyeInteriorSide(rootCell, i, eye)) { coherent = false; pastList.Add($"p{i}->0x{rootCell.Portals[i].OtherCellId:X4}(D={SideD(rootCell, i, eye):F3})"); } } coh = coherent ? "IN " : "OUT"; past = pastList.Count == 0 ? "-" : string.Join(",", pastList); if (!coherent) incoherentFrames++; if (vis.Count <= 1) collapses++; } else { floodStr = "root-missing/0"; stairs = "--"; coh = "?"; past = $"viewer=0x{viewer:X8}"; } bool viewerFlip = viewer != prevViewer; prevViewer = viewer; _out.WriteLine(FormattableString.Invariant( $"{x,6:F2} 0x{playerCell:X8} ({eye.X,7:F3},{eye.Y,7:F3},{eye.Z,7:F3}) 0x{viewer:X8} {coh} {floodStr} {stairs} {(viewerFlip ? "FLIP " : "")}{past}")); x += speed * dt; } _out.WriteLine($"SUMMARY: collapses(flood<=1)={collapses} incoherentFrames(eye-outside-root)={incoherentFrames}"); } finally { CameraDiagnostics.CollideCamera = savedColl; CameraDiagnostics.AlignToSlope = savedAlign; CameraDiagnostics.TranslationStiffness = savedT; CameraDiagnostics.RotationStiffness = savedR; } } }