acdream/tests/AcDream.App.Tests/Rendering/Issue120ReciprocalPingPongTests.cs
Erik 899145e1d7 #119-residual: tower-ascent harness pins the roof-lip flood gap; barrel claim RETRACTED (user axiom: not in retail)
User verdict on the post-#120 build: "Barrel is gone and more stairs
exist" - the #120 fix partially cured the tower, and the earlier
"legit dat barrels on the landings" claim is RETRACTED (USER AXIOM: the
barrel is NOT in the tower in retail; what the user saw was itself a
render artifact of the corrupted floods, and what the 0x020005D8 cell
statics actually render as is unverified - do not assume barrel).

Remaining tower bugs, both PINNED by TowerAscentReplayTests (the #118
exit-walk pattern, vertical - a helix ascent with the gaze locked ON
the staircase, so a cull has no gaze excuse):
- steps 195-201 (eye z 126.9-127.3, the roof-lip band between the main
  cell's ceiling at 126.8 and the roof aperture plane at ~127.2) resolve
  OUTDOOR and the per-building exterior flood admits NOTHING (flood=1 =
  the outdoor node alone): the eye is above every side aperture's useful
  view and ON/INSIDE the roof aperture's plane, so BuildFromExterior's
  seed side-test / in-plane reject refuses every exit portal. The tower
  interior never floods -> the staircase (a 0x0107 static) cone-culls
  while staying walkable (user symptom 1), and the roof-lip cell
  geometry flaps as the live eye bobs across the band's edges (user
  symptom 2). One mechanism, both symptoms.
- The pin is committed as a SKIPPED red test
  (TowerAscent_StaircaseStaysConeVisible_EveryStep; the skip reason
  carries the defect) so the suite stays green - un-skip with the fix.
- TowerAscent_RootDoesNotPingPong + the per-step diagnostic stay active.

Fix direction (oracle-first, next): determine which side diverges from
retail - (a) viewer-cell resolution (retail curr_cell may keep the eye
INTERIOR through the band: keep-curr above open-top cells / cell BSP
classifying the parapet bowl as inside 0x010A, where our resolution
demotes to outdoor), or (b) exterior seed admission (retail
ConstructView(CBldPortal) Sidedness with an in-plane eye). Grep the
named decomp for both before touching either layer.

Suites: App 238 + 1 skip (236+3 new, 1 pinned), Core 1419+2skip,
UI 420, Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 18:34:45 +02:00

195 lines
9.2 KiB
C#

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;
/// <summary>
/// #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.
/// </summary>
public class Issue120ReciprocalPingPongTests
{
private readonly ITestOutputHelper _out;
public Issue120ReciprocalPingPongTests(ITestOutputHelper output) => _out = output;
internal static Dictionary<uint, LoadedCell> LoadAllInteriorCells(DatCollection dats, uint landblock)
{
var lbi = dats.Get<DatLandBlockInfo>(landblock | 0xFFFEu);
Assert.NotNull(lbi);
var cells = new Dictionary<uint, LoadedCell>();
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<uint, uint, uint, float, float, float> 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 },
};
/// <summary>
/// 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).
/// </summary>
[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<uint, LoadedCell?> 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<string>();
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)");
}
/// <summary>
/// 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.
/// </summary>
[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<uint, LoadedCell?> 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<string>();
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)");
}
}