using System.Net; using AcDream.Core.Combat; using AcDream.Core.Items; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.Spells; using AcDream.Runtime.Entities; using AcDream.Runtime.Gameplay; using AcDream.Runtime.Session; using AcDream.Runtime.World; namespace AcDream.Runtime.Tests.Session; public sealed class RuntimeLiveEntitySessionControllerTests { [Fact] public void DirectSinkOwnsCanonicalCreateUpdateDeleteWithoutProjection() { using GameRuntime runtime = CreateRuntime(); using var session = new WorldSession( new IPEndPoint(IPAddress.Loopback, 9000), new FixtureTransport()); var controller = new RuntimeLiveEntitySessionController( runtime, session); LiveEntitySessionSink sink = controller.CreateSink(); WorldSession.EntitySpawn spawn = Spawn(0x70000001u, incarnation: 1); sink.Spawned(spawn); sink.PositionUpdated(new WorldSession.EntityPositionUpdate( spawn.Guid, spawn.Position!.Value with { PositionX = 20f, }, Velocity: null, PlacementId: null, IsGrounded: true, InstanceSequence: 1, PositionSequence: 2, TeleportSequence: 0, ForcePositionSequence: 0)); Assert.Equal(1, runtime.Entities.Count); Assert.Equal(1, runtime.Inventory.ObjectCount); Assert.True( runtime.EntityObjects.Entities.TryGetActive( spawn.Guid, out RuntimeEntityRecord canonical)); Assert.Equal( 20f, canonical.Snapshot.Position!.Value.PositionX); sink.Deleted(new DeleteObject.Parsed(spawn.Guid, 1)); Assert.Equal(0, runtime.Entities.Count); Assert.Equal(0, runtime.Inventory.ObjectCount); Assert.Equal( 0, runtime.EntityObjects.Entities.PendingTeardownCount); } [Fact] public void DirectSinkCompletesExactPortalAndSendsLoginComplete() { using GameRuntime runtime = CreateRuntime(); const uint playerGuid = 0x50000001u; runtime.PlayerIdentity.ServerGuid = playerGuid; using var session = new WorldSession( new IPEndPoint(IPAddress.Loopback, 9000), new FixtureTransport()); var gameActions = new List(); session.GameActionCapture = body => gameActions.Add(body); var controller = new RuntimeLiveEntitySessionController( runtime, session); LiveEntitySessionSink sink = controller.CreateSink(); WorldSession.EntitySpawn spawn = Spawn(playerGuid, incarnation: 1); sink.Spawned(spawn); sink.TeleportStarted(1u); sink.PositionUpdated(new WorldSession.EntityPositionUpdate( playerGuid, spawn.Position!.Value with { LandblockId = 0x01020001u, PositionX = 30f, }, Velocity: null, PlacementId: null, IsGrounded: true, InstanceSequence: 1, PositionSequence: 2, TeleportSequence: 1, ForcePositionSequence: 0)); Assert.Single(gameActions); Assert.True(runtime.TransitOwner.CaptureOwnership().IsSessionIdle); Assert.True(runtime.Portal.Snapshot.Completed); Assert.Equal(0x01020001u, runtime.Portal.Snapshot.DestinationCell); } [Fact] public void DirectSinkProjectsAcceptedLocalWorldStateThroughOneHostSeam() { using GameRuntime runtime = CreateRuntime(); const uint playerGuid = 0x50000002u; runtime.PlayerIdentity.ServerGuid = playerGuid; using var session = new WorldSession( new IPEndPoint(IPAddress.Loopback, 9000), new FixtureTransport()); session.GameActionCapture = _ => { }; var projection = new FixtureWorldProjection(); var controller = new RuntimeLiveEntitySessionController( runtime, session, worldProjection: projection); LiveEntitySessionSink sink = controller.CreateSink(); WorldSession.EntitySpawn spawn = Spawn(playerGuid, incarnation: 1); sink.Spawned(spawn); sink.TeleportStarted(1u); sink.PositionUpdated(new WorldSession.EntityPositionUpdate( playerGuid, spawn.Position!.Value with { LandblockId = 0x01020001u, PositionX = 30f, }, Velocity: null, PlacementId: null, IsGrounded: true, InstanceSequence: 1, PositionSequence: 2, TeleportSequence: 1, ForcePositionSequence: 0)); Assert.Equal(1, projection.SpawnCount); Assert.Equal(1, projection.PositionCount); Assert.Equal(1, projection.TeleportStartCount); Assert.Equal(1, projection.PrepareCount); Assert.True(projection.LastSpawnWasLocal); Assert.True(projection.LastPositionWasLocal); Assert.Equal( PositionTimestampDisposition.Apply, projection.LastPositionDisposition); Assert.Equal(playerGuid, projection.LastRecord?.ServerGuid); Assert.Equal(0x01020001u, projection.LastDestination.CellId); Assert.True(runtime.TransitOwner.CaptureOwnership().IsSessionIdle); Assert.True(runtime.Portal.Snapshot.Completed); } private static GameRuntime CreateRuntime() { var operations = new FixtureGameplayOperations(); var runtime = new GameRuntime(new GameRuntimeDependencies( operations, operations, operations, operations)); operations.Bind(runtime); return runtime; } private static WorldSession.EntitySpawn Spawn( uint guid, ushort incarnation) { var position = new CreateObject.ServerPosition( 0x01010001u, 10f, 10f, 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: incarnation); var physics = new PhysicsSpawnData( RawState: (uint)PhysicsStateFlags.ReportCollisions, Position: position, Movement: null, AnimationFrame: null, SetupTableId: 0x02000001u, MotionTableId: null, 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, [], [], [], null, null, "direct entity", null, null, null, PhysicsState: physics.RawState, InstanceSequence: incarnation, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics); } private sealed class FixtureTransport : IWorldSessionTransport { public void Send(ReadOnlySpan datagram) { } public void Send( IPEndPoint remote, ReadOnlySpan datagram) { } public int Receive( Span destination, TimeSpan timeout, out IPEndPoint? from) { from = null; return -1; } public ValueTask ReceiveAsync( Memory destination, CancellationToken cancellationToken) => ValueTask.FromException( new OperationCanceledException(cancellationToken)); public void Dispose() { } } private sealed class FixtureWorldProjection : IRuntimeDirectWorldProjection { public int SpawnCount { get; private set; } public int PositionCount { get; private set; } public int TeleportStartCount { get; private set; } public int PrepareCount { get; private set; } public bool LastSpawnWasLocal { get; private set; } public bool LastPositionWasLocal { get; private set; } public PositionTimestampDisposition LastPositionDisposition { get; private set; } public RuntimeEntityRecord? LastRecord { get; private set; } public RuntimeTeleportDestination LastDestination { get; private set; } public void ProjectSpawn( RuntimeEntityRecord record, bool isLocalPlayer) { SpawnCount++; LastRecord = record; LastSpawnWasLocal = isLocalPlayer; } public void ProjectPosition( RuntimeEntityRecord record, bool isLocalPlayer, PositionTimestampDisposition disposition) { PositionCount++; LastRecord = record; LastPositionWasLocal = isLocalPlayer; LastPositionDisposition = disposition; } public void BeginTeleport() => TeleportStartCount++; public RuntimeDestinationReadiness PrepareDestination( long revealGeneration, RuntimeTeleportDestination destination) { PrepareCount++; LastDestination = destination; bool indoor = (destination.CellId & 0xFFFFu) >= 0x0100u; return new RuntimeDestinationReadiness( revealGeneration, destination.CellId, indoor, IsUnhydratable: false, RequiredRenderRadius: indoor ? 0 : 1, IsRenderNeighborhoodReady: true, AreCompositeTexturesReady: true, IsCollisionReady: true); } } private sealed class FixtureGameplayOperations : IRuntimeCombatAttackOperations, IRuntimeCombatTargetOperations, IRuntimeCombatModeOperations, IRuntimeSpellCastOperations { private GameRuntime? _runtime; public void Bind(GameRuntime runtime) => _runtime = runtime; public bool CanStartAttack() => false; public void PrepareAttackRequest() { } public bool SendAttack(AttackHeight height, float power) => false; public void SendCancelAttack() { } public bool IsDualWield => false; public bool PlayerReadyForAttack => false; public bool AutoRepeatAttack => false; public bool AutoTarget => false; public uint? SelectClosestTarget() => null; public bool IsInWorld => _runtime?.Session.IsInWorld == true; public IReadOnlyList GetOrderedEquipment() => []; public void NotifyExplicitCombatModeRequest() { } public void SendChangeCombatMode(CombatMode mode) { } public uint LocalPlayerId => _runtime?.PlayerIdentity.ServerGuid ?? 0u; public bool CanSend => false; public bool HasRequiredComponents(uint spellId) => false; public bool IsTargetCompatible( uint targetId, SpellMetadata spell, bool showMessage) => false; public void StopCompletely() { } public void SendUntargeted(uint spellId) { } public void SendTargeted(uint targetId, uint spellId) { } public void DisplayMessage(string message) { } public void IncrementBusy() { } } }