fix(physics): L.2.3a — retail-realistic step heights (was 5m up, 4cm down)

Two values were producing weird live-test behavior:

- PlayerMovementController.StepUpHeight default = 5.0f (5 meters) and
  GameWindow's fallback = 2.0f. With these, walking horizontally into
  a steep slope let the step-up scan find walkable polygons up to 5m
  away, which often included a small building's flat top. The player
  visually "teleported" up onto the roof and then could walk on
  surfaces they should have just slid off.

- stepDownHeight was hardcoded 0.04f (4 cm) in two ResolveWithTransition
  call sites. A typical stair step is 15–25 cm tall, so when the player
  walked off the top of a stair onto level ground, the step-down probe
  didn't reach the next surface. For one frame the contact plane was
  invalid → ValidateTransition cleared OnWalkable → animation flickered
  to falling → next frame gravity dropped + terrain found. Visible 1-frame
  flicker reported as "small falling animation when reaching stair top."

Retail's Setup.step_up_height and Setup.step_down_height for human
characters are both ~0.4 m. Sourcing them from the player's Setup
(already cached in PhysicsDataCache) with a 0.4 m fallback when
the field is missing.

Files:
- PlayerMovementController.cs:104 — StepUpHeight default 5.0 → 0.4
- PlayerMovementController.cs (new) — StepDownHeight property, default 0.4
- PlayerMovementController.cs:414 — pass StepDownHeight from controller
- GameWindow.cs:7019-7036 — read Setup.StepDownHeight + reduce fallbacks
- GameWindow.cs:5759 — remote dead-reckoning: 2.0/0.04 → 0.4/0.4

No test changes; existing 12 BSPStepUp tests still cover the value flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-29 17:23:54 +02:00
parent 670f892bd3
commit b2aaac4e52
2 changed files with 34 additions and 10 deletions

View file

@ -97,11 +97,25 @@ public sealed class PlayerMovementController
/// <summary> /// <summary>
/// Maximum Z increase per movement step before the move is rejected. /// Maximum Z increase per movement step before the move is rejected.
/// AC's default StepUpHeight for human characters is ~2 units. /// Retail's <c>step_up_height</c> for human characters is ~0.4 m (hip-
/// Using 5 for the MVP to be forgiving — prevents walking up vertical /// level). Setting this too high lets the player teleport up small
/// walls but allows stairs, ramps, and terrain slopes. /// buildings via the step-up scan finding any walkable polygon within
/// reach (Bug 3 in L.2.3 testing — walking into a steep slope mounted
/// the building's flat top instead of sliding off the slope).
/// Authoritative source is the player's <c>Setup.StepUpHeight</c> set
/// in GameWindow.cs at world-entry time.
/// </summary> /// </summary>
public float StepUpHeight { get; set; } = 5.0f; public float StepUpHeight { get; set; } = 0.4f;
/// <summary>
/// L.2.3a (2026-04-29): how far below the foot the step-down probe
/// reaches when transitioning between surfaces. Retail's
/// <c>step_down_height</c> for human characters is ~0.4 m. With the
/// previous 4 cm hardcoded value, walking off the top of a stair onto
/// the ground 25 cm below produced a one-frame contact-plane gap — the
/// animation system briefly flickered to falling.
/// </summary>
public float StepDownHeight { get; set; } = 0.4f;
/// <summary> /// <summary>
/// Current portal-space state. Set to PortalSpace when the server sends /// Current portal-space state. Set to PortalSpace when the server sends
@ -411,7 +425,7 @@ public sealed class PlayerMovementController
sphereRadius: 0.48f, // human player radius from Setup sphereRadius: 0.48f, // human player radius from Setup
sphereHeight: 1.2f, // human player height from Setup sphereHeight: 1.2f, // human player height from Setup
stepUpHeight: StepUpHeight, stepUpHeight: StepUpHeight,
stepDownHeight: 0.04f, // retail default stepDownHeight: StepDownHeight, // L.2.3a: from Setup.StepDownHeight
isOnGround: _body.OnWalkable, isOnGround: _body.OnWalkable,
body: _body, // persist ContactPlane across frames for slope tracking body: _body, // persist ContactPlane across frames for slope tracking
// Commit C 2026-04-29 — local player is always IsPlayer. // Commit C 2026-04-29 — local player is always IsPlayer.

View file

@ -5755,8 +5755,8 @@ public sealed class GameWindow : IDisposable
preIntegratePos, postIntegratePos, rm.CellId, preIntegratePos, postIntegratePos, rm.CellId,
sphereRadius: 0.48f, sphereRadius: 0.48f,
sphereHeight: 1.2f, sphereHeight: 1.2f,
stepUpHeight: 2.0f, // retail default for unknown remotes stepUpHeight: 0.4f, // L.2.3a: retail human-scale, was 2.0f
stepDownHeight: 0.04f, // PhysicsGlobals.DefaultStepHeight stepDownHeight: 0.4f, // L.2.3a: retail human-scale, was 0.04f
// K-fix9 (2026-04-26): mirror the K-fix7 gate — // K-fix9 (2026-04-26): mirror the K-fix7 gate —
// airborne remotes must NOT pre-seed the // airborne remotes must NOT pre-seed the
// ContactPlane, otherwise AdjustOffset's snap-to-plane // ContactPlane, otherwise AdjustOffset's snap-to-plane
@ -7016,7 +7016,13 @@ public sealed class GameWindow : IDisposable
_playerController.SetCharacterSkills(_lastSeenRunSkill, _lastSeenJumpSkill); _playerController.SetCharacterSkills(_lastSeenRunSkill, _lastSeenJumpSkill);
Console.WriteLine($"live: {loggingTag} — applied server skills run={_lastSeenRunSkill} jump={_lastSeenJumpSkill}"); Console.WriteLine($"live: {loggingTag} — applied server skills run={_lastSeenRunSkill} jump={_lastSeenJumpSkill}");
} }
// Read the real step height from the player's Setup dat. // Read the real step heights from the player's Setup dat.
// L.2.3a (2026-04-29): retail's Setup.StepUpHeight for humans is
// ~0.4 m, NOT 2 m. With 2 m fallback the step-up scan reached
// small-building roofs and teleported the player onto them. Same
// for StepDownHeight — was hardcoded 0.04 m, causing stair-top
// contact-plane gaps. Both now come from Setup with retail-realistic
// 0.4 m fallbacks.
if (_dats is not null && (playerEntity.SourceGfxObjOrSetupId & 0xFF000000u) == 0x02000000u) if (_dats is not null && (playerEntity.SourceGfxObjOrSetupId & 0xFF000000u) == 0x02000000u)
{ {
var playerSetup = _dats.Get<DatReaderWriter.DBObjs.Setup>(playerEntity.SourceGfxObjOrSetupId); var playerSetup = _dats.Get<DatReaderWriter.DBObjs.Setup>(playerEntity.SourceGfxObjOrSetupId);
@ -7024,11 +7030,15 @@ public sealed class GameWindow : IDisposable
_physicsDataCache.CacheSetup(playerEntity.SourceGfxObjOrSetupId, playerSetup); _physicsDataCache.CacheSetup(playerEntity.SourceGfxObjOrSetupId, playerSetup);
_playerController.StepUpHeight = (playerSetup is not null && playerSetup.StepUpHeight > 0f) _playerController.StepUpHeight = (playerSetup is not null && playerSetup.StepUpHeight > 0f)
? playerSetup.StepUpHeight ? playerSetup.StepUpHeight
: 2f; : 0.4f;
_playerController.StepDownHeight = (playerSetup is not null && playerSetup.StepDownHeight > 0f)
? playerSetup.StepDownHeight
: 0.4f;
} }
else else
{ {
_playerController.StepUpHeight = 2f; _playerController.StepUpHeight = 0.4f;
_playerController.StepDownHeight = 0.4f;
} }
int plbX = _liveCenterX + (int)MathF.Floor(playerEntity.Position.X / 192f); int plbX = _liveCenterX + (int)MathF.Floor(playerEntity.Position.X / 192f);
int plbY = _liveCenterY + (int)MathF.Floor(playerEntity.Position.Y / 192f); int plbY = _liveCenterY + (int)MathF.Floor(playerEntity.Position.Y / 192f);