1779 lines
72 KiB
C#
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);
|
|
}
|
|
}
|