using System.Numerics; using AcDream.Core.Terrain; using DatReaderWriter.DBObjs; using DatReaderWriter.Types; namespace AcDream.Core.Tests.Terrain; public class LandblockMeshTests { /// /// Synthetic height table that mirrors Phase 1's simplified "* 2.0f" scale so /// the existing tests continue to describe the same behavior. Real AC uses a /// non-linear table from Region.LandDefs.LandHeightTable loaded at runtime. /// private static readonly float[] IdentityHeightTable = Enumerable.Range(0, 256).Select(i => i * 2f).ToArray(); private static readonly IReadOnlyDictionary EmptyTerrainMap = new Dictionary(); 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_Produces81VerticesAnd128Triangles() { var block = BuildFlatLandBlock(); var mesh = LandblockMesh.Build(block, IdentityHeightTable, EmptyTerrainMap); Assert.Equal(81, mesh.Vertices.Length); Assert.Equal(128 * 3, mesh.Indices.Length); } [Fact] public void Build_Vertices_Cover192x192WorldUnits() { var block = BuildFlatLandBlock(); var mesh = LandblockMesh.Build(block, IdentityHeightTable, EmptyTerrainMap); 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 mesh = LandblockMesh.Build(block, IdentityHeightTable, EmptyTerrainMap); var zs = mesh.Vertices.Select(v => v.Position.Z).Distinct().ToArray(); Assert.Single(zs); } [Fact] public void Build_HeightValues_ScaleByTwo() { var block = BuildFlatLandBlock(heightIndex: 5); var mesh = LandblockMesh.Build(block, IdentityHeightTable, EmptyTerrainMap); // AC's Land::LandHeightTable scales height byte index by 2.0f for the simple ramp case. Assert.Equal(10.0f, mesh.Vertices[0].Position.Z); } [Fact] public void Build_PerVertexTerrainLayer_UsesMappedLayerIndex() { var block = BuildFlatLandBlock(); // TerrainInfo is bit-packed: bits 0-1 Road, bits 2-6 Type, bits 11-15 Scenery. // Raw ushort 0x001C = binary 0011100 → Type field = 7 (bits 2-6). // This is what a terrain sample with TerrainTextureType=7 looks like in the // underlying byte stream. LandblockMesh uses TerrainInfo.Type (not raw) as // the atlas lookup key. block.Terrain[2 * 9 + 3] = (ushort)(7 << 2); // Type=7, Road=0, Scenery=0 var map = new Dictionary { [0] = 0u, // default type → atlas layer 0 [7] = 4u, // TerrainTextureType 7 → atlas layer 4 }; var mesh = LandblockMesh.Build(block, IdentityHeightTable, map); // Vertex buffer internal order is y*9+x, so vertex at world (x=2, y=3) is at // index 3*9+2 = 29. Assert.Equal(4u, mesh.Vertices[3 * 9 + 2].TerrainLayer); // An untouched vertex still has Type 0, maps to layer 0. Assert.Equal(0u, mesh.Vertices[0].TerrainLayer); } [Fact] public void Build_HeightmapPackedAsXMajor_NotYMajor() { // Regression: Phase 1 used block.Height[y*9+x] which transposes the terrain // along its diagonal relative to AC's native x-major packing. Invisible on // flat landblocks but catastrophically wrong on Holtburg where static-object // positions reference the un-transposed ground truth, leaving buildings // buried by ~10 world-Z units. // // Set up an asymmetric heightmap: value at x-major index (x=2, y=0) = 5 // (scaled to Z=10), everything else 0. The vertex at world position // (x=2*24=48, y=0) should have Z=10. The vertex at (x=0, y=2*24=48) should // have Z=0. Y-major indexing would swap these. var block = BuildFlatLandBlock(); block.Height[2 * 9 + 0] = 5; // x=2, y=0 in x-major packing var mesh = LandblockMesh.Build(block, IdentityHeightTable, EmptyTerrainMap); // Find vertices by position. Vertex buffer uses y*9+x internally. var vAt_x2_y0 = mesh.Vertices[0 * 9 + 2]; // world (48, 0) var vAt_x0_y2 = mesh.Vertices[2 * 9 + 0]; // world (0, 48) Assert.Equal(new Vector3(48, 0, 10), vAt_x2_y0.Position); Assert.Equal(new Vector3(0, 48, 0), vAt_x0_y2.Position); } }