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; + } + } }