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>
196 lines
8.9 KiB
C#
196 lines
8.9 KiB
C#
using System.Numerics;
|
|
using Chorizite.OpenGLSDLBackend.Lib;
|
|
using DatReaderWriter;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Types;
|
|
using WorldBuilder.Shared.Modules.Landscape.Lib;
|
|
|
|
namespace AcDream.Core.World;
|
|
|
|
/// <summary>
|
|
/// Procedural scenery placement for a landblock. AC encodes "sparse decoration"
|
|
/// (trees, bushes, rocks, fences, small props) NOT as explicit Stab entries on
|
|
/// the LandBlockInfo but as per-terrain-vertex Scene/ObjectDesc references in
|
|
/// the Region dat, placed pseudo-randomly via deterministic LCG math keyed on
|
|
/// the vertex's global cell coordinates. Without this generator, any landblock
|
|
/// rendered from dats is missing all of its natural scenery.
|
|
///
|
|
/// Algorithm verified against the decompiled retail acclient.exe (Ghidra output):
|
|
/// - Scene-selection hash: chunk_00530000.c line 1144
|
|
/// - Per-object frequency: chunk_00530000.c lines 1168-1174
|
|
/// - Displacement formula: chunk_005A0000.c lines 4858-4878 (FUN_005a6cc0)
|
|
/// - Quadrant rotation: chunk_005A0000.c lines 4880-4902
|
|
/// - Object rotation hash: chunk_005A0000.c lines 4924-4926 (FUN_005a6e60)
|
|
/// - Object scale: ACViewer Physics/Common/ObjectDesc.cs ScaleObj()
|
|
/// (scale hash constant 0x7f51=32593 not in dumped chunks;
|
|
/// confirmed against ACViewer which matches all other constants)
|
|
///
|
|
/// Phase N.1 (2026-05-08): migrated all algorithm calls to WorldBuilder's
|
|
/// <c>SceneryHelpers</c> + <c>TerrainUtils</c>. The legacy in-line implementations
|
|
/// have been removed; <c>WbSceneryAdapter</c> bridges <c>LandBlock</c> data to WB's
|
|
/// <c>TerrainEntry[]</c>. See
|
|
/// <c>docs/superpowers/specs/2026-05-08-phase-n1-scenery-via-wb-helpers-design.md</c>.
|
|
/// </summary>
|
|
public static class SceneryGenerator
|
|
{
|
|
// AC landblock geometry — matches LandblockMesh.
|
|
private const int VerticesPerSide = 9;
|
|
private const float CellSize = 24.0f;
|
|
private const float LandblockSize = 192.0f; // 8 cells * 24 units
|
|
private const int CellsPerSide = 8;
|
|
|
|
public readonly record struct ScenerySpawn(
|
|
uint ObjectId, // GfxObj or Setup id
|
|
Vector3 LocalPosition, // landblock-local world units
|
|
Quaternion Rotation,
|
|
float Scale);
|
|
|
|
/// <summary>
|
|
/// Generate all scenery entries for one landblock. Phase N.1 migrated this
|
|
/// to call WorldBuilder's <c>SceneryHelpers</c> + <c>TerrainUtils</c>;
|
|
/// see <c>docs/superpowers/specs/2026-05-08-phase-n1-scenery-via-wb-helpers-design.md</c>.
|
|
/// </summary>
|
|
public static IReadOnlyList<ScenerySpawn> Generate(
|
|
DatCollection dats,
|
|
Region region,
|
|
LandBlock block,
|
|
uint landblockId,
|
|
HashSet<int>? buildingCells = null,
|
|
float[]? heightTable = null)
|
|
{
|
|
// heightTable kept for backward compat; WB path uses
|
|
// region.LandDefs.LandHeightTable internally via TerrainUtils.GetNormal.
|
|
_ = heightTable;
|
|
return GenerateInternal(dats, region, block, landblockId, buildingCells);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the raw terrain word indicates a road vertex.
|
|
/// Bits 0-1 of the terrain word encode the road type; any non-zero value
|
|
/// means the vertex is on a road. Ported from ACViewer GetRoad().
|
|
/// </summary>
|
|
public static bool IsRoadVertex(ushort raw) => (raw & 0x3u) != 0;
|
|
|
|
private static IReadOnlyList<ScenerySpawn> GenerateInternal(
|
|
DatCollection dats,
|
|
Region region,
|
|
LandBlock block,
|
|
uint landblockId,
|
|
HashSet<int>? buildingCells)
|
|
{
|
|
var result = new List<ScenerySpawn>();
|
|
|
|
if (region.TerrainInfo?.TerrainTypes is null || region.SceneInfo?.SceneTypes is null)
|
|
return result;
|
|
|
|
// Build the TerrainEntry[] WB's helpers consume — once per landblock.
|
|
var terrainEntries = WbSceneryAdapter.BuildTerrainEntries(block);
|
|
|
|
uint blockX = (landblockId >> 24) * 8;
|
|
uint blockY = ((landblockId >> 16) & 0xFFu) * 8;
|
|
uint lbX = landblockId >> 24;
|
|
uint lbY = (landblockId >> 16) & 0xFFu;
|
|
|
|
for (int x = 0; x < VerticesPerSide; x++)
|
|
{
|
|
for (int y = 0; y < VerticesPerSide; y++)
|
|
{
|
|
int i = x * VerticesPerSide + y;
|
|
ushort raw = block.Terrain[i];
|
|
|
|
uint terrainType = (uint)((raw >> 2) & 0x1F);
|
|
uint sceneType = (uint)((raw >> 11) & 0x1F);
|
|
|
|
if (terrainType >= region.TerrainInfo.TerrainTypes.Count) continue;
|
|
var sceneTypeList = region.TerrainInfo.TerrainTypes[(int)terrainType].SceneTypes;
|
|
if (sceneType >= sceneTypeList.Count) continue;
|
|
|
|
uint sceneInfo = sceneTypeList[(int)sceneType];
|
|
if (sceneInfo >= region.SceneInfo.SceneTypes.Count) continue;
|
|
|
|
var scenes = region.SceneInfo.SceneTypes[(int)sceneInfo].Scenes;
|
|
if (scenes.Count == 0) continue;
|
|
|
|
uint cellX = (uint)x;
|
|
uint cellY = (uint)y;
|
|
uint globalCellX = cellX + blockX;
|
|
uint globalCellY = cellY + blockY;
|
|
|
|
// Scene-selection hash: identical to Generate.
|
|
uint cellMat = globalCellY * (712977289u * globalCellX + 1813693831u)
|
|
- 1109124029u * globalCellX + 2139937281u;
|
|
double offset = cellMat * 2.3283064e-10;
|
|
int sceneIdx = (int)(scenes.Count * offset);
|
|
if (sceneIdx >= scenes.Count || sceneIdx < 0) sceneIdx = 0;
|
|
|
|
uint sceneId = (uint)scenes[sceneIdx];
|
|
var scene = dats.Get<Scene>(sceneId);
|
|
if (scene is null) continue;
|
|
|
|
// Per-object frequency setup: identical to Generate.
|
|
uint cellXMat = unchecked(0u - 1109124029u * globalCellX);
|
|
uint cellYMat = 1813693831u * globalCellY;
|
|
uint cellMat2 = 1360117743u * globalCellX * globalCellY + 1888038839u;
|
|
|
|
for (uint j = 0; j < scene.Objects.Count; j++)
|
|
{
|
|
var obj = scene.Objects[(int)j];
|
|
if (obj.WeenieObj != 0) continue;
|
|
|
|
double noise = unchecked((uint)(cellXMat + cellYMat - cellMat2 * (23399u + j))) * 2.3283064e-10;
|
|
if (noise >= obj.Frequency) continue;
|
|
|
|
// ─── WB substitution: displacement ───────────────────
|
|
var localPos = SceneryHelpers.Displace(obj, globalCellX, globalCellY, j);
|
|
|
|
float lx = cellX * CellSize + localPos.X;
|
|
float ly = cellY * CellSize + localPos.Y;
|
|
|
|
if (lx < 0 || ly < 0 || lx >= LandblockSize || ly >= LandblockSize)
|
|
continue;
|
|
|
|
// ─── WB substitution: road check ──────────────────────
|
|
if (TerrainUtils.OnRoad(new Vector3(lx, ly, 0), terrainEntries))
|
|
continue;
|
|
|
|
// Building check: identical to Generate.
|
|
if (buildingCells is not null)
|
|
{
|
|
int dcx = Math.Clamp((int)(lx / CellSize), 0, CellsPerSide - 1);
|
|
int dcy = Math.Clamp((int)(ly / CellSize), 0, CellsPerSide - 1);
|
|
if (buildingCells.Contains(dcx * VerticesPerSide + dcy))
|
|
continue;
|
|
}
|
|
|
|
// ─── WB substitution: slope check ─────────────────────
|
|
Vector3 normal = TerrainUtils.GetNormal(
|
|
region, terrainEntries, lbX, lbY,
|
|
new Vector3(lx, ly, 0));
|
|
if (!SceneryHelpers.CheckSlope(obj, normal.Z))
|
|
continue;
|
|
|
|
float lz = obj.BaseLoc.Origin.Z;
|
|
|
|
// ─── WB substitution: rotation ────────────────────────
|
|
Quaternion rotation;
|
|
if (obj.Align != 0)
|
|
rotation = SceneryHelpers.ObjAlign(obj, normal, lz, localPos);
|
|
else
|
|
rotation = SceneryHelpers.RotateObj(obj, globalCellX, globalCellY, j, localPos);
|
|
|
|
// ─── WB substitution: scale ───────────────────────────
|
|
float scale = SceneryHelpers.ScaleObj(obj, globalCellX, globalCellY, j);
|
|
if (scale <= 0) scale = 1f;
|
|
|
|
result.Add(new ScenerySpawn(
|
|
ObjectId: obj.ObjectId,
|
|
LocalPosition: new Vector3(lx, ly, lz),
|
|
Rotation: rotation,
|
|
Scale: scale));
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|