fix(physics): route remote Positions on contact, not walkability (AP-140)
The two gates that decide whether an accepted remote Position is interpolated
or hard-snapped read `Airborne`, which is `!Body.OnWalkable` — WALKABILITY.
Retail reads CONTACT: InterpolationManager::adjust_offset @0x00555D30 gates its
entire body on `transient_state & 1` @0x00555D52, so a retail body in contact
with a non-walkable face still interpolates.
The two predicates disagree in exactly one state — in contact, not on walkable
ground — which 204d0ae0 turned from unreachable into ordinary. Before it, the
per-tick forge made every non-airborne remote walkable by construction, so the
disagreement could not occur.
Both gates now read `!Body.InContact`: ApplyRemoteContactRouting's flight
carve-out and OnPosition's player-remote arm.
`Airborne` is deliberately NOT re-derived from CONTACT. That would perturb all
five of its writers and contradict a pinned assertion in
RemoteTeleportPlacementTests.Apply_PendingGroundToSteepContact_ (InContact:
true, OnWalkable: false -> Assert.True(remote.Airborne)); a previous
implementer attempted it and correctly backed out rather than editing the
assertion. This narrower shape touches no existing test.
AP-140's register row is retired in this commit, as the row itself specified.
Honest scope: this is a faithfulness fix, not a visible one. ACE derives its
IsGrounded flag with the same floor_z test, so during a slide it almost
certainly reports not-grounded, the classifier returns NoPositionOperation, and
neither arm is taken. Expect no observable change against ACE.
Suite 11,027 passed / 4 skipped / 0 failed (baseline 11,023).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f4f2579575
commit
2eb39a0250
6 changed files with 273 additions and 33 deletions
|
|
@ -161,7 +161,12 @@ public sealed class LiveEntityNetworkRemoteSteadyStateIntegrationTests
|
|||
(RuntimeEntityRecord record, RemoteMotion remote, _) =
|
||||
fixture.AddRemote(0x70005001u, new Vector3(12f, 14f, 7f));
|
||||
remote.Body.Position = new Vector3(10f, 10f, 5f);
|
||||
// AP-140 (retired 2026-08-04): free flight is retail's CONTACT_TS-clear
|
||||
// (`InterpolationManager::adjust_offset` gates on `transient_state & 1`
|
||||
// @0x00555D52), not the client `Airborne` walkability flag. Both are
|
||||
// stated so the test reads the same before and after that change.
|
||||
remote.Airborne = true;
|
||||
remote.Body.TransientState = TransientStateFlags.Active;
|
||||
var landing = new Vector3(10.5f, 10f, 5f); // 0.5 m — well within 4 m.
|
||||
|
||||
LiveEntityNetworkUpdateController.RemoteContactRouting routing =
|
||||
|
|
@ -211,6 +216,137 @@ public sealed class LiveEntityNetworkRemoteSteadyStateIntegrationTests
|
|||
Assert.Equal(new Vector3(10f, 10f, 5f), remote.Body.Position);
|
||||
}
|
||||
|
||||
// ── AP-140 (retired 2026-08-04): the gate's predicate is CONTACT ────────
|
||||
|
||||
/// <summary>
|
||||
/// The divergence AP-140 recorded, as behaviour. A remote in CONTACT with
|
||||
/// a NON-walkable face — sliding down a steep roof — is
|
||||
/// <c>Airborne == true</c> by the client flag's own definition
|
||||
/// (<c>!Body.OnWalkable</c>), and used to be hard-snapped at
|
||||
/// UpdatePosition cadence. Retail interpolates it:
|
||||
/// <c>InterpolationManager::adjust_offset</c> @0x00555D30 gates its entire
|
||||
/// body on <c>transient_state & 1</c> @0x00555D52, and bit 0 is
|
||||
/// <c>CONTACT_TS</c> (acclient.h:3690), not <c>ON_WALKABLE_TS</c> (0x2).
|
||||
///
|
||||
/// <para>
|
||||
/// This is the ONE state on which the two predicates disagree, so it is
|
||||
/// the only test that can discriminate the fix: point the gate back at
|
||||
/// <c>remote.Airborne</c> and this test alone fails, with
|
||||
/// <c>AirborneSnap</c> and a moved body.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SteepContactBody_InterpolatesInsteadOfSnapping()
|
||||
{
|
||||
using var fixture = new RemotePlacementDriveFixture();
|
||||
(RuntimeEntityRecord record, RemoteMotion remote, _) =
|
||||
fixture.AddRemote(0x70005003u, new Vector3(12f, 14f, 7f));
|
||||
var before = new Vector3(10f, 10f, 5f);
|
||||
remote.Body.Position = before;
|
||||
// Contact with a face too steep to stand on: CONTACT set, ON_WALKABLE
|
||||
// clear — exactly what the sweep's SetPositionInternal commit derives
|
||||
// on a 52-degree roof since Bug B (204d0ae0) deleted the per-tick
|
||||
// walkability forge that used to make this state unreachable.
|
||||
remote.Body.TransientState =
|
||||
TransientStateFlags.Active | TransientStateFlags.Contact;
|
||||
Assert.True(remote.Body.InContact);
|
||||
Assert.False(remote.Body.OnWalkable);
|
||||
// The client flag agrees with its five writers, all `!Body.OnWalkable`.
|
||||
remote.Airborne = !remote.Body.OnWalkable;
|
||||
Assert.True(remote.Airborne);
|
||||
|
||||
LiveEntityNetworkUpdateController.RemoteContactRouting routing =
|
||||
LiveEntityNetworkUpdateController.ApplyRemoteContactRouting(
|
||||
fixture.Drive,
|
||||
record,
|
||||
remote,
|
||||
Classify(hasContact: true, playerDistance: 10f),
|
||||
before + new Vector3(0.5f, 0f, 0f),
|
||||
Quaternion.Identity,
|
||||
willBeDrTicked: true);
|
||||
|
||||
Assert.Equal(
|
||||
LiveEntityNetworkUpdateController.RemoteContactArm
|
||||
.SteadyStateInterpolate,
|
||||
routing.Arm);
|
||||
// The interpolate arm enqueues a waypoint and leaves the body to the
|
||||
// per-tick catch-up; the snap arm would have written it directly.
|
||||
Assert.Equal(before, remote.Body.Position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The regression guard on the other side of the same gate: a genuinely
|
||||
/// free-flying remote — no contact with anything — must still take the
|
||||
/// hard snap. Retail's <c>adjust_offset</c> writes nothing at all with
|
||||
/// <c>CONTACT_TS</c> clear, so the queued waypoint cannot advance the
|
||||
/// body and the authoritative position is the only thing that can.
|
||||
/// Widen the gate to "always interpolate" and this test fails.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FreeFlightBodyWithNoContact_StillSnaps()
|
||||
{
|
||||
using var fixture = new RemotePlacementDriveFixture();
|
||||
(RuntimeEntityRecord record, RemoteMotion remote, _) =
|
||||
fixture.AddRemote(0x70005004u, new Vector3(12f, 14f, 7f));
|
||||
remote.Body.Position = new Vector3(10f, 10f, 5f);
|
||||
remote.Body.TransientState = TransientStateFlags.Active;
|
||||
Assert.False(remote.Body.InContact);
|
||||
remote.Airborne = !remote.Body.OnWalkable;
|
||||
var landing = new Vector3(10.5f, 10f, 5f);
|
||||
|
||||
LiveEntityNetworkUpdateController.RemoteContactRouting routing =
|
||||
LiveEntityNetworkUpdateController.ApplyRemoteContactRouting(
|
||||
fixture.Drive,
|
||||
record,
|
||||
remote,
|
||||
Classify(hasContact: true, playerDistance: 10f),
|
||||
landing,
|
||||
Quaternion.Identity,
|
||||
willBeDrTicked: true);
|
||||
|
||||
Assert.Equal(
|
||||
LiveEntityNetworkUpdateController.RemoteContactArm.AirborneSnap,
|
||||
routing.Arm);
|
||||
Assert.Equal(landing, remote.Body.Position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The unchanged third state: contact with WALKABLE ground. Both
|
||||
/// predicates agree here, and both before and after AP-140's fix the
|
||||
/// packet interpolates. Pinned so a future edit cannot "simplify" the
|
||||
/// gate into something that only satisfies the two tests above.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void WalkableGroundedBody_IsUnchangedAndStillInterpolates()
|
||||
{
|
||||
using var fixture = new RemotePlacementDriveFixture();
|
||||
(RuntimeEntityRecord record, RemoteMotion remote, _) =
|
||||
fixture.AddRemote(0x70005005u, new Vector3(12f, 14f, 7f));
|
||||
var before = new Vector3(10f, 10f, 5f);
|
||||
remote.Body.Position = before;
|
||||
remote.Body.TransientState = TransientStateFlags.Active
|
||||
| TransientStateFlags.Contact
|
||||
| TransientStateFlags.OnWalkable;
|
||||
remote.Airborne = !remote.Body.OnWalkable;
|
||||
Assert.False(remote.Airborne);
|
||||
|
||||
LiveEntityNetworkUpdateController.RemoteContactRouting routing =
|
||||
LiveEntityNetworkUpdateController.ApplyRemoteContactRouting(
|
||||
fixture.Drive,
|
||||
record,
|
||||
remote,
|
||||
Classify(hasContact: true, playerDistance: 10f),
|
||||
before + new Vector3(0.5f, 0f, 0f),
|
||||
Quaternion.Identity,
|
||||
willBeDrTicked: true);
|
||||
|
||||
Assert.Equal(
|
||||
LiveEntityNetworkUpdateController.RemoteContactArm
|
||||
.SteadyStateInterpolate,
|
||||
routing.Arm);
|
||||
Assert.Equal(before, remote.Body.Position);
|
||||
}
|
||||
|
||||
private static void AssertRenderPoseSuppressed(
|
||||
RuntimeAuthoritativePositionRoute? route)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue