fix(physics): skip collision registration for phantom-Setup scenery (small plants / grass)

User asked how AC differentiates collidable vs phantom scenery.
The retail signal is at the Setup level: a Setup with NO
CylSpheres, NO Spheres, AND zero overall Radius is decorative
-- the player walks through it. retail header
docs/research/named-retail/acclient.h enum PhysicsState
includes ETHEREAL_PS = 0x4 / IGNORE_COLLISIONS_PS = 0x10 for
runtime-toggled flags, but the static signal for scenery is
the empty Setup collision arrays.

Pre-fix the mesh-bounds-fallback at
GameWindow.cs:4297-4429 ran on every outdoor scenery entity
regardless of Setup intent, then clamped the resulting
cylinder radius to >= 0.3 m. So small plants/grass got a
0.3 m collision cylinder and blocked the player even though
the Setup explicitly said no collision.

Fix: before the mesh-bounds fallback, check the cached Setup.
If it's a Setup-typed object (0x020xxxxx) AND CylSpheres /
Spheres / Radius are all empty/zero, mark it phantom and
skip the collision registration entirely. Non-phantom
scenery (trees with real CylSpheres or canopy-only BSPs)
still gets the mesh-bounds fallback so the player walks
under canopies but bumps into trunks. Raw GfxObjs
(0x010xxxxx, no Setup metadata) keep the old fallback
behaviour because they don't expose phantom intent.

Tests stay 1439 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-29 11:31:46 +02:00
parent 46544ef3c1
commit 90aa74a3cb

View file

@ -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;