feat(runtime): publish dormant local physics ownership

This commit is contained in:
Erik 2026-08-01 10:01:30 +02:00
parent 442cb8f97b
commit 22651c823d
10 changed files with 1617 additions and 25 deletions

View file

@ -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()
{

View file

@ -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);
}
}