diff --git a/docs/plans/2026-04-29-movement-collision-conformance.md b/docs/plans/2026-04-29-movement-collision-conformance.md
index 8bfc2c1..cdabd45 100644
--- a/docs/plans/2026-04-29-movement-collision-conformance.md
+++ b/docs/plans/2026-04-29-movement-collision-conformance.md
@@ -92,6 +92,35 @@ Goal: make every bad movement outcome explainable.
- Build real-DAT fixture capture for known walls, building ledges, rooftops,
slopes, landblock seams, and dungeon entrances.
+Current shipped slices:
+
+- 2026-04-30: cdb + TTD retail-observer toolchain (`tools/pdb-extract/`,
+ `tools/ttd-record.ps1`, `tools/ttd-query.ps1`) with PDB pairing checker
+ and ring-buffer trace replay. The "retail observer harness" line item.
+- 2026-04 (pre-L.2 rename): `ACDREAM_DUMP_MOVE_TRUTH` paired
+ outbound/server-echo dumper in `GameWindow` covers outbound packet
+ fields + server echo + correction delta with cell-id mismatch.
+- Pre-L.2: scenario-specific dumps `ACDREAM_DUMP_MOTION`,
+ `ACDREAM_DUMP_STEEP_ROOF`, `ACDREAM_DUMP_STEPUP`,
+ `ACDREAM_DUMP_EDGE_SLIDE` for the codepaths hit during prior bug chases.
+- 2026-05-12 (slice 1): general-purpose probes via new
+ `AcDream.Core.Physics.PhysicsDiagnostics` static class.
+ `ACDREAM_PROBE_RESOLVE` emits one `[resolve]` line per
+ `PhysicsEngine.ResolveWithTransition` call (input/output pos+cell,
+ ok-vs-partial, grounded-in, contact-plane status, wall normal if hit,
+ walkable polygon valid, moving entity id).
+ `ACDREAM_PROBE_CELL` emits one `[cell-transit]` line per
+ `PlayerMovementController.CellId` change with old→new + position +
+ reason tag (`resolver`/`teleport`). Both flippable live via the
+ DebugPanel "Diagnostics" section — checkbox toggles take effect on
+ the next resolve, no relaunch required.
+
+Remaining L.2a work: contact-plane probe (general, not just steep-roof),
+ShadowObjectRegistry hit log ("you collided with entity X"), water probe,
+real-DAT fixture-capture pipeline, and folding the older sticky-at-startup
+`ACDREAM_DUMP_*` flags into `PhysicsDiagnostics` for unified runtime
+toggling.
+
### L.2b - Movement Wire / Contact Authority
Goal: stop sending movement packets that claim more certainty than the local
diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs
index 1bc88b8..cb9b34b 100644
--- a/src/AcDream.App/Input/PlayerMovementController.cs
+++ b/src/AcDream.App/Input/PlayerMovementController.cs
@@ -288,12 +288,29 @@ public sealed class PlayerMovementController
_motion.apply_current_movement(cancelMoveTo: false, allowJump: false);
}
+ // L.2a slice 1 (2026-05-12): centralized CellId mutation so the
+ // [cell-transit] probe fires from a single chokepoint. Both the
+ // server-snap path (SetPosition) and the per-frame resolver path
+ // route through here. When PhysicsDiagnostics.ProbeCellEnabled is
+ // off this collapses to a single bool-compare + assignment — zero
+ // logging cost.
+ private void UpdateCellId(uint newCellId, string reason)
+ {
+ if (newCellId != CellId && PhysicsDiagnostics.ProbeCellEnabled)
+ {
+ var pos = _body.Position;
+ Console.WriteLine(System.FormattableString.Invariant(
+ $"[cell-transit] 0x{CellId:X8} -> 0x{newCellId:X8} pos=({pos.X:F3},{pos.Y:F3},{pos.Z:F3}) reason={reason}"));
+ }
+ CellId = newCellId;
+ }
+
public void SetPosition(Vector3 pos, uint cellId)
{
_body.Position = pos;
_prevPhysicsPos = pos;
_currPhysicsPos = pos;
- CellId = cellId;
+ UpdateCellId(cellId, "teleport");
// Treat as grounded after a server-side position snap.
_body.TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable;
@@ -760,7 +777,7 @@ public sealed class PlayerMovementController
_wasAirborneLastFrame = !_body.OnWalkable;
- CellId = resolveResult.CellId;
+ UpdateCellId(resolveResult.CellId, "resolver");
// ── 6. Determine outbound motion commands ─────────────────────────────
uint? outForwardCmd = null;
diff --git a/src/AcDream.Core/Physics/PhysicsDiagnostics.cs b/src/AcDream.Core/Physics/PhysicsDiagnostics.cs
new file mode 100644
index 0000000..2440bb1
--- /dev/null
+++ b/src/AcDream.Core/Physics/PhysicsDiagnostics.cs
@@ -0,0 +1,40 @@
+using System;
+
+namespace AcDream.Core.Physics;
+
+///
+/// L.2a slice 1 (2026-05-12) — runtime-toggleable physics probe flags.
+/// Initialized from env vars at process start; flippable at runtime via
+/// the DebugPanel mirror (or by direct assignment). Log call sites read
+/// these statics so a checkbox toggle takes effect on the next resolve
+/// without relaunching.
+///
+///
+/// Slice 1 ships +
+/// . Future slices may fold the older
+/// ACDREAM_DUMP_* env vars into this class for unified runtime
+/// toggling. Until then, those older flags remain sticky-at-startup
+/// per their original implementation.
+///
+///
+public static class PhysicsDiagnostics
+{
+ ///
+ /// When true, emits
+ /// one structured [resolve] line per call: input + target +
+ /// output position/cell, grounded state, contact-plane status,
+ /// collision-normal validity, walkable polygon status, moving entity
+ /// id. Initial state from ACDREAM_PROBE_RESOLVE=1.
+ ///
+ public static bool ProbeResolveEnabled { get; set; } =
+ Environment.GetEnvironmentVariable("ACDREAM_PROBE_RESOLVE") == "1";
+
+ ///
+ /// When true, every change to PlayerMovementController.CellId
+ /// emits one [cell-transit] line: old → new cell, current
+ /// world position, reason tag (resolver / teleport).
+ /// Initial state from ACDREAM_PROBE_CELL=1.
+ ///
+ public static bool ProbeCellEnabled { get; set; } =
+ Environment.GetEnvironmentVariable("ACDREAM_PROBE_CELL") == "1";
+}
diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs
index fe308ae..696a635 100644
--- a/src/AcDream.Core/Physics/PhysicsEngine.cs
+++ b/src/AcDream.Core/Physics/PhysicsEngine.cs
@@ -677,6 +677,28 @@ public sealed class PhysicsEngine
$"deltaXY=({dx:F3},{dy:F3}) cp={cpInfo}");
}
+ // L.2a slice 1 (2026-05-12): general-purpose resolver probe.
+ // One line per call when PhysicsDiagnostics.ProbeResolveEnabled
+ // is set (env var ACDREAM_PROBE_RESOLVE=1 at startup, or the
+ // DebugPanel checkbox flipped at runtime). Captures every
+ // dimension L.2 cares about: input/output position, input/output
+ // cell, ok-vs-partial, grounded-in vs contact-out, contact-plane
+ // status, wall normal if hit, walkable polygon valid. Zero cost
+ // when off (one static-bool read).
+ if (PhysicsDiagnostics.ProbeResolveEnabled)
+ {
+ var probePost = sp.CheckPos;
+ string probeCp = ci.ContactPlaneValid
+ ? "valid"
+ : (ci.LastKnownContactPlaneValid ? "lastKnown" : "none");
+ string probeHit = collisionNormalValid
+ ? System.FormattableString.Invariant(
+ $"yes n=({collisionNormal.X:F2},{collisionNormal.Y:F2},{collisionNormal.Z:F2})")
+ : "no";
+ Console.WriteLine(System.FormattableString.Invariant(
+ $"[resolve] ent=0x{movingEntityId:X8} in=({currentPos.X:F3},{currentPos.Y:F3},{currentPos.Z:F3}) cell=0x{cellId:X8} tgt=({targetPos.X:F3},{targetPos.Y:F3},{targetPos.Z:F3}) out=({probePost.X:F3},{probePost.Y:F3},{probePost.Z:F3}) cell=0x{sp.CheckCellId:X8} ok={ok} groundedIn={isOnGround} cp={probeCp} hit={probeHit} walkable={sp.HasLastWalkablePolygon}"));
+ }
+
if (ok)
{
bool onGround = ci.ContactPlaneValid
diff --git a/src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs b/src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs
index dc55080..e990686 100644
--- a/src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs
+++ b/src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs
@@ -199,15 +199,21 @@ public sealed class DebugPanel : IPanel
{
if (!r.CollapsingHeader("Diagnostics", defaultOpen: true)) return;
- bool dumpMotion = _vm.DumpMotion;
- bool dumpVitals = _vm.DumpVitals;
- bool dumpOpcodes = _vm.DumpOpcodes;
- bool dumpSky = _vm.DumpSky;
+ bool dumpMotion = _vm.DumpMotion;
+ bool dumpVitals = _vm.DumpVitals;
+ bool dumpOpcodes = _vm.DumpOpcodes;
+ bool dumpSky = _vm.DumpSky;
+ bool probeResolve = _vm.ProbeResolve;
+ bool probeCell = _vm.ProbeCell;
- if (r.Checkbox("Dump motion (ACDREAM_DUMP_MOTION)", ref dumpMotion)) _vm.DumpMotion = dumpMotion;
- if (r.Checkbox("Dump vitals (ACDREAM_DUMP_VITALS)", ref dumpVitals)) _vm.DumpVitals = dumpVitals;
- if (r.Checkbox("Dump opcodes (ACDREAM_DUMP_OPCODES)", ref dumpOpcodes)) _vm.DumpOpcodes = dumpOpcodes;
- if (r.Checkbox("Dump sky (ACDREAM_DUMP_SKY)", ref dumpSky)) _vm.DumpSky = dumpSky;
+ if (r.Checkbox("Dump motion (ACDREAM_DUMP_MOTION)", ref dumpMotion)) _vm.DumpMotion = dumpMotion;
+ if (r.Checkbox("Dump vitals (ACDREAM_DUMP_VITALS)", ref dumpVitals)) _vm.DumpVitals = dumpVitals;
+ if (r.Checkbox("Dump opcodes (ACDREAM_DUMP_OPCODES)", ref dumpOpcodes)) _vm.DumpOpcodes = dumpOpcodes;
+ if (r.Checkbox("Dump sky (ACDREAM_DUMP_SKY)", ref dumpSky)) _vm.DumpSky = dumpSky;
+ // L.2a slice 1 (2026-05-12): unlike the four above, these
+ // forward to PhysicsDiagnostics so a toggle takes effect live.
+ if (r.Checkbox("Probe resolve (ACDREAM_PROBE_RESOLVE)", ref probeResolve)) _vm.ProbeResolve = probeResolve;
+ if (r.Checkbox("Probe cell-transit (ACDREAM_PROBE_CELL)",ref probeCell)) _vm.ProbeCell = probeCell;
r.Spacing();
diff --git a/src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs b/src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs
index 9e914af..693cc2d 100644
--- a/src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs
+++ b/src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs
@@ -1,5 +1,6 @@
using System.Numerics;
using AcDream.Core.Combat;
+using AcDream.Core.Physics;
namespace AcDream.UI.Abstractions.Panels.Debug;
@@ -234,6 +235,32 @@ public sealed class DebugVM
/// Mirror of ACDREAM_DUMP_SKY.
public bool DumpSky { get; set; }
+ // L.2a slice 1 (2026-05-12): unlike DumpMotion/Vitals/Opcodes/Sky
+ // above (which are display-only mirrors of sticky-at-startup env
+ // vars), these forward directly to the PhysicsDiagnostics statics,
+ // so checkbox toggles take effect on the next physics resolve.
+ ///
+ /// Runtime mirror of PhysicsDiagnostics.ProbeResolveEnabled
+ /// (env var ACDREAM_PROBE_RESOLVE). Toggling here flips the
+ /// resolver probe live — no relaunch required.
+ ///
+ public bool ProbeResolve
+ {
+ get => PhysicsDiagnostics.ProbeResolveEnabled;
+ set => PhysicsDiagnostics.ProbeResolveEnabled = value;
+ }
+
+ ///
+ /// Runtime mirror of PhysicsDiagnostics.ProbeCellEnabled
+ /// (env var ACDREAM_PROBE_CELL). Toggling here flips the
+ /// cell-transit probe live.
+ ///
+ public bool ProbeCell
+ {
+ get => PhysicsDiagnostics.ProbeCellEnabled;
+ set => PhysicsDiagnostics.ProbeCellEnabled = value;
+ }
+
// ── Action hooks invoked by panel buttons ──────────────────────────
///