fix(motion): port ResolveWithTransition into env-var per-tick path (Commit B)

Restores per-frame collision/terrain sweep that was DROPPED by e94e791
(L.3.1+L.3.2 Task 3) when the ACDREAM_INTERP_MANAGER=1 path replaced
the per-tick logic with a stripped-down version intended to mirror
retail's CPhysicsObj::MoveOrTeleport.

That was a category error: MoveOrTeleport (acclient @ 0x00516330) is
the *network packet handler* entry point — minimal work. The per-frame
physics tick is retail's update_object (FUN_00515020) — full chain
including FUN_005148A0 Transition::FindTransitionalPosition (the
collision sweep). The legacy (env-var off) path mirrors update_object
correctly; the env-var path was missing this single step.

Symptoms that map directly to the missing sweep:
  - "Staircase" Z drift on slopes (horizontal Euler motion sinks into
    rising ground until the next UP pops it up). User-confirmed for
    BOTH retail-driven AND acdream-driven remotes when observed from
    acdream.
  - Position blips during steady-state motion (predicted XY drifts
    unconstrained between UPs, then UP hard-snaps).

Fix: copy the legacy path's "Step 4: collision sweep" block (lines
~6483-6569) into the env-var per-frame branch, between
UpdatePhysicsInternal and the existing landing fallback. Includes
post-resolve landing detection (K-fix15 + K-fix17 mirror) so airborne
remotes correctly transition back to grounded after the sweep clamps
them to a walkable surface.

Sphere dims match the legacy path verbatim (0.48m radius, 1.2m height,
0.4m step-up/down, EdgeSlide moverFlags) — retail human-scale, already
proven via the legacy path before the e94e791 regression.

Does NOT address the separate Run↔Walk cycle bug (different root
cause: missing velocity-derived cycle inference for player remotes).
That's a follow-up commit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-04 08:10:55 +02:00
parent eaa8fc5c67
commit 039149a9d0

View file

@ -6117,6 +6117,12 @@ public sealed class GameWindow : IDisposable
// semantics — retail's PositionManager::adjust_offset
// overwrites the offset frame with the catch-up direction,
// not adding to it.
//
// 2026-05-03 (Commit B fix for staircase regression): capture
// the pre-translation position so the collision sweep below
// (Step 4b) can resolve the full per-tick movement through
// BSP + terrain.
var preIntegratePos = rm.Body.Position;
float maxSpeed = rm.Motion.GetMaxSpeed();
System.Numerics.Vector3 offset = rm.Position.ComputeOffset(
dt: (double)dt,
@ -6181,6 +6187,93 @@ public sealed class GameWindow : IDisposable
// Step 4: physics integration (Euler: pos += vel*dt + 0.5*accel*dt²).
rm.Body.UpdatePhysicsInternal(dt);
// Step 4b (Commit B fix 2026-05-03): collision sweep — port of
// retail update_object's FUN_005148A0 Transition::FindTransitionalPosition.
// This was MISSING in the env-var path introduced by e94e791
// (L.3.1+L.3.2 Task 3). The legacy (env-var off) path at the
// bottom of this function has it (line ~6483 "Step 4: collision
// sweep"); we just need the same call here.
//
// Without this:
// - Body Z drifts on slopes (visible "staircase" — horizontal
// Euler motion up a slope sinks into rising ground until
// the next UP pops it up).
// - Body slides through walls / objects between UPs.
// - Step-up / step-down doesn't engage on ledges.
// - Edge-slide doesn't engage on cliff edges.
//
// The env-var path was originally designed to mirror retail
// CPhysicsObj::MoveOrTeleport (acclient @ 0x00516330) — a network
// packet handler entry point that does minimal work. But
// TickAnimations is the per-frame physics tick (mirrors retail
// FUN_00515020 update_object), which DOES include the collision
// sweep. Adding the sweep here makes the env-var path retail-
// faithful for the per-frame tick (matching the legacy path,
// which had it).
var postIntegratePos = rm.Body.Position;
if (rm.CellId != 0 && _physicsEngine.LandblockCount > 0)
{
// Sphere dims match local-player + legacy-path defaults
// (~0.48m radius, ~1.2m height humanoid). Step-up/down 0.4m
// matches L.2.3a retail human-scale. EdgeSlide is the retail
// default mover-flags state.
var resolveResult = _physicsEngine.ResolveWithTransition(
preIntegratePos, postIntegratePos, rm.CellId,
sphereRadius: 0.48f,
sphereHeight: 1.2f,
stepUpHeight: 0.4f,
stepDownHeight: 0.4f,
// Airborne remotes must NOT pre-seed the ContactPlane —
// mirrors K-fix9 in the legacy path; otherwise
// AdjustOffset's snap-to-plane branch zeroes the +Z
// offset every step on a jump arc.
isOnGround: !rm.Airborne,
body: rm.Body,
moverFlags: AcDream.Core.Physics.ObjectInfoState.EdgeSlide);
rm.Body.Position = resolveResult.Position;
if (resolveResult.CellId != 0)
rm.CellId = resolveResult.CellId;
// Post-resolve landing detection — mirrors K-fix15 in the
// legacy path. When the resolver says we're on ground AND
// velocity is no longer pointing up, transition back to
// grounded. Without this, gravity keeps building negative Z
// velocity until the sphere-sweep clamps each frame, but
// Airborne stays true forever.
if (rm.Airborne
&& resolveResult.IsOnGround
&& rm.Body.Velocity.Z <= 0f)
{
rm.Airborne = false;
rm.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Contact
| AcDream.Core.Physics.TransientStateFlags.OnWalkable;
rm.Body.State &= ~AcDream.Core.Physics.PhysicsStateFlags.Gravity;
rm.Body.Velocity = new System.Numerics.Vector3(
rm.Body.Velocity.X, rm.Body.Velocity.Y, 0f);
rm.Motion.HitGround();
// Reset sequencer cycle from Falling back to whatever
// InterpretedState says. Mirrors K-fix17 in the legacy
// path.
if (ae.Sequencer is not null)
{
uint landStyle = ae.Sequencer.CurrentStyle != 0
? ae.Sequencer.CurrentStyle
: 0x8000003Du;
uint landingCmd = rm.Motion.InterpretedState.ForwardCommand;
if (landingCmd == 0)
landingCmd = AcDream.Core.Physics.MotionCommand.Ready;
float landingSpeed = rm.Motion.InterpretedState.ForwardSpeed;
if (landingSpeed <= 0f) landingSpeed = 1f;
ae.Sequencer.SetCycle(landStyle, landingCmd, landingSpeed);
}
if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1")
Console.WriteLine($"VU.land guid=0x{serverGuid:X8} Z={rm.Body.Position.Z:F2}");
}
}
// Step 5: landing fallback. The retail-faithful path leaves
// the landing transition to OnLivePositionUpdated when ACE
// sends IsGrounded=true. In practice ACE doesn't always