diff --git a/tests/AcDream.Core.Tests/Terrain/TerrainModernConformanceTests.cs b/tests/AcDream.Core.Tests/Terrain/TerrainModernConformanceTests.cs
new file mode 100644
index 0000000..3bc403b
--- /dev/null
+++ b/tests/AcDream.Core.Tests/Terrain/TerrainModernConformanceTests.cs
@@ -0,0 +1,184 @@
+using System.Collections.Generic;
+using System.IO;
+using AcDream.Core.Physics;
+using AcDream.Core.Terrain;
+using DatReaderWriter;
+using DatReaderWriter.DBObjs;
+using DatReaderWriter.Options;
+using Xunit;
+using Xunit.Abstractions;
+using Env = System.Environment;
+
+namespace AcDream.Core.Tests.Terrain;
+
+///
+/// Phase N.5b Z-conformance sentinel: proves that the visual terrain mesh
+/// produced by agrees with the physics-side
+/// at arbitrary (X, Y)
+/// within 1 mm. This is the exact bug class issue #51 names — if a future
+/// refactor silently changes formula or vertex layout in either path,
+/// this test fires before the player floats above (or sinks below) the
+/// visible ground.
+///
+/// The test is dat-data-dependent. If ACDREAM_DAT_DIR isn't set or
+/// the directory doesn't exist, the test logs a SKIP and passes — keeps CI
+/// (no dat data) green while still firing locally on every developer run.
+///
+public class TerrainModernConformanceTests
+{
+ private readonly ITestOutputHelper _out;
+
+ public TerrainModernConformanceTests(ITestOutputHelper output) => _out = output;
+
+ private static readonly (string name, uint lbX, uint lbY)[] RepresentativeLandblocks =
+ {
+ ("Holtburg flat 0xA9B0", 0xA9, 0xB0),
+ ("Holtburg sloped 0xA9B1", 0xA9, 0xB1),
+ ("Foundry-area 0x8080", 0x80, 0x80),
+ ("Cragstone 0xCB99", 0xCB, 0x99),
+ ("Direlands sample 0xC040", 0xC0, 0x40),
+ ("MapOrigin 0x0000", 0x00, 0x00),
+ ("Mid-map 0x7F7F", 0x7F, 0x7F),
+ ("MapCorner 0xFEFE", 0xFE, 0xFE),
+ ("Subway outdoor 0x0185", 0x01, 0x85),
+ ("North continent 0x4D96", 0x4D, 0x96),
+ };
+
+ [Fact]
+ public void VisualMeshZ_AgreesWith_PhysicsZ_WithinOneMillimeter()
+ {
+ var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
+ ?? Path.Combine(Env.GetFolderPath(Env.SpecialFolder.UserProfile),
+ "Documents", "Asheron's Call");
+ if (!Directory.Exists(datDir))
+ {
+ _out.WriteLine($"SKIP: dat directory not found at {datDir}");
+ return;
+ }
+
+ using var dats = new DatCollection(datDir, DatAccessType.Read);
+ var region = dats.Get(0x13000000u);
+ Assert.NotNull(region);
+ var heightTable = region.LandDefs.LandHeightTable;
+ Assert.NotNull(heightTable);
+ Assert.True(heightTable.Length >= 256, "heightTable must have at least 256 entries");
+
+ // Empty blending context — the conformance test only cares about
+ // vertex Z values, never the surface info / atlas layers. An empty
+ // dictionary + empty arrays are sufficient for BuildSurface to
+ // resolve every cell to a "base only" surface (the Z values come
+ // from the heightmap, not from the surface info).
+ var ctx = new TerrainBlendingContext(
+ TerrainTypeToLayer: new Dictionary(),
+ RoadLayer: SurfaceInfo.None,
+ CornerAlphaLayers: Array.Empty(),
+ SideAlphaLayers: Array.Empty(),
+ RoadAlphaLayers: Array.Empty(),
+ CornerAlphaTCodes: Array.Empty(),
+ SideAlphaTCodes: Array.Empty(),
+ RoadAlphaRCodes: Array.Empty());
+
+ long totalSamples = 0;
+ long totalLandblocksTested = 0;
+ double maxDelta = 0;
+ (string name, uint lbX, uint lbY, float lx, float ly, float meshZ, float physicsZ) worstCase = default;
+
+ // Fixed seed for reproducible sample distribution. If a future change
+ // makes the test fire, the same (lx, ly) sequence reproduces the
+ // exact failing point on a follow-up run.
+ var rng = new Random(42);
+
+ foreach (var (name, lbX, lbY) in RepresentativeLandblocks)
+ {
+ uint landblockId = (lbX << 24) | (lbY << 16) | 0xFFFFu;
+ var landblock = dats.Get(landblockId);
+ if (landblock is null)
+ {
+ _out.WriteLine($" skipped {name}: dat not found (probably water-only)");
+ continue;
+ }
+ totalLandblocksTested++;
+
+ var surfaceCache = new Dictionary();
+ var meshData = LandblockMesh.Build(landblock, lbX, lbY, heightTable, ctx, surfaceCache);
+
+ // Sample 100 (localX, localY) points uniformly in [0, 192).
+ // We avoid the exact upper bound (192) because that maps to
+ // cell index 8 which the physics path clamps; the pure mesh
+ // sampler doesn't have triangles past 192 anyway.
+ for (int s = 0; s < 100; s++)
+ {
+ float lx = (float)rng.NextDouble() * 191.999f;
+ float ly = (float)rng.NextDouble() * 191.999f;
+
+ float meshZ = SampleMeshZ(meshData, lx, ly);
+ float physicsZ = TerrainSurface.SampleZFromHeightmap(
+ landblock.Height, heightTable, lbX, lbY, lx, ly);
+
+ double delta = Math.Abs(meshZ - physicsZ);
+ if (delta > maxDelta)
+ {
+ maxDelta = delta;
+ worstCase = (name, lbX, lbY, lx, ly, meshZ, physicsZ);
+ }
+ totalSamples++;
+ Assert.True(delta < 0.001,
+ $"Mesh Z disagrees with physics Z at lb=0x{lbX:X2}{lbY:X2} ({name}) " +
+ $"local=({lx:F2},{ly:F2}): meshZ={meshZ:F4} physicsZ={physicsZ:F4} delta={delta:F4}m");
+ }
+ }
+
+ _out.WriteLine($"=== Phase N.5b conformance sweep ===");
+ _out.WriteLine($"Landblocks tested: {totalLandblocksTested}/{RepresentativeLandblocks.Length}");
+ _out.WriteLine($"Total samples: {totalSamples}");
+ _out.WriteLine($"Max |delta|: {maxDelta * 1000:F4} mm (tolerance: 1.0 mm)");
+ if (totalSamples > 0)
+ _out.WriteLine($"Worst case: {worstCase.name} local=({worstCase.lx:F2},{worstCase.ly:F2}) " +
+ $"meshZ={worstCase.meshZ:F4} physicsZ={worstCase.physicsZ:F4}");
+
+ Assert.True(totalLandblocksTested >= 5,
+ $"Expected at least 5 representative landblocks loadable; got {totalLandblocksTested}.");
+ }
+
+ ///
+ /// Sample the mesh's triangle-interpolated Z at (localX, localY). Walks
+ /// the mesh's triangles (3 indices each), tests point-in-triangle in 2D,
+ /// and barycentric-interpolates Z from the matching triangle's three Zs.
+ ///
+ /// The mesh has 128 triangles per landblock (64 cells × 2). Every (lx, ly)
+ /// in [0, 192) lies in exactly one triangle (or on a shared edge — the
+ /// epsilon makes either side acceptable since they agree at the seam).
+ ///
+ private static float SampleMeshZ(LandblockMeshData mesh, float lx, float ly)
+ {
+ for (int triBase = 0; triBase < mesh.Indices.Length; triBase += 3)
+ {
+ var v0 = mesh.Vertices[mesh.Indices[triBase + 0]];
+ var v1 = mesh.Vertices[mesh.Indices[triBase + 1]];
+ var v2 = mesh.Vertices[mesh.Indices[triBase + 2]];
+
+ // Barycentric coords for (lx, ly) wrt triangle v0/v1/v2 in 2D.
+ float denom = (v1.Position.Y - v2.Position.Y) * (v0.Position.X - v2.Position.X)
+ + (v2.Position.X - v1.Position.X) * (v0.Position.Y - v2.Position.Y);
+ if (Math.Abs(denom) < 1e-9f) continue;
+
+ float a = ((v1.Position.Y - v2.Position.Y) * (lx - v2.Position.X)
+ + (v2.Position.X - v1.Position.X) * (ly - v2.Position.Y)) / denom;
+ float b = ((v2.Position.Y - v0.Position.Y) * (lx - v2.Position.X)
+ + (v0.Position.X - v2.Position.X) * (ly - v2.Position.Y)) / denom;
+ float c = 1f - a - b;
+
+ // Inside test with epsilon for boundary stability — points that
+ // land exactly on a shared edge between two triangles still
+ // resolve, picking whichever the loop hits first (Z agrees on
+ // the seam either way).
+ const float eps = 1e-4f;
+ if (a >= -eps && b >= -eps && c >= -eps)
+ return a * v0.Position.Z + b * v1.Position.Z + c * v2.Position.Z;
+ }
+
+ // Should not happen for valid mesh + in-bounds (lx, ly).
+ throw new InvalidOperationException(
+ $"No triangle found containing local=({lx:F2},{ly:F2}); mesh has {mesh.Indices.Length / 3} triangles.");
+ }
+}