feat(phys L.2a slice 1): resolver + cell-transit probes (PhysicsDiagnostics)

New static `AcDream.Core.Physics.PhysicsDiagnostics` holds two
runtime-toggleable flags initialized from env vars:

- ACDREAM_PROBE_RESOLVE=1 — emit one [resolve] line per
  PhysicsEngine.ResolveWithTransition call: input/target/output
  position+cell, ok-vs-partial, grounded-in, contact-plane status,
  wall normal if hit, walkable-polygon valid, moving entity id.
- ACDREAM_PROBE_CELL=1 — emit one [cell-transit] line per
  PlayerMovementController.CellId change: old → new cell, current
  world position, reason tag (resolver / teleport).

Both also exposed as runtime-toggleable checkboxes in the DebugPanel
"Diagnostics" section. Unlike the existing four Dump-* checkboxes
(which only mirror sticky-at-startup env vars), the two new ones
forward directly to PhysicsDiagnostics — toggling on/off takes
effect on the next physics resolve, no relaunch.

Why now: L.2's plan-of-record (docs/plans/2026-04-29-movement-collision-
conformance.md) explicitly says "Land L.2a diagnostics first. Do not
make another physics change blind." This slice closes the most-load-
bearing gap in L.2a — a general-purpose probe on the resolver outcome
and a cell-transit log — so that later L.2b/c/d/e physics changes can
be evidence-driven instead of guessed. Foundation for the indoor /
dungeon walking trajectory (G.3 unblock).

Pure additive: when both flags are off (default), the probes collapse
to a single static-bool read per resolve, zero log cost. PlayerMovement
Controller's two CellId-mutation sites are now routed through a
private UpdateCellId(reason) helper for diag chokepoint.

Build green, 1032/1040 unit tests pass. The 8 failing tests are
pre-existing on the branch base (verified by stash-and-rerun);
none touch resolver or cell-transit code; all fail identically with
this slice stashed. Investigation deferred to a follow-up.

Refs: docs/plans/2026-04-29-movement-collision-conformance.md (L.2a
shipped-slice note added in same commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-12 17:19:05 +02:00
parent eab347d7e4
commit ebef82034e
6 changed files with 151 additions and 10 deletions

View file

@ -0,0 +1,40 @@
using System;
namespace AcDream.Core.Physics;
/// <summary>
/// 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.
///
/// <para>
/// Slice 1 ships <see cref="ProbeResolveEnabled"/> +
/// <see cref="ProbeCellEnabled"/>. Future slices may fold the older
/// <c>ACDREAM_DUMP_*</c> env vars into this class for unified runtime
/// toggling. Until then, those older flags remain sticky-at-startup
/// per their original implementation.
/// </para>
/// </summary>
public static class PhysicsDiagnostics
{
/// <summary>
/// When true, <see cref="PhysicsEngine.ResolveWithTransition"/> emits
/// one structured <c>[resolve]</c> 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 <c>ACDREAM_PROBE_RESOLVE=1</c>.
/// </summary>
public static bool ProbeResolveEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_RESOLVE") == "1";
/// <summary>
/// When true, every change to <c>PlayerMovementController.CellId</c>
/// emits one <c>[cell-transit]</c> line: old → new cell, current
/// world position, reason tag (<c>resolver</c> / <c>teleport</c>).
/// Initial state from <c>ACDREAM_PROBE_CELL=1</c>.
/// </summary>
public static bool ProbeCellEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_CELL") == "1";
}

View file

@ -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