using System.Numerics; using AcDream.App.World; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; namespace AcDream.App.Tests.World; public sealed class InboundPhysicsStateControllerTests { [Fact] public void SameGenerationCreate_RoutesChannelsIndependently() { var controller = new InboundPhysicsStateController(); CreateObject.ServerPosition original = Position(0x0101FFFFu, 10f); CreateObject.ServerPosition incoming = Position(0x0101FFFFu, 20f); controller.AcceptCreate(Spawn(0x70000001u, 1, 10, 10, original, 0x408u)); InboundCreateResult refresh = controller.AcceptCreate( Spawn(0x70000001u, 1, 9, 11, incoming, 0x448u)); Assert.Equal(CreateObjectTimestampDisposition.ExistingGeneration, refresh.Disposition); Assert.NotNull(refresh.SameGenerationEvents); SameGenerationCreateObjectEvents events = refresh.SameGenerationEvents.Value; Assert.True(controller.TryApplyPosition( events.Position!.Value, isLocalPlayer: false, forcePositionRotation: null, currentLocalVelocity: null, out PositionTimestampDisposition positionResult, out _, out _)); Assert.Equal(PositionTimestampDisposition.Rejected, positionResult); Assert.True(controller.TryApplyState(events.State, out _)); Assert.True(controller.TryGetSnapshot(0x70000001u, out WorldSession.EntitySpawn accepted)); Assert.Equal(original, accepted.Position); Assert.Equal(0x448u, accepted.PhysicsState); } [Fact] public void PickupRetainsGeneration_AndFreshPositionReturnsToWorld() { var controller = new InboundPhysicsStateController(); controller.AcceptCreate(Spawn( 0x70000002u, 7, 20, 1, Position(0x0101FFFFu, 10f), 0x408u)); Assert.True(controller.TryApplyPickup( new PickupEvent.Parsed(0x70000002u, 7, 21), out WorldSession.EntitySpawn pickedUp)); Assert.Null(pickedUp.Position); var update = new WorldSession.EntityPositionUpdate( 0x70000002u, Position(0x0101FFFFu, 30f), null, null, true, 7, 22, 0, 0); Assert.True(controller.TryApplyPosition( update, false, null, null, out var disposition, out WorldSession.EntitySpawn returned, out _)); Assert.Equal(PositionTimestampDisposition.Apply, disposition); Assert.Equal(30f, returned.Position!.Value.PositionX); } [Fact] public void PositionPlacementAbsentAndPresentZeroBothApplyRetailZero() { var controller = new InboundPhysicsStateController(); WorldSession.EntitySpawn seed = Spawn( 0x70000009u, 7, 20, 1, Position(0x0101FFFFu, 10f), 0x408u); seed = seed with { PlacementId = 7, Physics = seed.Physics!.Value with { AnimationFrame = 7 }, }; controller.AcceptCreate(seed); Assert.True(controller.TryApplyPosition( new WorldSession.EntityPositionUpdate( seed.Guid, Position(0x0101FFFFu, 20f), null, null, true, 7, 21, 0, 0), false, null, null, out PositionTimestampDisposition firstDisposition, out WorldSession.EntitySpawn first, out _)); Assert.Equal(PositionTimestampDisposition.Apply, firstDisposition); Assert.Equal((uint)0, first.PlacementId); Assert.Equal((uint)0, first.Physics!.Value.AnimationFrame); Assert.True(controller.TryApplyPosition( new WorldSession.EntityPositionUpdate( seed.Guid, Position(0x0101FFFFu, 30f), null, 0, true, 7, 22, 0, 0), false, null, null, out PositionTimestampDisposition secondDisposition, out WorldSession.EntitySpawn second, out _)); Assert.Equal(PositionTimestampDisposition.Apply, secondDisposition); Assert.Equal((uint)0, second.PlacementId); Assert.Equal((uint)0, second.Physics!.Value.AnimationFrame); } [Fact] public void RemotePositionWithoutVelocityAppliesUnpackedZeroVector() { var controller = new InboundPhysicsStateController(); WorldSession.EntitySpawn seed = Spawn( 0x7000000Au, 7, 20, 1, Position(0x0101FFFFu, 10f), 0x408u); seed = seed with { Physics = seed.Physics!.Value with { Velocity = new Vector3(4f, 5f, 6f) }, }; controller.AcceptCreate(seed); Assert.True(controller.TryApplyPosition( new WorldSession.EntityPositionUpdate( seed.Guid, Position(0x0101FFFFu, 20f), null, null, true, 7, 21, 0, 0), false, null, null, out PositionTimestampDisposition disposition, out WorldSession.EntitySpawn accepted, out _)); Assert.Equal(PositionTimestampDisposition.Apply, disposition); Assert.Equal(Vector3.Zero, accepted.Physics!.Value.Velocity); } [Fact] public void LocalNormalCorrectionRetainsLiveVelocity_ButFreshTeleportZerosIt() { var controller = new InboundPhysicsStateController(); WorldSession.EntitySpawn seed = WithTimestamps( Spawn(0x5000000Au, 7, 20, 1, Position(0x0101FFFFu, 10f), 0x408u), teleport: 10); controller.AcceptCreate(seed); Vector3 liveVelocity = new(4f, 5f, 6f); Assert.True(controller.TryApplyPosition( new WorldSession.EntityPositionUpdate( seed.Guid, Position(0x0101FFFFu, 20f), new Vector3(90f, 80f, 70f), null, true, 7, 21, 10, 0), true, null, liveVelocity, out PositionTimestampDisposition normalDisposition, out WorldSession.EntitySpawn normal, out _)); Assert.Equal(PositionTimestampDisposition.Apply, normalDisposition); Assert.Equal(liveVelocity, normal.Physics!.Value.Velocity); Assert.True(controller.TryApplyPosition( new WorldSession.EntityPositionUpdate( seed.Guid, Position(0x0101FFFFu, 30f), new Vector3(9f, 8f, 7f), null, true, 7, 22, 11, 0), true, null, liveVelocity, out PositionTimestampDisposition teleportDisposition, out WorldSession.EntitySpawn teleported, out _)); Assert.Equal(PositionTimestampDisposition.Apply, teleportDisposition); Assert.Equal(Vector3.Zero, teleported.Physics!.Value.Velocity); } [Fact] public void ParentEventWaitsForParent_ThenConsumesChildPositionOnce() { var controller = new InboundPhysicsStateController(); controller.AcceptCreate(Spawn( 0x70000003u, 3, 4, 1, Position(0x0101FFFFu, 10f), 0x408u)); var update = new ParentEvent.Parsed( 0x70000004u, 0x70000003u, 1, 2, 9, 5); Assert.False(controller.TryApplyParent(update, out _)); controller.AcceptCreate(Spawn( 0x70000004u, 9, 1, 1, Position(0x0101FFFFu, 15f), 0x408u)); Assert.True(controller.TryApplyParent(update, out WorldSession.EntitySpawn attached)); Assert.Equal(0x70000004u, attached.ParentGuid); Assert.Null(attached.Position); Assert.False(controller.TryApplyParent(update, out _)); } [Fact] public void LocalDeleteIsRejected_AndClearDropsSessionState() { var controller = new InboundPhysicsStateController(); controller.AcceptCreate(Spawn( 0x50000001u, 4, 1, 1, Position(0x0101FFFFu, 10f), 0x408u)); Assert.False(controller.TryDelete(new DeleteObject.Parsed(0x50000001u, 4), true)); Assert.True(controller.TryGetSnapshot(0x50000001u, out _)); controller.Clear(); Assert.Empty(controller.Snapshots); Assert.False(controller.IsFreshTeleportStart(0x50000001u, 1)); } [Fact] public void DeleteForUnknownGenerationIsRejected() { var controller = new InboundPhysicsStateController(); Assert.False(controller.TryDelete( new DeleteObject.Parsed(0x5000FFFFu, 9), isLocalPlayer: false)); } [Fact] public void TeleportStartIsComparedWithoutAdvancingTeleportTimestamp() { var controller = new InboundPhysicsStateController(); WorldSession.EntitySpawn spawn = Spawn( 0x50000002u, 2, 1, 1, Position(0x0101FFFFu, 10f), 0x408u); PhysicsSpawnData physics = spawn.Physics!.Value with { Timestamps = spawn.Physics.Value.Timestamps with { Teleport = 10 }, }; controller.AcceptCreate(spawn with { Physics = physics }); Assert.True(controller.IsFreshTeleportStart(0x50000002u, 10)); Assert.False(controller.IsFreshTeleportStart(0x50000002u, 9)); Assert.True(controller.IsFreshTeleportStart(0x50000002u, 11)); Assert.True(controller.IsFreshTeleportStart(0x50000002u, 11)); } [Fact] public void RejectedServerControlStillMirrorsConsumedMovementTimestamp() { var controller = new InboundPhysicsStateController(); WorldSession.EntitySpawn spawn = WithTimestamps( Spawn(0x70000005u, 3, 1, 1, Position(0x0101FFFFu, 10f), 0x408u), movement: 1, serverControl: 5); controller.AcceptCreate(spawn); bool applied = controller.TryApplyMotion( new WorldSession.EntityMotionUpdate( spawn.Guid, new CreateObject.ServerMotionState(0x3d, 0x11), 3, 2, 4, false), retainPayload: true, out _, out _); Assert.False(applied); Assert.True(controller.TryGetSnapshot(spawn.Guid, out WorldSession.EntitySpawn retained)); Assert.Equal((ushort)2, retained.MovementSequence); Assert.Equal((ushort)5, retained.ServerControlSequence); Assert.Equal((ushort)2, retained.Physics!.Value.Timestamps.Movement); Assert.Equal((ushort)5, retained.Physics.Value.Timestamps.ServerControlledMove); Assert.Equal(spawn.MotionState, retained.MotionState); } [Fact] public void AutonomousLocalEchoRetainsPayloadButMirrorsAcceptedTimestamps() { var controller = new InboundPhysicsStateController(); WorldSession.EntitySpawn spawn = WithTimestamps( Spawn(0x50000006u, 3, 1, 1, Position(0x0101FFFFu, 10f), 0x408u), movement: 1, serverControl: 5) with { MotionState = new CreateObject.ServerMotionState(0x3d, 0x10), }; controller.AcceptCreate(spawn); Assert.True(controller.TryApplyMotion( new WorldSession.EntityMotionUpdate( spawn.Guid, new CreateObject.ServerMotionState(0x3d, 0x12), 3, 2, 5, true), retainPayload: false, out WorldSession.EntitySpawn retained, out _)); Assert.Equal(spawn.MotionState, retained.MotionState); Assert.Equal((ushort)2, retained.MovementSequence); Assert.Equal((ushort)5, retained.ServerControlSequence); Assert.Equal((ushort)2, retained.Physics!.Value.Timestamps.Movement); } [Fact] public void FreshForceWithOlderTeleportMirrorsForceButRejectsPose() { var controller = new InboundPhysicsStateController(); WorldSession.EntitySpawn spawn = WithTimestamps( Spawn(0x50000007u, 3, 10, 1, Position(0x0101FFFFu, 10f), 0x408u), teleport: 10, forcePosition: 0); controller.AcceptCreate(spawn); Assert.True(controller.TryApplyPosition( new WorldSession.EntityPositionUpdate( spawn.Guid, Position(0x0101FFFFu, 99f), null, null, true, 3, 11, 9, 1), isLocalPlayer: true, forcePositionRotation: Quaternion.Identity, currentLocalVelocity: Vector3.Zero, out PositionTimestampDisposition disposition, out WorldSession.EntitySpawn retained, out _)); Assert.Equal(PositionTimestampDisposition.Rejected, disposition); Assert.Equal(10f, retained.Position!.Value.PositionX); Assert.Equal((ushort)10, retained.PositionSequence); Assert.Equal((ushort)1, retained.Physics!.Value.Timestamps.ForcePosition); Assert.Equal((ushort)10, retained.Physics.Value.Timestamps.Position); } [Fact] public void ForcePositionStoresPreservedLocalHeadingForLaterHydration() { var controller = new InboundPhysicsStateController(); WorldSession.EntitySpawn spawn = WithTimestamps( Spawn(0x50000008u, 3, 10, 1, Position(0x0101FFFFu, 10f), 0x408u), teleport: 10, forcePosition: 0); controller.AcceptCreate(spawn); Quaternion heading = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.75f); Vector3 liveVelocity = new(1f, 2f, 3f); Assert.True(controller.TryApplyPosition( new WorldSession.EntityPositionUpdate( spawn.Guid, Position(0x0101FFFFu, 99f), new Vector3(90f, 80f, 70f), null, true, 3, 9, 10, 1), isLocalPlayer: true, forcePositionRotation: heading, currentLocalVelocity: liveVelocity, out PositionTimestampDisposition disposition, out WorldSession.EntitySpawn retained, out _)); Assert.Equal(PositionTimestampDisposition.ForcePosition, disposition); Assert.Equal(99f, retained.Position!.Value.PositionX); Assert.Equal(heading.W, retained.Position.Value.RotationW, precision: 6); Assert.Equal(heading.X, retained.Position.Value.RotationX, precision: 6); Assert.Equal(heading.Y, retained.Position.Value.RotationY, precision: 6); Assert.Equal(heading.Z, retained.Position.Value.RotationZ, precision: 6); Assert.Equal(retained.Position, retained.Physics!.Value.Position); Assert.Equal(liveVelocity, retained.Physics.Value.Velocity); } private static WorldSession.EntitySpawn WithTimestamps( WorldSession.EntitySpawn spawn, ushort? movement = null, ushort? serverControl = null, ushort? teleport = null, ushort? forcePosition = null) { PhysicsSpawnData physics = spawn.Physics!.Value; PhysicsTimestamps timestamps = physics.Timestamps with { Movement = movement ?? physics.Timestamps.Movement, ServerControlledMove = serverControl ?? physics.Timestamps.ServerControlledMove, Teleport = teleport ?? physics.Timestamps.Teleport, ForcePosition = forcePosition ?? physics.Timestamps.ForcePosition, }; return spawn with { MovementSequence = timestamps.Movement, ServerControlSequence = timestamps.ServerControlledMove, Physics = physics with { Timestamps = timestamps }, }; } private static WorldSession.EntitySpawn Spawn( uint guid, ushort instance, ushort positionSequence, ushort stateSequence, CreateObject.ServerPosition? position, uint state) { var timestamps = new PhysicsTimestamps( positionSequence, 1, stateSequence, 1, 0, 1, 0, 1, instance); var physics = new PhysicsSpawnData( RawState: 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); return new WorldSession.EntitySpawn( guid, position, 0x02000001u, Array.Empty(), Array.Empty(), Array.Empty(), null, null, "fixture", null, null, 0x09000001u, PhysicsState: state, InstanceSequence: instance, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: positionSequence, Physics: physics); } private static CreateObject.ServerPosition Position(uint cell, float x) => new(cell, x, 10f, 5f, 1f, 0f, 0f, 0f); }