The retail frame walk's world model now materializes from production landblock-build owners through the legal IDatReaderWriter seam, with zero frame wiring (FW3.2 roots the frame): - WalkCellFactory: WalkCell built in the SAME pass as LoadedCell (EnvCellLandblockBuild.BuildVisibilityCell) from the raw portal Flags/polygons/planes/stab lists already parsed there; stored as LoadedCell.Walk, committed atomically with the cell. The fixture-pinned decodes (inverse-0x2 portal side, 0xFFFF->0xFFFFFFFF exit widening) live here. - WalkBuildingFactory + WalkBuildingRegistry: the production WalkBuilding build (drawing BSP with PORT nodes, degrade ladder, portal sides/stab lists, sort center, model frame) from the SAME LandBlockInfo the streaming build already fetches, under the factory's existing DAT lock - closing the gap where BuildingLoader drops every walk field at load. - WalkLandscapeAssembler: the retail 51x51 viewer-centred grid (mid_radius 25) fed incrementally from landblock publish/retire; per-block z-slab (heightTable[max]+200 / [min]-1) computed worker-side in LandblockBuildFactory from the heights already in hand. O(1) SetViewer on same-block frames. - WalkProductionFrameContext: the walk's frame contexts over CellVisibility + WalkBuildingRegistry with a generic inverse-view-projection ray caster (rays feed cross products only - scale-free) and the znear=0.1 CY plane. - Publication: LandblockRenderPublisher owns both walk registries, publishing in the same AdvanceCompleteOne step as BuildingRegistry and retiring in RemoveBuildingRegistry - same commit, same retirement, no new ticket stage. Conformance: ALL TEN oracle fixtures replay identically through the PRODUCTION builders (WalkProductionWorldConformanceTests) - same signatures as the test adapter, first run. Known gap documented for FW3.2: far-tier landblocks carry no EnvCell transaction, so their z-slab never reaches the assembler. Suites: full Release build 0 warnings; Walk lane 186/1 skip; hermetic 6,738/0 (+24); RuntimeDatAccessArchitectureTests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
321 lines
15 KiB
C#
321 lines
15 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 void Emit(in WalkEvent walkEvent) => Events.Add(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}");
|
||
}
|
||
|
||
[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));
|
||
}
|
||
}
|
||
}
|