diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkLandscapeDatBuilder.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkLandscapeDatBuilder.cs
new file mode 100644
index 00000000..8f8aab54
--- /dev/null
+++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkLandscapeDatBuilder.cs
@@ -0,0 +1,120 @@
+using System.Numerics;
+using AcDream.App.Rendering.Walk;
+using DatReaderWriter;
+using DatReaderWriter.DBObjs;
+
+namespace AcDream.App.Tests.Rendering.Walk;
+
+///
+/// Campaign FW1 — assembles the replay world for outdoor/landscape
+/// fixtures: retail's 11×11 viewer-centered block grid (mid_radius 5) in
+/// CAMERA-BLOCK-LOCAL coordinates (the same space the pose origin and the
+/// per-block corner math use), each block with its retail z-slab
+/// (CLandBlock::calc_lighting-adjacent unpack @0x0052f1d0:
+/// max_zval = heightTable[maxByte] + 200, min_zval = heightTable[minByte]
+/// − 1), buildings placed at their landcell indices with landblock-local
+/// transforms offset by the block delta, and every neighbor block's
+/// interior cells merged into one registry.
+///
+public static class WalkLandscapeDatBuilder
+{
+ public sealed record BuiltWorld(
+ WalkLandscape Landscape,
+ Dictionary Cells,
+ Dictionary Buildings);
+
+ /// The capture client's live landscape grid (dumped 2026-08-30:
+ /// LScape mid_radius=25 → a 51×51 grid — the owner's landscape-radius
+ /// setting) with the resolution pyramid observed live: side_cell_count 8
+ /// in the 3×3 core, 4 at ring 2, 2 at rings 3–4, 1 beyond. Buildings
+ /// attach only to full-res blocks (the traces show no BLD beyond ±1).
+ public const int MidRadius = 25;
+
+ private static int SideCellCountForRing(int ring)
+ => ring <= 1 ? 8 : ring == 2 ? 4 : ring <= 4 ? 2 : 1;
+
+ public static BuiltWorld Build(DatCollection dats, uint cameraCellId)
+ {
+ Region region = (Region)dats.Get(0x13000000u)!;
+ float[] heightTable = region.LandDefs.LandHeightTable;
+
+ int cameraBlockX = (int)(cameraCellId >> 24);
+ int cameraBlockY = (int)((cameraCellId >> 16) & 0xFF);
+ const int midRadius = MidRadius;
+ const int midWidth = midRadius * 2 + 1;
+
+ var landscape = new WalkLandscape
+ {
+ MidWidth = midWidth,
+ Blocks = new WalkLandBlock?[midWidth * midWidth],
+ ViewerBlockX = midRadius,
+ ViewerBlockY = midRadius,
+ };
+ uint low = cameraCellId & 0xFFFFu;
+ if (low >= 1 && low <= 0x40)
+ {
+ int cellIndex = (int)low - 1;
+ landscape.ViewerCellX = cellIndex / 8;
+ landscape.ViewerCellY = cellIndex % 8;
+ }
+
+ var cells = new Dictionary();
+ var buildings = new Dictionary();
+
+ for (int gx = 0; gx < midWidth; gx++)
+ {
+ for (int gy = 0; gy < midWidth; gy++)
+ {
+ int blockX = cameraBlockX + gx - midRadius;
+ int blockY = cameraBlockY + gy - midRadius;
+ if (blockX < 0 || blockX > 0xFF || blockY < 0 || blockY > 0xFF)
+ continue;
+ uint landblockId = (uint)((blockX << 24) | (blockY << 16));
+ if (dats.Get(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;
+ }
+ int ring = Math.Max(Math.Abs(gx - midRadius), Math.Abs(gy - midRadius));
+ int sideCellCount = SideCellCountForRing(ring);
+ var block = new WalkLandBlock
+ {
+ SideCellCount = sideCellCount,
+ MaxZ = heightTable[maxByte] + 200f,
+ MinZ = heightTable[minByte] - 1f,
+ };
+ block.EnsureCellArrays();
+
+ var blockOffset = new Vector3(
+ (gx - midRadius) * WalkLandscape.BlockLength,
+ (gy - midRadius) * WalkLandscape.BlockLength,
+ 0f);
+
+ if (sideCellCount == 8)
+ {
+ foreach (WalkWorldDatAdapter.BuildingEntry entry
+ in WalkWorldDatAdapter.BuildBuildings(dats, landblockId))
+ {
+ Matrix4x4 world = entry.WorldTransform
+ * Matrix4x4.CreateTranslation(blockOffset);
+ Matrix4x4.Invert(world, out Matrix4x4 inverse);
+ var placed = new WalkWorldDatAdapter.BuildingEntry(
+ entry.Building, world, inverse);
+ buildings[entry.Building] = placed;
+ int cellIndex = (int)(entry.Building.PositionCellId & 0xFFFFu) - 1;
+ if (cellIndex >= 0 && cellIndex < 64)
+ block.CellBuildings[cellIndex] = entry.Building;
+ }
+ WalkWorldDatAdapter.BuildInteriorCells(
+ dats, landblockId, blockOffset, cells);
+ }
+ landscape.Blocks[gx * midWidth + gy] = block;
+ }
+ }
+ return new BuiltWorld(landscape, cells, buildings);
+ }
+}
diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs
index 8c064669..fde4de27 100644
--- a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs
@@ -32,6 +32,37 @@ public sealed class WalkTraceConformanceTests
return new DatCollection(datDir!, DatAccessType.Read);
}
+ [Fact(Skip = "FW1 adjudication in progress: the walk over-culls six traced "
+ + "buildings (incl. block a9b3) and the look-in floods differ — see the "
+ + "campaign memory. Re-enable as the outdoor gate once adjudicated.")]
+ public void Street_outdoor_first_frame_diff()
+ {
+ // The first landscape-involving conformance case: diff-first (the
+ // assert prints both signatures on mismatch for adjudication).
+ IReadOnlyList frames =
+ WalkOracleTrace.Load("posed/holtburg-street-outdoor");
+ Assert.NotEmpty(frames);
+ using DatCollection dats = OpenDats();
+ WalkOracleFrame frame = frames[1]; // frame 2: pose settled (marker timing)
+ Assert.NotNull(frame.Pose);
+ 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 expected = WalkTraceReplayContext.Signature(frame);
+ string actual = WalkTraceReplayContext.Signature(recorder.Events);
+ Assert.True(
+ expected == actual,
+ $"walk diverged from retail\nEXPECTED: {expected}\nACTUAL: {actual}");
+ }
+
[Fact]
public void Foundry_deep_reproduces_every_complete_frame_exactly()
{
diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs
index 76778d58..7cf72809 100644
--- a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs
+++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs
@@ -72,6 +72,14 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk
CyPlane = new WalkPlane(forward, -Vector3.Dot(pose.Origin, forward) - 0.1f);
}
+ /// Building placements (camera-block-local) for the landscape
+ /// fixtures; empty for the interior-only ones.
+ public Dictionary Buildings { get; set; }
+ = new();
+
+ private Vector2[] _activeViewVerts = new Vector2[32];
+ private int _activeViewVertCount;
+
public Vector3 ViewpointIn(WalkCell cell)
=> Vector3.Transform(WorldViewpoint, cell.InverseWorldTransform);
@@ -85,13 +93,33 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk
public float ViewportHeight { get; }
public WalkPlane CyPlane { get; }
public IWalkFrameContext CellContext => this;
- public void SetActiveView(WalkPortalView views, int index) { }
- public Vector3 ViewpointInBuilding(WalkBuilding building) => WorldViewpoint;
+ public void SetActiveView(WalkPortalView views, int index)
+ {
+ WalkViewPoly poly = views.View.Polys[index];
+ if (_activeViewVerts.Length < poly.VertexCount)
+ _activeViewVerts = new Vector2[poly.VertexCount];
+ for (int k = 0; k < poly.VertexCount; k++)
+ _activeViewVerts[k] = views.View.Vertices[poly.VertexIndex + k].Point;
+ _activeViewVertCount = poly.VertexCount;
+ }
+
+ public Vector3 ViewpointInBuilding(WalkBuilding building)
+ => Vector3.Transform(WorldViewpoint, Buildings[building].InverseWorldTransform);
public int ClipBuildingPolygon(
WalkBuilding building, WalkPolygon polygon, int side, Span output)
- => 0; // building look-ins join the conformance surface with the landscape fixtures
+ {
+ Matrix4x4 objectToClip = Buildings[building].WorldTransform * _viewProjection;
+ Span projected = stackalloc WalkScreenPoint[polygon.Vertices.Length];
+ for (int i = 0; i < polygon.Vertices.Length; i++)
+ projected[i] = WalkScreenClip.TransformToScreen(
+ polygon.Vertices[i], objectToClip, ViewportWidth, ViewportHeight);
+ if (side != 0)
+ projected.Reverse();
+ return WalkScreenClip.ClipAgainstView(
+ projected, _activeViewVerts.AsSpan(0, _activeViewVertCount), output);
+ }
// ---- signatures for comparing walk output to oracle frames ----
diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs
index db7d6bd8..a8393cff 100644
--- a/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs
+++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs
@@ -32,7 +32,14 @@ using AcDream.App.Rendering.Walk;
///
public static class WalkWorldDatAdapter
{
+ /// One built building plus its landblock-local placement.
+ public sealed record BuildingEntry(
+ WalkBuilding Building, Matrix4x4 WorldTransform, Matrix4x4 InverseWorldTransform);
+
public static WalkCell? BuildCell(DatCollection dats, uint cellId)
+ => BuildCell(dats, cellId, Vector3.Zero);
+
+ public static WalkCell? BuildCell(DatCollection dats, uint cellId, Vector3 blockOffset)
{
if (dats.Get(cellId) is not EnvCell envCell)
return null;
@@ -67,9 +74,9 @@ public static class WalkWorldDatAdapter
Matrix4x4 worldTransform =
Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation)
* Matrix4x4.CreateTranslation(
- envCell.Position.Origin.X,
- envCell.Position.Origin.Y,
- envCell.Position.Origin.Z);
+ envCell.Position.Origin.X + blockOffset.X,
+ envCell.Position.Origin.Y + blockOffset.Y,
+ envCell.Position.Origin.Z + blockOffset.Z);
Matrix4x4.Invert(worldTransform, out Matrix4x4 inverse);
return new WalkCell
{
@@ -84,12 +91,17 @@ public static class WalkWorldDatAdapter
public static Dictionary BuildInteriorCells(
DatCollection dats, uint landblockId)
+ => BuildInteriorCells(dats, landblockId, Vector3.Zero, null);
+
+ public static Dictionary BuildInteriorCells(
+ DatCollection dats, uint landblockId, Vector3 blockOffset,
+ Dictionary? into)
{
uint lbMask = landblockId & 0xFFFF0000u;
- var cells = new Dictionary();
+ Dictionary cells = into ?? new Dictionary();
for (uint low = 0x0100; low <= 0xFFFD; low++)
{
- WalkCell? cell = BuildCell(dats, lbMask | low);
+ WalkCell? cell = BuildCell(dats, lbMask | low, blockOffset);
if (cell is null) break; // interior cells are contiguous from 0x0100
cells[cell.CellId] = cell;
}
@@ -97,11 +109,12 @@ public static class WalkWorldDatAdapter
}
/// Buildings of one landblock, with their walk event ids
- /// (containing landcell), portal records, and drawing-BSP portal trees.
- public static List BuildBuildings(DatCollection dats, uint landblockId)
+ /// (containing landcell), portal records, drawing-BSP portal trees, and
+ /// landblock-local placements.
+ public static List BuildBuildings(DatCollection dats, uint landblockId)
{
uint lbMask = landblockId & 0xFFFF0000u;
- var result = new List();
+ var result = new List();
if (dats.Get(lbMask | 0xFFFEu) is not LandBlockInfo info
|| info.Buildings is null)
{
@@ -138,12 +151,19 @@ public static class WalkWorldDatAdapter
if (dats.Get(buildingInfo.ModelId) is GfxObj gfxObj)
bsp = ConvertDrawingBsp(gfxObj, gfxObj.DrawingBSP?.Root);
- result.Add(new WalkBuilding
- {
- PositionCellId = positionCellId,
- Portals = portals,
- DrawingBsp = bsp,
- });
+ Matrix4x4 worldTransform =
+ Matrix4x4.CreateFromQuaternion(buildingInfo.Frame.Orientation)
+ * Matrix4x4.CreateTranslation(origin);
+ Matrix4x4.Invert(worldTransform, out Matrix4x4 inverse);
+ result.Add(new BuildingEntry(
+ new WalkBuilding
+ {
+ PositionCellId = positionCellId,
+ Portals = portals,
+ DrawingBsp = bsp,
+ },
+ worldTransform,
+ inverse));
}
return result;
}
diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapterTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapterTests.cs
index 38fb831a..ce777f60 100644
--- a/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapterTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapterTests.cs
@@ -60,8 +60,9 @@ public sealed class WalkWorldDatAdapterTests
{
using DatCollection dats = OpenDats();
- List buildings = WalkWorldDatAdapter.BuildBuildings(dats, landblockId);
- var roster = buildings.Select(b => b.PositionCellId).ToHashSet();
+ List buildings =
+ WalkWorldDatAdapter.BuildBuildings(dats, landblockId);
+ var roster = buildings.Select(e => e.Building.PositionCellId).ToHashSet();
foreach (uint traced in tracedBuildingIds)
Assert.Contains(traced, roster);
@@ -72,9 +73,10 @@ public sealed class WalkWorldDatAdapterTests
{
using DatCollection dats = OpenDats();
- List buildings = WalkWorldDatAdapter.BuildBuildings(dats, 0xF4180000u);
+ List buildings =
+ WalkWorldDatAdapter.BuildBuildings(dats, 0xF4180000u);
WalkBuilding sanctuary = Assert.Single(
- buildings, b => b.PositionCellId == 0xF418000Au);
+ buildings, e => e.Building.PositionCellId == 0xF418000Au).Building;
Assert.NotEmpty(sanctuary.Portals);
Assert.All(sanctuary.Portals, p => Assert.NotEmpty(p.StabList));