test(physics): measure whether the remote sweep alone tracks surface Z (AD-10 Stage 0)

AD-10 claims the remote slope projection is "relocated" out of the sweep
because "remote bodies don't run a full local transition sweep". That
justification is false at HEAD: RuntimeRemotePhysicsUpdater.Tick calls
PhysicsEngine.ResolveWithTransition with the remote's own body, and that
sweep runs acdream's verbatim port of CTransition::adjust_offset
(0x0050a370, pc:272271-272393) once per sub-step. So the boundary
projection is an EXTRA layer, not a relocation — and whether it is doing
anything the sweep does not is a measurement, never an argument.

This commit builds the fixture for that measurement and changes no
production code.

RuntimeRemoteSteepContactSlideTests' private Harness is extracted to
RemoteRampHarness so the new tests share it instead of cloning ~180 lines.
The extraction is behaviour-preserving; its only additions are the
fixture's own TerrainSurface (so an assertion about "is the body on the
surface" is answered by the surface geometry rather than by
re-implementing what the code under test computed), a SurfaceZ helper, and
a Tick overload that supplies a per-frame body-local root displacement —
the locomotion-cycle push a running remote actually carries. All ten Bug B
tests pass unchanged against it.

RuntimeRemoteSlopeProjectionTests then drives the production tick 30 ticks
down a 31-degree walkable ramp and asserts, on EVERY tick rather than at
the end, that the body's root stays within 5 mm of its settled offset from
the terrain beneath it. A staircase catching up on the final tick would
pass a start/end comparison; 30 unprojected ticks accumulate ~1.8 m.

Sabotage results, all from clean builds (bin/obj deleted), reported in
both directions:

  * Discard the sweep's answer (Body.Position = postIntegratePos instead
    of resolveResult.Position): RED at tick 1, body 0.05999 m off the
    surface. This is the tracking test's discriminating sabotage.
  * Flatten the ramp to gradient 0: RED on the anti-vacuity guard
    (dz = 0.0000 m). That guard exists because the tracking assertion
    passes trivially on flat ground, where Z never has to move.
  * Short-circuit Transition.AdjustOffset to `return offset;`: GREEN.
    Recorded, not hidden — it is the reason the contract's proposed T1
    sabotage was rejected. On terrain the sweep has a SECOND independent
    way to plant Z: ValidateWalkable's push-out re-seats the sphere at its
    natural resting distance from the terrain plane every sub-step.
    Removing the step-down probe as well does not change it either
    (measured). The tests therefore assert the OUTCOME the projection
    exists for, and say in their own doc comments that they are not unit
    tests of adjust_offset and must not be cited as such.

One test the contract asked for is deliberately absent. An uphill
counterpart was written, passed, and was then found VACUOUS: on this
fixture ResolveWithTransition returns ok=False for uphill motion and the
body does not move at all, so it "tracked the surface" by standing still.
That finding is filed separately rather than shipped as a green test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-06 09:34:56 +02:00
parent ef976c6dbb
commit fe6ee877d1
3 changed files with 443 additions and 226 deletions

View file

@ -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;
/// <summary>
/// Drives the production <see cref="RuntimeRemotePhysicsUpdater"/> 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.
///
/// <para>Extracted 2026-08-06 from <c>RuntimeRemoteSteepContactSlideTests</c>
/// (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 <see cref="Surface"/>,
/// <see cref="SurfaceZ"/>, and the root-motion-driving
/// <see cref="Tick(int, Vector3, float)"/> overload.</para>
/// </summary>
internal sealed class RemoteRampHarness : IDisposable
{
private const uint LandblockId = 0x0101FFFFu;
/// <summary>Local XY the body is placed at, near the landblock centre.</summary>
internal const float StartX = 96f;
/// <summary>Local XY the body is placed at, near the landblock centre.</summary>
internal const float StartY = 96f;
private readonly RuntimeEntityObjectLifetime _lifetime;
private readonly RuntimeEntityRecord _record;
private readonly RuntimeRemotePhysicsUpdater _updater;
internal RemoteMotion Remote { get; }
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>Terrain height at a world-space XY (the landblock sits at 0,0).</summary>
internal float SurfaceZ(float worldX, float worldY)
=> Surface.SampleZ(worldX, worldY);
/// <summary>Terrain height directly under the body's current XY.</summary>
internal float SurfaceZUnderBody()
=> SurfaceZ(Remote.Body.Position.X, Remote.Body.Position.Y);
/// <summary>Body already resting on the ramp, contact established.</summary>
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;
}
/// <summary>Body suspended above the ramp with no contact at all.</summary>
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<AcDream.Core.Physics.CellSurface>(),
Array.Empty<AcDream.Core.Physics.PortalPlane>(),
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);
}
/// <summary>Tick with an empty animation root-motion frame.</summary>
internal void Tick(int count, float dt = 1f / 30f)
=> Tick(count, Vector3.Zero, dt);
/// <summary>
/// Tick while <c>CSequence::update</c> reports the given body-local root
/// displacement every frame — the locomotion-cycle push that drives a
/// running remote between server position updates.
/// </summary>
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);
}
}
/// <summary>
/// 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.
/// </summary>
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<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
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();
}

View file

@ -0,0 +1,160 @@
using System.Numerics;
using AcDream.Core.Physics;
namespace AcDream.Runtime.Tests.Physics;
/// <summary>
/// AD-10 — the remote slope projection. Retail projects the per-sub-step
/// movement offset onto <c>collision_info.contact_plane</c> INSIDE the sweep
/// (<c>CTransition::adjust_offset</c> <c>0x0050a370</c>, pc:272271-272393,
/// called once per step from <c>find_transitional_position</c>
/// <c>0x0050bdf0</c>). acdream ports that faithfully in
/// <c>Transition.AdjustOffset</c>, 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.
///
/// <para>Every assertion here runs the production
/// <see cref="AcDream.Runtime.Physics.RuntimeRemotePhysicsUpdater"/> 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.</para>
/// </summary>
public sealed class RuntimeRemoteSlopeProjectionTests
{
/// <summary>
/// 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.
/// </summary>
private const float WalkableSlopeGradient = 0.6f;
/// <summary>
/// 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.
/// </summary>
private const float RootMotionPerTick = 0.10f;
private const int TrackedTicks = 30;
/// <summary>
/// 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.
/// </summary>
private const float SurfaceTrackingToleranceMeters = 0.005f;
/// <summary>
/// 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 <c>gradient</c>, so its plane is <c>gradient*y + z = c</c> and its
/// unit normal is <c>(0, gradient, 1)</c> normalized.
/// </summary>
private static Vector3 RampNormal(float gradient)
=> Vector3.Normalize(new Vector3(0f, gradient, 1f));
/// <summary>
/// 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.
/// </summary>
[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);
}
/// <summary>
/// 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.
///
/// <para>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.</para>
///
/// <para>Sabotage-verified 2026-08-06: discarding the sweep's answer
/// (<c>rm.Body.Position = postIntegratePos</c> instead of
/// <c>resolveResult.Position</c>) reddens this at tick 1 with the body
/// 0.05999 m off the surface.</para>
///
/// <para><b>What this test does NOT discriminate, stated so nobody infers
/// it later.</b> Short-circuiting <c>Transition.AdjustOffset</c> to
/// <c>return offset;</c> leaves it GREEN. On terrain the sweep has a
/// second, independent way to put the body on the surface:
/// <c>ValidateWalkable</c>'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 <c>adjust_offset</c> 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
/// <c>adjust_offset</c>, and it must not be cited as one.</para>
/// </summary>
[Fact]
public void TheRemoteTickTracksTheSurfaceWhileRunningDownhill()
{
using RemoteRampHarness harness =
RemoteRampHarness.OnRamp(WalkableSlopeGradient);
AssertTracksSurface(harness);
}
/// <summary>
/// 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.
/// </summary>
[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})");
}
}
}

View file

@ -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;
}
/// <summary>Body already resting on the ramp, contact established.</summary>
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;
}
/// <summary>Body suspended above the ramp with no contact at all.</summary>
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<AcDream.Core.Physics.CellSurface>(),
Array.Empty<AcDream.Core.Physics.PortalPlane>(),
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);
}
}
/// <summary>
/// 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 <c>normalize((0, -gradient, 1))</c>.
/// </summary>
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<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
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();
}
}