acdream/tests/AcDream.Runtime.Tests/Gameplay/RuntimeLocalPlayerPhysicsPublicationStateTests.cs

1779 lines
72 KiB
C#

using System.Collections.Immutable;
using System.Numerics;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics;
namespace AcDream.Runtime.Tests.Gameplay;
public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
{
private const uint Cell = 0xA9B40021u;
private const uint SetupId = 0x02000001u;
[Fact]
public void PreparationOwnsPrivateBodyAndMutatesNoCanonicalOrSharedState()
{
using var fixture = new Fixture();
uint fullCell = fixture.Record.FullCellId;
ulong spatial = fixture.Record.SpatialAuthorityVersion;
ulong physicsEpoch = fixture.Record.PhysicsOwnershipEpoch;
ulong clockEpoch = fixture.Record.ObjectClockEpoch;
RuntimePhysicsOwnershipSnapshot physics = fixture.Lifetime.Physics
.CaptureOwnership();
RuntimeLocalPlayerPhysicsPublicationToken token = fixture.Prepare();
Assert.Null(fixture.Record.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
Assert.Equal(fullCell, fixture.Record.FullCellId);
Assert.Equal(spatial, fixture.Record.SpatialAuthorityVersion);
Assert.Equal(physicsEpoch, fixture.Record.PhysicsOwnershipEpoch);
Assert.Equal(clockEpoch, fixture.Record.ObjectClockEpoch);
Assert.Null(fixture.Record.PhysicsHost);
Assert.Equal(physics.SpatialRootCount,
fixture.Lifetime.Physics.CaptureOwnership().SpatialRootCount);
Assert.Equal(physics.RetainedShadowRegistrationCount,
fixture.Lifetime.Physics.CaptureOwnership()
.RetainedShadowRegistrationCount);
Assert.Equal(1, fixture.Owner.CaptureOwnership().CandidateCount);
Assert.True(token.IsValid);
}
[Fact]
public void CommitOwnsExactDormantBodyAndControllerWithoutWorldEdges()
{
using var fixture = new Fixture();
RuntimeLocalPlayerPhysicsPublicationToken token = fixture.Prepare();
Assert.True(fixture.Owner.TryCaptureCandidateSnapshot(
token,
out RuntimeLocalPlayerPhysicsCandidateSnapshot prepared));
uint fullCell = fixture.Record.FullCellId;
ulong spatial = fixture.Record.SpatialAuthorityVersion;
ulong bodyEpoch = fixture.Record.PhysicsOwnershipEpoch;
ulong controllerEpoch = fixture.Movement.ControllerOwnershipEpoch;
double clockPending = fixture.Record.ObjectClock.PendingSeconds;
bool clockActive = fixture.Record.ObjectClock.IsActive;
object? currentCell = fixture.Lifetime.Physics.DataCache.CellGraph.CurrCell;
RuntimeLocalPlayerPhysicsPublicationStatus status = fixture.Owner
.Commit(token);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed, status);
PlayerMovementController candidate = Assert.IsType<PlayerMovementController>(
fixture.Movement.Controller);
PhysicsBody body = Assert.IsType<PhysicsBody>(fixture.Record.PhysicsBody);
Assert.Same(body, fixture.Record.PhysicsBody);
Assert.Same(candidate, fixture.Movement.Controller);
Assert.True(candidate.OwnsPhysicsBody(body));
Assert.Equal(bodyEpoch + 1UL, fixture.Record.PhysicsOwnershipEpoch);
Assert.Equal(controllerEpoch + 1UL,
fixture.Movement.ControllerOwnershipEpoch);
Assert.Equal(fullCell, fixture.Record.FullCellId);
Assert.Equal(spatial, fixture.Record.SpatialAuthorityVersion);
Assert.Same(currentCell,
fixture.Lifetime.Physics.DataCache.CellGraph.CurrCell);
Assert.Equal(clockPending, fixture.Record.ObjectClock.PendingSeconds);
Assert.Equal(clockActive, fixture.Record.ObjectClock.IsActive);
Assert.Equal(prepared.Position, body.Position);
Assert.Equal(prepared.Orientation, body.Orientation);
Assert.Equal(prepared.CellId, body.CellPosition.ObjCellId);
Assert.Equal(prepared.CellLocalPosition,
body.CellPosition.Frame.Origin);
Assert.Equal(prepared.State, body.State);
Assert.Equal(prepared.TransientState, body.TransientState);
Assert.Equal(prepared.InWorld, body.InWorld);
Assert.False(body.InWorld);
Assert.False((body.TransientState & TransientStateFlags.Active) != 0);
Assert.Null(fixture.Record.PhysicsHost);
Assert.Equal(0, fixture.Lifetime.Physics.CaptureOwnership()
.SpatialRootCount);
Assert.Equal(0, fixture.Lifetime.Physics.CaptureOwnership()
.RetainedShadowRegistrationCount);
Assert.True(fixture.Lifetime.Physics.SetPosition
.IsExactPreparedPlacementCurrent(
fixture.Record,
fixture.Placement,
fixture.Command));
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
AssertNotLive(candidate);
Assert.Equal(prepared.Position, body.Position);
Assert.Equal(prepared.Orientation, body.Orientation);
Assert.Equal(prepared.State, body.State);
Assert.Equal(prepared.TransientState, body.TransientState);
Assert.Equal(clockPending, fixture.Record.ObjectClock.PendingSeconds);
Assert.Equal(clockActive, fixture.Record.ObjectClock.IsActive);
Assert.Same(currentCell,
fixture.Lifetime.Physics.DataCache.CellGraph.CurrCell);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedToken,
fixture.Owner.Commit(token));
}
[Fact]
public void DeferredActivationEvaluationLeavesExactOwnedGraphDormantAndRetryable()
{
using var fixture = new Fixture();
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
PhysicsBody body = fixture.Record.PhysicsBody!;
Vector3 position = body.Position;
Quaternion orientation = body.Orientation;
EvaluationPuritySnapshot before = CaptureEvaluationPurity(fixture);
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell,
fixture.Owner.EvaluateActivation(token, out var receipt));
AssertEvaluationPurity(before, CaptureEvaluationPurity(fixture));
Assert.True(receipt.IsValid);
Assert.True(fixture.Owner.IsEvaluationCurrent(receipt));
Assert.False(body.InWorld);
Assert.False(body.TransientState.HasFlag(TransientStateFlags.Active));
Assert.Equal(position, body.Position);
Assert.Equal(orientation, body.Orientation);
Assert.Equal(Cell, fixture.Record.FullCellId);
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
Assert.Equal(0, fixture.Lifetime.Physics.CaptureOwnership()
.RetainedShadowRegistrationCount);
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
AssertNotLive(fixture.Movement.Controller!);
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell,
fixture.Owner.EvaluateActivation(token, out var retry));
AssertEvaluationPurity(before, CaptureEvaluationPurity(fixture));
Assert.True(retry.EvaluationId > receipt.EvaluationId);
}
[Fact]
public void CommittedEvaluationReceiptRemainsPureAndBindsExactDormantAuthority()
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
PhysicsBody body = fixture.Record.PhysicsBody!;
ulong clockEpoch = fixture.Record.ObjectClockEpoch;
ulong placementCommit = fixture.Record.PlacementCommitVersion;
uint fullCell = fixture.Record.FullCellId;
Vector3 bodyPosition = body.Position;
Quaternion bodyOrientation = body.Orientation;
Assert.Equal(fixture.Record.Key, token.Entity);
Assert.Equal(fixture.Record.PhysicsOwnershipEpoch,
token.PhysicsOwnershipEpoch);
Assert.Equal(fixture.Record.ObjectClockEpoch, token.ObjectClockEpoch);
Assert.Equal(fixture.Movement.ControllerOwnershipEpoch,
token.ControllerOwnershipEpoch);
EvaluationPuritySnapshot before = CaptureEvaluationPurity(fixture);
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var receipt));
AssertEvaluationPurity(before, CaptureEvaluationPurity(fixture));
Assert.True(receipt.Placement.Result.IsCommitted);
Assert.True(fixture.Owner.IsEvaluationCurrent(receipt));
Assert.False(body.InWorld);
Assert.False(body.TransientState.HasFlag(TransientStateFlags.Active));
Assert.Equal(bodyPosition, body.Position);
Assert.Equal(bodyOrientation, body.Orientation);
Assert.Equal(fullCell, fixture.Record.FullCellId);
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
Assert.Equal(clockEpoch, fixture.Record.ObjectClockEpoch);
Assert.Equal(placementCommit, fixture.Record.PlacementCommitVersion);
Assert.Equal(0, fixture.Lifetime.Physics.CaptureOwnership()
.RetainedShadowRegistrationCount);
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
Assert.False(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
out _));
AssertNotLive(fixture.Movement.Controller!);
}
[Fact]
public void RejectedPlacementReceiptIsPureAndRetryableUnderSameLease()
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
PhysicsBody body = fixture.Record.PhysicsBody!;
Vector3 position = body.Position;
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
static (_, _, _, _) => TransitionState.Collided;
EvaluationPuritySnapshot before = CaptureEvaluationPurity(fixture);
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.RejectedPlacement,
fixture.Owner.EvaluateActivation(token, out var rejected));
AssertEvaluationPurity(before, CaptureEvaluationPurity(fixture));
Assert.True(rejected.IsValid);
Assert.True(fixture.Owner.IsEvaluationCurrent(rejected));
Assert.False(body.InWorld);
Assert.Equal(position, body.Position);
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook = null;
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var retry));
AssertEvaluationPurity(before, CaptureEvaluationPurity(fixture));
Assert.True(fixture.Owner.IsEvaluationCurrent(retry));
Assert.False(fixture.Owner.IsEvaluationCurrent(rejected));
}
[Fact]
public void StaleActivationReceiptCannotPublishAndNextEvaluationRetiresDormantOwners()
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var receipt));
fixture.Lifetime.Entities.SetFinalPhysicsState(
fixture.Record,
fixture.Record.FinalPhysicsState | PhysicsStateFlags.Frozen);
Assert.False(fixture.Owner.IsEvaluationCurrent(receipt));
PhysicsBody staleBody = fixture.Record.PhysicsBody!;
Assert.False(staleBody.InWorld);
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
Assert.False(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
out _));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.RejectedAuthority,
fixture.Owner.EvaluateActivation(token, out _));
Assert.Null(fixture.Record.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
Assert.Equal(Cell, fixture.Record.FullCellId);
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
Assert.Equal(0, fixture.Lifetime.Physics.CaptureOwnership()
.RetainedShadowRegistrationCount);
Assert.Equal(0, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
}
[Fact]
public void CollisionAdmissionRejectsEvaluationUntilReplacementCommits()
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var receipt));
PhysicsBody body = fixture.Record.PhysicsBody!;
RuntimeCollisionAdmission admission = fixture.Lifetime.Physics
.BeginCollisionAdmission(Cell & 0xFFFF0000u);
Assert.False(fixture.Owner.IsEvaluationCurrent(receipt));
Assert.False(body.InWorld);
Assert.False(body.TransientState.HasFlag(TransientStateFlags.Active));
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.RejectedAuthority,
fixture.Owner.EvaluateActivation(token, out _));
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
using PreparedLandblockCollisionGeneration prepared = fixture
.Lifetime.Physics.PrepareCollisionGeneration(admission);
fixture.Lifetime.Physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(Cell & 0xFFFF0000u));
Assert.True(CommitPrepared(
fixture.Lifetime.Physics,
admission,
prepared).Committed);
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var replacement));
Assert.True(fixture.Owner.IsEvaluationCurrent(replacement));
}
[Fact]
public void ReentrantCollisionWorldMutationRejectsOtherwiseCurrentEvaluation()
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
bool mutated = false;
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
(_, phase, _, observed) =>
{
if (!mutated
&& phase is TransitionCellCollisionPhase.Environment)
{
mutated = true;
RuntimeCollisionAdmission admission = fixture.Lifetime
.Physics.BeginCollisionAdmission(0x01010000u);
fixture.Lifetime.Physics.CancelCollisionGeneration(
admission);
}
return observed;
};
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.RejectedAuthority,
fixture.Owner.EvaluateActivation(token, out _));
Assert.True(mutated);
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook = null;
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var replacement));
Assert.True(fixture.Owner.IsEvaluationCurrent(replacement));
}
[Fact]
public void ReentrantCollisionGenerationCommitRejectsOtherwiseCurrentEvaluation()
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
RuntimeCollisionAdmission admission = fixture.Lifetime.Physics
.BeginCollisionAdmission(0x01010000u);
using PreparedLandblockCollisionGeneration prepared = fixture
.Lifetime.Physics.PrepareCollisionGeneration(admission);
fixture.Lifetime.Physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(0x01010000u));
bool committed = false;
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
(_, phase, _, observed) =>
{
if (!committed
&& phase is TransitionCellCollisionPhase.Environment)
{
committed = CommitPrepared(
fixture.Lifetime.Physics,
admission,
prepared).Committed;
}
return observed;
};
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.RejectedAuthority,
fixture.Owner.EvaluateActivation(token, out _));
Assert.True(committed);
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook = null;
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var replacement));
Assert.True(fixture.Owner.IsEvaluationCurrent(replacement));
}
[Theory]
[InlineData(ActivationInvalidation.Position)]
[InlineData(ActivationInvalidation.Vector)]
[InlineData(ActivationInvalidation.ObjectDescription)]
[InlineData(ActivationInvalidation.Create)]
[InlineData(ActivationInvalidation.Identity)]
[InlineData(ActivationInvalidation.Body)]
[InlineData(ActivationInvalidation.Controller)]
[InlineData(ActivationInvalidation.Clock)]
[InlineData(ActivationInvalidation.Spatial)]
[InlineData(ActivationInvalidation.Host)]
[InlineData(ActivationInvalidation.Remote)]
[InlineData(ActivationInvalidation.Projectile)]
[InlineData(ActivationInvalidation.PlacementCancellation)]
public void EveryPostOwnershipAuthorityReplacementInvalidatesEvaluation(
ActivationInvalidation invalidation)
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var receipt));
switch (invalidation)
{
case ActivationInvalidation.Position:
Assert.True(fixture.Lifetime.Physics.SetPosition
.BeginAuthoredPlacement(
fixture.Record,
fixture.Record.PositionAuthorityVersion,
RuntimeSetPositionOperationKind.LocalAuthoritative)
.IsValid);
break;
case ActivationInvalidation.Vector:
fixture.Lifetime.Entities.AdvanceVectorAuthority(fixture.Record);
break;
case ActivationInvalidation.ObjectDescription:
fixture.Lifetime.Entities.AdvanceObjDescAuthority(fixture.Record);
break;
case ActivationInvalidation.Create:
fixture.Lifetime.Entities.AdvanceCreateAuthority(fixture.Record);
break;
case ActivationInvalidation.Identity:
fixture.Identity.ServerGuid++;
break;
case ActivationInvalidation.Body:
fixture.Lifetime.Entities.SetPhysicsBody(
fixture.Record,
new PhysicsBody());
break;
case ActivationInvalidation.Controller:
fixture.Movement.Controller = new PlayerMovementController(
new PhysicsEngine());
break;
case ActivationInvalidation.Clock:
fixture.Lifetime.Entities.SuspendObjectClock(fixture.Record);
break;
case ActivationInvalidation.Spatial:
fixture.Lifetime.Entities.SetFullCell(
fixture.Record,
Cell + 1u,
(Cell & 0xFFFF0000u) | 0xFFFFu);
break;
case ActivationInvalidation.Host:
fixture.Lifetime.Entities.SetPhysicsHost(
fixture.Record,
CreatePhysicsHost(fixture.Record.ServerGuid));
break;
case ActivationInvalidation.Remote:
fixture.Lifetime.Entities.SetRemoteMotion(
fixture.Record,
new RemoteMotion());
break;
case ActivationInvalidation.Projectile:
fixture.Lifetime.Entities.SetProjectile(
fixture.Record,
new RuntimeProjectile(
new PhysicsBody(),
new ProjectileCollisionSphere(
Vector3.Zero,
0.1f)));
break;
case ActivationInvalidation.PlacementCancellation:
Assert.True(fixture.Lifetime.Physics.SetPosition.Cancel(
fixture.Record,
publishWithdrawal: false));
break;
default:
throw new ArgumentOutOfRangeException(nameof(invalidation));
}
Assert.False(fixture.Owner.IsEvaluationCurrent(receipt));
Assert.False(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
Assert.False(fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
out _));
}
[Fact]
public void ShadowInsertMoveStateSuspendAndRemoveInvalidateReceipts()
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var emptyWorld));
Assert.True(fixture.Owner.IsEvaluationCurrent(emptyWorld));
uint queriedCell = emptyWorld.Placement.Result.QueriedCellIds[0];
const uint ownerId = 0x7000F001u;
Vector3 far = new(100f, 100f, 0f);
fixture.Lifetime.Physics.Engine.ShadowObjects.Register(
ownerId,
SetupId,
far,
Quaternion.Identity,
0.25f,
0f,
0f,
queriedCell & 0xFFFF0000u,
ShadowCollisionType.Cylinder,
cylHeight: 1f,
seedCellId: queriedCell,
isStatic: false);
Assert.False(fixture.Owner.IsEvaluationCurrent(emptyWorld));
RuntimeLocalPlayerPhysicsActivationReceipt current = Reevaluate();
fixture.Lifetime.Physics.Engine.ShadowObjects.UpdatePosition(
ownerId,
far + Vector3.One,
Quaternion.Identity,
0f,
0f,
queriedCell & 0xFFFF0000u,
queriedCell);
Assert.False(fixture.Owner.IsEvaluationCurrent(current));
current = Reevaluate();
fixture.Lifetime.Physics.Engine.ShadowObjects.UpdatePhysicsState(
ownerId,
(uint)PhysicsStateFlags.Ethereal);
Assert.False(fixture.Owner.IsEvaluationCurrent(current));
current = Reevaluate();
Assert.True(fixture.Lifetime.Physics.Engine.ShadowObjects.Suspend(
ownerId));
Assert.False(fixture.Owner.IsEvaluationCurrent(current));
current = Reevaluate();
fixture.Lifetime.Physics.Engine.ShadowObjects.Deregister(ownerId);
Assert.False(fixture.Owner.IsEvaluationCurrent(current));
RuntimeLocalPlayerPhysicsActivationReceipt Reevaluate()
{
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var next));
Assert.True(fixture.Owner.IsEvaluationCurrent(next));
return next;
}
}
[Theory]
[InlineData(RestrictionObjectMutation.RemoveHouseObject)]
[InlineData(RestrictionObjectMutation.HouseOwnerProperty)]
[InlineData(RestrictionObjectMutation.HouseGuestList)]
[InlineData(RestrictionObjectMutation.MoverMonarch)]
public void RestrictionObjectMutationInvalidatesEvaluatedReceipt(
RestrictionObjectMutation mutation)
{
using var fixture = new Fixture(residentWorld: true);
SeedRestrictionObjects(fixture);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var receipt));
ApplyRestrictionObjectMutation(fixture, mutation);
Assert.False(fixture.Owner.IsEvaluationCurrent(receipt));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var replacement));
Assert.True(fixture.Owner.IsEvaluationCurrent(replacement));
}
[Theory]
[InlineData(RestrictionObjectMutation.RemoveHouseObject)]
[InlineData(RestrictionObjectMutation.HouseOwnerProperty)]
[InlineData(RestrictionObjectMutation.HouseGuestList)]
[InlineData(RestrictionObjectMutation.MoverMonarch)]
public void ReentrantRestrictionObjectMutationAbortsEvaluation(
RestrictionObjectMutation mutation)
{
using var fixture = new Fixture(residentWorld: true);
SeedRestrictionObjects(fixture);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
bool mutated = false;
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
(_, phase, _, observed) =>
{
if (!mutated
&& phase is TransitionCellCollisionPhase.Environment)
{
mutated = true;
ApplyRestrictionObjectMutation(fixture, mutation);
}
return observed;
};
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.RejectedAuthority,
fixture.Owner.EvaluateActivation(token, out _));
Assert.True(mutated);
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook = null;
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var replacement));
Assert.True(fixture.Owner.IsEvaluationCurrent(replacement));
}
[Fact]
public void ObjectTableNullFreshAndEqualRevisionAbaInvalidateReceipt()
{
using var fixture = new Fixture(residentWorld: true);
SeedRestrictionObjects(fixture);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var originalReceipt));
ClientObjectTable original = fixture.Lifetime.Objects;
fixture.Lifetime.Physics.Engine.Objects = null;
Assert.False(fixture.Owner.IsEvaluationCurrent(originalReceipt));
// Returning to the same reference/revision is an ABA-shaped binding
// replacement. The engine binding authority keeps the old receipt
// stale even though object identity and mutation revision match again.
fixture.Lifetime.Physics.Engine.Objects = original;
Assert.False(fixture.Owner.IsEvaluationCurrent(originalReceipt));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var reboundReceipt));
var fresh = new ClientObjectTable();
fresh.AddOrUpdate(new ClientObject { ObjectId = 0x70003F10u });
fresh.AddOrUpdate(new ClientObject { ObjectId = 0x70003F11u });
Assert.Equal(original.MutationRevision, fresh.MutationRevision);
fixture.Lifetime.Physics.Engine.Objects = fresh;
Assert.False(fixture.Owner.IsEvaluationCurrent(reboundReceipt));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var freshReceipt));
Assert.True(fixture.Owner.IsEvaluationCurrent(freshReceipt));
}
[Fact]
public void HouseRestrictionGuestsAreAnImmutableInputSnapshot()
{
var sourceGuests = new Dictionary<uint, uint>();
var restrictions = new HouseRestrictionRecord(
OpenToPublic: false,
AllegianceMonarchId: 0u,
Guests: sourceGuests);
var table = new ClientObjectTable();
var house = new ClientObject
{
ObjectId = RestrictionObjectId,
Restrictions = restrictions,
};
table.AddOrUpdate(house);
ulong retainedRevision = table.MutationRevision;
sourceGuests[0x70003F20u] = 1u;
Assert.False(restrictions.IsAllowedIn(0x70003F20u, 0u));
if (restrictions.Guests is IDictionary<uint, uint> dictionaryView)
{
Assert.Throws<NotSupportedException>(
() => dictionaryView[0x70003F20u] = 1u);
}
Assert.False(restrictions.IsAllowedIn(0x70003F20u, 0u));
Assert.Equal(retainedRevision, table.MutationRevision);
}
[Fact]
public void RemovalAndClearUnbindDirectRestrictionMutationAuthority()
{
var table = new ClientObjectTable();
var removed = new ClientObject { ObjectId = RestrictionObjectId };
table.AddOrUpdate(removed);
Assert.True(table.Remove(removed.ObjectId));
ulong removedRevision = table.MutationRevision;
removed.HouseOwnerId = 0x70003F21u;
Assert.Equal(removedRevision, table.MutationRevision);
var cleared = new ClientObject { ObjectId = 0x70003F22u };
table.AddOrUpdate(cleared);
table.Clear();
ulong clearedRevision = table.MutationRevision;
cleared.MonarchId = 0x70003F23u;
Assert.Equal(clearedRevision, table.MutationRevision);
}
[Fact]
public void AddOrUpdateReplacementTransfersDirectMutationAuthority()
{
var table = new ClientObjectTable();
var displaced = new ClientObject { ObjectId = RestrictionObjectId };
var replacement = new ClientObject { ObjectId = RestrictionObjectId };
table.AddOrUpdate(displaced);
table.AddOrUpdate(replacement);
ulong replacementRevision = table.MutationRevision;
displaced.MonarchId = 0x70003F24u;
Assert.Equal(replacementRevision, table.MutationRevision);
replacement.MonarchId = 0x70003F25u;
Assert.True(table.MutationRevision > replacementRevision);
}
[Fact]
public void SharedRetainedObjectNotifiesEveryOwningTable()
{
var first = new ClientObjectTable();
var second = new ClientObjectTable();
var shared = new ClientObject { ObjectId = RestrictionObjectId };
first.AddOrUpdate(shared);
second.AddOrUpdate(shared);
ulong firstRevision = first.MutationRevision;
ulong secondRevision = second.MutationRevision;
shared.HouseOwnerId = 0x70003F26u;
Assert.True(first.MutationRevision > firstRevision);
Assert.True(second.MutationRevision > secondRevision);
}
[Theory]
[InlineData(ReentrantActivationInvalidation.Reset)]
[InlineData(ReentrantActivationInvalidation.DeleteAndGuidReuse)]
public void ReentrantInvalidationDuringCoreEvaluationRetiresLease(
ReentrantActivationInvalidation invalidation)
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
RuntimeEntityRecord? replacement = null;
bool invalidated = false;
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
(_, phase, _, observed) =>
{
if (invalidated
|| phase is not TransitionCellCollisionPhase.Environment)
{
return observed;
}
invalidated = true;
if (invalidation is ReentrantActivationInvalidation.Reset)
{
fixture.Movement.ResetSession();
}
else
{
uint guid = fixture.Record.ServerGuid;
Assert.True(fixture.Lifetime.TryAcceptDelete(
new DeleteObject.Parsed(
guid,
fixture.Record.Incarnation),
isLocalPlayer: false,
removeRetainedObject: false,
out RuntimeEntityDeleteAcceptance acceptance));
fixture.Lifetime.CompleteAcceptedDelete(acceptance);
Assert.Null(fixture.Lifetime.RetireCanonicalOnly(
fixture.Record));
replacement = fixture.Lifetime.RegisterEntity(
Spawn(guid, incarnation: 2)).Canonical!;
}
return observed;
};
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.RejectedAuthority,
fixture.Owner.EvaluateActivation(token, out _));
Assert.True(invalidated);
Assert.Equal(0, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
Assert.Null(fixture.Movement.Controller);
Assert.Null(replacement?.PhysicsBody);
}
[Fact]
public void ExistingActivationLeaseCannotBeOverwrittenAfterExternalClear()
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
fixture.Lifetime.Entities.SetPhysicsBody(fixture.Record, null);
fixture.Movement.Controller = null;
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Prepare(
fixture.Record,
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
out _));
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
Assert.True(fixture.Owner.DiscardActivation(token));
Assert.Equal(0, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Prepared,
fixture.Owner.Prepare(
fixture.Record,
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
out _));
}
[Fact]
public void ResetRetiresPendingActivationAndConvergesItsOwnershipLedger()
{
using var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(fixture.Prepare(), out _));
fixture.Movement.ResetSession();
Assert.Null(fixture.Record.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
Assert.Equal(0, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
}
[Fact]
public void ExplicitDiscardAndDisposeRetirePendingActivationExactlyOnce()
{
var fixture = new Fixture(residentWorld: true);
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.True(fixture.Owner.DiscardActivation(token));
Assert.False(fixture.Owner.DiscardActivation(token));
Assert.Null(fixture.Record.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
fixture.RepreparePlacement();
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(fixture.Prepare(), out _));
fixture.Owner.Dispose();
Assert.True(fixture.Owner.CaptureOwnership().IsConverged);
Assert.Null(fixture.Record.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
fixture.Dispose();
}
[Theory]
[InlineData(PublicationInvalidation.SetPositionReplacement)]
[InlineData(PublicationInvalidation.Vector)]
[InlineData(PublicationInvalidation.FinalState)]
[InlineData(PublicationInvalidation.ObjectDescription)]
[InlineData(PublicationInvalidation.Create)]
[InlineData(PublicationInvalidation.Remote)]
[InlineData(PublicationInvalidation.Projectile)]
[InlineData(PublicationInvalidation.Body)]
[InlineData(PublicationInvalidation.Clock)]
[InlineData(PublicationInvalidation.Controller)]
[InlineData(PublicationInvalidation.CancelPlacement)]
public void EveryNestedAuthorityReplacementDiscardsCandidateWithoutRollback(
PublicationInvalidation invalidation)
{
using var fixture = new Fixture();
RuntimeLocalPlayerPhysicsPublicationToken token = fixture.Prepare();
PhysicsBody? replacementBody = null;
PlayerMovementController? replacementController = null;
switch (invalidation)
{
case PublicationInvalidation.SetPositionReplacement:
Assert.True(fixture.Lifetime.Physics.SetPosition
.BeginAuthoredPlacement(
fixture.Record,
fixture.Record.PositionAuthorityVersion,
RuntimeSetPositionOperationKind.LocalAuthoritative)
.IsValid);
break;
case PublicationInvalidation.Vector:
fixture.Lifetime.Entities.AdvanceVectorAuthority(fixture.Record);
break;
case PublicationInvalidation.FinalState:
fixture.Lifetime.Entities.SetFinalPhysicsState(
fixture.Record,
fixture.Record.FinalPhysicsState | PhysicsStateFlags.Frozen);
break;
case PublicationInvalidation.ObjectDescription:
fixture.Lifetime.Entities.AdvanceObjDescAuthority(fixture.Record);
break;
case PublicationInvalidation.Create:
fixture.Lifetime.Entities.AdvanceCreateAuthority(fixture.Record);
break;
case PublicationInvalidation.Remote:
fixture.Lifetime.Entities.SetRemoteMotion(
fixture.Record,
new RemoteMotion());
break;
case PublicationInvalidation.Projectile:
fixture.Lifetime.Entities.SetProjectile(
fixture.Record,
new RuntimeProjectile(
new PhysicsBody(),
new ProjectileCollisionSphere(Vector3.Zero, 0.1f)));
break;
case PublicationInvalidation.Body:
replacementBody = new PhysicsBody();
fixture.Lifetime.Entities.SetPhysicsBody(
fixture.Record,
replacementBody);
break;
case PublicationInvalidation.Clock:
fixture.Lifetime.Entities.SuspendObjectClock(fixture.Record);
break;
case PublicationInvalidation.Controller:
replacementController = new PlayerMovementController(
new PhysicsEngine());
fixture.Movement.Controller = replacementController;
break;
case PublicationInvalidation.CancelPlacement:
Assert.True(fixture.Lifetime.Physics.SetPosition.Cancel(
fixture.Record,
publishWithdrawal: false));
break;
default:
throw new ArgumentOutOfRangeException(nameof(invalidation));
}
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Commit(token));
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
if (replacementBody is not null)
Assert.Same(replacementBody, fixture.Record.PhysicsBody);
if (replacementController is not null)
Assert.Same(replacementController, fixture.Movement.Controller);
}
[Fact]
public void DeleteAndGuidReuseCannotPublishRetiredCandidate()
{
using var fixture = new Fixture();
RuntimeLocalPlayerPhysicsPublicationToken token = fixture.Prepare();
uint guid = fixture.Record.ServerGuid;
Assert.True(fixture.Lifetime.TryAcceptDelete(
new DeleteObject.Parsed(guid, fixture.Record.Incarnation),
isLocalPlayer: false,
removeRetainedObject: false,
out RuntimeEntityDeleteAcceptance acceptance));
fixture.Lifetime.CompleteAcceptedDelete(acceptance);
Assert.Null(fixture.Lifetime.RetireCanonicalOnly(fixture.Record));
RuntimeEntityRecord replacement = fixture.Lifetime.RegisterEntity(
Spawn(guid, incarnation: 2)).Canonical!;
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Commit(token));
Assert.Null(replacement.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
}
[Fact]
public void ResetAndDisposeDiscardCandidatesAndConvergeOwnership()
{
var fixture = new Fixture();
_ = fixture.Prepare();
fixture.Movement.ResetSession();
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
fixture.RepreparePlacement();
_ = fixture.Prepare();
fixture.Movement.Dispose();
Assert.True(fixture.Movement.CaptureOwnership().IsConverged);
fixture.DisposeLifetimeOnly();
}
[Theory]
[InlineData(PristineViolation.Body)]
[InlineData(PristineViolation.Controller)]
[InlineData(PristineViolation.Host)]
[InlineData(PristineViolation.BodyAcquisition)]
[InlineData(PristineViolation.Remote)]
[InlineData(PristineViolation.RemoteBinding)]
[InlineData(PristineViolation.Projectile)]
[InlineData(PristineViolation.ProjectileBinding)]
[InlineData(PristineViolation.RemotePlacement)]
public void PreparationRejectsEveryNonPristineOwnershipGraph(
PristineViolation violation)
{
using var fixture = new Fixture();
switch (violation)
{
case PristineViolation.Body:
{
var activeBody = new PhysicsBody
{
TransientState = TransientStateFlags.Active,
};
fixture.Lifetime.Entities.SetPhysicsBody(
fixture.Record,
activeBody);
break;
}
case PristineViolation.Controller:
fixture.Movement.Controller = new PlayerMovementController(
new PhysicsEngine());
break;
case PristineViolation.Host:
fixture.Lifetime.Entities.SetPhysicsHost(
fixture.Record,
CreatePhysicsHost(fixture.Record.ServerGuid));
break;
case PristineViolation.BodyAcquisition:
fixture.Lifetime.Entities.SetPhysicsBodyAcquisitionInProgress(
fixture.Record,
true);
break;
case PristineViolation.Remote:
fixture.Lifetime.Entities.SetRemoteMotion(
fixture.Record,
new RemoteMotion());
break;
case PristineViolation.RemoteBinding:
fixture.Lifetime.Entities.SetRemoteMotionBindingInProgress(
fixture.Record,
true);
break;
case PristineViolation.Projectile:
fixture.Lifetime.Entities.SetProjectile(
fixture.Record,
new RuntimeProjectile(
new PhysicsBody(),
new ProjectileCollisionSphere(Vector3.Zero, 0.1f)));
break;
case PristineViolation.ProjectileBinding:
fixture.Lifetime.Entities.SetProjectileBindingInProgress(
fixture.Record,
true);
break;
case PristineViolation.RemotePlacement:
fixture.Lifetime.Entities.SetRequiresRemotePlacementRuntime(
fixture.Record,
true);
break;
default:
throw new ArgumentOutOfRangeException(nameof(violation));
}
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Prepare(
fixture.Record,
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
out RuntimeLocalPlayerPhysicsPublicationToken token));
Assert.False(token.IsValid);
Assert.Equal(0, fixture.Owner.CaptureOwnership().CandidateCount);
}
[Fact]
public void PreparationRejectsRecordWhichIsNotTheExactLocalIdentity()
{
using var fixture = new Fixture();
fixture.Identity.ServerGuid = 0x70003002u;
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Prepare(
fixture.Record,
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
out _));
}
[Fact]
public void PreparationRejectsDisposedLocalIdentity()
{
using var fixture = new Fixture();
fixture.Identity.Dispose();
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Prepare(
fixture.Record,
fixture.Placement,
fixture.Command,
PlayerMovementConstructionOptions.Fallback,
out _));
}
[Fact]
public void IdentityRevisionSwitchRejectsPreparedCandidate()
{
using var fixture = new Fixture();
RuntimeLocalPlayerPhysicsPublicationToken token = fixture.Prepare();
uint guid = fixture.Identity.ServerGuid;
fixture.Identity.ServerGuid = guid + 1u;
fixture.Identity.ServerGuid = guid;
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.RejectedAuthority,
fixture.Owner.Commit(token));
Assert.Null(fixture.Record.PhysicsBody);
Assert.Null(fixture.Movement.Controller);
}
[Theory]
[InlineData(PublishedRetirement.Replacement)]
[InlineData(PublishedRetirement.Reset)]
[InlineData(PublishedRetirement.Dispose)]
public void RuntimeOwnedControllerRejectsStaleOperationsAfterRetirement(
PublishedRetirement retirement)
{
using var fixture = new Fixture();
Assert.Equal(
RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(fixture.Prepare()));
PlayerMovementController stale = fixture.Movement.Controller!;
switch (retirement)
{
case PublishedRetirement.Replacement:
fixture.Movement.Controller = new PlayerMovementController(
new PhysicsEngine());
break;
case PublishedRetirement.Reset:
fixture.Movement.ResetSession();
break;
case PublishedRetirement.Dispose:
fixture.Movement.Dispose();
break;
default:
throw new ArgumentOutOfRangeException(nameof(retirement));
}
AssertNotLive(stale);
}
[Fact]
public void BodyAndControllerEpochsAdvanceOnlyOnOwnershipChanges()
{
using var fixture = new Fixture(preparePlacement: false);
var firstBody = new PhysicsBody();
var secondBody = new PhysicsBody();
ulong bodyEpoch = fixture.Record.PhysicsOwnershipEpoch;
fixture.Lifetime.Entities.SetPhysicsBody(fixture.Record, firstBody);
Assert.Equal(bodyEpoch + 1UL, fixture.Record.PhysicsOwnershipEpoch);
fixture.Lifetime.Entities.SetPhysicsBody(fixture.Record, firstBody);
Assert.Equal(bodyEpoch + 1UL, fixture.Record.PhysicsOwnershipEpoch);
fixture.Lifetime.Entities.SetPhysicsBody(fixture.Record, secondBody);
Assert.Equal(bodyEpoch + 2UL, fixture.Record.PhysicsOwnershipEpoch);
fixture.Lifetime.Entities.SetPhysicsBody(fixture.Record, null);
Assert.Equal(bodyEpoch + 3UL, fixture.Record.PhysicsOwnershipEpoch);
var first = new PlayerMovementController(new PhysicsEngine());
var second = new PlayerMovementController(new PhysicsEngine());
ulong controllerEpoch = fixture.Movement.ControllerOwnershipEpoch;
fixture.Movement.Controller = first;
Assert.Equal(controllerEpoch + 1UL,
fixture.Movement.ControllerOwnershipEpoch);
fixture.Movement.Controller = first;
Assert.Equal(controllerEpoch + 1UL,
fixture.Movement.ControllerOwnershipEpoch);
fixture.Movement.Controller = second;
Assert.Equal(controllerEpoch + 2UL,
fixture.Movement.ControllerOwnershipEpoch);
fixture.Movement.Controller = null;
Assert.Equal(controllerEpoch + 3UL,
fixture.Movement.ControllerOwnershipEpoch);
}
private static EvaluationPuritySnapshot CaptureEvaluationPurity(
Fixture fixture)
{
PhysicsBody body = fixture.Record.PhysicsBody!;
bool hasAwaiting = fixture.Lifetime.Physics.SetPosition
.TryGetAwaitingPreparationToken(
fixture.Record,
out RuntimeEntityPlacementToken awaiting);
return new EvaluationPuritySnapshot(
fixture.Lifetime.Physics.CaptureOwnership(),
fixture.Lifetime.Physics.SetPosition.CaptureOwnership(),
fixture.Lifetime.Physics.CollisionReports.CaptureOwnership(),
fixture.Lifetime.Physics.CollisionWorldAuthority,
fixture.Lifetime.Physics.ShadowWorldAuthority,
fixture.Lifetime.Physics.ObjectTable,
fixture.Lifetime.Physics.ObjectTableBindingAuthority,
fixture.Lifetime.Physics.ObjectTableAuthority,
hasAwaiting,
awaiting,
fixture.Lifetime.Physics.SetPosition
.IsExactPreparedPlacementCurrent(
fixture.Record,
fixture.Placement,
fixture.Command),
fixture.Record.FullCellId,
fixture.Record.SpatialAuthorityVersion,
fixture.Record.PlacementCommitVersion,
fixture.Record.ObjectClockEpoch,
fixture.Record.ObjectClock.PendingSeconds,
fixture.Record.ObjectClock.IsActive,
CaptureBody(body),
CaptureVectorBits(body.WalkableVertices));
}
private static void AssertEvaluationPurity(
in EvaluationPuritySnapshot expected,
in EvaluationPuritySnapshot actual)
{
Assert.Equal(expected.PhysicsOwnership, actual.PhysicsOwnership);
Assert.Equal(expected.SetPositionOwnership, actual.SetPositionOwnership);
Assert.Equal(expected.CollisionOwnership, actual.CollisionOwnership);
Assert.Equal(expected.CollisionWorldAuthority,
actual.CollisionWorldAuthority);
Assert.Equal(expected.ShadowWorldAuthority,
actual.ShadowWorldAuthority);
Assert.Same(expected.ObjectTable, actual.ObjectTable);
Assert.Equal(expected.ObjectTableBindingAuthority,
actual.ObjectTableBindingAuthority);
Assert.Equal(expected.ObjectTableAuthority,
actual.ObjectTableAuthority);
Assert.Equal(expected.HasAwaitingPlacement,
actual.HasAwaitingPlacement);
Assert.Equal(expected.AwaitingPlacement, actual.AwaitingPlacement);
Assert.Equal(expected.ExactPlacementCurrent,
actual.ExactPlacementCurrent);
Assert.Equal(expected.FullCellId, actual.FullCellId);
Assert.Equal(expected.SpatialAuthorityVersion,
actual.SpatialAuthorityVersion);
Assert.Equal(expected.PlacementCommitVersion,
actual.PlacementCommitVersion);
Assert.Equal(expected.ObjectClockEpoch, actual.ObjectClockEpoch);
Assert.Equal(expected.ObjectClockPending,
actual.ObjectClockPending);
Assert.Equal(expected.ObjectClockActive, actual.ObjectClockActive);
Assert.Equal(expected.Body, actual.Body);
Assert.True(expected.WalkableVertexBits.AsSpan().SequenceEqual(
actual.WalkableVertexBits.AsSpan()));
}
private static BodyPuritySnapshot CaptureBody(PhysicsBody body) => new(
body.Position,
body.CellPosition,
body.InWorld,
body.Orientation,
body.Velocity,
body.CachedVelocity,
body.FramesStationaryFall,
body.Acceleration,
body.Omega,
body.GroundNormal,
body.SlidingNormal,
body.ContactPlaneValid,
body.ContactPlane,
body.ContactPlaneCellId,
body.ContactPlaneIsWater,
body.WalkablePolygonValid,
body.WalkablePlane,
body.WalkableUp,
body.Elasticity,
body.Friction,
body.State,
body.TransientState,
body.LastUpdateTime,
body.IsFullyConstrained,
body.LastMoveWasAutonomous);
private static ImmutableArray<uint> CaptureVectorBits(
Vector3[]? vectors)
{
if (vectors is null)
return ImmutableArray<uint>.Empty;
var result = ImmutableArray.CreateBuilder<uint>(vectors.Length * 3);
foreach (Vector3 vector in vectors)
{
result.Add(BitConverter.SingleToUInt32Bits(vector.X));
result.Add(BitConverter.SingleToUInt32Bits(vector.Y));
result.Add(BitConverter.SingleToUInt32Bits(vector.Z));
}
return result.ToImmutable();
}
private readonly record struct EvaluationPuritySnapshot(
RuntimePhysicsOwnershipSnapshot PhysicsOwnership,
RuntimeSetPositionOwnershipSnapshot SetPositionOwnership,
RuntimeCollisionReportingOwnershipSnapshot CollisionOwnership,
ulong CollisionWorldAuthority,
ulong ShadowWorldAuthority,
ClientObjectTable? ObjectTable,
ulong ObjectTableBindingAuthority,
ulong ObjectTableAuthority,
bool HasAwaitingPlacement,
RuntimeEntityPlacementToken AwaitingPlacement,
bool ExactPlacementCurrent,
uint FullCellId,
ulong SpatialAuthorityVersion,
ulong PlacementCommitVersion,
ulong ObjectClockEpoch,
double ObjectClockPending,
bool ObjectClockActive,
BodyPuritySnapshot Body,
ImmutableArray<uint> WalkableVertexBits);
private readonly record struct BodyPuritySnapshot(
Vector3 Position,
AcDream.Core.Physics.Position CellPosition,
bool InWorld,
Quaternion Orientation,
Vector3 Velocity,
Vector3 CachedVelocity,
int FramesStationaryFall,
Vector3 Acceleration,
Vector3 Omega,
Vector3 GroundNormal,
Vector3 SlidingNormal,
bool ContactPlaneValid,
Plane ContactPlane,
uint ContactPlaneCellId,
bool ContactPlaneIsWater,
bool WalkablePolygonValid,
Plane WalkablePlane,
Vector3 WalkableUp,
float Elasticity,
float Friction,
PhysicsStateFlags State,
TransientStateFlags TransientState,
double LastUpdateTime,
bool IsFullyConstrained,
bool LastMoveWasAutonomous);
private sealed class Fixture : IDisposable
{
private bool _lifetimeDisposed;
internal Fixture(
bool preparePlacement = true,
bool residentWorld = false)
{
if (residentWorld)
{
var engine = new PhysicsEngine
{
DataCache = new PhysicsDataCache(),
};
engine.AddLandblock(
Cell & 0xFFFF0000u,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
Lifetime = new RuntimeEntityObjectLifetime(engine);
}
else
{
Lifetime = new RuntimeEntityObjectLifetime();
}
Movement = new RuntimeLocalPlayerMovementState();
Identity = new RuntimeLocalPlayerIdentityState();
Owner = new RuntimeLocalPlayerPhysicsPublicationState(
Lifetime.Entities,
Lifetime.Physics,
Movement,
Identity);
Movement.AttachPhysicsPublication(Owner);
Record = Lifetime.RegisterEntity(
Spawn(0x70003001u, incarnation: 1)).Canonical!;
Identity.ServerGuid = Record.ServerGuid;
if (preparePlacement)
RepreparePlacement();
}
internal RuntimeEntityObjectLifetime Lifetime { get; }
internal RuntimeLocalPlayerMovementState Movement { get; }
internal RuntimeLocalPlayerIdentityState Identity { get; }
internal RuntimeLocalPlayerPhysicsPublicationState Owner { get; }
internal RuntimeEntityRecord Record { get; }
internal RuntimeEntityPlacementToken Placement { get; private set; }
internal RuntimeSetPositionCommand Command { get; private set; }
internal void RepreparePlacement()
{
Placement = Lifetime.Physics.SetPosition.BeginAuthoredPlacement(
Record,
Record.PositionAuthorityVersion,
RuntimeSetPositionOperationKind.LocalAuthoritative);
Assert.True(Placement.IsValid);
var setup = new FlatSetupCollision(
ImmutableArray<FlatCollisionCylinder>.Empty,
[new FlatCollisionSphere(Vector3.Zero, 0.48f)],
height: 0f,
radius: 0f,
stepUpHeight: 0.4f,
stepDownHeight: 0.4f);
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared,
Lifetime.Physics.SetPosition.PrepareMover(
Placement,
new RuntimeSetPositionMoverPreparation(
RuntimeSetPositionMoverSetup.Resolved(SetupId, setup),
RuntimeSetPositionOperationKind.LocalAuthoritative,
GameTime: 10d,
PhysicsPlacementClass.Ordinary,
PhysicsSetPositionFlags.Placement
| PhysicsSetPositionFlags.Slide),
out RuntimeSetPositionCommand command));
Command = command;
}
internal RuntimeLocalPlayerPhysicsPublicationToken Prepare()
{
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Prepared,
Owner.Prepare(
Record,
Placement,
Command,
PlayerMovementConstructionOptions.Fallback,
out RuntimeLocalPlayerPhysicsPublicationToken token));
Assert.Equal(Record.Key, token.Entity);
Assert.Equal(Placement, token.Placement);
Assert.Equal(Identity.ServerGuid, token.LocalPlayerServerGuid);
Assert.Equal(Identity.Revision, token.LocalPlayerIdentityRevision);
return token;
}
internal void DisposeLifetimeOnly()
{
if (_lifetimeDisposed)
return;
Lifetime.Dispose();
_lifetimeDisposed = true;
}
public void Dispose()
{
Movement.Dispose();
Identity.Dispose();
DisposeLifetimeOnly();
}
}
private static WorldSession.EntitySpawn Spawn(uint guid, ushort incarnation)
{
var position = new CreateObject.ServerPosition(
Cell,
1f,
2f,
3f,
1f,
0f,
0f,
0f);
var timestamps = new PhysicsTimestamps(
Position: 1,
Movement: 1,
State: 1,
Vector: 1,
Teleport: 0,
ServerControlledMove: 1,
ForcePosition: 0,
ObjDesc: 1,
Instance: incarnation);
var physics = new PhysicsSpawnData(
RawState: (uint)(PhysicsStateFlags.Gravity
| PhysicsStateFlags.ReportCollisions),
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: SetupId,
MotionTableId: 0x09000001u,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: 1f,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
Guid: guid,
Position: position,
SetupTableId: SetupId,
AnimPartChanges: Array.Empty<CreateObject.AnimPartChange>(),
TextureChanges: Array.Empty<CreateObject.TextureChange>(),
SubPalettes: Array.Empty<CreateObject.SubPaletteSwap>(),
BasePaletteId: null,
ObjScale: 1f,
Name: "local-publication-fixture",
ItemType: null,
MotionState: null,
MotionTableId: 0x09000001u,
PhysicsState: physics.RawState,
ObjectDescriptionFlags: 0x8u,
InstanceSequence: incarnation,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
private const uint RestrictionObjectId = 0x70003F01u;
private static void SeedRestrictionObjects(Fixture fixture)
{
fixture.Lifetime.Objects.AddOrUpdate(new ClientObject
{
ObjectId = RestrictionObjectId,
HouseOwnerId = 0x70003F02u,
Restrictions = new HouseRestrictionRecord(
OpenToPublic: false,
AllegianceMonarchId: 0x70003F03u,
Guests: new Dictionary<uint, uint>()),
});
fixture.Lifetime.Objects.AddOrUpdate(new ClientObject
{
ObjectId = fixture.Record.ServerGuid,
MonarchId = 0x70003F03u,
});
}
private static void ApplyRestrictionObjectMutation(
Fixture fixture,
RestrictionObjectMutation mutation)
{
ClientObjectTable objects = fixture.Lifetime.Objects;
switch (mutation)
{
case RestrictionObjectMutation.RemoveHouseObject:
Assert.True(objects.Remove(RestrictionObjectId));
break;
case RestrictionObjectMutation.HouseOwnerProperty:
{
ClientObject house = objects.Get(RestrictionObjectId)!;
house.HouseOwnerId = 0x70003F04u;
break;
}
case RestrictionObjectMutation.HouseGuestList:
objects.Get(RestrictionObjectId)!.Restrictions =
new HouseRestrictionRecord(
OpenToPublic: false,
AllegianceMonarchId: 0x70003F03u,
Guests: new Dictionary<uint, uint>
{
[fixture.Record.ServerGuid] = 1u,
});
break;
case RestrictionObjectMutation.MoverMonarch:
{
ClientObject mover = objects.Get(fixture.Record.ServerGuid)!;
mover.MonarchId = 0x70003F05u;
break;
}
default:
throw new ArgumentOutOfRangeException(nameof(mutation));
}
}
private static RuntimeCollisionGenerationCommit CommitPrepared(
RuntimePhysicsState physics,
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared)
{
while (true)
{
while (!physics.AdvanceCollisionRetainedOwnerCapture(
admission,
prepared).Completed)
{
}
foreach (uint ownerId in prepared.RetainedOwnerIds)
{
physics.RefreshCollisionRetainedOwner(
admission,
prepared,
ownerId);
}
RuntimeCollisionSealStep seal;
do
{
seal = physics.AdvanceCollisionGenerationSeal(
admission,
prepared);
}
while (!seal.Completed && !seal.Restarted);
if (seal.Completed)
break;
}
return physics.CommitCollisionGeneration(admission, prepared);
}
private static RuntimeLandblockCollisionAssets CollisionAssets(
uint landblockId) => new(
landblockId,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
0f,
0f,
0u);
public enum PublicationInvalidation
{
SetPositionReplacement,
Vector,
FinalState,
ObjectDescription,
Create,
Remote,
Projectile,
Body,
Clock,
Controller,
CancelPlacement,
}
public enum PristineViolation
{
Body,
Controller,
Host,
BodyAcquisition,
Remote,
RemoteBinding,
Projectile,
ProjectileBinding,
RemotePlacement,
}
public enum PublishedRetirement
{
Replacement,
Reset,
Dispose,
}
public enum ActivationInvalidation
{
Position,
Vector,
ObjectDescription,
Create,
Identity,
Body,
Controller,
Clock,
Spatial,
Host,
Remote,
Projectile,
PlacementCancellation,
}
public enum ReentrantActivationInvalidation
{
Reset,
DeleteAndGuidReuse,
}
public enum RestrictionObjectMutation
{
RemoveHouseObject,
HouseOwnerProperty,
HouseGuestList,
MoverMonarch,
}
private static EntityPhysicsHost CreatePhysicsHost(uint id) => new(
id,
getPosition: static () => default,
getVelocity: static () => Vector3.Zero,
getRadius: static () => 0.48f,
inContact: static () => true,
minterpMaxSpeed: static () => null,
curTime: static () => 0d,
physicsTimerTime: static () => 0d,
getObjectA: static _ => null,
handleUpdateTarget: static _ => { },
interruptCurrentMovement: static () => { });
private static void AssertNotLive(PlayerMovementController controller)
{
Assert.Throws<InvalidOperationException>(() => controller.Update(
1f / 60f,
default));
Assert.Throws<InvalidOperationException>(() => controller.TickHidden(
1f / 60f));
Assert.Throws<InvalidOperationException>(() =>
controller.SuspendObjectUpdate(1f / 60f));
Assert.Throws<InvalidOperationException>(() => controller.SetPosition(
Vector3.One,
Cell,
Vector3.One));
Assert.Throws<InvalidOperationException>(() => controller.BlipPosition(
Vector3.One,
Cell,
Vector3.One));
Assert.Throws<InvalidOperationException>(() =>
controller.ApplyPhysicsState(PhysicsStateFlags.Frozen));
Assert.Throws<InvalidOperationException>(() => controller.Yaw = 1f);
Assert.Throws<InvalidOperationException>(() =>
controller.CaptureMovementResult(mouseLookEvent: false));
Assert.Throws<InvalidOperationException>(() =>
controller.CapturePresentationResult());
Assert.Throws<InvalidOperationException>(() =>
controller.TryGetOutboundPosition(out _));
Assert.Throws<InvalidOperationException>(() =>
controller.NoteMovementSent(1f));
Assert.Throws<InvalidOperationException>(() =>
controller.NotePositionSent(default, default, 1f));
Assert.Throws<InvalidOperationException>(() =>
controller.ShouldSendPositionEvent(default, default, 1f));
Assert.Throws<InvalidOperationException>(() =>
controller.BeginMouseLook(default));
Assert.Throws<InvalidOperationException>(() =>
controller.SubmitMouseTurnAdjustment(1f, default));
Assert.Throws<InvalidOperationException>(() =>
controller.StopMouseDrift(default));
Assert.Throws<InvalidOperationException>(() =>
controller.EndMouseLook(default));
Assert.Throws<InvalidOperationException>(() =>
controller.PrepareForAttackRequest());
Assert.Throws<InvalidOperationException>(() =>
controller.RequestPosture(MotionCommand.Ready));
Assert.Throws<InvalidOperationException>(() =>
controller.CommitPreparedPosition());
Assert.Throws<InvalidOperationException>(() =>
controller.PreparePositionForCommit(Vector3.One, Cell, Vector3.One));
Assert.Throws<InvalidOperationException>(() =>
controller.SetCharacterSkills(1, 1));
Assert.Throws<InvalidOperationException>(() =>
controller.SetBodyOrientation(Quaternion.Identity));
Assert.Throws<InvalidOperationException>(() =>
controller.SetLastMoveWasAutonomous(true));
Assert.Throws<InvalidOperationException>(() =>
controller.State = PlayerState.PortalSpace);
Assert.Throws<InvalidOperationException>(() =>
controller.StepUpHeight = 1f);
Assert.Throws<InvalidOperationException>(() =>
controller.AttachCycleVelocityAccessor(static () => Vector3.One));
Assert.Throws<InvalidOperationException>(() =>
controller.AttachAnimationRootMotionSource(static (_, _) => { }));
Assert.Throws<InvalidOperationException>(() => _ = controller.Movement);
Assert.Throws<InvalidOperationException>(() => _ = controller.MoveTo);
Assert.Throws<InvalidOperationException>(() => _ = controller.PositionManager);
Assert.Throws<InvalidOperationException>(() => _ = controller.Motion);
Assert.Throws<InvalidOperationException>(() => _ = controller.PhysicsBody);
}
}