fix(physics): split set_contact_plane from init_contact_plane (#32 local edge-slide)
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Measured live at Rithwic 2026-08-06 with ACDREAM_DUMP_EDGE_SLIDE=1. Six branch2/steep-cliffslide events, every one reporting curN=(-0.954,0.000,0.301) lastN=(-0.954,0.000,0.301) angle=0.0000 apply=False, outcome degenerate-cross/last-known. That is decision-table row 1 of the research doc, verbatim. CTransition::cliff_slide @0x0050a6d0 takes its slide direction from cross(steep contact normal, last_known_contact_plane.N) — it needs the surface the mover was STANDING ON as the second vector. acdream's CollisionInfo.SetContactPlane latched the last-known group on every call, so by the time cliff_slide ran, last-known had already been overwritten with the steep face itself: the cross product of a vector with itself, which is zero. Degenerate direction, no slide, walk off the cliff. Retail's COLLISIONINFO::set_contact_plane @0x00509d80 is 22 bytes and writes the CONTACT group only; the last-known group has four writers, none of them that function. So the four writes are DELETED and a new InitContactPlane mirrors CTransition::init_contact_plane @0x0050e850, writing both — the start-of-transition seed, where there is no earlier surface to remember. Only check_contact's SUCCESS branch calls it. The other eleven call sites keep the narrowed setter. This is a port, not a suppression: no guard, no grace period, no flag. The user's own A/B was the discriminator: Neftet's block plateaus hold (188 branch3/precipice-slide events, all before the teleport) while Rithwic's terrain cliff fails (6 branch2 events, all after). I had predicted the opposite — that terrain would be the flat-normal case — and position plus timeline corrected me, not reasoning. NEW DISCRIMINATING TEST, because the suite had none. It was green both before and after the production change, so nothing in it defended this behaviour. Issue32LastKnownContactPlaneTests seeds a walkable plane, asserts a steep mid-transition contact leaves it intact, and asserts the resulting cross product is non-degenerate. Sabotage-verified: restore the four writes and both discriminating rows fail while the InitContactPlane control keeps passing — the pair separates 'the latch is gone' from 'nothing writes last-known at all'. Two existing tests corrected rather than deleted. PhysicsSetPositionTests.FailedCheck_MapsCollisionHandlerResultToRetailError passed BECAUSE of the latch (the file the research named); its hook now populates both groups explicitly, since it asserts report plumbing, not setter semantics. RetailEdgeResponseOrderingTests.TransitionalInsert_ DegenerateCliffSlideOk_ContinuesOuterRetry was predicted to fail and did not — it now passes for a DIFFERENT reason (last-known absent rather than clobbered, which retail also answers with OK_TS). Its comment described the deleted behaviour and is corrected to say so, and to say it does not discriminate this fix. Also repairs the #338 probe. Its first placement in PlayerMovementController printed nothing across 11,523 live log lines — the wrong one of two resolve call sites — so it moves to PhysicsEngine.ResolveWithTransition where every caller passes through, filtered to the player. The dead site is removed rather than left in place; a probe that never fires is worse than none. The flag test now precedes the interpolated string: building it eagerly cost 128 B per resolve with the probe OFF, which Slice I1's zero-allocation gate caught. Suite 11,234 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
45d7154712
commit
332045c7ad
6 changed files with 243 additions and 14 deletions
|
|
@ -1927,6 +1927,26 @@ public sealed class PhysicsEngine
|
|||
{
|
||||
transition.ObjectInfo.StepUpHeight = stepUpHeight;
|
||||
transition.ObjectInfo.StepDownHeight = stepDownHeight;
|
||||
|
||||
// #338 (TEMPORARY): the resolver's own reading, taken where the
|
||||
// values actually land rather than at one of two candidate call
|
||||
// sites. The first attempt probed PlayerMovementController and
|
||||
// printed NOTHING across 11,523 live log lines — the wrong one of
|
||||
// its two resolve calls. A silent probe proves nothing, so this
|
||||
// one sits where every caller must pass through. Filtered to the
|
||||
// player so remotes cannot drown it.
|
||||
// The flag test MUST precede the interpolated string: this site runs
|
||||
// per resolve, and building the detail eagerly cost 128 B/resolve
|
||||
// with the probe OFF — caught by Slice I1's zero-allocation gate,
|
||||
// which is exactly what that gate is for.
|
||||
if (PhysicsDiagnostics.ProbeStepHeightsEnabled
|
||||
&& (moverFlags & ObjectInfoState.IsPlayer) != 0)
|
||||
{
|
||||
PhysicsDiagnostics.LogStepHeights(
|
||||
"resolve", stepUpHeight, stepDownHeight,
|
||||
$"onGround={isOnGround} hasBody={body is not null}");
|
||||
}
|
||||
|
||||
transition.ObjectInfo.StepDown = true;
|
||||
// Fix #42 (2026-05-05): the moving entity's ShadowEntry must be
|
||||
// skipped in FindObjCollisions or the sweep collides with self.
|
||||
|
|
@ -1993,7 +2013,15 @@ public sealed class PhysicsEngine
|
|||
transition.ObjectInfo.State |= ObjectInfoState.Contact;
|
||||
if (body.OnWalkable)
|
||||
transition.ObjectInfo.State |= ObjectInfoState.OnWalkable;
|
||||
transition.CollisionInfo.SetContactPlane(
|
||||
// #32 (2026-08-07): InitContactPlane, not SetContactPlane.
|
||||
// This is retail's check_contact SUCCESS branch — the
|
||||
// start-of-transition seed, where both groups are meant to
|
||||
// be written because there is no earlier surface to
|
||||
// remember. Every OTHER call site keeps the narrowed
|
||||
// setter, so a steep face met mid-transition can no longer
|
||||
// overwrite the walkable surface cliff_slide needs as its
|
||||
// second cross-product vector.
|
||||
transition.CollisionInfo.InitContactPlane(
|
||||
body.ContactPlane,
|
||||
body.ContactPlaneCellId,
|
||||
body.ContactPlaneIsWater);
|
||||
|
|
|
|||
|
|
@ -497,6 +497,53 @@ public sealed class CollisionInfo
|
|||
ContactPlaneCellId = cellId;
|
||||
ContactPlaneIsWater = isWater;
|
||||
|
||||
// #32 (2026-08-07): the four last-known writes that used to sit here
|
||||
// are DELETED. Retail's COLLISIONINFO::set_contact_plane @0x00509d80
|
||||
// is 22 bytes and writes the CONTACT group only; the last-known group
|
||||
// has exactly four writers, none of them this one.
|
||||
//
|
||||
// Why it mattered: CTransition::cliff_slide @0x0050a6d0 takes its
|
||||
// slide direction from cross(steep contact normal,
|
||||
// last_known_contact_plane.N) — it NEEDS the surface the mover was
|
||||
// standing on as the second vector. Latching last-known here meant
|
||||
// that by the time cliff_slide ran, last-known had already been
|
||||
// overwritten with the steep face itself, so the cross product was a
|
||||
// vector with itself: zero. Measured live at Rithwic 2026-08-06,
|
||||
// 6 events, every one curN == lastN == (-0.954, 0.000, 0.301),
|
||||
// angle=0.0000, apply=False, and the player ran off the cliff.
|
||||
// Capture + decision table:
|
||||
// docs/research/2026-08-06-32-local-edge-slide-research.md §6.
|
||||
//
|
||||
// Callers that legitimately want BOTH groups written — retail's
|
||||
// check_contact success path — call InitContactPlane instead.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CTransition::init_contact_plane</c> @0x0050e850 — write the
|
||||
/// contact group AND seed the last-known group from it.
|
||||
///
|
||||
/// <para>
|
||||
/// This is the split half of #32's fix. Retail has two distinct
|
||||
/// operations and acdream had collapsed them into one: an ordinary
|
||||
/// mid-transition contact assertion (<see cref="SetContactPlane"/>, which
|
||||
/// must NOT touch last-known, or the rim-slide direction degenerates) and
|
||||
/// the start-of-transition seed, which establishes both because there is
|
||||
/// no earlier surface to remember.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Only <c>check_contact</c>'s SUCCESS branch calls this. Its failure
|
||||
/// branch already mirrors retail's <c>init_last_known_contact_plane</c>
|
||||
/// and is deliberately left alone.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public void InitContactPlane(
|
||||
Plane plane,
|
||||
uint cellId,
|
||||
bool isWater = false)
|
||||
{
|
||||
SetContactPlane(plane, cellId, isWater);
|
||||
|
||||
LastKnownContactPlaneValid = true;
|
||||
LastKnownContactPlane = plane;
|
||||
LastKnownContactPlaneCellId = cellId;
|
||||
|
|
|
|||
|
|
@ -2625,15 +2625,6 @@ public sealed class PlayerMovementController
|
|||
// or it re-zeros the gravity velocity and the body re-wedges instead of falling off.
|
||||
bool candidateMoved = postIntegratePos != preIntegratePos;
|
||||
|
||||
// #338 (TEMPORARY): third and last reading along the chain — what
|
||||
// the resolver is ACTUALLY handed, per ordinary movement tick.
|
||||
// Edge-triggered inside the probe, so this per-tick site prints
|
||||
// once per distinct pair and cannot drown the two one-shot sites
|
||||
// it exists to be compared against.
|
||||
PhysicsDiagnostics.LogStepHeights(
|
||||
"resolve", StepUpHeight, StepDownHeight,
|
||||
$"onWalkable={_body.OnWalkable} cell=0x{CellId:X8}");
|
||||
|
||||
// ── 3. Collision resolution via CTransition sphere-sweep ─────────────
|
||||
// The Transition system subdivides the movement from pre→post into
|
||||
// sphere-radius steps, testing terrain collision at each step.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue