feat(physics): C4 route 4a — remote steady-state Position through the seam

Routes the classifier's two NO-PLACEMENT remote branches — Interpolate
(contact, PlayerDistance < 96 m) and NoPositionOperation (no contact) — through
a Runtime-owned seam, and fixes the two divergences they carried. Teleport,
far-snap and cell-less stay on the legacy App path; 4b owns them.

Route 4 was split into 4a/4b after scoping put the whole route at 1,500-2,500
lines against a ~400 budget. 4a's branches perform no SetPosition, so this slice
carries no deferred-cell park, no service-window guard and no allocation
exposure — which is what made the split worth doing.

Divergences fixed, both previously unfiled:

* D1 — the NPC airborne branch hard-snapped Body.Position/Orientation and
  branched on the client-tracked rmState.Airborne, never consulting the wire
  IsGrounded bit. Retail's MoveOrTeleport @0x00516330 returns 0 at 0x0051636D
  and writes nothing. Player remotes were already correct; NPCs were not.
* D2 — ConstrainTo was armed before the operation, unconditionally, so it fired
  on the airborne no-op retail skips and anchored to the PRE-move position.
  Retail arms it at 0x00454272, only when MoveOrTeleport returns nonzero,
  anchored to &arg2->m_position read live, i.e. post-move.

AP-87 and TS-44 were carried deliberately, not delegated away. AP-87's three
conditions — including firstUp, which one round silently dropped — are preserved
as an explicit acdream policy layer applied AFTER the classifier commits to
Interpolate; the two previously separate player/NPC copies are now one. TS-44
stays an NPC-only caller gate; extending sticky suppression to player remotes has
no retail basis and no live evidence, so it was declined rather than absorbed.

Landing is explicitly carved out of 4a's ownership on both arms. A landing packet
classifies Interpolate, so an ordering slip would ENQUEUE a body that must PLANT
and a creature knocked off a ledge would glide down over a packet interval. The
carve-out is a named entry point returning AirborneSnap/SteadyStateInterpolate/
Legacy precisely so the PRECEDENCE is observable and testable rather than implied
by statement order — that is how the slip happened once and was caught.

The player/NPC asymmetry on landing is real and NOT resolved here: retail draws
no such distinction, but converging them is a behaviour decision needing its own
evidence. Filed into the 4b plan.

Register: AP-135 filed for the two bookkeeping writes the airborne branch
deliberately retains (rmState.CellId, LastServerPos/Time) — not retail's model,
but load-bearing for our catch-up sweep and staleness timer, and verified not to
be a canonical cell commit for ordinary remotes. AP-87 and TS-44 rewritten to
describe the code.

Honest remainder: App still owns branch selection, the airborne return, the cell
write, the entity write and the shadow publish, and headless satisfies "both
hosts drive the identical entry point" only vacuously since it returns early for
remotes. That is written into the 4b bullet rather than left implicit.

Cost: 364 non-comment production lines, 91% of the ~400 budget — the split did
isolate the cheap half, but not by much. Do not carry "well under" into 4b's
scoping.

Gates: complete Release solution 10,938 passed / 4 skipped / 0 failed (pre-4a
baseline 10,909). Four review rounds; the first three each introduced a new
behavioural defect while fixing another, and each left a comment asserting
behaviour that no longer matched — the final round's precedence matrix was
traced cell-by-cell against HEAD with only the D1-intended difference. App tests
call production entry points against a real WorldEntity and real classifier
output, closing route 2's #292 gap rather than repeating it.

Connected acceptance NOT run — needs a live second character.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-04 00:19:05 +02:00
parent 19d9509497
commit 44830a0eb3
12 changed files with 1854 additions and 150 deletions

View file

@ -0,0 +1,316 @@
using System.Numerics;
using AcDream.App.Physics;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Physics;
namespace AcDream.App.Tests.Physics;
/// <summary>
/// C4 route 4a: behavioural proof that the generic top-of-<c>OnPosition</c>
/// render-pose write no longer double-writes a remote whose accepted Position
/// classifies to
/// <see cref="RuntimeAuthoritativePositionDisposition.NoPositionOperation"/> or
/// <see cref="RuntimeAuthoritativePositionDisposition.Interpolate"/>, and
/// still writes it for every classification route 4a does not own.
///
/// <para>
/// These tests call the PRODUCTION decision-and-mutation entry point
/// (<see cref="LiveEntityNetworkUpdateController.TryApplyGenericRemoteRenderPose"/>)
/// against a real <see cref="WorldEntity"/> and real
/// <see cref="RuntimeAuthoritativePositionRouteClassifier"/> output. Nothing
/// is re-derived in the test body: revert the suppression inside that method
/// and the two suppression tests fail; widen it to <c>route is not null</c>
/// and four of the five legacy-write tests fail (the fifth,
/// <see cref="NoClassificationAtAll_StillTakesTheLegacyRenderPoseWrite"/>,
/// passes a null route and so survives that particular widening — it
/// discriminates the OTHER direction, an over-narrow guard). That is the gap
/// #292 recorded for route 2's source-text pin.
/// </para>
///
/// <para>
/// The suite also pins the ORDERING carve-out
/// (<see cref="LiveEntityNetworkUpdateController.ApplyRemoteContactRouting"/>):
/// an airborne body's contact packet classifies <c>Interpolate</c>, so
/// precedence — not classification — is the only thing keeping a landing
/// creature planting instead of gliding.
/// </para>
/// </summary>
public sealed class LiveEntityNetworkRemoteSteadyStateIntegrationTests
{
private const uint OldCell = 0x01010001u;
private const uint WireCell = 0x01010002u;
private static readonly Vector3 OldPosition = new(5f, 5f, 5f);
private static readonly Vector3 WirePosition = new(6f, 7f, 8f);
[Fact]
public void NoPositionOperation_LeavesTheRenderEntityPoseAndCellUntouched()
{
// Retail's airborne no-op (D1): MoveOrTeleport @0x00516330 returns 0
// for arg4 == 0 and writes nothing. CommittedCellId is nonzero so the
// cell-less SetPosition branch cannot be what is selected here.
RuntimeAuthoritativePositionRoute route =
Classify(hasContact: false, playerDistance: 1f);
Assert.Equal(
RuntimeAuthoritativePositionDisposition.NoPositionOperation,
route.Disposition);
AssertRenderPoseSuppressed(route);
}
[Fact]
public void Interpolate_LeavesTheRenderEntityPoseAndCellUntouched()
{
// Retail's near InterpolateTo queue @0x005163AF. The resolved body,
// not the raw wire packet, is the branch tail's only writer of the
// render entity — writing the wire pose here first would be the
// second writer route 2's original defect consisted of.
RuntimeAuthoritativePositionRoute route =
Classify(hasContact: true, playerDistance: 10f);
Assert.Equal(
RuntimeAuthoritativePositionDisposition.Interpolate,
route.Disposition);
AssertRenderPoseSuppressed(route);
}
[Fact]
public void FarSetPositionSimple_StillTakesTheLegacyRenderPoseWrite()
{
RuntimeAuthoritativePositionRoute route =
Classify(hasContact: true, playerDistance: 200f);
Assert.Equal(
RuntimeAuthoritativePositionDisposition.SetPositionSimple,
route.Disposition);
AssertRenderPoseWritten(route);
}
[Fact]
public void CellLessRemote_StillTakesTheLegacyRenderPoseWrite()
{
RuntimeAuthoritativePositionRoute route = Classify(
hasContact: false,
playerDistance: 1f,
committedCellId: 0u);
Assert.Equal(
RuntimeAuthoritativePositionDisposition.SetPosition,
route.Disposition);
AssertRenderPoseWritten(route);
}
[Fact]
public void RejectedAuthority_StillTakesTheLegacyRenderPoseWrite()
{
RuntimeAuthoritativePositionRoute route = Classify(
hasContact: true,
playerDistance: 10f,
disposition: PositionTimestampDisposition.Rejected);
Assert.Equal(
RuntimeAuthoritativePositionDisposition.RejectedAuthority,
route.Disposition);
AssertRenderPoseWritten(route);
}
[Fact]
public void RejectedData_StillTakesTheLegacyRenderPoseWrite()
{
RuntimeAuthoritativePositionRoute route =
Classify(hasContact: true, playerDistance: float.NaN);
Assert.Equal(
RuntimeAuthoritativePositionDisposition.RejectedData,
route.Disposition);
AssertRenderPoseWritten(route);
}
[Fact]
public void NoClassificationAtAll_StillTakesTheLegacyRenderPoseWrite()
{
// The login window (no local-player controller yet) and a canonical
// record that has not claimed a local id both produce a null route.
// Its doc comment promises the legacy path runs unchanged — so the
// write must still happen.
AssertRenderPoseWritten(null);
}
// ── Ordering: the airborne snap outranks route 4a's near branch ─────────
[Fact]
public void AirborneBodyWithAContactPacket_SnapsInsteadOfEnqueuing()
{
// The NPC LANDING packet: the client still believes the body airborne
// and ACE reports it in contact, close by. That classifies
// Interpolate — and AP-87's conditions are all benign (0.5 m, sampled,
// DR-ticked) — so route 4a's branch would ENQUEUE it. It must SNAP:
// a drudge knocked off a ledge plants, it does not glide to the
// ground over a packet interval.
RuntimeAuthoritativePositionRoute route =
Classify(hasContact: true, playerDistance: 10f);
Assert.Equal(
RuntimeAuthoritativePositionDisposition.Interpolate,
route.Disposition);
RemoteMotion remote = MakeLandingRemote();
var landing = new Vector3(10.5f, 10f, 5f); // 0.5 m — well within 4 m.
LiveEntityNetworkUpdateController.RemoteContactArm arm =
LiveEntityNetworkUpdateController.ApplyRemoteContactRouting(
remote,
route,
landing,
Quaternion.Identity,
willBeDrTicked: true);
Assert.Equal(
LiveEntityNetworkUpdateController.RemoteContactArm.AirborneSnap,
arm);
Assert.Equal(landing, remote.Body.Position);
}
[Fact]
public void GroundedBodyWithANearContactPacket_TakesRoute4asInterpolateBranch()
{
// The same packet on a body the client already considers grounded is
// route 4a's own branch: queued for the per-tick catch-up, body
// untouched. This is what proves the airborne test above is a
// PRECEDENCE carve-out and not a blanket disable of route 4a.
RemoteMotion remote = MakeLandingRemote();
remote.Airborne = false;
var target = new Vector3(10.5f, 10f, 5f);
LiveEntityNetworkUpdateController.RemoteContactArm arm =
LiveEntityNetworkUpdateController.ApplyRemoteContactRouting(
remote,
Classify(hasContact: true, playerDistance: 10f),
target,
Quaternion.Identity,
willBeDrTicked: true);
Assert.Equal(
LiveEntityNetworkUpdateController.RemoteContactArm
.SteadyStateInterpolate,
arm);
Assert.Equal(new Vector3(10f, 10f, 5f), remote.Body.Position);
}
[Fact]
public void GroundedBodyOnAClassificationRoute4aDoesNotOwn_FallsThroughToLegacy()
{
RemoteMotion remote = MakeLandingRemote();
remote.Airborne = false;
Vector3 before = remote.Body.Position;
LiveEntityNetworkUpdateController.RemoteContactArm arm =
LiveEntityNetworkUpdateController.ApplyRemoteContactRouting(
remote,
Classify(hasContact: true, playerDistance: 200f),
new Vector3(10.5f, 10f, 5f),
Quaternion.Identity,
willBeDrTicked: true);
Assert.Equal(
LiveEntityNetworkUpdateController.RemoteContactArm.Legacy,
arm);
// Nothing was written — the caller's own legacy routing owns it.
Assert.Equal(before, remote.Body.Position);
}
private static RemoteMotion MakeLandingRemote()
{
var remote = new RemoteMotion();
remote.Body.Position = new Vector3(10f, 10f, 5f);
remote.Body.Orientation = Quaternion.Identity;
// Past the AP-87 firstUp hint, so the near case really is near.
remote.LastServerPosTime = 1_700_000_000d;
remote.Airborne = true;
return remote;
}
private static void AssertRenderPoseSuppressed(
RuntimeAuthoritativePositionRoute? route)
{
WorldEntity entity = MakeEntity();
var beforeRotation = entity.Rotation;
bool written = LiveEntityNetworkUpdateController
.TryApplyGenericRemoteRenderPose(
entity,
route,
WirePosition,
WireCell,
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 1.1f));
Assert.False(written);
Assert.Equal(OldPosition, entity.Position);
Assert.Equal(OldCell, entity.ParentCellId);
Assert.Equal(beforeRotation, entity.Rotation);
}
private static void AssertRenderPoseWritten(
RuntimeAuthoritativePositionRoute? route)
{
WorldEntity entity = MakeEntity();
var wireRotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 1.1f);
bool written = LiveEntityNetworkUpdateController
.TryApplyGenericRemoteRenderPose(
entity,
route,
WirePosition,
WireCell,
wireRotation);
Assert.True(written);
Assert.Equal(WirePosition, entity.Position);
Assert.Equal(WireCell, entity.ParentCellId);
Assert.Equal(wireRotation, entity.Rotation);
}
private static WorldEntity MakeEntity() => new()
{
Id = 1u,
SourceGfxObjOrSetupId = 1u,
Position = OldPosition,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
ParentCellId = OldCell,
};
private const uint Cell = 0x0101FFFFu;
private static RuntimeAuthoritativePositionRoute Classify(
bool hasContact,
float playerDistance,
uint committedCellId = OldCell,
PositionTimestampDisposition disposition =
PositionTimestampDisposition.Apply) =>
RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition(
new RuntimeAcceptedPositionRouteRequest(
new RuntimeAuthoritativePositionAuthority(
new RuntimeGenerationToken(7),
new RuntimeEntityKey(0x70000001u, 3),
PositionAuthorityVersion: 11UL,
AcceptedPositionSequence: 20,
PreviousTeleportSequence: 10,
AcceptedTeleportSequence: 10,
disposition),
RuntimePositionEntityKind.Remote,
RuntimeAcceptedPositionSource.PositionEvent,
new CreateObject.ServerPosition(
Cell, 10f, 20f, 30f, 1f, 0f, 0f, 0f),
PlacementFrame: 0u,
PositionPackVelocity: Vector3.Zero,
committedCellId,
hasContact,
playerDistance,
UsePositionFromServer: false,
HasAnimations: false,
default));
}

