feat(runtime): publish dormant local physics ownership
This commit is contained in:
parent
442cb8f97b
commit
22651c823d
10 changed files with 1617 additions and 25 deletions
|
|
@ -1100,6 +1100,74 @@ public class PlayerMovementControllerTests
|
|||
public bool StopMotion(uint motion) => true;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PublicationLifecycleRequiresExplicitActivationAfterOwnershipCommit()
|
||||
{
|
||||
var candidate = PlayerMovementController.CreatePublicationCandidate(
|
||||
new PhysicsEngine(),
|
||||
PlayerMovementConstructionOptions.Fallback);
|
||||
candidate.LocalEntityId = 0x70004001u;
|
||||
candidate.StepUpHeight = 0.4f;
|
||||
candidate.StepDownHeight = 0.4f;
|
||||
candidate.ObjectScale = 1f;
|
||||
candidate.PreparePositionForCommit(
|
||||
new Vector3(1f, 2f, 3f),
|
||||
0xA9B40021u,
|
||||
new Vector3(1f, 2f, 3f));
|
||||
candidate.SetBodyOrientation(Quaternion.Identity);
|
||||
candidate.ApplyPhysicsState(PhysicsStateFlags.Gravity);
|
||||
PhysicsBody body = candidate.PhysicsBody;
|
||||
body.InWorld = false;
|
||||
body.TransientState &= ~TransientStateFlags.Active;
|
||||
Assert.False(body.InWorld);
|
||||
|
||||
candidate.SealPublicationCandidate();
|
||||
|
||||
Assert.True(candidate.IsSealedPublicationCandidate);
|
||||
Assert.Throws<InvalidOperationException>(() => candidate.Update(
|
||||
1f / 60f,
|
||||
default));
|
||||
Assert.Throws<InvalidOperationException>(() => candidate.TickHidden(
|
||||
1f / 60f));
|
||||
Assert.Throws<InvalidOperationException>(() => candidate.SetPosition(
|
||||
Vector3.One,
|
||||
0xA9B40021u,
|
||||
Vector3.One));
|
||||
Assert.Throws<InvalidOperationException>(() => candidate.BlipPosition(
|
||||
Vector3.One,
|
||||
0xA9B40021u,
|
||||
Vector3.One));
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
candidate.CaptureMovementResult(mouseLookEvent: false));
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
candidate.NoteMovementSent(1f));
|
||||
Assert.Throws<InvalidOperationException>(candidate.CommitPreparedPosition);
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
candidate.ApplyPhysicsState(PhysicsStateFlags.Frozen));
|
||||
Assert.Throws<InvalidOperationException>(() => candidate.LocalEntityId = 2u);
|
||||
Assert.Throws<InvalidOperationException>(() => _ = candidate.Movement);
|
||||
|
||||
var canonicalClock = new RetailObjectQuantumClock();
|
||||
candidate.CommitRuntimeOwnership(canonicalClock);
|
||||
Assert.Throws<InvalidOperationException>(() => candidate.Update(
|
||||
1f / 60f,
|
||||
default));
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
candidate.CaptureMovementResult(mouseLookEvent: false));
|
||||
Assert.Throws<InvalidOperationException>(() => _ = candidate.PhysicsBody);
|
||||
|
||||
candidate.ActivateRuntimePublication();
|
||||
Assert.Same(body, candidate.PhysicsBody);
|
||||
_ = candidate.CaptureMovementResult(mouseLookEvent: false);
|
||||
_ = candidate.Update(0f, default);
|
||||
candidate.ApplyPhysicsState(PhysicsStateFlags.Gravity);
|
||||
|
||||
candidate.RetireRuntimePublication();
|
||||
Assert.Throws<InvalidOperationException>(() => candidate.Update(
|
||||
1f / 60f,
|
||||
default));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_RunningJumpLandsOnFlatGround_ResidualVelocitySurvivesAndDecays_NotFrozen()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,713 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Numerics;
|
||||
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));
|
||||
}
|
||||
|
||||
[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 sealed class Fixture : IDisposable
|
||||
{
|
||||
private bool _lifetimeDisposed;
|
||||
|
||||
internal Fixture(bool preparePlacement = true)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue