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; /// /// 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 /// SceneryHelpers + TerrainUtils. The legacy in-line implementations /// have been removed; WbSceneryAdapter bridges LandBlock data to WB's /// TerrainEntry[]. See /// docs/superpowers/specs/2026-05-08-phase-n1-scenery-via-wb-helpers-design.md. /// 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); /// /// Generate all scenery entries for one landblock. Phase N.1 migrated this /// to call WorldBuilder's SceneryHelpers + TerrainUtils; /// see docs/superpowers/specs/2026-05-08-phase-n1-scenery-via-wb-helpers-design.md. /// public static IReadOnlyList Generate( DatCollection dats, Region region, LandBlock block, uint landblockId, HashSet? 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); } /// /// 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(). /// public static bool IsRoadVertex(ushort raw) => (raw & 0x3u) != 0; private static IReadOnlyList GenerateInternal( DatCollection dats, Region region, LandBlock block, uint landblockId, HashSet? buildingCells) { var result = new List(); 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(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; } }