feat(physics): Cluster A — indoor BSP collision probe

Adds the [indoor-bsp] probe + ProbeIndoorBspEnabled toggle for the
Indoor walking Phase 1 BSP-cluster investigation. Mirrors the existing
[resolve] / [cell-transit] / [indoor-*] pattern: one log line per
BSPQuery.FindCollisions call from FindEnvCollisions' cell branch,
capturing cell id, sphere local-pos, result TransitionState, and the
hit poly's normal + side-type via the LastBspHitPoly side-channel
(already wired for ProbeBuildingEnabled, now also fires for the indoor
flag).

Toggle via ACDREAM_PROBE_INDOOR_BSP=1 env var or DebugPanel checkbox.
Zero-cost when off.

Predecessor for the three fix commits that will close ISSUES.md
#84/#85/#86 after the capture session.

Spec: docs/superpowers/specs/2026-05-19-indoor-walking-phase1-bsp-cluster-design.md
Plan: docs/superpowers/plans/2026-05-19-indoor-walking-phase1-bsp-cluster.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-19 14:24:07 +02:00
parent 18a2e28875
commit 27d7de11d8
6 changed files with 96 additions and 9 deletions

View file

@ -1215,7 +1215,7 @@ public static class BSPQuery
{
collisions.SetCollisionNormal(collisionNormal);
// L.2d slice 1 (2026-05-13): diagnostic side-channel.
if (PhysicsDiagnostics.ProbeBuildingEnabled)
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
PhysicsDiagnostics.LastBspHitPoly = hitPoly;
return TransitionState.Collided;
}
@ -1228,14 +1228,14 @@ public static class BSPQuery
// the early-out — collisions.SetCollisionNormal isn't called on
// this path, but the caller's CollisionInfo.CollisionNormalValid
// check will catch the parent slide site's normal write instead.
if (PhysicsDiagnostics.ProbeBuildingEnabled)
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
PhysicsDiagnostics.LastBspHitPoly = hitPoly;
return TransitionState.Collided;
}
collisions.SetCollisionNormal(collisionNormal);
// L.2d slice 1 (2026-05-13): diagnostic side-channel.
if (PhysicsDiagnostics.ProbeBuildingEnabled)
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
PhysicsDiagnostics.LastBspHitPoly = hitPoly;
var adjusted = validPos.Center - checkPos.Center;
@ -1551,7 +1551,7 @@ public static class BSPQuery
// 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)
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
PhysicsDiagnostics.LastBspHitPoly = hitPoly0;
var worldNormal = L2W(hitPoly0!.Plane.Normal);
@ -1585,7 +1585,7 @@ public static class BSPQuery
// 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)
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
PhysicsDiagnostics.LastBspHitPoly = hitPoly1;
var worldNormal = L2W(hitPoly1!.Plane.Normal);
@ -1669,7 +1669,7 @@ public static class BSPQuery
collisions.SetCollisionNormal(worldNormal0);
collisions.SetSlidingNormal(worldNormal0);
// L.2d slice 1 (2026-05-13): diagnostic side-channel.
if (PhysicsDiagnostics.ProbeBuildingEnabled)
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
PhysicsDiagnostics.LastBspHitPoly = hitPoly0;
return TransitionState.Slid;
}
@ -1679,7 +1679,7 @@ public static class BSPQuery
path.SetCollide(worldNormal0);
path.WalkableAllowance = PhysicsGlobals.LandingZ;
// L.2d slice 1 (2026-05-13): diagnostic side-channel.
if (PhysicsDiagnostics.ProbeBuildingEnabled)
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
PhysicsDiagnostics.LastBspHitPoly = hitPoly0;
return TransitionState.Adjusted;
}
@ -1709,7 +1709,7 @@ public static class BSPQuery
collisions.SetCollisionNormal(worldNormal1);
collisions.SetSlidingNormal(worldNormal1);
// L.2d slice 1 (2026-05-13): diagnostic side-channel.
if (PhysicsDiagnostics.ProbeBuildingEnabled)
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
PhysicsDiagnostics.LastBspHitPoly = hitPoly1;
return TransitionState.Slid;
}
@ -1718,7 +1718,7 @@ public static class BSPQuery
path.SetCollide(worldNormal1);
path.WalkableAllowance = PhysicsGlobals.LandingZ;
// L.2d slice 1 (2026-05-13): diagnostic side-channel.
if (PhysicsDiagnostics.ProbeBuildingEnabled)
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
PhysicsDiagnostics.LastBspHitPoly = hitPoly1;
return TransitionState.Adjusted;
}

View file

