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.
139 lines
5.1 KiB
C#
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);
|
|
}
|
|
}
|