using System.Numerics; using AcDream.Core.World; using DatReaderWriter.Types; namespace AcDream.Core.Tests.World; /// /// 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. /// 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"); } }