acdream/tests/AcDream.Runtime.Tests/Gameplay/RuntimeLocalPlayerPhysicsPublicationStateTests.cs
Erik 6dc7ba51ee feat(physics): C4 route 4b-3 — remote teleport + cell-less through the canonical placement
Flips the last remote classification (SetPosition: teleport-advanced and
cell-less) onto 4b-1's RuntimeRemotePlacementDriveController, runs retail's
teleport_hook before the placement, and deletes the legacy remote-teleport
machinery. Contract: docs/research/2026-08-04-c4-route-4b-3-contract.md.

Retail: MoveOrTeleport @0x00516330's branch @0x00516386 -> teleport_hook
@0x005163EF -> SetFlags(0x1012) @0x00516414 -> SetPosition @0x00516420 ->
return 1 @0x00516438. The hook @0x00514ED0 runs BEFORE the placement and
regardless of its outcome. Retail places this branch unconditionally, at any
distance and any contact state (arg4 is read only @0x0051638E, after the
branch) — which is what retires AP-137's cell-less enqueue-vs-place delta.

D1 — the classifier's cell-less input is now the PRE-merge committed cell.
Retail's predicate is `this_1->cell == 0`, the BODY's own cell at
MoveOrTeleport entry (this_1 is assigned from this @0x00516334). acdream fed
the POST-merge canonical.FullCellId, which RefreshSnapshot ->
RefreshDerivedState -> SetFullCell has already stamped with the accepted wire
cell; a zero wire cell fails validation into RejectedData first. The shipped
remote cell-less predicate was therefore dead code, not merely different from
remotePlacementRequired. Threaded via a builder overload; route 1's overload
is untouched. The graphical !IsSpatiallyVisible arm of
projectionRequiresTeleportHook is deleted — a presentation predicate with no
retail analogue that fired the teleport machinery on a routine hot path.

Deleted: RemoteTeleportController (605), RemoteTeleportPlacement (85),
RemoteShadowPlacementSynchronizer (49), their 1,709 lines of tests, the
remotePlacementRequired predicate, the TeleportHookRequired plumbing, the
legacy pre-operation ConstrainTo fallback, and the player arm's legacy
!IsGrounded fallback. Net -2,030 lines.

Structural fix (two independent Opus reviews, round 1 FAIL/FAIL): three of the
four MAJORs were one defect — OnPosition carried two parallel inline copies of
the routing tail (player-guid, NPC-guid) that had drifted. Extracted
RunRemoteArmTail (3 call sites) and ApplyWireAirborneLeftoverBookkeeping (2),
both branches now share one implementation.

  A1  ToConstraintArm mapped AirborneSnap -> AirborneNoOperation, so the NPC
      arm armed ConstrainTo ZERO times for an out-of-contact wire-grounded
      creature — a regression this slice introduced while closing a
      structurally identical hole. Now maps to NearInterpolate; switch made
      total with a throwing default proven unreachable.
  R1  D2's write-nothing shape existed on the player arm only; NPC packets
      fell through and wrote the body. Retail makes no player/NPC distinction.
  R2  report_collision_end(this,1) @0x00514F31 was bound to
      ShadowObjects.Suspend, a port of a DIFFERENT retail function
      (remove_shadows_from_cells) that teleport_hook never calls. Now routes
      to RuntimeCollisionReportingState.LeaveWorld, which wraps the private
      ForceEnd in an admission-blocking transaction so a DoCollisionEnd
      callback cannot recreate the contact table.
  R3/A2 A teleported NPC synthesized ServerVelocity from the teleport distance
      (~1,000+ m/s) and planned a run cycle from it. Both the install and
      RemoteServerControlledVelocityCycle.Apply now gate on !isTeleportRoute.

BISECT HAZARD — A1's fix is correct only BECAUSE R1 landed. AirborneSnap is
reachable wire-airborne on the NPC arm only while D2's shape is missing there.
Reverting R1 alone silently inverts A1 into the opposite divergence: arming
where retail returns 0. Revert both or neither.

Also in the velocity hunk: the NPC block's two !IsPlayerGuid(update.Guid)
guards were dropped when it was wrapped in `if (!isTeleportRoute)`. Safe — all
five exit paths of the enclosing IsPlayerGuid block return, so the predicate is
unconditionally false below it — but it was unremarked by both reviews.

Register: AP-137 REWRITTEN (not deleted) to the surviving acdream-only
divergences — null classification during the login window and Rejected*
through UnroutedCatchUp keep a row. AD-42's RemoteTeleportController citation
retired; AP-136/AP-138 writer lists corrected to the two surviving non-Position
rebucket writers; AP-138 gains the teleport arm as a second producer of the
visible-without-collision residual (retirement path remains #309). AP-135 is
untouched and its two airborne bookkeeping writes are preserved on both arms.
AP-131 does not retire; #276 does not close.

Proof obligation 1: ParkCollisionResidents' overlap throw stays unreachable —
the teleport arm adds packets to the same TryBeginExclusiveAuthoredPlacement
one-operation-per-key machinery the far arm uses, opens no new operation shape,
and every DeferredCell outcome cancels synchronously with
restoreCancelledPark: true. The guarded property remains
HasOldPrefixPlacementDebt's stall, not a throw (4b-1's B2 caveat stands).

Correction to an earlier claim: LiveEntityPresentationController's
_activePlacementOwners was NOT write-never at HEAD —
remotePlacementRequired -> BeginPlacement -> Begin -> BeginAuthoritativePlacement
was a live writer chain. It becomes write-never BECAUSE this slice deletes that
chain, which is why deleting the dead half is behaviour-preserving.

Probe: ACDREAM_PROBE_REMOTE_TELEPORT=1 emits one [remote-teleport] line per
routed arm (guid, cause, hook-ran, placement status). TEMPORARY, strip with the
probe family.

Carried, disclosed not fixed: no dedicated bidirectional collision-partner test
for R2 (the wiring, not LeaveWorld itself, is what lacks coverage); the
stress test's teleport step drives hand-written field assignments rather than
the canonical arm; the per-packet runTeleportHook closure allocation (network
path, not the resolve path Slice I's 0 B discipline governs — file before
route 5 adds a fourth call site). B2: IRuntimeCollisionReportObserver has zero
production implementations, so retail's bidirectional DoCollisionEnd half still
reaches no gameplay consumer — this fix closes the wrong-function binding, not
that nobody listens.

Complete Release suite MEASURED at 11,013 passed / 4 skipped / 0 failed
(baseline 11,027/4/0; net -14 = ~33 deleted test cases against ~19 added).
Neither known flake fired (#302 PortalProjectionTests GC-allocation, #308
NakEmissionTests wall-clock).

STILL OWED: the two-client connected gate, which MUST use an NPC/creature
teleport target. Both round-1 MAJORs lived on the NPC arm and the velocity
cycle early-returns for 0x50xxxxxx guids, so a player target structurally
cannot observe A1, A2, or R3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:00:10 +02:00

3030 lines
130 KiB
C#

