using System.Numerics; using AcDream.App.Physics; using AcDream.App.Rendering; using AcDream.App.Rendering.Vfx; using AcDream.Core.Physics; using AcDream.Core.World; 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_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); } 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; } }