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:
parent
eab347d7e4
commit
ebef82034e
6 changed files with 151 additions and 10 deletions
|
|
@ -92,6 +92,35 @@ Goal: make every bad movement outcome explainable.
|
||||||
- Build real-DAT fixture capture for known walls, building ledges, rooftops,
|
- Build real-DAT fixture capture for known walls, building ledges, rooftops,
|
||||||
slopes, landblock seams, and dungeon entrances.
|
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
|
### L.2b - Movement Wire / Contact Authority
|
||||||
|
|
||||||
Goal: stop sending movement packets that claim more certainty than the local
|
Goal: stop sending movement packets that claim more certainty than the local
|
||||||
|
|
|
||||||
|
|
@ -288,12 +288,29 @@ public sealed class PlayerMovementController
|
||||||
_motion.apply_current_movement(cancelMoveTo: false, allowJump: false);
|
_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)
|
public void SetPosition(Vector3 pos, uint cellId)
|
||||||
{
|
{
|
||||||
_body.Position = pos;
|
_body.Position = pos;
|
||||||
_prevPhysicsPos = pos;
|
_prevPhysicsPos = pos;
|
||||||
_currPhysicsPos = pos;
|
_currPhysicsPos = pos;
|
||||||
CellId = cellId;
|
UpdateCellId(cellId, "teleport");
|
||||||
|
|
||||||
// Treat as grounded after a server-side position snap.
|
// Treat as grounded after a server-side position snap.
|
||||||
_body.TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable;
|
_body.TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable;
|
||||||
|
|
@ -760,7 +777,7 @@ public sealed class PlayerMovementController
|
||||||
|
|
||||||
|
|
||||||
_wasAirborneLastFrame = !_body.OnWalkable;
|
_wasAirborneLastFrame = !_body.OnWalkable;
|
||||||
CellId = resolveResult.CellId;
|
UpdateCellId(resolveResult.CellId, "resolver");
|
||||||
|
|
||||||
// ── 6. Determine outbound motion commands ─────────────────────────────
|
// ── 6. Determine outbound motion commands ─────────────────────────────
|
||||||
uint? outForwardCmd = null;
|
uint? outForwardCmd = null;
|
||||||
|
|
|
||||||
40
src/AcDream.Core/Physics/PhysicsDiagnostics.cs
Normal file
40
src/AcDream.Core/Physics/PhysicsDiagnostics.cs
Normal 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";
|
||||||
|
}
|
||||||
|
|
@ -677,6 +677,28 @@ public sealed class PhysicsEngine
|
||||||
$"deltaXY=({dx:F3},{dy:F3}) cp={cpInfo}");
|
$"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)
|
if (ok)
|
||||||
{
|
{
|
||||||
bool onGround = ci.ContactPlaneValid
|
bool onGround = ci.ContactPlaneValid
|
||||||
|
|
|
||||||
|
|
@ -203,11 +203,17 @@ public sealed class DebugPanel : IPanel
|
||||||
bool dumpVitals = _vm.DumpVitals;
|
bool dumpVitals = _vm.DumpVitals;
|
||||||
bool dumpOpcodes = _vm.DumpOpcodes;
|
bool dumpOpcodes = _vm.DumpOpcodes;
|
||||||
bool dumpSky = _vm.DumpSky;
|
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 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 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 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 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();
|
r.Spacing();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.Combat;
|
using AcDream.Core.Combat;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
|
||||||
namespace AcDream.UI.Abstractions.Panels.Debug;
|
namespace AcDream.UI.Abstractions.Panels.Debug;
|
||||||
|
|
||||||
|
|
@ -234,6 +235,32 @@ public sealed class DebugVM
|
||||||
/// <summary>Mirror of <c>ACDREAM_DUMP_SKY</c>.</summary>
|
/// <summary>Mirror of <c>ACDREAM_DUMP_SKY</c>.</summary>
|
||||||
public bool DumpSky { get; set; }
|
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.
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime mirror of <c>PhysicsDiagnostics.ProbeResolveEnabled</c>
|
||||||
|
/// (env var <c>ACDREAM_PROBE_RESOLVE</c>). Toggling here flips the
|
||||||
|
/// resolver probe live — no relaunch required.
|
||||||
|
/// </summary>
|
||||||
|
public bool ProbeResolve
|
||||||
|
{
|
||||||
|
get => PhysicsDiagnostics.ProbeResolveEnabled;
|
||||||
|
set => PhysicsDiagnostics.ProbeResolveEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime mirror of <c>PhysicsDiagnostics.ProbeCellEnabled</c>
|
||||||
|
/// (env var <c>ACDREAM_PROBE_CELL</c>). Toggling here flips the
|
||||||
|
/// cell-transit probe live.
|
||||||
|
/// </summary>
|
||||||
|
public bool ProbeCell
|
||||||
|
{
|
||||||
|
get => PhysicsDiagnostics.ProbeCellEnabled;
|
||||||
|
set => PhysicsDiagnostics.ProbeCellEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Action hooks invoked by panel buttons ──────────────────────────
|
// ── Action hooks invoked by panel buttons ──────────────────────────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue