fix(physics): C4 route 5 — projectile authoritative placement (#276 partial)

Ports retail's missile Position handling into the canonical Runtime
placement owner instead of the deleted ApplyAuthoritativePosition
short-circuit. The Create/residence-window halves of the projectile
pipeline (RuntimeProjectile binding, TryBind's adopted-body branch,
the collision/shadow registration) were already canonical from prior
slices; this closes the remaining gap — how an ACCEPTED Position for
an in-flight missile is classified, placed, and presented.

Byte-decode (Step 1 hard gate, before any code was written):
CPhysicsObj::MoveOrTeleport @0x00516330-0x00516438 disassembled from
the PDB-paired binary (Capstone, x86 32-bit thiscall). `ret 0x10`
establishes four stack args; [esp+0x7c] (arg5, the velocity pointer)
is never referenced in any of the three branches (teleport/near/far).
The retail reviewer independently reproduced this by searching the
whole function body for the `24 7c` mod/rm+disp8 encoding a
`[esp+0x7c]` read would require and found zero occurrences. This
retired a fabricated `?? Vector3.Zero` fallback in the deleted method
— retail's PositionPack::UnPack initializes an absent velocity to
zero and MoveOrTeleport never installs it; the projectile's Vector
channel (RuntimeProjectilePhysicsUpdater.ApplyAuthoritativeVector)
remains the sole velocity authority for a missile. D-P5 in the
contract; the Runtime seam commits no velocity from the Position
packet at all.

The unbound-missile fix: RuntimeEntityObjectLifetime's
ClassifyRemoteAcceptedPosition now derives ProjectileAuthoritative
from a CONJUNCTIVE predicate — the Missile bit AND a bound
RuntimeProjectile whose Body is the canonical PhysicsBody — never the
bit alone. Retail places every non-player CPhysicsObj unconditionally
(there is no missile-specific placement gate in MoveOrTeleport or its
callers), so an unbindable or not-yet-bound missile taking the
ordinary remote tail is retail-faithful, not a fallback: the earlier
bit-only discriminator would have silently frozen it instead.

AP-141 records this as a deliberate, recorded divergence, not
fidelity. Retail mechanically WOULD arm a missile's ConstrainTo leash
on any nonzero MoveOrTeleport return: HandleReceivedPosition
@0x00453FD0's only kind test is player-vs-not, ConstrainTo
@0x00454272 has no kind test of its own, and CPhysicsObj::ConstrainTo
@0x00510520 creates a PositionManager on demand via
MakePositionManager @0x00510523 if one doesn't exist. acdream
deliberately does not construct that EntityPhysicsHost/
PositionManager/InterpolationManager chain for a ballistic body — the
route-5b split the C4 route 5 contract rejected — so a live missile
never shows an armed leash and never catches up via the near/
UnroutedCatchUp policy. This divergence is safe specifically because
ACE never sends UpdatePosition for a missile
(references/ACE/Source/ACE.Server/WorldObjects/WorldObject_Tick.cs:
333-334, SendUpdatePosition() commented out inside the
PhysicsState.Missile branch at :265) — every half of this row is
deterministic-test-gated only, never exercised against a real server.

AP-141 also records the surviving ConstrainTo re-anchor divergence
under clause (b): for the adopted-body case (TryBind's shared-body
branch — an ordinary remote whose Missile bit is set by a later
State packet, so it still carries a live RemoteMotion), acdream now
ports retail's teleport-branch and far-branch StopInterpolating
action (Interp.Clear()), but never re-arms or re-anchors the
inherited ConstrainTo leash the way retail's HandleReceivedPosition
@0x00454254/@0x00454272 does on every nonzero return. The risk
column's earlier wording — that a stale leash "would drag the body
toward a stale anchor" — was wrong and is retracted in this same
commit: ConstraintManager.ConstraintPos is write-only in both retail
and the port (never read by AdjustOffset), and
ConstraintManager::adjust_offset @0x00556180 only tapers or zeroes an
already-composed per-tick offset while InContact — a leash brakes
motion the interp/sticky chain already produced, it cannot pull
anything toward the anchor. The real residual is one tick of un-reset
brake accumulator, contact-gated, and it cannot move an airborne
far-snapped missile at all (the clamp branch does not run while
airborne).

NO CONNECTED GATE EXISTS for this route, by design: ACE never sends a
missile UpdatePosition (see above), so retail's own server never
exercises this code path in play. Every proof obligation here is
test-gated only — Runtime and App-level fixtures constructing the
packet directly — never a live client/server capture.

Three review rounds closed 8 MAJOR findings before this landed:
round 1 (A1 App discarded the seam's status; A2/R1 silent swallow on
an unbound missile; A3/R2 the adopted-body teleport_hook never
wired; A4/A5 zero Runtime/App test coverage); round 2 (a
ParentCellId regression introduced by round 1's own R6 finding,
which the retail reviewer retracted the following round as factually
wrong — the fix here is the REVERT to record.FullCellId, not the
relocation round 1 shipped; B2 the far-branch StopInterpolating skip
never extended to the adopted-body case; residual App/Runtime store-
path coverage; a per-packet closure contradicting the file's own
#315 cached-delegate pattern). Round 3 closed on coverage alone (no
defect): the Advance() retry arm's projectile branch — added at
round 2, semantically reordered at round 2's B5 fix (skip prediction
invalidation on a re-parked Contention, since it writes nothing) —
had never been executed by any test; two new tests drive it directly
and are sabotage-verified against both the reordering and the
retry-arm's own SyncProjectilePresentation call site. The one
recorded defect this campaign produced (the ParentCellId regression)
was caused by complying with a review finding that its own author
later retracted — the standing lesson recorded for future rounds is
that review findings are evidence to re-verify against the code, not
commands to obey unconditionally.

