fix(motion): port ResolveWithTransition into env-var per-tick path (Commit B)
Restores per-frame collision/terrain sweep that was DROPPED bye94e791(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 thee94e791regression. 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:
parent
eaa8fc5c67
commit
039149a9d0
1 changed files with 93 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue