415 lines
19 KiB
C#
415 lines
19 KiB
C#
using AcDream.App.Rendering;
|
||
using AcDream.App.Tests.Rendering;
|
||
using AcDream.App.Rendering.Walk;
|
||
using AcDream.Content;
|
||
using DatReaderWriter;
|
||
using DatReaderWriter.DBObjs;
|
||
using DatReaderWriter.Options;
|
||
using System.Numerics;
|
||
|
||
namespace AcDream.App.Tests.Rendering.Walk;
|
||
|
||
/// <summary>
|
||
/// Campaign FW3.1's load-bearing deliverable: the ten pose-stamped oracle
|
||
/// fixtures (docs/research/2026-08-30-fw-walk-oracle/), replayed through the
|
||
/// PRODUCTION world-data builders
|
||
/// (<see cref="WalkCellFactory"/>/<see cref="WalkBuildingFactory"/>
|
||
/// /<see cref="WalkLandscapeAssembler"/> — the same code
|
||
/// <c>EnvCellLandblockBuildBuilder</c>/<c>LandblockBuildFactory</c> run at
|
||
/// real landblock-build time, consuming ONLY the legal
|
||
/// <see cref="IDatReaderWriter"/> seam), must reproduce the identical walk
|
||
/// output <see cref="WalkTraceConformanceTests"/> already proves against the
|
||
/// FW1 test adapter (<c>WalkWorldDatAdapter</c>/<c>WalkLandscapeDatBuilder</c>).
|
||
///
|
||
/// This class deliberately does NOT touch <see cref="WalkTraceConformanceTests"/>
|
||
/// (frozen — must stay green untouched) but reuses its driver/signature
|
||
/// helpers verbatim: <see cref="WalkTraceReplayContext"/>,
|
||
/// <see cref="WalkOracleTrace"/>, <see cref="WalkOraclePose"/>,
|
||
/// <see cref="WalkOracleFrame"/>. <see cref="WalkTraceReplayContext.Buildings"/>
|
||
/// is typed against the TEST adapter's <c>WalkWorldDatAdapter.BuildingEntry</c>
|
||
/// record — structurally identical to <see cref="WalkBuildingFactory.Entry"/>
|
||
/// (same three fields) — so <see cref="BuildProductionWorld"/> below adapts
|
||
/// one into the other rather than touching the shared context type.
|
||
///
|
||
/// <see cref="BuildProductionWorld"/> mirrors the FW1 harness's
|
||
/// <c>WalkLandscapeDatBuilder.Build</c> loop structure and ring math
|
||
/// EXACTLY (51×51 grid, <c>WalkLandscapeAssembler.MidRadius</c> = 25,
|
||
/// buildings/stab-cells only at full resolution), but every DATA-BUILDING
|
||
/// step below it — cells, buildings, the drawing BSP, z-slab — calls the
|
||
/// PRODUCTION functions. That is the whole conformance claim: the grid/ring
|
||
/// assembly is proven once (FW1's existing gate, via
|
||
/// <see cref="WalkLandscapeAssembler"/>'s own port of that same math); this
|
||
/// class proves the DATA those slots are filled with is identical to the
|
||
/// FW1 test adapter's.
|
||
/// </summary>
|
||
[Trait("Lane", "InstalledDat")]
|
||
public sealed class WalkProductionWorldConformanceTests
|
||
{
|
||
private sealed class Recorder : IWalkEventSink
|
||
{
|
||
public readonly List<WalkEvent> Events = new();
|
||
public Action<WalkEvent>? OnEmit { get; init; }
|
||
|
||
public void Emit(in WalkEvent walkEvent)
|
||
{
|
||
Events.Add(walkEvent);
|
||
OnEmit?.Invoke(walkEvent);
|
||
}
|
||
}
|
||
|
||
private static DatCollection OpenDats()
|
||
{
|
||
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
||
if (datDir is null)
|
||
{
|
||
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
|
||
}
|
||
return new DatCollection(datDir!, DatAccessType.Read);
|
||
}
|
||
|
||
/// <summary>Builds the walk's world data (landscape + interior cells +
|
||
/// buildings) for a camera pose through the PRODUCTION builders, in the
|
||
/// exact shape <see cref="WalkTraceReplayContext"/> expects. Mirrors
|
||
/// <c>WalkLandscapeDatBuilder.Build</c>'s grid loop; every cell/building
|
||
/// constructed inside it comes from <see cref="WalkCellFactory"/> /
|
||
/// <see cref="WalkBuildingFactory"/>.</summary>
|
||
private static (WalkLandscapeAssembler Assembler, Dictionary<uint, WalkCell> Cells,
|
||
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> Buildings)
|
||
BuildProductionWorld(IDatReaderWriter dats, uint cameraCellId, Vector3 cameraOrigin)
|
||
{
|
||
int cameraBlockX = (int)(cameraCellId >> 24);
|
||
int cameraBlockY = (int)((cameraCellId >> 16) & 0xFF);
|
||
var assembler = new WalkLandscapeAssembler();
|
||
var cells = new Dictionary<uint, WalkCell>();
|
||
var buildings = new Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry>();
|
||
Region region = (Region)dats.Get<Region>(0x13000000u)!;
|
||
float[] heightTable = region.LandDefs.LandHeightTable;
|
||
|
||
for (int gx = 0; gx < WalkLandscapeAssembler.GridWidth; gx++)
|
||
{
|
||
for (int gy = 0; gy < WalkLandscapeAssembler.GridWidth; gy++)
|
||
{
|
||
int blockX = cameraBlockX + gx - WalkLandscapeAssembler.MidRadius;
|
||
int blockY = cameraBlockY + gy - WalkLandscapeAssembler.MidRadius;
|
||
if (blockX < 0 || blockX > 0xFF || blockY < 0 || blockY > 0xFF)
|
||
continue;
|
||
uint landblockId = (uint)((blockX << 24) | (blockY << 16));
|
||
if (dats.Get<LandBlock>(landblockId | 0xFFFFu) is not LandBlock landBlock)
|
||
continue;
|
||
|
||
byte maxByte = 0, minByte = 255;
|
||
foreach (byte h in landBlock.Height)
|
||
{
|
||
if (h > maxByte) maxByte = h;
|
||
if (h < minByte) minByte = h;
|
||
}
|
||
float maxZ = heightTable[maxByte] + 200f;
|
||
float minZ = heightTable[minByte] - 1f;
|
||
|
||
var blockOffset = new Vector3(
|
||
(gx - WalkLandscapeAssembler.MidRadius) * WalkLandscape.BlockLength,
|
||
(gy - WalkLandscapeAssembler.MidRadius) * WalkLandscape.BlockLength,
|
||
0f);
|
||
|
||
var blockBuildings = new List<WalkBuildingFactory.Entry>();
|
||
if (WalkLandscapeAssembler.SideCellCountForRing(WalkLandscapeAssembler.RingOf(gx, gy)) == 8)
|
||
{
|
||
LandBlockInfo? info = dats.Get<LandBlockInfo>(landblockId | 0xFFFEu);
|
||
blockBuildings = WalkBuildingFactory.Build(dats, landblockId, info?.Buildings, blockOffset);
|
||
foreach (WalkBuildingFactory.Entry entry in blockBuildings)
|
||
{
|
||
buildings[entry.Building] = new WalkWorldDatAdapter.BuildingEntry(
|
||
entry.Building, entry.WorldTransform, entry.InverseWorldTransform);
|
||
|
||
// Retail's loaded-interior rule (CLandBlock::init_buildings
|
||
// @0052fd80 -> add_to_stablist -> grab_visible_cells): a
|
||
// full-res block loads exactly its buildings' portal stab
|
||
// cells, matching WalkLandscapeDatBuilder's harness scope.
|
||
foreach (WalkBldPortal portal in entry.Building.Portals)
|
||
{
|
||
if (portal.OtherCellId != 0xFFFFFFFFu && !cells.ContainsKey(portal.OtherCellId))
|
||
{
|
||
WalkCell? c = WalkCellFactory.BuildCell(dats, portal.OtherCellId, blockOffset);
|
||
if (c is not null) cells[c.CellId] = c;
|
||
}
|
||
foreach (uint stab in portal.StabList)
|
||
{
|
||
if (cells.ContainsKey(stab)) continue;
|
||
WalkCell? c = WalkCellFactory.BuildCell(dats, stab, blockOffset);
|
||
if (c is not null) cells[c.CellId] = c;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
assembler.PublishLandblock(landblockId, maxZ, minZ, blockBuildings);
|
||
}
|
||
}
|
||
assembler.SetViewer(cameraCellId, cameraOrigin);
|
||
return (assembler, cells, buildings);
|
||
}
|
||
|
||
[Fact]
|
||
public void Street_outdoor_first_frame_diff()
|
||
{
|
||
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load("posed/holtburg-street-outdoor");
|
||
Assert.NotEmpty(frames);
|
||
using DatCollection dats = OpenDats();
|
||
using var adapter = new DatCollectionAdapter(dats);
|
||
WalkOracleFrame frame = frames[1];
|
||
Assert.NotNull(frame.Pose);
|
||
(WalkLandscapeAssembler assembler, Dictionary<uint, WalkCell> cells,
|
||
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> buildings) =
|
||
BuildProductionWorld(adapter, frame.Pose!.CellId, frame.Pose.Origin);
|
||
var ctx = new WalkTraceReplayContext(frame.Pose, cells) { Buildings = buildings };
|
||
var walk = new RetailFrameWalk();
|
||
var recorder = new Recorder();
|
||
|
||
walk.WalkFrame(frame.Pose.CellId, null, assembler.Landscape, ctx, recorder);
|
||
|
||
string expected = WalkTraceReplayContext.Signature(frame);
|
||
string actual = WalkTraceReplayContext.Signature(recorder.Events);
|
||
Assert.True(
|
||
expected == actual,
|
||
$"production walk diverged from the FW1 test adapter\nEXPECTED: {expected}\nACTUAL: {actual}");
|
||
}
|
||
|
||
[Fact]
|
||
public void Doorway_still_first_frame_diff()
|
||
{
|
||
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load("posed/holtburg-doorway-still");
|
||
Assert.NotEmpty(frames);
|
||
using DatCollection dats = OpenDats();
|
||
using var adapter = new DatCollectionAdapter(dats);
|
||
WalkOracleFrame frame = frames[1];
|
||
Assert.NotNull(frame.Pose);
|
||
(WalkLandscapeAssembler assembler, Dictionary<uint, WalkCell> cells,
|
||
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> buildings) =
|
||
BuildProductionWorld(adapter, frame.Pose!.CellId, frame.Pose.Origin);
|
||
var ctx = new WalkTraceReplayContext(frame.Pose, cells) { Buildings = buildings };
|
||
WalkCell camera = Assert.Contains(frame.Pose.CellId, cells);
|
||
// Matches WalkTraceConformanceTests.Doorway_still_first_frame_diff:
|
||
// this capture ran under cdb load with Render::deg_mul depressed to
|
||
// the portless-arm threshold. Same environment pin, same reason.
|
||
var walk = new RetailFrameWalk { DegradeMultiplier = 0f };
|
||
var recorder = new Recorder();
|
||
|
||
walk.WalkFrame(frame.Pose.CellId, camera, assembler.Landscape, ctx, recorder);
|
||
|
||
string expected = WalkTraceReplayContext.Signature(frame);
|
||
string actual = WalkTraceReplayContext.Signature(recorder.Events);
|
||
Assert.True(
|
||
expected == actual,
|
||
$"production walk diverged from the FW1 test adapter\nEXPECTED: {expected}\nACTUAL: {actual}");
|
||
}
|
||
|
||
[Theory]
|
||
[InlineData("posed/terrace-center")]
|
||
[InlineData("posed/terrace-edge")]
|
||
[InlineData("posed/cathedral-arrival")]
|
||
public void Still_fixture_first_frame_reproduces_exactly(string fixture)
|
||
{
|
||
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load(fixture);
|
||
Assert.NotEmpty(frames);
|
||
using DatCollection dats = OpenDats();
|
||
using var adapter = new DatCollectionAdapter(dats);
|
||
WalkOracleFrame frame = frames[1];
|
||
Assert.NotNull(frame.Pose);
|
||
(WalkLandscapeAssembler assembler, Dictionary<uint, WalkCell> cells,
|
||
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> buildings) =
|
||
BuildProductionWorld(adapter, frame.Pose!.CellId, frame.Pose.Origin);
|
||
var ctx = new WalkTraceReplayContext(frame.Pose, cells) { Buildings = buildings };
|
||
WalkCell? camera = (frame.Pose.CellId & 0xFFFFu) >= 0x100
|
||
? Assert.Contains(frame.Pose.CellId, cells)
|
||
: null;
|
||
var walk = new RetailFrameWalk();
|
||
var recorder = new Recorder();
|
||
|
||
walk.WalkFrame(frame.Pose.CellId, camera, assembler.Landscape, ctx, recorder);
|
||
|
||
string expected = WalkTraceReplayContext.Signature(frame);
|
||
string actual = WalkTraceReplayContext.Signature(recorder.Events);
|
||
Assert.True(
|
||
expected == actual,
|
||
$"production walk diverged from the FW1 test adapter ({fixture})\nEXPECTED: {expected}\nACTUAL: {actual}");
|
||
}
|
||
|
||
[Theory]
|
||
[InlineData(0xF4180100u, 36.166267f, 79.828407f)]
|
||
[InlineData(0xF4180101u, 36.391270f, 72.167931f)]
|
||
public void Cathedral_transition_keeps_south_hall_admission_bounded_for_depth_occlusion(
|
||
uint cameraCellId,
|
||
float x,
|
||
float y)
|
||
{
|
||
// Owner's exact 2026-08-31 repro: the remote player and special NPC
|
||
// are parented in 0xF4180112, behind opaque cathedral walls. Retail
|
||
// hides them on BOTH sides of the 0x100 <-> 0x101 transition. The walk
|
||
// legitimately reaches 0x112 through one authored building aperture.
|
||
// Retail uses that cone only for coarse sphere admission, then draws
|
||
// each accepted shell/object whole; the complete intervening wall
|
||
// shell hides the remote actors through ordinary depth. Preserve the
|
||
// installed-DAT fact this depends on: every admitted route is a real,
|
||
// bounded portal polygon, never a pass-all zero-plane route.
|
||
var pose = new WalkOraclePose(
|
||
cameraCellId,
|
||
new Vector3(x, y, 169.804993f),
|
||
Q0: -0.004591f,
|
||
Q1: 0f,
|
||
Q2: 0f,
|
||
Q3: 0.999989f);
|
||
using DatCollection dats = OpenDats();
|
||
using var adapter = new DatCollectionAdapter(dats);
|
||
(WalkLandscapeAssembler assembler, Dictionary<uint, WalkCell> cells,
|
||
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> buildings) =
|
||
BuildProductionWorld(adapter, pose.CellId, pose.Origin);
|
||
var ctx = new WalkTraceReplayContext(pose, cells) { Buildings = buildings };
|
||
WalkCell camera = Assert.Contains(cameraCellId, cells);
|
||
var remotePlayer = new Vector3(36.299465f, 18.594580f, 169.804993f);
|
||
var southHallAdmission = new List<string>();
|
||
var recorder = new Recorder
|
||
{
|
||
OnEmit = e =>
|
||
{
|
||
if (e.Kind != WalkEventKind.DrawCells
|
||
|| !e.Cells.Contains(0xF4180112u))
|
||
{
|
||
return;
|
||
}
|
||
|
||
WalkPortalView views = cells[0xF4180112u].TopView;
|
||
for (int viewIndex = 0; viewIndex < views.ViewCount; viewIndex++)
|
||
{
|
||
WalkViewPoly poly = views.View.Polys[viewIndex];
|
||
var planes = new WalkPlane[poly.VertexCount];
|
||
for (int edge = 0; edge < poly.VertexCount; edge++)
|
||
{
|
||
planes[edge] = views.View.Vertices[
|
||
poly.VertexIndex + edge].Plane;
|
||
}
|
||
|
||
WalkBoundingType verdict = WalkVisibilityMath.ViewconeCheck(
|
||
remotePlayer,
|
||
radius: 1.5f,
|
||
ctx.CyPlane,
|
||
planes);
|
||
float minDistance = planes
|
||
.Select(plane => Vector3.Dot(plane.Normal, remotePlayer) + plane.D)
|
||
.Append(Vector3.Dot(ctx.CyPlane.Normal, remotePlayer) + ctx.CyPlane.D)
|
||
.Min();
|
||
southHallAdmission.Add(
|
||
$"view={viewIndex} planes={poly.VertexCount} verdict={verdict} min={minDistance:F4}");
|
||
}
|
||
},
|
||
};
|
||
|
||
new RetailFrameWalk().WalkFrame(
|
||
cameraCellId,
|
||
camera,
|
||
assembler.Landscape,
|
||
ctx,
|
||
recorder);
|
||
|
||
WalkEvent[] drawCells = recorder.Events
|
||
.Where(static e => e.Kind == WalkEventKind.DrawCells)
|
||
.ToArray();
|
||
Assert.NotEmpty(drawCells);
|
||
Assert.Contains(cameraCellId, drawCells[0].Cells);
|
||
Assert.NotEmpty(southHallAdmission);
|
||
Assert.Contains(
|
||
southHallAdmission,
|
||
static verdict => verdict.Contains("verdict=PartiallyInside")
|
||
&& !verdict.Contains("planes=0"));
|
||
}
|
||
|
||
[Fact]
|
||
public void Foundry_entry_reproduces_every_frame_before_the_f67_order_segment()
|
||
{
|
||
// Same F67-F79 parked segment as WalkTraceConformanceTests (the
|
||
// building-a9b40036-root-plane viewpoint question is a walk/ported-
|
||
// algorithm question, not a world-data question — out of scope for
|
||
// this class). Frames 1-66 must still reproduce exactly.
|
||
MovingFixtureReplay("posed/foundry-entry", stopBeforeFrame: 67);
|
||
}
|
||
|
||
[Theory]
|
||
[InlineData("posed/holtburg-walkout")]
|
||
[InlineData("posed/holtburg-transitions")]
|
||
[InlineData("posed/holtburg-walkabout")]
|
||
public void Moving_fixture_reproduces_every_pairable_frame(string fixture)
|
||
=> MovingFixtureReplay(fixture);
|
||
|
||
private void MovingFixtureReplay(string fixture, int stopBeforeFrame = int.MaxValue)
|
||
{
|
||
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load(fixture);
|
||
Assert.True(frames.Count >= 3);
|
||
using DatCollection dats = OpenDats();
|
||
using var adapter = new DatCollectionAdapter(dats);
|
||
WalkOraclePose anchor = frames[1].Pose!;
|
||
(WalkLandscapeAssembler assembler, Dictionary<uint, WalkCell> cells,
|
||
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> buildings) =
|
||
BuildProductionWorld(adapter, anchor.CellId, anchor.Origin);
|
||
var walk = new RetailFrameWalk();
|
||
|
||
for (int n = 1; n < frames.Count - 1; n++)
|
||
{
|
||
WalkOracleFrame frame = frames[n];
|
||
if (frame.Number >= stopBeforeFrame) break;
|
||
string expected = WalkTraceReplayContext.Signature(frame);
|
||
string? firstActual = null;
|
||
bool matched = false;
|
||
foreach (WalkOraclePose pose in new[] { frames[n + 1].Pose!, frame.Pose! })
|
||
{
|
||
Assert.NotNull(pose);
|
||
assembler.SetViewer(pose.CellId, pose.Origin);
|
||
var ctx = new WalkTraceReplayContext(pose, cells) { Buildings = buildings };
|
||
WalkCell? camera = null;
|
||
if ((pose.CellId & 0xFFFFu) >= 0x100)
|
||
{
|
||
Assert.True(
|
||
cells.TryGetValue(pose.CellId, out camera),
|
||
$"frame {frame.Number}: interior camera cell {pose.CellId:x8} not loaded");
|
||
}
|
||
var recorder = new Recorder();
|
||
walk.WalkFrame(pose.CellId, camera, assembler.Landscape, ctx, recorder);
|
||
string actual = WalkTraceReplayContext.Signature(recorder.Events);
|
||
firstActual ??= actual;
|
||
if (actual == expected)
|
||
{
|
||
matched = true;
|
||
break;
|
||
}
|
||
}
|
||
Assert.True(
|
||
matched,
|
||
$"frame {frame.Number} diverged under both adjacent poses ({fixture})\n"
|
||
+ $"EXPECTED: {expected}\nACTUAL: {firstActual}");
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public void Foundry_deep_reproduces_every_complete_frame_exactly()
|
||
{
|
||
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load("posed/foundry-deep");
|
||
Assert.NotEmpty(frames);
|
||
using DatCollection dats = OpenDats();
|
||
using var adapter = new DatCollectionAdapter(dats);
|
||
Dictionary<uint, WalkCell> cells =
|
||
WalkCellFactory.BuildInteriorCells(adapter, 0xA9B40000u, Vector3.Zero);
|
||
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
|
||
|
||
foreach (WalkOracleFrame frame in frames)
|
||
{
|
||
Assert.NotNull(frame.Pose);
|
||
WalkCell camera = Assert.Contains(frame.Pose!.CellId, cells);
|
||
var ctx = new WalkTraceReplayContext(frame.Pose, cells);
|
||
var walk = new RetailFrameWalk();
|
||
var recorder = new Recorder();
|
||
|
||
walk.WalkFrame(frame.Pose.CellId, camera, landscape, ctx, recorder);
|
||
|
||
Assert.Equal(
|
||
WalkTraceReplayContext.Signature(frame),
|
||
WalkTraceReplayContext.Signature(recorder.Events));
|
||
}
|
||
}
|
||
}
|