feat(runtime): commit dormant SetPosition activation

This commit is contained in:
Erik 2026-08-01 14:25:02 +02:00
parent 99f867f053
commit 5785a07b3e
13 changed files with 4674 additions and 111 deletions

View file

@ -10,6 +10,284 @@ public sealed class ShadowSetPositionCommitTests
private const uint Cell1 = Landblock | 0x0001u;
private const uint Cell9 = Landblock | 0x0009u;
[Fact]
public void PreparedAuthoredMoveKeepsOldRowsUntilAtomicApply()
{
var registry = RegisteredSingle();
var moved = new Vector3(36f, 12f, 50f);
Assert.True(registry.TryPrepareSetPosition(
1u,
moved,
Quaternion.Identity,
Cell9,
0f,
0f,
PhysicsShadowCommitAction.Replace,
[Cell9],
provenShapeless: false,
suspendOwner: false,
out var prepared));
Assert.Equal(new Vector3(12f, 12f, 50f),
Assert.Single(registry.GetObjectsInCell(Cell1)).Position);
Assert.Empty(registry.GetObjectsInCell(Cell9));
Assert.True(registry.TryApplySetPosition(prepared!, out var receipt));
Assert.True(receipt.Mutated);
Assert.Empty(registry.GetObjectsInCell(Cell1));
Assert.Equal(moved,
Assert.Single(registry.GetObjectsInCell(Cell9)).Position);
}
[Fact]
public void PreparedCrossPrefixDispatchesMembershipThenMutationOnce()
{
const uint otherCell = 0xA9B50001u;
var registry = RegisteredSingle();
var callbacks = new List<string>();
registry.OwnerPrefixMembershipChanged += (owner, prefix) =>
callbacks.Add($"prefix:{owner:X8}:{prefix:X8}");
registry.OwnerMutated += (owner, version) =>
callbacks.Add($"owner:{owner:X8}:{version}");
Assert.True(registry.TryPrepareSetPosition(
1u,
new Vector3(204f, 12f, 50f),
Quaternion.Identity,
otherCell,
192f,
0f,
PhysicsShadowCommitAction.Replace,
[otherCell],
provenShapeless: false,
suspendOwner: false,
out var prepared));
Assert.True(registry.TryApplySetPosition(prepared!, out var receipt));
Assert.Empty(callbacks);
registry.DispatchSetPositionCommit(receipt);
registry.DispatchSetPositionCommit(receipt);
Assert.Equal(
[
"prefix:00000001:A9B40000",
"prefix:00000001:A9B50000",
$"owner:00000001:{receipt.OwnerVersion}",
], callbacks);
}
[Fact]
public void TwoOwnerReceiptsDispatchExactlyOnceInReverseOrder()
{
const uint otherCell = 0xA9B50001u;
var registry = RegisteredSingle();
registry.Register(
2u, 0x01000002u, new Vector3(13f, 12f, 50f),
Quaternion.Identity, 1f, 0f, 0f, Landblock,
seedCellId: Cell1, isStatic: false);
var owners = new List<uint>();
registry.OwnerMutated += (owner, _) => owners.Add(owner);
Assert.True(registry.TryPrepareSetPosition(
1u, new Vector3(204f, 12f, 50f), Quaternion.Identity,
otherCell, 192f, 0f, PhysicsShadowCommitAction.Replace,
[otherCell], provenShapeless: false, suspendOwner: false,
out var first));
Assert.True(registry.TryApplySetPosition(first!, out var firstReceipt));
Assert.True(registry.TryPrepareSetPosition(
2u, new Vector3(205f, 12f, 50f), Quaternion.Identity,
otherCell, 192f, 0f, PhysicsShadowCommitAction.Replace,
[otherCell], provenShapeless: false, suspendOwner: false,
out var second));
Assert.True(registry.TryApplySetPosition(second!, out var secondReceipt));
registry.DispatchSetPositionCommit(secondReceipt);
registry.DispatchSetPositionCommit(firstReceipt);
registry.DispatchSetPositionCommit(secondReceipt);
registry.DispatchSetPositionCommit(firstReceipt);
Assert.Equal([2u, 1u], owners);
Assert.Equal(0, registry.PendingSetPositionDispatchCount);
}
[Fact]
public void PrefixObserverMutationCannotRegressFinalOwnerVersion()
{
const uint otherCell = 0xA9B50001u;
var registry = RegisteredSingle();
var versions = new List<ulong>();
var prefixes = new List<uint>();
bool mutated = false;
registry.OwnerPrefixMembershipChanged += (owner, prefix) =>
{
prefixes.Add(prefix);
if (mutated)
return;
mutated = true;
registry.UpdatePhysicsState(
owner,
(uint)PhysicsStateFlags.Hidden);
};
registry.OwnerMutated += (_, version) => versions.Add(version);
Assert.True(registry.TryPrepareSetPosition(
1u, new Vector3(204f, 12f, 50f), Quaternion.Identity,
otherCell, 192f, 0f, PhysicsShadowCommitAction.Replace,
[otherCell], provenShapeless: false, suspendOwner: false,
out var prepared));
Assert.True(registry.TryApplySetPosition(prepared!, out var receipt));
registry.DispatchSetPositionCommit(receipt);
Assert.Single(versions);
Assert.Equal(registry.GetOwnerVersion(1u), versions[0]);
Assert.Equal([Landblock], prefixes);
}
[Fact]
public void PreparedShapelessRequiresExplicitDispositionAndCreatesNoRows()
{
var registry = new ShadowObjectRegistry();
Assert.False(registry.TryPrepareSetPosition(
77u,
Vector3.One,
Quaternion.Identity,
Cell1,
0f,
0f,
PhysicsShadowCommitAction.Replace,
[Cell1],
provenShapeless: false,
suspendOwner: false,
out _));
Assert.True(registry.TryPrepareSetPosition(
77u,
Vector3.One,
Quaternion.Identity,
Cell1,
0f,
0f,
PhysicsShadowCommitAction.Replace,
[Cell1],
provenShapeless: true,
suspendOwner: false,
out var prepared));
Assert.True(registry.TryApplySetPosition(prepared!, out var receipt));
Assert.False(receipt.Mutated);
Assert.Empty(registry.GetObjectsInCell(Cell1));
Assert.False(registry.TryApplySetPosition(prepared!, out _));
}
[Fact]
public void ClearInvalidatesPreparedUnappliedShapelessCommit()
{
var registry = new ShadowObjectRegistry();
Assert.True(registry.TryPrepareSetPosition(
77u,
Vector3.One,
Quaternion.Identity,
Cell1,
0f,
0f,
PhysicsShadowCommitAction.Replace,
[Cell1],
provenShapeless: true,
suspendOwner: false,
out var prepared));
registry.Clear();
Assert.False(registry.TryApplySetPosition(prepared!, out _));
Assert.Equal(0, registry.PendingSetPositionDispatchCount);
Assert.Empty(registry.GetObjectsInCell(Cell1));
}
[Fact]
public void PreparedCommitRejectsStaleGlobalRevisionWithoutMutation()
{
var registry = RegisteredSingle();
Assert.True(registry.TryPrepareSetPosition(
1u, new Vector3(36f, 12f, 50f), Quaternion.Identity,
Cell9, 0f, 0f, PhysicsShadowCommitAction.Replace, [Cell9],
provenShapeless: false, suspendOwner: false, out var prepared));
registry.Register(
2u, 0x01000002u, new Vector3(13f, 12f, 50f),
Quaternion.Identity, 1f, 0f, 0f, Landblock,
seedCellId: Cell1, isStatic: false);
Vector3 before = registry.GetObjectsInCell(Cell1)
.Single(entry => entry.EntityId == 1u).Position;
Assert.False(registry.TryApplySetPosition(prepared!, out _));
Assert.Equal(before, registry.GetObjectsInCell(Cell1)
.Single(entry => entry.EntityId == 1u).Position);
Assert.Empty(registry.GetObjectsInCell(Cell9));
}
[Fact]
public void PreparedCommitRejectsStaleOwnerVersionWithoutMutation()
{
var registry = RegisteredSingle();
Assert.True(registry.TryPrepareSetPosition(
1u, new Vector3(36f, 12f, 50f), Quaternion.Identity,
Cell9, 0f, 0f, PhysicsShadowCommitAction.Replace, [Cell9],
provenShapeless: false, suspendOwner: false, out var prepared));
registry.UpdatePhysicsState(1u, (uint)PhysicsStateFlags.Hidden);
ShadowEntry before = Assert.Single(registry.GetObjectsInCell(Cell1));
Assert.False(registry.TryApplySetPosition(prepared!, out _));
Assert.Equal(before, Assert.Single(registry.GetObjectsInCell(Cell1)));
Assert.Empty(registry.GetObjectsInCell(Cell9));
}
[Fact]
public void PreparedApplyTailAllocatesZeroManagedBytes()
{
// Warm the generic owner-value and dictionary replacement paths.
var warm = RegisteredSingle();
Assert.True(warm.TryPrepareSetPosition(
1u, new Vector3(36f, 12f, 50f), Quaternion.Identity,
Cell9, 0f, 0f, PhysicsShadowCommitAction.Replace, [Cell9],
provenShapeless: false, suspendOwner: false, out var warmPrepared));
Assert.True(warm.TryApplySetPosition(warmPrepared!, out _));
var registry = RegisteredSingle();
Assert.True(registry.TryPrepareSetPosition(
1u, new Vector3(36f, 12f, 50f), Quaternion.Identity,
Cell9, 0f, 0f, PhysicsShadowCommitAction.Replace, [Cell9],
provenShapeless: false, suspendOwner: false, out var prepared));
long before = GC.GetAllocatedBytesForCurrentThread();
bool applied = registry.TryApplySetPosition(prepared!, out _);
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.True(applied);
Assert.Equal(0, allocated);
}
[Fact]
public void DeferredDispatchRejectsReceiptAfterInterveningOwnerMutation()
{
const uint otherCell = 0xA9B50001u;
var registry = RegisteredSingle();
var versions = new List<ulong>();
var prefixes = new List<uint>();
registry.OwnerMutated += (_, version) => versions.Add(version);
registry.OwnerPrefixMembershipChanged += (_, prefix) =>
prefixes.Add(prefix);
Assert.True(registry.TryPrepareSetPosition(
1u, new Vector3(204f, 12f, 50f), Quaternion.Identity,
otherCell, 192f, 0f, PhysicsShadowCommitAction.Replace,
[otherCell], provenShapeless: false, suspendOwner: false,
out var prepared));
Assert.True(registry.TryApplySetPosition(prepared!, out var receipt));
registry.UpdatePhysicsState(1u, (uint)PhysicsStateFlags.Hidden);
ulong current = registry.GetOwnerVersion(1u);
registry.DispatchSetPositionCommit(receipt);
Assert.Equal([current], versions);
Assert.Empty(prefixes);
}
[Fact]
public void NoneRefreshesFrameWithoutChangingExactMembership()
{

View file

@ -1156,6 +1156,13 @@ public class PlayerMovementControllerTests
candidate.CaptureMovementResult(mouseLookEvent: false));
Assert.Throws<InvalidOperationException>(() => _ = candidate.PhysicsBody);
candidate.BeginDormantSetPositionGroundPhase();
Assert.Throws<InvalidOperationException>(
candidate.CommitRuntimeActivationFrame);
Assert.Throws<InvalidOperationException>(
candidate.ActivateRuntimePublication);
candidate.EndDormantSetPositionGroundPhase();
candidate.ActivateRuntimePublication();
Assert.Same(body, candidate.PhysicsBody);
_ = candidate.CaptureMovementResult(mouseLookEvent: false);

