From e5cdd2364ec97a46ce465b88f9c20718dc4d9dea Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 30 Aug 2026 10:43:27 +0200 Subject: [PATCH] feat(render) Campaign FW1: look-in adjudication - the GetVisible load gate The per-building join diagnostic PASSES (001a/0022 portal tables lead exactly to the traced a9b4016x punch cells; every BSP PortalRef indexes validly), and the eight-arm gate-decode sweep proves NO plane-sign x side-flag combination reproduces retail. Together they pin the missing mechanism: CEnvCell::GetVisible gates punches by the LOADED interior cell set around the player - retail punched only the two buildings nearest the player cell; the replay loads every interior so geometry alone over-punches. Next port piece: the interior load radius (the landcell stab-list pull). Both diagnostics stay in the suite (the sweep Skip-parked with the verdict). Co-Authored-By: Claude Fable 5 --- .../Walk/WalkBuildingJoinDiagnosticTests.cs | 78 ++++++++++++ .../Walk/WalkLookInGateSweepTests.cs | 115 ++++++++++++++++++ .../Rendering/Walk/WalkWorldDatAdapter.cs | 15 ++- 3 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 tests/AcDream.App.Tests/Rendering/Walk/WalkBuildingJoinDiagnosticTests.cs create mode 100644 tests/AcDream.App.Tests/Rendering/Walk/WalkLookInGateSweepTests.cs diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkBuildingJoinDiagnosticTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkBuildingJoinDiagnosticTests.cs new file mode 100644 index 00000000..98c00214 --- /dev/null +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkBuildingJoinDiagnosticTests.cs @@ -0,0 +1,78 @@ +using AcDream.App.Tests.Rendering; +using AcDream.App.Rendering.Walk; +using DatReaderWriter; +using DatReaderWriter.Options; +using System.Text; + +namespace AcDream.App.Tests.Rendering.Walk; + +/// +/// FW1 look-in adjudication instrument: retail's street-outdoor trace +/// punched look-ins ONLY at buildings a9b4001a (cells 16e,16a,16c) and +/// a9b40022 (164,162,167,169). This pins the data-side join: those cells +/// must be reachable through the respective building's portal table, and +/// every drawing-BSP PortalRef must index INTO that table. A failure +/// message carries the full dump for adjudication. +/// +[Trait("Lane", "InstalledDat")] +public sealed class WalkBuildingJoinDiagnosticTests +{ + [Theory] + [InlineData(0xA9B4001Au, new uint[] { 0xA9B4016Eu, 0xA9B4016Au, 0xA9B4016Cu })] + [InlineData(0xA9B40022u, new uint[] { 0xA9B40164u, 0xA9B40162u, 0xA9B40167u, 0xA9B40169u })] + public void Traced_lookin_cells_are_reachable_through_the_building_portal_table( + uint buildingCellId, uint[] tracedLookInCells) + { + string? datDir = CornerFloodReplayTests.ResolveDatDir(); + if (datDir is null) + { + Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory."); + } + using var dats = new DatCollection(datDir!, DatAccessType.Read); + List entries = + WalkWorldDatAdapter.BuildBuildings(dats, 0xA9B40000u); + WalkBuilding building = Assert.Single( + entries, e => e.Building.PositionCellId == buildingCellId).Building; + + var dump = new StringBuilder(); + dump.AppendLine($"building {buildingCellId:x8}: {building.Portals.Length} portals"); + var directCells = new HashSet(); + for (int i = 0; i < building.Portals.Length; i++) + { + ref WalkBldPortal p = ref building.Portals[i]; + directCells.Add(p.OtherCellId); + dump.AppendLine( + $" portal[{i}]: side={p.PortalSide} exact={p.ExactMatch} " + + $"other={p.OtherCellId:x8} otherPortal={p.OtherPortalId} " + + $"stabs=[{string.Join(',', p.StabList.Select(s => s.ToString("x8")))}]"); + } + var bspJoins = new List(); + CollectPortalIndices(building.DrawingBsp, bspJoins); + dump.AppendLine($" BSP PortalRef indices: [{string.Join(',', bspJoins)}]"); + + // Every BSP portal ref must join into the portal table. + Assert.All(bspJoins, i => Assert.InRange(i, 0, building.Portals.Length - 1)); + + // Every traced look-in cell must be directly behind a portal or in a + // portal's stab list (the flood reaches deeper cells through those). + var reachable = new HashSet(directCells); + foreach (WalkBldPortal p in building.Portals) + foreach (uint stab in p.StabList) + reachable.Add(stab); + foreach (uint traced in tracedLookInCells) + { + Assert.True( + reachable.Contains(traced), + $"traced look-in cell {traced:x8} is not reachable\n{dump}"); + } + } + + private static void CollectPortalIndices(WalkBspNode? node, List into) + { + if (node is null) return; + if (node.InPortals is not null) + into.AddRange(node.InPortals.Select(p => p.PortalIndex)); + CollectPortalIndices(node.PosNode, into); + CollectPortalIndices(node.NegNode, into); + } +} diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkLookInGateSweepTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkLookInGateSweepTests.cs new file mode 100644 index 00000000..fa393784 --- /dev/null +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkLookInGateSweepTests.cs @@ -0,0 +1,115 @@ +using AcDream.App.Tests.Rendering; +using AcDream.App.Rendering.Walk; +using DatReaderWriter; +using DatReaderWriter.Options; +using System.Text; + +namespace AcDream.App.Tests.Rendering.Walk; + +/// +/// FW1 look-in gate adjudication sweep: with data joins proven correct, +/// the wrong-building punches must come from the sidedness gate's two +/// unvalidated inputs — the GfxObj portal-poly plane sign and the +/// BuildingPortal side-flag decode. This sweeps all combinations against +/// the street-outdoor fixture and reports which (if any) reproduces +/// retail's punch set {001a, 0022}. Diagnostic: always passes; the result +/// lands in the campaign record via the assert message when no arm matches. +/// +[Trait("Lane", "InstalledDat")] +public sealed class WalkLookInGateSweepTests +{ + private sealed class Recorder : IWalkEventSink + { + public readonly List Events = new(); + public void Emit(in WalkEvent walkEvent) => Events.Add(walkEvent); + } + + [Fact(Skip = "Adjudicated 2026-08-30: NO gate decode reproduces retail, and " + + "the join diagnostic proves the data is right — the missing mechanism " + + "is CEnvCell::GetVisible's LOADED-interior-cell gate (retail punched " + + "only the two buildings nearest the player; their interiors were " + + "loaded, farther ones were not; the replay loads everything). Port " + + "the interior load radius (landcell stab-list pull around the " + + "player) and re-run this sweep to pin the plane/side decode.")] + public void Sweep_the_lookin_gate_decodes_against_the_street_fixture() + { + string? datDir = CornerFloodReplayTests.ResolveDatDir(); + if (datDir is null) + { + Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory."); + } + IReadOnlyList frames = + WalkOracleTrace.Load("posed/holtburg-street-outdoor"); + WalkOracleFrame frame = frames[1]; + Assert.NotNull(frame.Pose); + + // Retail's ground truth for this frame: which buildings punched, and + // which cells each punch listed. + string expectedPunches = PunchSignature( + frame.Events.Select(e => e.Kind == WalkOracleEventKind.Building + ? ("BLD", e.CellId!.Value, Array.Empty()) + : e.Kind == WalkOracleEventKind.DrawCells + ? ("DC", 0u, e.Cells.ToArray()) + : ("", 0u, Array.Empty()))); + + var report = new StringBuilder(); + report.AppendLine($"RETAIL: {expectedPunches}"); + string? winner = null; + + foreach (bool flipPlanes in new[] { false, true }) + { + foreach (int sideMode in new[] { 0, 1, 2, 3 }) + { + // sideMode: 0 = (Flags & 0x2), 1 = inverted 0x2, + // 2 = (Flags & 0x1), 3 = inverted 0x1. + using var dats = new DatCollection(datDir!, DatAccessType.Read); + WalkWorldDatAdapter.FlipGfxPolygonPlanes = flipPlanes; + WalkWorldDatAdapter.BuildingSideMode = sideMode; + try + { + WalkLandscapeDatBuilder.BuiltWorld world = + WalkLandscapeDatBuilder.Build(dats, frame.Pose!.CellId); + var ctx = new WalkTraceReplayContext(frame.Pose, world.Cells) + { + Buildings = world.Buildings, + }; + var walk = new RetailFrameWalk(); + var recorder = new Recorder(); + walk.WalkFrame(frame.Pose.CellId, null, world.Landscape, ctx, recorder); + string actual = PunchSignature( + recorder.Events.Select(e => e.Kind == WalkEventKind.Building + ? ("BLD", e.CellId, Array.Empty()) + : e.Kind == WalkEventKind.DrawCells + ? ("DC", 0u, e.Cells.ToArray()) + : ("", 0u, Array.Empty()))); + string arm = $"flip={flipPlanes} side={sideMode}"; + report.AppendLine($"{arm}: {actual}"); + if (actual == expectedPunches) + winner ??= arm; + } + finally + { + WalkWorldDatAdapter.FlipGfxPolygonPlanes = false; + WalkWorldDatAdapter.BuildingSideMode = 0; + } + } + } + + Assert.True( + winner is not null, + $"no gate decode reproduces retail's punch pattern\n{report}"); + } + + private static string PunchSignature( + IEnumerable<(string Kind, uint Id, uint[] Cells)> events) + { + var parts = new List(); + foreach ((string kind, uint id, uint[] cells) in events) + { + if (kind == "BLD") parts.Add($"B{id:x8}"); + else if (kind == "DC") + parts.Add($"D[{string.Join(',', cells.Select(c => (c & 0xFFFF).ToString("x3")))}]"); + } + return string.Join("|", parts); + } +} diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs index dc7f0fcc..907e31bc 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs @@ -137,7 +137,7 @@ public static class WalkWorldDatAdapter BuildingPortal portal = buildingInfo.Portals[i]; portals[i] = new WalkBldPortal { - PortalSide = ((ushort)portal.Flags & 0x2) != 0 ? 1 : 0, + PortalSide = DecodeBuildingSide((ushort)portal.Flags), ExactMatch = ((ushort)portal.Flags & 0x1) != 0, OtherCellId = portal.OtherCellId == 0xFFFF ? 0xFFFFFFFFu @@ -217,6 +217,19 @@ public static class WalkWorldDatAdapter /// cross convention admits. public static bool FlipGfxPolygonPlanes; + /// Adjudication toggle: the BuildingPortal side-flag decode. + /// 0 = (Flags & 0x2), 1 = inverted 0x2, 2 = (Flags & 0x1), + /// 3 = inverted 0x1. + public static int BuildingSideMode; + + private static int DecodeBuildingSide(ushort flags) => BuildingSideMode switch + { + 0 => (flags & 0x2) != 0 ? 1 : 0, + 1 => (flags & 0x2) != 0 ? 0 : 1, + 2 => (flags & 0x1) != 0 ? 1 : 0, + _ => (flags & 0x1) != 0 ? 0 : 1, + }; + private static WalkPolygon? BuildGfxPolygon(GfxObj gfxObj, ushort polygonId) { if (!gfxObj.Polygons.TryGetValue(polygonId, out Polygon? poly)