@ -166,4 +166,33 @@ public static class PhysicsDiagnostics
/// </summary>
public static bool DumpSteepRoofEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_DUMP_STEEP_ROOF") == "1";
/// <summary>
/// Indoor walking Phase 1 (2026-05-19). When true, emits one
/// <c>[indoor-bsp]</c> line per <see cref="BSPQuery.FindCollisions"/>
/// call made from <see cref="Transition.FindEnvCollisions"/>'s indoor
/// cell-BSP branch. Captures the cell id, sphere local position,
/// resulting <see cref="TransitionState"/>, and the hit poly's id,
/// local-normal, and side-type — pinpoints why indoor collision
/// returns spurious collisions (#84) and helps cross-check the
/// outdoor-in approach path (#85).
///
/// <para>
/// While true, this also un-gates the diagnostic
/// <see cref="LastBspHitPoly"/> side-channel inside
/// <see cref="BSPQuery"/> — see the OR'd condition at every poly
/// write site. Zero-cost when off.
/// </para>
///
/// <para>
/// Initial state from <c>ACDREAM_PROBE_INDOOR_BSP=1</c>.
/// Runtime-toggleable via DebugPanel.
/// </para>
///
/// <para>
/// Spec: <c>docs/superpowers/specs/2026-05-19-indoor-walking-phase1-bsp-cluster-design.md</c>.
/// </para>
/// </summary>
public static bool ProbeIndoorBspEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_BSP") == "1";
}

View file

@ -1217,6 +1217,12 @@ public sealed class Transition
};
}
// Indoor walking Phase 1 (2026-05-19): clear the LastBspHitPoly
// side-channel before the call so a missed write (no collision)
// is greppable as "poly=n/a" in the probe line below.
if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
PhysicsDiagnostics.LastBspHitPoly = null;
// Use the full 6-path BSP dispatcher for retail-faithful collision.
// Use pre-resolved polygons (vertices+planes computed at cache time).
var cellState = BSPQuery.FindCollisions(
@ -1231,6 +1237,18 @@ public sealed class Transition
Quaternion.Identity,
engine); // engine needed for Path 5 step-up
if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
{
var hit = PhysicsDiagnostics.LastBspHitPoly;
string polyDesc = hit is null
? "poly=n/a"
: System.FormattableString.Invariant(
$"n=({hit.Plane.Normal.X:F3},{hit.Plane.Normal.Y:F3},{hit.Plane.Normal.Z:F3}) sides={hit.SidesType}");
Console.WriteLine(System.FormattableString.Invariant(
$"[indoor-bsp] cell=0x{sp.CheckCellId:X8} wpos=({footCenter.X:F3},{footCenter.Y:F3},{footCenter.Z:F3}) lpos=({localCenter.X:F3},{localCenter.Y:F3},{localCenter.Z:F3}) lprev=({localCurrCenter.X:F3},{localCurrCenter.Y:F3},{localCurrCenter.Z:F3}) r={sphereRadius:F3} result={cellState} ")
+ polyDesc);
}
if (cellState != TransitionState.OK)
{
if (!ObjectInfo.State.HasFlag(ObjectInfoState.Contact))

View file

@ -275,6 +275,9 @@ public sealed class DebugPanel : IPanel
if (r.Checkbox("Indoor: xform (ACDREAM_PROBE_INDOOR_XFORM)", ref probeIndoorXform)) _vm.ProbeIndoorXform = probeIndoorXform;
if (r.Checkbox("Indoor: cull (ACDREAM_PROBE_INDOOR_CULL)", ref probeIndoorCull)) _vm.ProbeIndoorCull = probeIndoorCull;
bool probeIndoorBsp = _vm.ProbeIndoorBsp;
if (r.Checkbox("Indoor: BSP collision (ACDREAM_PROBE_INDOOR_BSP)", ref probeIndoorBsp)) _vm.ProbeIndoorBsp = probeIndoorBsp;
r.Spacing();
// Cycle / toggle actions live on the VM as Action handles; the

View file

@ -345,6 +345,20 @@ public sealed class DebugVM
set => RenderingDiagnostics.ProbeIndoorCullEnabled = value;
}
/// <summary>
/// Indoor walking Phase 1 (2026-05-19). Runtime mirror of
/// <c>PhysicsDiagnostics.ProbeIndoorBspEnabled</c> (env var
/// <c>ACDREAM_PROBE_INDOOR_BSP</c>). Toggling here flips the
/// <c>[indoor-bsp]</c> probe live — no relaunch required.
/// Physics-side companion to the five render-side
/// <c>ProbeIndoor*</c> mirrors directly above.
/// </summary>
public bool ProbeIndoorBsp
{
get => PhysicsDiagnostics.ProbeIndoorBspEnabled;
set => PhysicsDiagnostics.ProbeIndoorBspEnabled = value;
}
/// <summary>
/// Runtime mirror of <c>RenderingDiagnostics.IndoorAll</c> — toggles all
/// five indoor probes together. No dedicated env var; set any individual