View file

@ -45,6 +45,30 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
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 CommitOwnsExactDormantBodyAndControllerWithoutWorldEdges()
{
@ -115,6 +139,39 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
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);
}
[Fact]
public void DeferredActivationEvaluationLeavesExactOwnedGraphDormantAndRetryable()
{
@ -198,6 +255,752 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
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);
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>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
fixture.Lifetime.Physics.SetPosition.CommitCollisionGeneration(
Cell & 0xFFFF0000u,
generation,
ready: true);
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 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);
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);
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,
projectionRequiresTeleportHook: false,
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()
{
@ -813,6 +1616,7 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
fixture.ActivationPreparation,
out _));
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
@ -820,12 +1624,14 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
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 _));
}
@ -1077,6 +1883,7 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
fixture.ActivationPreparation,
out RuntimeLocalPlayerPhysicsPublicationToken token));
Assert.False(token.IsValid);
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
@ -1095,6 +1902,7 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
fixture.ActivationPreparation,
out _));
}
@ -1111,6 +1919,7 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
fixture.ActivationPreparation,
out _));
}
@ -1360,7 +2169,14 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
internal Fixture(
bool preparePlacement = true,
bool residentWorld = false)
bool residentWorld = false,
RuntimeLocalPlayerShadowDisposition shadowDisposition =
RuntimeLocalPlayerShadowDisposition.ProvenShapeless,
float terrainHeight = 0f,
Vector3? initialVelocity = null,
Vector3? initialOmega = null,
float? initialFriction = null,
float? initialElasticity = null)
{
if (residentWorld)
{
@ -1370,7 +2186,9 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
};
engine.AddLandblock(
Cell & 0xFFFF0000u,
new TerrainSurface(new byte[81], new float[256]),
new TerrainSurface(
new byte[81],
Enumerable.Repeat(terrainHeight, 256).ToArray()),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
@ -1390,8 +2208,18 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
Identity);
Movement.AttachPhysicsPublication(Owner);
Record = Lifetime.RegisterEntity(
Spawn(0x70003001u, incarnation: 1)).Canonical!;
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();
}
@ -1403,6 +2231,8 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
internal RuntimeEntityRecord Record { get; }
internal RuntimeEntityPlacementToken Placement { get; private set; }
internal RuntimeSetPositionCommand Command { get; private set; }
internal RuntimeLocalPlayerPhysicsActivationPreparation
ActivationPreparation { get; }
internal void RepreparePlacement()
{
@ -1440,6 +2270,7 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
Placement,
Command,
PlayerMovementConstructionOptions.Fallback,
ActivationPreparation,
out RuntimeLocalPlayerPhysicsPublicationToken token));
Assert.Equal(Record.Key, token.Entity);
Assert.Equal(Placement, token.Placement);
@ -1464,7 +2295,50 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
}
}
private static WorldSession.EntitySpawn Spawn(uint guid, ushort incarnation)
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,
@ -1498,12 +2372,12 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
Parent: null,
Children: null,
Scale: 1f,
Friction: null,
Elasticity: null,
Friction: initialFriction,
Elasticity: initialElasticity,
Translucency: null,
Velocity: null,
Velocity: initialVelocity,
Acceleration: null,
AngularVelocity: null,
AngularVelocity: initialOmega,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
@ -1522,6 +2396,8 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
MotionTableId: 0x09000001u,
PhysicsState: physics.RawState,
ObjectDescriptionFlags: 0x8u,
Friction: initialFriction,
Elasticity: initialElasticity,
InstanceSequence: incarnation,
MovementSequence: 1,
ServerControlSequence: 1,

View file

@ -13,6 +13,604 @@ public sealed class RuntimeCollisionReportingStateTests
private const uint Landblock = 0xA9B40000u;
private const uint Cell = Landblock | 0x0001u;
[Fact]
public void PreparedBatchDefersTrackingUntilOrderedDispatchAndDispatchesOnce()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime,
0x70001F01u,
1,
PhysicsStateFlags.ReportCollisions);
RuntimeEntityRecord target = Entity(
lifetime,
0x70001F02u,
1,
PhysicsStateFlags.ReportCollisions);
target.PhysicsBody!.TransientState |= TransientStateFlags.Contact;
RegisterDynamicShadow(lifetime, target);
var observer = new CollisionObserver();
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
physicsTime: 10d,
previousContact: false,
previousOnWalkable: false,
finalOnWalkable: false,
collidedWithEnvironment: false,
[target.Key!.Value.LocalEntityId],
out var prepared));
Assert.NotNull(prepared);
Assert.Empty(observer.Reports);
Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership()
.TrackedObjectCount);
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
prepared!,
out var receipt));
Assert.Empty(observer.Reports);
Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership()
.TrackedObjectCount);
Assert.Equal(1, lifetime.Physics.CaptureOwnership()
.PendingCollisionSetPositionDispatchCount);
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
Assert.Equal(2, observer.Reports.Count);
Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership()
.TrackedObjectCount);
Assert.False(observer.Reports[0].RecipientWasInContact);
Assert.True(observer.Reports[0].OtherWasInContact);
Assert.True(observer.Reports[1].RecipientWasInContact);
Assert.False(observer.Reports[1].OtherWasInContact);
Assert.Equal(0, lifetime.Physics.CaptureOwnership()
.PendingCollisionSetPositionDispatchCount);
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
Assert.Equal(2, observer.Reports.Count);
}
[Fact]
public void CombinedPhysicsLedgerIncludesPendingShadowSetPositionReceipt()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime, 0x70001F03u, 1, PhysicsStateFlags.None);
RegisterDynamicShadow(lifetime, owner);
ShadowObjectRegistry shadows = lifetime.Physics.Engine.ShadowObjects;
Assert.True(shadows.TryPrepareSetPosition(
owner.Key!.Value.LocalEntityId,
new Vector3(13f, 12f, 7f),
Quaternion.Identity,
Cell,
0f,
0f,
PhysicsShadowCommitAction.Replace,
[Cell],
provenShapeless: false,
suspendOwner: false,
out var prepared));
Assert.True(shadows.TryApplySetPosition(prepared!, out var receipt));
Assert.Equal(1, lifetime.Physics.CaptureOwnership()
.PendingShadowSetPositionDispatchCount);
Assert.True(shadows.DiscardSetPositionCommit(receipt));
Assert.Equal(0, lifetime.Physics.CaptureOwnership()
.PendingShadowSetPositionDispatchCount);
}
[Fact]
public void TwoOwnerPreparedBatchesDispatchExactlyOnceInReverseOrder()
{
using var lifetime = Lifetime();
RuntimeEntityRecord first = Entity(
lifetime, 0x70001E01u, 1, PhysicsStateFlags.ReportCollisions);
RuntimeEntityRecord second = Entity(
lifetime, 0x70001E02u, 1, PhysicsStateFlags.ReportCollisions);
const uint staticId = 0x00F01E01u;
RegisterShadow(lifetime, staticId, PhysicsStateFlags.Static,
isStatic: true);
var observer = new CollisionObserver();
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
first, first.PhysicsBody!, 10d, false, false, false, false,
[staticId], out var firstPrepared));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
firstPrepared!, out var firstReceipt));
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
second, second.PhysicsBody!, 10d, false, false, false, false,
[staticId], out var secondPrepared));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
secondPrepared!, out var secondReceipt));
Assert.True(lifetime.Physics.CollisionReports.DispatchSetPositionBatch(
secondReceipt));
Assert.True(lifetime.Physics.CollisionReports.DispatchSetPositionBatch(
firstReceipt));
Assert.False(lifetime.Physics.CollisionReports.DispatchSetPositionBatch(
secondReceipt));
Assert.False(lifetime.Physics.CollisionReports.DispatchSetPositionBatch(
firstReceipt));
Assert.Equal(2, observer.Reports.Count);
Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership()
.PendingSetPositionDispatchCount);
}
[Fact]
public void OldBatchRetirementCannotRemoveBatchInstalledByItsCallback()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime, 0x70001E03u, 1, PhysicsStateFlags.ReportCollisions);
const uint staticId = 0x00F01E03u;
RegisterShadow(lifetime, staticId, PhysicsStateFlags.Static,
isStatic: true);
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner, owner.PhysicsBody!, 10d, false, false, false, false,
[staticId], out var oldPrepared));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
oldPrepared!, out var oldReceipt));
RuntimeCollisionReportingState.SetPositionCollisionBatchReceipt
newerReceipt = default;
bool installedNewer = false;
var observer = new CollisionObserver(ignoredReport =>
{
if (installedNewer)
return;
installedNewer = true;
Assert.True(lifetime.Physics.CollisionReports
.TryPrepareSetPositionBatch(
owner, owner.PhysicsBody!, 11d, false, false, false, true,
ImmutableArray<uint>.Empty, out var newerPrepared));
Assert.True(lifetime.Physics.CollisionReports
.TryInstallSetPositionBatch(newerPrepared!, out newerReceipt));
_ = lifetime.Physics.CollisionReports.DispatchSetPositionBatch(
newerReceipt);
_ = ignoredReport;
});
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
Assert.True(lifetime.Physics.CollisionReports.DispatchSetPositionBatch(
oldReceipt));
Assert.True(installedNewer);
Assert.True(newerReceipt.IsValid);
Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership()
.OwnerCount);
lifetime.Physics.CollisionReports.RetireSetPositionBatchOwner(
oldReceipt);
Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership()
.OwnerCount);
lifetime.Physics.CollisionReports.RetireSetPositionBatchOwner(
newerReceipt);
Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership()
.OwnerCount);
}
[Fact]
public void PreparedBatchTracksHiddenIgnoredPeerButSuppressesCallbacks()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime,
0x70001F11u,
1,
PhysicsStateFlags.ReportCollisions);
RuntimeEntityRecord target = Entity(
lifetime,
0x70001F12u,
1,
PhysicsStateFlags.ReportCollisions);
RegisterDynamicShadow(lifetime, target);
var observer = new CollisionObserver();
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
10d,
previousContact: false,
previousOnWalkable: false,
finalOnWalkable: false,
collidedWithEnvironment: false,
[target.Key!.Value.LocalEntityId],
out var prepared));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
prepared!,
out var receipt));
PhysicsStateFlags hidden = PhysicsStateFlags.Hidden
| PhysicsStateFlags.IgnoreCollisions;
lifetime.Entities.SetFinalPhysicsState(target, hidden);
target.PhysicsBody!.State = hidden;
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
Assert.Empty(observer.Reports);
Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership()
.TrackedObjectCount);
}
[Fact]
public void PreparedBatchConvertsLiveReportAsEnvironmentAfterGroundEdge()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime,
0x70001F21u,
1,
PhysicsStateFlags.ReportCollisions);
RuntimeEntityRecord target = Entity(
lifetime,
0x70001F22u,
1,
PhysicsStateFlags.ReportCollisions);
RegisterDynamicShadow(lifetime, target);
var observer = new CollisionObserver();
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
10d,
previousContact: false,
previousOnWalkable: false,
finalOnWalkable: false,
collidedWithEnvironment: false,
[target.Key!.Value.LocalEntityId],
out var prepared));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
prepared!,
out var receipt));
lifetime.Entities.SetFinalPhysicsState(
target,
PhysicsStateFlags.ReportAsEnvironment);
target.PhysicsBody!.State = PhysicsStateFlags.ReportAsEnvironment;
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
Assert.Single(observer.Reports);
Assert.Equal(RuntimeCollisionReportKind.EnvironmentCollision,
observer.Reports[0].Kind);
Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership()
.TrackedObjectCount);
}
[Fact]
public void PreparedEnvironmentBatchClearsCapturedMissileWithoutReport()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime,
0x70001F31u,
1,
PhysicsStateFlags.Missile | PhysicsStateFlags.AlignPath);
RegisterDynamicShadow(lifetime, owner);
const uint staticId = 0x00F01F31u;
RegisterShadow(
lifetime,
staticId,
PhysicsStateFlags.Static,
isStatic: true);
var observer = new CollisionObserver();
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
10d,
previousContact: false,
previousOnWalkable: false,
finalOnWalkable: false,
collidedWithEnvironment: false,
[staticId],
out var prepared));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
prepared!,
out var receipt));
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
Assert.Empty(observer.Reports);
Assert.False(owner.FinalPhysicsState.HasFlag(PhysicsStateFlags.Missile));
Assert.False(owner.PhysicsBody!.State.HasFlag(PhysicsStateFlags.Missile));
}
[Fact]
public void PreparedExpiredEndDegradesToMissingTargetAfterInstall()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime,
0x70001F41u,
1,
PhysicsStateFlags.ReportCollisions);
RuntimeEntityRecord target = Entity(
lifetime,
0x70001F42u,
1,
PhysicsStateFlags.ReportCollisions);
target.PhysicsBody!.TransientState |= TransientStateFlags.Contact;
RegisterDynamicShadow(lifetime, target);
var observer = new CollisionObserver();
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
10d,
false,
false,
false,
false,
[target.Key!.Value.LocalEntityId],
out var start));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
start!,
out var startReceipt));
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(startReceipt);
observer.Reports.Clear();
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
11.0001d,
false,
false,
false,
false,
ImmutableArray<uint>.Empty,
out var end));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
end!,
out var endReceipt));
Assert.True(lifetime.TryAcceptDelete(
new DeleteObject.Parsed(target.ServerGuid, target.Incarnation),
isLocalPlayer: false,
removeRetainedObject: false,
out RuntimeEntityDeleteAcceptance acceptance));
lifetime.CompleteAcceptedDelete(acceptance);
Assert.Null(lifetime.RetireCanonicalOnly(target));
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(endReceipt);
RuntimeCollisionReport report = Assert.Single(observer.Reports);
Assert.Equal(RuntimeCollisionReportKind.ObjectCollisionEnd, report.Kind);
Assert.Equal(owner.Key, report.Recipient);
Assert.Equal(target.ServerGuid, report.OtherServerGuid);
Assert.False(report.OtherWasInContact);
}
[Fact]
public void PreparedBatchPreservesPreLoopEnvironmentLatchUntilFinalSuffix()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime,
0x70001F51u,
1,
PhysicsStateFlags.ReportCollisions);
RuntimeEntityRecord target = Entity(
lifetime,
0x70001F52u,
1,
PhysicsStateFlags.ReportCollisions);
RegisterDynamicShadow(lifetime, target);
var observer = new CollisionObserver();
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
Assert.True(lifetime.Physics.CollisionReports.HandleReports(
owner,
owner.PhysicsBody!,
9d,
previousContact: false,
previousOnWalkable: false,
collidedWithEnvironment: true,
ImmutableArray<uint>.Empty));
observer.Reports.Clear();
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
10d,
previousContact: false,
previousOnWalkable: false,
finalOnWalkable: false,
collidedWithEnvironment: false,
[target.Key!.Value.LocalEntityId],
out var prepared));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
prepared!,
out var receipt));
lifetime.Entities.SetFinalPhysicsState(
target,
PhysicsStateFlags.ReportAsEnvironment);
target.PhysicsBody!.State = PhysicsStateFlags.ReportAsEnvironment;
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
Assert.Empty(observer.Reports);
Assert.Equal(1, lifetime.Physics.CollisionReports.CaptureOwnership()
.TrackedObjectCount);
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
11d,
previousContact: false,
previousOnWalkable: false,
finalOnWalkable: false,
collidedWithEnvironment: true,
ImmutableArray<uint>.Empty,
out var next));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
next!,
out var nextReceipt));
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(nextReceipt);
RuntimeCollisionReport report = Assert.Single(observer.Reports);
Assert.Equal(RuntimeCollisionReportKind.EnvironmentCollision,
report.Kind);
}
[Fact]
public void PreparedEnvironmentBatchStopsMissileAddedByCallback()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime,
0x70001F61u,
1,
PhysicsStateFlags.ReportCollisions);
RegisterDynamicShadow(lifetime, owner);
const uint staticId = 0x00F01F61u;
RegisterShadow(
lifetime,
staticId,
PhysicsStateFlags.Static,
isStatic: true);
var observer = new CollisionObserver(_ =>
{
PhysicsStateFlags callbackState = owner.PhysicsBody!.State
| PhysicsStateFlags.Missile
| PhysicsStateFlags.AlignPath;
lifetime.Entities.SetFinalPhysicsState(owner, callbackState);
owner.PhysicsBody.State = callbackState;
});
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
10d,
previousContact: false,
previousOnWalkable: false,
finalOnWalkable: false,
collidedWithEnvironment: false,
[staticId],
out var prepared));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
prepared!,
out var receipt));
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
Assert.Single(observer.Reports);
Assert.False(owner.FinalPhysicsState.HasFlag(PhysicsStateFlags.Missile));
Assert.False(owner.PhysicsBody!.State.HasFlag(PhysicsStateFlags.Missile));
}
[Fact]
public void PreparedExistingCollisionRefreshesLiveEtherealBeforeExpiry()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime,
0x70001F71u,
1,
PhysicsStateFlags.ReportCollisions);
RuntimeEntityRecord target = Entity(
lifetime,
0x70001F72u,
1,
PhysicsStateFlags.ReportCollisions);
RegisterDynamicShadow(lifetime, target);
var observer = new CollisionObserver();
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
DispatchPrepared(
lifetime,
owner,
physicsTime: 10d,
[target.Key!.Value.LocalEntityId]);
observer.Reports.Clear();
PhysicsStateFlags ethereal = PhysicsStateFlags.ReportCollisions
| PhysicsStateFlags.Ethereal;
lifetime.Entities.SetFinalPhysicsState(target, ethereal);
target.PhysicsBody!.State = ethereal;
DispatchPrepared(
lifetime,
owner,
physicsTime: 10.5d,
[target.Key!.Value.LocalEntityId]);
Assert.Empty(observer.Reports);
DispatchPrepared(
lifetime,
owner,
physicsTime: 10.5001d,
ImmutableArray<uint>.Empty);
Assert.Equal(2, observer.Reports.Count);
Assert.All(observer.Reports, report => Assert.Equal(
RuntimeCollisionReportKind.ObjectCollisionEnd,
report.Kind));
}
[Fact]
public void PreparedExistingCollisionBecomingStaticKeepsOldAgeForSamePassEnd()
{
using var lifetime = Lifetime();
RuntimeEntityRecord owner = Entity(
lifetime,
0x70001F81u,
1,
PhysicsStateFlags.ReportCollisions);
RuntimeEntityRecord target = Entity(
lifetime,
0x70001F82u,
1,
PhysicsStateFlags.ReportCollisions);
RegisterDynamicShadow(lifetime, target);
var observer = new CollisionObserver();
using IDisposable subscription = lifetime.Physics.CollisionReports
.Subscribe(observer);
DispatchPrepared(
lifetime,
owner,
physicsTime: 10d,
[target.Key!.Value.LocalEntityId]);
observer.Reports.Clear();
Assert.True(lifetime.Physics.CollisionReports.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
11.0001d,
previousContact: false,
previousOnWalkable: false,
finalOnWalkable: false,
collidedWithEnvironment: false,
[target.Key!.Value.LocalEntityId],
out var prepared));
Assert.True(lifetime.Physics.CollisionReports.TryInstallSetPositionBatch(
prepared!,
out var receipt));
PhysicsStateFlags becameStatic = PhysicsStateFlags.ReportCollisions
| PhysicsStateFlags.Static;
lifetime.Entities.SetFinalPhysicsState(target, becameStatic);
target.PhysicsBody!.State = becameStatic;
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
Assert.Equal(3, observer.Reports.Count);
Assert.Equal(RuntimeCollisionReportKind.EnvironmentCollision,
observer.Reports[0].Kind);
Assert.Equal(RuntimeCollisionReportKind.ObjectCollisionEnd,
observer.Reports[1].Kind);
Assert.Equal(RuntimeCollisionReportKind.ObjectCollisionEnd,
observer.Reports[2].Kind);
Assert.Equal(0, lifetime.Physics.CollisionReports.CaptureOwnership()
.TrackedObjectCount);
}
[Fact]
public void ExactObjectReportsAreOrderedDeduplicatedAndExpireAtRetailThreshold()
{
@ -1656,6 +2254,28 @@ public sealed class RuntimeCollisionReportingStateTests
owner.PhysicsBody.OnWalkable,
collision);
private static void DispatchPrepared(
RuntimeEntityObjectLifetime lifetime,
RuntimeEntityRecord owner,
double physicsTime,
ImmutableArray<uint> collidedObjectIds)
{
Assert.True(lifetime.Physics.CollisionReports
.TryPrepareSetPositionBatch(
owner,
owner.PhysicsBody!,
physicsTime,
previousContact: owner.PhysicsBody!.InContact,
previousOnWalkable: owner.PhysicsBody.OnWalkable,
finalOnWalkable: owner.PhysicsBody.OnWalkable,
collidedWithEnvironment: false,
collidedObjectIds,
out var prepared));
Assert.True(lifetime.Physics.CollisionReports
.TryInstallSetPositionBatch(prepared!, out var receipt));
lifetime.Physics.CollisionReports.DispatchSetPositionBatch(receipt);
}
private static RuntimeSetPositionCommand PlacementCommand(
RuntimeEntityRecord owner,
Vector3 position)