using System.Numerics; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.Physics.Motion; using AcDream.Runtime.Entities; using AcDream.Runtime.Physics; namespace AcDream.Runtime.Tests.Physics; /// /// Bug B (2026-08-04) — remote characters froze on steep surfaces instead of /// sliding. The per-tick remote owner forged retail's two contact transients /// (Contact | OnWalkable) before every sweep, discarded the /// authoritative velocity, decided its landing edge from the contact-derived /// ResolveResult.IsOnGround rather than the plane-derived /// OnWalkable, and cleared the persistent Gravity state bit. /// /// /// Retail derives all of it: CPhysicsObj::SetPositionInternal /// (0x00515330) writes CONTACT_TS from /// collision_info.contact_plane_valid (0x00515430) and then routes /// ON_WALKABLE_TS through set_on_walkable (0x00511310) purely on /// contact_plane.N.z < PhysicsGlobals::floor_z /// (0x00515465-0x0051548E). set_on_walkable is the SOLE source of /// MovementManager::HitGround/::LeaveGround. Gravity survives a /// steep contact because calc_acceleration (0x00510950) only /// zeroes acceleration when CONTACT and ON_WALKABLE are BOTH set, and /// calc_friction (0x0050EE70) returns at its first line when /// ON_WALKABLE is clear. /// /// /// /// Every test here runs the production /// tick over a synthetic landblock whose terrain is a single constant-gradient /// ramp, so the contact plane the sweep finds is a real geometric result, not a /// stubbed value. /// /// public sealed class RuntimeRemoteSteepContactSlideTests { /// /// Ramp gradient chosen so the terrain-plane normal's Z lands just under /// retail's walkable limit — 52.4 degrees against a 48.4-degree limit, the /// same relationship as the live house roof that produced the freeze /// (measured contact-plane Normal.Z 0.6097 versus FloorZ 0.6642). /// private const float SteepGradient = 1.30f; /// A gentle ramp that is comfortably walkable. private const float WalkableGradient = 0.10f; [Fact] public void SteepTerrainProducesANonWalkableContactPlane() { using Harness harness = Harness.OnRamp(SteepGradient); Assert.True(harness.Remote.Body.ContactPlaneValid); Assert.InRange( harness.Remote.Body.ContactPlane.Normal.Z, 0.55f, PhysicsGlobals.FloorZ - 0.001f); } /// /// The landing edge must be the sweep's plane-derived /// OnWalkable, never IsOnGround (which is /// inContact || … and is therefore TRUE on a steep contact). /// [Fact] public void SteepContactDoesNotLatchALanding() { using Harness harness = Harness.OnRamp(SteepGradient); harness.Remote.Airborne = true; int groundEdges = 0; harness.Remote.Motion.RemoveLinkAnimations = () => groundEdges++; harness.Tick(40); Assert.True(harness.Remote.Body.InContact); Assert.False(harness.Remote.Body.OnWalkable); Assert.Equal(0, groundEdges); } /// /// Gravity is a persistent object property in retail; nothing on a ground /// edge may clear it. Before the fix both landing blocks did, which is why /// calc_acceleration returned zero forever afterwards. /// [Fact] public void GravityPersistsAcrossTicksOnASteepContact() { using Harness harness = Harness.OnRamp(SteepGradient); harness.Remote.Airborne = true; harness.Tick(40); Assert.True(harness.Remote.Body.HasGravity); Assert.True(harness.Remote.Body.Acceleration.Z < -1f); } /// /// The visible consequence: a remote resting on a non-walkable face keeps /// moving. Before the fix the body reported moved=0.0000 on every /// tick, forever. /// [Fact] public void SteepContactKeepsTheBodySlidingDownhill() { using Harness harness = Harness.OnRamp(SteepGradient); Vector3 start = harness.Remote.Body.Position; harness.Tick(40); Vector3 travelled = harness.Remote.Body.Position - start; Assert.True( travelled.Length() > 0.25f, $"expected a slide, body moved {travelled.Length():F4} m"); Assert.True( travelled.Z < -0.1f, $"expected downhill travel, dz = {travelled.Z:F4} m"); } /// /// The direct statement of "stop forging inputs": with no sweep to derive /// from — no starting cell, so ResolveWithTransition is skipped /// entirely — the tick must leave both retail transients exactly as it /// found them. Retail's only writer is SetPositionInternal /// (0x00515330), which a skipped transition never reaches. /// [Fact] public void TheTickNeverAssertsContactOrWalkableWithoutASweep() { using Harness harness = Harness.OnRamp(WalkableGradient); harness.Remote.CellId = 0u; harness.Remote.Body.TransientState &= ~(TransientStateFlags.Contact | TransientStateFlags.OnWalkable); harness.Remote.Airborne = false; harness.Tick(1); Assert.False(harness.Remote.Body.InContact); Assert.False(harness.Remote.Body.OnWalkable); } /// /// The tick immediately after a body crossed from walkable ground onto a /// steep face: it enters carrying last tick's grounded transients and its /// downhill speed. The tick must NOT re-assert those transients — with /// Contact | OnWalkable forced, calc_acceleration /// (0x00510950) returns zero and calc_friction /// (0x0050EE70) engages, so the body decelerates to a stop on a face /// retail would keep accelerating it down. /// [Fact] public void AGroundedTickOnASteepFaceReleasesTheBodyInsteadOfPinningIt() { using Harness harness = Harness.OnRamp(SteepGradient); harness.Remote.Body.TransientState |= TransientStateFlags.Contact | TransientStateFlags.OnWalkable; harness.Remote.Airborne = false; harness.Remote.Body.Velocity = Vector3.Normalize(new Vector3(0f, 1f, -SteepGradient)) * 3f; Vector3 start = harness.Remote.Body.Position; harness.Tick(40); Assert.False(harness.Remote.Body.OnWalkable); float travelled = (harness.Remote.Body.Position - start).Length(); Assert.True( travelled > 2f, $"expected the steep face to release the body, travelled {travelled:F3} m"); } /// /// Retail's MoveOrTeleport (0x00516330) never reads or writes /// the wire velocity for a remote; the deleted per-tick /// Body.Velocity = Zero threw away whatever ACE delivered through /// 0xF74E as well as everything gravity had accumulated. /// [Fact] public void AuthoritativeVelocityIsNotDiscardedOnAGroundedTick() { using Harness harness = Harness.OnRamp(WalkableGradient); Assert.False(harness.Remote.Airborne); harness.Remote.Body.Velocity = new Vector3(2.146f, 2.264f, -3.549f); harness.Tick(1); Assert.NotEqual(Vector3.Zero, harness.Remote.Body.Velocity); Assert.True( harness.Remote.Body.Velocity.X > 0.5f, $"velocity X was {harness.Remote.Body.Velocity.X:F4}"); } /// /// The committed transients must be the ones the sweep's contact plane /// implies — Contact from plane validity, OnWalkable from /// Normal.Z >= floor_z — and never an independently asserted pair. /// /// /// Deliberately NOT stated as the two equalities /// ContactPlaneValid == InContact and /// IsWalkableContact(committed plane) == OnWalkable. Neither is an /// invariant of the production code, and this test asserted both until the /// 2026-08-04 review: PhysicsEngine.ResolveWithTransition publishes /// the contact plane whenever the transition returned ok, while the /// transient commit additionally requires candidateMoved /// (RuntimeRemotePhysicsUpdater's SetPositionInternal commit, /// matching retail UpdateObjectInternal pc:283657), so a zero-move /// frame can legitimately leave the two one tick apart. The same writeback /// also falls back to LastKnownContactPlane, which keeps /// ContactPlaneValid true across a contact-FREE frame by design. /// The old assertions passed only because every body in this fixture moves /// on every tick. What is asserted instead is what the commit path DOES /// guarantee — the two implications — plus each fixture's known ramp /// geometry checked on BOTH sides of retail's walkability comparison, so /// re-forging Contact | OnWalkable still fails the steep case. /// /// [Fact] public void CommittedTransientsAgreeWithTheCommittedContactPlane() { using Harness steep = Harness.OnRamp(SteepGradient); steep.Tick(20); AssertTransientsAreContactPlaneDerived( steep.Remote.Body, expectWalkable: false); using Harness gentle = Harness.OnRamp(WalkableGradient); gentle.Tick(20); AssertTransientsAreContactPlaneDerived( gentle.Remote.Body, expectWalkable: true); } private static void AssertTransientsAreContactPlaneDerived( PhysicsBody body, bool expectWalkable) { // Unconditional: OnWalkable is only ever written as // `inContact && onWalkable` // (PhysicsObjUpdate.CommitSetPositionContactPrefix), so it cannot // outlive Contact on any frame, committed or not. Assert.True( !body.OnWalkable || body.InContact, "OnWalkable without Contact — the two transients were asserted " + "independently of the contact plane"); // Contact is written from the sweep's contact-plane validity by the // same resolve that publishes the plane, so a body in contact carries // a valid plane. The CONVERSE is not guaranteed — see the summary. Assert.True( !body.InContact || body.ContactPlaneValid, "Contact without a valid contact plane — Contact was not " + "plane-derived"); // Both sides of retail's walkability comparison // (SetPositionInternal 0x00515465-0x0051548E) against this fixture's // known constant-gradient ramp: the plane the sweep found, and the // transient that plane drove. Assert.Equal( expectWalkable, body.ContactPlane.Normal.Z >= PhysicsGlobals.FloorZ); Assert.Equal(expectWalkable, body.OnWalkable); } /// /// The other half of the edge: a genuine walkable landing must still fire /// retail's set_on_walkable(1) -> MovementManager::HitGround /// exactly once and leave the body grounded. /// [Fact] public void WalkableLandingStillLandsAndFiresTheGroundEdgeOnce() { using Harness harness = Harness.Airborne(WalkableGradient, height: 3f); int groundEdges = 0; harness.Remote.Motion.RemoveLinkAnimations = () => groundEdges++; harness.Tick(60); Assert.True(harness.Remote.Body.OnWalkable); Assert.False(harness.Remote.Airborne); Assert.Equal(1, groundEdges); } /// /// GRAVITY_PS is set by the retail CPhysicsObj constructor /// (state 0x400C08 @0x00512508) and thereafter assigned wholesale from the /// wire by set_description's set_state (0x00514DD0), /// which post-processes only lighting/nodraw/hidden. No ground edge /// anywhere in retail toggles it — acdream's two landing blocks did, which /// is what left a landed remote permanently unable to fall again. /// [Fact] public void WalkableLandingDoesNotClearTheGravityStateBit() { using Harness harness = Harness.Airborne(WalkableGradient, height: 3f); harness.Tick(60); Assert.True(harness.Remote.Body.OnWalkable); Assert.True(harness.Remote.Body.HasGravity); } private sealed class Harness : IDisposable { private const uint LandblockId = 0x0101FFFFu; private readonly RuntimeEntityObjectLifetime _lifetime; private readonly RuntimeEntityRecord _record; private readonly RuntimeRemotePhysicsUpdater _updater; internal RemoteMotion Remote { get; } private Harness( RuntimeEntityObjectLifetime lifetime, RuntimeEntityRecord record, RemoteMotion remote, RuntimeRemotePhysicsUpdater updater) { _lifetime = lifetime; _record = record; Remote = remote; _updater = updater; } /// Body already resting on the ramp, contact established. internal static Harness OnRamp(float gradient) { Harness harness = Create(gradient, heightAboveSurface: 0f); // Retail gains spawn contact from the first gravity frame; the // stationary-remote settle (SpawnPlacementSettler, #270) compresses // it. Use the production seam so the fixture starts from exactly // the state a live spawn would. SpawnPlacementSettler.TrySettle( harness._lifetime.Physics.Engine, harness.Remote.Body, harness.Remote.Body.Position, harness.Remote.CellId, sphereRadius: 0.48f, sphereHeight: 1.835f, ObjectInfoState.EdgeSlide, harness._record.LocalEntityId!.Value, harness.Remote.Movement.HitGround, harness.Remote.Motion.LeaveGround); harness.Remote.Airborne = !harness.Remote.Body.OnWalkable; return harness; } /// Body suspended above the ramp with no contact at all. internal static Harness Airborne(float gradient, float height) { Harness harness = Create(gradient, heightAboveSurface: height); harness.Remote.Body.TransientState &= ~(TransientStateFlags.Contact | TransientStateFlags.OnWalkable); harness.Remote.Body.ContactPlaneValid = false; harness.Remote.Airborne = true; return harness; } private static Harness Create(float gradient, float heightAboveSurface) { var lifetime = new RuntimeEntityObjectLifetime(); lifetime.Physics.Engine.AddLandblock( LandblockId, Ramp(gradient), Array.Empty(), Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f); RuntimeEntityRecord record = lifetime.Entities.AddActive(Spawn()); var body = new PhysicsBody { // Retail CPhysicsObj constructor state 0x400C08 @0x00512508 // (EdgeSlide | Lighting | Gravity | ReportCollisions), which // ACE also sends for every creature (PhysicsGlobals.DefaultState). State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.EdgeSlide, InWorld = true, }; var remote = new RemoteMotion(body); lifetime.Entities.SetPhysicsBody(record, body); lifetime.Entities.SetRemoteMotion(record, remote); lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true); const float localX = 96f; const float localY = 96f; float surfaceZ = Ramp(gradient).SampleZ(localX, localY); body.Position = new Vector3( localX, localY, surfaceZ + heightAboveSurface); body.Orientation = Quaternion.Identity; remote.CellId = TerrainSurface.ComputeOutdoorCellId( LandblockId, localX, localY); remote.LastServerPos = body.Position; remote.LastServerPosTime = 1.0; return new Harness( lifetime, record, remote, new RuntimeRemotePhysicsUpdater(lifetime.Physics)); } internal void Tick(int count, float dt = 1f / 30f) { var frame = new MotionDeltaFrame(); for (int i = 0; i < count; i++) { frame.Reset(); _updater.Tick( _record, Remote, objectScale: 1f, sequencer: null, dt, _record.ObjectClockEpoch, frame, radius: 0.48f, height: 1.835f, liveCenterX: 1, liveCenterY: 1); } } /// /// A constant-gradient ramp climbing along +Y. The heightmap byte at /// (x, y) indexes a table whose entries rise linearly, so every cell of /// the landblock has the same plane normal and the sampled contact /// plane is exactly normalize((0, -gradient, 1)). /// private static TerrainSurface Ramp(float gradient) { var heightTable = new float[256]; for (int i = 0; i < heightTable.Length; i++) heightTable[i] = i * gradient * TerrainSurface.CellSize; var heights = new byte[81]; for (int x = 0; x < 9; x++) for (int y = 0; y < 9; y++) heights[x * 9 + y] = (byte)(8 - y); return new TerrainSurface(heights, heightTable); } private static WorldSession.EntitySpawn Spawn() { var position = new CreateObject.ServerPosition( LandblockId, 96f, 96f, 0f, 1f, 0f, 0f, 0f); var timestamps = new PhysicsTimestamps( Position: 1, Movement: 1, State: 1, Vector: 1, Teleport: 0, ServerControlledMove: 1, ForcePosition: 0, ObjDesc: 1, Instance: 1); const uint rawState = (uint)(PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.EdgeSlide); var physics = new PhysicsSpawnData( RawState: rawState, Position: position, Movement: null, AnimationFrame: null, SetupTableId: 0x02000001u, MotionTableId: 0x09000001u, SoundTableId: null, PhysicsScriptTableId: null, Parent: null, Children: null, Scale: null, Friction: null, Elasticity: null, Translucency: null, Velocity: null, Acceleration: null, AngularVelocity: null, DefaultScriptType: null, DefaultScriptIntensity: null, Timestamps: timestamps); return new WorldSession.EntitySpawn( 0x70000101u, position, 0x02000001u, Array.Empty(), Array.Empty(), Array.Empty(), null, null, "bug-b-fixture", null, null, 0x09000001u, PhysicsState: rawState, InstanceSequence: 1, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics); } public void Dispose() => _lifetime.Dispose(); } }