Complete Release suite: 11,063 passed / 4 skipped / 0 failed
(baseline 11,036 at 30d3d114, +27 new tests across this campaign).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-04 21:03:41 +02:00
parent 30d3d114b0
commit 36255af0f6
19 changed files with 5390 additions and 393 deletions

View file

@ -117,9 +117,16 @@ public sealed class RuntimeRemotePlacementDriveControllerTests
/// teleport branches (<c>RuntimeSetPositionOperationKind.LocalAuthoritative</c>),
/// not only for remotes. The static predicate itself is exercised
/// directly (no entity/body needed) since it takes only the route.
///
/// <para>
/// C4 route 5 (D-P3): <c>ProjectileAuthoritative</c> is REMOVED from this
/// negative list — the widening makes it a positively-owned kind now
/// (see <see cref="OwnsPlacement_TrueForProjectileAuthoritative_SetPositionAndSetPositionSimple"/>).
/// Only the two kinds that stay excluded remain here.
/// </para>
/// </summary>
[Fact]
public void OwnsPlacement_FalseWhenOperationKindIsNotRemoteAuthoritative()
public void OwnsPlacement_FalseWhenOperationKindIsNotRemoteOrProjectileAuthoritative()
{
// RuntimeSetPositionOperationKind is internal, so a public [Theory]
// cannot take it as a parameter (CS0051) — iterate directly instead,
@ -130,7 +137,6 @@ public sealed class RuntimeRemotePlacementDriveControllerTests
{
RuntimeSetPositionOperationKind.InitialLogin,
RuntimeSetPositionOperationKind.LocalAuthoritative,
RuntimeSetPositionOperationKind.ProjectileAuthoritative,
})
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
@ -2256,6 +2262,845 @@ public sealed class RuntimeRemotePlacementDriveControllerTests
AssertConverged(lifetime);
}
// ── C4 route 5: projectile arm (D-P2/D-P3/D-P4/D-P5) ───────────────────
/// <summary>
/// D-P3: the widening itself, isolated from any entity/body — mirrors
/// <see cref="OwnsPlacement_FalseWhenOperationKindIsNotRemoteAuthoritative"/>'s
/// shape but for the positive case.
/// </summary>
[Fact]
public void OwnsPlacement_TrueForProjectileAuthoritative_SetPositionAndSetPositionSimple()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70004001u);
foreach (RuntimeAuthoritativePositionDisposition disposition in
new[]
{
RuntimeAuthoritativePositionDisposition.SetPosition,
RuntimeAuthoritativePositionDisposition.SetPositionSimple,
})
{
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
disposition,
DestinationCell,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
Assert.True(RuntimeRemotePlacementDriveController.OwnsPlacement(route));
}
// A projectile Create (InitialCreateFlags, no Teleport bit) is still
// excluded — the same Teleport-flag discriminator that excludes a
// remote top-level Create.
RuntimeAuthoritativePositionRoute createRoute = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPosition,
DestinationCell,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative,
setPositionFlags: PhysicsSetPositionFlags.Placement
| PhysicsSetPositionFlags.Slide);
Assert.False(RuntimeRemotePlacementDriveController.OwnsPlacement(createRoute));
}
[Fact]
public void ApplyAcceptedProjectilePosition_Null_WhenOperationKindIsNotProjectileAuthoritative()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
(RuntimeEntityRecord record, _) = CreateProjectileRecord(
lifetime, 0x70004002u, SourceCell);
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPosition,
DestinationCell,
operationKind: RuntimeSetPositionOperationKind.RemoteAuthoritative);
Assert.Null(drive.ApplyAcceptedProjectilePosition(record, route));
Assert.Equal(0, drive.PendingCount);
AssertConverged(lifetime);
}
[Fact]
public void ApplyAcceptedProjectilePosition_Null_WhenNoProjectileComponentIsBound()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
RuntimeEntityRecord record = CreateRemoteRecord(lifetime, 0x70004003u);
AttachBody(lifetime, record, SourceCell);
// Deliberately never BindProjectile — record.Projectile stays null.
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPosition,
DestinationCell,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
Assert.Null(drive.ApplyAcceptedProjectilePosition(record, route));
}
/// <summary>
/// D-P2's teleport/cell-less row + D-P4's force-end + D-P5's no-velocity,
/// asserted together on the ONE committed outcome (process rule 4 —
/// assert the full observable surface, not a subset). The body moves to
/// the resolved (world-frame-shifted) destination, the prediction version
/// advances (trap T3's guard), an in-flight nonzero velocity survives
/// bit-identical (D-P5), no <c>RemoteMotion</c>/constraint host exists
/// anywhere for the entity (D-P4's never-armed pin), and the collision
/// table the teleport hook reduction force-ends is empty afterward
/// (proof obligation P5).
/// </summary>
[Fact]
public void ApplyAcceptedProjectilePosition_TeleportCommit_MovesBodyForceEndsCollisionNoVelocityNoConstraint()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
CommitLandblockCollision(lifetime, DestinationLandblock);
(RuntimeEntityRecord record, RuntimeProjectile projectile) =
CreateProjectileRecord(lifetime, 0x70004004u, SourceCell);
PhysicsBody body = record.PhysicsBody!;
var inFlightVelocity = new Vector3(5f, 0f, -2f);
body.set_velocity(inFlightVelocity);
ulong predictionBefore = projectile.PredictionAuthorityVersion;
SeedCollisionOwner(lifetime, record, 0x70004104u, SourceCell);
Assert.Equal(
1, lifetime.Physics.CollisionReports.CaptureOwnership().OwnerCount);
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
// Airborne (well above CommitLandblockCollision's flat terrain at
// SpawnHeight): landing in ground contact would legitimately let the
// shared placement pipeline's ordinary contact response touch
// velocity (retail landing behaviour, not this route's concern) —
// an airborne destination isolates the no-velocity-FROM-THE-PACKET
// assertion from that confound.
var destination = new Vector3(12f, 14f, SpawnHeight + 10f);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPosition,
DestinationCell,
destination,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
RuntimeRemotePlacementExecutionStatus? status =
drive.ApplyAcceptedProjectilePosition(record, route);
Assert.Equal(RuntimeRemotePlacementExecutionStatus.Committed, status);
Assert.Equal(destination + new Vector3(192f, 0f, 0f), body.Position);
Assert.NotEqual(predictionBefore, projectile.PredictionAuthorityVersion);
Assert.Equal(inFlightVelocity, body.Velocity);
Assert.Null(record.RemoteMotion);
Assert.Equal(
0, lifetime.Physics.CollisionReports.CaptureOwnership().OwnerCount);
// A4 fix (review round): the shadow-sync half of
// SyncProjectilePresentation, asserted directly rather than left
// vacuous — the shadow row moves to the RESOLVED body position.
Assert.True(body.InWorld);
ShadowEntry shadowEntry = Assert.Single(
lifetime.Physics.Engine.ShadowObjects.AllEntriesForDebug(),
entry => entry.EntityId == record.Key!.Value.LocalEntityId);
Assert.Equal(body.Position, shadowEntry.Position);
DrainPlacementFifo(lifetime);
AssertConverged(lifetime);
}
/// <summary>D-P2's far row, mirroring the teleport commit's assertions.</summary>
[Fact]
public void ApplyAcceptedProjectilePosition_FarCommit_MovesBodyNoVelocityNoConstraint()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
CommitLandblockCollision(lifetime, DestinationLandblock);
(RuntimeEntityRecord record, RuntimeProjectile projectile) =
CreateProjectileRecord(lifetime, 0x70004005u, SourceCell);
PhysicsBody body = record.PhysicsBody!;
var inFlightVelocity = new Vector3(0f, 7f, 1f);
body.set_velocity(inFlightVelocity);
ulong predictionBefore = projectile.PredictionAuthorityVersion;
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
var destination = new Vector3(12f, 14f, SpawnHeight + 10f);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPositionSimple,
DestinationCell,
destination,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
RuntimeRemotePlacementExecutionStatus? status =
drive.ApplyAcceptedProjectilePosition(record, route);
Assert.Equal(RuntimeRemotePlacementExecutionStatus.Committed, status);
Assert.Equal(destination + new Vector3(192f, 0f, 0f), body.Position);
Assert.NotEqual(predictionBefore, projectile.PredictionAuthorityVersion);
Assert.Equal(inFlightVelocity, body.Velocity);
Assert.Null(record.RemoteMotion);
Assert.True(body.InWorld);
ShadowEntry shadowEntry = Assert.Single(
lifetime.Physics.Engine.ShadowObjects.AllEntriesForDebug(),
entry => entry.EntityId == record.Key!.Value.LocalEntityId);
Assert.Equal(body.Position, shadowEntry.Position);
DrainPlacementFifo(lifetime);
AssertConverged(lifetime);
}
/// <summary>
/// A4 fix (review round): the spatial+hidden branch — the entity stays
/// <c>InWorld</c> (retail keeps a Hidden object as a retained live
/// <c>CPhysicsObj</c>, not a leave-world) but its shadow row is
/// suspended, not published at the resolved pose.
/// </summary>
[Fact]
public void ApplyAcceptedProjectilePosition_TeleportCommit_HiddenSuspendsShadowStaysInWorld()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
CommitLandblockCollision(lifetime, DestinationLandblock);
(RuntimeEntityRecord record, _) =
CreateProjectileRecord(lifetime, 0x7000400Bu, SourceCell);
Assert.Equal(1, lifetime.Physics.Engine.ShadowObjects.TotalRegistered);
lifetime.Entities.SetFinalPhysicsState(
record, record.FinalPhysicsState | PhysicsStateFlags.Hidden);
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
var destination = new Vector3(12f, 14f, SpawnHeight + 10f);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPosition,
DestinationCell,
destination,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
Assert.Equal(
RuntimeRemotePlacementExecutionStatus.Committed,
drive.ApplyAcceptedProjectilePosition(record, route));
Assert.True(record.PhysicsBody!.InWorld);
Assert.Equal(0, lifetime.Physics.Engine.ShadowObjects.TotalRegistered);
DrainPlacementFifo(lifetime);
AssertConverged(lifetime);
}
/// <summary>
/// A4 fix (review round): the non-spatial branch — a record that never
/// became a spatial root (e.g. still pending a landblock) is left
/// <c>InWorld = false</c>, its <c>Active</c> transient flag cleared, and
/// its shadow suspended.
///
/// <para>
/// Uses the STORE (<c>Refused</c>) path rather than a commit: a
/// successful canonical commit re-establishes spatial-root status as
/// part of entering the world, so the non-spatial branch is reachable
/// only through the outcomes that never touch spatial registration —
/// exactly the store fallback's shape.
/// </para>
/// </summary>
[Fact]
public void ApplyAcceptedProjectilePosition_Refused_NonSpatialDeactivatesAndSuspends()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
lifetime.Physics.ObserveLocalWorldFrame(SourceCell, teleportAdvanced: false);
(RuntimeEntityRecord record, _) =
CreateProjectileRecord(lifetime, 0x7000400Cu, SourceCell);
Assert.Equal(1, lifetime.Physics.Engine.ShadowObjects.TotalRegistered);
// Withdraw spatial-root status — AcknowledgeSpatialProjection
// (spatial: false) is a no-op (only its `true` branch touches
// _spatialRoots); RemoveSpatialProjection is the actual withdrawal.
lifetime.Physics.RemoveSpatialProjection(record);
var window = new FakeServiceWindow();
// Deliberately NOT allowed — the pre-flight refuses, so the store
// fallback runs without ever touching spatial registration.
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
var destination = new Vector3(12f, 14f, SpawnHeight + 10f);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPositionSimple,
DestinationCell,
destination,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
Assert.Equal(
RuntimeRemotePlacementExecutionStatus.Refused,
drive.ApplyAcceptedProjectilePosition(record, route));
Assert.False(record.PhysicsBody!.InWorld);
Assert.Equal(
TransientStateFlags.None,
record.PhysicsBody.TransientState & TransientStateFlags.Active);
Assert.Equal(0, lifetime.Physics.Engine.ShadowObjects.TotalRegistered);
DrainPlacementFifo(lifetime);
AssertConverged(lifetime);
}
/// <summary>
/// A6 fix (review round): the re-entry activation edge. A projectile
/// that had left the world (suspended, <c>InWorld = false</c>) and comes
/// back through a committed accepted Position must be re-flagged
/// <c>Active</c> and have its legacy <c>LastUpdateTime</c> rebased — the
/// exact branch that reading <c>body.InWorld</c> AFTER the placement
/// (instead of capturing it before) made permanently dead.
/// </summary>
[Fact]
public void ApplyAcceptedProjectilePosition_TeleportCommit_ReenteringWorldReactivatesBody()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
CommitLandblockCollision(lifetime, DestinationLandblock);
(RuntimeEntityRecord record, _) =
CreateProjectileRecord(lifetime, 0x7000400Du, SourceCell);
PhysicsBody body = record.PhysicsBody!;
body.InWorld = false;
body.TransientState &= ~TransientStateFlags.Active;
body.LastUpdateTime = -1d;
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
var destination = new Vector3(12f, 14f, SpawnHeight + 10f);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPosition,
DestinationCell,
destination,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
Assert.Equal(
RuntimeRemotePlacementExecutionStatus.Committed,
drive.ApplyAcceptedProjectilePosition(record, route));
Assert.True(body.InWorld);
Assert.Equal(
TransientStateFlags.Active,
body.TransientState & TransientStateFlags.Active);
Assert.NotEqual(-1d, body.LastUpdateTime);
DrainPlacementFifo(lifetime);
AssertConverged(lifetime);
}
/// <summary>
/// D3's <c>Refused</c> row: the destination is outside the service
/// window, so the pre-flight declines before the engine ever runs — but
/// the accepted destination STILL advances through
/// <c>StoreAcceptedDestinationPose</c> (4b-3 invariant 1, extended by
/// D-P3 to the projectile column). Positive assertions throughout
/// (round-2 finding B1): the pose moved, the entity stayed in-world, and
/// prediction still invalidated once (the store fallback is a body write
/// too).
/// </summary>
[Fact]
public void ApplyAcceptedProjectilePosition_Refused_StillAdvancesPoseNoParkPredictionInvalidated()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
// StoreAcceptedDestinationPose resolves the destination through
// Runtime's OWN world frame (never a caller-supplied position) —
// establish it exactly like CommitLandblockCollision's first step,
// without needing the destination's collision generation to commit
// (this test never reaches the engine).
lifetime.Physics.ObserveLocalWorldFrame(SourceCell, teleportAdvanced: false);
(RuntimeEntityRecord record, RuntimeProjectile projectile) =
CreateProjectileRecord(lifetime, 0x70004006u, SourceCell);
PhysicsBody body = record.PhysicsBody!;
ulong predictionBefore = projectile.PredictionAuthorityVersion;
var window = new FakeServiceWindow();
// Deliberately NOT allowed — the pre-flight refuses.
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
var destination = new Vector3(12f, 14f, SpawnHeight);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPositionSimple,
DestinationCell,
destination,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
RuntimeRemotePlacementExecutionStatus? status =
drive.ApplyAcceptedProjectilePosition(record, route);
Assert.Equal(RuntimeRemotePlacementExecutionStatus.Refused, status);
// The store fallback resolves through Runtime's world frame, the
// same +192m shift on X the committed-outcome tests observe.
Assert.Equal(destination + new Vector3(192f, 0f, 0f), body.Position);
Assert.NotEqual(predictionBefore, projectile.PredictionAuthorityVersion);
Assert.True(body.InWorld);
Assert.True(record.ObjectClock.IsActive);
Assert.Equal(0, drive.PendingCount);
// Residual 2 close (round-2 review): the store path publishes the
// shadow row too, not only the commit path the teleport/far commit
// tests already assert — SyncProjectilePresentation runs on every
// storing outcome, Refused included.
ShadowEntry shadowEntry = Assert.Single(
lifetime.Physics.Engine.ShadowObjects.AllEntriesForDebug(),
entry => entry.EntityId == record.Key!.Value.LocalEntityId);
Assert.Equal(body.Position, shadowEntry.Position);
AssertConverged(lifetime);
}
/// <summary>
/// D-P4's pinned no-op pair: <c>Interpolate</c> (near) and
/// <c>NoPositionOperation</c> (airborne) write nothing and do not
/// invalidate prediction — the positive fact that a straddling quantum
/// may complete over either.
/// </summary>
[Fact]
public void ApplyAcceptedProjectilePosition_PinnedNoOps_BodyAndPredictionUnchanged()
{
// RuntimeAuthoritativePositionDisposition is internal, so a public
// [Theory] cannot take it as a parameter (CS0051) — iterate directly,
// mirroring OwnsPlacement_FalseWhenOperationKindIsNotRemoteAuthoritative's
// own foreach-over-internal-enum shape.
foreach (RuntimeAuthoritativePositionDisposition disposition in
new[]
{
RuntimeAuthoritativePositionDisposition.Interpolate,
RuntimeAuthoritativePositionDisposition.NoPositionOperation,
})
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
(RuntimeEntityRecord record, RuntimeProjectile projectile) =
CreateProjectileRecord(lifetime, 0x70004007u, SourceCell);
PhysicsBody body = record.PhysicsBody!;
Vector3 positionBefore = body.Position;
Quaternion orientationBefore = body.Orientation;
ulong predictionBefore = projectile.PredictionAuthorityVersion;
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
disposition,
DestinationCell,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
Assert.Null(drive.ApplyAcceptedProjectilePosition(record, route));
Assert.Equal(positionBefore, body.Position);
Assert.Equal(orientationBefore, body.Orientation);
Assert.Equal(predictionBefore, projectile.PredictionAuthorityVersion);
Assert.Null(record.RemoteMotion);
Assert.Equal(0, drive.PendingCount);
AssertConverged(lifetime);
}
}
/// <summary>
/// D-P4's swallow rule (trap T5): a <c>RejectedAuthority</c>/
/// <c>RejectedData</c> classification for a missile packet writes
/// nothing — no body write, no store, no fall-through to any remote arm
/// (there is none reachable from this method regardless).
/// </summary>
[Fact]
public void ApplyAcceptedProjectilePosition_RejectedClassification_Swallowed()
{
foreach (RuntimeAuthoritativePositionDisposition disposition in
new[]
{
RuntimeAuthoritativePositionDisposition.RejectedAuthority,
RuntimeAuthoritativePositionDisposition.RejectedData,
})
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
(RuntimeEntityRecord record, RuntimeProjectile projectile) =
CreateProjectileRecord(lifetime, 0x70004008u, SourceCell);
PhysicsBody body = record.PhysicsBody!;
Vector3 positionBefore = body.Position;
ulong predictionBefore = projectile.PredictionAuthorityVersion;
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
disposition,
DestinationCell,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
Assert.Null(drive.ApplyAcceptedProjectilePosition(record, route));
Assert.Equal(positionBefore, body.Position);
Assert.Equal(predictionBefore, projectile.PredictionAuthorityVersion);
Assert.Equal(0, drive.PendingCount);
AssertConverged(lifetime);
}
}
/// <summary>
/// Test-plan item 6 / proof obligation-11: the SAME "retryable
/// preparation" shape <see cref="Teleport_LedgerConverges_AfterDetachRouteClearsARetainedRetry"/>
/// uses, driven through the projectile arm — proves the shared
/// <c>_pending</c>/<c>_awaitingAcknowledgement</c> ledgers converge for a
/// projectile operation with no new code (trap T9: no second map).
/// </summary>
[Fact]
public void ApplyAcceptedProjectilePosition_LedgerConverges_AfterDetachRouteClearsARetainedRetry()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
CommitLandblockCollision(lifetime, DestinationLandblock);
(RuntimeEntityRecord record, _) = CreateProjectileRecord(
lifetime, 0x70004009u, SourceCell);
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
var routeOwner = new object();
drive.AttachRoute(routeOwner);
var destination = new Vector3(12f, 14f, SpawnHeight);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPosition,
DestinationCell,
destination,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
RuntimeRemotePlacementExecutionStatus? status =
drive.ApplyAcceptedProjectilePosition(record, route);
Assert.Equal(RuntimeRemotePlacementExecutionStatus.Committed, status);
// A committed placement's Place receipt is never acknowledged in
// this bare fixture (no host subscription wired) — exactly the
// awaiting-acknowledgement dimension the ledger must also converge.
Assert.Equal(
1, lifetime.CaptureOwnership().RemotePlacementDrivePendingCount);
drive.DetachRoute(routeOwner);
Assert.Equal(0, drive.PendingCount);
Assert.Equal(
0, lifetime.CaptureOwnership().RemotePlacementDrivePendingCount);
AssertConverged(lifetime);
}
/// <summary>
/// Round-3 architecture review C1: <c>Advance()</c>'s projectile branch
/// (parked at round 2 / R3, reordered at round 2 / B5) had never been
/// executed by any test — all six pre-existing <c>drive.Advance()</c>
/// call sites in this file are remote-kind. This is the re-parked-
/// <c>Contention</c> half, mirroring
/// <see cref="FarSnap_RetryablePreparation_StoresThePoseAndStillRetainsTheRetry"/>
/// against a projectile instead of a remote: <c>UnusedCollisionSource</c>
/// never resolves a nonzero Setup id, so BOTH the entry-point call and
/// the retry keep returning <c>RetrySetupUnavailable</c> —
/// <c>Contention</c> — and the pending entry never drains on its own.
///
/// <para>
/// The B5 semantic change under test: the entry-point call invalidates
/// prediction unconditionally BEFORE the write (the existing, already-
/// asserted behaviour); the RETRY call must NOT invalidate a second time
/// when it re-parks, because a re-parked <c>Contention</c> writes
/// nothing (no store, no commit) — invalidating for it would violate
/// "the no-op dispositions invalidate nothing" on an arm that wrote
/// nothing. <see cref="RuntimeProjectile.PredictionAuthorityVersion"/>
/// captured immediately before and after <c>Advance()</c> must be equal.
/// </para>
/// </summary>
[Fact]
public void Advance_ProjectileRetryReParksAsContention_PredictionNotInvalidatedASecondTime()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
// Deliberately NOT committing DestinationLandblock's collision
// generation — CanAttemptDestination only tests the service window
// and the collision PREFIX quiescence, neither of which this
// scenario needs to fail; the retryable failure comes from the
// Setup read below, exactly like FarSnap_RetryablePreparation_….
lifetime.Physics.ObserveLocalWorldFrame(SourceCell, teleportAdvanced: false);
(RuntimeEntityRecord record, RuntimeProjectile projectile) =
CreateProjectileRecord(
lifetime,
0x7000400Bu,
SourceCell,
setupTableId: 0x02000001u);
PhysicsBody body = record.PhysicsBody!;
Vector3 positionBefore = body.Position;
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
var destination = new Vector3(12f, 14f, SpawnHeight);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPositionSimple,
DestinationCell,
destination,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative,
stopInterpolating: true);
// Entry point: Contention, retained, and prediction invalidated
// exactly once (the pre-existing, already-tested entry-point
// behaviour — asserted again here only as the retry's baseline).
Assert.Equal(
RuntimeRemotePlacementExecutionStatus.Contention,
drive.ApplyAcceptedProjectilePosition(record, route));
Assert.Equal(1, drive.PendingCount);
Assert.Equal(destination + new Vector3(192f, 0f, 0f), body.Position);
Assert.NotEqual(positionBefore, body.Position);
ulong predictionAfterEntry = projectile.PredictionAuthorityVersion;
// The retry: Setup is still unresolved, so SubmitAndResolve returns
// Contention again and re-parks — the B5 no-invalidate branch.
drive.Advance();
Assert.Equal(1, drive.PendingCount);
Assert.Equal(
predictionAfterEntry, projectile.PredictionAuthorityVersion);
// The re-park wrote nothing — the stored pose from the entry point
// is untouched.
Assert.Equal(destination + new Vector3(192f, 0f, 0f), body.Position);
RuntimePlacementCancellationReceipt cancellation =
lifetime.Physics.SetPosition.Forget(record);
if (cancellation.IsValid)
lifetime.Physics.SetPosition.PublishCancellation(cancellation);
drive.Advance();
Assert.Equal(0, drive.PendingCount);
AssertConverged(lifetime);
}
/// <summary>
/// Round-3 architecture review C1, the second half: a retained
/// projectile retry whose destination leaves the service window before
/// the next cadence pump — mirroring
/// <see cref="Advance_DestinationLeavesTheWindow_StoresTheNewestDestinationPose"/>
/// against a projectile. This exercises the STORING side C1 named as
/// unexercised: the retry's window-drop branch invalidates prediction
/// unconditionally (unlike the re-parked-<c>Contention</c> branch above)
/// because it runs <c>StoreAcceptedDestinationPose</c> — a real body
/// write — and it is the second call site (besides the entry point) that
/// must run <see cref="RuntimeRemotePlacementDriveController"/>'s
/// <c>SyncProjectilePresentation</c>, so the shadow row must follow the
/// body here too, closing B3's remaining retry-arm gap.
/// </summary>
[Fact]
public void Advance_ProjectileRetryDestinationLeavesTheWindow_StoresNewestPoseInvalidatesPredictionSyncsShadow()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
lifetime.Physics.ObserveLocalWorldFrame(SourceCell, teleportAdvanced: false);
(RuntimeEntityRecord record, RuntimeProjectile projectile) =
CreateProjectileRecord(
lifetime,
0x7000400Cu,
SourceCell,
setupTableId: 0x02000001u);
PhysicsBody body = record.PhysicsBody!;
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
var firstDestination = new Vector3(12f, 14f, SpawnHeight);
Assert.Equal(
RuntimeRemotePlacementExecutionStatus.Contention,
drive.ApplyAcceptedProjectilePosition(
record,
MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPositionSimple,
DestinationCell,
firstDestination,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative,
stopInterpolating: true)));
Assert.Equal(1, drive.PendingCount);
ulong predictionAfterEntry = projectile.PredictionAuthorityVersion;
// The server keeps broadcasting while the retry sits retained: the
// accepted snapshot moves on, and the destination falls out of the
// service window before the next cadence pump.
var newestDestination = new Vector3(40f, 50f, SpawnHeight);
record.Snapshot = record.Snapshot with
{
Position = new CreateObject.ServerPosition(
DestinationCell,
newestDestination.X,
newestDestination.Y,
newestDestination.Z,
1f,
0f,
0f,
0f),
};
window.Forbid(DestinationLandblock);
drive.Advance();
Assert.Equal(0, drive.PendingCount);
Assert.Equal(
newestDestination + new Vector3(192f, 0f, 0f), body.Position);
Assert.NotEqual(
predictionAfterEntry, projectile.PredictionAuthorityVersion);
// SyncProjectilePresentation ran on the retry arm too — the shadow
// row followed the body to the newest stored pose.
ShadowEntry shadowEntry = Assert.Single(
lifetime.Physics.Engine.ShadowObjects.AllEntriesForDebug(),
entry => entry.EntityId == record.Key!.Value.LocalEntityId);
Assert.Equal(body.Position, shadowEntry.Position);
Assert.Equal(
0, lifetime.Physics.CaptureOwnership().SetPositionOperationCount);
AssertConverged(lifetime);
}
/// <summary>
/// Test-plan item 4 (trap T3): a split quantum straddling an accepted
/// far/teleport Position must abort at <c>Complete</c> rather than
/// clobber the committed placement — the scenario invisible from reading
/// the classifier alone, and the one this route's App-level predecessor
/// test (<c>AuthoritativeMutationBetweenQuantumHalvesDiscardsPrediction</c>)
/// used to cover before its Position case retired.
/// </summary>
[Fact]
public void ApplyAcceptedProjectilePosition_DuringOpenQuantum_CompleteAbortsAfterPredictionInvalidated()
{
using var lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
CommitLandblockCollision(lifetime, DestinationLandblock);
(RuntimeEntityRecord record, RuntimeProjectile projectile) =
CreateProjectileRecord(lifetime, 0x7000400Au, SourceCell);
var updater = new RuntimeProjectilePhysicsUpdater(lifetime.Physics);
Assert.True(updater.TryBegin(
record,
quantum: 0.05f,
record.ObjectClockEpoch,
externalOwnerValid: null,
out RuntimeProjectilePhysicsCommit commit));
var window = new FakeServiceWindow();
window.Allow(DestinationLandblock);
RuntimeRemotePlacementDriveController drive = CreateDrive(lifetime, window);
var destination = new Vector3(12f, 14f, SpawnHeight);
RuntimeAuthoritativePositionRoute route = MakeRoute(
record,
RuntimeAuthoritativePositionDisposition.SetPosition,
DestinationCell,
destination,
operationKind: RuntimeSetPositionOperationKind.ProjectileAuthoritative);
RuntimeRemotePlacementExecutionStatus? status =
drive.ApplyAcceptedProjectilePosition(record, route);
Assert.Equal(RuntimeRemotePlacementExecutionStatus.Committed, status);
Vector3 committedPosition = record.PhysicsBody!.Position;
bool completed = updater.Complete(
commit,
liveCenterX: 0,
liveCenterY: 0,
acknowledgeProjection: static _ => true);
Assert.False(completed);
Assert.Equal(committedPosition, record.PhysicsBody.Position);
DrainPlacementFifo(lifetime);
AssertConverged(lifetime);
}
private static (RuntimeEntityRecord Record, RuntimeProjectile Projectile) CreateProjectileRecord(
RuntimeEntityObjectLifetime lifetime,
uint guid,
uint cellId,
bool registerShadow = true,
// C1 fix (round-3 architecture review): a nonzero setupTableId is
// what makes CanonicalSetupTableId != 0, which is what makes
// TryPrepareAuthoredMover actually consult UnusedCollisionSource
// (RuntimeSetPositionState.cs:1900-1918) instead of taking the
// ResolvedAbsent no-Setup path every other projectile fixture in
// this file relies on. Default null preserves every existing
// caller's behaviour exactly (id 0, ResolvedAbsent, always
// Prepared) — only the two new retry-arm tests pass a real id to
// deliberately provoke RetrySetupUnavailable.
uint? setupTableId = null)
{
RuntimeEntityRecord record = CreateRemoteRecord(
lifetime, guid, setupTableId);
lifetime.Entities.SetFinalPhysicsState(
record,
PhysicsStateFlags.Gravity
| PhysicsStateFlags.Missile
| PhysicsStateFlags.ReportCollisions);
PhysicsBody body = AttachBody(lifetime, record, cellId);
var sphere = new ProjectileCollisionSphere(Vector3.Zero, 0.1f, 1f);
var projectile = (RuntimeProjectile)lifetime.Physics.BindProjectile(
record, body, sphere);
// A4 fix (review round): a shadow registration is the prerequisite
// for ShadowObjectRegistry.UpdatePosition to do anything at all
// (it early-returns "not registered" otherwise) — without this, a
// test could assert the shadow-sync branch ran while
// SyncProjectilePresentation's shadow write was silently a no-op.
if (registerShadow)
{
lifetime.Physics.Engine.ShadowObjects.Register(
record.Key!.Value.LocalEntityId,
gfxObjId: 0u,
body.Position,
body.Orientation,
radius: 0.1f,
worldOffsetX: 0f,
worldOffsetY: 0f,
cellId & 0xFFFF0000u,
ShadowCollisionType.Sphere,
state: (uint)record.FinalPhysicsState,
seedCellId: cellId,
isStatic: false);
}
return (record, projectile);
}
/// <summary>
/// Seeds one collision-table owner row on <paramref name="owner"/> via
/// a peer entity's dynamic shadow + one reported collision — the same
/// mechanism <c>RuntimeCollisionReportingStateTests</c> uses, reduced to
/// the minimum this file's proof obligation P5 needs.
/// </summary>
private static void SeedCollisionOwner(
RuntimeEntityObjectLifetime lifetime,
RuntimeEntityRecord owner,
uint peerGuid,
uint cellId)
{
RuntimeEntityRecord peer = CreateRemoteRecord(lifetime, peerGuid);
AttachBody(lifetime, peer, cellId);
uint peerLocalId = peer.Key!.Value.LocalEntityId;
lifetime.Physics.Engine.ShadowObjects.Register(
peerLocalId,
gfxObjId: 0u,
peer.PhysicsBody!.Position,
Quaternion.Identity,
radius: 0.4f,
worldOffsetX: 0f,
worldOffsetY: 0f,
cellId & 0xFFFF0000u,
ShadowCollisionType.Sphere,
state: (uint)peer.FinalPhysicsState,
seedCellId: cellId,
isStatic: false);
var report = new PhysicsSetPositionCollisionReport(
ContactPlaneValid: false,
ContactPlane: default,
ContactPlaneCellId: 0u,
ContactPlaneIsWater: false,
LastKnownContactPlaneValid: false,
LastKnownContactPlane: default,
LastKnownContactPlaneCellId: 0u,
LastKnownContactPlaneIsWater: false,
SlidingNormalValid: false,
SlidingNormal: default,
CollisionNormalValid: false,
CollisionNormal: default,
CollidedWithEnvironment: false,
FramesStationaryFall: 0,
AdjustOffset: default,
LastCollidedObjectId: peerLocalId,
CollidedObjectIds: System.Collections.Immutable.ImmutableArray
.Create(peerLocalId));
Assert.True(lifetime.Physics.HandleSetPositionCollisions(
owner,
owner.PositionAuthorityVersion,
owner.SpatialAuthorityVersion,
owner.VelocityAuthorityVersion,
physicsTime: 1d,
previousContact: false,
previousOnWalkable: false,
report));
}
// ── Fixture ──────────────────────────────────────────────────────────
/// <summary>