From 039149a9d04a1e553c5eae78b036f04b1038040d Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 4 May 2026 08:10:55 +0200 Subject: [PATCH] fix(motion): port ResolveWithTransition into env-var per-tick path (Commit B) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/AcDream.App/Rendering/GameWindow.cs | 93 +++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 655aee7..e44a975 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -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