diff --git a/src/AcDream.Core/World/SceneryGenerator.cs b/src/AcDream.Core/World/SceneryGenerator.cs index 610d4fe..d4ae229 100644 --- a/src/AcDream.Core/World/SceneryGenerator.cs +++ b/src/AcDream.Core/World/SceneryGenerator.cs @@ -59,6 +59,7 @@ public static class SceneryGenerator float[]? heightTable = null) { var result = new List(); + var sortingRadiusCache = new Dictionary(); if (region.TerrainInfo?.TerrainTypes is null || region.SceneInfo?.SceneTypes is null) return result; @@ -182,6 +183,15 @@ public static class SceneryGenerator if (nz < obj.MinSlope || nz > obj.MaxSlope) continue; } + // Retail obj_within_block check: the model's sorting sphere must + // fit entirely within the landblock bounds. This rejects edge-vertex + // spawns whose bounding sphere would extend past the boundary. + // Named-retail: CPhysicsObj::obj_within_block (0x00461c30), called + // inside CLandBlock::get_land_scenes after find_terrain_poly/CheckSlope. + float sortRadius = GetSortingSphereRadius(dats, obj.ObjectId, sortingRadiusCache); + if (!IsWithinBlock(lx, ly, sortRadius)) + continue; + // BaseLoc.Z offset: scenery-specific vertical offset from // the ground (e.g., flowers planted at -0.1m so they // don't float above grass). The renderer adds groundZ @@ -262,20 +272,10 @@ public static class SceneryGenerator /// private const float RoadHalfWidth = 5.0f; - /// - /// Retail-faithful post-displacement road test. Ported from ACViewer - /// Landblock.OnRoad (Physics/Common/Landblock.cs lines 300-398), which is - /// a direct port of FUN_00530d30 in the retail client. - /// - /// Examines the 4 corners of the cell containing (lx, ly) and, depending - /// on how many are road vertices (0, 1, 2, 3, or 4), applies a polygonal - /// test using the 5-unit road half-width to check if (lx, ly) lies on the - /// road ribbon. Returns true if the point is on a road. - /// /// /// Retail-faithful road ribbon test — direct port of ACViewer's /// Landblock.OnRoad (Physics/Common/Landblock.cs lines 300-398), which - /// itself is a port of FUN_00530d30 in acclient.exe. + /// itself is a port of CLandBlock::on_road (named-retail 0x0052FFF0). /// /// Classifies the 4 corners of the cell containing (lx, ly) by road type /// (bits 0-1 of the terrain word) and applies a different geometric test @@ -370,6 +370,54 @@ public static class SceneryGenerator private const int CellsPerSide = 8; + /// + /// Retail CPhysicsObj::obj_within_block check — verifies the object's sorting + /// sphere stays entirely within the landblock bounds. Returns true if the object + /// is within bounds. + /// + /// Named-retail: CPhysicsObj::obj_within_block (0x00461c30). + /// ACViewer: PhysicsObj.obj_within_block. + /// + /// Retail loads the full PhysicsObj, transforms the sorting sphere center via + /// LocalToGlobal, then checks center ± radius against [0, BlockLength]. + /// We approximate by ignoring the sorting sphere center offset (typically small + /// for scenery) and checking the spawn position directly against [radius, 192-radius]. + /// + private static bool IsWithinBlock(float lx, float ly, float sortingSphereRadius) + { + if (sortingSphereRadius <= 0f) return true; + return lx >= sortingSphereRadius && ly >= sortingSphereRadius + && lx < LandblockSize - sortingSphereRadius + && ly < LandblockSize - sortingSphereRadius; + } + + /// + /// Gets the sorting sphere radius for a scenery object, with caching. + /// Setup objects (0x02xxxxxx) use Setup.SortingSphere.Radius. + /// GfxObj objects (0x01xxxxxx) fall back to 0 (allows placement; these are + /// typically small items like grass patches whose BSP sphere is tiny). + /// + private static float GetSortingSphereRadius( + DatCollection dats, uint objectId, Dictionary cache) + { + if (cache.TryGetValue(objectId, out float cached)) + return cached; + + float radius = 0f; + if ((objectId >> 24) == 0x02) + { + // Setup — has an explicit SortingSphere + var setup = dats.Get(objectId); + if (setup?.SortingSphere is { } sphere) + radius = sphere.Radius; + } + // GfxObj (0x01) — no Setup.SortingSphere; use 0 (permissive). + // These are typically small single-mesh scenery items. + + cache[objectId] = radius; + return radius; + } + /// /// Pseudo-random displacement within a cell for a scenery object. Returns a /// Vector3 in local cell-offset space (the caller adds it to the cell corner