phase(N.1): delete legacy scenery code path; WB is the only path
Phase N.1 step 8 (final code cleanup): now that ACDREAM_USE_WB_SCENERY
has been default-on (commit b84ecbd), remove the legacy in-line
algorithms so we don't accumulate dead-code drift.
Deleted:
- SceneryGenerator.UseWbScenery (feature flag)
- SceneryGenerator.IsOnRoad / DisplaceObject / RoadHalfWidth (legacy
ports — Generate used to call them)
- The legacy in-line implementation in Generate()
- SceneryGeneratorTests.DisplaceObject_* (test the deleted method)
- SceneryWbConformanceTests.cs entirely (purpose served — proved
equivalence pre-migration; would compare WB to WB after delete)
Renamed:
- GenerateViaWb -> GenerateInternal (it's the only path now)
Kept:
- Public IsRoadVertex predicate (small surface, useful)
- WbSceneryAdapter (consumed by GenerateInternal)
- All WbSceneryAdapterTests (still cover the adapter)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b84ecbda51
commit
b0ec6deb50
3 changed files with 24 additions and 615 deletions
|
|
@ -1,13 +1,14 @@
|
|||
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.
|
||||
/// Tests for SceneryGenerator. As of Phase N.1 (commit b84ecbd / Task 8 final
|
||||
/// commit), the displacement / road / slope / rotation / scale algorithms run
|
||||
/// through WorldBuilder's helpers (SceneryHelpers + TerrainUtils). The only
|
||||
/// our-side code remaining is the small <see cref="SceneryGenerator.IsRoadVertex"/>
|
||||
/// predicate, which is what these tests cover.
|
||||
/// </summary>
|
||||
public class SceneryGeneratorTests
|
||||
{
|
||||
|
|
@ -47,63 +48,4 @@ public class SceneryGeneratorTests
|
|||
$"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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,176 +0,0 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
using WB_TerrainUtils = WorldBuilder.Shared.Modules.Landscape.Lib.TerrainUtils;
|
||||
using WB_SceneryHelpers = Chorizite.OpenGLSDLBackend.Lib.SceneryHelpers;
|
||||
|
||||
namespace AcDream.Core.Tests.World;
|
||||
|
||||
/// <summary>
|
||||
/// Phase N.1 helper-level conformance tests. Each test compares an algorithm
|
||||
/// in our existing <see cref="SceneryGenerator"/> path against WorldBuilder's
|
||||
/// equivalent for representative inputs. Passing tests are empirical evidence
|
||||
/// that swapping our inline logic for WB's helpers is behavior-preserving.
|
||||
///
|
||||
/// Inputs exercise both typical vertices (gx=100, gy=100, j=0) and edge
|
||||
/// vertices at y=8 specifically (Issue #49 territory).
|
||||
///
|
||||
/// IMPORTANT: rotation (RotateObj) is intentionally NOT conformance-tested.
|
||||
/// During Phase N.1 design we discovered our acdream port of retail's
|
||||
/// AFrame::set_heading uses a shortcut formula `yawDeg = -(450-degrees)%360`
|
||||
/// that does NOT match retail's actual behavior. Retail goes through an
|
||||
/// atan2 round-trip in Frame::set_vector_heading (named-retail symbol
|
||||
/// 0x00535db0); WorldBuilder ports that round-trip faithfully. Our code
|
||||
/// produces rotations ~180° off from retail/WB. This bug has been visually
|
||||
/// undetectable because per-tree rotation noise masks the constant offset.
|
||||
/// Phase N.1's migration to WB.SceneryHelpers.RotateObj fixes it. Adding a
|
||||
/// conformance test for rotation here would fail forever — it's the bug
|
||||
/// the migration is meant to close, not something to preserve.
|
||||
/// </summary>
|
||||
public class SceneryWbConformanceTests
|
||||
{
|
||||
private static ObjectDesc MakeObj(
|
||||
float displaceX = 12f,
|
||||
float displaceY = 12f,
|
||||
float minScale = 1f,
|
||||
float maxScale = 1f,
|
||||
float maxRotation = 0f,
|
||||
float minSlope = 0f,
|
||||
float maxSlope = 1f,
|
||||
int align = 0)
|
||||
{
|
||||
return new ObjectDesc
|
||||
{
|
||||
ObjectId = 0x02000258u,
|
||||
DisplaceX = displaceX,
|
||||
DisplaceY = displaceY,
|
||||
MinScale = minScale,
|
||||
MaxScale = maxScale,
|
||||
MaxRotation = maxRotation,
|
||||
MinSlope = minSlope,
|
||||
MaxSlope = maxSlope,
|
||||
Align = align,
|
||||
BaseLoc = new Frame { Origin = new Vector3(0, 0, 0) },
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Our DisplaceObject ↔ WB's SceneryHelpers.Displace must produce the
|
||||
/// same Vector3 for the same (obj, ix, iy, iq).
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(100u, 100u, 0u)] // typical
|
||||
[InlineData( 50u, 50u, 1u)] // typical, j=1
|
||||
[InlineData( 4u, 8u, 0u)] // edge vertex y=8
|
||||
[InlineData( 8u, 4u, 0u)] // edge vertex x=8
|
||||
public void Displace_OursMatchesWb(uint ix, uint iy, uint iq)
|
||||
{
|
||||
var obj = MakeObj();
|
||||
var ours = SceneryGenerator.DisplaceObject(obj, ix, iy, iq);
|
||||
var wb = WB_SceneryHelpers.Displace(obj, ix, iy, iq);
|
||||
|
||||
Assert.Equal(ours.X, wb.X, precision: 4);
|
||||
Assert.Equal(ours.Y, wb.Y, precision: 4);
|
||||
Assert.Equal(ours.Z, wb.Z, precision: 4);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Our IsOnRoad ↔ WB's TerrainUtils.OnRoad must produce the same bool
|
||||
/// for the same (lx, ly) when the underlying terrain bits match.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData( 12.0f, 12.0f)] // cell (0,0) center
|
||||
[InlineData( 85.08f, 190.97f)] // the 0xA9B1 edge-vertex bug location
|
||||
[InlineData( 3.0f, 3.0f)] // near a road if r0 is set
|
||||
[InlineData( 23.5f, 12.0f)] // edge of cell, between cells
|
||||
public void OnRoad_OursMatchesWb_DiagonalRoad(float lx, float ly)
|
||||
{
|
||||
// Build a synthetic LandBlock with road bits at SW (0,0) and NE (1,1)
|
||||
// of cell (0,0) — the diagonal pattern we saw at 0xA9B1.
|
||||
var block = new LandBlock();
|
||||
// road bit at vertex (0,0) — index 0*9+0 = 0
|
||||
block.Terrain[0] = (TerrainInfo)0x0003; // road=3
|
||||
// road bit at vertex (1,1) — index 1*9+1 = 10
|
||||
block.Terrain[10] = (TerrainInfo)0x0003;
|
||||
|
||||
bool ours = SceneryGenerator.IsOnRoad(block, lx, ly);
|
||||
|
||||
var entries = WbSceneryAdapter.BuildTerrainEntries(block);
|
||||
bool wb = WB_TerrainUtils.OnRoad(new Vector3(lx, ly, 0), entries);
|
||||
|
||||
Assert.Equal(ours, wb);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Our SampleNormalZFromHeightmap ↔ WB's TerrainUtils.GetNormal(...).Z
|
||||
/// must produce the same Z for representative slope inputs.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData( 12.0f, 12.0f)] // cell center
|
||||
[InlineData( 85.08f, 190.97f)] // the 0xA9B1 edge-vertex location
|
||||
[InlineData( 3.0f, 188.0f)] // near a y-edge
|
||||
public void GetNormalZ_OursMatchesWb_LinearTable(float lx, float ly)
|
||||
{
|
||||
// Heightmap with non-flat terrain so normals are non-trivial.
|
||||
var heights = new byte[81];
|
||||
for (int x = 0; x < 9; x++)
|
||||
for (int y = 0; y < 9; y++)
|
||||
heights[x * 9 + y] = (byte)((x * 17 + y * 13) % 256);
|
||||
|
||||
var heightTable = new float[256];
|
||||
for (int i = 0; i < 256; i++) heightTable[i] = i * 1.0f;
|
||||
|
||||
const uint lbX = 0xA9, lbY = 0xB1;
|
||||
|
||||
// Build a Region-shaped object with the LandHeightTable populated.
|
||||
var region = new Region();
|
||||
region.LandDefs = new LandDefs();
|
||||
region.LandDefs.LandHeightTable = heightTable;
|
||||
|
||||
var block = new LandBlock();
|
||||
// Copy heights into block.Height (LandBlock self-initializes Height to byte[81]).
|
||||
for (int i = 0; i < 81; i++) block.Height[i] = heights[i];
|
||||
var entries = WbSceneryAdapter.BuildTerrainEntries(block);
|
||||
|
||||
float ours = AcDream.Core.Physics.TerrainSurface.SampleNormalZFromHeightmap(
|
||||
heights, heightTable, lbX, lbY, lx, ly);
|
||||
float wb = WB_TerrainUtils.GetNormal(region, entries, lbX, lbY,
|
||||
new Vector3(lx, ly, 0)).Z;
|
||||
|
||||
Assert.Equal(ours, wb, precision: 4);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Our inline scale logic ↔ WB's SceneryHelpers.ScaleObj must produce
|
||||
/// the same float for representative inputs.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(100u, 100u, 0u, 0.5f, 1.5f)]
|
||||
[InlineData( 4u, 8u, 0u, 1.0f, 1.0f)]
|
||||
[InlineData(200u, 250u, 1u, 0.8f, 1.2f)]
|
||||
public void ScaleObj_OursMatchesWb(uint gx, uint gy, uint j, float minScale, float maxScale)
|
||||
{
|
||||
var obj = MakeObj(minScale: minScale, maxScale: maxScale);
|
||||
|
||||
// Our inline logic from SceneryGenerator.Generate (~lines 236-247):
|
||||
float ours;
|
||||
if (obj.MinScale == obj.MaxScale)
|
||||
{
|
||||
ours = obj.MaxScale;
|
||||
}
|
||||
else
|
||||
{
|
||||
double scaleNoise = unchecked((uint)(1813693831u * gy
|
||||
- (j + 32593u) * (1360117743u * gy * gx + 1888038839u)
|
||||
- 1109124029u * gx)) * 2.3283064e-10;
|
||||
ours = (float)(Math.Pow(obj.MaxScale / obj.MinScale, scaleNoise) * obj.MinScale);
|
||||
}
|
||||
if (ours <= 0) ours = 1f;
|
||||
|
||||
float wb = WB_SceneryHelpers.ScaleObj(obj, gx, gy, j);
|
||||
if (wb <= 0) wb = 1f;
|
||||
|
||||
Assert.Equal(ours, wb, precision: 4);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue