diff --git a/tests/AcDream.Runtime.Tests/Physics/RemoteRampHarness.cs b/tests/AcDream.Runtime.Tests/Physics/RemoteRampHarness.cs
new file mode 100644
index 00000000..e7f42443
--- /dev/null
+++ b/tests/AcDream.Runtime.Tests/Physics/RemoteRampHarness.cs
@@ -0,0 +1,272 @@
+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;
+
+///
+/// Drives the production tick over a
+/// synthetic single-landblock world whose terrain is a constant-gradient ramp,
+/// so every contact plane the sweep reports is a real geometric result rather
+/// than a stubbed value.
+///
+/// Extracted 2026-08-06 from RuntimeRemoteSteepContactSlideTests
+/// (where it was a private nested class) so the AD-10 slope-projection tests
+/// can build on the same fixture instead of cloning it. Behaviour is
+/// unchanged; the only additions are ,
+/// , and the root-motion-driving
+/// overload.
+///
+internal sealed class RemoteRampHarness : IDisposable
+{
+ private const uint LandblockId = 0x0101FFFFu;
+
+ /// Local XY the body is placed at, near the landblock centre.
+ internal const float StartX = 96f;
+
+ /// Local XY the body is placed at, near the landblock centre.
+ internal const float StartY = 96f;
+
+ private readonly RuntimeEntityObjectLifetime _lifetime;
+ private readonly RuntimeEntityRecord _record;
+ private readonly RuntimeRemotePhysicsUpdater _updater;
+
+ internal RemoteMotion Remote { get; }
+
+ ///
+ /// The exact terrain this fixture published into the engine. Tests read
+ /// the ground's own geometry from here, so an assertion about "is the body
+ /// on the surface" is answered by the surface rather than by
+ /// re-implementing whatever the code under test computed.
+ ///
+ internal TerrainSurface Surface { get; }
+
+ internal PhysicsEngine Engine => _lifetime.Physics.Engine;
+
+ private RemoteRampHarness(
+ RuntimeEntityObjectLifetime lifetime,
+ RuntimeEntityRecord record,
+ RemoteMotion remote,
+ RuntimeRemotePhysicsUpdater updater,
+ TerrainSurface surface)
+ {
+ _lifetime = lifetime;
+ _record = record;
+ Remote = remote;
+ _updater = updater;
+ Surface = surface;
+ }
+
+ /// Terrain height at a world-space XY (the landblock sits at 0,0).
+ internal float SurfaceZ(float worldX, float worldY)
+ => Surface.SampleZ(worldX, worldY);
+
+ /// Terrain height directly under the body's current XY.
+ internal float SurfaceZUnderBody()
+ => SurfaceZ(Remote.Body.Position.X, Remote.Body.Position.Y);
+
+ /// Body already resting on the ramp, contact established.
+ internal static RemoteRampHarness OnRamp(float gradient)
+ {
+ RemoteRampHarness 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 RemoteRampHarness Airborne(float gradient, float height)
+ {
+ RemoteRampHarness 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 RemoteRampHarness Create(float gradient, float heightAboveSurface)
+ {
+ var lifetime = new RuntimeEntityObjectLifetime();
+ TerrainSurface surface = Ramp(gradient);
+ lifetime.Physics.Engine.AddLandblock(
+ LandblockId,
+ surface,
+ 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);
+
+ float surfaceZ = surface.SampleZ(StartX, StartY);
+ body.Position = new Vector3(
+ StartX,
+ StartY,
+ surfaceZ + heightAboveSurface);
+ body.Orientation = Quaternion.Identity;
+ remote.CellId = TerrainSurface.ComputeOutdoorCellId(
+ LandblockId,
+ StartX,
+ StartY);
+ remote.LastServerPos = body.Position;
+ remote.LastServerPosTime = 1.0;
+
+ return new RemoteRampHarness(
+ lifetime,
+ record,
+ remote,
+ new RuntimeRemotePhysicsUpdater(lifetime.Physics),
+ surface);
+ }
+
+ /// Tick with an empty animation root-motion frame.
+ internal void Tick(int count, float dt = 1f / 30f)
+ => Tick(count, Vector3.Zero, dt);
+
+ ///
+ /// Tick while CSequence::update reports the given body-local root
+ /// displacement every frame — the locomotion-cycle push that drives a
+ /// running remote between server position updates.
+ ///
+ internal void Tick(int count, Vector3 rootMotionLocalPerTick, float dt = 1f / 30f)
+ {
+ var frame = new MotionDeltaFrame();
+ for (int i = 0; i < count; i++)
+ {
+ frame.Reset();
+ frame.Origin = rootMotionLocalPerTick;
+ _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. 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 a single constant plane.
+ ///
+ 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,
+ StartX,
+ StartY,
+ 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,
+ "remote-ramp-fixture",
+ null,
+ null,
+ 0x09000001u,
+ PhysicsState: rawState,
+ InstanceSequence: 1,
+ MovementSequence: 1,
+ ServerControlSequence: 1,
+ PositionSequence: 1,
+ Physics: physics);
+ }
+
+ public void Dispose() => _lifetime.Dispose();
+}
diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimeRemoteSlopeProjectionTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimeRemoteSlopeProjectionTests.cs
new file mode 100644
index 00000000..da7e1e5d
--- /dev/null
+++ b/tests/AcDream.Runtime.Tests/Physics/RuntimeRemoteSlopeProjectionTests.cs
@@ -0,0 +1,160 @@
+using System.Numerics;
+using AcDream.Core.Physics;
+
+namespace AcDream.Runtime.Tests.Physics;
+
+///
+/// AD-10 — the remote slope projection. Retail projects the per-sub-step
+/// movement offset onto collision_info.contact_plane INSIDE the sweep
+/// (CTransition::adjust_offset 0x0050a370, pc:272271-272393,
+/// called once per step from find_transitional_position
+/// 0x0050bdf0). acdream ports that faithfully in
+/// Transition.AdjustOffset, and remotes run that sweep — so the extra
+/// pre-sweep projection the remote tick used to apply at the combiner boundary
+/// was a second copy of the same operation against a single-point terrain
+/// sample. It was deleted 2026-08-06 after the measurement these tests carry.
+///
+/// Every assertion here runs the production
+/// tick and
+/// takes its expected Z from the fixture's own terrain geometry, never from a
+/// re-implementation of the projection formula — so a projection against the
+/// WRONG plane produces a wrong answer rather than a self-consistent one.
+///
+public sealed class RuntimeRemoteSlopeProjectionTests
+{
+ ///
+ /// A comfortably walkable ramp steep enough that a failure to track it is
+ /// unmistakable: plane normal Z = 1/sqrt(1.36) ≈ 0.8575 (30.96 degrees)
+ /// against retail's 0.6642 floor_z limit (48.4 degrees). The ramp descends
+ /// along +Y, so +Y root motion is downhill.
+ ///
+ private const float WalkableSlopeGradient = 0.6f;
+
+ ///
+ /// Body-local root displacement per tick — the locomotion-cycle push that
+ /// moves a running remote between server position updates. 0.1 m at 30 Hz
+ /// is a 3 m/s run.
+ ///
+ private const float RootMotionPerTick = 0.10f;
+
+ private const int TrackedTicks = 30;
+
+ ///
+ /// How far the body's root may drift from the terrain surface directly
+ /// below it, relative to where the settled fixture put it. The measured
+ /// drift on this fixture is under 1e-4 m; 30 unprojected ticks down this
+ /// ramp accumulate about 1.8 m, so this band is ~350x below the failure it
+ /// must catch and ~50x above the float noise it must tolerate.
+ ///
+ private const float SurfaceTrackingToleranceMeters = 0.005f;
+
+ ///
+ /// The ramp's own plane, derived from the fixture heightmap rather than
+ /// from anything the code under test computed. The ramp descends along +Y
+ /// at gradient, so its plane is gradient*y + z = c and its
+ /// unit normal is (0, gradient, 1) normalized.
+ ///
+ private static Vector3 RampNormal(float gradient)
+ => Vector3.Normalize(new Vector3(0f, gradient, 1f));
+
+ ///
+ /// Fixture validation, run before any motion assertion: the sweep really
+ /// does report this ramp's own geometric plane, and that plane is walkable.
+ /// A fixture whose contact plane were flat could not discriminate anything.
+ ///
+ [Fact]
+ public void TheFixtureRampIsWalkableAndItsPlaneIsTheGeometricOne()
+ {
+ using RemoteRampHarness harness =
+ RemoteRampHarness.OnRamp(WalkableSlopeGradient);
+
+ Assert.True(harness.Remote.Body.OnWalkable);
+ Assert.True(harness.Remote.Body.ContactPlaneValid);
+
+ Vector3 expected = RampNormal(WalkableSlopeGradient);
+ Vector3 actual = harness.Remote.Body.ContactPlane.Normal;
+ Assert.True(
+ Vector3.Distance(expected, actual) < 0.001f,
+ $"contact plane normal was {actual}, expected the ramp's {expected}");
+ Assert.True(actual.Z >= PhysicsGlobals.FloorZ);
+ }
+
+ ///
+ /// The artifact the deleted projection existed to remove: a remote running
+ /// across a slope must have its feet track the ground CONTINUOUSLY between
+ /// server position updates, not ratchet down in ~5 Hz steps.
+ ///
+ /// Asserted on every tick rather than at the end, because a
+ /// start/end comparison passes a staircase that happens to catch up on the
+ /// final tick.
+ ///
+ /// Sabotage-verified 2026-08-06: discarding the sweep's answer
+ /// (rm.Body.Position = postIntegratePos instead of
+ /// resolveResult.Position) reddens this at tick 1 with the body
+ /// 0.05999 m off the surface.
+ ///
+ /// What this test does NOT discriminate, stated so nobody infers
+ /// it later. Short-circuiting Transition.AdjustOffset to
+ /// return offset; leaves it GREEN. On terrain the sweep has a
+ /// second, independent way to put the body on the surface:
+ /// ValidateWalkable's push-out re-plants the sphere at its natural
+ /// resting distance from the terrain plane on every sub-step, so the Z
+ /// outcome survives even with the offset projection gone (what changes is
+ /// the XY, which adjust_offset shortens). Removing the step-down
+ /// probe does not change that either — measured. So this test asserts the
+ /// OUTCOME "a running remote's feet stay on the ground", which is what the
+ /// deleted projection was there for; it is not a unit test of
+ /// adjust_offset, and it must not be cited as one.
+ ///
+ [Fact]
+ public void TheRemoteTickTracksTheSurfaceWhileRunningDownhill()
+ {
+ using RemoteRampHarness harness =
+ RemoteRampHarness.OnRamp(WalkableSlopeGradient);
+ AssertTracksSurface(harness);
+ }
+
+ ///
+ /// Anti-vacuity guard for the two tests above: on flat ground they pass
+ /// without Z ever having to move, so a fixture that quietly flattened
+ /// would make them meaningless. This asserts the ramp genuinely forces a
+ /// large Z excursion over the same number of ticks.
+ ///
+ [Fact]
+ public void TheTrackingFixtureActuallyRequiresTheBodyToChangeZ()
+ {
+ using RemoteRampHarness harness =
+ RemoteRampHarness.OnRamp(WalkableSlopeGradient);
+ float startZ = harness.Remote.Body.Position.Z;
+
+ harness.Tick(TrackedTicks, new Vector3(0f, RootMotionPerTick, 0f));
+
+ float dz = harness.Remote.Body.Position.Z - startZ;
+ Assert.True(
+ dz < -1.0f,
+ $"fixture is not exercising slope descent: dz = {dz:F4} m");
+ }
+
+ private static void AssertTracksSurface(RemoteRampHarness harness)
+ {
+ Assert.True(harness.Remote.Body.OnWalkable);
+
+ // The settled resting offset between the body's root and the terrain
+ // directly beneath it. Measured, not assumed: the spawn settle may put
+ // the root a hair off the sampled surface.
+ float restingOffset =
+ harness.Remote.Body.Position.Z - harness.SurfaceZUnderBody();
+
+ for (int tick = 1; tick <= TrackedTicks; tick++)
+ {
+ harness.Tick(1, new Vector3(0f, RootMotionPerTick, 0f));
+ float offset =
+ harness.Remote.Body.Position.Z - harness.SurfaceZUnderBody();
+ Assert.True(
+ MathF.Abs(offset - restingOffset) < SurfaceTrackingToleranceMeters,
+ $"tick {tick}: body root sits {offset:F5} m above the terrain "
+ + $"under it, expected {restingOffset:F5} m "
+ + $"(pos {harness.Remote.Body.Position})");
+ }
+ }
+}
diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimeRemoteSteepContactSlideTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimeRemoteSteepContactSlideTests.cs
index 9040ac64..7e1488be 100644
--- a/tests/AcDream.Runtime.Tests/Physics/RuntimeRemoteSteepContactSlideTests.cs
+++ b/tests/AcDream.Runtime.Tests/Physics/RuntimeRemoteSteepContactSlideTests.cs
@@ -53,7 +53,7 @@ public sealed class RuntimeRemoteSteepContactSlideTests
[Fact]
public void SteepTerrainProducesANonWalkableContactPlane()
{
- using Harness harness = Harness.OnRamp(SteepGradient);
+ using RemoteRampHarness harness = RemoteRampHarness.OnRamp(SteepGradient);
Assert.True(harness.Remote.Body.ContactPlaneValid);
Assert.InRange(
@@ -70,7 +70,7 @@ public sealed class RuntimeRemoteSteepContactSlideTests
[Fact]
public void SteepContactDoesNotLatchALanding()
{
- using Harness harness = Harness.OnRamp(SteepGradient);
+ using RemoteRampHarness harness = RemoteRampHarness.OnRamp(SteepGradient);
harness.Remote.Airborne = true;
int groundEdges = 0;
harness.Remote.Motion.RemoveLinkAnimations = () => groundEdges++;
@@ -90,7 +90,7 @@ public sealed class RuntimeRemoteSteepContactSlideTests
[Fact]
public void GravityPersistsAcrossTicksOnASteepContact()
{
- using Harness harness = Harness.OnRamp(SteepGradient);
+ using RemoteRampHarness harness = RemoteRampHarness.OnRamp(SteepGradient);
harness.Remote.Airborne = true;
harness.Tick(40);
@@ -107,7 +107,7 @@ public sealed class RuntimeRemoteSteepContactSlideTests
[Fact]
public void SteepContactKeepsTheBodySlidingDownhill()
{
- using Harness harness = Harness.OnRamp(SteepGradient);
+ using RemoteRampHarness harness = RemoteRampHarness.OnRamp(SteepGradient);
Vector3 start = harness.Remote.Body.Position;
harness.Tick(40);
@@ -131,7 +131,7 @@ public sealed class RuntimeRemoteSteepContactSlideTests
[Fact]
public void TheTickNeverAssertsContactOrWalkableWithoutASweep()
{
- using Harness harness = Harness.OnRamp(WalkableGradient);
+ using RemoteRampHarness harness = RemoteRampHarness.OnRamp(WalkableGradient);
harness.Remote.CellId = 0u;
harness.Remote.Body.TransientState &= ~(TransientStateFlags.Contact
| TransientStateFlags.OnWalkable);
@@ -155,7 +155,7 @@ public sealed class RuntimeRemoteSteepContactSlideTests
[Fact]
public void AGroundedTickOnASteepFaceReleasesTheBodyInsteadOfPinningIt()
{
- using Harness harness = Harness.OnRamp(SteepGradient);
+ using RemoteRampHarness harness = RemoteRampHarness.OnRamp(SteepGradient);
harness.Remote.Body.TransientState |=
TransientStateFlags.Contact | TransientStateFlags.OnWalkable;
harness.Remote.Airborne = false;
@@ -181,7 +181,7 @@ public sealed class RuntimeRemoteSteepContactSlideTests
[Fact]
public void AuthoritativeVelocityIsNotDiscardedOnAGroundedTick()
{
- using Harness harness = Harness.OnRamp(WalkableGradient);
+ using RemoteRampHarness harness = RemoteRampHarness.OnRamp(WalkableGradient);
Assert.False(harness.Remote.Airborne);
harness.Remote.Body.Velocity = new Vector3(2.146f, 2.264f, -3.549f);
@@ -221,12 +221,12 @@ public sealed class RuntimeRemoteSteepContactSlideTests
[Fact]
public void CommittedTransientsAgreeWithTheCommittedContactPlane()
{
- using Harness steep = Harness.OnRamp(SteepGradient);
+ using RemoteRampHarness steep = RemoteRampHarness.OnRamp(SteepGradient);
steep.Tick(20);
AssertTransientsAreContactPlaneDerived(
steep.Remote.Body, expectWalkable: false);
- using Harness gentle = Harness.OnRamp(WalkableGradient);
+ using RemoteRampHarness gentle = RemoteRampHarness.OnRamp(WalkableGradient);
gentle.Tick(20);
AssertTransientsAreContactPlaneDerived(
gentle.Remote.Body, expectWalkable: true);
@@ -271,7 +271,7 @@ public sealed class RuntimeRemoteSteepContactSlideTests
[Fact]
public void WalkableLandingStillLandsAndFiresTheGroundEdgeOnce()
{
- using Harness harness = Harness.Airborne(WalkableGradient, height: 3f);
+ using RemoteRampHarness harness = RemoteRampHarness.Airborne(WalkableGradient, height: 3f);
int groundEdges = 0;
harness.Remote.Motion.RemoveLinkAnimations = () => groundEdges++;
@@ -293,226 +293,11 @@ public sealed class RuntimeRemoteSteepContactSlideTests
[Fact]
public void WalkableLandingDoesNotClearTheGravityStateBit()
{
- using Harness harness = Harness.Airborne(WalkableGradient, height: 3f);
+ using RemoteRampHarness harness = RemoteRampHarness.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();
- }
}