View file

@ -0,0 +1,368 @@
using System.Numerics;
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>
/// C4 route 4a: focused tests for the Runtime-owned decision that replaces
/// the two independent per-kind (player-remote / NPC-remote) copies that
/// used to live in <c>LiveEntityNetworkUpdateController</c>. Each test below
/// was verified to actually discriminate its own fix by temporarily
/// reverting the corresponding condition in
/// <see cref="RuntimeRemoteSteadyStatePosition"/> and confirming the
/// matching test failed, then restoring it — see the route 4a implementation
/// report for the revert/restore log.
/// </summary>
public sealed class RuntimeRemoteSteadyStatePositionTests
{
private const uint Cell = 0x0101FFFFu;
/// <summary>A remote that has already received at least one server
/// sample, so AP-87's <c>firstUp</c> hint is false and the other two
/// conditions are the ones under test.</summary>
private static RemoteMotion MakeSampledRemote(Vector3 bodyPosition)
{
var remote = new RemoteMotion();
remote.Body.Position = bodyPosition;
remote.Body.Orientation = Quaternion.Identity;
remote.LastServerPosTime = 1_700_000_000d;
return remote;
}
private static (RemoteMotion Remote, EntityPhysicsHost Host) MakeRemoteWithHost(
Vector3 bodyPosition)
{
RemoteMotion remote = MakeSampledRemote(bodyPosition);
EntityPhysicsHost host = new(
id: 0x70000001u,
getPosition: () => new Position(0x0001u, remote.Body.Position, remote.Body.Orientation),
getVelocity: () => remote.Body.Velocity,
getRadius: () => 0.48f,
inContact: () => remote.Body.InContact,
minterpMaxSpeed: () => null,
curTime: () => 0d,
physicsTimerTime: () => 0d,
getObjectA: _ => null,
handleUpdateTarget: _ => { },
interruptCurrentMovement: () => { });
remote.BindCanonicalRuntime(
() => host,
() => 0x0001u,
_ => { });
remote.MarkFullPhysicsHostBound();
return (remote, host);
}
// ── AP-87 (register row, carried forward) ──────────────────────────────
[Fact]
public void ApplyInterpolate_BodyFarFromTarget_SnapsRatherThanEnqueues()
{
// AP-87's load-bearing condition: |Body.Position - worldPos| > 4 m.
// An unplaced body (spawn-seed origin) must SNAP, never enqueue —
// enqueuing here is exactly the #184 invisible-but-solid regression
// this backstop exists to prevent.
RemoteMotion remote = MakeSampledRemote(new Vector3(0f, 0f, 0f));
var target = new Vector3(50f, 0f, 0f); // 50 m away — far beyond 4 m.
RuntimeRemoteSteadyStatePosition.Action action =
RuntimeRemoteSteadyStatePosition.ApplyInterpolate(
remote,
target,
Quaternion.Identity,
isMovingTo: false,
willBeDrTicked: true);
Assert.Equal(RuntimeRemoteSteadyStatePosition.Action.Snapped, action);
Assert.Equal(target, remote.Body.Position);
}
[Fact]
public void ApplyInterpolate_NotDrTicked_SnapsEvenWhenClose()
{
// AP-87's second condition: !willBeDrTicked. A body with no consumer
// to walk the queue must snap even for a 1 m correction, or it never
// reaches the target at all.
RemoteMotion remote = MakeSampledRemote(new Vector3(10f, 10f, 5f));
var target = new Vector3(10.5f, 10f, 5f); // 0.5 m — well within 4 m.
RuntimeRemoteSteadyStatePosition.Action action =
RuntimeRemoteSteadyStatePosition.ApplyInterpolate(
remote,
target,
Quaternion.Identity,
isMovingTo: false,
willBeDrTicked: false);
Assert.Equal(RuntimeRemoteSteadyStatePosition.Action.Snapped, action);
Assert.Equal(target, remote.Body.Position);
}
[Fact]
public void ApplyInterpolate_FirstUpBeforeAnyServerSample_Snaps()
{
// AP-87's third condition, carried forward from the NPC copy rather
// than silently dropped: a remote that has never been stamped with a
// server sample snaps even when both other conditions are benign.
// Structurally unreachable for player remotes, whose caller stamps
// LastServerPosTime before it routes — which is exactly why keeping
// it costs the player arm nothing.
var remote = new RemoteMotion();
remote.Body.Position = new Vector3(10f, 10f, 5f);
remote.Body.Orientation = Quaternion.Identity;
Assert.Equal(0d, remote.LastServerPosTime);
var target = new Vector3(10.5f, 10f, 5f);
RuntimeRemoteSteadyStatePosition.Action action =
RuntimeRemoteSteadyStatePosition.ApplyInterpolate(
remote,
target,
Quaternion.Identity,
isMovingTo: false,
willBeDrTicked: true);
Assert.Equal(RuntimeRemoteSteadyStatePosition.Action.Snapped, action);
Assert.Equal(target, remote.Body.Position);
}
[Fact]
public void ApplyInterpolate_NearAndDrTicked_EnqueuesWithoutTouchingBodyPosition()
{
// The ordinary retail near case: no AP-87 condition holds, so the
// body is queued for the per-tick catch-up, not hard-snapped.
RemoteMotion remote = MakeSampledRemote(new Vector3(10f, 10f, 5f));
var target = new Vector3(10.5f, 10f, 5f);
RuntimeRemoteSteadyStatePosition.Action action =
RuntimeRemoteSteadyStatePosition.ApplyInterpolate(
remote,
target,
Quaternion.Identity,
isMovingTo: false,
willBeDrTicked: true);
Assert.Equal(RuntimeRemoteSteadyStatePosition.Action.Enqueued, action);
// The body itself is not hard-moved by the enqueue path — the
// per-tick InterpolationManager/adjust_offset chain walks it there.
Assert.Equal(new Vector3(10f, 10f, 5f), remote.Body.Position);
}
/// <summary>
/// TS-44 stays an NPC-only CALLER gate: the seam itself is the
/// kind-agnostic retail decision, so a sticky-armed host does NOT change
/// what it does. Folding the sticky check in here would have silently
/// extended TS-44 to player remotes, which have never had one.
/// </summary>
[Fact]
public void ApplyInterpolate_IsIndifferentToTheStickyLease()
{
(RemoteMotion remote, EntityPhysicsHost host) = MakeRemoteWithHost(
new Vector3(0f, 0f, 0f));
host.PositionManager.StickTo(objectId: 0x70000002u, radius: 1f, height: 1f);
Assert.NotEqual(0u, host.PositionManager.GetStickyObjectId());
RuntimeRemoteSteadyStatePosition.Action action =
RuntimeRemoteSteadyStatePosition.ApplyInterpolate(
remote,
new Vector3(50f, 0f, 0f),
Quaternion.Identity,
isMovingTo: false,
willBeDrTicked: true);
Assert.Equal(RuntimeRemoteSteadyStatePosition.Action.Snapped, action);
}
// ── Per-entity currency ─────────────────────────────────────────────────
// Contract item 4: N remotes, no shared pending slot — unlike route 2's
// single-slot RuntimeAcceptedPositionDriveController, this decision
// carries no state of its own between calls. Two remotes classified in
// the same tick must not observe or mutate each other.
[Fact]
public void TwoRemotesInTheSameTick_ApplyIndependentlyWithNoCrossContamination()
{
RemoteMotion farRemote = MakeSampledRemote(new Vector3(0f, 0f, 0f)); // will snap.
RemoteMotion nearRemote = MakeSampledRemote(new Vector3(10f, 10f, 5f)); // will enqueue.
RuntimeRemoteSteadyStatePosition.Action farAction =
RuntimeRemoteSteadyStatePosition.ApplyInterpolate(
farRemote,
new Vector3(50f, 0f, 0f),
Quaternion.Identity,
isMovingTo: false,
willBeDrTicked: true);
RuntimeRemoteSteadyStatePosition.Action nearAction =
RuntimeRemoteSteadyStatePosition.ApplyInterpolate(
nearRemote,
new Vector3(10.5f, 10f, 5f),
Quaternion.Identity,
isMovingTo: false,
willBeDrTicked: true);
Assert.Equal(RuntimeRemoteSteadyStatePosition.Action.Snapped, farAction);
Assert.Equal(RuntimeRemoteSteadyStatePosition.Action.Enqueued, nearAction);
Assert.Equal(new Vector3(50f, 0f, 0f), farRemote.Body.Position);
// nearRemote's body is untouched by farRemote's snap — no shared state.
Assert.Equal(new Vector3(10f, 10f, 5f), nearRemote.Body.Position);
}
// ── Ownership: exactly two dispositions, everything else falls through ──
[Fact]
public void OwnsSteadyState_IsTrueForExactlyTheTwoNoPlacementDispositions()
{
Assert.True(RuntimeRemoteSteadyStatePosition.OwnsSteadyState(
Classify(hasContact: false, playerDistance: 1f)));
Assert.True(RuntimeRemoteSteadyStatePosition.OwnsSteadyState(
Classify(hasContact: true, playerDistance: 10f)));
}
[Theory]
// Far (>=96 m) — SetPositionSimple, route 4b.
[InlineData(true, 200f, Cell, 10, 10, PositionTimestampDisposition.Apply)]
// Cell-less canonical body — SetPosition, route 4b.
[InlineData(true, 10f, 0u, 10, 10, PositionTimestampDisposition.Apply)]
// Fresh TELEPORT_TS — SetPosition, route 4b.
[InlineData(true, 10f, Cell, 10, 11, PositionTimestampDisposition.Apply)]
// Rejected admission — RejectedAuthority.
[InlineData(true, 10f, Cell, 10, 10, PositionTimestampDisposition.Rejected)]
// Nonfinite derived distance — RejectedData.
[InlineData(true, float.NaN, Cell, 10, 10, PositionTimestampDisposition.Apply)]
public void OwnsSteadyState_IsFalseForEveryOtherClassification(
bool hasContact,
float playerDistance,
uint committedCellId,
ushort previousTeleport,
ushort acceptedTeleport,
PositionTimestampDisposition disposition)
{
RuntimeAuthoritativePositionRoute route = Classify(
hasContact,
playerDistance,
committedCellId,
previousTeleport,
acceptedTeleport,
disposition);
Assert.NotEqual(
RuntimeAuthoritativePositionDisposition.Interpolate,
route.Disposition);
Assert.NotEqual(
RuntimeAuthoritativePositionDisposition.NoPositionOperation,
route.Disposition);
Assert.False(RuntimeRemoteSteadyStatePosition.OwnsSteadyState(route));
}
[Fact]
public void OwnsSteadyState_IsFalseWhenNothingWasClassified()
{
// A null route is "route 4a has no opinion" — the legacy path runs
// completely unchanged, exactly as its doc comment claims.
Assert.False(RuntimeRemoteSteadyStatePosition.OwnsSteadyState(null));
Assert.False(RuntimeRemoteSteadyStatePosition.IsAirborneNoOperation(null));
Assert.False(RuntimeRemoteSteadyStatePosition.IsNearInterpolate(null));
}
// ── D2: ConstrainTo timing/anchor ───────────────────────────────────────
[Fact]
public void ArmConstraintAfterOperation_AnchorsToTheHostsCurrentLivePosition()
{
(RemoteMotion remote, EntityPhysicsHost host) = MakeRemoteWithHost(
new Vector3(1f, 2f, 3f));
// Simulate the operation having already moved the body (a Snapped
// outcome) BEFORE ConstrainTo arms — D2 requires the anchor to read
// the POST-move position, never the pre-move one.
remote.Body.Position = new Vector3(9f, 9f, 9f);
RuntimeRemoteSteadyStatePosition.ArmConstraintAfterOperation(host);
ConstraintManager? constraint = host.PositionManager.Constraint;
Assert.NotNull(constraint);
Assert.True(constraint!.IsConstrained);
Assert.Equal(new Vector3(9f, 9f, 9f), constraint.ConstraintPos.Frame.Origin);
// Anchored to itself at arm time -> zero initial offset, matching
// retail's per-packet re-anchor.
Assert.Equal(0f, constraint.ConstraintPosOffset, 3);
}
[Fact]
public void TryArmConstraintAfterOperation_ArmsForTheNearInterpolateBranch()
{
(RemoteMotion remote, EntityPhysicsHost host) = MakeRemoteWithHost(
new Vector3(1f, 2f, 3f));
Assert.True(RuntimeRemoteSteadyStatePosition.TryArmConstraintAfterOperation(
Classify(hasContact: true, playerDistance: 10f),
remote));
Assert.True(host.PositionManager.Constraint?.IsConstrained);
}
[Fact]
public void TryArmConstraintAfterOperation_SkipsTheAirborneNoOperation()
{
// MoveOrTeleport returns 0 for arg4 == 0, so HandleReceivedPosition's
// `if (MoveOrTeleport(...) != 0)` never reaches ConstrainTo.
(RemoteMotion remote, EntityPhysicsHost host) = MakeRemoteWithHost(
new Vector3(1f, 2f, 3f));
Assert.False(RuntimeRemoteSteadyStatePosition.TryArmConstraintAfterOperation(
Classify(hasContact: false, playerDistance: 10f),
remote));
Assert.Null(host.PositionManager.Constraint);
}
[Fact]
public void TryArmConstraintAfterOperation_SkipsClassificationsRoute4aDoesNotOwn()
{
// The far branch still arms its leash — through the untouched legacy
// PRE-operation call site, not through here. Arming here too would
// double-arm it.
(RemoteMotion remote, EntityPhysicsHost host) = MakeRemoteWithHost(
new Vector3(1f, 2f, 3f));
Assert.False(RuntimeRemoteSteadyStatePosition.TryArmConstraintAfterOperation(
Classify(hasContact: true, playerDistance: 200f),
remote));
Assert.False(RuntimeRemoteSteadyStatePosition.TryArmConstraintAfterOperation(
null,
remote));
Assert.Null(host.PositionManager.Constraint);
}
private static RuntimeAuthoritativePositionRoute Classify(
bool hasContact,
float playerDistance,
uint committedCellId = Cell,
ushort previousTeleport = 10,
ushort acceptedTeleport = 10,
PositionTimestampDisposition disposition =
PositionTimestampDisposition.Apply) =>
RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition(
new RuntimeAcceptedPositionRouteRequest(
new RuntimeAuthoritativePositionAuthority(
new RuntimeGenerationToken(7),
new RuntimeEntityKey(0x70000001u, 3),
PositionAuthorityVersion: 11UL,
AcceptedPositionSequence: 20,
previousTeleport,
acceptedTeleport,
disposition),
RuntimePositionEntityKind.Remote,
RuntimeAcceptedPositionSource.PositionEvent,
new CreateObject.ServerPosition(
Cell, 10f, 20f, 30f, 1f, 0f, 0f, 0f),
PlacementFrame: 0u,
PositionPackVelocity: Vector3.Zero,
committedCellId,
hasContact,
playerDistance,
UsePositionFromServer: false,
HasAnimations: false,
default));
}