using System.Collections.Immutable;
using System.Numerics;
using AcDream.Core.Items;
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.Gameplay;
using AcDream.Runtime.Physics;
namespace AcDream.Runtime.Tests.Gameplay;
public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
{
private const uint Cell = 0xA9B40021u;
private const uint SetupId = 0x02000001u;
[Fact]
public void PreparationOwnsPrivateBodyAndMutatesNoCanonicalOrSharedState()
{
using var fixture = new Fixture();
uint fullCell = fixture.Record.FullCellId;
ulong spatial = fixture.Record.SpatialAuthorityVersion;
ulong physicsEpoch = fixture.Record.PhysicsOwnershipEpoch;
ulong clockEpoch = fixture.Record.ObjectClockEpoch;
RuntimePhysicsOwnershipSnapshot physics = fixture.Lifetime.Physics
.CaptureOwnership();
RuntimeLocalPlayerPhysicsPublicationToken token = fixture.Prepare();
Assert.Null(fixture.Record.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
Assert.Equal(fullCell, fixture.Record.FullCellId);
Assert.Equal(spatial, fixture.Record.SpatialAuthorityVersion);
Assert.Equal(physicsEpoch, fixture.Record.PhysicsOwnershipEpoch);
Assert.Equal(clockEpoch, fixture.Record.ObjectClockEpoch);
Assert.Null(fixture.Record.PhysicsHost);
Assert.Equal(physics.SpatialRootCount,
fixture.Lifetime.Physics.CaptureOwnership().SpatialRootCount);
Assert.Equal(physics.RetainedShadowRegistrationCount,
fixture.Lifetime.Physics.CaptureOwnership()
.RetainedShadowRegistrationCount);
Assert.Equal(1, fixture.Owner.CaptureOwnership().CandidateCount);
Assert.True(token.IsValid);
}
[Fact]
public void ActivationIdExhaustionMutatesNoCandidateOrCanonicalOwner()
{
using var fixture = new Fixture();
typeof(RuntimeLocalPlayerPhysicsPublicationState)
.GetField(
"_nextActivationId",
System.Reflection.BindingFlags.Instance
| System.Reflection.BindingFlags.NonPublic)!
.SetValue(fixture.Owner, ulong.MaxValue);
Assert.Throws<OverflowException>(() => fixture.Prepare());
Assert.Null(fixture.Record.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
Assert.Equal(0, fixture.Owner.CaptureOwnership().PendingActivationCount);
Assert.True(fixture.Lifetime.Physics.SetPosition
.IsExactPreparedPlacementCurrent(
fixture.Record,
fixture.Placement,
fixture.Command));
}
[Fact]
public void DormantOwnershipPreflightFailurePublishesNoCanonicalOwner()
{
using var fixture = new Fixture();
RuntimeLocalPlayerPhysicsPublicationToken publication =
fixture.Prepare();
Assert.True(fixture.Lifetime.Physics.SetPosition.Cancel(
fixture.Record,
publishWithdrawal: false));
var unownedBody = new PhysicsBody();
Assert.Throws<InvalidOperationException>(() => fixture.Lifetime.Physics
.SetPosition.PrepareDormantLocalActivationOwnership(
fixture.Record,
unownedBody,
publication.Placement));
Assert.Null(fixture.Record.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
Assert.False(unownedBody.InWorld);
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
Assert.Equal(1, fixture.Owner.CaptureOwnership().CandidateCount);
Assert.Equal(0, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
}
[Fact]
public void CommitOwnsExactDormantBodyAndControllerWithoutWorldEdges()
{
using var fixture = new Fixture();
RuntimeLocalPlayerPhysicsPublicationToken token = fixture.Prepare();
Assert.True(fixture.Owner.TryCaptureCandidateSnapshot(
token,
out RuntimeLocalPlayerPhysicsCandidateSnapshot prepared));
uint fullCell = fixture.Record.FullCellId;
ulong spatial = fixture.Record.SpatialAuthorityVersion;
ulong bodyEpoch = fixture.Record.PhysicsOwnershipEpoch;
ulong controllerEpoch = fixture.Movement.ControllerOwnershipEpoch;
double clockPending = fixture.Record.ObjectClock.PendingSeconds;
bool clockActive = fixture.Record.ObjectClock.IsActive;
object? currentCell = fixture.Lifetime.Physics.DataCache.CellGraph.CurrCell;
RuntimeLocalPlayerPhysicsPublicationStatus status = fixture.Owner
.Commit(token);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed, status);
PlayerMovementController candidate = Assert.IsType<PlayerMovementController>(
fixture.Movement.Controller);
PhysicsBody body = Assert.IsType<PhysicsBody>(fixture.Record.PhysicsBody);
Assert.Same(body, fixture.Record.PhysicsBody);
Assert.Same(candidate, fixture.Movement.Controller);
Assert.True(candidate.OwnsPhysicsBody(body));
Assert.Equal(bodyEpoch + 1UL, fixture.Record.PhysicsOwnershipEpoch);
Assert.Equal(controllerEpoch + 1UL,
fixture.Movement.ControllerOwnershipEpoch);
Assert.Equal(fullCell, fixture.Record.FullCellId);
Assert.Equal(spatial, fixture.Record.SpatialAuthorityVersion);
Assert.Same(currentCell,
fixture.Lifetime.Physics.DataCache.CellGraph.CurrCell);
Assert.Equal(clockPending, fixture.Record.ObjectClock.PendingSeconds);
Assert.Equal(clockActive, fixture.Record.ObjectClock.IsActive);
Assert.Equal(prepared.Position, body.Position);
Assert.Equal(prepared.Orientation, body.Orientation);
Assert.Equal(prepared.CellId, body.CellPosition.ObjCellId);
Assert.Equal(prepared.CellLocalPosition,
body.CellPosition.Frame.Origin);
Assert.Equal(prepared.State, body.State);
Assert.Equal(prepared.TransientState, body.TransientState);
Assert.Equal(prepared.InWorld, body.InWorld);
Assert.False(body.InWorld);
Assert.False((body.TransientState & TransientStateFlags.Active) != 0);
Assert.Null(fixture.Record.PhysicsHost);
Assert.Equal(0, fixture.Lifetime.Physics.CaptureOwnership()
.SpatialRootCount);
Assert.Equal(0, fixture.Lifetime.Physics.CaptureOwnership()
.RetainedShadowRegistrationCount);
Assert.True(fixture.Lifetime.Physics.SetPosition
.IsExactPreparedPlacementCurrent(
fixture.Record,
fixture.Placement,
fixture.Command));
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
AssertNotLive(candidate);
Assert.Equal(prepared.Position, body.Position);
Assert.Equal(prepared.Orientation, body.Orientation);
Assert.Equal(prepared.State, body.State);
Assert.Equal(prepared.TransientState, body.TransientState);
Assert.Equal(clockPending, fixture.Record.ObjectClock.PendingSeconds);
Assert.Equal(clockActive, fixture.Record.ObjectClock.IsActive);
Assert.Same(currentCell,
fixture.Lifetime.Physics.DataCache.CellGraph.CurrCell);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedToken,
fixture.Owner.Commit(token));
}
[Fact]
public void InitialPhysicsVectorsAndCoefficientsSeedCanonicalActivation()
{
Vector3 omega = new(1f, 2f, 3f);
using var fixture = new Fixture(
residentWorld: true,
initialVelocity: new Vector3(100f, 0f, 0f),
initialOmega: omega,
initialFriction: 0.25f,
initialElasticity: 0.08f);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(fixture.Prepare(), out var token));
PhysicsBody body = Assert.IsType<PhysicsBody>(fixture.Record.PhysicsBody);
Assert.Equal(new Vector3(50f, 0f, 0f), body.Velocity);
Assert.Equal(omega, body.Omega);
Assert.Equal(0.25f, body.Friction);
Assert.Equal(0.08f, body.Elasticity);
Assert.False(body.InWorld);
Assert.False(body.TransientState.HasFlag(TransientStateFlags.Active));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var evaluation));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(evaluation, out _));
Assert.Equal(new Vector3(50f, 0f, 0f), body.Velocity);
Assert.Equal(omega, body.Omega);
Assert.Equal(0.25f, body.Friction);
Assert.Equal(0.08f, body.Elasticity);
Assert.True(body.InWorld);
}
/// <summary>
/// C3c-F5: retail seeds the local player's ground contact from the first
/// gravity frame after <c>enter_world</c> (SmartBox::HandleCreateObject
/// 0x00454C80 → init_player 0x00455010 → CPhysicsObj::enter_world
/// 0x00516170; SetPosition records no touch), which the activation
/// compresses through the shared #270 settle sweep. Spawn (1,2,3) with a
/// flat floor at 2.7 — feet 0.3 m above it, inside the 0.5 m settle
/// reach — and the production human bottom-sphere origin (0.475) so the
/// authored placement itself stands clear of the floor.
/// </summary>
[Fact]
public void CommitActivationOnFlatGroundSeedsRetailFirstGravityFrameContact()
{
using var fixture = new Fixture(
residentWorld: true,
terrainHeight: 2.7f,
moverSphereOriginZ: 0.475f);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var evaluation));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(evaluation, out _));
PhysicsBody body = Assert.IsType<PhysicsBody>(fixture.Record.PhysicsBody);
Assert.True(body.InWorld);
Assert.True(body.InContact);
Assert.True(body.OnWalkable);
Assert.True(body.ContactPlaneValid);
Assert.True(body.ContactPlane.Normal.Z > 0.9f);
// The settle snapped the feet onto the floor, exactly like retail's
// first gravity frame.
Assert.InRange(body.Position.Z, 2.65f, 2.76f);
PlayerMovementController controller =
Assert.IsType<PlayerMovementController>(
fixture.Movement.Controller);
// The exact outbound predicate LocalPlayerOutboundController
// serializes as the wire contact bit (InContact && OnWalkable).
Assert.True(controller.CanSendPositionEvent);
Assert.True(controller.CaptureMovementResult(
mouseLookEvent: false).IsOnGround);
}
/// <summary>
/// C3c-F5 counterpart: a spawn with no floor within the settle's reach
/// (flat terrain at 0, feet at 3) must stay genuinely airborne — the
/// seed only ever commits contact the sweep actually found, exactly
/// like retail's fall after enter_world.
/// </summary>
[Fact]
public void CommitActivationOverVoidLeavesFirstEntryGenuinelyAirborne()
{
using var fixture = new Fixture(
residentWorld: true,
moverSphereOriginZ: 0.475f);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var evaluation));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(evaluation, out _));
PhysicsBody body = Assert.IsType<PhysicsBody>(fixture.Record.PhysicsBody);
Assert.True(body.InWorld);
Assert.False(body.InContact);
Assert.False(body.OnWalkable);
Assert.False(body.ContactPlaneValid);
Assert.Equal(3f, body.Position.Z);
PlayerMovementController controller =
Assert.IsType<PlayerMovementController>(
fixture.Movement.Controller);
Assert.False(controller.CanSendPositionEvent);
Assert.False(controller.CaptureMovementResult(
mouseLookEvent: false).IsOnGround);
}
/// <summary>
/// C3c-R1 review R1: the login constraint leash. Retail arms the leash
/// at every accepted-position event (SmartBox::HandleReceivedPosition
/// 0x00453FD0); the flip deleted the App-side login arm
/// (CommitPreparedPosition), so the dormant activation's final commit
/// must arm it — anchored at the committed placement (already
/// floor-snapped by the faithful placement transaction), before the
/// compressed first-gravity-frame settle runs, exactly like retail arms
/// at the received position and only then simulates the first frame.
/// </summary>
[Fact]
public void CommitActivationArmsTheLoginConstraintLeashAtTheCommittedPlacement()
{
using var fixture = new Fixture(
residentWorld: true,
terrainHeight: 2.7f,
moverSphereOriginZ: 0.475f);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var evaluation));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(evaluation, out _));
PlayerMovementController controller =
Assert.IsType<PlayerMovementController>(
fixture.Movement.Controller);
ConstraintManager? constraint =
controller.PositionManager?.Constraint;
Assert.NotNull(constraint);
Assert.True(constraint!.IsConstrained);
// The anchor cell is the placement's committed containing cell.
Assert.Equal(
controller.PhysicsBody.CellPosition.ObjCellId,
constraint.ConstraintPos.ObjCellId);
// Anchored at the committed (floor-snapped) placement pose.
Assert.InRange(
constraint.ConstraintPos.Frame.Origin.Z, 2.65f, 2.76f);
Assert.InRange(
controller.PhysicsBody.Position.Z, 2.65f, 2.76f);
Assert.Equal(
ConstraintDistance.GetStartConstraintDistance(
constraint.ConstraintPos.ObjCellId),
constraint.ConstraintDistanceStart);
Assert.Equal(
ConstraintDistance.GetMaxConstraintDistance(
constraint.ConstraintPos.ObjCellId),
constraint.ConstraintDistanceMax);
}
/// <summary>
/// C3c-R1 review R1: the arm is exactly-once — the successful final
/// commit nulls the activation envelope, so a stale CommitActivation
/// retry is RejectedAuthority and can never re-arm the leash.
/// </summary>
[Fact]
public void CommitActivationNeverRearmsTheLeashOnAStaleRetry()
{
using var fixture = new Fixture(
residentWorld: true,
terrainHeight: 2.7f,
moverSphereOriginZ: 0.475f);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var evaluation));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(evaluation, out _));
PlayerMovementController controller =
Assert.IsType<PlayerMovementController>(
fixture.Movement.Controller);
Assert.True(
controller.PositionManager!.Constraint!.IsConstrained);
controller.PositionManager.UnConstrain();
Assert.Equal(RuntimeDormantSetPositionCommitStatus.RejectedAuthority,
fixture.Owner.CommitActivation(evaluation, out _));
Assert.False(
controller.PositionManager.Constraint.IsConstrained);
}
[Fact]
public void DeferredActivationEvaluationLeavesExactOwnedGraphDormantAndRetryable()
{
using var fixture = new Fixture();
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
PhysicsBody body = fixture.Record.PhysicsBody!;
Vector3 position = body.Position;
Quaternion orientation = body.Orientation;
EvaluationPuritySnapshot before = CaptureEvaluationPurity(fixture);
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell,
fixture.Owner.EvaluateActivation(token, out var receipt));
AssertEvaluationPurity(before, CaptureEvaluationPurity(fixture));
Assert.True(receipt.IsValid);
Assert.True(fixture.Owner.IsEvaluationCurrent(receipt));
Assert.False(body.InWorld);
Assert.False(body.TransientState.HasFlag(TransientStateFlags.Active));
Assert.Equal(position, body.Position);
Assert.Equal(orientation, body.Orientation);
Assert.Equal(Cell, fixture.Record.FullCellId);
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
Assert.Equal(0, fixture.Lifetime.Physics.CaptureOwnership()
.RetainedShadowRegistrationCount);
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
AssertNotLive(fixture.Movement.Controller!);
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell,
fixture.Owner.EvaluateActivation(token, out var retry));
AssertEvaluationPurity(before, CaptureEvaluationPurity(fixture));
Assert.True(retry.EvaluationId > receipt.EvaluationId);
}
[Fact]
public void CommittedEvaluationReceiptRemainsPureAndBindsExactDormantAuthority()
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
PhysicsBody body = fixture.Record.PhysicsBody!;
ulong clockEpoch = fixture.Record.ObjectClockEpoch;
ulong placementCommit = fixture.Record.PlacementCommitVersion;
uint fullCell = fixture.Record.FullCellId;
Vector3 bodyPosition = body.Position;
Quaternion bodyOrientation = body.Orientation;
Assert.Equal(fixture.Record.Key, token.Entity);
Assert.Equal(fixture.Record.PhysicsOwnershipEpoch,
token.PhysicsOwnershipEpoch);
Assert.Equal(fixture.Record.ObjectClockEpoch, token.ObjectClockEpoch);
Assert.Equal(fixture.Movement.ControllerOwnershipEpoch,
token.ControllerOwnershipEpoch);
EvaluationPuritySnapshot before = CaptureEvaluationPurity(fixture);
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var receipt));
AssertEvaluationPurity(before, CaptureEvaluationPurity(fixture));
Assert.True(receipt.Placement.Result.IsCommitted);
Assert.True(fixture.Owner.IsEvaluationCurrent(receipt));
Assert.False(body.InWorld);
Assert.False(body.TransientState.HasFlag(TransientStateFlags.Active));
Assert.Equal(bodyPosition, body.Position);
Assert.Equal(bodyOrientation, body.Orientation);
Assert.Equal(fullCell, fixture.Record.FullCellId);
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
Assert.Equal(clockEpoch, fixture.Record.ObjectClockEpoch);
Assert.Equal(placementCommit, fixture.Record.PlacementCommitVersion);
Assert.Equal(0, fixture.Lifetime.Physics.CaptureOwnership()
.RetainedShadowRegistrationCount);
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
Assert.False(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
out _));
AssertNotLive(fixture.Movement.Controller!);
}
[Fact]
public void CommitActivationPublishesExactLiveRuntimeGraphOnce()
{
using var fixture = new Fixture(residentWorld: true);
bool callbackSawExactGraph = false;
var placements = new PlacementObserver(delta =>
{
if (delta.Placement.Kind is not RuntimePlacementProjectionKind.Place)
return;
callbackSawExactGraph = fixture.Record.PhysicsBody is { InWorld: true }
&& fixture.Record.PhysicsHost is not null
&& fixture.Movement.Controller is { IsRuntimePublished: true }
&& fixture.Movement.Controller.Movement.MoveTo is not null
&& fixture.Record.ObjectClock.IsActive
&& fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record);
});
using IDisposable subscription = fixture.Lifetime.Events
.SubscribePlacement(placements);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var evaluation));
ulong clockEpoch = fixture.Record.ObjectClockEpoch;
RuntimeDormantSetPositionCommitStatus status = fixture.Owner
.CommitActivation(evaluation, out var projection);
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed, status);
Assert.True(projection.IsValid);
PhysicsBody body = Assert.IsType<PhysicsBody>(fixture.Record.PhysicsBody);
Assert.True(body.InWorld);
Assert.True(body.TransientState.HasFlag(TransientStateFlags.Active));
Assert.NotNull(fixture.Record.PhysicsHost);
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
Assert.True(fixture.Record.ObjectClock.IsActive);
Assert.Equal(clockEpoch + 1UL, fixture.Record.ObjectClockEpoch);
Assert.True(fixture.Movement.Controller!.IsRuntimePublished);
RuntimePlacementDelta place = Assert.Single(placements.Deltas);
Assert.Equal(RuntimePlacementProjectionKind.Place,
place.Placement.Kind);
Assert.Equal(projection.Sequence,
place.Placement.Token.Sequence);
Assert.True(callbackSawExactGraph);
Assert.Equal(0, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
Assert.Equal(RuntimeDormantSetPositionCommitStatus.RejectedAuthority,
fixture.Owner.CommitActivation(evaluation, out _));
Assert.Single(placements.Deltas);
}
[Fact]
public void DeferredCommitWaitsThenRearmsSameLeaseAfterExactGenerationWake()
{
using var fixture = new Fixture();
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell,
fixture.Owner.EvaluateActivation(token, out var deferred));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.DeferredCell,
fixture.Owner.CommitActivation(deferred, out var noProjection));
Assert.False(noProjection.IsValid);
Assert.False(fixture.Record.PhysicsBody!.InWorld);
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell,
fixture.Owner.EvaluateActivation(token, out var waiting));
Assert.False(waiting.IsValid);
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
// C3c-F2: the wake is driven through the SAME owner production uses
// (RuntimePhysicsState's collision admission), not through the raw
// SetPosition seam. Driving SetPosition directly leaves the admission
// ledger empty, which is a state production can never be in and which
// hid the login rearm wedge this test is supposed to cover.
CommitProductionCollisionGeneration(fixture, Cell & 0xFFFF0000u);
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var ready));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(ready, out var projection));
Assert.True(projection.IsValid);
Assert.True(fixture.Record.PhysicsBody!.InWorld);
Assert.Equal(0, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
}
[Fact]
public void DeferredCommitRearmsAfterProductionAdmissionCommitsItsGeneration()
{
// C3c-F2 regression (the connected-gate login wedge). The sibling
// test above wakes the parked lease by calling BeginCollisionGeneration
// / CommitCollisionGeneration DIRECTLY on the SetPosition state, which
// leaves RuntimePhysicsState's admission ledger empty. PRODUCTION
// always wakes through BeginCollisionAdmission -> stage ->
// CommitCollisionGeneration, and that path retires the admission the
// instant the generation commits — after which
// ExpectedCollisionGeneration names the NEXT, never-begun generation.
// The rearm must key off the generation the collision world now HOLDS.
using var fixture = new Fixture();
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell,
fixture.Owner.EvaluateActivation(token, out var deferred));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.DeferredCell,
fixture.Owner.CommitActivation(deferred, out var noProjection));
Assert.False(noProjection.IsValid);
Assert.False(fixture.Record.PhysicsBody!.InWorld);
Assert.False(fixture.Movement.Controller!.IsRuntimePublished);
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
RuntimeCollisionAdmission admission = fixture.Lifetime.Physics
.BeginCollisionAdmission(Cell & 0xFFFF0000u);
// While the destination admission is in flight the lease must simply
// keep waiting — never a rejection, never an early rearm.
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell,
fixture.Owner.EvaluateActivation(token, out var waiting));
Assert.False(waiting.IsValid);
using PreparedLandblockCollisionGeneration prepared = fixture
.Lifetime.Physics.PrepareCollisionGeneration(admission);
fixture.Lifetime.Physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(Cell & 0xFFFF0000u));
Assert.True(CommitPrepared(
fixture.Lifetime.Physics,
admission,
prepared).Committed);
// The exact divergence this test pins: the committed authority is the
// generation the lease parked on; "expected" has already moved past it.
Assert.Equal(
admission.Generation,
fixture.Lifetime.Physics.CollisionGenerationAuthority(Cell));
Assert.NotEqual(
admission.Generation,
fixture.Lifetime.Physics.ExpectedCollisionGeneration(Cell));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var ready));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(ready, out var projection));
Assert.True(projection.IsValid);
Assert.True(fixture.Record.PhysicsBody!.InWorld);
Assert.NotNull(fixture.Record.PhysicsHost);
Assert.True(fixture.Movement.Controller!.IsRuntimePublished);
Assert.Equal(0, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
}
[Fact]
public void DeferredCommitStaysParkedWhileTheCommittingAdmissionIsStillRegistered()
{
// C3c-F2, the link the live probe caught: the collision-generation
// commit marks the parked lease ready and only THEN retires its
// admission (RuntimePhysicsState.cs:2503 vs :2552-2558). Production
// reenters the first-entry pump inside that window. Rearming there
// moves the lease out of AwaitingCell and its very next evaluation
// fails the seal on the still-registered admission — after which
// EvaluateActivation can no longer answer DeferredCell and reports
// RejectedAuthority, which is TERMINAL for the conductor. The lease
// must stay parked and retryable instead.
using var fixture = new Fixture();
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell,
fixture.Owner.EvaluateActivation(token, out var deferred));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.DeferredCell,
fixture.Owner.CommitActivation(deferred, out _));
RuntimeCollisionAdmission admission = fixture.Lifetime.Physics
.BeginCollisionAdmission(Cell & 0xFFFF0000u);
// Reconstruct the exact production instant: the landblock is in the
// engine and the parked lease has been marked ready by the SAME call
// RuntimePhysicsState.cs:2503 makes, while the admission that is
// committing has not yet been retired (:2552-2558).
fixture.Lifetime.Physics.Engine.AddLandblock(
Cell & 0xFFFF0000u,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
fixture.Lifetime.Physics.SetPosition.CommitCollisionGeneration(
Cell & 0xFFFF0000u,
admission.Generation,
ready: true);
Assert.Equal(
admission.Generation,
fixture.Lifetime.Physics.CollisionGenerationAuthority(Cell));
Assert.Equal(
admission.Generation,
fixture.Lifetime.Physics.ExpectedCollisionGeneration(Cell));
Assert.False(fixture.Lifetime.Physics
.IsCollisionEvaluationPrefixAdmissible(Cell));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell,
fixture.Owner.EvaluateActivation(token, out var stillWaiting));
Assert.False(stillWaiting.IsValid);
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
Assert.False(fixture.Record.PhysicsBody!.InWorld);
Assert.False(fixture.Movement.Controller!.IsRuntimePublished);
using (PreparedLandblockCollisionGeneration prepared = fixture
.Lifetime.Physics.PrepareCollisionGeneration(admission))
{
fixture.Lifetime.Physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(Cell & 0xFFFF0000u));
Assert.True(CommitPrepared(
fixture.Lifetime.Physics,
admission,
prepared).Committed);
}
Assert.True(fixture.Lifetime.Physics
.IsCollisionEvaluationPrefixAdmissible(Cell));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var ready));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(ready, out var projection));
Assert.True(projection.IsValid);
Assert.True(fixture.Record.PhysicsBody!.InWorld);
Assert.True(fixture.Movement.Controller!.IsRuntimePublished);
Assert.Equal(0, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
}
[Fact]
public void RejectedCommitAppliesResponseOnceAndRetainsRetryableLease()
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
PhysicsBody body = fixture.Record.PhysicsBody!;
Vector3 position = body.Position;
uint fullCell = fixture.Record.FullCellId;
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
static (_, phase, _, observed) => phase
is TransitionCellCollisionPhase.Objects
? TransitionState.Collided
: observed;
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.RejectedPlacement,
fixture.Owner.EvaluateActivation(token, out var rejected));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.RejectedPlacement,
fixture.Owner.CommitActivation(rejected, out var projection));
Assert.True(fixture.Lifetime.Physics.SetPosition
.TryCaptureDormantLocalActivationResult(
token.Placement,
out PhysicsSetPositionResult rejectedResult));
Assert.Equal(PhysicsSetPositionError.NoValidPosition,
rejectedResult.Error);
Assert.False(rejectedResult.CollisionHandlerResult);
Assert.False(projection.IsValid);
Assert.Equal(position, body.Position);
Assert.Equal(fullCell, fixture.Record.FullCellId);
Assert.False(body.InWorld);
Assert.Null(fixture.Record.PhysicsHost);
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
Assert.Equal(RuntimeDormantSetPositionCommitStatus.RejectedAuthority,
fixture.Owner.CommitActivation(rejected, out _));
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook = null;
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var retry));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(retry, out projection));
Assert.True(projection.IsValid);
}
[Fact]
public void UndefinedShadowDispositionIsRejectedBeforeCandidateConstruction()
{
using var fixture = new Fixture(residentWorld: true);
var invalid = new RuntimeLocalPlayerPhysicsActivationPreparation(
0.48f,
1.835f,
(RuntimeLocalPlayerShadowDisposition)0x7F);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Prepare(
fixture.Record,
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
invalid,
out var token));
Assert.False(token.IsValid);
Assert.Null(fixture.Record.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
}
[Fact]
public void RejectedCommitMapsEligibleEnvironmentCallbackToCollided()
{
using var fixture = new Fixture(residentWorld: true);
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
static (transition, _, _, _) =>
{
transition.CollisionInfo.CollidedWithEnvironment = true;
return TransitionState.Collided;
};
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.RejectedPlacement,
fixture.Owner.EvaluateActivation(token, out var rejected));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.RejectedPlacement,
fixture.Owner.CommitActivation(rejected, out _));
Assert.True(fixture.Lifetime.Physics.SetPosition
.TryCaptureDormantLocalActivationResult(
token.Placement,
out PhysicsSetPositionResult result));
Assert.Equal(PhysicsSetPositionError.Collided, result.Error);
Assert.True(result.CollisionHandlerResult);
Assert.Equal(1, fixture.Lifetime.Physics.CollisionReports
.CaptureOwnership().OwnerCount);
Assert.True(fixture.Owner.DiscardActivation(token));
Assert.Null(fixture.Record.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
Assert.Equal(0, fixture.Lifetime.Physics.SetPosition
.CaptureOwnership().ActiveOperationCount);
RuntimeCollisionReportingOwnershipSnapshot ownership = fixture.Lifetime
.Physics.CollisionReports.CaptureOwnership();
Assert.Equal(0, ownership.OwnerCount);
Assert.Equal(0, ownership.TrackedObjectCount);
Assert.Equal(0, ownership.ReversePeerCount);
Assert.Equal(0, ownership.PendingSetPositionDispatchCount);
}
[Fact]
public void RegisteredAuthoredShadowActivatesAndMovesExactRows()
{
using var fixture = new Fixture(
residentWorld: true,
shadowDisposition:
RuntimeLocalPlayerShadowDisposition.RegisteredAuthoredPayload);
uint localId = fixture.Record.Key!.Value.LocalEntityId;
fixture.Lifetime.Physics.Engine.ShadowObjects.Register(
localId,
SetupId,
fixture.Command.Physics.Position + Vector3.UnitX,
Quaternion.Identity,
0.48f,
0f,
0f,
Cell & 0xFFFF0000u,
ShadowCollisionType.Sphere,
state: (uint)fixture.Record.FinalPhysicsState,
seedCellId: Cell,
isStatic: false);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var evaluation));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(evaluation, out _));
ShadowEntry row = Assert.Single(fixture.Lifetime.Physics.Engine
.ShadowObjects.AllEntriesForDebug(),
entry => entry.EntityId == localId);
Assert.Equal(fixture.Record.PhysicsBody!.Position, row.Position);
}
[Fact]
public void DeferredAuthoredActivationSuspendsRowsAndExactWakeRestoresThem()
{
using var fixture = new Fixture(
shadowDisposition:
RuntimeLocalPlayerShadowDisposition.RegisteredAuthoredPayload);
uint localId = fixture.Record.Key!.Value.LocalEntityId;
fixture.Lifetime.Physics.Engine.ShadowObjects.Register(
localId, SetupId, fixture.Command.Physics.Position,
Quaternion.Identity, 0.48f, 0f, 0f,
Cell & 0xFFFF0000u, ShadowCollisionType.Sphere,
state: (uint)fixture.Record.FinalPhysicsState,
seedCellId: Cell, isStatic: false);
Assert.Contains(fixture.Lifetime.Physics.Engine.ShadowObjects
.AllEntriesForDebug(), row => row.EntityId == localId);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(fixture.Prepare(), out var token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell,
fixture.Owner.EvaluateActivation(token, out var deferred));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.DeferredCell,
fixture.Owner.CommitActivation(deferred, out _));
Assert.False(fixture.Record.PhysicsBody!.InWorld);
Assert.DoesNotContain(fixture.Lifetime.Physics.Engine.ShadowObjects
.AllEntriesForDebug(), row => row.EntityId == localId);
Assert.Equal(1, fixture.Lifetime.Physics.Engine.ShadowObjects
.SuspendedRegistrationCount);
Assert.Equal(0, fixture.Lifetime.Physics.Engine.ShadowObjects
.PendingSetPositionDispatchCount);
// C3c-F2: production wake path (collision admission), see
// CommitProductionCollisionGeneration.
CommitProductionCollisionGeneration(fixture, Cell & 0xFFFF0000u);
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var ready));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(ready, out _));
Assert.Contains(fixture.Lifetime.Physics.Engine.ShadowObjects
.AllEntriesForDebug(), row => row.EntityId == localId);
}
[Fact]
public void DeferredDiscardTransfersSuspendedShadowToEntityForLaterReuse()
{
using var fixture = new Fixture(
shadowDisposition:
RuntimeLocalPlayerShadowDisposition.RegisteredAuthoredPayload);
uint localId = fixture.Record.Key!.Value.LocalEntityId;
ShadowObjectRegistry shadows = fixture.Lifetime.Physics.Engine
.ShadowObjects;
shadows.Register(
localId, SetupId, fixture.Command.Physics.Position,
Quaternion.Identity, 0.48f, 0f, 0f,
Cell & 0xFFFF0000u, ShadowCollisionType.Sphere,
state: (uint)fixture.Record.FinalPhysicsState,
seedCellId: Cell, isStatic: false);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(fixture.Prepare(), out var token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell,
fixture.Owner.EvaluateActivation(token, out var deferred));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.DeferredCell,
fixture.Owner.CommitActivation(deferred, out _));
Assert.True(fixture.Owner.DiscardActivation(token));
Assert.Null(fixture.Record.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
Assert.Equal(0, fixture.Lifetime.Physics.SetPosition
.CaptureOwnership().ActiveOperationCount);
Assert.Equal(1, shadows.SuspendedRegistrationCount);
Assert.Equal(0, shadows.PendingSetPositionDispatchCount);
Assert.DoesNotContain(shadows.AllEntriesForDebug(),
row => row.EntityId == localId);
const ulong generation = 1UL;
fixture.Lifetime.Physics.SetPosition.BeginCollisionGeneration(
Cell & 0xFFFF0000u, generation);
fixture.Lifetime.Physics.Engine.AddLandblock(
Cell & 0xFFFF0000u,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty<CellSurface>(), Array.Empty<PortalPlane>(), 0f, 0f);
fixture.Lifetime.Physics.SetPosition.CommitCollisionGeneration(
Cell & 0xFFFF0000u, generation, ready: true);
fixture.RepreparePlacement();
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(fixture.Prepare(), out var retry));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(retry, out var ready));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(ready, out _));
Assert.Equal(0, shadows.SuspendedRegistrationCount);
Assert.Contains(shadows.AllEntriesForDebug(),
row => row.EntityId == localId);
}
[Fact]
public void CollisionCallbackDeleteRetiresPrephaseWithoutShadowOrPlace()
{
using var fixture = new Fixture(residentWorld: true);
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
static (transition, phase, _, observed) =>
{
if (phase is TransitionCellCollisionPhase.Environment)
transition.CollisionInfo.CollidedWithEnvironment = true;
return observed;
};
var placements = new PlacementObserver();
using IDisposable placementSubscription = fixture.Lifetime.Events
.SubscribePlacement(placements);
bool deleted = false;
var collisions = new PublicationCollisionObserver(ignoredReport =>
{
if (deleted)
return;
deleted = true;
Assert.True(fixture.Lifetime.TryAcceptDelete(
new DeleteObject.Parsed(
fixture.Record.ServerGuid,
fixture.Record.Incarnation),
isLocalPlayer: false,
removeRetainedObject: false,
out RuntimeEntityDeleteAcceptance acceptance));
fixture.Lifetime.CompleteAcceptedDelete(acceptance);
Assert.Null(fixture.Lifetime.RetireCanonicalOnly(fixture.Record));
});
using IDisposable collisionSubscription = fixture.Lifetime.Physics
.CollisionReports.Subscribe(collisions);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(fixture.Prepare(), out var token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var evaluation));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.RejectedAuthority,
fixture.Owner.CommitActivation(evaluation, out var projection));
Assert.True(deleted);
Assert.False(projection.IsValid);
Assert.DoesNotContain(placements.Deltas,
delta => delta.Placement.Kind is RuntimePlacementProjectionKind.Place);
Assert.Equal(0, fixture.Owner.CaptureOwnership().PendingActivationCount);
Assert.Null(fixture.Movement.Controller);
Assert.Equal(0, fixture.Lifetime.Physics.CollisionReports
.CaptureOwnership().PendingSetPositionDispatchCount);
Assert.Equal(0, fixture.Lifetime.Physics.Engine.ShadowObjects
.PendingSetPositionDispatchCount);
}
[Fact]
public void CollisionCallbackVectorRefreshesDormantBodyWithRetailClamp()
{
using var fixture = new Fixture(residentWorld: true);
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
static (transition, phase, _, observed) =>
{
if (phase is TransitionCellCollisionPhase.Environment)
transition.CollisionInfo.CollidedWithEnvironment = true;
return observed;
};
bool updated = false;
Vector3 omega = new(1f, 2f, 3f);
var collisions = new PublicationCollisionObserver(ignoredReport =>
{
if (updated)
return;
updated = true;
Assert.True(fixture.Lifetime.TryApplyVector(
new VectorUpdate.Parsed(
fixture.Record.ServerGuid,
new Vector3(100f, 0f, 0f),
omega,
InstanceSequence: fixture.Record.Incarnation,
VectorSequence: 2),
acknowledgeProjection: null,
out _));
_ = ignoredReport;
});
using IDisposable subscription = fixture.Lifetime.Physics
.CollisionReports.Subscribe(collisions);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(fixture.Prepare(), out var token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var evaluation));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(evaluation, out _));
Assert.True(updated);
Assert.Equal(new Vector3(50f, 0f, 0f),
fixture.Record.PhysicsBody!.Velocity);
Assert.Equal(omega, fixture.Record.PhysicsBody.Omega);
}
[Fact]
public void CollisionCallbackAuthorityChangeRetiresInstalledTracking()
{
using var fixture = new Fixture(residentWorld: true);
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
static (transition, phase, _, observed) =>
{
if (phase is TransitionCellCollisionPhase.Environment)
transition.CollisionInfo.CollidedWithEnvironment = true;
return observed;
};
bool invalidated = false;
var collisions = new PublicationCollisionObserver(_ =>
{
if (invalidated)
return;
invalidated = true;
fixture.Lifetime.Entities.AdvanceObjDescAuthority(fixture.Record);
});
using IDisposable subscription = fixture.Lifetime.Physics
.CollisionReports.Subscribe(collisions);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(fixture.Prepare(), out var token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var evaluation));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.RejectedAuthority,
fixture.Owner.CommitActivation(evaluation, out _));
RuntimeCollisionReportingOwnershipSnapshot ownership = fixture.Lifetime
.Physics.CollisionReports.CaptureOwnership();
Assert.True(invalidated);
Assert.Equal(0, ownership.OwnerCount);
Assert.Equal(0, ownership.TrackedObjectCount);
Assert.Equal(0, ownership.ReversePeerCount);
Assert.Equal(0, ownership.PendingSetPositionDispatchCount);
}
[Fact]
public void CollisionCallbackNewerPositionSuppressesReciprocalAndPreservesNewLease()
{
using var fixture = new Fixture(residentWorld: true);
RuntimeEntityRecord target = fixture.Lifetime.RegisterEntity(
Spawn(0x70003002u, incarnation: 1)).Canonical!;
fixture.Lifetime.Entities.SetFullCell(
target, Cell, Cell & 0xFFFF0000u);
fixture.Lifetime.Entities.SetFinalPhysicsState(
target, PhysicsStateFlags.ReportCollisions);
var targetBody = new PhysicsBody
{
Position = new Vector3(1f, 2f, 3f),
Orientation = Quaternion.Identity,
State = PhysicsStateFlags.ReportCollisions,
InWorld = true,
};
targetBody.SnapToCell(Cell, targetBody.Position, targetBody.Position);
fixture.Lifetime.Entities.SetPhysicsBody(target, targetBody);
fixture.Lifetime.Physics.AcknowledgeSpatialProjection(
target, spatial: true);
fixture.Lifetime.Physics.Engine.ShadowObjects.Register(
target.Key!.Value.LocalEntityId,
SetupId,
targetBody.Position,
Quaternion.Identity,
0.48f,
0f,
0f,
Cell & 0xFFFF0000u,
ShadowCollisionType.Sphere,
(uint)targetBody.State,
Cell,
isStatic: false);
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
(transition, phase, _, observed) =>
{
if (phase is TransitionCellCollisionPhase.Environment)
{
transition.CollisionInfo.CollideObjectGuids.Add(
target.Key.Value.LocalEntityId);
}
return observed;
};
bool updated = false;
RuntimeEntityPlacementToken newer = default;
var reports = new List<RuntimeCollisionReport>();
var collisions = new PublicationCollisionObserver(report =>
{
reports.Add(report);
if (updated || report.Kind is not RuntimeCollisionReportKind.ObjectCollision)
return;
updated = true;
Assert.True(fixture.Lifetime.TryApplyPosition(
new WorldSession.EntityPositionUpdate(
fixture.Record.ServerGuid,
new CreateObject.ServerPosition(
Cell, 4f, 5f, 3f, 1f, 0f, 0f, 0f),
Velocity: null,
PlacementId: null,
IsGrounded: true,
InstanceSequence: fixture.Record.Incarnation,
PositionSequence: 2,
TeleportSequence: 0,
ForcePositionSequence: 0),
isLocalPlayer: true,
forcePositionRotation: null,
currentLocalVelocity: null,
acknowledgeProjection: null,
out _,
out _,
out _));
newer = fixture.Lifetime.Physics.SetPosition.BeginAuthoredPlacement(
fixture.Record,
fixture.Record.PositionAuthorityVersion,
RuntimeSetPositionOperationKind.LocalAuthoritative);
Assert.True(newer.IsValid);
});
using IDisposable subscription = fixture.Lifetime.Physics
.CollisionReports.Subscribe(collisions);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(fixture.Prepare(), out var token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.RejectedPlacement,
fixture.Owner.EvaluateActivation(token, out var evaluation));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.RejectedAuthority,
fixture.Owner.CommitActivation(evaluation, out _));
Assert.True(updated);
Assert.DoesNotContain(reports, report =>
report.Kind is RuntimeCollisionReportKind.ObjectCollision
&& report.Recipient == target.Key);
Assert.Equal(1, fixture.Lifetime.Physics.SetPosition
.CaptureOwnership().ActiveOperationCount);
RuntimeCollisionReportingOwnershipSnapshot ownership = fixture.Lifetime
.Physics.CollisionReports.CaptureOwnership();
Assert.Equal(0, ownership.OwnerCount);
Assert.Equal(0, ownership.TrackedObjectCount);
Assert.Equal(0, ownership.ReversePeerCount);
}
[Fact]
public void HitGroundReapplyStaysDormantUntilFinalActivationTail()
{
using var fixture = new Fixture(residentWorld: true);
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
static (transition, phase, _, observed) =>
{
if (phase is TransitionCellCollisionPhase.Environment)
{
transition.CollisionInfo.SetContactPlane(
new Plane(Vector3.UnitZ, -3f), Cell, isWater: false);
}
return observed;
};
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(fixture.Prepare(), out var token));
PlayerMovementController controller = fixture.Movement.Controller!;
bool observedDormant = false;
controller.BeginDormantSetPositionGroundPhase();
controller.Motion.InterpretedState.ForwardCommand =
MotionCommand.RunForward;
controller.Motion.InterpretedState.ForwardSpeed = 1f;
controller.Motion.DefaultSink = new CallbackMotionSink(() =>
{
observedDormant = true;
Assert.False(fixture.Record.PhysicsBody!.InWorld);
Assert.False(fixture.Record.PhysicsBody.TransientState
.HasFlag(TransientStateFlags.Active));
Assert.Null(fixture.Record.PhysicsHost);
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
Assert.False(fixture.Record.ObjectClock.IsActive);
});
controller.EndDormantSetPositionGroundPhase();
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var evaluation));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(evaluation, out var projection));
Assert.True(observedDormant);
Assert.True(projection.IsValid);
Assert.True(fixture.Record.PhysicsBody!.InWorld);
Assert.NotNull(fixture.Record.PhysicsHost);
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
Assert.True(fixture.Record.ObjectClock.IsActive);
Assert.True(controller.IsRuntimePublished);
}
[Fact]
public void HitGroundInvalidationRetiresInstalledBatchBeforeDispatch()
{
using var fixture = new Fixture(residentWorld: true);
RuntimeEntityRecord target = fixture.Lifetime.RegisterEntity(
Spawn(0x70003003u, incarnation: 1)).Canonical!;
fixture.Lifetime.Entities.SetFullCell(
target, Cell, Cell & 0xFFFF0000u);
fixture.Lifetime.Entities.SetFinalPhysicsState(
target, PhysicsStateFlags.ReportCollisions);
var targetBody = new PhysicsBody
{
Position = new Vector3(100f, 100f, 3f),
Orientation = Quaternion.Identity,
State = PhysicsStateFlags.ReportCollisions,
InWorld = true,
TransientState = TransientStateFlags.Active,
};
targetBody.SnapToCell(Cell, targetBody.Position, targetBody.Position);
fixture.Lifetime.Entities.SetPhysicsBody(target, targetBody);
fixture.Lifetime.Physics.AcknowledgeSpatialProjection(target, true);
fixture.Lifetime.Physics.Engine.ShadowObjects.Register(
target.Key!.Value.LocalEntityId, SetupId, targetBody.Position,
Quaternion.Identity, 0.48f, 0f, 0f, Cell & 0xFFFF0000u,
ShadowCollisionType.Sphere, (uint)targetBody.State, Cell,
isStatic: false);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(fixture.Prepare(), out var token));
PhysicsBody ownerBody = fixture.Record.PhysicsBody!;
ownerBody.InWorld = true;
ownerBody.TransientState |= TransientStateFlags.Active;
Assert.True(fixture.Lifetime.Physics.CollisionReports.HandleReports(
fixture.Record, ownerBody, 9d, false, false, false,
[target.Key.Value.LocalEntityId]));
ownerBody.InWorld = false;
ownerBody.TransientState &= ~TransientStateFlags.Active;
Assert.Equal(1, fixture.Lifetime.Physics.CollisionReports
.CaptureOwnership().TrackedObjectCount);
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
static (transition, phase, _, observed) =>
{
if (phase is TransitionCellCollisionPhase.Environment)
{
transition.CollisionInfo.SetContactPlane(
new Plane(Vector3.UnitZ, -3f), Cell, isWater: false);
}
return observed;
};
PlayerMovementController controller = fixture.Movement.Controller!;
bool invalidated = false;
controller.BeginDormantSetPositionGroundPhase();
controller.Motion.InterpretedState.ForwardCommand =
MotionCommand.RunForward;
controller.Motion.DefaultSink = new CallbackMotionSink(() =>
{
if (invalidated)
return;
invalidated = true;
fixture.Lifetime.Entities.AdvanceObjDescAuthority(fixture.Record);
});
controller.EndDormantSetPositionGroundPhase();
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var evaluation));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.RejectedAuthority,
fixture.Owner.CommitActivation(evaluation, out _));
Assert.True(invalidated);
RuntimeCollisionReportingOwnershipSnapshot ownership = fixture.Lifetime
.Physics.CollisionReports.CaptureOwnership();
Assert.Equal(0, ownership.OwnerCount);
Assert.Equal(0, ownership.TrackedObjectCount);
Assert.Equal(0, ownership.ReversePeerCount);
Assert.Equal(0, ownership.PendingSetPositionDispatchCount);
Assert.Equal(0, fixture.Lifetime.Physics.Engine.ShadowObjects
.PendingSetPositionDispatchCount);
Assert.Equal(0, fixture.Lifetime.Physics.SetPosition
.CaptureOwnership().ActiveOperationCount);
}
[Fact]
public void HiddenNoDrawStateDoesNotCancelDormantActivation()
{
using var fixture = new Fixture(residentWorld: true);
PhysicsStateFlags hidden = fixture.Record.FinalPhysicsState
| PhysicsStateFlags.Hidden
| PhysicsStateFlags.NoDraw;
fixture.Lifetime.Entities.SetFinalPhysicsState(fixture.Record, hidden);
fixture.RepreparePlacement();
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var evaluation));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(evaluation, out var projection));
Assert.True(projection.IsValid);
Assert.True(fixture.Record.PhysicsBody!.InWorld);
Assert.True(fixture.Record.PhysicsBody.State.HasFlag(
PhysicsStateFlags.Hidden));
Assert.True(fixture.Record.PhysicsBody.State.HasFlag(
PhysicsStateFlags.NoDraw));
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
}
[Fact]
public void RejectedPlacementReceiptIsPureAndRetryableUnderSameLease()
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
PhysicsBody body = fixture.Record.PhysicsBody!;
Vector3 position = body.Position;
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
static (_, _, _, _) => TransitionState.Collided;
EvaluationPuritySnapshot before = CaptureEvaluationPurity(fixture);
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.RejectedPlacement,
fixture.Owner.EvaluateActivation(token, out var rejected));
AssertEvaluationPurity(before, CaptureEvaluationPurity(fixture));
Assert.True(rejected.IsValid);
Assert.True(fixture.Owner.IsEvaluationCurrent(rejected));
Assert.False(body.InWorld);
Assert.Equal(position, body.Position);
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook = null;
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var retry));
AssertEvaluationPurity(before, CaptureEvaluationPurity(fixture));
Assert.True(fixture.Owner.IsEvaluationCurrent(retry));
Assert.False(fixture.Owner.IsEvaluationCurrent(rejected));
}
[Fact]
public void StaleActivationReceiptCannotPublishAndNextEvaluationRetiresDormantOwners()
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var receipt));
fixture.Lifetime.Entities.SetFinalPhysicsState(
fixture.Record,
fixture.Record.FinalPhysicsState | PhysicsStateFlags.Frozen);
Assert.False(fixture.Owner.IsEvaluationCurrent(receipt));
PhysicsBody staleBody = fixture.Record.PhysicsBody!;
Assert.False(staleBody.InWorld);
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
Assert.False(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
out _));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.RejectedAuthority,
fixture.Owner.EvaluateActivation(token, out _));
Assert.Null(fixture.Record.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
Assert.Equal(Cell, fixture.Record.FullCellId);
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
Assert.Equal(0, fixture.Lifetime.Physics.CaptureOwnership()
.RetainedShadowRegistrationCount);
Assert.Equal(0, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
}
[Fact]
public void CollisionAdmissionRejectsEvaluationUntilReplacementCommits()
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var receipt));
PhysicsBody body = fixture.Record.PhysicsBody!;
RuntimeCollisionAdmission admission = fixture.Lifetime.Physics
.BeginCollisionAdmission(Cell & 0xFFFF0000u);
Assert.False(fixture.Owner.IsEvaluationCurrent(receipt));
Assert.False(body.InWorld);
Assert.False(body.TransientState.HasFlag(TransientStateFlags.Active));
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.RejectedAuthority,
fixture.Owner.EvaluateActivation(token, out _));
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
using PreparedLandblockCollisionGeneration prepared = fixture
.Lifetime.Physics.PrepareCollisionGeneration(admission);
fixture.Lifetime.Physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(Cell & 0xFFFF0000u));
Assert.True(CommitPrepared(
fixture.Lifetime.Physics,
admission,
prepared).Committed);
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var replacement));
Assert.True(fixture.Owner.IsEvaluationCurrent(replacement));
}
[Fact]
public void ReentrantCollisionWorldMutationRejectsOtherwiseCurrentEvaluation()
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
bool mutated = false;
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
(_, phase, _, observed) =>
{
if (!mutated
&& phase is TransitionCellCollisionPhase.Environment)
{
mutated = true;
RuntimeCollisionAdmission admission = fixture.Lifetime
.Physics.BeginCollisionAdmission(0x01010000u);
fixture.Lifetime.Physics.CancelCollisionGeneration(
admission);
}
return observed;
};
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.RejectedAuthority,
fixture.Owner.EvaluateActivation(token, out _));
Assert.True(mutated);
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook = null;
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var replacement));
Assert.True(fixture.Owner.IsEvaluationCurrent(replacement));
}
[Fact]
public void ReentrantCollisionGenerationCommitRejectsOtherwiseCurrentEvaluation()
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
RuntimeCollisionAdmission admission = fixture.Lifetime.Physics
.BeginCollisionAdmission(0x01010000u);
using PreparedLandblockCollisionGeneration prepared = fixture
.Lifetime.Physics.PrepareCollisionGeneration(admission);
fixture.Lifetime.Physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(0x01010000u));
bool committed = false;
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
(_, phase, _, observed) =>
{
if (!committed
&& phase is TransitionCellCollisionPhase.Environment)
{
committed = CommitPrepared(
fixture.Lifetime.Physics,
admission,
prepared).Committed;
}
return observed;
};
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.RejectedAuthority,
fixture.Owner.EvaluateActivation(token, out _));
Assert.True(committed);
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook = null;
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var replacement));
Assert.True(fixture.Owner.IsEvaluationCurrent(replacement));
}
[Theory]
[InlineData(ActivationInvalidation.Position)]
[InlineData(ActivationInvalidation.Vector)]
[InlineData(ActivationInvalidation.ObjectDescription)]
[InlineData(ActivationInvalidation.Create)]
[InlineData(ActivationInvalidation.Identity)]
[InlineData(ActivationInvalidation.Body)]
[InlineData(ActivationInvalidation.Controller)]
[InlineData(ActivationInvalidation.Clock)]
[InlineData(ActivationInvalidation.Spatial)]
[InlineData(ActivationInvalidation.Host)]
[InlineData(ActivationInvalidation.Remote)]
[InlineData(ActivationInvalidation.Projectile)]
[InlineData(ActivationInvalidation.PlacementCancellation)]
public void EveryPostOwnershipAuthorityReplacementInvalidatesEvaluation(
ActivationInvalidation invalidation)
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var receipt));
switch (invalidation)
{
case ActivationInvalidation.Position:
Assert.True(fixture.Lifetime.Physics.SetPosition
.BeginAuthoredPlacement(
fixture.Record,
fixture.Record.PositionAuthorityVersion,
RuntimeSetPositionOperationKind.LocalAuthoritative)
.IsValid);
break;
case ActivationInvalidation.Vector:
fixture.Lifetime.Entities.AdvanceVectorAuthority(fixture.Record);
break;
case ActivationInvalidation.ObjectDescription:
fixture.Lifetime.Entities.AdvanceObjDescAuthority(fixture.Record);
break;
case ActivationInvalidation.Create:
fixture.Lifetime.Entities.AdvanceCreateAuthority(fixture.Record);
break;
case ActivationInvalidation.Identity:
fixture.Identity.ServerGuid++;
break;
case ActivationInvalidation.Body:
fixture.Lifetime.Entities.SetPhysicsBody(
fixture.Record,
new PhysicsBody());
break;
case ActivationInvalidation.Controller:
fixture.Movement.Controller = new PlayerMovementController(
new PhysicsEngine());
break;
case ActivationInvalidation.Clock:
fixture.Lifetime.Entities.SuspendObjectClock(fixture.Record);
break;
case ActivationInvalidation.Spatial:
fixture.Lifetime.Entities.SetFullCell(
fixture.Record,
Cell + 1u,
(Cell & 0xFFFF0000u) | 0xFFFFu);
break;
case ActivationInvalidation.Host:
fixture.Lifetime.Entities.SetPhysicsHost(
fixture.Record,
CreatePhysicsHost(fixture.Record.ServerGuid));
break;
case ActivationInvalidation.Remote:
fixture.Lifetime.Entities.SetRemoteMotion(
fixture.Record,
new RemoteMotion());
break;
case ActivationInvalidation.Projectile:
fixture.Lifetime.Entities.SetProjectile(
fixture.Record,
new RuntimeProjectile(
new PhysicsBody(),
new ProjectileCollisionSphere(
Vector3.Zero,
0.1f)));
break;
case ActivationInvalidation.PlacementCancellation:
Assert.True(fixture.Lifetime.Physics.SetPosition.Cancel(
fixture.Record,
publishWithdrawal: false));
break;
default:
throw new ArgumentOutOfRangeException(nameof(invalidation));
}
Assert.False(fixture.Owner.IsEvaluationCurrent(receipt));
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
Assert.False(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
out _));
}
[Fact]
public void ShadowInsertMoveStateSuspendAndRemoveInvalidateReceipts()
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var emptyWorld));
Assert.True(fixture.Owner.IsEvaluationCurrent(emptyWorld));
uint queriedCell = emptyWorld.Placement.Result.QueriedCellIds[0];
const uint ownerId = 0x7000F001u;
Vector3 far = new(100f, 100f, 0f);
fixture.Lifetime.Physics.Engine.ShadowObjects.Register(
ownerId,
SetupId,
far,
Quaternion.Identity,
0.25f,
0f,
0f,
queriedCell & 0xFFFF0000u,
ShadowCollisionType.Cylinder,
cylHeight: 1f,
seedCellId: queriedCell,
isStatic: false);
Assert.False(fixture.Owner.IsEvaluationCurrent(emptyWorld));
RuntimeLocalPlayerPhysicsActivationReceipt current = Reevaluate();
fixture.Lifetime.Physics.Engine.ShadowObjects.UpdatePosition(
ownerId,
far + Vector3.One,
Quaternion.Identity,
0f,
0f,
queriedCell & 0xFFFF0000u,
queriedCell);
Assert.False(fixture.Owner.IsEvaluationCurrent(current));
current = Reevaluate();
fixture.Lifetime.Physics.Engine.ShadowObjects.UpdatePhysicsState(
ownerId,
(uint)PhysicsStateFlags.Ethereal);
Assert.False(fixture.Owner.IsEvaluationCurrent(current));
current = Reevaluate();
Assert.True(fixture.Lifetime.Physics.Engine.ShadowObjects.Suspend(
ownerId));
Assert.False(fixture.Owner.IsEvaluationCurrent(current));
current = Reevaluate();
fixture.Lifetime.Physics.Engine.ShadowObjects.Deregister(ownerId);
Assert.False(fixture.Owner.IsEvaluationCurrent(current));
RuntimeLocalPlayerPhysicsActivationReceipt Reevaluate()
{
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var next));
Assert.True(fixture.Owner.IsEvaluationCurrent(next));
return next;
}
}
[Theory]
[InlineData(RestrictionObjectMutation.RemoveHouseObject)]
[InlineData(RestrictionObjectMutation.HouseOwnerProperty)]
[InlineData(RestrictionObjectMutation.HouseGuestList)]
[InlineData(RestrictionObjectMutation.MoverMonarch)]
public void RestrictionObjectMutationInvalidatesEvaluatedReceipt(
RestrictionObjectMutation mutation)
{
using var fixture = new Fixture(residentWorld: true);
SeedRestrictionObjects(fixture);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var receipt));
ApplyRestrictionObjectMutation(fixture, mutation);
Assert.False(fixture.Owner.IsEvaluationCurrent(receipt));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var replacement));
Assert.True(fixture.Owner.IsEvaluationCurrent(replacement));
}
[Theory]
[InlineData(RestrictionObjectMutation.RemoveHouseObject)]
[InlineData(RestrictionObjectMutation.HouseOwnerProperty)]
[InlineData(RestrictionObjectMutation.HouseGuestList)]
[InlineData(RestrictionObjectMutation.MoverMonarch)]
public void ReentrantRestrictionObjectMutationAbortsEvaluation(
RestrictionObjectMutation mutation)
{
using var fixture = new Fixture(residentWorld: true);
SeedRestrictionObjects(fixture);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
bool mutated = false;
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
(_, phase, _, observed) =>
{
if (!mutated
&& phase is TransitionCellCollisionPhase.Environment)
{
mutated = true;
ApplyRestrictionObjectMutation(fixture, mutation);
}
return observed;
};
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.RejectedAuthority,
fixture.Owner.EvaluateActivation(token, out _));
Assert.True(mutated);
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook = null;
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var replacement));
Assert.True(fixture.Owner.IsEvaluationCurrent(replacement));
}
[Fact]
public void ObjectTableNullFreshAndEqualRevisionAbaInvalidateReceipt()
{
using var fixture = new Fixture(residentWorld: true);
SeedRestrictionObjects(fixture);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var originalReceipt));
ClientObjectTable original = fixture.Lifetime.Objects;
fixture.Lifetime.Physics.Engine.Objects = null;
Assert.False(fixture.Owner.IsEvaluationCurrent(originalReceipt));
// Returning to the same reference/revision is an ABA-shaped binding
// replacement. The engine binding authority keeps the old receipt
// stale even though object identity and mutation revision match again.
fixture.Lifetime.Physics.Engine.Objects = original;
Assert.False(fixture.Owner.IsEvaluationCurrent(originalReceipt));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var reboundReceipt));
var fresh = new ClientObjectTable();
fresh.AddOrUpdate(new ClientObject { ObjectId = 0x70003F10u });
fresh.AddOrUpdate(new ClientObject { ObjectId = 0x70003F11u });
Assert.Equal(original.MutationRevision, fresh.MutationRevision);
fixture.Lifetime.Physics.Engine.Objects = fresh;
Assert.False(fixture.Owner.IsEvaluationCurrent(reboundReceipt));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var freshReceipt));
Assert.True(fixture.Owner.IsEvaluationCurrent(freshReceipt));
}
[Fact]
public void HouseRestrictionGuestsAreAnImmutableInputSnapshot()
{
var sourceGuests = new Dictionary<uint, uint>();
var restrictions = new HouseRestrictionRecord(
OpenToPublic: false,
AllegianceMonarchId: 0u,
Guests: sourceGuests);
var table = new ClientObjectTable();
var house = new ClientObject
{
ObjectId = RestrictionObjectId,
Restrictions = restrictions,
};
table.AddOrUpdate(house);
ulong retainedRevision = table.MutationRevision;
sourceGuests[0x70003F20u] = 1u;
Assert.False(restrictions.IsAllowedIn(0x70003F20u, 0u));
if (restrictions.Guests is IDictionary<uint, uint> dictionaryView)
{
Assert.Throws<NotSupportedException>(
() => dictionaryView[0x70003F20u] = 1u);
}
Assert.False(restrictions.IsAllowedIn(0x70003F20u, 0u));
Assert.Equal(retainedRevision, table.MutationRevision);
}
[Fact]
public void RemovalAndClearUnbindDirectRestrictionMutationAuthority()
{
var table = new ClientObjectTable();
var removed = new ClientObject { ObjectId = RestrictionObjectId };
table.AddOrUpdate(removed);
Assert.True(table.Remove(removed.ObjectId));
ulong removedRevision = table.MutationRevision;
removed.HouseOwnerId = 0x70003F21u;
Assert.Equal(removedRevision, table.MutationRevision);
var cleared = new ClientObject { ObjectId = 0x70003F22u };
table.AddOrUpdate(cleared);
table.Clear();
ulong clearedRevision = table.MutationRevision;
cleared.MonarchId = 0x70003F23u;
Assert.Equal(clearedRevision, table.MutationRevision);
}
[Fact]
public void AddOrUpdateReplacementTransfersDirectMutationAuthority()
{
var table = new ClientObjectTable();
var displaced = new ClientObject { ObjectId = RestrictionObjectId };
var replacement = new ClientObject { ObjectId = RestrictionObjectId };
table.AddOrUpdate(displaced);
table.AddOrUpdate(replacement);
ulong replacementRevision = table.MutationRevision;
displaced.MonarchId = 0x70003F24u;
Assert.Equal(replacementRevision, table.MutationRevision);
replacement.MonarchId = 0x70003F25u;
Assert.True(table.MutationRevision > replacementRevision);
}
[Fact]
public void SharedRetainedObjectNotifiesEveryOwningTable()
{
var first = new ClientObjectTable();
var second = new ClientObjectTable();
var shared = new ClientObject { ObjectId = RestrictionObjectId };
first.AddOrUpdate(shared);
second.AddOrUpdate(shared);
ulong firstRevision = first.MutationRevision;
ulong secondRevision = second.MutationRevision;
shared.HouseOwnerId = 0x70003F26u;
Assert.True(first.MutationRevision > firstRevision);
Assert.True(second.MutationRevision > secondRevision);
}
[Theory]
[InlineData(ReentrantActivationInvalidation.Reset)]
[InlineData(ReentrantActivationInvalidation.DeleteAndGuidReuse)]
public void ReentrantInvalidationDuringCoreEvaluationRetiresLease(
ReentrantActivationInvalidation invalidation)
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
RuntimeEntityRecord? replacement = null;
bool invalidated = false;
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
(_, phase, _, observed) =>
{
if (invalidated
|| phase is not TransitionCellCollisionPhase.Environment)
{
return observed;
}
invalidated = true;
if (invalidation is ReentrantActivationInvalidation.Reset)
{
fixture.Movement.ResetSession();
}
else
{
uint guid = fixture.Record.ServerGuid;
Assert.True(fixture.Lifetime.TryAcceptDelete(
new DeleteObject.Parsed(
guid,
fixture.Record.Incarnation),
isLocalPlayer: false,
removeRetainedObject: false,
out RuntimeEntityDeleteAcceptance acceptance));
fixture.Lifetime.CompleteAcceptedDelete(acceptance);
Assert.Null(fixture.Lifetime.RetireCanonicalOnly(
fixture.Record));
replacement = fixture.Lifetime.RegisterEntity(
Spawn(guid, incarnation: 2)).Canonical!;
}
return observed;
};
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.RejectedAuthority,
fixture.Owner.EvaluateActivation(token, out _));
Assert.True(invalidated);
Assert.Equal(0, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
Assert.Null(fixture.Movement.Controller);
Assert.Null(replacement?.PhysicsBody);
}
[Fact]
public void ExistingActivationLeaseCannotBeOverwrittenAfterExternalClear()
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
fixture.Lifetime.Entities.SetPhysicsBody(fixture.Record, null);
fixture.Movement.Controller = null;
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Prepare(
fixture.Record,
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
fixture.ActivationPreparation,
out _));
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
Assert.True(fixture.Owner.DiscardActivation(token));
Assert.Equal(0, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
fixture.RepreparePlacement();
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Prepared,
fixture.Owner.Prepare(
fixture.Record,
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
fixture.ActivationPreparation,
out _));
}
[Fact]
public void ResetRetiresPendingActivationAndConvergesItsOwnershipLedger()
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(fixture.Prepare(), out _));
fixture.Movement.ResetSession();
Assert.Null(fixture.Record.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
Assert.Equal(0, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
}
[Fact]
public void ExplicitDiscardAndDisposeRetirePendingActivationExactlyOnce()
{
var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.True(fixture.Owner.DiscardActivation(token));
Assert.False(fixture.Owner.DiscardActivation(token));
Assert.Null(fixture.Record.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
fixture.RepreparePlacement();
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(fixture.Prepare(), out _));
fixture.Owner.Dispose();
Assert.True(fixture.Owner.CaptureOwnership().IsConverged);
Assert.Null(fixture.Record.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
fixture.Dispose();
}
[Theory]
[InlineData(PublicationInvalidation.SetPositionReplacement)]
[InlineData(PublicationInvalidation.Vector)]
[InlineData(PublicationInvalidation.FinalState)]
[InlineData(PublicationInvalidation.ObjectDescription)]
[InlineData(PublicationInvalidation.Create)]
[InlineData(PublicationInvalidation.Remote)]
[InlineData(PublicationInvalidation.Projectile)]
[InlineData(PublicationInvalidation.Body)]
[InlineData(PublicationInvalidation.Clock)]
[InlineData(PublicationInvalidation.Controller)]
[InlineData(PublicationInvalidation.CancelPlacement)]
public void EveryNestedAuthorityReplacementDiscardsCandidateWithoutRollback(
PublicationInvalidation invalidation)
{
using var fixture = new Fixture();
RuntimeLocalPlayerPhysicsPublicationToken token = fixture.Prepare();
PhysicsBody? replacementBody = null;
PlayerMovementController? replacementController = null;
switch (invalidation)
{
case PublicationInvalidation.SetPositionReplacement:
Assert.True(fixture.Lifetime.Physics.SetPosition
.BeginAuthoredPlacement(
fixture.Record,
fixture.Record.PositionAuthorityVersion,
RuntimeSetPositionOperationKind.LocalAuthoritative)
.IsValid);
break;
case PublicationInvalidation.Vector:
fixture.Lifetime.Entities.AdvanceVectorAuthority(fixture.Record);
break;
case PublicationInvalidation.FinalState:
fixture.Lifetime.Entities.SetFinalPhysicsState(
fixture.Record,
fixture.Record.FinalPhysicsState | PhysicsStateFlags.Frozen);
break;
case PublicationInvalidation.ObjectDescription:
fixture.Lifetime.Entities.AdvanceObjDescAuthority(fixture.Record);
break;
case PublicationInvalidation.Create:
fixture.Lifetime.Entities.AdvanceCreateAuthority(fixture.Record);
break;
case PublicationInvalidation.Remote:
fixture.Lifetime.Entities.SetRemoteMotion(
fixture.Record,
new RemoteMotion());
break;
case PublicationInvalidation.Projectile:
fixture.Lifetime.Entities.SetProjectile(
fixture.Record,
new RuntimeProjectile(
new PhysicsBody(),
new ProjectileCollisionSphere(Vector3.Zero, 0.1f)));
break;
case PublicationInvalidation.Body:
replacementBody = new PhysicsBody();
fixture.Lifetime.Entities.SetPhysicsBody(
fixture.Record,
replacementBody);
break;
case PublicationInvalidation.Clock:
fixture.Lifetime.Entities.SuspendObjectClock(fixture.Record);
break;
case PublicationInvalidation.Controller:
replacementController = new PlayerMovementController(
new PhysicsEngine());
fixture.Movement.Controller = replacementController;
break;
case PublicationInvalidation.CancelPlacement:
Assert.True(fixture.Lifetime.Physics.SetPosition.Cancel(
fixture.Record,
publishWithdrawal: false));
break;
default:
throw new ArgumentOutOfRangeException(nameof(invalidation));
}
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Commit(token));
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
if (replacementBody is not null)
Assert.Same(replacementBody, fixture.Record.PhysicsBody);
if (replacementController is not null)
Assert.Same(replacementController, fixture.Movement.Controller);
}
[Fact]
public void DeleteAndGuidReuseCannotPublishRetiredCandidate()
{
using var fixture = new Fixture();
RuntimeLocalPlayerPhysicsPublicationToken token = fixture.Prepare();
uint guid = fixture.Record.ServerGuid;
Assert.True(fixture.Lifetime.TryAcceptDelete(
new DeleteObject.Parsed(guid, fixture.Record.Incarnation),
isLocalPlayer: false,
removeRetainedObject: false,
out RuntimeEntityDeleteAcceptance acceptance));
fixture.Lifetime.CompleteAcceptedDelete(acceptance);
Assert.Null(fixture.Lifetime.RetireCanonicalOnly(fixture.Record));
RuntimeEntityRecord replacement = fixture.Lifetime.RegisterEntity(
Spawn(guid, incarnation: 2)).Canonical!;
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Commit(token));
Assert.Null(replacement.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
}
[Fact]
public void ResetAndDisposeDiscardCandidatesAndConvergeOwnership()
{
var fixture = new Fixture();
_ = fixture.Prepare();
fixture.Movement.ResetSession();
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
fixture.RepreparePlacement();
_ = fixture.Prepare();
fixture.Movement.Dispose();
Assert.True(fixture.Movement.CaptureOwnership().IsConverged);
fixture.DisposeLifetimeOnly();
}
[Theory]
[InlineData(PristineViolation.Body)]
[InlineData(PristineViolation.Controller)]
[InlineData(PristineViolation.Host)]
[InlineData(PristineViolation.BodyAcquisition)]
[InlineData(PristineViolation.Remote)]
[InlineData(PristineViolation.RemoteBinding)]
[InlineData(PristineViolation.Projectile)]
[InlineData(PristineViolation.ProjectileBinding)]
[InlineData(PristineViolation.RemotePlacement)]
public void PreparationRejectsEveryNonPristineOwnershipGraph(
PristineViolation violation)
{
using var fixture = new Fixture();
switch (violation)
{
case PristineViolation.Body:
{
var activeBody = new PhysicsBody
{
TransientState = TransientStateFlags.Active,
};
fixture.Lifetime.Entities.SetPhysicsBody(
fixture.Record,
activeBody);
break;
}
case PristineViolation.Controller:
fixture.Movement.Controller = new PlayerMovementController(
new PhysicsEngine());
break;
case PristineViolation.Host:
fixture.Lifetime.Entities.SetPhysicsHost(
fixture.Record,
CreatePhysicsHost(fixture.Record.ServerGuid));
break;
case PristineViolation.BodyAcquisition:
fixture.Lifetime.Entities.SetPhysicsBodyAcquisitionInProgress(
fixture.Record,
true);
break;
case PristineViolation.Remote:
fixture.Lifetime.Entities.SetRemoteMotion(
fixture.Record,
new RemoteMotion());
break;
case PristineViolation.RemoteBinding:
fixture.Lifetime.Entities.SetRemoteMotionBindingInProgress(
fixture.Record,
true);
break;
case PristineViolation.Projectile:
fixture.Lifetime.Entities.SetProjectile(
fixture.Record,
new RuntimeProjectile(
new PhysicsBody(),
new ProjectileCollisionSphere(Vector3.Zero, 0.1f)));
break;
case PristineViolation.ProjectileBinding:
fixture.Lifetime.Entities.SetProjectileBindingInProgress(
fixture.Record,
true);
break;
case PristineViolation.RemotePlacement:
fixture.Lifetime.Entities.SetRequiresRemotePlacementRuntime(
fixture.Record,
true);
break;
default:
throw new ArgumentOutOfRangeException(nameof(violation));
}
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Prepare(
fixture.Record,
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
fixture.ActivationPreparation,
out RuntimeLocalPlayerPhysicsPublicationToken token));
Assert.False(token.IsValid);
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
}
[Fact]
public void PreparationRejectsRecordWhichIsNotTheExactLocalIdentity()
{
using var fixture = new Fixture();
fixture.Identity.ServerGuid = 0x70003002u;
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Prepare(
fixture.Record,
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
fixture.ActivationPreparation,
out _));
}
[Fact]
public void PreparationRejectsDisposedLocalIdentity()
{
using var fixture = new Fixture();
fixture.Identity.Dispose();
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Prepare(
fixture.Record,
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
fixture.ActivationPreparation,
out _));
}
[Fact]
public void IdentityRevisionSwitchRejectsPreparedCandidate()
{
using var fixture = new Fixture();
RuntimeLocalPlayerPhysicsPublicationToken token = fixture.Prepare();
uint guid = fixture.Identity.ServerGuid;
fixture.Identity.ServerGuid = guid + 1u;
fixture.Identity.ServerGuid = guid;
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Commit(token));
Assert.Null(fixture.Record.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
}
[Theory]
[InlineData(PublishedRetirement.Replacement)]
[InlineData(PublishedRetirement.Reset)]
[InlineData(PublishedRetirement.Dispose)]
public void RuntimeOwnedControllerRejectsStaleOperationsAfterRetirement(
PublishedRetirement retirement)
{
using var fixture = new Fixture();
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(fixture.Prepare()));
PlayerMovementController stale = fixture.Movement.Controller!;
switch (retirement)
{
case PublishedRetirement.Replacement:
fixture.Movement.Controller = new PlayerMovementController(
new PhysicsEngine());
break;
case PublishedRetirement.Reset:
fixture.Movement.ResetSession();
break;
case PublishedRetirement.Dispose:
fixture.Movement.Dispose();
break;
default:
throw new ArgumentOutOfRangeException(nameof(retirement));
}
AssertNotLive(stale);
}
[Fact]
public void BodyAndControllerEpochsAdvanceOnlyOnOwnershipChanges()
{
using var fixture = new Fixture(preparePlacement: false);
var firstBody = new PhysicsBody();
var secondBody = new PhysicsBody();
ulong bodyEpoch = fixture.Record.PhysicsOwnershipEpoch;
fixture.Lifetime.Entities.SetPhysicsBody(fixture.Record, firstBody);
Assert.Equal(bodyEpoch + 1UL, fixture.Record.PhysicsOwnershipEpoch);
fixture.Lifetime.Entities.SetPhysicsBody(fixture.Record, firstBody);
Assert.Equal(bodyEpoch + 1UL, fixture.Record.PhysicsOwnershipEpoch);
fixture.Lifetime.Entities.SetPhysicsBody(fixture.Record, secondBody);
Assert.Equal(bodyEpoch + 2UL, fixture.Record.PhysicsOwnershipEpoch);
fixture.Lifetime.Entities.SetPhysicsBody(fixture.Record, null);
Assert.Equal(bodyEpoch + 3UL, fixture.Record.PhysicsOwnershipEpoch);
var first = new PlayerMovementController(new PhysicsEngine());
var second = new PlayerMovementController(new PhysicsEngine());
ulong controllerEpoch = fixture.Movement.ControllerOwnershipEpoch;
fixture.Movement.Controller = first;
Assert.Equal(controllerEpoch + 1UL,
fixture.Movement.ControllerOwnershipEpoch);
fixture.Movement.Controller = first;
Assert.Equal(controllerEpoch + 1UL,
fixture.Movement.ControllerOwnershipEpoch);
fixture.Movement.Controller = second;
Assert.Equal(controllerEpoch + 2UL,
fixture.Movement.ControllerOwnershipEpoch);
fixture.Movement.Controller = null;
Assert.Equal(controllerEpoch + 3UL,
fixture.Movement.ControllerOwnershipEpoch);
}
private static EvaluationPuritySnapshot CaptureEvaluationPurity(
Fixture fixture)
{
PhysicsBody body = fixture.Record.PhysicsBody!;
bool hasAwaiting = fixture.Lifetime.Physics.SetPosition
.TryGetAwaitingPreparationToken(
fixture.Record,
out RuntimeEntityPlacementToken awaiting);
return new EvaluationPuritySnapshot(
fixture.Lifetime.Physics.CaptureOwnership(),
fixture.Lifetime.Physics.SetPosition.CaptureOwnership(),
fixture.Lifetime.Physics.CollisionReports.CaptureOwnership(),
fixture.Lifetime.Physics.CollisionWorldAuthority,
fixture.Lifetime.Physics.ShadowWorldAuthority,
fixture.Lifetime.Physics.ObjectTable,
fixture.Lifetime.Physics.ObjectTableBindingAuthority,
fixture.Lifetime.Physics.ObjectTableAuthority,
hasAwaiting,
awaiting,
fixture.Lifetime.Physics.SetPosition
.IsExactPreparedPlacementCurrent(
fixture.Record,
fixture.Placement,
fixture.Command),
fixture.Record.FullCellId,
fixture.Record.SpatialAuthorityVersion,
fixture.Record.PlacementCommitVersion,
fixture.Record.ObjectClockEpoch,
fixture.Record.ObjectClock.PendingSeconds,
fixture.Record.ObjectClock.IsActive,
CaptureBody(body),
CaptureVectorBits(body.WalkableVertices));
}
private static void AssertEvaluationPurity(
in EvaluationPuritySnapshot expected,
in EvaluationPuritySnapshot actual)
{
Assert.Equal(expected.PhysicsOwnership, actual.PhysicsOwnership);
Assert.Equal(expected.SetPositionOwnership, actual.SetPositionOwnership);
Assert.Equal(expected.CollisionOwnership, actual.CollisionOwnership);
Assert.Equal(expected.CollisionWorldAuthority,
actual.CollisionWorldAuthority);
Assert.Equal(expected.ShadowWorldAuthority,
actual.ShadowWorldAuthority);
Assert.Same(expected.ObjectTable, actual.ObjectTable);
Assert.Equal(expected.ObjectTableBindingAuthority,
actual.ObjectTableBindingAuthority);
Assert.Equal(expected.ObjectTableAuthority,
actual.ObjectTableAuthority);
Assert.Equal(expected.HasAwaitingPlacement,
actual.HasAwaitingPlacement);
Assert.Equal(expected.AwaitingPlacement, actual.AwaitingPlacement);
Assert.Equal(expected.ExactPlacementCurrent,
actual.ExactPlacementCurrent);
Assert.Equal(expected.FullCellId, actual.FullCellId);
Assert.Equal(expected.SpatialAuthorityVersion,
actual.SpatialAuthorityVersion);
Assert.Equal(expected.PlacementCommitVersion,
actual.PlacementCommitVersion);
Assert.Equal(expected.ObjectClockEpoch, actual.ObjectClockEpoch);
Assert.Equal(expected.ObjectClockPending,
actual.ObjectClockPending);
Assert.Equal(expected.ObjectClockActive, actual.ObjectClockActive);
Assert.Equal(expected.Body, actual.Body);
Assert.True(expected.WalkableVertexBits.AsSpan().SequenceEqual(
actual.WalkableVertexBits.AsSpan()));
}
private static BodyPuritySnapshot CaptureBody(PhysicsBody body) => new(
body.Position,
body.CellPosition,
body.InWorld,
body.Orientation,
body.Velocity,
body.CachedVelocity,
body.FramesStationaryFall,
body.Acceleration,
body.Omega,
body.GroundNormal,
body.SlidingNormal,
body.ContactPlaneValid,
body.ContactPlane,
body.ContactPlaneCellId,
body.ContactPlaneIsWater,
body.WalkablePolygonValid,
body.WalkablePlane,
body.WalkableUp,
body.Elasticity,
body.Friction,
body.State,
body.TransientState,
body.LastUpdateTime,
body.IsFullyConstrained,
body.LastMoveWasAutonomous);
private static ImmutableArray<uint> CaptureVectorBits(
Vector3[]? vectors)
{
if (vectors is null)
return ImmutableArray<uint>.Empty;
var result = ImmutableArray.CreateBuilder<uint>(vectors.Length * 3);
foreach (Vector3 vector in vectors)
{
result.Add(BitConverter.SingleToUInt32Bits(vector.X));
result.Add(BitConverter.SingleToUInt32Bits(vector.Y));
result.Add(BitConverter.SingleToUInt32Bits(vector.Z));
}
return result.ToImmutable();
}
private readonly record struct EvaluationPuritySnapshot(
RuntimePhysicsOwnershipSnapshot PhysicsOwnership,
RuntimeSetPositionOwnershipSnapshot SetPositionOwnership,
RuntimeCollisionReportingOwnershipSnapshot CollisionOwnership,
ulong CollisionWorldAuthority,
ulong ShadowWorldAuthority,
ClientObjectTable? ObjectTable,
ulong ObjectTableBindingAuthority,
ulong ObjectTableAuthority,
bool HasAwaitingPlacement,
RuntimeEntityPlacementToken AwaitingPlacement,
bool ExactPlacementCurrent,
uint FullCellId,
ulong SpatialAuthorityVersion,
ulong PlacementCommitVersion,
ulong ObjectClockEpoch,
double ObjectClockPending,
bool ObjectClockActive,
BodyPuritySnapshot Body,
ImmutableArray<uint> WalkableVertexBits);
private readonly record struct BodyPuritySnapshot(
Vector3 Position,
AcDream.Core.Physics.Position CellPosition,
bool InWorld,
Quaternion Orientation,
Vector3 Velocity,
Vector3 CachedVelocity,
int FramesStationaryFall,
Vector3 Acceleration,
Vector3 Omega,
Vector3 GroundNormal,
Vector3 SlidingNormal,
bool ContactPlaneValid,
Plane ContactPlane,
uint ContactPlaneCellId,
bool ContactPlaneIsWater,
bool WalkablePolygonValid,
Plane WalkablePlane,
Vector3 WalkableUp,
float Elasticity,
float Friction,
PhysicsStateFlags State,
TransientStateFlags TransientState,
double LastUpdateTime,
bool IsFullyConstrained,
bool LastMoveWasAutonomous);
private sealed class Fixture : IDisposable
{
private bool _lifetimeDisposed;
// C3c-F5: default 0 keeps every pre-existing test's mover shape
// byte-identical; the first-entry settle tests pass the production
// human Setup's bottom-sphere origin (0.475) so an authored
// placement can stand clear of a floor the settle then reaches.
private readonly float _moverSphereOriginZ;
internal Fixture(
bool preparePlacement = true,
bool residentWorld = false,
RuntimeLocalPlayerShadowDisposition shadowDisposition =
RuntimeLocalPlayerShadowDisposition.ProvenShapeless,
float terrainHeight = 0f,
float moverSphereOriginZ = 0f,
Vector3? initialVelocity = null,
Vector3? initialOmega = null,
float? initialFriction = null,
float? initialElasticity = null)
{
_moverSphereOriginZ = moverSphereOriginZ;
if (residentWorld)
{
var engine = new PhysicsEngine
{
DataCache = new PhysicsDataCache(),
};
engine.AddLandblock(
Cell & 0xFFFF0000u,
new TerrainSurface(
new byte[81],
Enumerable.Repeat(terrainHeight, 256).ToArray()),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
Lifetime = new RuntimeEntityObjectLifetime(engine);
}
else
{
Lifetime = new RuntimeEntityObjectLifetime();
}
Movement = new RuntimeLocalPlayerMovementState();
Identity = new RuntimeLocalPlayerIdentityState();
Owner = new RuntimeLocalPlayerPhysicsPublicationState(
Lifetime.Entities,
Lifetime.Physics,
Movement,
Identity);
Movement.AttachPhysicsPublication(Owner);
Record = Lifetime.RegisterEntity(
Spawn(
0x70003001u,
incarnation: 1,
initialVelocity,
initialOmega,
initialFriction,
initialElasticity)).Canonical!;
Identity.ServerGuid = Record.ServerGuid;
ActivationPreparation = new(
Radius: 0.48f,
Height: 1.835f,
shadowDisposition);
if (preparePlacement)
RepreparePlacement();
}
internal RuntimeEntityObjectLifetime Lifetime { get; }
internal RuntimeLocalPlayerMovementState Movement { get; }
internal RuntimeLocalPlayerIdentityState Identity { get; }
internal RuntimeLocalPlayerPhysicsPublicationState Owner { get; }
internal RuntimeEntityRecord Record { get; }
internal RuntimeEntityPlacementToken Placement { get; private set; }
internal RuntimeSetPositionCommand Command { get; private set; }
internal RuntimeLocalPlayerPhysicsActivationPreparation
ActivationPreparation { get; }
internal void RepreparePlacement()
{
Placement = Lifetime.Physics.SetPosition.BeginAuthoredPlacement(
Record,
Record.PositionAuthorityVersion,
RuntimeSetPositionOperationKind.LocalAuthoritative);
Assert.True(Placement.IsValid);
var setup = new FlatSetupCollision(
ImmutableArray<FlatCollisionCylinder>.Empty,
[new FlatCollisionSphere(
new Vector3(0f, 0f, _moverSphereOriginZ),
0.48f)],
height: 0f,
radius: 0f,
stepUpHeight: 0.4f,
stepDownHeight: 0.4f);
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared,
Lifetime.Physics.SetPosition.PrepareMover(
Placement,
new RuntimeSetPositionMoverPreparation(
RuntimeSetPositionMoverSetup.Resolved(SetupId, setup),
RuntimeSetPositionOperationKind.LocalAuthoritative,
GameTime: 10d,
PhysicsPlacementClass.Ordinary,
PhysicsSetPositionFlags.Placement
| PhysicsSetPositionFlags.Slide),
out RuntimeSetPositionCommand command));
Command = command;
}
internal RuntimeLocalPlayerPhysicsPublicationToken Prepare()
{
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Prepared,
Owner.Prepare(
Record,
Placement,
Command,
PlayerMovementConstructionOptions.Fallback,
ActivationPreparation,
out RuntimeLocalPlayerPhysicsPublicationToken token));
Assert.Equal(Record.Key, token.Entity);
Assert.Equal(Placement, token.Placement);
Assert.Equal(Identity.ServerGuid, token.LocalPlayerServerGuid);
Assert.Equal(Identity.Revision, token.LocalPlayerIdentityRevision);
return token;
}
internal void DisposeLifetimeOnly()
{
if (_lifetimeDisposed)
return;
Lifetime.Dispose();
_lifetimeDisposed = true;
}
public void Dispose()
{
Movement.Dispose();
Identity.Dispose();
DisposeLifetimeOnly();
}
}
private sealed class PlacementObserver(
Action<RuntimePlacementDelta>? onPlacement = null)
: IRuntimePlacementObserver
{
internal List<RuntimePlacementDelta> Deltas { get; } = [];
public void OnPlacement(in RuntimePlacementDelta delta)
{
Deltas.Add(delta);
onPlacement?.Invoke(delta);
}
}
private sealed class PublicationCollisionObserver(
Action<RuntimeCollisionReport> onReport)
: IRuntimeCollisionReportObserver
{
public void OnCollisionReport(in RuntimeCollisionReport report) =>
onReport(report);
}
private sealed class CallbackMotionSink(Action onApply)
: IInterpretedMotionSink
{
public bool ApplyMotion(uint motion, float speed)
{
onApply();
return true;
}
public bool StopMotion(uint motion)
{
onApply();
return true;
}
}
private static WorldSession.EntitySpawn Spawn(
uint guid,
ushort incarnation,
Vector3? initialVelocity = null,
Vector3? initialOmega = null,
float? initialFriction = null,
float? initialElasticity = null)
{
var position = new CreateObject.ServerPosition(
Cell,
1f,
2f,
3f,
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: incarnation);
var physics = new PhysicsSpawnData(
RawState: (uint)(PhysicsStateFlags.Gravity
| PhysicsStateFlags.ReportCollisions),
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: SetupId,
MotionTableId: 0x09000001u,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: 1f,
Friction: initialFriction,
Elasticity: initialElasticity,
Translucency: null,
Velocity: initialVelocity,
Acceleration: null,
AngularVelocity: initialOmega,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
Guid: guid,
Position: position,
SetupTableId: SetupId,
AnimPartChanges: Array.Empty<CreateObject.AnimPartChange>(),
TextureChanges: Array.Empty<CreateObject.TextureChange>(),
SubPalettes: Array.Empty<CreateObject.SubPaletteSwap>(),
BasePaletteId: null,
ObjScale: 1f,
Name: "local-publication-fixture",
ItemType: null,
MotionState: null,
MotionTableId: 0x09000001u,
PhysicsState: physics.RawState,
ObjectDescriptionFlags: 0x8u,
Friction: initialFriction,
Elasticity: initialElasticity,
InstanceSequence: incarnation,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
private const uint RestrictionObjectId = 0x70003F01u;
private static void SeedRestrictionObjects(Fixture fixture)
{
fixture.Lifetime.Objects.AddOrUpdate(new ClientObject
{
ObjectId = RestrictionObjectId,
HouseOwnerId = 0x70003F02u,
Restrictions = new HouseRestrictionRecord(
OpenToPublic: false,
AllegianceMonarchId: 0x70003F03u,
Guests: new Dictionary<uint, uint>()),
});
fixture.Lifetime.Objects.AddOrUpdate(new ClientObject
{
ObjectId = fixture.Record.ServerGuid,
MonarchId = 0x70003F03u,
});
}
private static void ApplyRestrictionObjectMutation(
Fixture fixture,
RestrictionObjectMutation mutation)
{
ClientObjectTable objects = fixture.Lifetime.Objects;
switch (mutation)
{
case RestrictionObjectMutation.RemoveHouseObject:
Assert.True(objects.Remove(RestrictionObjectId));
break;
case RestrictionObjectMutation.HouseOwnerProperty:
{
ClientObject house = objects.Get(RestrictionObjectId)!;
house.HouseOwnerId = 0x70003F04u;
break;
}
case RestrictionObjectMutation.HouseGuestList:
objects.Get(RestrictionObjectId)!.Restrictions =
new HouseRestrictionRecord(
OpenToPublic: false,
AllegianceMonarchId: 0x70003F03u,
Guests: new Dictionary<uint, uint>
{
[fixture.Record.ServerGuid] = 1u,
});
break;
case RestrictionObjectMutation.MoverMonarch:
{
ClientObject mover = objects.Get(fixture.Record.ServerGuid)!;
mover.MonarchId = 0x70003F05u;
break;
}
default:
throw new ArgumentOutOfRangeException(nameof(mutation));
}
}
private static RuntimeCollisionGenerationCommit CommitPrepared(
RuntimePhysicsState physics,
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared)
{
bool engineCommitted = false;
for (int poll = 0; poll < 10_000; poll++)
{
if (!engineCommitted)
{
while (!physics.AdvanceCollisionRetainedOwnerCapture(
admission,
prepared).Completed)
{
}
foreach (uint ownerId in prepared.RetainedOwnerIds)
{
physics.RefreshCollisionRetainedOwner(
admission,
prepared,
ownerId);
}
RuntimeCollisionSealStep seal;
do
{
seal = physics.AdvanceCollisionGenerationSeal(
admission,
prepared);
}
while (!seal.Completed && !seal.Restarted);
if (!seal.Completed)
continue;
}
RuntimeCollisionGenerationCommit result =
physics.CommitCollisionGeneration(admission, prepared);
if (result.Completed)
return result;
while (physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot projection))
{
Assert.True(physics.SetPosition.AcknowledgeProjection(
projection.Token));
}
engineCommitted = result.EngineCommitted;
}
throw new InvalidOperationException(
"Collision generation did not complete its Runtime mutation transaction.");
}
/// <summary>
/// C3c-F2: publishes one landblock collision generation through the exact
/// production owner chain — BeginCollisionAdmission -> prepare -> stage ->
/// CommitCollisionGeneration. Waking a parked activation any other way
/// (calling the SetPosition seam directly) leaves RuntimePhysicsState's
/// admission ledger empty, a state production can never reach.
/// </summary>
private static void CommitProductionCollisionGeneration(
Fixture fixture,
uint landblockId)
{
RuntimeCollisionAdmission admission = fixture.Lifetime.Physics
.BeginCollisionAdmission(landblockId);
using PreparedLandblockCollisionGeneration prepared = fixture
.Lifetime.Physics.PrepareCollisionGeneration(admission);
fixture.Lifetime.Physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(landblockId));
Assert.True(CommitPrepared(
fixture.Lifetime.Physics,
admission,
prepared).Committed);
}
private static RuntimeLandblockCollisionAssets CollisionAssets(
uint landblockId) => new(
landblockId,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
0f,
0f,
0u);
public enum PublicationInvalidation
{
SetPositionReplacement,
Vector,
FinalState,
ObjectDescription,
Create,
Remote,
Projectile,
Body,
Clock,
Controller,
CancelPlacement,
}
public enum PristineViolation
{
Body,
Controller,
Host,
BodyAcquisition,
Remote,
RemoteBinding,
Projectile,
ProjectileBinding,
RemotePlacement,
}
public enum PublishedRetirement
{
Replacement,
Reset,
Dispose,
}
public enum ActivationInvalidation
{
Position,
Vector,
ObjectDescription,
Create,
Identity,
Body,
Controller,
Clock,
Spatial,
Host,
Remote,
Projectile,
PlacementCancellation,
}
public enum ReentrantActivationInvalidation
{
Reset,
DeleteAndGuidReuse,
}
public enum RestrictionObjectMutation
{
RemoveHouseObject,
HouseOwnerProperty,
HouseGuestList,
MoverMonarch,
}
private static EntityPhysicsHost CreatePhysicsHost(uint id) => new(
id,
getPosition: static () => default,
getVelocity: static () => Vector3.Zero,
getRadius: static () => 0.48f,
inContact: static () => true,
minterpMaxSpeed: static () => null,
curTime: static () => 0d,
physicsTimerTime: static () => 0d,
getObjectA: static _ => null,
handleUpdateTarget: static _ => { },
interruptCurrentMovement: static () => { });
private static void AssertNotLive(PlayerMovementController controller)
{
Assert.Throws<InvalidOperationException>(() => controller.Update(
1f / 60f,
default));
Assert.Throws<InvalidOperationException>(() => controller.TickHidden(
1f / 60f));
Assert.Throws<InvalidOperationException>(() =>
controller.SuspendObjectUpdate(1f / 60f));
Assert.Throws<InvalidOperationException>(() => controller.SetPosition(
Vector3.One,
Cell,
Vector3.One));
Assert.Throws<InvalidOperationException>(() =>
controller.CommitCanonicalForcePositionFrame());
Assert.Throws<InvalidOperationException>(() =>
controller.ApplyPhysicsState(PhysicsStateFlags.Frozen));
Assert.Throws<InvalidOperationException>(() => controller.Yaw = 1f);
Assert.Throws<InvalidOperationException>(() =>
controller.CaptureMovementResult(mouseLookEvent: false));
Assert.Throws<InvalidOperationException>(() =>
controller.CapturePresentationResult());
Assert.Throws<InvalidOperationException>(() =>
controller.TryGetOutboundPosition(out _));
Assert.Throws<InvalidOperationException>(() =>
controller.NoteMovementSent(1f));
Assert.Throws<InvalidOperationException>(() =>
controller.NotePositionSent(default, default, 1f));
Assert.Throws<InvalidOperationException>(() =>
controller.ShouldSendPositionEvent(default, default, 1f));
Assert.Throws<InvalidOperationException>(() =>
controller.BeginMouseLook(default));
Assert.Throws<InvalidOperationException>(() =>
controller.SubmitMouseTurnAdjustment(1f, default));
Assert.Throws<InvalidOperationException>(() =>
controller.StopMouseDrift(default));
Assert.Throws<InvalidOperationException>(() =>
controller.EndMouseLook(default));
Assert.Throws<InvalidOperationException>(() =>
controller.PrepareForAttackRequest());
Assert.Throws<InvalidOperationException>(() =>
controller.RequestPosture(MotionCommand.Ready));
Assert.Throws<InvalidOperationException>(() =>
controller.CommitPreparedPosition());
Assert.Throws<InvalidOperationException>(() =>
controller.PreparePositionForCommit(Vector3.One, Cell, Vector3.One));
Assert.Throws<InvalidOperationException>(() =>
controller.SetCharacterSkills(1, 1));
Assert.Throws<InvalidOperationException>(() =>
controller.SetBodyOrientation(Quaternion.Identity));
Assert.Throws<InvalidOperationException>(() =>
controller.SetLastMoveWasAutonomous(true));
Assert.Throws<InvalidOperationException>(() =>
controller.State = PlayerState.PortalSpace);
Assert.Throws<InvalidOperationException>(() =>
controller.StepUpHeight = 1f);
Assert.Throws<InvalidOperationException>(() =>
controller.AttachCycleVelocityAccessor(static () => Vector3.One));
Assert.Throws<InvalidOperationException>(() =>
controller.AttachAnimationRootMotionSource(static (_, _) => { }));
Assert.Throws<InvalidOperationException>(() => _ = controller.Movement);
Assert.Throws<InvalidOperationException>(() => _ = controller.MoveTo);
Assert.Throws<InvalidOperationException>(() => _ = controller.PositionManager);
Assert.Throws<InvalidOperationException>(() => _ = controller.Motion);
Assert.Throws<InvalidOperationException>(() => _ = controller.PhysicsBody);
}
}