using System.Numerics; using AcDream.App.Physics; using AcDream.App.Rendering; using AcDream.App.Rendering.Vfx; using AcDream.App.Streaming; using AcDream.App.World; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.World; using DatReaderWriter.DBObjs; namespace AcDream.App.Tests.Physics; public sealed class RemotePhysicsUpdaterTests { [Fact] public void TickHidden_AppliesPositionManagerOffsetWithoutAdvancingPhysics() { var motion = new GameWindow.RemoteMotion(); motion.Body.Position = Vector3.Zero; motion.Body.Orientation = Quaternion.Identity; motion.Body.Velocity = new Vector3(4f, 0f, 5f); motion.Body.Acceleration = new Vector3(0f, 0f, -9.8f); motion.CellId = 0x01010001u; motion.Interp.Enqueue( new Vector3(1f, 0f, 0f), heading: 0f, isMovingTo: false, currentBodyPosition: Vector3.Zero); var entity = new WorldEntity { Id = 1u, ServerGuid = 0x70000001u, SourceGfxObjOrSetupId = 0x02000001u, Position = Vector3.Zero, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), }; var updater = new RemotePhysicsUpdater( new PhysicsEngine(), (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); var poses = new EntityEffectPoseRegistry(); poses.Publish(entity, Array.Empty()); updater.TickHidden(motion, entity, 0.1f); Assert.True(poses.UpdateRoot(entity)); Assert.InRange(motion.Body.Position.X, 0.01f, 1f); Assert.Equal(0f, motion.Body.Position.Z); Assert.Equal(new Vector3(4f, 0f, 5f), motion.Body.Velocity); Assert.Equal(motion.Body.Position, entity.Position); Assert.Equal(motion.CellId, entity.ParentCellId); Assert.True(poses.TryGetRootPose(entity.Id, out Matrix4x4 root)); Assert.Equal(entity.Position, root.Translation); } [Fact] public void TickHidden_RunsPartArrayManagerTailWithoutAdvancingSequence() { var updater = new RemotePhysicsUpdater( new PhysicsEngine(), (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); var motion = new GameWindow.RemoteMotion(); motion.Body.Orientation = Quaternion.Identity; var entity = new WorldEntity { Id = 2u, ServerGuid = 0x70000002u, SourceGfxObjOrSetupId = 0x02000001u, Position = Vector3.Zero, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), }; int partArrayTailCalls = 0; updater.TickHidden( motion, entity, 0.1f, () => partArrayTailCalls++); Assert.Equal(1, partArrayTailCalls); } [Fact] public void TickHidden_CrossCellCommitUpdatesCanonicalCellBeforeUnhide() { var engine = BuildBoundaryEngine(); var updater = new RemotePhysicsUpdater( engine, (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); var motion = new GameWindow.RemoteMotion(); motion.Body.Position = new Vector3(191f, 10f, 50f); motion.Body.Orientation = Quaternion.Identity; motion.Body.TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable; uint canonicalCell = 0xA9B40039u; int canonicalWrites = 0; motion.BindCanonicalCell( () => canonicalCell, value => { canonicalCell = value; canonicalWrites++; }); motion.CellId = canonicalCell; canonicalWrites = 0; motion.Interp.Enqueue( new Vector3(193f, 10f, 50f), heading: 0f, isMovingTo: false, currentBodyPosition: motion.Body.Position); var entity = new WorldEntity { Id = 1u, ServerGuid = 0x70000002u, SourceGfxObjOrSetupId = 0x02000001u, Position = motion.Body.Position, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), }; updater.TickHidden(motion, entity, 2f); Assert.Equal(0xAAB40001u, canonicalCell); Assert.Equal(canonicalCell, entity.ParentCellId); Assert.True(canonicalWrites > 0); Assert.True(entity.Position.X > 192f); } [Fact] public void TickHidden_LandingSynchronizesRemoteAirborneState() { var engine = BuildBoundaryEngine(); var updater = new RemotePhysicsUpdater( engine, (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); var motion = new GameWindow.RemoteMotion { Airborne = true, }; motion.Body.Position = new Vector3(10f, 10f, 50f); motion.Body.Orientation = Quaternion.Identity; motion.Body.TransientState = TransientStateFlags.Active; motion.CellId = 0xA9B40001u; motion.Interp.Enqueue( new Vector3(11f, 10f, 50f), heading: 0f, isMovingTo: false, currentBodyPosition: motion.Body.Position); var entity = new WorldEntity { Id = 3u, ServerGuid = 0x70000003u, SourceGfxObjOrSetupId = 0x02000001u, Position = motion.Body.Position, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), }; updater.TickHidden(motion, entity, 0.1f); Assert.True(motion.Body.InContact); Assert.True(motion.Body.OnWalkable); Assert.False(motion.Airborne); } [Fact] public void TickHiddenEntities_DeletionCallbackCannotAdvanceStaleSnapshotOwner() { const uint firstGuid = 0x70000011u; const uint secondGuid = 0x70000012u; var spatial = new GpuWorldState(); spatial.AddLandblock(new LoadedLandblock( 0x0101FFFFu, new LandBlock(), Array.Empty())); var live = new LiveEntityRuntime( spatial, new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { })); BindHiddenRemote(live, firstGuid); BindHiddenRemote(live, secondGuid); Assert.Equal(2, live.SpatialRemoteMotionRuntimes.Count); var updater = new RemotePhysicsUpdater( new PhysicsEngine(), (_, _) => (0.48f, 1.835f), (_, _, _, _) => { }); var published = new List(); updater.TickHiddenEntities( live, localPlayerServerGuid: 0x50000001u, dt: 0.1f, entity => { published.Add(entity.ServerGuid); uint other = entity.ServerGuid == firstGuid ? secondGuid : firstGuid; Assert.True(live.UnregisterLiveEntity( new DeleteObject.Parsed(other, InstanceSequence: 1), isLocalPlayer: false)); }); Assert.Single(published); Assert.Single(live.SpatialRemoteMotionRuntimes); } private static PhysicsEngine BuildBoundaryEngine() { static TerrainSurface FlatTerrain() { var heights = new byte[81]; var table = new float[256]; Array.Fill(table, 50f); return new TerrainSurface(heights, table); } var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() }; engine.AddLandblock( 0xA9B4FFFFu, FlatTerrain(), Array.Empty(), Array.Empty(), 0f, 0f); engine.AddLandblock( 0xAAB4FFFFu, FlatTerrain(), Array.Empty(), Array.Empty(), 192f, 0f); return engine; } private static void BindHiddenRemote(LiveEntityRuntime live, uint guid) { const uint cellId = 0x01010001u; PhysicsStateFlags state = PhysicsStateFlags.Hidden | PhysicsStateFlags.IgnoreCollisions; var position = new CreateObject.ServerPosition( cellId, 10f, 10f, 5f, 1f, 0f, 0f, 0f); var timestamps = new PhysicsTimestamps(1, 1, 1, 1, 0, 1, 0, 1, 1); var physics = new PhysicsSpawnData( RawState: (uint)state, 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); live.RegisterLiveEntity(new WorldSession.EntitySpawn( guid, position, 0x02000001u, Array.Empty(), Array.Empty(), Array.Empty(), null, null, "hidden fixture", null, null, 0x09000001u, PhysicsState: (uint)state, InstanceSequence: 1, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics)); WorldEntity entity = live.MaterializeLiveEntity( guid, cellId, id => new WorldEntity { Id = id, ServerGuid = guid, SourceGfxObjOrSetupId = 0x02000001u, Position = new Vector3(10f, 10f, 5f), Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), ParentCellId = cellId, })!; var remote = new GameWindow.RemoteMotion(); remote.Body.Position = entity.Position; remote.Body.Orientation = entity.Rotation; remote.CellId = cellId; remote.Interp.Enqueue( entity.Position + Vector3.UnitX, heading: 0f, isMovingTo: false, currentBodyPosition: entity.Position); live.SetRemoteMotionRuntime(guid, remote); } }