From 6e8117741bd400da57ae75d736198ee27bf145c4 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 7 Jul 2026 13:39:28 +0200 Subject: [PATCH] feat(#182): PhysicsBody fsf state + CachedVelocity; ungate small-velocity-zero (Slice 1a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verbatim UpdatePhysicsInternal (0x00510700): the small-velocity-zero (<0.25 m/s) fires unconditionally, not gated on OnWalkable (the old acdream divergence) — gravity re-accelerates the same frame via the unconditional v += a*dt. Adds TransientStateFlags.Stationary{Fall,Stop,Stuck} (0x10/0x20/0x40) for the fsf round-trip, plus PhysicsBody.FramesStationaryFall and the separate CachedVelocity field (retail's two-velocity model — reporting/DR only, never fed to the integrator). All 1536 physics tests green. Co-Authored-By: Claude Opus 4.8 --- src/AcDream.Core/Physics/PhysicsBody.cs | 38 +++++++++++++++--- .../Physics/PhysicsBodyTests.cs | 40 +++++++++++++++++++ 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/AcDream.Core/Physics/PhysicsBody.cs b/src/AcDream.Core/Physics/PhysicsBody.cs index 897e90dc..9bd67c5c 100644 --- a/src/AcDream.Core/Physics/PhysicsBody.cs +++ b/src/AcDream.Core/Physics/PhysicsBody.cs @@ -66,6 +66,12 @@ public enum TransientStateFlags : uint Contact = 0x00000001, // bit 0 — touching any surface OnWalkable = 0x00000002, // bit 1 — standing on a walkable surface Sliding = 0x00000004, // bit 2 — carry sliding normal into next transition + // retail frames_stationary_fall carried across frames: transition() seeds fsf from + // these bits before the sweep (pc:280940-947); handle_all_collisions re-encodes fsf + // into them at the end of the frame (pc:282743/282749/282753). + StationaryFall = 0x00000010, // bit 4 — fsf == 1 + StationaryStop = 0x00000020, // bit 5 — fsf == 2 + StationaryStuck = 0x00000040, // bit 6 — fsf == 3 Active = 0x00000080, // bit 7 — object needs per-frame update } @@ -179,9 +185,28 @@ public sealed class PhysicsBody /// Orientation quaternion (struct offsets 0x60–0x80 column matrix). public Quaternion Orientation { get; set; } = Quaternion.Identity; - /// World-space velocity (+0xE0/E4/E8). + /// World-space velocity (+0xE0/E4/E8) — retail m_velocityVector: the INTENDED + /// velocity that the integrator advances (gravity, friction, MaxVelocity clamp) and that + /// drives the collision sweep. Zeroed by handle_all_collisions when + /// > 1 (the "bleed on block"). public Vector3 Velocity { get; set; } + /// Retail cached_velocity — a SEPARATE field from : the + /// REALIZED velocity (resolved displacement / dt) written after each transition in + /// UpdateObjectInternal (0x005158cb-005158ff, pc:283693). Read only for reporting / + /// dead-reckoning / camera slope-align (get_velocity 0x005113c0); it is NEVER fed back + /// into or the integrator. Kept separate per the verified retail + /// two-velocity model — do not collapse the two. + public Vector3 CachedVelocity { get; set; } + + /// Retail collision_info.frames_stationary_fall carried on the body between + /// frames. ValidateTransition increments it (0→1→2→3) when the sphere fails to advance and + /// resets it to 0 when it moves; at fsf≥2 an upward contact plane is manufactured; + /// handle_all_collisions zeros when fsf>1 (the airborne-stuck + /// bleed). Round-trips across frames via the Stationary* transient bits. validate_transition + /// pc:272625-656; handle_all_collisions pc:282695. + public int FramesStationaryFall { get; set; } + /// World-space acceleration (+0xEC/F0/F4). public Vector3 Acceleration { get; set; } @@ -497,11 +522,12 @@ public sealed class PhysicsBody calc_friction(dt, velocityMag2); - // If velocity fell below the "small" threshold after friction, stop. - // Only apply when grounded — while airborne, gravity must accumulate - // even when velocity is near zero (e.g., at jump apex). - if (velocityMag2 - SmallVelocitySquared < 0.0002f - && TransientState.HasFlag(TransientStateFlags.OnWalkable)) + // Retail UpdatePhysicsInternal (0x005107be): zero velocity below 0.25 m/s + // UNCONDITIONALLY — NOT gated on OnWalkable. At the jump apex this zeros the + // residual horizontal drift; the unconditional `Velocity += Acceleration * dt` + // below immediately re-applies gravity, so the fall still accumulates. (The old + // OnWalkable gate was an acdream divergence; the verbatim rebuild removes it.) + if (velocityMag2 - SmallVelocitySquared < 0.0002f) Velocity = Vector3.Zero; // Euler integration: position += v*dt + 0.5*a*dt² diff --git a/tests/AcDream.Core.Tests/Physics/PhysicsBodyTests.cs b/tests/AcDream.Core.Tests/Physics/PhysicsBodyTests.cs index f5593edd..1c936d34 100644 --- a/tests/AcDream.Core.Tests/Physics/PhysicsBodyTests.cs +++ b/tests/AcDream.Core.Tests/Physics/PhysicsBodyTests.cs @@ -162,6 +162,46 @@ public sealed class PhysicsBodyTests Assert.False(body.IsActive); } + [Fact] + public void UpdatePhysicsInternal_zeroes_small_velocity_even_when_airborne() + { + // Retail UpdatePhysicsInternal (0x005107be) zeroes velocity below 0.25 m/s + // UNCONDITIONALLY — NOT gated on OnWalkable. acdream previously gated it on + // OnWalkable; the verbatim rebuild removes the gate. Gravity re-accelerates the + // same frame via the unconditional `Velocity += Acceleration * dt`, so the fall + // still accumulates on Z. + var body = MakeAirborne(); // not Contact, not OnWalkable + body.set_velocity(new Vector3(0.1f, 0f, 0f)); // < 0.25 m/s + body.Acceleration = new Vector3(0f, 0f, PhysicsBody.Gravity); + + body.UpdatePhysicsInternal(1f / 30f); + + Assert.True(MathF.Abs(body.Velocity.X) < 1e-4f, $"X not zeroed: {body.Velocity.X}"); + Assert.True(body.Velocity.Z < 0f, $"gravity did not accumulate: {body.Velocity.Z}"); + } + + // ════════════════════════════════════════════════════════════════════ + // frames_stationary_fall carry state (retail transient_state bits) + // ════════════════════════════════════════════════════════════════════ + + [Fact] + public void TransientStateFlags_has_stationary_bits() + { + // retail transient_state StationaryFall/Stop/Stuck (handle_all_collisions + // pc:282743/282749/282753; seeded back into transition pc:280940-947). + Assert.Equal(0x10u, (uint)TransientStateFlags.StationaryFall); + Assert.Equal(0x20u, (uint)TransientStateFlags.StationaryStop); + Assert.Equal(0x40u, (uint)TransientStateFlags.StationaryStuck); + } + + [Fact] + public void PhysicsBody_has_fsf_and_cached_velocity_defaults() + { + var body = new PhysicsBody(); + Assert.Equal(0, body.FramesStationaryFall); + Assert.Equal(Vector3.Zero, body.CachedVelocity); + } + // ════════════════════════════════════════════════════════════════════ // set_velocity — velocity clamping // ════════════════════════════════════════════════════════════════════