using System.Net; using AcDream.Core.Combat; using AcDream.Core.Items; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Spells; using AcDream.Runtime.Entities; using AcDream.Runtime.Gameplay; using AcDream.Runtime.Session; namespace AcDream.Runtime.Tests.Session; public sealed class DirectGameRuntimeCommandAdapterTests { [Fact] public void DirectRouteSendsTypedChatAndPortalAndRejectsOldGeneration() { var operations = new FixtureSessionOperations(); var gameplay = new FixtureGameplayOperations(); using var runtime = new GameRuntime(new GameRuntimeDependencies( gameplay, gameplay, gameplay, gameplay, SessionOperations: operations)); gameplay.Bind(runtime); var resetHost = new FixtureResetHost(); DirectGameRuntimeCommandAdapter? adapter = null; LiveSessionConnectOptions options = new( true, "127.0.0.1", 9000, "account", "password"); var live = new LiveSessionHost( runtime.Session, new LiveSessionHostBindings( new LiveSessionRoutingFactories( _ => new FixtureEventRoute(), session => adapter!.CreateRoute(session)), generation => runtime.ResetGeneration( generation, resetHost), new LiveSessionSelectionBindings( id => runtime.PlayerIdentity.ServerGuid = id, _ => { }, runtime.CommunicationOwner.Chat.SetLocalPlayerGuid, _ => { }, _ => { }, runtime.ActionOwner.Combat.Clear), new LiveSessionEnteredWorldBindings( _ => { }, () => { }, () => { }, _ => { }, () => { }), (_, _, _) => { }, () => { }), options); adapter = new DirectGameRuntimeCommandAdapter(runtime, live); var trace = new RuntimeTraceRecorder(); using IDisposable subscription = runtime.Subscribe(trace); RuntimeSessionStartResult started = adapter.Session.Start(runtime.Generation); RuntimeGenerationToken firstGeneration = runtime.Generation; var gameActions = new List(); operations.Sessions[^1].GameActionCapture = body => gameActions.Add(body); const uint selectedObject = 0x70000001u; runtime.InventoryOwner.Objects.AddOrUpdate(new ClientObject { ObjectId = selectedObject, Type = ItemType.Misc, }); RuntimeCommandResult chat = adapter.Chat.Execute( runtime.Generation, new RuntimeChatCommand( RuntimeChatChannel.Say, "hello")); RuntimeCommandResult portal = adapter.Portal.Execute( runtime.Generation, RuntimePortalCommand.RecallLifestone); runtime.CommunicationOwner.TurbineChat.OnChannelsReceived( allegianceRoom: 0x10u, generalRoom: 0x11u, tradeRoom: 0x12u, lfgRoom: 0x13u, roleplayRoom: 0x14u, olthoiRoom: 0x15u, societyRoom: 0x16u, societyCelestialHandRoom: 0u, societyEldrytchWebRoom: 0u, societyRadiantBloodRoom: 0u); RuntimeCommandResult[] stateAndWireCommands = [ adapter.Selection.SelectObject( runtime.Generation, selectedObject), adapter.Selection.Clear(runtime.Generation), adapter.Movement.SetIntent( runtime.Generation, new MovementInput(Forward: true, Run: true)), adapter.Movement.ClearIntent(runtime.Generation), adapter.Movement.Execute( runtime.Generation, RuntimeMovementCommand.Stop), adapter.Chat.Execute( runtime.Generation, new RuntimeChatCommand( RuntimeChatChannel.Fellowship, "group")), adapter.Chat.Execute( runtime.Generation, new RuntimeChatCommand( RuntimeChatChannel.General, "global")), adapter.InventoryState.AddShortcut( runtime.Generation, new RuntimeShortcutCommand(0, 0x70000001u, 0u)), adapter.InventoryState.RemoveShortcut( runtime.Generation, 0), adapter.Spellbook.AddFavorite( runtime.Generation, tabIndex: 0, position: 0, spellId: 7u), adapter.Spellbook.RemoveFavorite( runtime.Generation, tabIndex: 0, spellId: 7u), adapter.Spellbook.SetFilter( runtime.Generation, filters: 3u), adapter.Spellbook.ForgetSpell( runtime.Generation, spellId: 7u), adapter.Spellbook.SetDesiredComponent( runtime.Generation, componentId: 11u, amount: 3u), adapter.Spellbook.ClearDesiredComponents( runtime.Generation), adapter.Character.Advance( runtime.Generation, new RuntimeAdvancementCommand( RuntimeAdvancementKind.Attribute, StatId: 1u, Cost: 10u)), adapter.Character.Advance( runtime.Generation, new RuntimeAdvancementCommand( RuntimeAdvancementKind.Vital, StatId: 2u, Cost: 11u)), adapter.Character.Advance( runtime.Generation, new RuntimeAdvancementCommand( RuntimeAdvancementKind.Skill, StatId: 3u, Cost: 12u)), adapter.Character.Advance( runtime.Generation, new RuntimeAdvancementCommand( RuntimeAdvancementKind.TrainSkill, StatId: 4u, Cost: 1u)), adapter.Character.SetOptions1( runtime.Generation, options: 0x1234u), adapter.Social.Execute( runtime.Generation, new RuntimeFriendCommand( RuntimeFriendCommandKind.Add, Name: "Friend")), adapter.Social.Execute( runtime.Generation, new RuntimeFriendCommand( RuntimeFriendCommandKind.Remove, CharacterId: 0x50000003u)), adapter.Social.Execute( runtime.Generation, new RuntimeFriendCommand( RuntimeFriendCommandKind.Clear)), adapter.Social.Execute( runtime.Generation, new RuntimeSquelchCommand( RuntimeSquelchScope.Character, Add: true, CharacterId: 0x50000004u, Name: "Muted")), adapter.Social.Execute( runtime.Generation, new RuntimeSquelchCommand( RuntimeSquelchScope.Account, Add: true, Name: "AccountMuted")), adapter.Social.Execute( runtime.Generation, new RuntimeSquelchCommand( RuntimeSquelchScope.Global, Add: true, MessageType: 2u)), ]; RuntimeSessionStartResult reconnected = adapter.Session.Reconnect(runtime.Generation); RuntimeCommandResult stale = adapter.Chat.Execute( firstGeneration, new RuntimeChatCommand( RuntimeChatChannel.Say, "stale")); RuntimeCommandResult staleMovement = adapter.Movement.SetIntent( firstGeneration, new MovementInput(Forward: true)); Assert.Equal(RuntimeSessionStartStatus.Connected, started.Status); Assert.Equal( RuntimeSessionStartStatus.Connected, reconnected.Status); Assert.True(chat.Accepted); Assert.True(portal.Accepted); Assert.All( stateAndWireCommands, result => Assert.Equal( RuntimeCommandStatus.Accepted, result.Status)); Assert.Equal( RuntimeCommandStatus.StaleGeneration, stale.Status); Assert.Equal( RuntimeCommandStatus.StaleGeneration, staleMovement.Status); Assert.False(runtime.MovementOwner.HasCommandInput); Assert.True(gameActions.Count >= 20); Assert.Contains( trace.Entries, entry => entry.Kind == RuntimeTraceKind.Command && (entry.Code >> 16) == (int)RuntimeCommandDomain.Chat && entry.Text == "hello"); Assert.Contains( trace.Entries, entry => entry.Kind == RuntimeTraceKind.Command && (entry.Code >> 16) == (int)RuntimeCommandDomain.Portal); Assert.Contains( trace.Entries, entry => entry.Kind == RuntimeTraceKind.Combat); RuntimeTeardownAcknowledgement stopped = adapter.Session.Stop(runtime.Generation); Assert.True(stopped.IsComplete); Assert.False(runtime.Session.IsInWorld); } private sealed class FixtureSessionOperations : ILiveSessionOperations { public List Sessions { get; } = []; public IPEndPoint ResolveEndpoint(string host, int port) => new(IPAddress.Loopback, port); public WorldSession CreateSession(IPEndPoint endpoint) { var session = new WorldSession( endpoint, new FixtureTransport()); Sessions.Add(session); return session; } public void Connect( WorldSession session, string user, string password) { } public CharacterList.Parsed GetCharacters(WorldSession session) => new( 0u, [ new CharacterList.Character( 0x50000001u, "Direct", 0u), ], [], 11, "account", true, true); public void EnterWorld( WorldSession session, int activeCharacterIndex) { } public void Tick(WorldSession session) { } public void DisposeSession(WorldSession session) => session.Dispose(); } private sealed class FixtureEventRoute : ILiveSessionEventRouting { public void Attach() { } public void Dispose() { } } private sealed class FixtureResetHost : IRuntimeGenerationResetHost { public void RetireEntityProjection(RuntimeEntityRecord entity) { } public void DrainEntityProjectionBoundary() { } public void CompleteEntityProjectionRetirement() { } } 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 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() { } } }