diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 73ccb1c..2b9bfb0 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -4284,6 +4284,36 @@ public sealed class GameWindow : IDisposable } } + // L-fix3 (2026-04-28): retail "decorative / phantom" detection. + // A Setup is phantom (no collision) when it has NO CylSpheres, + // NO Spheres, AND zero overall Radius. Small plants, grass + // tufts, flowers, ground-cover bushes all match — retail + // ships them with empty collision arrays so the player walks + // through them. Without this gate the mesh-bounds fallback + // below assigns every plant a 0.3 m+ collision cylinder + // (line ~4409 clamp) and they block the player. + // + // The gate is layered AFTER the Setup CylSphere / BSP + // registrations above (which are no-ops for phantom Setups + // anyway), so non-phantom scenery (trees with real + // CylSpheres or canopy-only BSPs) still gets the + // mesh-bounds fallback. The check is on entity.SourceGfxObjOrSetupId + // to look up the cached Setup; if it's a raw GfxObj + // (0x010xxxxx, no Setup metadata) we keep the old fallback + // behaviour because GfxObjs don't expose phantom intent. + bool isPhantomSetup = false; + if ((entity.SourceGfxObjOrSetupId & 0xFF000000u) == 0x02000000u) + { + var setupInfoForPhantom = _physicsDataCache.GetSetup(entity.SourceGfxObjOrSetupId); + if (setupInfoForPhantom is not null + && setupInfoForPhantom.CylSpheres.Count == 0 + && setupInfoForPhantom.Spheres.Count == 0 + && setupInfoForPhantom.Radius <= 0.0001f) + { + isPhantomSetup = true; + } + } + // VISUAL mesh-bounds collision: for SCENERY entities (IDs with // 0x80000000 bit set, indicating procedurally-placed scenery), // ALWAYS compute a cylinder from the world-space mesh AABB. @@ -4294,7 +4324,12 @@ public sealed class GameWindow : IDisposable // For stabs (low IDs) and live entities, keep the existing Setup // CylSphere / BSP registrations — those are placed with precise // frame data and don't have the scenery offset issue. - if ((_isOutdoorMesh || (entityBsp == 0 && entityCyl == 0)) && entity.MeshRefs.Count > 0) + // + // L-fix3: skip entirely when the Setup is phantom — retail + // decorative meshes have no collision data on purpose. + if (!isPhantomSetup + && (_isOutdoorMesh || (entityBsp == 0 && entityCyl == 0)) + && entity.MeshRefs.Count > 0) { float entScale = entity.Scale > 0f ? entity.Scale : 1f; bool haveBounds = false;