fix(physics): port retail slope landing stop

This commit is contained in:
Erik 2026-07-31 09:10:53 +02:00
parent 1d8371dbe5
commit 5a0f9868a6
13 changed files with 870 additions and 103 deletions

View file

@ -5530,6 +5530,30 @@ public sealed class Transition
else if (transitionState != TransitionState.Invalid)
{
// Collision/slide/adjusted: revert to current position.
// Retail CTransition::validate_transition 0x0050AA70 consumes a
// remembered contact plane only on this non-OK recovery path. It
// first calls OBJECTINFO::kill_velocity, then restores the plane
// when the current sphere remains within radius + EPSILON. Omitting
// the kill preserved a landing reflection while repeatedly
// re-grounding the mover, producing Campaign P #269's long slide.
if (ci.LastKnownContactPlaneValid)
{
oi.StopVelocity();
var sphereCenter = sp.GlobalCurrCenter[0].Origin;
var radius = sp.GlobalSphere[0].Radius;
float angle = Vector3.Dot(ci.LastKnownContactPlane.Normal, sphereCenter)
+ ci.LastKnownContactPlane.D;
if (radius + PhysicsGlobals.EPSILON > MathF.Abs(angle))
{
ci.SetContactPlane(
ci.LastKnownContactPlane,
ci.LastKnownContactPlaneCellId,
ci.LastKnownContactPlaneIsWater);
}
}
if (!ci.CollisionNormalValid)
ci.SetCollisionNormal(Vector3.UnitZ); // default: push up
@ -5541,14 +5565,11 @@ public sealed class Transition
if (ci.CollisionNormalValid)
ci.SetSlidingNormal(ci.CollisionNormal);
// Preserve contact plane for next step.
// 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.
// Retail 0x0050ACFF-0x0050AD9A overwrites last-known validity with
// current contact validity, copies the plane only when valid, and then
// derives Contact/OnWalkable from that final plane. A remembered plane
// can have been restored only in the non-OK recovery branch above; it
// must never re-ground a clean move away from the surface.
if (ci.ContactPlaneValid)
{
ci.LastKnownContactPlaneValid = true;
@ -5569,79 +5590,9 @@ 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.
//
// L.2.4 (2026-04-30): PROXIMITY GUARD. Only trust the
// last-known plane if the sphere is still actually near it.
// Geometrically: `angle` is the signed distance from the
// sphere center to the plane. If |angle| exceeds the sphere
// radius (plus a tiny epsilon), the sphere has SEPARATED
// from the plane — typically because we fell off an edge or
// the body dropped vertically while the resolver bounced
// through edge-slide attempts. Without this guard the player
// gets stuck mid-fall in a falling animation forever (live
// bug 2026-04-30: cur.Z=96.6, check.Z=95.1 — 1.5 m below the
// remembered floor, but still being marked Contact + OnWalkable).
//
// Matches ACE PhysicsObj's pre-reuse check on the last-known
// plane and retail's CPhysicsObj::get_object_info logic.
// A6.P3 slice 1 (2026-05-21). Retail uses global_curr_center (NOT
// global_sphere->center) for this proximity check — see
// acclient_2013_pseudo_c.txt:272568. global_sphere is the START
// sphere of the transition; global_curr_center is the CURRENT center
// after sub-step accumulation. Using the wrong one made the proximity
// guard fire on the wrong reference point.
var sphereCenter = sp.GlobalCurrCenter[0].Origin;
var radius = sp.GlobalSphere[0].Radius;
float angle = Vector3.Dot(ci.LastKnownContactPlane.Normal, sphereCenter)
+ ci.LastKnownContactPlane.D;
if (radius + PhysicsGlobals.EPSILON > MathF.Abs(angle))
{
// ── Mechanism B — restore CP from LKCP per retail ────────────────
// A6.P3 slice 1 (2026-05-21). Retail oracle:
// acclient_2013_pseudo_c.txt:272577 (inside CTransition::validate_transition
// at line 272547). When the sphere is geometrically close to the
// LastKnownContactPlane, retail restores CP from LKCP via
// set_contact_plane(&collision_info, &last_known_contact_plane,
// last_known_contact_plane_is_water). This closes the gap that the
// stripped TryFindIndoorWalkablePlane synthesis path used to fill —
// when no fresh Path-6 CP write lands in this transition, CP is
// retained from the previous frame instead of being re-synthesized.
//
// NOTE: SetContactPlane also re-latches LKCP fields
// (TransitionTypes.cs:258-261), which is a no-op here since we
// pass LKCP as the source.
ci.SetContactPlane(
ci.LastKnownContactPlane,
ci.LastKnownContactPlaneCellId,
ci.LastKnownContactPlaneIsWater);
// Still close enough to the last-known plane — preserve
// grounded state. L.2.3i FloorZ test for OnWalkable.
oi.State |= ObjectInfoState.Contact;
if (ci.LastKnownContactPlane.Normal.Z >= PhysicsGlobals.FloorZ)
oi.State |= ObjectInfoState.OnWalkable;
else
oi.State &= ~ObjectInfoState.OnWalkable;
}
else
{
// Sphere has separated from the last-known plane.
// Drop the memory and let the body resolve normally
// (gravity → next-frame terrain probe → real contact).
ci.LastKnownContactPlaneValid = false;
oi.State &= ~(ObjectInfoState.Contact | ObjectInfoState.OnWalkable);
}
}
else
{
ci.LastKnownContactPlaneValid = false;
oi.State &= ~(ObjectInfoState.Contact | ObjectInfoState.OnWalkable);
}