fix(phys L.2d slice 1.5): probe captures hit poly under StepSphereUp recursion

First Holtburg-doorway capture showed all 191 [resolve-bldg] entries
labeled "n/a (cylinder)" — including hits attributed to the building
0xA9B47900 which [entity-source] confirmed was registered as type=BSP.
The label was a probe bug, not a real cylinder route.

Root cause: BSPQuery's grounded-path (Path 5) returns early via
`StepSphereUp(transition, worldNormal, engine)` when no step is already
in progress. The slice-1 side-channel write at line 1546 came AFTER
that early return, so it never fired for the dominant grounded-player
case. Compounding: StepSphereUp recurses into ResolveWithTransition →
FindObjCollisions, whose per-entity `LastBspHitPoly = null` clear
wiped any earlier write before the outer attribution emitter read it.

Fix:
1. BSPQuery Path 5: move LastBspHitPoly write to the top of
   `if (hit0 || hitPoly0 != null)` blocks (both foot- and head-sphere),
   BEFORE the StepSphereUp early return. Recursion-safe — the inner
   resolve's BSP writes will overwrite with the inner entity's poly,
   but for the dominant case (same wall hit on both outer and inner)
   that's still the correct attribution.
2. TransitionTypes.FindObjCollisions: drop the per-entity clear of
   LastBspHitPoly. With BSPQuery now writing at hit-detection time
   instead of response-computation time, the side-channel value is
   reliable without per-iteration zeroing.
3. TransitionTypes [resolve-bldg] emission: key the "n/a (cylinder)"
   label on `obj.CollisionType` directly, not on LastBspHitPoly being
   null. A BSP entity with a null poly now logs "n/a (BSP path —
   side-channel not written, missing BSPQuery wire site)" so any
   future BSPQuery path that's missing the wire is visible in the
   trace rather than being silently mis-labeled.

Verified: build green, the 2 slice-1 tests still pass, 8 pre-existing
failures unchanged.

Spec: docs/superpowers/specs/2026-05-13-l2d-cbuildingobj-collision-design.md
First capture (showing the label bug): launch-l2d-slice1.log lines
12086-12120 (representative [resolve-bldg] entries for obj=0xA9B47900).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-12 19:25:32 +02:00
parent 66dc23e087
commit 8bacef0598
2 changed files with 34 additions and 13 deletions

View file

@ -1544,6 +1544,16 @@ public static class BSPQuery
if (hit0 || hitPoly0 is not null)
{
// L.2d slice 1.5 (2026-05-13): record the hit poly EARLY,
// before the StepSphereUp branch can recurse into
// ResolveWithTransition → FindObjCollisions and clobber the
// side-channel via the inner call's per-resolve clear. Path 5
// is the dominant grounded-player path; without this the
// probe's [resolve-bldg] line for every grounded BSP hit was
// mis-labeled as "n/a (cylinder)".
if (PhysicsDiagnostics.ProbeBuildingEnabled)
PhysicsDiagnostics.LastBspHitPoly = hitPoly0;
var worldNormal = L2W(hitPoly0!.Plane.Normal);
// L.2.3b (2026-04-29): recursion guard. Retail
// (acclient_2013_pseudo_c.txt:272954) gates step_sphere_up on
@ -1559,9 +1569,6 @@ public static class BSPQuery
// back to wall-slide so the inner sphere doesn't recurse.
collisions.SetCollisionNormal(worldNormal);
collisions.SetSlidingNormal(worldNormal);
// L.2d slice 1 (2026-05-13): diagnostic side-channel.
if (PhysicsDiagnostics.ProbeBuildingEnabled)
PhysicsDiagnostics.LastBspHitPoly = hitPoly0;
return TransitionState.Slid;
}
@ -1575,6 +1582,12 @@ public static class BSPQuery
if (hit1 || hitPoly1 is not null)
{
// L.2d slice 1.5 (2026-05-13): same early-record as foot
// sphere — head-sphere wall hits also recurse via
// StepSphereUp on the grounded path.
if (PhysicsDiagnostics.ProbeBuildingEnabled)
PhysicsDiagnostics.LastBspHitPoly = hitPoly1;
var worldNormal = L2W(hitPoly1!.Plane.Normal);
// L.2.3b: same recursion guard as the foot-sphere branch.
if (engine is not null && !path.StepUp && !path.StepDown)
@ -1582,9 +1595,6 @@ public static class BSPQuery
collisions.SetCollisionNormal(worldNormal);
collisions.SetSlidingNormal(worldNormal);
// L.2d slice 1 (2026-05-13): diagnostic side-channel.
if (PhysicsDiagnostics.ProbeBuildingEnabled)
PhysicsDiagnostics.LastBspHitPoly = hitPoly1;
return TransitionState.Slid;
}
}

View file

@ -1469,12 +1469,14 @@ public sealed class Transition
// the [resolve] probe surfaces the responsible entity id.
bool collisionWasValidPre = ci.CollisionNormalValid;
// L.2d slice 1 (2026-05-13): clear the BSP-hit side-channel so the
// [resolve-bldg] emission below reads only this iteration's poly.
// Cylinder collisions leave it null on purpose (probe emits
// "hitPoly: n/a (cylinder)").
if (PhysicsDiagnostics.ProbeBuildingEnabled)
PhysicsDiagnostics.LastBspHitPoly = null;
// L.2d slice 1.5 (2026-05-13): no per-iteration LastBspHitPoly
// clear. BSPQuery writes the side-channel early (inside
// `if (hit0 || hitPoly0 != null)` BEFORE any StepSphereUp call),
// so by the time we read it back for the [resolve-bldg] emission
// it reflects THIS entity's hit (or stays null if BSP didn't
// hit). For cylinder dispatch we key the "n/a (cylinder)" label
// off `obj.CollisionType` directly at the emission site, so a
// stale BSP value from a prior iteration can't leak through.
TransitionState result;
@ -1588,10 +1590,19 @@ public sealed class Transition
$" entOrigin_lb=({entOriginLb.X:F1},{entOriginLb.Y:F1},{entOriginLb.Z:F1})"));
var poly = PhysicsDiagnostics.LastBspHitPoly;
if (poly is null)
// L.2d slice 1.5 (2026-05-13): key the n/a label on the
// entity's CollisionType, not on LastBspHitPoly nullness —
// a BSP hit with null side-channel indicates a BSPQuery code
// path that didn't write (a bug; we should fix it, not
// pretend the entity was a cylinder).
if (obj.CollisionType == ShadowCollisionType.Cylinder)
{
sb.Append("\n hitPoly: n/a (cylinder)");
}
else if (poly is null)
{
sb.Append("\n hitPoly: n/a (BSP path — side-channel not written, missing BSPQuery wire site)");
}
else
{
sb.Append(System.FormattableString.Invariant(