using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using AcDream.App.Rendering; using DatReaderWriter; using DatReaderWriter.Options; using Xunit; using Xunit.Abstractions; namespace AcDream.App.Tests.Rendering; /// /// #176 (purple flashing at corridor seams, camera-angle dependent) + #177 /// (stairs pop in/out) — headless portal-flood replay in the Facility Hub /// landblock 0x8A02. The unified hypothesis after the dat + draw-path reads: /// both artifacts are FLOOD ADMISSION instability (a cell dropping out of /// PortalVisibilityBuilder's admitted set paints the fog-purple clear color /// where its geometry was; stair cells failing admission = the pop). /// /// Production-matched inputs: Build(root, eye, lookup, viewProj, /// buildingMembership: null, drawLiftZ: ShellDrawLiftZ) — the drawLiftZ /// mirrors RetailPViewRenderer.DrawInside. /// /// Scenarios: /// A. #177 approach — stand in corridor 0x0178, look +X at the stair ramp /// (0x0182) and the lower cell (0x0183): are they admitted? /// B. #177 descent — eye path down the ramp crossing into 0x0183: does /// 0x0182 (the ramp geometry's owner) drop near the transit? /// C. #176 gaze sweep — eye parked in 0x016E near the 0x017A seam, yaw /// sweep at several pitches: any cell admitted at angle k, gone at k+1, /// back at k+2 (the bistability signature)? /// D. #176 walk — eye tracks down the corridor across two seams, gaze /// locked +X: per-step admitted-set diffs (drop-for-one-step churn). /// public class Issue176177FacilityHubFloodReplayTests { private const uint FacilityHub = 0x8A020000u; private readonly ITestOutputHelper _out; public Issue176177FacilityHubFloodReplayTests(ITestOutputHelper output) => _out = output; private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 gazeDir) { var view = Matrix4x4.CreateLookAt(eye, eye + gazeDir, Vector3.UnitZ); var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 0.1f, 5000f); return view * proj; } private static List Flood( Dictionary cells, uint rootId, Vector3 eye, Vector3 gazeDir) { Func lookup = id => cells.TryGetValue(id, out var c) ? c : null; var frame = PortalVisibilityBuilder.Build( cells[rootId], eye, lookup, ViewProjFor(eye, gazeDir), buildingMembership: null, drawLiftZ: PortalVisibilityBuilder.ShellDrawLiftZ); var result = new List(frame.OrderedVisibleCells); result.Sort(); return result; } private static string CellSetString(IEnumerable ids) => string.Join(" ", ids.Select(id => $"{id & 0xFFFFu:X4}")); [Fact] public void ScenarioA_StairApproach_AdmissionsFromCorridor() { var datDir = CornerFloodReplayTests.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); Assert.True(cells.ContainsKey(FacilityHub | 0x0178u), "0x0178 not loaded"); // 0x0178 spans x<=95 at z −6..−3 (floor −6); the ramp cell 0x0182 runs // x 95→98.33 descending; 0x0183 continues at z −9 beyond x=98.33. // Eye at standing height (~1.7 m above the −6 floor), approaching the // stair portal at x=95, gazing +X with a slight downward pitch (the // natural look at a descending stair). foreach (float eyeX in new[] { 88f, 90f, 92f, 94f, 94.9f }) { var eye = new Vector3(eyeX, -40f, -4.3f); foreach (var (gaze, label) in new (Vector3, string)[] { (new Vector3(1f, 0f, 0f), "level"), (Vector3.Normalize(new Vector3(1f, 0f, -0.35f)), "pitch-19"), (Vector3.Normalize(new Vector3(1f, 0f, -0.7f)), "pitch-35"), }) { var visible = Flood(cells, FacilityHub | 0x0178u, eye, gaze); bool ramp = visible.Contains(FacilityHub | 0x0182u); bool lower = visible.Contains(FacilityHub | 0x0183u); _out.WriteLine($"eyeX={eyeX,5:F1} gaze={label,-8} flood={visible.Count,2} " + $"ramp0182={(ramp ? "Y" : "MISSING")} lower0183={(lower ? "Y" : "MISSING")} " + $"[{CellSetString(visible)}]"); } } } [Fact] public void ScenarioB_StairDescent_RampCellRetention() { var datDir = CornerFloodReplayTests.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); // Descend the ramp: floor z goes −6 (x=95) → −9 (x=98.33), then flat. // Eye rides ~1.7 m above the local floor. Root = the cell containing // the eye by x-range (0x0178 x<95, ramp 0x0182 95..98.33, 0x0183 after). // Gaze: forward and slightly down (running down stairs). var gazeDir = Vector3.Normalize(new Vector3(1f, 0f, -0.4f)); List? prev = null; for (float x = 94.0f; x <= 100.5f; x += 0.1f) { float floorZ = x < 95f ? -6f : x < 98.333f ? -6f - 3f * (x - 95f) / 3.333f : -9f; var eye = new Vector3(x, -40f, floorZ + 1.7f); uint rootId = x < 95f ? FacilityHub | 0x0178u : x < 98.333f ? FacilityHub | 0x0182u : FacilityHub | 0x0183u; var visible = Flood(cells, rootId, eye, gazeDir); bool ramp = visible.Contains(FacilityHub | 0x0182u); bool upper = visible.Contains(FacilityHub | 0x0178u); string diff = ""; if (prev is not null) { var removed = prev.Except(visible).ToList(); var added = visible.Except(prev).ToList(); if (removed.Count > 0) diff += $" REMOVED=[{CellSetString(removed)}]"; if (added.Count > 0) diff += $" added=[{CellSetString(added)}]"; } _out.WriteLine($"x={x,6:F1} root={rootId & 0xFFFFu:X4} eyeZ={eye.Z,6:F2} flood={visible.Count,2} " + $"ramp0182={(ramp ? "Y" : "MISSING")} up0178={(upper ? "Y" : "-")}{diff}"); prev = visible; } } [Fact] public void ScenarioC_CorridorSeamGazeSweep_Bistability() { var datDir = CornerFloodReplayTests.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); // Parked eye in 0x016E near the 0x017A seam (x=85), standing height. var eye = new Vector3(83.5f, -40f, -4.3f); uint rootId = FacilityHub | 0x016Eu; int churnEvents = 0; foreach (float pitchZ in new[] { 0f, -0.35f, -0.75f, -1.2f }) { List? prevSet = null; float prevYaw = 0f; var perYawSets = new List<(float yaw, List set)>(); for (float yawDeg = -180f; yawDeg <= 180f; yawDeg += 2f) { float rad = yawDeg * MathF.PI / 180f; var gaze = Vector3.Normalize(new Vector3(MathF.Cos(rad), MathF.Sin(rad), pitchZ)); var visible = Flood(cells, rootId, eye, gaze); perYawSets.Add((yawDeg, visible)); if (prevSet is not null) { var removed = prevSet.Except(visible).ToList(); var added = visible.Except(prevSet).ToList(); if (removed.Count > 0 || added.Count > 0) { _out.WriteLine($"pitch={pitchZ,5:F2} yaw {prevYaw,6:F0}->{yawDeg,6:F0}: " + $"flood {prevSet.Count}->{visible.Count}" + (removed.Count > 0 ? $" REMOVED=[{CellSetString(removed)}]" : "") + (added.Count > 0 ? $" added=[{CellSetString(added)}]" : "")); } } prevSet = visible; prevYaw = yawDeg; } // Bistability: a cell present at yaw k, absent at k+1, present at k+2. for (int i = 2; i < perYawSets.Count; i++) { var flicker = perYawSets[i - 2].set .Intersect(perYawSets[i].set) .Except(perYawSets[i - 1].set) .ToList(); if (flicker.Count > 0) { churnEvents++; _out.WriteLine($">>> BISTABLE pitch={pitchZ:F2} yaw={perYawSets[i - 1].yaw:F0}: " + $"cells [{CellSetString(flicker)}] dropped for ONE 2-degree step"); } } } _out.WriteLine($"bistable one-step drop events: {churnEvents}"); } /// /// THE production lag-window scenario (from launch-137-gate2.log /// [cell-transit] lines): membership transits fire 0.1–0.6 m PAST the /// portal plane in the travel direction (016E→017A at x=85.33–85.47 vs /// the plane at x=85.00; 0182→0183 at 98.56–98.64 vs 98.33). The render /// root (viewer cell, same membership machinery) therefore holds the OLD /// cell while the camera eye is already beyond the boundary portal's /// plane. This scenario reproduces exactly that window: root=old cell, /// eye stepped across and past the plane, gaze forward. If the forward /// chain (the next corridor cells) drops inside the window, that is #176 /// (purple = fog clear color where the forward cells' geometry was) and /// #177(a)/(c) at the stair transit. /// [Theory] [InlineData(0x016Eu, 0x017Au, 85.00f, -4.3f)] // corridor seam, plane x=85 [InlineData(0x0182u, 0x0183u, 98.333f, -7.3f)] // stair-bottom transit, plane x=98.33 public void ScenarioE_RootLagWindow_ForwardChainRetention( uint rootLow, uint forwardLow, float planeX, float eyeZ) { var datDir = CornerFloodReplayTests.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); uint rootId = FacilityHub | rootLow; uint forwardId = FacilityHub | forwardLow; var gazeDir = Vector3.Normalize(new Vector3(1f, 0f, -0.3f)); _out.WriteLine($"root=0x{rootLow:X4} forward=0x{forwardLow:X4} plane x={planeX:F2} " + "(eye sweeps across; root HELD at the old cell = the production lag window)"); foreach (float dx in new[] { -0.30f, -0.10f, -0.02f, 0.00f, 0.02f, 0.05f, 0.10f, 0.20f, 0.30f, 0.45f, 0.60f }) { var eye = new Vector3(planeX + dx, -40f, eyeZ); var visible = Flood(cells, rootId, eye, gazeDir); bool fwd = visible.Contains(forwardId); _out.WriteLine($" eyeX=plane{(dx >= 0 ? "+" : "")}{dx:F2} flood={visible.Count,2} " + $"forward={(fwd ? "Y" : ">>> DROPPED <<<")} [{CellSetString(visible)}]"); } } [Fact] public void ScenarioD_CorridorWalk_PerStepChurn() { var datDir = CornerFloodReplayTests.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); // Walk 0x0165 → 0x016E → 0x017A along y=−40 (seams at x=75 and x=85), // gaze locked +X, slight downward pitch (the running view). Root flips // by x-range at the seams (the camera transits the same portals). var gazeDir = Vector3.Normalize(new Vector3(1f, 0f, -0.3f)); List? prev = null; int churn = 0; for (float x = 71f; x <= 89f; x += 0.05f) { uint rootId = x < 75f ? FacilityHub | 0x0165u : x < 85f ? FacilityHub | 0x016Eu : FacilityHub | 0x017Au; if (!cells.ContainsKey(rootId)) continue; var eye = new Vector3(x, -40f, -4.3f); var visible = Flood(cells, rootId, eye, gazeDir); if (prev is not null) { var removed = prev.Except(visible).ToList(); var added = visible.Except(prev).ToList(); if (removed.Count > 0 || added.Count > 0) { churn++; _out.WriteLine($"x={x,6:F2} root={rootId & 0xFFFFu:X4} flood={visible.Count,2}" + (removed.Count > 0 ? $" REMOVED=[{CellSetString(removed)}]" : "") + (added.Count > 0 ? $" added=[{CellSetString(added)}]" : "")); } } prev = visible; } _out.WriteLine($"admitted-set change events over the walk: {churn}"); } }