fix(physics): pass cell world-transform to indoor BSP collision

Indoor cell BSP queries at TransitionTypes.cs:1442 were calling
BSPQuery.FindCollisions with Quaternion.Identity + defaulted
Vector3.Zero worldOrigin. Inside the BSP, Path 3 (step_sphere_down)
and Path 4 (land-on-surface) use those params to build the
world-space ContactPlane. Result: planes written with D ~ 0 instead
of the cell's world floor Z (e.g. -94.02 for Holtburg cottages).
320 corrupt CP writes per Holtburg session per the [cp-write] probe.

Fix: decompose cellPhysics.WorldTransform once at the call site,
pass the rotation as localToWorld and the translation as
worldOrigin. Mirrors the existing correct pattern at :1808
(FindObjCollisions, passes obj.Rotation + obj.Position).

Retail oracle: BSPTREE::find_collisions (acclient_2013_pseudo_c.txt:323924)
calls Plane::localtoglobal at :323921 before set_contact_plane.
Our TransformNormal + TransformVertices + BuildWorldPlane chain is
the equivalent — it just needs the right rotation + origin.

Spec: docs/superpowers/specs/2026-05-20-indoor-bsp-worldorigin-fix-design.md.
Plan: docs/superpowers/plans/2026-05-20-indoor-bsp-worldorigin-fix.md.
Evidence: launch-cp-probe.log capture 2026-05-20, [cp-write] probe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-20 08:00:16 +02:00
parent 39d4e6512b
commit de8ffde4ca

View file

@ -1439,6 +1439,38 @@ public sealed class Transition
// Use the full 6-path BSP dispatcher for retail-faithful collision. // Use the full 6-path BSP dispatcher for retail-faithful collision.
// Use pre-resolved polygons (vertices+planes computed at cache time). // Use pre-resolved polygons (vertices+planes computed at cache time).
//
// 2026-05-20 (Bug B fix): pass the cell's world rotation +
// translation so the BSP's internal find_walkable /
// step_sphere_down / Path-4 land write world-space ContactPlanes
// (NOT cell-local). The prior call passed Quaternion.Identity
// and defaulted worldOrigin = Vector3.Zero — which corrupted
// every Path 3 + Path 4 CP write inside an indoor cell with
// D ≈ 0 instead of D = -world_floor_Z. Mirrors the existing
// correct pattern at the FindObjCollisions call site (~line
// 1808: passes obj.Rotation + worldOrigin: obj.Position).
//
// Retail oracle: BSPTREE::find_collisions (decomp
// acclient_2013_pseudo_c.txt:323924) calls Plane::localtoglobal
// (:323921) before set_contact_plane. Our TransformNormal +
// TransformVertices + BuildWorldPlane chain is the equivalent
// — it just needs the right rotation + origin.
Quaternion cellRotation;
Vector3 cellOrigin;
if (!Matrix4x4.Decompose(cellPhysics.WorldTransform, out _, out cellRotation, out cellOrigin))
{
// Matrix has shear or other non-decomposable parts. EnvCell
// WorldTransform is always rigid rotation + translation per
// the dat format, so this branch should never fire. Log a
// warning + fall back to identity rotation + .Translation
// so we degrade to "translation-only" instead of the prior
// "both broken".
Console.WriteLine(System.FormattableString.Invariant(
$"[indoor-bsp] WARN cellPhysics.WorldTransform did not decompose cleanly for cell 0x{sp.CheckCellId:X8} — falling back to identity rotation"));
cellRotation = Quaternion.Identity;
cellOrigin = cellPhysics.WorldTransform.Translation;
}
var cellState = BSPQuery.FindCollisions( var cellState = BSPQuery.FindCollisions(
cellPhysics.BSP.Root, cellPhysics.BSP.Root,
cellPhysics.Resolved, cellPhysics.Resolved,
@ -1448,8 +1480,9 @@ public sealed class Transition
localCurrCenter, localCurrCenter,
Vector3.UnitZ, // local space Z is up Vector3.UnitZ, // local space Z is up
1.0f, // scale = 1.0 for cell geometry 1.0f, // scale = 1.0 for cell geometry
Quaternion.Identity, cellRotation,
engine); // engine needed for Path 5 step-up engine, // engine needed for Path 5 step-up
worldOrigin: cellOrigin);
if (PhysicsDiagnostics.ProbeIndoorBspEnabled) if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
{ {