Three fixes to match retail CLandBlock::get_land_scenes (0x00530460): 1. Loop bound: iterate 9×9 vertices (side_vertex_count=9), not 8×8 cells. Edge vertices (x=8 or y=8) produce valid spawns when the per-object displacement shifts the position back into [0, 192). Confirmed by named retail decomp do-while condition, WorldBuilder vertLength=9, ACViewer Terrain.Count=81, AC2D wTopo[9][9]. 2. Building suppression: check at the DISPLACED position's cell (CSortCell::has_building per spawn), not at the loop vertex index. Matches WorldBuilder buildingsGrid[gx2, gy2] pattern. 3. Slope filter: replace finite-difference gradient approximation with triangle-aware normal sampling via new static method TerrainSurface.SampleNormalZFromHeightmap. Picks the correct triangle via IsSplitSWtoNE, matching retail find_terrain_poly → polygon->plane.N.z and WorldBuilder's GetNormal(). Tests: 5 new tests for SampleNormalZFromHeightmap (flat=1.0, sloped<1, cross-validates with SampleSurface instance method) and DisplaceObject edge-vertex validity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
109 lines
4 KiB
C#
109 lines
4 KiB
C#
using System.Numerics;
|
||
using AcDream.Core.World;
|
||
using DatReaderWriter.Types;
|
||
|
||
namespace AcDream.Core.Tests.World;
|
||
|
||
/// <summary>
|
||
/// Tests for SceneryGenerator: road-exclusion, loop bounds, building
|
||
/// suppression, and slope filter. The full Generate() pipeline requires
|
||
/// real dat files so behavior is tested via internal helpers.
|
||
/// </summary>
|
||
public class SceneryGeneratorTests
|
||
{
|
||
// Terrain word layout (ushort):
|
||
// bits 0-1 = Road (non-zero → on a road)
|
||
// bits 2-6 = TerrainType
|
||
// bits 11-15 = SceneType
|
||
|
||
[Theory]
|
||
[InlineData(0x0000, false)] // no road bits
|
||
[InlineData(0x0001, true)] // road bit 0 set
|
||
[InlineData(0x0002, true)] // road bit 1 set
|
||
[InlineData(0x0003, true)] // both road bits set
|
||
[InlineData(0x007C, false)] // terrain type bits only, no road
|
||
[InlineData(0xF800, false)] // scenery bits only, no road
|
||
[InlineData(0xF803, true)] // road + scenery bits
|
||
public void IsRoadVertex_CorrectlyIdentifiesRoadBits(ushort raw, bool expectedIsRoad)
|
||
{
|
||
Assert.Equal(expectedIsRoad, SceneryGenerator.IsRoadVertex(raw));
|
||
}
|
||
|
||
[Fact]
|
||
public void IsRoadVertex_ZeroTerrain_IsNotRoad()
|
||
{
|
||
Assert.False(SceneryGenerator.IsRoadVertex(0));
|
||
}
|
||
|
||
[Fact]
|
||
public void IsRoadVertex_MatchesTerrainInfoRoadProperty()
|
||
{
|
||
for (ushort raw = 0; raw < 4; raw++)
|
||
{
|
||
TerrainInfo ti = raw;
|
||
bool expectedFromStruct = ti.Road != 0;
|
||
bool actual = SceneryGenerator.IsRoadVertex(raw);
|
||
Assert.True(actual == expectedFromStruct,
|
||
$"raw=0x{raw:X4}: IsRoadVertex={actual} but TerrainInfo.Road={ti.Road}");
|
||
}
|
||
}
|
||
|
||
// --- Edge vertex displacement tests ---
|
||
// Retail iterates 9×9 vertices (0..8 on each axis). Vertices at x=8 or y=8
|
||
// have base positions at 192 (= 8 * 24), which is AT the landblock boundary.
|
||
// These produce valid scenery when displacement shifts them back into [0, 192).
|
||
|
||
[Fact]
|
||
public void DisplaceObject_EdgeVertex_CanProduceValidPosition()
|
||
{
|
||
// Vertex (3, 8): base_y = 8 * 24 = 192.
|
||
// With DisplaceY > 0, some LCG seeds will produce negative displacement,
|
||
// shifting the Y back below 192 into the valid range.
|
||
var obj = new ObjectDesc
|
||
{
|
||
DisplaceX = 12f,
|
||
DisplaceY = 12f,
|
||
BaseLoc = new Frame { Origin = new Vector3(0, 0, 0) }
|
||
};
|
||
|
||
// Search across a range of global cell coords to find at least one
|
||
// case where vertex y=8 displaces into [0, 192).
|
||
bool foundValid = false;
|
||
for (uint gx = 0; gx < 64 && !foundValid; gx++)
|
||
{
|
||
for (uint gy = 0; gy < 64 && !foundValid; gy++)
|
||
{
|
||
var localPos = SceneryGenerator.DisplaceObject(obj, gx, gy, 0);
|
||
// Vertex (3, 8): cell corner at (3*24, 8*24) = (72, 192)
|
||
float lx = 3 * 24f + localPos.X;
|
||
float ly = 8 * 24f + localPos.Y;
|
||
if (ly >= 0 && ly < 192f && lx >= 0 && lx < 192f)
|
||
foundValid = true;
|
||
}
|
||
}
|
||
|
||
Assert.True(foundValid,
|
||
"Expected at least one (globalCellX, globalCellY) where vertex y=8 " +
|
||
"displaces back into [0, 192) — retail's 9×9 loop relies on this");
|
||
}
|
||
|
||
[Fact]
|
||
public void DisplaceObject_InteriorVertex_AlwaysNearOrigin()
|
||
{
|
||
var obj = new ObjectDesc
|
||
{
|
||
DisplaceX = 12f,
|
||
DisplaceY = 12f,
|
||
BaseLoc = new Frame { Origin = new Vector3(0, 0, 0) }
|
||
};
|
||
|
||
// For interior vertices (x < 8, y < 8), displacement is bounded by
|
||
// DisplaceX/Y (max 12 units each), so the result stays within one
|
||
// cell of the origin.
|
||
var localPos = SceneryGenerator.DisplaceObject(obj, 100, 100, 0);
|
||
Assert.True(Math.Abs(localPos.X) <= 12f,
|
||
$"Interior displacement X={localPos.X} exceeds DisplaceX=12");
|
||
Assert.True(Math.Abs(localPos.Y) <= 12f,
|
||
$"Interior displacement Y={localPos.Y} exceeds DisplaceY=12");
|
||
}
|
||
}
|