using System.Numerics; using AcDream.Core.Terrain; using DatReaderWriter.DBObjs; using DatReaderWriter.Types; namespace AcDream.Core.Tests.Terrain; public class LandblockMeshTests { /// /// Synthetic height table with a * 2.0f scale (mirrors Phase 1's ramp so /// existing test intuition carries through the Phase 3c rewrite). /// private static readonly float[] IdentityHeightTable = Enumerable.Range(0, 256).Select(i => i * 2f).ToArray(); private static TerrainBlendingContext MakeContext() => new( TerrainTypeToLayer: new Dictionary { [0u] = 0 }, RoadLayer: SurfaceInfo.None, CornerAlphaLayers: Array.Empty(), SideAlphaLayers: Array.Empty(), RoadAlphaLayers: Array.Empty(), CornerAlphaTCodes: Array.Empty(), SideAlphaTCodes: Array.Empty(), RoadAlphaRCodes: Array.Empty()); private static LandBlock BuildFlatLandBlock(byte heightIndex = 0) { var block = new LandBlock { HasObjects = false, Terrain = new TerrainInfo[81], Height = new byte[81], }; for (int i = 0; i < 81; i++) { block.Terrain[i] = (ushort)0; block.Height[i] = heightIndex; } return block; } [Fact] public void Build_FlatBlock_Produces384VerticesAnd128Triangles() { var block = BuildFlatLandBlock(); var cache = new Dictionary(); var mesh = LandblockMesh.Build(block, 0, 0, IdentityHeightTable, MakeContext(), cache); // 64 cells × 6 vertices per cell = 384 Assert.Equal(384, mesh.Vertices.Length); // Each cell emits 2 triangles = 6 indices, 64 cells → 384 indices (= 128 triangles) Assert.Equal(128 * 3, mesh.Indices.Length); } [Fact] public void Build_Vertices_CoverExactly192x192WorldUnits() { var block = BuildFlatLandBlock(); var cache = new Dictionary(); var mesh = LandblockMesh.Build(block, 0, 0, IdentityHeightTable, MakeContext(), cache); var minX = mesh.Vertices.Min(v => v.Position.X); var maxX = mesh.Vertices.Max(v => v.Position.X); var minY = mesh.Vertices.Min(v => v.Position.Y); var maxY = mesh.Vertices.Max(v => v.Position.Y); Assert.Equal(0.0f, minX); Assert.Equal(192.0f, maxX); Assert.Equal(0.0f, minY); Assert.Equal(192.0f, maxY); } [Fact] public void Build_FlatBlock_AllVerticesSameZ() { var block = BuildFlatLandBlock(heightIndex: 10); var cache = new Dictionary(); var mesh = LandblockMesh.Build(block, 0, 0, IdentityHeightTable, MakeContext(), cache); var zs = mesh.Vertices.Select(v => v.Position.Z).Distinct().ToArray(); Assert.Single(zs); Assert.Equal(20.0f, zs[0]); // heightIndex 10 × IdentityHeightTable[10] = 20 } [Fact] public void Build_FlatBlock_NormalsPointStraightUp() { var block = BuildFlatLandBlock(); var cache = new Dictionary(); var mesh = LandblockMesh.Build(block, 0, 0, IdentityHeightTable, MakeContext(), cache); foreach (var v in mesh.Vertices) { Assert.Equal(new Vector3(0, 0, 1), v.Normal); } } [Fact] public void Build_AllVerticesOfACellShareIdenticalData() { var block = BuildFlatLandBlock(); var cache = new Dictionary(); var mesh = LandblockMesh.Build(block, 0, 0, IdentityHeightTable, MakeContext(), cache); // Vertices are emitted in strides of 6 per cell. Within each stride, // Data0..3 must be identical — the vertex shader relies on that when // it propagates the cell's blend recipe to all 3 fragment-shader outputs. for (int cellIdx = 0; cellIdx < 64; cellIdx++) { int baseIdx = cellIdx * 6; var d0 = mesh.Vertices[baseIdx].Data0; var d1 = mesh.Vertices[baseIdx].Data1; var d2 = mesh.Vertices[baseIdx].Data2; var d3 = mesh.Vertices[baseIdx].Data3; for (int i = 1; i < 6; i++) { Assert.Equal(d0, mesh.Vertices[baseIdx + i].Data0); Assert.Equal(d1, mesh.Vertices[baseIdx + i].Data1); Assert.Equal(d2, mesh.Vertices[baseIdx + i].Data2); Assert.Equal(d3, mesh.Vertices[baseIdx + i].Data3); } } } [Fact] public void Build_SurfaceCacheIsReusedAcrossIdenticalCells() { var block = BuildFlatLandBlock(); // every cell has identical all-zero corners var cache = new Dictionary(); LandblockMesh.Build(block, 0, 0, IdentityHeightTable, MakeContext(), cache); // A uniform flat landblock produces exactly ONE palette code (all // corners are type 0, no roads) → BuildSurface called once, cache // contains a single entry even though 64 cells were processed. Assert.Single(cache); } [Fact] public void Build_CellsWithDistinctTerrainTypes_ProducesDistinctPaletteCodes() { // Put a dirt cell (type 4) at the center of an otherwise grass landblock. // Grass cells all share one palCode; the "dirt + grass border" cells // around the center introduce additional palette codes. var block = BuildFlatLandBlock(); // Type is at bits 2-6, so type=4 → ushort = (4 << 2) = 0x10. block.Terrain[4 * 9 + 4] = (ushort)(4 << 2); var ctx = new TerrainBlendingContext( TerrainTypeToLayer: new Dictionary { [0u] = 0, [4u] = 1 }, RoadLayer: SurfaceInfo.None, CornerAlphaLayers: new byte[] { 0, 1, 2, 3 }, SideAlphaLayers: Array.Empty(), RoadAlphaLayers: Array.Empty(), CornerAlphaTCodes: new uint[] { 1, 2, 4, 8 }, SideAlphaTCodes: Array.Empty(), RoadAlphaRCodes: Array.Empty()); var cache = new Dictionary(); LandblockMesh.Build(block, 0, 0, IdentityHeightTable, ctx, cache); // Should have more than one palette code now — uniform-grass cells // plus at least one boundary cell with a non-zero corner type. Assert.True(cache.Count >= 2, $"Expected mix of palette codes, got {cache.Count}"); } [Fact] public void Build_HeightmapPackedAsXMajor_NotYMajor() { // Regression from the Phase 1 → 2a transpose bug. The underlying // heightmap is indexed x*9+y; testing this lives on even after the // per-cell refactor because the corner lookup in the cell loop still // reads block.Height[cx*9+cy] for the BL corner. var block = BuildFlatLandBlock(); block.Height[2 * 9 + 0] = 5; // x=2, y=0 → world (48, 0), Z should be 10 var cache = new Dictionary(); var mesh = LandblockMesh.Build(block, 0, 0, IdentityHeightTable, MakeContext(), cache); // Search the vertex buffer for a vertex at world position (48, 0). var atX48Y0 = mesh.Vertices.FirstOrDefault(v => Math.Abs(v.Position.X - 48f) < 0.01f && Math.Abs(v.Position.Y) < 0.01f); var atX0Y48 = mesh.Vertices.FirstOrDefault(v => Math.Abs(v.Position.X) < 0.01f && Math.Abs(v.Position.Y - 48f) < 0.01f); Assert.Equal(10.0f, atX48Y0.Position.Z); Assert.Equal(0.0f, atX0Y48.Position.Z); } }