probe(physics): ACDREAM_PROBE_SUPPORT + ACDREAM_WIRE_MESH — separate #337's three candidates

The user is wedged at the top of Neftet rock plateaus, jumps sink into the
mesh, and a corpse falls straight through. ACDREAM_PROBE_REACH already ruled
out its own domain: blocked=0, every candidate tested-ok. Three candidates
remain — terrain support, a collision mesh not where its visual is, or the
transition wedging on an unobstructed path.

ACDREAM_PROBE_RESOLVE alone cannot separate them. It prints a three-value
contact-plane token, no plane normal, no plane height, no terrain sample and
no plane provenance, so all three produce the same line. Two additions:

[support] — one line per resolve for EVERY body, not just the player. A corpse
is a plain physics body with no player-specific logic, so its fall-through is
the cheapest available control on "movement code vs geometry data", and it is
invisible to any player-filtered probe. The line samples the outdoor terrain
INDEPENDENTLY at the body's own out-XY and prints the contact plane's own
height at that same XY. Two heights at one point make support=terrain /
object / none a measurement rather than an inference, and cpSrc= names the
site that asserted the plane so provenance and classification cross-check.

[geom] — once per GfxObj that comes near a mover: the object's physics-BSP
vertex cloud against its visual mesh AABB in the same local frame, through the
same prepared accessors the resolver queries. verdict=coincident REFUTES the
working hypothesis for that object outright; no-physics-bsp / empty-physics-bsp
/ displaced / extent-mismatch each name a specific data defect. Built to
refute, not to confirm — two diagnoses on this defect's lineage have already
been refuted by measurement.

ACDREAM_WIRE_MESH upgrades the existing F2 overlay, which drew a broadphase
proxy cylinder for BSP objects and so could not answer the question at all, to
the real physics-BSP polygon edges (cyan) beside the visual mesh box (magenta)
and the terrain surface (yellow). Own class per code-structure rule 1.

The provenance latch lives on PhysicsDiagnostics, not on CollisionInfo. Two
fields there first — the obvious home — broke the flat/graph differential
referee and the scratch-reset poison test, both of which compare CollisionInfo
member-for-member. Teaching either to skip a member is a one-line green fix
that puts a permanent hole in a referee whose whole job is comparing
everything. Captured as feedback_probe_state_off_compared_types.

Seven tests cover the support classifier's boundaries: a wrong classifier does
not fail to answer, it answers confidently wrong.

Gates: Release build 0 errors; complete suite 11,225 passed / 4 skipped / 0
failed from a cleaned tree — baseline 11,218/4/0 plus exactly the seven new
tests, skips unchanged.

Issue #337 filed with the symptom set, what is ruled out, and a table of what
each possible output means. All of this is TEMPORARY and recorded for
stripping with the physics-probe family.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-06 19:49:59 +02:00
parent 13fcf38138
commit 49a7e90652
9 changed files with 1376 additions and 1 deletions

View file

@ -1913,6 +1913,15 @@ public sealed class PhysicsEngine
? PhysicsResolveCapture.Snapshot(body)
: null;
// #337 (2026-08-06 — TEMPORARY): arm the [support] probe's
// contact-plane provenance latch for this resolve, ahead of everything
// including the carried-plane seed below. The seed is itself one of
// the ten sites that assert a plane, so it stamps its own name and a
// capture can read `cpSrc=ResolveWithTransition:<line>` as "carried
// from the body, nothing re-derived it this resolve" without needing a
// sentinel value for that case. No-op when the probe is off.
PhysicsDiagnostics.BeginContactPlaneAttribution();
var transition = RentTransition();
try
{
@ -2299,6 +2308,67 @@ 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}"));
}
// #337 [support] probe (2026-08-06 — TEMPORARY, strip with the
// physics-probe family). Runs for EVERY body, not just the player:
// a corpse sinking through geometry is a plain physics body with
// no player-specific logic, so it is the cheapest possible control
// on whether the movement code or the geometry is at fault, and it
// is invisible to any player-filtered probe.
//
// The terrain sample below is INDEPENDENT of whatever the sweep
// decided — it asks the landblock directly what the ground height
// is under the body's own out-XY. Pairing that with the contact
// plane's height at the same XY is what separates "terrain is
// holding this body up" from "some object surface is". Read-only:
// SampleTerrainWalkable takes no locks, mutates nothing, and is
// not on the resolve's committed path.
if (PhysicsDiagnostics.ProbeSupportEnabled)
{
Vector3 outPos = sp.CheckPos;
TerrainWalkableSample? terrain =
SampleTerrainWalkable(outPos.X, outPos.Y);
bool terrainSampled = terrain.HasValue
&& PhysicsDiagnostics.TryPlaneZAt(
terrain.Value.Plane, outPos.X, outPos.Y, out _);
float terrainZ = float.NaN;
if (terrainSampled)
{
PhysicsDiagnostics.TryPlaneZAt(
terrain!.Value.Plane, outPos.X, outPos.Y, out terrainZ);
}
PhysicsDiagnostics.LogSupport(
moverId: movingEntityId,
isPlayer: (moverFlags & ObjectInfoState.IsPlayer) != 0,
inPos: currentPos,
inCell: cellId,
targetPos: targetPos,
outPos: outPos,
outCell: sp.CheckCellId,
ok: ok,
groundedIn: isOnGround,
contact: transition.ObjectInfo.Contact,
onWalkable: transition.ObjectInfo.OnWalkable,
contactPlaneValid: ci.ContactPlaneValid,
contactPlane: ci.ContactPlane,
contactPlaneCellId: ci.ContactPlaneCellId,
contactPlaneIsWater: ci.ContactPlaneIsWater,
contactPlaneSource: PhysicsDiagnostics.ContactPlaneSource,
lastKnownValid: ci.LastKnownContactPlaneValid,
lastKnownPlane: ci.LastKnownContactPlane,
terrainSampled: terrainSampled,
terrainZ: terrainZ,
terrainNormal: terrain?.Plane.Normal ?? Vector3.Zero,
terrainCellId: terrain?.CellId ?? 0u,
terrainIsWater: terrain?.IsWater ?? false,
walkablePolygon: sp.HasWalkablePolygon,
lastWalkablePolygon: sp.HasLastWalkablePolygon,
stepUpHeight: stepUpHeight,
stepDownHeight: stepDownHeight,
velocity: body?.Velocity ?? Vector3.Zero);
}
// Phase W Stage 0 (2026-06-02): [cell-swept] probe — swept cell vs static-derived cell.
// Emits before the ResolveResult is built so it shows what BOTH paths would return.
// No ResolveCellId call here (it has a CellGraph.CurrCell side effect). No behavior change.