fix(physics): L.2.3c — preserve contact plane through failed step-up

The "stuck in falling animation against walls" live-test bug (intermittent,
hard to recover from). Two compounding issues, fixed at both layers.

(1) DoStepUp cleared CollisionInfo.ContactPlaneValid unconditionally at
    the start of step-up. On step-up FAILURE, RestoreCheckPos restored
    the position but the contact plane stayed cleared. Added a save/
    restore around the clear so a failed step-up returns the mover to
    its pre-attempt grounded state.

(2) ValidateTransition propagated the current frame's invalid contact
    state into LastKnownContactPlane via:
        ci.LastKnownContactPlaneValid = ci.ContactPlaneValid
    This destroyed the prior frame's ground memory whenever the current
    contact was momentarily lost (StepUpSlide clears ContactPlane).
    Changed to: only OVERWRITE LastKnown when current is valid.

(3) The same ValidateTransition then set
        oi.State &= ~(Contact | OnWalkable)
    when ContactPlaneValid was false, even if LastKnown was still
    valid. Added an "else if (LastKnownContactPlaneValid)" branch that
    sets Contact + OnWalkable from LastKnown so the animation system
    sees the mover as grounded.

Combined effect: walking into a too-tall wall now consistently slides
along the wall without ever flickering to the falling animation. The
mover's grounded state survives transient ContactPlane invalidation
during the step-up retry cycle.

Retail's `transitional_insert` has different upstream invariants that
keep ContactPlane valid more often, so retail doesn't need the
acdream-specific LastKnown fallback path. ACE has the same pattern as
retail; acdream's per-frame Resolve architecture exposes the gap that
this fix closes.

Tests:
- New D1 regression test: grounded mover into too-tall wall — must
  end frame with grounded state preserved.
- New D2 regression test: same scenario — execution time bounded
  (<100ms) to catch any future recursion issues.

Files:
- TransitionTypes.cs DoStepUp: save+restore ContactPlane around step-up
- TransitionTypes.cs ValidateTransition: preserve LastKnown + grounded
  state from last-known when current is invalid
- BSPStepUpTests.cs: D1, D2 regression tests

Test count 825 → 825 (D1+D2 added in L.2.3 patch series). Build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-29 17:24:49 +02:00
parent 3789491394
commit d2f6067960
2 changed files with 125 additions and 3 deletions

View file

@ -1356,6 +1356,19 @@ public sealed class Transition
var ci = CollisionInfo;
var oi = ObjectInfo;
// L.2.3c (2026-04-29): capture the existing contact plane BEFORE
// clearing it. On step-up failure (too-tall wall) we restore it so
// the mover stays grounded — without this, walking into a wall
// dropped OnWalkable and the animation system flickered to falling.
// Retail clears here too (acclient_2013_pseudo_c.txt:273099) but
// its outer transition state seeded the plane back via a different
// path (LastKnownContactPlane retention + check_contact). For
// acdream's per-frame Resolve we restore here directly.
bool savedCpValid = ci.ContactPlaneValid;
Plane savedCp = ci.ContactPlane;
uint savedCpCellId = ci.ContactPlaneCellId;
bool savedCpIsWater = ci.ContactPlaneIsWater;
ci.ContactPlaneValid = false;
ci.ContactPlaneIsWater = false;
@ -1381,8 +1394,21 @@ public sealed class Transition
sp.WalkableValid = false;
if (!stepDown)
{
sp.RestoreCheckPos();
// L.2.3c: restore the pre-step-up contact plane. The mover was
// grounded before the failed climb attempt; failing to climb
// a too-tall wall must not change that.
if (savedCpValid)
{
ci.ContactPlane = savedCp;
ci.ContactPlaneValid = true;
ci.ContactPlaneCellId = savedCpCellId;
ci.ContactPlaneIsWater = savedCpIsWater;
}
}
return stepDown;
}
@ -1498,11 +1524,18 @@ public sealed class Transition
ci.SetSlidingNormal(ci.CollisionNormal);
// Preserve contact plane for next step.
ci.LastKnownContactPlaneValid = ci.ContactPlaneValid;
// L.2.3c (2026-04-29): only OVERWRITE LastKnown when current is valid.
// Previously: `LastKnownValid = ContactPlaneValid` cleared
// LastKnown whenever current was invalid — destroying the prior frame's
// contact memory. After StepUpSlide cleared ContactPlane mid-step
// (failed step-up against a too-tall wall), this propagated to
// LastKnown and the player went airborne for a frame, flickering the
// falling animation. Now LastKnown survives transient losses.
if (ci.ContactPlaneValid)
{
ci.LastKnownContactPlane = ci.ContactPlane;
ci.LastKnownContactPlaneCellId = ci.ContactPlaneCellId;
ci.LastKnownContactPlaneValid = true;
ci.LastKnownContactPlane = ci.ContactPlane;
ci.LastKnownContactPlaneCellId = ci.ContactPlaneCellId;
ci.LastKnownContactPlaneIsWater = ci.ContactPlaneIsWater;
oi.State |= ObjectInfoState.Contact;
@ -1511,6 +1544,19 @@ public sealed class Transition
else
oi.State &= ~ObjectInfoState.OnWalkable;
}
else if (ci.LastKnownContactPlaneValid)
{
// L.2.3c: current contact lost transiently (e.g. StepUpSlide
// cleared it during a failed step-up) but the prior frame's
// contact is still valid — keep the mover grounded via the
// last-known plane. Without this, every wall bump dropped the
// player into the falling animation for one frame.
oi.State |= ObjectInfoState.Contact;
if (ci.LastKnownContactPlane.Normal.Z >= PhysicsGlobals.LandingZ)
oi.State |= ObjectInfoState.OnWalkable;
else
oi.State &= ~ObjectInfoState.OnWalkable;
}
else
{
oi.State &= ~(ObjectInfoState.Contact | ObjectInfoState.OnWalkable);