From 8bacef0598450b543d9db4885545aa8f77502352 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 12 May 2026 19:25:32 +0200 Subject: [PATCH] fix(phys L.2d slice 1.5): probe captures hit poly under StepSphereUp recursion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/AcDream.Core/Physics/BSPQuery.cs | 22 +++++++++++++----- src/AcDream.Core/Physics/TransitionTypes.cs | 25 +++++++++++++++------ 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/AcDream.Core/Physics/BSPQuery.cs b/src/AcDream.Core/Physics/BSPQuery.cs index 6c7178b..289ff0e 100644 --- a/src/AcDream.Core/Physics/BSPQuery.cs +++ b/src/AcDream.Core/Physics/BSPQuery.cs @@ -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; } } diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs index c881d2c..d5077b8 100644 --- a/src/AcDream.Core/Physics/TransitionTypes.cs +++ b/src/AcDream.Core/Physics/TransitionTypes.cs @@ -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(