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(); var remotes = new List(); 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(() => 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(() => 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(() => first.Physics.AdmitCollisionAssets( admission, CollisionAssets(0xA9B4FFFFu))); Assert.Throws(() => 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(() => 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 RemoteBindingOwnsCanonicalBodyCellAndTypedCellPublication() { using var lifetime = new RuntimeEntityObjectLifetime(); RuntimeEntityRecord record = lifetime.Entities.AddActive(Spawn(0x70000021u, 1)); var remote = new RemoteMotion(); var commits = new List(); 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 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 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(() => 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); } [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(), Array.Empty(), 0f, 0f, 0u); } private static WorldSession.EntitySpawn Spawn(uint guid, ushort instance) { 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: null, Acceleration: null, AngularVelocity: null, DefaultScriptType: null, DefaultScriptIntensity: null, Timestamps: timestamps); return new WorldSession.EntitySpawn( guid, position, 0x02000001u, Array.Empty(), Array.Empty(), Array.Empty(), 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(); } }