769 lines
27 KiB
C#
769 lines
27 KiB
C#
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Runtime.Entities;
|
|
using AcDream.Runtime.Physics;
|
|
using AcDream.Core.Physics.Motion;
|
|
using System.Numerics;
|
|
|
|
namespace AcDream.Runtime.Tests.Physics;
|
|
|
|
public sealed class RuntimePhysicsStateTests
|
|
{
|
|
[Fact]
|
|
public void EntityLifetimeConstructsOneCanonicalProductionPhysicsWorld()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
|
|
|
Assert.Same(
|
|
lifetime.Physics.DataCache,
|
|
lifetime.Physics.Engine.DataCache);
|
|
Assert.True(lifetime.Physics.CaptureOwnership().OwnsProductionDataCache);
|
|
Assert.Equal(0, lifetime.Physics.CaptureOwnership().LandblockCount);
|
|
Assert.False(lifetime.Physics.CaptureOwnership().IsDisposed);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConcurrentRuntimeLifetimesDoNotShareMutablePhysicsState()
|
|
{
|
|
using var first = new RuntimeEntityObjectLifetime();
|
|
using var second = new RuntimeEntityObjectLifetime();
|
|
|
|
Assert.NotSame(first.Physics, second.Physics);
|
|
Assert.NotSame(first.Physics.Engine, second.Physics.Engine);
|
|
Assert.NotSame(first.Physics.DataCache, second.Physics.DataCache);
|
|
Assert.NotSame(
|
|
first.Physics.Engine.ShadowObjects,
|
|
second.Physics.Engine.ShadowObjects);
|
|
Assert.NotSame(
|
|
first.Physics.DataCache.CellGraph,
|
|
second.Physics.DataCache.CellGraph);
|
|
}
|
|
|
|
[Fact]
|
|
public void PhysicsOwnerDisposesWithItsEntityLifetime()
|
|
{
|
|
var lifetime = new RuntimeEntityObjectLifetime();
|
|
var physics = lifetime.Physics;
|
|
|
|
lifetime.Dispose();
|
|
|
|
Assert.True(physics.CaptureOwnership().IsDisposed);
|
|
}
|
|
|
|
[Fact]
|
|
public void CanonicalRecordAndPhysicsOwnerOwnRemoteComponentAndWorksets()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
|
RuntimeEntityRecord record =
|
|
lifetime.Entities.AddActive(Spawn(0x70000001u, 1));
|
|
var remote = new TestRemoteMotion();
|
|
lifetime.Entities.SetPhysicsBody(record, remote.Body);
|
|
lifetime.Entities.SetRemoteMotion(record, remote);
|
|
|
|
lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true);
|
|
|
|
Assert.Same(remote, record.RemoteMotion);
|
|
Assert.Same(remote.Body, record.PhysicsBody);
|
|
Assert.True(lifetime.Physics.IsSpatialRoot(record));
|
|
Assert.True(lifetime.Physics.IsSpatialRemote(record, remote));
|
|
Assert.Equal(1, lifetime.Physics.SpatialRootCount);
|
|
Assert.Equal(1, lifetime.Physics.SpatialRemoteCount);
|
|
|
|
var roots = new List<RuntimeEntityRecord>();
|
|
var remotes = new List<RuntimeEntityRecord>();
|
|
lifetime.Physics.CopySpatialRootsTo(roots);
|
|
lifetime.Physics.CopySpatialRemotesTo(remotes);
|
|
Assert.Same(record, Assert.Single(roots));
|
|
Assert.Same(record, Assert.Single(remotes));
|
|
|
|
lifetime.Entities.SetRemoteMotion(record, null);
|
|
lifetime.Physics.RefreshRemoteComponent(record);
|
|
Assert.True(lifetime.Physics.IsSpatialRoot(record));
|
|
Assert.Equal(0, lifetime.Physics.SpatialRemoteCount);
|
|
|
|
lifetime.Physics.RemoveSpatialProjection(record);
|
|
Assert.Equal(0, lifetime.Physics.SpatialRootCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void CanonicalRecordAndPhysicsOwnerOwnProjectileComponentAndWorkset()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
|
RuntimeEntityRecord record =
|
|
lifetime.Entities.AddActive(Spawn(0x70000031u, 1));
|
|
lifetime.Entities.SetFullCell(record, 0x01010001u, 0x0101FFFFu);
|
|
lifetime.Entities.SetFinalPhysicsState(
|
|
record,
|
|
PhysicsStateFlags.ReportCollisions
|
|
| PhysicsStateFlags.Missile
|
|
| PhysicsStateFlags.AlignPath
|
|
| PhysicsStateFlags.PathClipped);
|
|
var body = new PhysicsBody
|
|
{
|
|
Position = new Vector3(10f, 20f, 5f),
|
|
Orientation = Quaternion.Identity,
|
|
InWorld = true,
|
|
TransientState = TransientStateFlags.Active,
|
|
};
|
|
body.SnapToCell(
|
|
record.FullCellId,
|
|
body.Position,
|
|
body.Position);
|
|
lifetime.Entities.SetPhysicsBody(record, body);
|
|
var sphere = new ProjectileCollisionSphere(
|
|
new Vector3(0.1f, 0f, 0f),
|
|
0.25f);
|
|
|
|
IRuntimeProjectile projectile = lifetime.Physics.BindProjectile(
|
|
record,
|
|
body,
|
|
sphere);
|
|
lifetime.Physics.AcknowledgeSpatialProjection(
|
|
record,
|
|
spatial: true);
|
|
|
|
Assert.Same(projectile, record.Projectile);
|
|
Assert.Same(body, projectile.Body);
|
|
Assert.Equal(sphere, projectile.CollisionSphere);
|
|
Assert.True(
|
|
lifetime.Physics.IsSpatialProjectile(record, projectile));
|
|
Assert.Equal(1, lifetime.Physics.SpatialProjectileCount);
|
|
Assert.Equal(
|
|
1,
|
|
lifetime.Physics.CaptureOwnership().SpatialProjectileCount);
|
|
|
|
Assert.Same(
|
|
projectile,
|
|
lifetime.Physics.BindProjectile(record, body, sphere));
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
lifetime.Physics.BindProjectile(
|
|
record,
|
|
new PhysicsBody(),
|
|
sphere));
|
|
|
|
lifetime.Physics.RemoveSpatialProjection(record);
|
|
Assert.Equal(0, lifetime.Physics.SpatialProjectileCount);
|
|
Assert.Same(projectile, record.Projectile);
|
|
|
|
Assert.True(lifetime.Physics.ClearProjectile(record));
|
|
Assert.Null(record.Projectile);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProjectileAuthoritativeMutationInvalidatesSplitPrediction()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
|
RuntimeEntityRecord record =
|
|
lifetime.Entities.AddActive(Spawn(0x70000032u, 1));
|
|
lifetime.Entities.SetFullCell(record, 0x01010001u, 0x0101FFFFu);
|
|
lifetime.Entities.SetFinalPhysicsState(
|
|
record,
|
|
PhysicsStateFlags.ReportCollisions
|
|
| PhysicsStateFlags.Missile
|
|
| PhysicsStateFlags.AlignPath
|
|
| PhysicsStateFlags.PathClipped);
|
|
var body = new PhysicsBody
|
|
{
|
|
Position = new Vector3(10f, 20f, 5f),
|
|
Orientation = Quaternion.Identity,
|
|
InWorld = true,
|
|
TransientState = TransientStateFlags.Active,
|
|
};
|
|
body.set_velocity(new Vector3(10f, 0f, 0f));
|
|
body.SnapToCell(
|
|
record.FullCellId,
|
|
body.Position,
|
|
body.Position);
|
|
lifetime.Entities.SetPhysicsBody(record, body);
|
|
IRuntimeProjectile projectile = lifetime.Physics.BindProjectile(
|
|
record,
|
|
body,
|
|
new ProjectileCollisionSphere(Vector3.Zero, 0.25f));
|
|
lifetime.Physics.AcknowledgeSpatialProjection(
|
|
record,
|
|
spatial: true);
|
|
var updater =
|
|
new RuntimeProjectilePhysicsUpdater(lifetime.Physics);
|
|
|
|
Assert.True(updater.TryBegin(
|
|
record,
|
|
quantum: 0.05f,
|
|
record.ObjectClockEpoch,
|
|
externalOwnerValid: null,
|
|
out RuntimeProjectilePhysicsCommit commit));
|
|
|
|
lifetime.Entities.AdvanceVectorAuthority(record);
|
|
Vector3 correction = new(7f, 8f, 9f);
|
|
Assert.True(updater.ApplyAuthoritativeVector(
|
|
record,
|
|
record.VectorAuthorityVersion,
|
|
record.VelocityAuthorityVersion,
|
|
correction,
|
|
Vector3.UnitZ,
|
|
currentTime: 1.0));
|
|
|
|
Assert.True(
|
|
projectile.PredictionAuthorityVersion
|
|
> commit.PredictionAuthorityVersion);
|
|
Assert.False(updater.Complete(
|
|
commit,
|
|
liveCenterX: 1,
|
|
liveCenterY: 1,
|
|
_ => true));
|
|
Assert.Equal(correction, body.Velocity);
|
|
Assert.Equal(Vector3.UnitZ, body.Omega);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProjectileQuantumPublishesOneImmutableRuntimeFrame()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
|
RuntimeEntityRecord record =
|
|
lifetime.Entities.AddActive(Spawn(0x70000033u, 1));
|
|
lifetime.Entities.SetFullCell(record, 0x01010001u, 0x0101FFFFu);
|
|
lifetime.Entities.SetFinalPhysicsState(
|
|
record,
|
|
PhysicsStateFlags.ReportCollisions
|
|
| PhysicsStateFlags.Missile
|
|
| PhysicsStateFlags.PathClipped);
|
|
var body = new PhysicsBody
|
|
{
|
|
Position = new Vector3(10f, 20f, 5f),
|
|
Orientation = Quaternion.Identity,
|
|
InWorld = true,
|
|
TransientState = TransientStateFlags.Active,
|
|
};
|
|
body.SnapToCell(
|
|
record.FullCellId,
|
|
body.Position,
|
|
body.Position);
|
|
lifetime.Entities.SetPhysicsBody(record, body);
|
|
lifetime.Physics.BindProjectile(
|
|
record,
|
|
body,
|
|
new ProjectileCollisionSphere(Vector3.Zero, 0.25f));
|
|
lifetime.Physics.AcknowledgeSpatialProjection(
|
|
record,
|
|
spatial: true);
|
|
var updater =
|
|
new RuntimeProjectilePhysicsUpdater(lifetime.Physics);
|
|
|
|
Assert.True(updater.TryBegin(
|
|
record,
|
|
quantum: 0.05f,
|
|
record.ObjectClockEpoch,
|
|
externalOwnerValid: null,
|
|
out RuntimeProjectilePhysicsCommit commit));
|
|
int acknowledgements = 0;
|
|
RuntimePhysicsFrameSnapshot published = default;
|
|
|
|
Assert.True(updater.Complete(
|
|
commit,
|
|
liveCenterX: 1,
|
|
liveCenterY: 1,
|
|
snapshot =>
|
|
{
|
|
published = snapshot;
|
|
acknowledgements++;
|
|
return true;
|
|
}));
|
|
|
|
Assert.Equal(1, acknowledgements);
|
|
Assert.Equal(body.Position, published.Position);
|
|
Assert.Equal(body.Orientation, published.Orientation);
|
|
Assert.Equal(record.FullCellId, published.FullCellId);
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
updater.Complete(
|
|
commit,
|
|
liveCenterX: 1,
|
|
liveCenterY: 1,
|
|
_ => true));
|
|
}
|
|
|
|
[Fact]
|
|
public void EqualLocalKeysInConcurrentRuntimesDoNotShareWorksets()
|
|
{
|
|
using var first = new RuntimeEntityObjectLifetime();
|
|
using var second = new RuntimeEntityObjectLifetime();
|
|
RuntimeEntityRecord firstRecord =
|
|
first.Entities.AddActive(Spawn(0x70000002u, 1));
|
|
RuntimeEntityRecord secondRecord =
|
|
second.Entities.AddActive(Spawn(0x70000003u, 1));
|
|
Assert.Equal(firstRecord.Key, secondRecord.Key);
|
|
|
|
first.Physics.AcknowledgeSpatialProjection(firstRecord, spatial: true);
|
|
|
|
Assert.True(first.Physics.IsSpatialRoot(firstRecord));
|
|
Assert.False(second.Physics.IsSpatialRoot(secondRecord));
|
|
Assert.Equal(1, first.Physics.SpatialRootCount);
|
|
Assert.Equal(0, second.Physics.SpatialRootCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void CollisionAdmissionIsExactRuntimeScopedAndWithdrawalIsTyped()
|
|
{
|
|
using var first = new RuntimeEntityObjectLifetime();
|
|
using var second = new RuntimeEntityObjectLifetime();
|
|
RuntimeCollisionAdmission admission =
|
|
first.Physics.BeginCollisionAdmission(0xA9B4FFFFu);
|
|
RuntimeCollisionAdmission newer =
|
|
first.Physics.BeginCollisionAdmission(0xA9B4FFFFu);
|
|
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
first.Physics.AdmitCollisionAssets(
|
|
admission,
|
|
CollisionAssets(0xA9B4FFFFu)));
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
second.Physics.AdmitCollisionAssets(
|
|
newer,
|
|
CollisionAssets(0xA9B4FFFFu)));
|
|
|
|
first.Physics.AdmitCollisionAssets(
|
|
newer,
|
|
CollisionAssets(0xA9B4FFFFu));
|
|
RuntimeCollisionAcknowledgement completed =
|
|
first.Physics.CompleteCollisionAdmission(newer);
|
|
|
|
Assert.True(completed.WasResident);
|
|
Assert.Equal(1, first.Physics.Engine.LandblockCount);
|
|
Assert.Equal(
|
|
0,
|
|
first.Physics.CaptureOwnership().CollisionAdmissionCount);
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
first.Physics.CompleteCollisionAdmission(newer));
|
|
|
|
RuntimeCollisionAcknowledgement withdrawn =
|
|
first.Physics.WithdrawCollision(0xA9B4FFFFu);
|
|
Assert.True(withdrawn.WasResident);
|
|
Assert.True(withdrawn.Generation > completed.Generation);
|
|
Assert.Equal(0, first.Physics.Engine.LandblockCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void TerminalDisposalClearsLandblocksShadowsAndWorksets()
|
|
{
|
|
var lifetime = new RuntimeEntityObjectLifetime();
|
|
RuntimeEntityRecord record =
|
|
lifetime.Entities.AddActive(Spawn(0x70000041u, 1));
|
|
lifetime.Physics.AcknowledgeSpatialProjection(
|
|
record,
|
|
spatial: true);
|
|
|
|
RuntimeCollisionAdmission admission =
|
|
lifetime.Physics.BeginCollisionAdmission(0x0101FFFFu);
|
|
lifetime.Physics.AdmitCollisionAssets(
|
|
admission,
|
|
CollisionAssets(0x0101FFFFu));
|
|
_ = lifetime.Physics.CompleteCollisionAdmission(admission);
|
|
lifetime.Physics.Engine.ShadowObjects.Register(
|
|
entityId: record.LocalEntityId!.Value,
|
|
gfxObjId: 0x01000001u,
|
|
worldPos: new Vector3(10f, 20f, 5f),
|
|
rotation: Quaternion.Identity,
|
|
radius: 0.5f,
|
|
worldOffsetX: 0f,
|
|
worldOffsetY: 0f,
|
|
landblockId: 0x0101FFFFu,
|
|
seedCellId: 0x01010001u,
|
|
isStatic: false);
|
|
|
|
Assert.False(lifetime.Physics.CaptureOwnership().IsConverged);
|
|
Assert.True(
|
|
lifetime.Physics.Engine.ShadowObjects
|
|
.RetainedRegistrationCount > 0);
|
|
|
|
lifetime.Dispose();
|
|
|
|
RuntimePhysicsOwnershipSnapshot retired =
|
|
lifetime.Physics.CaptureOwnership();
|
|
Assert.True(retired.IsConverged);
|
|
Assert.Equal(0, retired.LandblockCount);
|
|
Assert.Equal(0, retired.RetainedShadowRegistrationCount);
|
|
Assert.Equal(0, retired.SpatialRootCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void RemoteBindingOwnsCanonicalBodyCellAndTypedCellPublication()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
|
RuntimeEntityRecord record =
|
|
lifetime.Entities.AddActive(Spawn(0x70000021u, 1));
|
|
var remote = new RemoteMotion();
|
|
var commits = new List<RuntimePhysicsCellCommit>();
|
|
lifetime.Physics.CellCommitted += commits.Add;
|
|
lifetime.Physics.SetRemoteMotion(record, remote);
|
|
lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true);
|
|
|
|
remote.CellId = 0x02020001u;
|
|
|
|
Assert.Same(remote, record.RemoteMotion);
|
|
Assert.Same(remote.Body, record.PhysicsBody);
|
|
Assert.Equal(0x02020001u, record.FullCellId);
|
|
RuntimePhysicsCellCommit commit = Assert.Single(commits);
|
|
Assert.Same(record, commit.Record);
|
|
Assert.Equal(0x0101FFFFu, commit.PreviousFullCellId);
|
|
Assert.Equal(0x02020001u, commit.FullCellId);
|
|
Assert.True(lifetime.Physics.IsSpatialRemote(record, remote));
|
|
|
|
Assert.True(lifetime.Physics.ClearRemoteMotion(record));
|
|
Assert.Null(record.RemoteMotion);
|
|
Assert.Equal(0, lifetime.Physics.SpatialRemoteCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuiltInRemoteConstructionInstallsCreateVectorsOnlyOnce()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
|
RuntimeEntityRecord record = lifetime.Entities.AddActive(
|
|
Spawn(
|
|
0x70000042u,
|
|
1,
|
|
velocity: new Vector3(40f, 0f, 0f),
|
|
angularVelocity: new Vector3(0f, 0f, 2f)));
|
|
|
|
RemoteMotion remote =
|
|
lifetime.Physics.GetOrCreateRemoteMotion(record);
|
|
|
|
Assert.Equal(new Vector3(40f, 0f, 0f), remote.Body.Velocity);
|
|
Assert.Equal(new Vector3(0f, 0f, 2f), remote.Body.Omega);
|
|
remote.Body.set_velocity(new Vector3(8f, 0f, 0f));
|
|
remote.Body.Omega = new Vector3(0f, 0f, 3f);
|
|
|
|
Assert.Same(
|
|
remote,
|
|
lifetime.Physics.GetOrCreateRemoteMotion(record));
|
|
Assert.Equal(new Vector3(8f, 0f, 0f), remote.Body.Velocity);
|
|
Assert.Equal(new Vector3(0f, 0f, 3f), remote.Body.Omega);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuiltInMovementActivationUsesExactRuntimeIncarnation()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
|
RuntimeEntityRecord ordinary =
|
|
lifetime.Entities.AddActive(Spawn(0x70000043u, 1));
|
|
RemoteMotion ordinaryRemote =
|
|
lifetime.Physics.GetOrCreateRemoteMotion(ordinary);
|
|
ordinary.ObjectClock.Deactivate();
|
|
ordinaryRemote.Body.TransientState &=
|
|
~TransientStateFlags.Active;
|
|
|
|
ordinaryRemote.Movement.ActivatePhysicsObject!();
|
|
|
|
Assert.True(ordinary.ObjectClock.IsActive);
|
|
Assert.True(ordinaryRemote.Body.IsActive);
|
|
|
|
RuntimeEntityRecord staticRecord =
|
|
lifetime.Entities.AddActive(Spawn(0x70000044u, 1));
|
|
lifetime.Entities.SetFinalPhysicsState(
|
|
staticRecord,
|
|
PhysicsStateFlags.Static);
|
|
RemoteMotion staticRemote =
|
|
lifetime.Physics.GetOrCreateRemoteMotion(staticRecord);
|
|
staticRecord.ObjectClock.Deactivate();
|
|
staticRemote.Body.TransientState &=
|
|
~TransientStateFlags.Active;
|
|
|
|
staticRemote.Movement.ActivatePhysicsObject!();
|
|
|
|
Assert.False(staticRecord.ObjectClock.IsActive);
|
|
Assert.False(staticRemote.Body.IsActive);
|
|
}
|
|
|
|
[Fact]
|
|
public void RemotePhysicsTickUsesCanonicalRuntimeAndPublishesPoseSnapshot()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
|
RuntimeEntityRecord record =
|
|
lifetime.Entities.AddActive(Spawn(0x70000022u, 1));
|
|
var remote = new RemoteMotion();
|
|
remote.Body.Position = new Vector3(10f, 20f, 5f);
|
|
remote.Body.Orientation = Quaternion.Identity;
|
|
remote.Body.TransientState = TransientStateFlags.Active
|
|
| TransientStateFlags.Contact
|
|
| TransientStateFlags.OnWalkable;
|
|
lifetime.Physics.SetRemoteMotion(record, remote);
|
|
lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true);
|
|
var updater = new RuntimeRemotePhysicsUpdater(lifetime.Physics);
|
|
RuntimeRemotePhysicsSnapshot snapshot = default;
|
|
int publishes = 0;
|
|
|
|
Assert.True(updater.Tick(
|
|
record,
|
|
remote,
|
|
objectScale: 1f,
|
|
sequencer: null,
|
|
dt: 0.1f,
|
|
objectClockEpoch: record.ObjectClockEpoch,
|
|
new MotionDeltaFrame
|
|
{
|
|
Origin = Vector3.UnitX,
|
|
Orientation = Quaternion.Identity,
|
|
},
|
|
radius: 0.48f,
|
|
height: 1.835f,
|
|
liveCenterX: 1,
|
|
liveCenterY: 1,
|
|
acknowledgeProjection: value =>
|
|
{
|
|
snapshot = value;
|
|
publishes++;
|
|
return true;
|
|
}));
|
|
|
|
Assert.Equal(1, publishes);
|
|
Assert.True(snapshot.Position.X > 10f);
|
|
Assert.Equal(remote.Body.Position, snapshot.Position);
|
|
Assert.Equal(record.FullCellId, snapshot.FullCellId);
|
|
}
|
|
|
|
[Fact]
|
|
public void RemoteVelocityStalenessUsesInjectedRuntimeClock()
|
|
{
|
|
var time = new ManualTimeProvider(
|
|
DateTimeOffset.UnixEpoch.AddSeconds(100d));
|
|
using var lifetime = new RuntimeEntityObjectLifetime(
|
|
timeProvider: time);
|
|
RuntimeEntityRecord record =
|
|
lifetime.Entities.AddActive(Spawn(0x70000025u, 1));
|
|
var remote = new RemoteMotion
|
|
{
|
|
HasServerVelocity = true,
|
|
LastServerPosTime = 99d,
|
|
ServerVelocity = Vector3.UnitX,
|
|
};
|
|
remote.Body.Position = new Vector3(10f, 20f, 5f);
|
|
remote.Body.TransientState = TransientStateFlags.Active
|
|
| TransientStateFlags.Contact
|
|
| TransientStateFlags.OnWalkable;
|
|
lifetime.Physics.SetRemoteMotion(record, remote);
|
|
lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true);
|
|
var updater = new RuntimeRemotePhysicsUpdater(lifetime.Physics);
|
|
Vector3? cycle = null;
|
|
|
|
Assert.True(updater.Tick(
|
|
record,
|
|
remote,
|
|
objectScale: 1f,
|
|
sequencer: null,
|
|
dt: 0.1f,
|
|
objectClockEpoch: record.ObjectClockEpoch,
|
|
new MotionDeltaFrame
|
|
{
|
|
Orientation = Quaternion.Identity,
|
|
},
|
|
radius: 0.48f,
|
|
height: 1.835f,
|
|
liveCenterX: 1,
|
|
liveCenterY: 1,
|
|
applyStaleVelocityCycle: value => cycle = value));
|
|
|
|
Assert.False(remote.HasServerVelocity);
|
|
Assert.Equal(Vector3.Zero, remote.ServerVelocity);
|
|
Assert.Equal(Vector3.Zero, cycle);
|
|
}
|
|
|
|
[Fact]
|
|
public void PhysicsBodyAcquisitionIsCanonicalAndRejectsGuidReuse()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
|
RuntimeEntityRecord first =
|
|
lifetime.Entities.AddActive(Spawn(0x70000023u, 1));
|
|
var body = new PhysicsBody();
|
|
|
|
Assert.Same(
|
|
body,
|
|
lifetime.Physics.GetOrCreatePhysicsBody(first, _ => body));
|
|
Assert.Same(
|
|
body,
|
|
lifetime.Physics.GetOrCreatePhysicsBody(
|
|
first,
|
|
_ => throw new InvalidOperationException(
|
|
"The factory must not replay.")));
|
|
|
|
RuntimeEntityRecord stale =
|
|
lifetime.Entities.AddActive(Spawn(0x70000024u, 1));
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
lifetime.Physics.GetOrCreatePhysicsBody(
|
|
stale,
|
|
_ =>
|
|
{
|
|
Assert.True(lifetime.Entities.RemoveActive(stale));
|
|
lifetime.Entities.AddActive(Spawn(0x70000024u, 2));
|
|
return new PhysicsBody();
|
|
}));
|
|
Assert.Null(stale.PhysicsBody);
|
|
Assert.False(stale.PhysicsBodyAcquisitionInProgress);
|
|
}
|
|
|
|
private sealed class ManualTimeProvider(DateTimeOffset utcNow)
|
|
: TimeProvider
|
|
{
|
|
public override DateTimeOffset GetUtcNow() => utcNow;
|
|
}
|
|
|
|
[Fact]
|
|
public void PhysicsHostIdentityAndLookupBelongToExactRuntimeIncarnation()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
|
RuntimeEntityRecord record =
|
|
lifetime.Entities.AddActive(Spawn(0x70000025u, 1));
|
|
EntityPhysicsHost initial = Host(record.ServerGuid, 1f);
|
|
EntityPhysicsHost replacement = Host(record.ServerGuid, 2f);
|
|
|
|
Assert.Same(
|
|
initial,
|
|
lifetime.Physics.InstallOrRebindPhysicsHost(record, initial));
|
|
Assert.True(
|
|
lifetime.Physics.TryGetPhysicsHost(
|
|
record.ServerGuid,
|
|
out IPhysicsObjHost resolved));
|
|
Assert.Same(initial, resolved);
|
|
Assert.Same(
|
|
initial,
|
|
lifetime.Physics.InstallOrRebindPhysicsHost(record, replacement));
|
|
Assert.Equal(2f, initial.Position.Frame.Origin.X);
|
|
|
|
Assert.True(lifetime.Entities.RemoveActive(record));
|
|
RuntimeEntityRecord next =
|
|
lifetime.Entities.AddActive(Spawn(record.ServerGuid, 2));
|
|
Assert.False(
|
|
lifetime.Physics.TryGetPhysicsHost(
|
|
record.ServerGuid,
|
|
out _));
|
|
Assert.Null(next.PhysicsHost);
|
|
}
|
|
|
|
[Fact]
|
|
public void OrdinaryCellCommitIsCanonicalBeforePublication()
|
|
{
|
|
using var lifetime = new RuntimeEntityObjectLifetime();
|
|
RuntimeEntityRecord record =
|
|
lifetime.Entities.AddActive(Spawn(0x70000026u, 1));
|
|
var body = new PhysicsBody();
|
|
lifetime.Entities.SetPhysicsBody(record, body);
|
|
lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true);
|
|
RuntimePhysicsCellCommit observed = default;
|
|
lifetime.Physics.CellCommitted += value =>
|
|
{
|
|
Assert.Equal(value.FullCellId, value.Record.FullCellId);
|
|
observed = value;
|
|
};
|
|
|
|
Assert.True(lifetime.Physics.CommitOrdinaryCell(
|
|
record,
|
|
body,
|
|
record.ObjectClockEpoch,
|
|
0x02020001u,
|
|
externalOwnerValid: null));
|
|
|
|
Assert.Equal(0x02020001u, record.FullCellId);
|
|
Assert.Same(record, observed.Record);
|
|
Assert.Equal(record.SpatialAuthorityVersion, observed.SpatialAuthorityVersion);
|
|
}
|
|
|
|
private static RuntimeLandblockCollisionAssets CollisionAssets(
|
|
uint landblockId)
|
|
{
|
|
var heights = new byte[81];
|
|
var table = new float[256];
|
|
return new RuntimeLandblockCollisionAssets(
|
|
landblockId,
|
|
new TerrainSurface(heights, table),
|
|
Array.Empty<CellSurface>(),
|
|
Array.Empty<PortalPlane>(),
|
|
0f,
|
|
0f,
|
|
0u);
|
|
}
|
|
|
|
private static WorldSession.EntitySpawn Spawn(
|
|
uint guid,
|
|
ushort instance,
|
|
Vector3? velocity = null,
|
|
Vector3? angularVelocity = null)
|
|
{
|
|
var position = new CreateObject.ServerPosition(
|
|
0x0101FFFFu,
|
|
10f,
|
|
20f,
|
|
5f,
|
|
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: instance);
|
|
var physics = new PhysicsSpawnData(
|
|
RawState: 0x408u,
|
|
Position: position,
|
|
Movement: null,
|
|
AnimationFrame: null,
|
|
SetupTableId: 0x02000001u,
|
|
MotionTableId: 0x09000001u,
|
|
SoundTableId: null,
|
|
PhysicsScriptTableId: null,
|
|
Parent: null,
|
|
Children: null,
|
|
Scale: null,
|
|
Friction: null,
|
|
Elasticity: null,
|
|
Translucency: null,
|
|
Velocity: velocity,
|
|
Acceleration: null,
|
|
AngularVelocity: angularVelocity,
|
|
DefaultScriptType: null,
|
|
DefaultScriptIntensity: null,
|
|
Timestamps: timestamps);
|
|
return new WorldSession.EntitySpawn(
|
|
guid,
|
|
position,
|
|
0x02000001u,
|
|
Array.Empty<CreateObject.AnimPartChange>(),
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
|
null,
|
|
null,
|
|
"fixture",
|
|
null,
|
|
null,
|
|
0x09000001u,
|
|
PhysicsState: 0x408u,
|
|
InstanceSequence: instance,
|
|
MovementSequence: 1,
|
|
ServerControlSequence: 1,
|
|
PositionSequence: 1,
|
|
Physics: physics);
|
|
}
|
|
|
|
private static EntityPhysicsHost Host(uint guid, float x) =>
|
|
new(
|
|
guid,
|
|
getPosition: () => new Position(
|
|
0x0101FFFFu,
|
|
new Vector3(x, 0f, 0f),
|
|
Quaternion.Identity),
|
|
getVelocity: () => Vector3.Zero,
|
|
getRadius: () => 0.48f,
|
|
inContact: () => true,
|
|
minterpMaxSpeed: () => null,
|
|
curTime: () => 0d,
|
|
physicsTimerTime: () => 0d,
|
|
getObjectA: _ => null,
|
|
handleUpdateTarget: _ => { },
|
|
interruptCurrentMovement: () => { });
|
|
|
|
private sealed class TestRemoteMotion : IRuntimeRemoteMotion
|
|
{
|
|
public PhysicsBody Body { get; } = new();
|
|
}
|
|
}
|