fix(physics): enforce retail step-down support radius (#273)

This commit is contained in:
Erik 2026-07-31 12:10:03 +02:00
parent 4dd40ad8fe
commit c24bc571cf
9 changed files with 2117 additions and 24 deletions

View file

@ -284,28 +284,46 @@ public static class BSPQuery
CollisionSphere sphere,
Vector3 up,
bool small)
=> CheckWalkableSupport(
poly.Plane,
poly.Vertices,
sphere.Center,
small ? sphere.Radius * 0.5f : sphere.Radius,
up);
/// <summary>
/// Retail <c>CPolygon::check_walkable</c> against an already resolved
/// polygon. The caller supplies the effective support radius; retail's
/// <c>SPHEREPATH::check_walkables</c> halves its saved sphere before
/// entering this routine.
/// </summary>
internal static bool CheckWalkableSupport(
Plane plane,
ReadOnlySpan<Vector3> vertices,
Vector3 center,
float supportRadius,
Vector3 up)
{
float angleUp = Vector3.Dot(poly.Plane.Normal, up);
float angleUp = Vector3.Dot(plane.Normal, up);
if (angleUp < PhysicsGlobals.EPSILON) return false;
float angle = (Vector3.Dot(poly.Plane.Normal, sphere.Center) + poly.Plane.D) / angleUp;
var center = sphere.Center - up * angle;
float angle = (Vector3.Dot(plane.Normal, center) + plane.D) / angleUp;
center -= up * angle;
float radsum = sphere.Radius * sphere.Radius;
if (small) radsum *= 0.25f;
float radsum = supportRadius * supportRadius;
int n = poly.Vertices.Length;
int n = vertices.Length;
int prevIdx = n - 1;
for (int i = 0; i < n; i++)
{
var v = poly.Vertices[i];
var lv = poly.Vertices[prevIdx];
var v = vertices[i];
var lv = vertices[prevIdx];
prevIdx = i;
var edge = v - lv;
var disp = center - lv;
var cross = Vector3.Cross(poly.Plane.Normal, edge);
var cross = Vector3.Cross(plane.Normal, edge);
float diff = Vector3.Dot(disp, cross);
if (diff < 0f)

View file

@ -850,6 +850,27 @@ public sealed class SpherePath
return true;
}
/// <summary>
/// Retail <c>SPHEREPATH::check_walkables</c> (0x0050C3E0). A missing
/// remembered polygon passes; otherwise the foot sphere is tested against
/// that polygon with half its normal radius. Retail mutates an embedded
/// scratch sphere before the test. Our world-space representation can pass
/// the equivalent effective radius without mutating canonical sphere state.
/// </summary>
internal bool CheckWalkables()
{
if (!HasWalkablePolygon || WalkableVertices is null)
return true;
var footSphere = GlobalSphere[0];
return BSPQuery.CheckWalkableSupport(
WalkablePlane,
WalkableVertices,
footSphere.Origin,
footSphere.Radius * 0.5f,
WalkableUp);
}
/// <summary>
/// Retail <c>SPHEREPATH::init</c> reset for a retained transition record.
/// Every logical value is restored; only the two private exact-length
@ -5211,6 +5232,22 @@ public sealed class Transition
&& CollisionInfo.ContactPlaneValid
&& CollisionInfo.ContactPlane.Normal.Z >= walkableZ)
{
// Retail CTransition::step_down (0x0050B2A0) validates the
// candidate's actual support before placement whenever an
// already-grounded mover is not performing a step-up. The first
// check uses SPHEREPATH::check_walkables' half-radius foot sphere;
// when that fails, DoCheckWalkable performs the downward BSP
// probe with the same small-support rule. Omitting this gate let a
// wall/post slide leave most of the player sphere beyond a floor
// edge while the full-radius step-down overlap still counted as
// grounded (issue #273).
if (ObjectInfo.EdgeSlide
&& !sp.StepUp
&& !DoCheckWalkable(walkableZ, engine))
{
return false;
}
// L.2.3h (2026-04-29): Placement validation is for the
// DoStepUp use case (prevents climbing through walls by
// stepping up onto ground beyond a tall wall). For the
@ -5461,8 +5498,10 @@ public sealed class Transition
if ((oi.State & ObjectInfoState.OnWalkable) == 0)
return true;
// If the current walkable entry is still valid, skip the probe.
if (sp.WalkableValid)
// Retail first validates the remembered polygon with a half-radius
// support sphere. Merely having a polygon pointer is insufficient:
// the candidate may already hang too far beyond its edge.
if (sp.CheckWalkables())
return true;
sp.SaveCheckPos();