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();
}
}