using System.Runtime.CompilerServices; using AcDream.App.Net; using AcDream.App.UI; using AcDream.Core.Chat; using AcDream.Core.Items; using AcDream.Core.Net.Messages; using AcDream.Core.Social; using AcDream.Runtime.Gameplay; using AcDream.Runtime; using AcDream.Runtime.Session; using AcDream.UI.Abstractions; namespace AcDream.App.Tests.Net; public sealed class LiveSessionCommandRouterTests { [Fact] public void LoginCommandSequenceUsesTheIdenticalGraphicalCommandSurface() { using var communication = new RuntimeCommunicationState(); var calls = new List(); ClientCommandController.Bindings client = NewClientBindings() with { QueryAge = () => calls.Add("client:age"), }; var router = NewRouter( chat: communication.Chat, turbine: communication.TurbineChat, communication: communication, clientBindings: client, sendTalk: text => calls.Add($"talk:{text}"), sendTell: (target, text) => calls.Add($"tell:{target}:{text}"), sendChannel: (channel, text) => calls.Add($"channel:{channel:X8}:{text}")); var surface = new LiveSessionCommandSurface(); using ILiveSessionCommandRouting lease = surface.Attach(router); lease.Activate(); var sequence = new LoginCommandSequence( ["hello", "/tell Bob, secret", "@admin raw", "/age"], TimeSpan.Zero, new RuntimeChatCommandFeedback(communication), surface); sequence.EnteredWorld(new RuntimeGenerationToken(9)); Assert.Equal( [ "talk:hello", "tell:Bob:secret", "channel:00000002:raw", "client:age", ], calls); } [Fact] public void GraphicalRouteKeepsWireOnlyClientCommandParityWithHeadless() { var calls = new List(); ClientCommandController.Bindings client = NewClientBindings() with { AddPlayerPermission = name => calls.Add($"permit:add:{name}"), RemovePlayerPermission = name => calls.Add($"permit:remove:{name}"), ModifyGlobalSquelch = (add, messageType) => calls.Add($"squelch:{add}:{messageType}"), }; var router = NewRouter(clientBindings: client); router.Activate(); router.Publish(new ExecuteClientCommandCmd( ClientCommandId.Permit, "add Aunt Agatha")); router.Publish(new ExecuteClientCommandCmd( ClientCommandId.Permit, "remove Lord Gnarly Beard")); router.Publish(new ExecuteClientCommandCmd( ClientCommandId.ChatToggle, "on")); router.Publish(new ExecuteClientCommandCmd( ClientCommandId.ChatToggle, "off")); router.Publish(new ExecuteClientCommandCmd( ClientCommandId.NoTellToggle, "on")); router.Publish(new ExecuteClientCommandCmd( ClientCommandId.NoTellToggle, "off")); Assert.Equal( [ "permit:add:Aunt Agatha", "permit:remove:Lord Gnarly Beard", "squelch:False:2", "squelch:True:2", "squelch:True:3", "squelch:False:3", ], calls); } [Fact] public void InactiveAndDisposedRouter_CannotReachTransport() { var sent = new List(); var router = NewRouter(sendTalk: sent.Add); router.Publish(new SendServerCommandCmd("@before")); router.Activate(); router.Activate(); router.Publish(new SendServerCommandCmd("@active")); router.Dispose(); router.Dispose(); router.Publish(new SendServerCommandCmd("@after")); Assert.Equal(["@active"], sent); Assert.False(router.IsActive); } [Fact] public void CommandSurfacePublishesToOneBorrowedRouteAndRetiresStaleRoute() { var sent = new List(); var surface = new LiveSessionCommandSurface(); var first = NewRouter(sendTalk: text => sent.Add($"first:{text}")); var firstLease = surface.Attach(first); firstLease.Activate(); surface.Publish(new SendServerCommandCmd("@one")); var second = NewRouter(sendTalk: text => sent.Add($"second:{text}")); Assert.Throws(() => surface.Attach(second)); firstLease.Dispose(); first.Publish(new SendServerCommandCmd("@stale")); var secondLease = surface.Attach(second); secondLease.Activate(); surface.Publish(new SendServerCommandCmd("@two")); secondLease.Dispose(); Assert.Equal(["first:@one", "second:@two"], sent); Assert.False(first.IsActive); Assert.False(second.IsActive); } [Fact] public void TellAndLegacyChannel_PreserveOutboundAndEchoPolicy() { var tells = new List<(string Target, string Text)>(); var channels = new List<(uint Id, string Text)>(); var chat = new ChatLog(); var router = NewRouter( chat: chat, sendTell: (target, text) => tells.Add((target, text)), sendChannel: (id, text) => channels.Add((id, text))); router.Activate(); router.Publish(new SendChatCmd(ChatChannelKind.Tell, "Friend", "hello")); router.Publish(new SendChatCmd(ChatChannelKind.Fellowship, null, "group")); Assert.Equal([("Friend", "hello")], tells); Assert.Equal([(0x00000800u, "group")], channels); // CH3 (2026-08-09): Fellowship is one of the ACE server-echoing // legacy channels (resends with an empty sender) — the router must // NOT also emit a local optimistic echo, or the line double-prints. // Only the Tell echo (which the server never resends) survives. Assert.Collection( chat.Snapshot(), entry => { Assert.Equal(ChatKind.Tell, entry.Kind); Assert.Equal("Friend", entry.Sender); }); } [Fact] public void TurbineChannel_UsesRuntimeRoomCookieAndNoOptimisticEcho() { var turbine = new TurbineChatState(); turbine.OnChannelsReceived( allegianceRoom: 0u, generalRoom: 0x70000001u, tradeRoom: 0u, lfgRoom: 0u, roleplayRoom: 0u, olthoiRoom: 0u, societyRoom: 0u, societyCelestialHandRoom: 0u, societyEldrytchWebRoom: 0u, societyRadiantBloodRoom: 0u); var sent = new List<(uint Room, uint Type, uint Dispatch, uint Sender, string Text, uint Cookie)>(); var chat = new ChatLog(); var router = NewRouter( chat: chat, turbine: turbine, playerGuid: () => 0x50000001u, sendTurbine: (room, type, dispatch, sender, text, cookie) => sent.Add((room, type, dispatch, sender, text, cookie))); router.Activate(); router.Publish(new SendChatCmd(ChatChannelKind.General, null, "world")); Assert.Collection(sent, message => { Assert.Equal(0x70000001u, message.Room); Assert.Equal(0x02u, message.Type); Assert.Equal(0x02u, message.Dispatch); Assert.Equal(0x50000001u, message.Sender); Assert.Equal("world", message.Text); Assert.Equal(1u, message.Cookie); }); Assert.Equal(0, chat.Count); } // ── Campaign CH slice CH3 (2026-08-09): Turbine membership gate ── [Fact] public void TurbineUnavailable_RaisesRetailStringThroughAddText_AndSendsNothing() { var communication = new RuntimeCommunicationState(); var sent = new List(); var router = NewRouter( chat: communication.Chat, communication: communication, turbine: new TurbineChatState(), // never received SetTurbineChatChannels sendTurbine: (_, _, _, _, text, _) => sent.Add(text)); router.Activate(); router.Publish(new SendChatCmd(ChatChannelKind.General, null, "hello")); Assert.Empty(sent); Assert.Equal(1, communication.Chat.Count); Assert.Equal( "Turbine chat is not available.", communication.Chat.Snapshot()[0].Text); } [Fact] public void HearOptionOff_RaisesNotListeningRefusal_AndSendsNothing() { // Fresh RuntimeCharacterOptionsState defaults omit HearRoleplayChat // (matches ACE CharacterOptions2.Default) — a populated Roleplay // room must still refuse locally rather than send. var turbine = new TurbineChatState(); turbine.OnChannelsReceived( 0u, 0u, 0u, 0u, roleplayRoom: 0x14u, 0u, 0u, 0u, 0u, 0u); var communication = new RuntimeCommunicationState(); var sent = new List(); var router = NewRouter( chat: communication.Chat, communication: communication, turbine: turbine, sendTurbine: (_, _, _, _, text, _) => sent.Add(text)); router.Activate(); router.Publish(new SendChatCmd(ChatChannelKind.Roleplay, null, "hi")); Assert.Empty(sent); Assert.Equal(1, communication.Chat.Count); Assert.Equal( "You are not listening to the Roleplay channel!", communication.Chat.Snapshot()[0].Text); } [Fact] public void AllegianceTurbineEnabledButNoRoom_RefusesLocally_NeverDowngradesToLegacyChannel() { // S3 (CH3 Opus review, 2026-08-09) corrected the original CH3 // headline /a fix (research doc §5.3): retail's @a is bound to // Turbine only once StartupTurbineChatSystem succeeds — i.e. once // Turbine chat is ENABLED. With Turbine up but no allegiance room // (this test), retail hits SendTurbineChat's own roomId<=0 branch // and refuses locally — it must NOT fall through to the legacy // AllegianceBroadcast bitflag. (Contrast the Enabled==false case // below, which DOES fall back — Turbine was never started at all.) var communication = new RuntimeCommunicationState(); var legacySent = new List<(uint Id, string Text)>(); var turbineSent = new List(); var turbine = new TurbineChatState(); turbine.OnChannelsReceived( allegianceRoom: 0u, generalRoom: 0x70000001u, tradeRoom: 0u, lfgRoom: 0u, roleplayRoom: 0u, olthoiRoom: 0u, societyRoom: 0u, societyCelestialHandRoom: 0u, societyEldrytchWebRoom: 0u, societyRadiantBloodRoom: 0u); // Enabled = true, AllegianceRoom stays 0 var router = NewRouter( chat: communication.Chat, communication: communication, turbine: turbine, sendChannel: (id, text) => legacySent.Add((id, text)), sendTurbine: (_, _, _, _, text, _) => turbineSent.Add(text)); router.Activate(); router.Publish(new SendChatCmd(ChatChannelKind.Allegiance, null, "guild hi")); Assert.Empty(legacySent); Assert.Empty(turbineSent); Assert.Equal( "Turbine chat is not available.", communication.Chat.Snapshot()[0].Text); } [Fact] public void AllegianceTurbineNeverStarted_FallsBackToLegacyAllegianceBroadcast() { // S3 (CH3 Opus review, 2026-08-09): retail's BASE binding keeps /a // on the legacy AllegianceBroadcast (0x02000000) channel until // Turbine chat successfully starts and StartupTurbineChatSystem // rebinds it (research doc §4.3) — with NO 0x0295 // SetTurbineChatChannels ever received, retail does not print // "Turbine chat is not available." (that refusal only exists once // the Turbine-bound verb has actually been armed). var chat = new ChatLog(); var legacySent = new List<(uint Id, string Text)>(); var turbineSent = new List(); var router = NewRouter( chat: chat, turbine: new TurbineChatState(), // never received SetTurbineChatChannels sendChannel: (id, text) => legacySent.Add((id, text)), sendTurbine: (_, _, _, _, text, _) => turbineSent.Add(text)); router.Activate(); router.Publish(new SendChatCmd(ChatChannelKind.Allegiance, null, "guild hi")); Assert.Equal([(0x02000000u, "guild hi")], legacySent); Assert.Empty(turbineSent); // S1: AllegianceBroadcast is now a server-echoing channel — no // local optimistic echo. Assert.Equal(0, chat.Count); } [Fact] public void AllegianceBroadcast_AbVerbChannel_RoutesLegacyWithNoLocalEcho() { // /ab (retail DoAllegianceBroadcast) rides the legacy 0x0147 pipe — // distinct from /a, which routes through Turbine whenever Turbine // chat is enabled (see the two Allegiance tests above). var chat = new ChatLog(); var legacySent = new List<(uint Id, string Text)>(); var router = NewRouter( chat: chat, sendChannel: (id, text) => legacySent.Add((id, text))); router.Activate(); router.Publish(new SendChatCmd( ChatChannelKind.AllegianceBroadcast, null, "to the whole allegiance")); Assert.Equal([(0x02000000u, "to the whole allegiance")], legacySent); // S1 (CH3 Opus review): ACE's GameActionChatChannel handler // includes the sender in its real-name Allegiance.Members // broadcast, so the client must NOT also echo locally. Assert.Equal(0, chat.Count); } // ── Campaign CH slice CH3: per-channel self-echo matrix ── [Theory] [InlineData(ChatChannelKind.Fellowship, 0x00000800u)] [InlineData(ChatChannelKind.Vassals, 0x00001000u)] [InlineData(ChatChannelKind.Patron, 0x00002000u)] [InlineData(ChatChannelKind.Monarch, 0x00004000u)] [InlineData(ChatChannelKind.CoVassals, 0x01000000u)] public void ServerEchoingLegacyChannels_SkipLocalOptimisticEcho( ChatChannelKind kind, uint expectedId) { var chat = new ChatLog(); var legacySent = new List<(uint Id, string Text)>(); var router = NewRouter( chat: chat, sendChannel: (id, text) => legacySent.Add((id, text))); router.Activate(); router.Publish(new SendChatCmd(kind, null, "hi")); Assert.Equal([(expectedId, "hi")], legacySent); Assert.Equal(0, chat.Count); // no local echo — the server's own // "" -sender resend is the only echo. } [Fact] public void AllegianceBroadcast_SkipsLocalOptimisticEcho() { // S1 (CH3 Opus review, 2026-08-09): flips this test's original // (wrong) premise. ACE's GameActionChatChannel handler iterates // player.Allegiance.Members — the sender IS a member, so their own // line comes back with their real name through the SAME broadcast // every other member gets. Retail agrees: // ClientCommunicationSystem::DoAllegianceBroadcast @0x005761F0 has // no AddTextToScroll of its own. Keeping a local echo double-prints. var chat = new ChatLog(); var router = NewRouter( chat: chat, sendChannel: (_, _) => { }); router.Activate(); router.Publish(new SendChatCmd( ChatChannelKind.AllegianceBroadcast, null, "hi")); Assert.Equal(0, chat.Count); } [Fact] public void ActivateAfterDispose_IsRejected() { var router = NewRouter(); router.Dispose(); Assert.Throws(router.Activate); } [Fact] public async Task ConcurrentDispose_WaitsForInFlightTransportThenMakesRouterInert() { using var sendEntered = new ManualResetEventSlim(); using var releaseSend = new ManualResetEventSlim(); var sent = new List(); var router = NewRouter(sendTalk: text => { sendEntered.Set(); releaseSend.Wait(); sent.Add(text); }); router.Activate(); Task publish = Task.Run(() => router.Publish(new SendServerCommandCmd("@active"))); Assert.True(sendEntered.Wait(TimeSpan.FromSeconds(5))); using var disposeStarted = new ManualResetEventSlim(); Task dispose = Task.Run(() => { disposeStarted.Set(); router.Dispose(); }); Assert.True(disposeStarted.Wait(TimeSpan.FromSeconds(5))); Task firstCompletion = await Task.WhenAny( dispose, Task.Delay(TimeSpan.FromMilliseconds(100))); Assert.NotSame(dispose, firstCompletion); releaseSend.Set(); await Task.WhenAll(publish, dispose); router.Publish(new SendServerCommandCmd("@after")); Assert.Equal(["@active"], sent); Assert.False(router.IsActive); } [Fact] public void ReentrantDisposeFromLog_PreventsLaterTransportAndEcho() { var turbine = new TurbineChatState(); turbine.OnChannelsReceived( 0u, 0x70000001u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u); var sent = new List(); var chat = new ChatLog(); LiveSessionCommandRouter? router = null; router = NewRouter( chat: chat, turbine: turbine, sendTurbine: (_, _, _, _, text, _) => sent.Add(text), log: _ => router!.Dispose()); router.Activate(); router.Publish(new SendChatCmd(ChatChannelKind.General, null, "world")); Assert.Empty(sent); Assert.Equal(0, chat.Count); Assert.False(router.IsActive); } [Fact] public void RetainedDisposedRouter_ReleasesCapturedTransportGraph() { (LiveSessionCommandRouter router, WeakReference transport) = CreateRouterWithCapturedTransport(); router.Dispose(); ForceFullCollection(); Assert.False(transport.TryGetTarget(out _)); GC.KeepAlive(router); } [Fact] public void DelayedConfirmationCallback_ReleasesTransportAndBecomesInert() { (LiveSessionCommandRouter router, WeakReference transport, Action callback) = CreateRouterWithDelayedConfirmation(); router.Dispose(); ForceFullCollection(); Assert.False(transport.TryGetTarget(out _)); callback(true); Assert.False(router.IsActive); GC.KeepAlive(callback); GC.KeepAlive(router); } [Fact] public void RuntimeStateCommandsUseActiveGenerationRouteAndBecomeInert() { var shortcuts = new List(); var options = new List<(uint OptionId, bool Value)>(); LiveSessionCommandRouter router = NewRouter( addShortcut: shortcuts.Add, sendSingleCharacterOption: (id, value) => options.Add((id, value))); router.Publish(new AddShortcutRuntimeCmd( new ShortcutEntry(1, 0x80000001u, 0u))); router.Activate(); router.Publish(new AddShortcutRuntimeCmd( new ShortcutEntry(2, 0x80000002u, 0u))); router.Publish(new SetSingleCharacterOptionRuntimeCmd(0x26u, true)); router.Dispose(); router.Publish(new AddShortcutRuntimeCmd( new ShortcutEntry(3, 0x80000003u, 0u))); Assert.Collection( shortcuts, entry => { Assert.Equal(2, entry.Index); Assert.Equal(0x80000002u, entry.ObjectId); }); Assert.Equal([(0x26u, true)], options); } // ── OP1 (Campaign OP, 2026-08-10): SaveCharacterOptionsRuntimeCmd ──── [Fact] public void SaveCharacterOptionsCommand_RoutesToBindingOnlyWhileActive() { int flushes = 0; LiveSessionCommandRouter router = NewRouter( saveCharacterOptions: () => flushes++); router.Publish(new SaveCharacterOptionsRuntimeCmd()); router.Activate(); router.Publish(new SaveCharacterOptionsRuntimeCmd()); router.Dispose(); router.Publish(new SaveCharacterOptionsRuntimeCmd()); Assert.Equal(1, flushes); } // ── CH4 re-review SHOULD-FIX 2 (2026-08-09) ───────────────────────── // The Settings Chat toggles reach this same SetSingleCharacterOptionRuntimeCmd // route (RuntimeSettingsController.PublishHearOptionChange -> // RuntimeSettingsTargets.SetSingleCharacterOption -> // SetSingleCharacterOptionRuntimeCmd -> here -> bindings.SendSingleCharacterOption) // as a second entrance alongside @join/@leave. LiveSessionRuntimeFactory now // wires SendSingleCharacterOption through the SAME local-write chokepoint as // the @join/@leave binding, so this mirrors // TurbineChatMembershipGateTests.JoinChannel_SetOptionBit_FlipsGateToAllowed_ // WithoutFreshPlayerDescription, but drives the write through the router's // Settings-route command instead of calling RuntimeCharacterOptionsState // directly. [Fact] public void SettingsRouteSetSingleCharacterOption_FlipsGateWithoutFreshPlayerDescription() { var characterState = new RuntimeCharacterState(); characterState.Options.Replace(characterState.Options.Options1, 0u); // every Options2 bit off — General starts refused var turbine = new TurbineChatState(); turbine.OnChannelsReceived( allegianceRoom: 0x10u, generalRoom: 0x11u, tradeRoom: 0x12u, lfgRoom: 0x13u, roleplayRoom: 0x14u, olthoiRoom: 0x15u, societyRoom: 0x16u, societyCelestialHandRoom: 0u, societyEldrytchWebRoom: 0u, societyRadiantBloodRoom: 0u); var sent = new List<(uint OptionId, bool Value)>(); // S1 (Campaign OP OP1 review fix, blast lens, 2026-08-11): drives the // REAL production binding — RuntimeCharacterOptionsState. // TrySetOption, the SAME shared local-write-then-send/dirty seam // LiveSessionRuntimeFactory.CreateCommandBindings' SendSingleCharacterOption // local function calls — instead of a hand-rolled substitute that had // silently drifted from it after OP1 (the previous shape called // SetOptionBit directly, which does not run TrySetOption's // unchanged-value early return or its MF-2 fellowship // mutual-exclusion side effect). LiveSessionCommandRouter router = NewRouter( characterState: characterState, sendSingleCharacterOption: (id, value) => characterState.Options.TrySetOption( id, value, sendAutoSave: (sentId, sentValue) => sent.Add((sentId, sentValue)))); router.Activate(); Assert.Equal( TurbineChatGateStatus.NotListening, TurbineChatMembershipGate.Evaluate( ChatChannelKindLite.General, turbine, characterState.Options, isOlthoiPlayer: false).Status); router.Publish(new SetSingleCharacterOptionRuntimeCmd( (uint)CharacterOptionId.ListenToGeneralChat, true)); Assert.Equal([((uint)CharacterOptionId.ListenToGeneralChat, true)], sent); Assert.Equal( TurbineChatGateStatus.Allowed, TurbineChatMembershipGate.Evaluate( ChatChannelKindLite.General, turbine, characterState.Options, isOlthoiPlayer: false).Status); } [Fact] public void ShowWeenieErrorFriendsFull_ResolvesThroughAddText_AndLandsInSpewBoxNotChat() { // CH2 re-review nit 7 (docs/plans/2026-08-09-chat-parity-campaign.md): // exercises the FULL retail path for id 0x0561 (the 50-friends-cap // refusal): ClientCommandController.AddFriend's cap guard calls // ShowWeenieError(0x0561u); the binding below mirrors // LiveSessionRuntimeFactory.CreateCommandBindings' ShowWeenieError // closure exactly (WeenieErrorMessages.Resolve then // RuntimeCommunicationState.AddText) — the same chokepoint that // fans a ClientLocal line out to SpewBox instead of chat // (RuntimeCommunicationStateTests pins that half in isolation // already). This is "the closest seam" per the review: every other // LiveSessionRuntimeFactory test in this tree is a source-text // conformance grep (ReadSource), not an instantiation — the real // factory needs a live WorldSession/domain graph no test builds. var communication = new RuntimeCommunicationState(); ClientCommandController.Bindings bindings = NewClientBindings() with { Friends = communication.Friends, ShowWeenieError = code => { (string? text, RetailLogTextType type) = WeenieErrorMessages.Resolve(code, null); if (text is not null) communication.AddText(text, type); }, }; LiveSessionCommandRouter router = NewRouter(clientBindings: bindings); router.Activate(); // Seed 50 friends so AddFriend's cap guard fires // (ClientCommandController.cs's AddFriend). var seeded = new List(); for (uint i = 0; i < 50; i++) seeded.Add(new FriendEntry(i + 1, $"Friend{i}", true, false, [], [])); communication.Friends.Apply(new FriendsUpdate(FriendsUpdateType.Full, seeded)); router.Publish(new ExecuteClientCommandCmd(ClientCommandId.FriendsAdd, "OneTooMany")); communication.SpewBox.Tick(0d); Assert.Equal(1, communication.SpewBox.Count); Assert.Equal( "You may only have a maximum of 50 friends at once. If you wish to add more friends, you must first remove some.", communication.SpewBox.Snapshot()[0].Text); Assert.Equal(0, communication.Chat.Count); Assert.Equal(50, communication.Friends.Count); } private static LiveSessionCommandRouter NewRouter( ChatLog? chat = null, TurbineChatState? turbine = null, Func? playerGuid = null, Action? sendTalk = null, Action? sendTell = null, Action? sendChannel = null, Action? sendTurbine = null, Action? addShortcut = null, Action? log = null, ClientCommandController.Bindings? clientBindings = null, RuntimeCommunicationState? communication = null, RuntimeCharacterState? characterState = null, Action? sendSingleCharacterOption = null, Action? saveCharacterOptions = null) => new( new LiveSessionCommandBindings( clientBindings ?? NewClientBindings(), chat ?? new ChatLog(), turbine ?? new TurbineChatState(), playerGuid ?? (() => 0u), sendTalk ?? (_ => { }), sendTell ?? ((_, _) => { }), sendChannel ?? ((_, _) => { }), sendTurbine ?? ((_, _, _, _, _, _) => { }), AddShortcut: addShortcut ?? (_ => { }), RemoveShortcut: _ => { }, AddFavorite: (_, _, _) => { }, RemoveFavorite: (_, _) => { }, SetSpellbookFilter: _ => { }, ForgetSpell: _ => { }, SetDesiredComponent: (_, _) => { }, ClearDesiredComponents: () => { }, RaiseAttribute: (_, _) => { }, RaiseVital: (_, _) => { }, RaiseSkill: (_, _) => { }, TrainSkill: (_, _) => { }, AddFriend: _ => { }, RemoveFriend: _ => { }, ClearFriends: () => { }, RequestLegacyFriends: () => { }, OpenTradeNegotiations: _ => { }, CloseTradeNegotiations: () => { }, AddToTrade: _ => { }, AcceptTrade: (_, _, _) => { }, DeclineTrade: () => { }, ResetTrade: () => { }, ModifyCharacterSquelch: (_, _, _, _) => { }, ModifyAccountSquelch: (_, _) => { }, ModifyGlobalSquelch: (_, _) => { }, Communication: communication ?? new RuntimeCommunicationState(), CharacterState: characterState ?? new RuntimeCharacterState(), SendSingleCharacterOption: sendSingleCharacterOption ?? ((_, _) => { }), SaveCharacterOptions: saveCharacterOptions ?? (() => { }), SendFellowshipCreate: (_, _) => { }, SendFellowshipRecruit: _ => { }, SendFellowshipDismiss: _ => { }, SendFellowshipQuit: _ => { }, SendFellowshipAssignNewLeader: _ => { }, SendFellowshipChangeOpenness: _ => { }, SendFellowshipUpdateRequest: _ => { }, SendAllegianceSwear: _ => { }, SendAllegianceBreak: _ => { }, SendAllegianceKick: _ => { }, SendAllegianceInfoRequest: _ => { }, SendAllegianceUpdateRequest: _ => { }, Log: log)); [MethodImpl(MethodImplOptions.NoInlining)] private static (LiveSessionCommandRouter, WeakReference) CreateRouterWithCapturedTransport() { object transport = new object(); var weak = new WeakReference(transport); var router = NewRouter(sendTalk: _ => GC.KeepAlive(transport)); return (router, weak); } [MethodImpl(MethodImplOptions.NoInlining)] private static (LiveSessionCommandRouter, WeakReference, Action) CreateRouterWithDelayedConfirmation() { object transport = new object(); var weak = new WeakReference(transport); Action? callback = null; ClientCommandController.Bindings bindings = NewClientBindings() with { ShowConfirmation = (_, completion) => callback = completion, Suicide = () => GC.KeepAlive(transport), }; var router = NewRouter(clientBindings: bindings); router.Activate(); router.Publish(new ExecuteClientCommandCmd(ClientCommandId.Die, string.Empty)); return (router, weak, Assert.IsType>(callback)); } [MethodImpl(MethodImplOptions.NoInlining)] private static void ForceFullCollection() { GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); } private static ClientCommandController.Bindings NewClientBindings() => new( TeleportToLifestone: () => { }, TeleportToMarketplace: () => { }, TeleportToPkArena: () => { }, TeleportToPkLiteArena: () => { }, TeleportToHouse: () => { }, TeleportToMansion: () => { }, QueryAge: () => { }, QueryBirth: () => { }, ToggleFrameRate: () => { }, ToggleUiLock: () => { }, ShowSystemMessage: _ => { }, ShowWeenieError: _ => { }, PlayerPublicWeenieBitfield: () => null, ClientVersion: () => "test", CurrentPosition: () => null, LastOutsideCorpsePosition: () => null, ShowConfirmation: (_, _) => { }, Suicide: () => { }, ClearChat: _ => { }, SaveUi: _ => { }, LoadUi: _ => { }, SaveAutoUi: () => { }, LoadAutoUi: () => { }, IsAway: () => false, SetAway: _ => { }, SetAwayMessage: _ => { }, AcceptLootPermits: () => false, SetAcceptLootPermits: _ => { }, DisplayConsent: () => { }, ClearConsent: () => { }, RemoveConsent: _ => { }, SendEmote: _ => { }, Friends: new FriendsState(), AddFriend: _ => { }, RemoveFriend: _ => { }, ClearFriends: () => { }, RequestLegacyFriends: () => { }, Squelch: new SquelchState(), ModifyCharacterSquelch: (_, _, _, _) => { }, ModifyAccountSquelch: (_, _) => { }, ModifyGlobalSquelch: (_, _) => { }, LastTeller: () => null, ClearDesiredComponents: () => { }, HasOpenVendor: () => false, FillComponentBuyList: (_, _) => { }, EnterPkLite: () => { }, IsUsingTurbineChat: () => false, SetChatTitle: _ => { }, SetSingleCharacterOption: (_, _) => { }, AddPlayerPermission: _ => { }, RemovePlayerPermission: _ => { }, RequestAvailableHouses: _ => { }, RequestChannelIndex: () => { }, RequestChannelList: _ => { }, JoinGmChannel: _ => { }, LeaveGmChannel: _ => { }, RecallAllegianceHometown: () => { }, RequestAllegianceInfo: _ => { }, AbandonHouse: () => { }); }