feat(runtime): commit dormant SetPosition activation
This commit is contained in:
parent
99f867f053
commit
5785a07b3e
13 changed files with 4674 additions and 111 deletions
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue