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; using DatLandBlockInfo = DatReaderWriter.DBObjs.LandBlockInfo; namespace AcDream.App.Tests.Rendering; /// /// #120 reproduction (2026-06-11): the armed tripwire self-attributed on the /// re-gate launch (regate-118-119-launch.log) — a pure TWO-CELL reciprocal /// ping-pong, 64 laps each: `chain root=0xA9B4015C eye=(109.995,37.158,96.249) /// cells: 0xA9B40162x64 0xA9B4015Cx64` and `root=0xA9B3010F /// eye=(175.771,-107.310,118.814) cells: 0xA9B30103x64 0xA9B3010Fx64`. /// /// Mechanism: with the eye within PortalSideEpsilon (±1 cm — the T2-refuted- /// to-tighten root-lag tolerance; retail's is 0.0002) of the portal plane, /// the in-plane case counts as interior for BOTH cells, so views flow /// A→B→A…; each lap re-clips through two near-edge-on apertures whose /// intersection numerics wobble by more than CellView's 1e-3 dedup grid → /// every lap keys as a NEW polygon → 128-deep in-place recursion. The prior /// convergence sweeps (CornerFloodReplayTests) could not reproduce because /// they only load the corner building 0x016F-0x0175 — both firing pairs are /// OUTSIDE that set. /// /// The fix class is the handoff's own prediction ("dedup admitting /// near-duplicates per lap"): a round-trip re-emission is, in exact math, a /// SUBSET of the region that originated it — CellView.Add must reject /// contained polygons, which makes union growth strictly area-increasing and /// the flood convergent regardless of clip-numerics drift. /// public class Issue120ReciprocalPingPongTests { private readonly ITestOutputHelper _out; public Issue120ReciprocalPingPongTests(ITestOutputHelper output) => _out = output; internal static Dictionary LoadAllInteriorCells(DatCollection dats, uint landblock) { var lbi = dats.Get(landblock | 0xFFFEu); Assert.NotNull(lbi); var cells = new Dictionary(); for (uint low = 0x0100u; low < 0x0100u + lbi!.NumCells; low++) { uint id = landblock | low; try { cells[id] = CornerFloodReplayTests.LoadCell(dats, id); } catch (InvalidOperationException) { /* sparse cell ids — skip */ } } return cells; } private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 lookAt) { var view = Matrix4x4.CreateLookAt(eye, lookAt, Vector3.UnitZ); var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 0.1f, 5000f); return view * proj; } public static readonly TheoryData CapturedSites = new() { // landblock, cellA (root at firing), cellB, eye x, y, z — straight from the chain dumps { 0xA9B40000u, 0x015Cu, 0x0162u, 109.995f, 37.158f, 96.249f }, { 0xA9B30000u, 0x010Fu, 0x0103u, 175.771f, -107.310f, 118.814f }, }; /// /// The captured firing sites, swept over orientations (the dump has no view /// matrix) and over both cells as root. Invariants: the tripwire stays 0 /// AND no CellView accumulates a pathological polygon pile (the duplicate /// build-up is the defect even below the depth-128 failsafe). /// [Theory] [MemberData(nameof(CapturedSites))] public void CapturedPingPongSites_Converge(uint landblock, uint lowA, uint lowB, float ex, float ey, float ez) { var datDir = CornerFloodReplayTests.ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cells = LoadAllInteriorCells(dats, landblock); Func lookup = id => cells.TryGetValue(id, out var c) ? c : null; Assert.True(cells.ContainsKey(landblock | lowA), $"cell 0x{landblock | lowA:X8} not loaded"); Assert.True(cells.ContainsKey(landblock | lowB), $"cell 0x{landblock | lowB:X8} not loaded"); var eye = new Vector3(ex, ey, ez); var roots = new[] { cells[landblock | lowA], cells[landblock | lowB] }; PortalVisibilityBuilder.ConvergenceTripwireCount = 0; var failures = new List(); int builds = 0, maxPolys = 0; foreach (var root in roots) { for (int yaw = 0; yaw < 8; yaw++) { float a = yaw * MathF.PI / 4f; foreach (float pitch in new[] { -0.4f, 0f, 0.4f }) { var dir = Vector3.Normalize(new Vector3(MathF.Cos(a), MathF.Sin(a), pitch)); int before = PortalVisibilityBuilder.ConvergenceTripwireCount; var frame = PortalVisibilityBuilder.Build(root, eye, lookup, ViewProjFor(eye, eye + dir * 3f)); builds++; int after = PortalVisibilityBuilder.ConvergenceTripwireCount; int polys = frame.CellViews.Count == 0 ? 0 : frame.CellViews.Values.Max(v => v.Polygons.Count); if (polys > maxPolys) maxPolys = polys; if (after != before || polys > 32) failures.Add(FormattableString.Invariant( $"root=0x{root.CellId:X8} yaw={yaw} pitch={pitch} tripwire={after - before} maxCellPolys={polys}")); } } } _out.WriteLine($"builds={builds} maxCellPolys={maxPolys} failures={failures.Count}"); foreach (var f in failures) _out.WriteLine($" {f}"); Assert.True(failures.Count == 0, $"{failures.Count}/{builds} builds broke convergence at the captured #120 site (see output)"); } /// /// The geometric trigger, driven directly: eye swept through the /// both-sides-pass window (±PortalSideEpsilon = ±1 cm) of the reciprocal /// portal plane between the two captured cells, looking across and along /// the aperture. Same convergence invariants. /// [Theory] [MemberData(nameof(CapturedSites))] public void PortalPlaneWindow_BothSidesPass_Converges(uint landblock, uint lowA, uint lowB, float ex, float ey, float ez) { _ = ex; _ = ey; _ = ez; // geometric variant derives its own eyes var datDir = CornerFloodReplayTests.ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cells = LoadAllInteriorCells(dats, landblock); Func lookup = id => cells.TryGetValue(id, out var c) ? c : null; var cellA = cells[landblock | lowA]; uint cellBId = landblock | lowB; // the portal from A to B (the ping-pong pair) int portalIdx = -1; for (int i = 0; i < cellA.Portals.Count; i++) if (cellA.Portals[i].OtherCellId == (ushort)lowB) { portalIdx = i; break; } Assert.True(portalIdx >= 0, $"no portal 0x{lowA:X4}->0x{lowB:X4}"); var poly = cellA.PortalPolygons[portalIdx]; Assert.True(poly is { Length: >= 3 }, "portal polygon degenerate"); var centroidLocal = Vector3.Zero; foreach (var v in poly!) centroidLocal += v; centroidLocal /= poly.Length; var centroidWorld = Vector3.Transform(centroidLocal, cellA.WorldTransform); var plane = cellA.ClipPlanes[portalIdx]; var normalWorld = Vector3.Normalize(Vector3.TransformNormal(plane.Normal, cellA.WorldTransform)); PortalVisibilityBuilder.ConvergenceTripwireCount = 0; var failures = new List(); int builds = 0, maxPolys = 0; foreach (float off in new[] { -0.009f, -0.004f, 0f, 0.004f, 0.009f }) { var eye = centroidWorld + normalWorld * off; foreach (var root in new[] { cellA, cells[cellBId] }) { for (int yaw = 0; yaw < 8; yaw++) { float a = yaw * MathF.PI / 4f; var dir = Vector3.Normalize(new Vector3(MathF.Cos(a), MathF.Sin(a), 0.1f)); int before = PortalVisibilityBuilder.ConvergenceTripwireCount; var frame = PortalVisibilityBuilder.Build(root, eye, lookup, ViewProjFor(eye, eye + dir * 3f)); builds++; int after = PortalVisibilityBuilder.ConvergenceTripwireCount; int polys = frame.CellViews.Count == 0 ? 0 : frame.CellViews.Values.Max(v => v.Polygons.Count); if (polys > maxPolys) maxPolys = polys; if (after != before || polys > 32) failures.Add(FormattableString.Invariant( $"off={off:F3} root=0x{root.CellId:X8} yaw={yaw} tripwire={after - before} maxCellPolys={polys}")); } } } _out.WriteLine($"builds={builds} maxCellPolys={maxPolys} failures={failures.Count}"); foreach (var f in failures) _out.WriteLine($" {f}"); Assert.True(failures.Count == 0, $"{failures.Count}/{builds} builds broke convergence in the ±ε portal-plane window (see output)"); } }