using System; using System.Collections.Generic; using System.IO; using System.Numerics; using AcDream.App.Rendering; using DatReaderWriter; using DatReaderWriter.Options; using Xunit; using Xunit.Abstractions; using DatEnvCell = DatReaderWriter.DBObjs.EnvCell; using DatEnvironment = DatReaderWriter.DBObjs.Environment; namespace AcDream.App.Tests.Rendering; /// /// §4 corner-press flood replay (2026-06-10). The §2b camera-collision hypothesis is /// REFUTED (CameraCornerSealReplayTests, commit b21bb28): the eye legitimately enters /// the neighbour room through the 0171↔0173↔0172 doorway chain. The user still sees /// BACKGROUND at certain angles — so the defect is in the FLOOD/CLIP output for those /// eye positions. This harness drives the REAL /// over the REAL Holtburg building cells (dat-loaded, mirroring GameWindow.BuildLoadedCell) /// along the captured corner eye path, and prints which cells the flood keeps/drops per /// step. The player's room (0172) dropping while the player is visible on screen is the /// background defect; the step where it happens pins the mechanism. /// /// Geometry (corner-seal-capture.log + the b21bb28 room map): all cells share origin /// (161.93, 7.50, 94.00), local = R180z·(world−origin). The 0171-side doorway plane is /// local x=4.10 (world x≈157.83), the 0172-side plane local x=3.90 (world x≈158.03); /// the opening is 1.9 m wide, full height (z 94..96.5). Player dwell position during the /// corner press: (159.94, 7.70, 94.00) in 0172; captured eyes hover near (157.4..157.5, /// 7.91, 96.25) — inside 0171, 0.35 m past the doorway plane, near the ceiling. /// public class CornerFloodReplayTests { private readonly ITestOutputHelper _out; public CornerFloodReplayTests(ITestOutputHelper output) => _out = output; internal const uint Landblock = 0xA9B40000u; private const uint EnvironmentFilePrefix = 0x0D000000u; internal 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; } /// /// Dat → LoadedCell, mirroring GameWindow.BuildLoadedCell (GameWindow.cs:5636-5776) /// field-for-field: portals (with OtherPortalId back-link), clip planes from the /// portal polygon's first 3 verts + centroid InsideSide, full portal polygons in /// cell-local space, local AABB, world transform from EnvCell.Position. /// internal static LoadedCell LoadCell(DatCollection dats, uint cellId) => LoadCell(dats, cellId, Vector3.Zero); /// worldOffset: block offset for multi-landblock fixtures (dat cell /// positions are landblock-local; a neighbour block needs ±192 per axis). internal static LoadedCell LoadCell(DatCollection dats, uint cellId, Vector3 worldOffset) { var envCell = dats.Get(cellId) ?? throw new InvalidOperationException($"EnvCell 0x{cellId:X8} not found"); var environment = dats.Get(EnvironmentFilePrefix | envCell.EnvironmentId) ?? throw new InvalidOperationException($"Environment 0x{envCell.EnvironmentId:X8} not found"); if (!environment.Cells.TryGetValue(envCell.CellStructure, out var cellStruct) || cellStruct is null) throw new InvalidOperationException($"CellStruct {envCell.CellStructure} missing"); var cellTransform = Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) * Matrix4x4.CreateTranslation(envCell.Position.Origin + worldOffset); Matrix4x4.Invert(cellTransform, out var inverse); var boundsMin = new Vector3(float.MaxValue); var boundsMax = new Vector3(float.MinValue); foreach (var kvp in cellStruct.VertexArray.Vertices) { var v = kvp.Value; var pos = new Vector3(v.Origin.X, v.Origin.Y, v.Origin.Z); boundsMin = Vector3.Min(boundsMin, pos); boundsMax = Vector3.Max(boundsMax, pos); } if (boundsMin.X == float.MaxValue) { boundsMin = Vector3.Zero; boundsMax = Vector3.Zero; } var portals = new List(); var clipPlanes = new List(); var portalPolygons = new List(); var centroid = (boundsMin + boundsMax) * 0.5f; foreach (var portal in envCell.CellPortals) { portals.Add(new CellPortalInfo( portal.OtherCellId, portal.PolygonId, (ushort)portal.Flags, portal.OtherPortalId)); if (cellStruct.Polygons.TryGetValue(portal.PolygonId, out var poly) && poly.VertexIds.Count >= 3 && cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[0], out var v0) && cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[1], out var v1) && cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[2], out var v2)) { var p0 = new Vector3(v0.Origin.X, v0.Origin.Y, v0.Origin.Z); var p1 = new Vector3(v1.Origin.X, v1.Origin.Y, v1.Origin.Z); var p2 = new Vector3(v2.Origin.X, v2.Origin.Y, v2.Origin.Z); var normal = Vector3.Normalize(Vector3.Cross(p1 - p0, p2 - p0)); float d = -Vector3.Dot(normal, p0); float centroidDot = Vector3.Dot(normal, centroid) + d; clipPlanes.Add(new PortalClipPlane { Normal = normal, D = d, InsideSide = centroidDot >= 0 ? 0 : 1, }); } else { clipPlanes.Add(default); } Vector3[] polyVerts = Array.Empty(); if (cellStruct.Polygons.TryGetValue(portal.PolygonId, out var portalPoly) && portalPoly.VertexIds.Count >= 3) { polyVerts = new Vector3[portalPoly.VertexIds.Count]; bool allResolved = true; for (int vi = 0; vi < portalPoly.VertexIds.Count; vi++) { if (cellStruct.VertexArray.Vertices.TryGetValue( (ushort)portalPoly.VertexIds[vi], out var pv)) polyVerts[vi] = new Vector3(pv.Origin.X, pv.Origin.Y, pv.Origin.Z); else { allResolved = false; break; } } if (!allResolved) polyVerts = Array.Empty(); } portalPolygons.Add(polyVerts); } uint lbPrefix = cellId & 0xFFFF0000u; var visibleCells = new List(); if (envCell.VisibleCells is not null) foreach (var lowId in envCell.VisibleCells) visibleCells.Add(lbPrefix | lowId); return new LoadedCell { CellId = cellId, WorldPosition = envCell.Position.Origin + worldOffset, WorldTransform = cellTransform, InverseWorldTransform = inverse, LocalBoundsMin = boundsMin, LocalBoundsMax = boundsMax, Portals = portals, ClipPlanes = clipPlanes, PortalPolygons = portalPolygons, VisibleCells = visibleCells, SeenOutside = envCell.Flags.HasFlag(DatReaderWriter.Enums.EnvCellFlags.SeenOutside), }; } internal static Dictionary LoadBuilding(DatCollection dats) { var cells = new Dictionary(); for (uint low = 0x016Fu; low <= 0x0175u; low++) { uint id = Landblock | low; cells[id] = LoadCell(dats, id); } return cells; } // Production projection: ChaseCamera/FlyCamera use FovY ~1.2, near 1, far 5000, // 1280x720 (the capture's viewport). The flood's clip is near-independent // (PortalProjection header), so near/far exactness is not load-bearing. private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 lookAt) { var view = Matrix4x4.CreateLookAt(eye, lookAt, Vector3.UnitZ); var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 1f, 5000f); return view * proj; } // World-x of the doorway planes (local x=4.10 / 3.90 under the R180z transform // about origin x=161.93). private const float Plane0171X = 161.93f - 4.10f; // ≈157.83 private const float Plane0172X = 161.93f - 3.90f; // ≈158.03 private static uint RootCellFor(float eyeX) => eyeX > Plane0172X ? (Landblock | 0x0172u) : eyeX > Plane0171X ? (Landblock | 0x0173u) : (Landblock | 0x0171u); /// /// Diagnostic: full [pv-trace] decision log for one GOOD step (47) vs one GLITCH /// step (48) of the sweep below — 2 cm apart, root 0171 both, yet 0172 (and its /// downstream 016F + outside) vanish from the flood at 48. The trace diff pins the /// exact gate that flips. /// [Fact] public void Diagnostic_TraceGoodVsGlitchStep() { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cells = LoadBuilding(dats); Func lookup = id => cells.TryGetValue(id, out var c) ? c : null; var player = new Vector3(159.936676f, 7.701012f, 94.000000f); var pivot = player + new Vector3(0f, 0f, 1.5f); foreach (int i in new[] { 47, 48 }) { float ex = 158.43f - i * 0.02f; var eye = new Vector3(ex, 7.912722f, 96.248833f); uint root = RootCellFor(ex); var sw = new StringWriter(); var prev = Console.Out; PortalVisibilityFrame frame; try { Console.SetOut(sw); AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled = true; frame = PortalVisibilityBuilder.Build(cells[root], eye, lookup, ViewProjFor(eye, pivot)); } finally { AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled = false; Console.SetOut(prev); } _out.WriteLine($"########## step {i} (eyeX={ex:F2}) ##########"); foreach (var line in sw.ToString().Split('\n', StringSplitOptions.RemoveEmptyEntries)) _out.WriteLine(line.TrimEnd()); // Per-hop region vertices: where does the sliver come from? foreach (uint low in new uint[] { 0x0171u, 0x0173u, 0x0172u }) { if (!frame.CellViews.TryGetValue(Landblock | low, out var view)) { _out.WriteLine($" view 0x{low:X4}: (absent)"); continue; } foreach (var p in view.Polygons) { string verts = ""; foreach (var v in p.Vertices) verts += System.FormattableString.Invariant($" ({v.X:F5},{v.Y:F5})"); _out.WriteLine($" view 0x{low:X4} [{p.Vertices.Length}]:{verts}"); } if (view.Polygons.Count == 0) _out.WriteLine($" view 0x{low:X4}: 0 polygons (rejected by Add)"); } } } /// /// §4 conformance gate (2026-06-08 handoff §7's pre-gate, realized 2026-06-10): /// a smooth monotonic eye moving through/near a doorway must produce a STABLE, /// monotone flood — the full cell chain present on every step, the outside view /// always reached, and the player-room region never collapsing nor oscillating. /// Before the homogeneous-reciprocal fix this failed on ~10 of 61 steps (room /// 0172 vanished entirely or collapsed to a sub-pixel sliver — the corner / /// transition background strobe). Guards the ApplyReciprocalClip pipeline. /// [Fact] public void CornerSweep_FloodIsCompleteAndMonotone() { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cells = LoadBuilding(dats); Func lookup = id => cells.TryGetValue(id, out var c) ? c : null; var player = new Vector3(159.936676f, 7.701012f, 94.000000f); var pivot = player + new Vector3(0f, 0f, 1.5f); // W=0 port (2026-06-11) — the criterion is retail's, not the rescue's: // - Cells BEHIND the camera are NOT required (retail polyClipFinish clips an // all-behind portal to empty; the rescue used to flood them anyway). The eye // looks AT the player throughout, so the doorway chain is behind the camera // until the eye recedes through it: require 0173 from root=0173 on, 0171 // from root=0171 on. 0172 (the looked-at room with the player) is required // at EVERY step — that is THE user-visible §4 invariant. // - The two KNIFE-EDGE steps (eye exactly ON a doorway plane, the sweep grid // lands on the plane constants) propagate retail's zero-area degenerate view: // the chain stays in the draw list (cells draw whole) but the 0172 region is // legitimately zero-area and the onward flood (016F/outside) legitimately // dies for that frame — exempt those assertions there. // - Monotone shrink holds WITHIN a root regime; the root flip is a legitimate // discontinuity (FullScreen root view -> portal-clipped view). var failures = new List(); float prevArea = float.MaxValue; uint prevRoot = 0; for (int i = 0; i <= 60; i++) { float ex = 158.43f - i * 0.02f; var eye = new Vector3(ex, 7.912722f, 96.248833f); uint root = RootCellFor(ex); bool knifeEdge = MathF.Abs(ex - Plane0172X) < 0.005f || MathF.Abs(ex - Plane0171X) < 0.005f; var frame = PortalVisibilityBuilder.Build( cells[root], eye, lookup, ViewProjFor(eye, pivot)); // Knife-edge steps: the eye sits EXACTLY on a cell-boundary plane, so the // root pick itself is ambiguous (production BSP picks either side; a damped // float eye never lands exactly on the plane). The portal whose plane // contains the eye is ~perpendicular to the gaze here — genuinely // off-screen, retail's screen-bounded clip floods nothing through it from // the far-side root. Require only the root; the neighbouring steps (±2 cm, // where the real strobe class lived) carry the full criterion. var required = new List(); if (knifeEdge) { required.Add(root & 0xFFFFu); } else { required.Add(0x0172u); if (root != (Landblock | 0x0172u)) required.Add(0x0173u); if (root == (Landblock | 0x0171u)) required.Add(0x0171u); required.Add(0x016Fu); } foreach (uint low in required) if (!frame.OrderedVisibleCells.Contains(Landblock | low)) failures.Add($"step {i}: 0x{low:X4} missing from flood (root=0x{root & 0xFFFFu:X4}{(knifeEdge ? ", knife-edge" : "")})"); if (!knifeEdge && frame.OutsideView.Polygons.Count == 0) failures.Add($"step {i}: outside view empty"); float area = 0f; if (frame.CellViews.TryGetValue(Landblock | 0x0172u, out var view)) foreach (var p in view.Polygons) area += MathF.Max(0f, p.MaxX - p.MinX) * MathF.Max(0f, p.MaxY - p.MinY); if (!knifeEdge && area < 0.5f) failures.Add(System.FormattableString.Invariant( $"step {i}: 0172 region collapsed (area={area:F3})")); // Monotone shrink as the eye recedes — allow float-noise upticks only. // Reset at root flips + knife-edge frames (legitimate discontinuities). if (root == prevRoot && !knifeEdge && prevArea != float.MaxValue && area > prevArea + 0.01f) failures.Add(System.FormattableString.Invariant( $"step {i}: 0172 region grew {prevArea:F3}->{area:F3} (oscillation)")); prevArea = knifeEdge ? float.MaxValue : area; prevRoot = root; } Assert.True(failures.Count == 0, "Flood instability under a monotonic eye sweep (the §4 strobe):\n " + string.Join("\n ", failures)); } /// /// #120 repro (T5 gate, 2026-06-11): the T5 launch log fired /// `[pv-ERROR] in-place propagation tripwire at depth 128` on cottage /// interior cells 0xA9B40175/0174 (+0162, a different building) while /// the user walked the doorways — i.e. the eye-on-portal-plane regime. /// Sweep the eye ACROSS every portal plane of every cell in this /// building (±6 cm in 5 mm steps, looking through the opening), seeding /// from BOTH sides' cells. /// The in-place growth's fixpoint invariant must hold at every step — /// the tripwire count stays 0. /// [Fact] public void PortalPlaneCrossings_InPlacePropagationConverges() { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cells = LoadBuilding(dats); Func lookup = id => cells.TryGetValue(id, out var c) ? c : null; PortalVisibilityBuilder.ConvergenceTripwireCount = 0; var firings = new List(); foreach (var cell in cells.Values) { for (int i = 0; i < cell.Portals.Count && i < cell.PortalPolygons.Count; i++) { var poly = cell.PortalPolygons[i]; if (poly == null || poly.Length < 3) continue; if (i >= cell.ClipPlanes.Count) continue; var plane = cell.ClipPlanes[i]; if (plane.Normal.LengthSquared() < 1e-6f) continue; // Portal centroid + plane normal in WORLD space. var centroidLocal = Vector3.Zero; foreach (var v in poly) centroidLocal += v; centroidLocal /= poly.Length; var centroidWorld = Vector3.Transform(centroidLocal, cell.WorldTransform); var normalWorld = Vector3.Normalize( Vector3.TransformNormal(plane.Normal, cell.WorldTransform)); uint neighbourId = cell.Portals[i].OtherCellId == 0xFFFF ? 0u : (Landblock | cell.Portals[i].OtherCellId); var roots = new List { cell }; if (neighbourId != 0u && cells.TryGetValue(neighbourId, out var nb)) roots.Add(nb); for (int step = -12; step <= 12; step++) { var eye = centroidWorld + normalWorld * (step * 0.005f); // Look through the opening (along -normal), slightly down // — the portal fills the view, maximizing flood activity. var lookAt = centroidWorld - normalWorld * 2f + new Vector3(0f, 0f, -0.2f); foreach (var root in roots) { int before = PortalVisibilityBuilder.ConvergenceTripwireCount; PortalVisibilityBuilder.Build(root, eye, lookup, ViewProjFor(eye, lookAt)); int after = PortalVisibilityBuilder.ConvergenceTripwireCount; if (after != before) firings.Add(System.FormattableString.Invariant( $"cell=0x{cell.CellId:X8} portal#{i}->0x{cell.Portals[i].OtherCellId:X4} step={step} root=0x{root.CellId:X8} eye=({eye.X:F4},{eye.Y:F4},{eye.Z:F4})")); } } } } Assert.True(firings.Count == 0, "#120: in-place propagation convergence tripwire fired during the " + "portal-plane sweep:\n " + string.Join("\n ", firings)); } /// /// #120 repro attempt 2: eyes INSIDE each cell's volume (3×3 XY grid × /// 2 Z levels) with full yaw (8) × pitch (3) direction sweeps — the /// walking-and-turning regime of the T5 session, including the steep /// pitches of the cellar stairs. Same invariant: the tripwire count /// stays 0 across every Build. /// [Fact] public void InCellDirectionSweep_InPlacePropagationConverges() { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cells = LoadBuilding(dats); Func lookup = id => cells.TryGetValue(id, out var c) ? c : null; PortalVisibilityBuilder.ConvergenceTripwireCount = 0; var firings = new List(); int builds = 0; foreach (var cell in cells.Values) { var lo = cell.LocalBoundsMin; var hi = cell.LocalBoundsMax; if (hi.X - lo.X < 0.05f || hi.Y - lo.Y < 0.05f) continue; for (int gx = 0; gx < 3; gx++) for (int gy = 0; gy < 3; gy++) for (int gz = 0; gz < 2; gz++) { var local = new Vector3( lo.X + (hi.X - lo.X) * (0.2f + 0.3f * gx), lo.Y + (hi.Y - lo.Y) * (0.2f + 0.3f * gy), lo.Z + (hi.Z - lo.Z) * (gz == 0 ? 0.3f : 0.8f)); var eye = Vector3.Transform(local, cell.WorldTransform); for (int yaw = 0; yaw < 8; yaw++) for (int pitch = -1; pitch <= 1; pitch++) { float a = yaw * MathF.PI / 4f; var dir = new Vector3( MathF.Cos(a), MathF.Sin(a), pitch * 0.9f); var lookAt = eye + Vector3.Normalize(dir) * 3f; int before = PortalVisibilityBuilder.ConvergenceTripwireCount; PortalVisibilityBuilder.Build(cell, eye, lookup, ViewProjFor(eye, lookAt)); builds++; int after = PortalVisibilityBuilder.ConvergenceTripwireCount; if (after != before) firings.Add(System.FormattableString.Invariant( $"cell=0x{cell.CellId:X8} eye=({eye.X:F3},{eye.Y:F3},{eye.Z:F3}) yaw={yaw} pitch={pitch}")); } } } _out.WriteLine($"builds={builds} firings={firings.Count}"); Assert.True(firings.Count == 0, "#120: convergence tripwire fired during the in-cell direction sweep:\n " + string.Join("\n ", firings)); } /// /// Diagnostic: microscope on the failing hop. Replays the two portal hops /// (0171→0173, then 0173→0172) through the PUBLIC PortalProjection APIs for the /// good step (47) and the glitch step (48), printing the homogeneous subject /// vertices and full-precision outputs at every stage — the concrete numbers for /// the collapsing intersection. /// [Fact] public void Diagnostic_Hop2Microscope() { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cells = LoadBuilding(dats); var c171 = cells[Landblock | 0x0171u]; var c173 = cells[Landblock | 0x0173u]; var player = new Vector3(159.936676f, 7.701012f, 94.000000f); var pivot = player + new Vector3(0f, 0f, 1.5f); // 0171's portal to 0173 is index 1; 0173's portal to 0172 is index 1 (room map). var poly171to173 = c171.PortalPolygons[1]; var poly173to172 = c173.PortalPolygons[1]; foreach (int i in new[] { 47, 48 }) { float ex = 158.43f - i * 0.02f; var eye = new Vector3(ex, 7.912722f, 96.248833f); var vp = ViewProjFor(eye, pivot); _out.WriteLine($"########## step {i} (eyeX={ex:F2}) ##########"); // Hop 1: 0171 -> 0173 against the full screen. var subj1 = PortalProjection.ProjectToClip(poly171to173, c171.WorldTransform, vp); DumpClip("hop1 subject (0171->0173 portal, clip space)", subj1); var region173 = PortalProjection.ClipToRegion(subj1, new[] { new Vector2(-1f, -1f), new Vector2(1f, -1f), new Vector2(1f, 1f), new Vector2(-1f, 1f), }); DumpNdc("hop1 out = 0173 region", region173); if (region173.Length < 3) continue; // Hop 2: 0173 -> 0172 against the 0173 region. var subj2 = PortalProjection.ProjectToClip(poly173to172, c173.WorldTransform, vp); DumpClip("hop2 subject (0173->0172 portal, clip space)", subj2); var region172 = PortalProjection.ClipToRegion(subj2, region173); DumpNdc("hop2 out = 0172 region", region172); } } /// Scratch: the homogeneous reciprocal primitive in isolation, on the /// synthetic fixture geometry that Build_AppliesReciprocalOtherPortalClip uses. [Fact] public void Scratch_ReciprocalPrimitive_SyntheticPair() { var view = Matrix4x4.CreateLookAt(Vector3.Zero, new Vector3(0, 0, -1), Vector3.UnitY); var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1.0f, 0.1f, 1000f); var vp = view * proj; Vector3[] Quad(float cx, float cy, float hw, float hh, float z) => new[] { new Vector3(cx - hw, cy - hh, z), new Vector3(cx + hw, cy - hh, z), new Vector3(cx + hw, cy + hh, z), new Vector3(cx - hw, cy + hh, z), }; var wide = Quad(0f, 0f, 0.9f, 0.9f, -3f); var narrow = Quad(0f, 0f, 0.3f, 0.9f, -3f); // Near-side region: wide portal clipped against the full screen. var wideClip = PortalProjection.ProjectToClip(wide, Matrix4x4.Identity, vp); DumpClip("wide subject", wideClip); var wideRegion = PortalProjection.ClipToRegion(wideClip, new[] { new Vector2(-1f, -1f), new Vector2(1f, -1f), new Vector2(1f, 1f), new Vector2(-1f, 1f), }); DumpNdc("wide region (near-side)", wideRegion); // Reciprocal: narrow portal, homogeneous, clipped against the wide region. var narrowClip = PortalProjection.ProjectToClip(narrow, Matrix4x4.Identity, vp); DumpClip("narrow subject (reciprocal)", narrowClip); var tightened = PortalProjection.ClipToRegion(narrowClip, wideRegion); DumpNdc("tightened = narrow ∩ wide", tightened); // Now the FULL Build on the same synthetic pair (mirrors // Build_AppliesReciprocalOtherPortalClip) with output dumps. var a = new LoadedCell { CellId = 0x0001, WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity, Portals = new List { new CellPortalInfo(0x0002, 0, 0, 0) }, }; a.PortalPolygons.Add(wide); var b = new LoadedCell { CellId = 0x0002, WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity, Portals = new List { new CellPortalInfo(0x0001, 0, 0, 0) }, }; b.PortalPolygons.Add(narrow); var all = new Dictionary { [0x0001u] = a, [0x0002u] = b }; var f = PortalVisibilityBuilder.Build( a, Vector3.Zero, id => all.TryGetValue(id, out var c) ? c : null, vp); foreach (var kv in f.CellViews) foreach (var p in kv.Value.Polygons) { string s = ""; foreach (var v in p.Vertices) s += System.FormattableString.Invariant($" ({v.X:F4},{v.Y:F4})"); _out.WriteLine($" Build view 0x{kv.Key:X4} [{p.Vertices.Length}]:{s}"); } } private void DumpClip(string label, Vector4[] verts) { string s = ""; foreach (var v in verts) s += System.FormattableString.Invariant($" ({v.X:F4},{v.Y:F4},{v.Z:F4},w={v.W:F4})"); _out.WriteLine($" {label} [{verts.Length}]:{s}"); } private void DumpNdc(string label, Vector2[] verts) { string s = ""; foreach (var v in verts) s += System.FormattableString.Invariant($" ({v.X:F7},{v.Y:F7})"); _out.WriteLine($" {label} [{verts.Length}]:{s}"); } /// /// Diagnostic (no assertions yet): sweep the eye along the corner-press path — /// from inside the player's room (0172), across the doorway threshold (0173), /// into the neighbour room (0171) — looking back at the player throughout, and /// print the flood result per step. The defect signature: the PLAYER's room /// (0172) absent from the flood (or its view region empty) while the root is /// 0171/0173 — the screen area where the player stands then shows background. /// [Fact] public void Diagnostic_CornerPress_FloodAcrossDoorwayPlane() { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cells = LoadBuilding(dats); Func lookup = id => cells.TryGetValue(id, out var c) ? c : null; // The captured corner scenario: player parked in 0172, eye orbiting near the // doorway plane at head height (z=96.25), looking at the pivot. var player = new Vector3(159.936676f, 7.701012f, 94.000000f); var pivot = player + new Vector3(0f, 0f, 1.5f); _out.WriteLine("step | eyeX (worldX | dPlane0171) | root | flood | 0171/0173/0172 polys | outside"); for (int i = 0; i <= 60; i++) { // Sweep world-x 158.43 (inside 0172) -> 157.23 (inside 0171), 2 cm steps, // crossing the 0172 plane (~158.03) and the 0171 plane (~157.83). float ex = 158.43f - i * 0.02f; var eye = new Vector3(ex, 7.912722f, 96.248833f); uint root = RootCellFor(ex); var frame = PortalVisibilityBuilder.Build( cells[root], eye, lookup, ViewProjFor(eye, pivot)); string flood = ""; foreach (uint id in frame.OrderedVisibleCells) flood += $" {id & 0xFFFFu:X4}"; string PolyInfo(uint low) { uint id = Landblock | low; if (!frame.CellViews.TryGetValue(id, out var view)) return "-"; int n = view.Polygons.Count; float area = 0f; foreach (var p in view.Polygons) area += MathF.Max(0f, p.MaxX - p.MinX) * MathF.Max(0f, p.MaxY - p.MinY); return System.FormattableString.Invariant($"{n}:{area:F3}"); } string p71 = PolyInfo(0x0171u); string p73 = PolyInfo(0x0173u); string p72 = PolyInfo(0x0172u); _out.WriteLine(System.FormattableString.Invariant( $"{i,3} | {ex:F2} ({ex - Plane0171X,6:F3}) | {root & 0xFFFFu:X4} |{flood} | {p71} / {p73} / {p72} | out={frame.OutsideView.Polygons.Count}")); } } }