acdream/tests/AcDream.Core.Tests/Terrain/LandblockMeshTests.cs
Erik 78ce099440 fix(core): LandblockMesh keys atlas lookup on TerrainInfo.Type
Task 1's subagent used the raw ushort as the map key because the test
used raw ushort 7 as the value. But the atlas map is built from
Region.TerrainInfo.LandSurfaces.TexMerge.TerrainDesc which keys on
TerrainTextureType enum values, extracted from bits 2-6 of the
TerrainInfo ushort per DatReaderWriter's Types/TerrainInfo.cs.

Reverts to using block.Terrain[hi].Type so the Task 2 TerrainAtlas can
actually find matching keys against real dat terrain. The test is
updated to encode Type=7 correctly as (7 << 2) in the raw ushort.
2026-04-10 20:18:09 +02:00

139 lines
5.1 KiB
C#

using System.Numerics;
using AcDream.Core.Terrain;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;
namespace AcDream.Core.Tests.Terrain;
public class LandblockMeshTests
{
/// <summary>
/// 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.
/// </summary>
private static readonly float[] IdentityHeightTable =
Enumerable.Range(0, 256).Select(i => i * 2f).ToArray();
private static readonly IReadOnlyDictionary<uint, uint> EmptyTerrainMap =
new Dictionary<uint, uint>();
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<uint, uint>
{
[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);
}
}