diff --git a/src/AcDream.Core/Physics/BSPQuery.cs b/src/AcDream.Core/Physics/BSPQuery.cs
index 289ff0e..031d8f6 100644
--- a/src/AcDream.Core/Physics/BSPQuery.cs
+++ b/src/AcDream.Core/Physics/BSPQuery.cs
@@ -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;
}
diff --git a/src/AcDream.Core/Physics/PhysicsDiagnostics.cs b/src/AcDream.Core/Physics/PhysicsDiagnostics.cs
index a8649a0..bb49b20 100644
--- a/src/AcDream.Core/Physics/PhysicsDiagnostics.cs
+++ b/src/AcDream.Core/Physics/PhysicsDiagnostics.cs
@@ -166,4 +166,33 @@ public static class PhysicsDiagnostics
///
public static bool DumpSteepRoofEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_DUMP_STEEP_ROOF") == "1";
+
+ ///
+ /// Indoor walking Phase 1 (2026-05-19). When true, emits one
+ /// [indoor-bsp] line per
+ /// call made from 's indoor
+ /// cell-BSP branch. Captures the cell id, sphere local position,
+ /// resulting , 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).
+ ///
+ ///
+ /// While true, this also un-gates the diagnostic
+ /// side-channel inside
+ /// — see the OR'd condition at every poly
+ /// write site. Zero-cost when off.
+ ///
+ ///
+ ///
+ /// Initial state from ACDREAM_PROBE_INDOOR_BSP=1.
+ /// Runtime-toggleable via DebugPanel.
+ ///
+ ///
+ ///
+ /// Spec: docs/superpowers/specs/2026-05-19-indoor-walking-phase1-bsp-cluster-design.md.
+ ///
+ ///
+ public static bool ProbeIndoorBspEnabled { get; set; } =
+ Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_BSP") == "1";
}
diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs
index 9fa7ba2..7d33d97 100644
--- a/src/AcDream.Core/Physics/TransitionTypes.cs
+++ b/src/AcDream.Core/Physics/TransitionTypes.cs
@@ -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))
diff --git a/src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs b/src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs
index bcf58be..6593f90 100644
--- a/src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs
+++ b/src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs
@@ -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
diff --git a/src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs b/src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs
index b051dc0..731ee9e 100644
--- a/src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs
+++ b/src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs
@@ -345,6 +345,20 @@ public sealed class DebugVM
set => RenderingDiagnostics.ProbeIndoorCullEnabled = value;
}
+ ///
+ /// Indoor walking Phase 1 (2026-05-19). Runtime mirror of
+ /// PhysicsDiagnostics.ProbeIndoorBspEnabled (env var
+ /// ACDREAM_PROBE_INDOOR_BSP). Toggling here flips the
+ /// [indoor-bsp] probe live — no relaunch required.
+ /// Physics-side companion to the five render-side
+ /// ProbeIndoor* mirrors directly above.
+ ///
+ public bool ProbeIndoorBsp
+ {
+ get => PhysicsDiagnostics.ProbeIndoorBspEnabled;
+ set => PhysicsDiagnostics.ProbeIndoorBspEnabled = value;
+ }
+
///
/// Runtime mirror of RenderingDiagnostics.IndoorAll — toggles all
/// five indoor probes together. No dedicated env var; set any individual
diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Debug/DebugVMTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Debug/DebugVMTests.cs
index 33b0fde..f89cb7f 100644
--- a/tests/AcDream.UI.Abstractions.Tests/Panels/Debug/DebugVMTests.cs
+++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Debug/DebugVMTests.cs
@@ -1,5 +1,6 @@
using System.Numerics;
using AcDream.Core.Combat;
+using AcDream.Core.Physics;
using AcDream.UI.Abstractions.Panels.Debug;
namespace AcDream.UI.Abstractions.Tests.Panels.Debug;
@@ -285,4 +286,26 @@ public sealed class DebugVMTests
Assert.Equal(1, weatherHits);
Assert.Equal(1, wireHits);
}
+
+ [Fact]
+ public void ProbeIndoorBsp_ForwardsToPhysicsDiagnostics()
+ {
+ var originalEnabled = PhysicsDiagnostics.ProbeIndoorBspEnabled;
+ try
+ {
+ var vm = NewVm();
+
+ vm.ProbeIndoorBsp = true;
+ Assert.True(PhysicsDiagnostics.ProbeIndoorBspEnabled);
+ Assert.True(vm.ProbeIndoorBsp);
+
+ vm.ProbeIndoorBsp = false;
+ Assert.False(PhysicsDiagnostics.ProbeIndoorBspEnabled);
+ Assert.False(vm.ProbeIndoorBsp);
+ }
+ finally
+ {
+ PhysicsDiagnostics.ProbeIndoorBspEnabled = originalEnabled;
+ }
+ }
}