test(phys): A6.P3 #98 — live ResolveWithTransition capture apparatus
Apparatus only — no fix attempt. Per the systematic-debugging skill's
"3+ failures = question architecture" rule, the 6 hypotheses we
tested speculatively on the harness's airborne-at-tick-1 bug all
failed because we kept guessing what state the harness lacks. This
commit ships the evidence-driven path: capture the EXACT player
ResolveWithTransition call (every input + body-before + body-after +
result) into a JSON Lines fixture, then a comparison test loads the
fixture and replays it against the test engine. The first per-field
divergence pinpoints the missing apparatus state — no more guessing.
Adds:
- src/AcDream.Core/Physics/PhysicsResolveCapture.cs — new static module
with CapturePath (env var ACDREAM_CAPTURE_RESOLVE), PhysicsBodySnapshot
record, JSON Lines writer (thread-safe, flushes per record), process-
exit hook for clean shutdown.
- PhysicsEngine.ResolveWithTransition probe wiring: snapshot body at
method entry, snapshot again before return, refactor the two returns
into one path so the capture call site is single. Filtered to
IsPlayer mover flag so NPC/remote DR calls don't pollute.
- CellarUpTrajectoryReplayTests.cs:
• Capture_WritesJsonLinesRecordsWhenIsPlayerAndEnabled — drives 3
ticks with capture on, reads file back, verifies round-trip of
inputs + body-before/after snapshots.
• Capture_SkipsNonPlayerCalls — drives 3 NPC-style ticks (no
IsPlayer flag), confirms the file is not created.
Off by default. Set ACDREAM_CAPTURE_RESOLVE=<path> to a writable file
path; capture starts on the next player ResolveWithTransition call.
Test baseline: 1172 + 8 pre-existing failures + 2 new smoke tests
that pass = 1174 + 8. Verified by stashed-baseline comparison.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ec47159a2e
commit
fb5fba6229
4 changed files with 461 additions and 16 deletions
|
|
@ -595,6 +595,17 @@ public sealed class PhysicsEngine
|
|||
ObjectInfoState moverFlags = ObjectInfoState.None,
|
||||
uint movingEntityId = 0)
|
||||
{
|
||||
// A6.P3 #98 (2026-05-23) live capture. Filtered to IsPlayer so NPC /
|
||||
// remote ResolveWithTransition calls don't pollute the capture. Snapshot
|
||||
// the body BEFORE the engine mutates it so the replay test can seed its
|
||||
// PhysicsBody with the exact pre-call state. See PhysicsResolveCapture.cs.
|
||||
bool captureEnabled = PhysicsResolveCapture.IsEnabled
|
||||
&& moverFlags.HasFlag(ObjectInfoState.IsPlayer);
|
||||
PhysicsBodySnapshot? bodyBeforeSnap =
|
||||
captureEnabled && body is not null
|
||||
? PhysicsResolveCapture.Snapshot(body)
|
||||
: null;
|
||||
|
||||
var transition = new Transition();
|
||||
transition.ObjectInfo.StepUpHeight = stepUpHeight;
|
||||
transition.ObjectInfo.StepDownHeight = stepDownHeight;
|
||||
|
|
@ -825,34 +836,68 @@ public sealed class PhysicsEngine
|
|||
$"[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}"));
|
||||
}
|
||||
|
||||
ResolveResult resolveResult;
|
||||
if (ok)
|
||||
{
|
||||
bool onGround = ci.ContactPlaneValid
|
||||
|| transition.ObjectInfo.State.HasFlag(ObjectInfoState.OnWalkable);
|
||||
|
||||
return new ResolveResult(
|
||||
resolveResult = new ResolveResult(
|
||||
sp.CheckPos,
|
||||
ResolveCellId(sp.GlobalSphere[0].Origin, sphereRadius, sp.CheckCellId),
|
||||
onGround,
|
||||
collisionNormalValid,
|
||||
collisionNormal);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Transition failed (e.g., stuck in corner, too many steps).
|
||||
// Use whatever position the transition reached (partial movement)
|
||||
// instead of falling back to the no-collision Resolve.
|
||||
// If CheckPos hasn't moved from CurPos, the player stays put —
|
||||
// this is correct behavior when completely blocked.
|
||||
bool partialOnGround = ci.ContactPlaneValid
|
||||
|| transition.ObjectInfo.State.HasFlag(ObjectInfoState.OnWalkable)
|
||||
|| isOnGround;
|
||||
|
||||
// Transition failed (e.g., stuck in corner, too many steps).
|
||||
// Use whatever position the transition reached (partial movement)
|
||||
// instead of falling back to the no-collision Resolve.
|
||||
// If CheckPos hasn't moved from CurPos, the player stays put —
|
||||
// this is correct behavior when completely blocked.
|
||||
bool partialOnGround = ci.ContactPlaneValid
|
||||
|| transition.ObjectInfo.State.HasFlag(ObjectInfoState.OnWalkable)
|
||||
|| isOnGround;
|
||||
uint partialCellId = sp.CheckCellId != 0 ? sp.CheckCellId : cellId;
|
||||
resolveResult = new ResolveResult(
|
||||
sp.CheckPos,
|
||||
ResolveCellId(sp.GlobalSphere[0].Origin, sphereRadius, partialCellId),
|
||||
partialOnGround,
|
||||
collisionNormalValid,
|
||||
collisionNormal);
|
||||
}
|
||||
|
||||
uint partialCellId = sp.CheckCellId != 0 ? sp.CheckCellId : cellId;
|
||||
return new ResolveResult(
|
||||
sp.CheckPos,
|
||||
ResolveCellId(sp.GlobalSphere[0].Origin, sphereRadius, partialCellId),
|
||||
partialOnGround,
|
||||
collisionNormalValid,
|
||||
collisionNormal);
|
||||
// A6.P3 #98 capture: emit one JSON Lines record per player call,
|
||||
// with bodyBefore snapshot (taken at method entry, before any
|
||||
// engine mutation) + bodyAfter snapshot (taken now, after the
|
||||
// engine wrote back the contact plane / walkable / sliding state
|
||||
// to the body). Loaded by CellarUpTrajectoryReplayTests.cs.
|
||||
if (captureEnabled)
|
||||
{
|
||||
PhysicsResolveCapture.LogCall(
|
||||
new ResolveCallInputs(
|
||||
CurrentPos: currentPos,
|
||||
TargetPos: targetPos,
|
||||
CellId: cellId,
|
||||
SphereRadius: sphereRadius,
|
||||
SphereHeight: sphereHeight,
|
||||
StepUpHeight: stepUpHeight,
|
||||
StepDownHeight: stepDownHeight,
|
||||
IsOnGround: isOnGround,
|
||||
MoverFlags: (uint)moverFlags,
|
||||
MovingEntityId: movingEntityId),
|
||||
bodyBeforeSnap,
|
||||
new ResolveCallResult(
|
||||
Position: resolveResult.Position,
|
||||
CellId: resolveResult.CellId,
|
||||
IsOnGround: resolveResult.IsOnGround,
|
||||
CollisionNormalValid: resolveResult.CollisionNormalValid,
|
||||
CollisionNormal: resolveResult.CollisionNormal),
|
||||
body is not null ? PhysicsResolveCapture.Snapshot(body) : null);
|
||||
}
|
||||
|
||||
return resolveResult;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue