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

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