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 <noreply@anthropic.com>
This commit is contained in:
parent
faba9be635
commit
e5cdd2364e
3 changed files with 207 additions and 1 deletions
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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<WalkWorldDatAdapter.BuildingEntry> 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<uint>();
|
||||
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<int>();
|
||||
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<uint>(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<int> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[Trait("Lane", "InstalledDat")]
|
||||
public sealed class WalkLookInGateSweepTests
|
||||
{
|
||||
private sealed class Recorder : IWalkEventSink
|
||||
{
|
||||
public readonly List<WalkEvent> 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<WalkOracleFrame> 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<uint>())
|
||||
: e.Kind == WalkOracleEventKind.DrawCells
|
||||
? ("DC", 0u, e.Cells.ToArray())
|
||||
: ("", 0u, Array.Empty<uint>())));
|
||||
|
||||
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<uint>())
|
||||
: e.Kind == WalkEventKind.DrawCells
|
||||
? ("DC", 0u, e.Cells.ToArray())
|
||||
: ("", 0u, Array.Empty<uint>())));
|
||||
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<string>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.</summary>
|
||||
public static bool FlipGfxPolygonPlanes;
|
||||
|
||||
/// <summary>Adjudication toggle: the BuildingPortal side-flag decode.
|
||||
/// 0 = (Flags & 0x2), 1 = inverted 0x2, 2 = (Flags & 0x1),
|
||||
/// 3 = inverted 0x1.</summary>
|
||||
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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue