using AcDream.App.UI; using AcDream.Core.Chat; using AcDream.Core.Items; using AcDream.Runtime.Gameplay; using AcDream.Runtime.Session; namespace AcDream.App.Net; internal sealed record LiveSessionCommandBindings( ClientCommandController.Bindings ClientCommands, ChatLog Chat, TurbineChatState TurbineChat, Func PlayerGuid, Action SendTalk, Action SendTell, Action SendChannel, Action SendTurbineChat, Action AddShortcut, Action RemoveShortcut, Action AddFavorite, Action RemoveFavorite, Action SetSpellbookFilter, Action ForgetSpell, Action SetDesiredComponent, Action ClearDesiredComponents, Action RaiseAttribute, Action RaiseVital, Action RaiseSkill, Action TrainSkill, Action AddFriend, Action RemoveFriend, Action ClearFriends, Action RequestLegacyFriends, // Secure trade (2026-08-14) — the CM_Trade senders. Action OpenTradeNegotiations, Action CloseTradeNegotiations, Action AddToTrade, Action AcceptTrade, Action DeclineTrade, Action ResetTrade, Action ModifyCharacterSquelch, Action ModifyAccountSquelch, Action ModifyGlobalSquelch, // Campaign CH slice CH3 (2026-08-09): the RuntimeCommunicationState. // AddText chokepoint (local refusals — Turbine unavailable / not // listening) and the RuntimeCharacterState owner (Hear*Chat options + // IsOlthoiPlayer) the Turbine membership gate reads, plus the // SetSingleCharacterOption (0x0005) sender that replaced the malformed, // callerless full-blob SetCharacterOptions (0x01A1) path. RuntimeCommunicationState Communication, RuntimeCharacterState CharacterState, Action SendSingleCharacterOption, // Campaign OP slice OP1 (2026-08-10): the real SetCharacterOptions // (0x01A1) blob-flush verb — CH3's TODO, now resurrected per wire // research §2.3-§2.7. No-ops when the batched module is clean, matching // retail's CPlayerModule::SaveToServer(force: 0). Action SaveCharacterOptions, // Campaign FA slice FA2 (2026-08-12): fellowship + allegiance send // wrappers, the App-bus twin of DirectGameRuntimeCommandAdapter's // direct session.SendXxx calls. Action SendFellowshipCreate, Action SendFellowshipRecruit, Action SendFellowshipDismiss, Action SendFellowshipQuit, Action SendFellowshipAssignNewLeader, Action SendFellowshipChangeOpenness, Action SendFellowshipUpdateRequest, Action SendAllegianceSwear, Action SendAllegianceBreak, Action SendAllegianceKick, Action SendAllegianceInfoRequest, Action SendAllegianceUpdateRequest, Action? Log = null); internal readonly record struct AddShortcutRuntimeCmd(ShortcutEntry Entry); internal readonly record struct RemoveShortcutRuntimeCmd(uint Index); internal readonly record struct AddFavoriteRuntimeCmd( uint SpellId, int Position, int TabIndex); internal readonly record struct RemoveFavoriteRuntimeCmd( uint SpellId, int TabIndex); internal readonly record struct SetSpellbookFilterRuntimeCmd(uint Filters); internal readonly record struct ForgetSpellRuntimeCmd(uint SpellId); internal readonly record struct SetDesiredComponentRuntimeCmd( uint ComponentId, uint Amount); internal readonly record struct ClearDesiredComponentsRuntimeCmd; internal readonly record struct RaiseAttributeRuntimeCmd(uint StatId, ulong Cost); internal readonly record struct RaiseVitalRuntimeCmd(uint StatId, ulong Cost); internal readonly record struct RaiseSkillRuntimeCmd(uint StatId, ulong Cost); internal readonly record struct TrainSkillRuntimeCmd(uint StatId, uint Cost); internal readonly record struct SetSingleCharacterOptionRuntimeCmd( uint OptionId, bool Value); internal readonly record struct SaveCharacterOptionsRuntimeCmd; internal readonly record struct AddFriendRuntimeCmd(string Name); internal readonly record struct RemoveFriendRuntimeCmd(uint CharacterId); // ── Secure trade (2026-08-14) ────────────────────────────────────────────── internal readonly record struct OpenTradeNegotiationsRuntimeCmd(uint PartnerGuid); internal readonly record struct CloseTradeNegotiationsRuntimeCmd; internal readonly record struct AddToTradeRuntimeCmd(uint ItemGuid); internal readonly record struct AcceptTradeRuntimeCmd( uint PartnerGuid, bool SelfAccepted, bool PartnerAccepted); internal readonly record struct DeclineTradeRuntimeCmd; internal readonly record struct ResetTradeRuntimeCmd; internal readonly record struct ClearFriendsRuntimeCmd; internal readonly record struct RequestLegacyFriendsRuntimeCmd; internal readonly record struct ModifyCharacterSquelchRuntimeCmd( bool Add, uint CharacterId, string Name, uint MessageType); internal readonly record struct ModifyAccountSquelchRuntimeCmd( bool Add, string Name); internal readonly record struct ModifyGlobalSquelchRuntimeCmd( bool Add, uint MessageType); // ── Fellowship / Allegiance (Campaign FA slice FA2, 2026-08-12) ──────────── internal readonly record struct FellowshipCreateRuntimeCmd( string FellowshipName, bool ShareXp); internal readonly record struct FellowshipRecruitRuntimeCmd(uint TargetGuid); internal readonly record struct FellowshipDismissRuntimeCmd(uint TargetGuid); internal readonly record struct FellowshipQuitRuntimeCmd(bool Disband); internal readonly record struct FellowshipAssignNewLeaderRuntimeCmd(uint NewLeaderGuid); internal readonly record struct FellowshipChangeOpennessRuntimeCmd(bool IsOpen); internal readonly record struct FellowshipUpdateRequestRuntimeCmd(bool PanelOpen); internal readonly record struct AllegianceSwearRuntimeCmd(uint PatronGuid); internal readonly record struct AllegianceBreakRuntimeCmd(uint TargetGuid); internal readonly record struct AllegianceKickRuntimeCmd(uint VassalGuid); internal readonly record struct AllegianceInfoRequestRuntimeCmd(string PlayerName); internal readonly record struct AllegianceUpdateRequestRuntimeCmd(bool On); /// /// Owns the command surface for one exact live-session generation. The router /// itself is the published bus, so a retained reference becomes inert before /// the displaced transport is disposed. /// internal sealed class LiveSessionCommandRouter : ILiveSessionCommandRouting { private readonly object _gate = new(); private LiveCommandBus? _commands; private LiveChatCommandRoute? _chatCommands; private ClientCommandController.Bindings? _clientCommands; private int _state; // 0 = constructed, 1 = active, 2 = disposed public LiveSessionCommandRouter(LiveSessionCommandBindings bindings) { ArgumentNullException.ThrowIfNull(bindings); ArgumentNullException.ThrowIfNull(bindings.ClientCommands); ArgumentNullException.ThrowIfNull(bindings.Chat); ArgumentNullException.ThrowIfNull(bindings.TurbineChat); ArgumentNullException.ThrowIfNull(bindings.PlayerGuid); ArgumentNullException.ThrowIfNull(bindings.SendTalk); ArgumentNullException.ThrowIfNull(bindings.SendTell); ArgumentNullException.ThrowIfNull(bindings.SendChannel); ArgumentNullException.ThrowIfNull(bindings.SendTurbineChat); ArgumentNullException.ThrowIfNull(bindings.Communication); ArgumentNullException.ThrowIfNull(bindings.CharacterState); _clientCommands = bindings.ClientCommands; var commands = new LiveCommandBus(); var clientCommands = new ClientCommandController( BuildGuardedClientCommands(bindings.ClientCommands)); _chatCommands = new LiveChatCommandRoute(new LiveChatCommandBindings( clientCommands.Execute, bindings.Communication, bindings.Chat, bindings.TurbineChat, bindings.CharacterState, bindings.PlayerGuid, bindings.SendTalk, bindings.SendTell, bindings.SendChannel, bindings.SendTurbineChat, bindings.Log)); // Campaign CH slice CH4 (2026-08-09): the 22 unregistered // ChannelSystem::GetChannelID fallback tags — bypasses // ChatChannelKind/ChannelResolver entirely and sends the raw // legacy ChatChannel (0x0147) broadcast directly. commands.Register( command => SendIfActive(() => bindings.AddShortcut(command.Entry))); commands.Register( command => SendIfActive(() => bindings.RemoveShortcut(command.Index))); commands.Register( command => SendIfActive(() => bindings.AddFavorite( command.SpellId, command.Position, command.TabIndex))); commands.Register( command => SendIfActive(() => bindings.RemoveFavorite( command.SpellId, command.TabIndex))); commands.Register( command => SendIfActive(() => bindings.SetSpellbookFilter(command.Filters))); commands.Register( command => SendIfActive(() => bindings.ForgetSpell(command.SpellId))); commands.Register( command => SendIfActive(() => bindings.SetDesiredComponent( command.ComponentId, command.Amount))); commands.Register( _ => SendIfActive(bindings.ClearDesiredComponents)); commands.Register( command => SendIfActive(() => bindings.RaiseAttribute(command.StatId, command.Cost))); commands.Register( command => SendIfActive(() => bindings.RaiseVital(command.StatId, command.Cost))); commands.Register( command => SendIfActive(() => bindings.RaiseSkill(command.StatId, command.Cost))); commands.Register( command => SendIfActive(() => bindings.TrainSkill(command.StatId, command.Cost))); commands.Register( command => SendIfActive(() => bindings.SendSingleCharacterOption( command.OptionId, command.Value))); commands.Register( _ => SendIfActive(bindings.SaveCharacterOptions)); commands.Register( command => SendIfActive(() => bindings.AddFriend(command.Name))); commands.Register( command => SendIfActive(() => bindings.OpenTradeNegotiations(command.PartnerGuid))); commands.Register( _ => SendIfActive(bindings.CloseTradeNegotiations)); commands.Register( command => SendIfActive(() => bindings.AddToTrade(command.ItemGuid))); commands.Register( command => SendIfActive(() => bindings.AcceptTrade( command.PartnerGuid, command.SelfAccepted, command.PartnerAccepted))); commands.Register( _ => SendIfActive(bindings.DeclineTrade)); commands.Register( _ => SendIfActive(bindings.ResetTrade)); commands.Register( command => SendIfActive(() => bindings.RemoveFriend(command.CharacterId))); commands.Register( _ => SendIfActive(bindings.ClearFriends)); commands.Register( _ => SendIfActive(bindings.RequestLegacyFriends)); commands.Register( command => SendIfActive(() => bindings.ModifyCharacterSquelch( command.Add, command.CharacterId, command.Name, command.MessageType))); commands.Register( command => SendIfActive(() => bindings.ModifyAccountSquelch( command.Add, command.Name))); commands.Register( command => SendIfActive(() => bindings.ModifyGlobalSquelch( command.Add, command.MessageType))); commands.Register( command => SendIfActive(() => bindings.SendFellowshipCreate( command.FellowshipName, command.ShareXp))); commands.Register( command => SendIfActive(() => bindings.SendFellowshipRecruit(command.TargetGuid))); commands.Register( command => SendIfActive(() => bindings.SendFellowshipDismiss(command.TargetGuid))); commands.Register( command => SendIfActive(() => bindings.SendFellowshipQuit(command.Disband))); commands.Register( command => SendIfActive(() => bindings.SendFellowshipAssignNewLeader(command.NewLeaderGuid))); commands.Register( command => SendIfActive(() => bindings.SendFellowshipChangeOpenness(command.IsOpen))); commands.Register( command => SendIfActive(() => bindings.SendFellowshipUpdateRequest(command.PanelOpen))); commands.Register( command => SendIfActive(() => bindings.SendAllegianceSwear(command.PatronGuid))); commands.Register( command => SendIfActive(() => bindings.SendAllegianceBreak(command.TargetGuid))); commands.Register( command => SendIfActive(() => bindings.SendAllegianceKick(command.VassalGuid))); commands.Register( command => SendIfActive(() => bindings.SendAllegianceInfoRequest(command.PlayerName))); commands.Register( command => SendIfActive(() => bindings.SendAllegianceUpdateRequest(command.On))); _commands = commands; } public bool IsActive { get { lock (_gate) return _state == 1; } } public void Activate() { lock (_gate) { if (_state == 2) throw new ObjectDisposedException(nameof(LiveSessionCommandRouter)); _chatCommands?.Activate(); _state = 1; } } public void Publish(T command) where T : notnull { lock (_gate) { if (_state == 1) { if (_chatCommands?.TryPublish(command) != true) _commands?.Publish(command); } } } public void Dispose() { LiveCommandBus? commands; LiveChatCommandRoute? chatCommands; lock (_gate) { _state = 2; commands = _commands; _commands = null; chatCommands = _chatCommands; _chatCommands = null; _clientCommands = null; } chatCommands?.Dispose(); commands?.Clear(); } private ClientCommandController.Bindings BuildGuardedClientCommands( ClientCommandController.Bindings source) => new( TeleportToLifestone: () => InvokeClient(static b => b.TeleportToLifestone()), TeleportToMarketplace: () => InvokeClient(static b => b.TeleportToMarketplace()), TeleportToPkArena: () => InvokeClient(static b => b.TeleportToPkArena()), TeleportToPkLiteArena: () => InvokeClient(static b => b.TeleportToPkLiteArena()), TeleportToHouse: () => InvokeClient(static b => b.TeleportToHouse()), TeleportToMansion: () => InvokeClient(static b => b.TeleportToMansion()), QueryAge: () => InvokeClient(static b => b.QueryAge()), QueryBirth: () => InvokeClient(static b => b.QueryBirth()), ToggleFrameRate: () => InvokeClient(static b => b.ToggleFrameRate()), ToggleUiLock: () => InvokeClient(static b => b.ToggleUiLock()), ShowSystemMessage: text => InvokeClient(b => b.ShowSystemMessage(text)), ShowWeenieError: error => InvokeClient(b => b.ShowWeenieError(error)), PlayerPublicWeenieBitfield: () => ReadClient(static b => b.PlayerPublicWeenieBitfield(), default(uint?)), ClientVersion: () => ReadClient(static b => b.ClientVersion(), string.Empty), CurrentPosition: () => ReadClient(static b => b.CurrentPosition(), default(AcDream.Core.Physics.Position?)), LastOutsideCorpsePosition: () => ReadClient(static b => b.LastOutsideCorpsePosition(), default(AcDream.Core.Physics.Position?)), ShowConfirmation: (text, callback) => InvokeClient(b => b.ShowConfirmation(text, callback)), Suicide: () => InvokeClient(static b => b.Suicide()), ClearChat: all => InvokeClient(b => b.ClearChat(all)), SaveUi: name => InvokeClient(b => b.SaveUi(name)), LoadUi: name => InvokeClient(b => b.LoadUi(name)), SaveAutoUi: () => InvokeClient(static b => b.SaveAutoUi()), LoadAutoUi: () => InvokeClient(static b => b.LoadAutoUi()), IsAway: () => ReadClient(static b => b.IsAway(), false), SetAway: away => InvokeClient(b => b.SetAway(away)), SetAwayMessage: message => InvokeClient(b => b.SetAwayMessage(message)), AcceptLootPermits: () => ReadClient(static b => b.AcceptLootPermits(), false), SetAcceptLootPermits: accept => InvokeClient(b => b.SetAcceptLootPermits(accept)), DisplayConsent: () => InvokeClient(static b => b.DisplayConsent()), ClearConsent: () => InvokeClient(static b => b.ClearConsent()), RemoveConsent: name => InvokeClient(b => b.RemoveConsent(name)), SendEmote: text => InvokeClient(b => b.SendEmote(text)), Friends: source.Friends, AddFriend: name => InvokeClient(b => b.AddFriend(name)), RemoveFriend: id => InvokeClient(b => b.RemoveFriend(id)), ClearFriends: () => InvokeClient(static b => b.ClearFriends()), RequestLegacyFriends: () => InvokeClient(static b => b.RequestLegacyFriends()), Squelch: source.Squelch, ModifyCharacterSquelch: (add, id, name, chatType) => InvokeClient(b => b.ModifyCharacterSquelch(add, id, name, chatType)), ModifyAccountSquelch: (add, name) => InvokeClient(b => b.ModifyAccountSquelch(add, name)), ModifyGlobalSquelch: (add, chatType) => InvokeClient(b => b.ModifyGlobalSquelch(add, chatType)), LastTeller: () => ReadClient(static b => b.LastTeller(), default(string?)), ClearDesiredComponents: () => InvokeClient(static b => b.ClearDesiredComponents()), HasOpenVendor: () => ReadClient(static b => b.HasOpenVendor(), false), FillComponentBuyList: (componentId, targetCount) => InvokeClient(b => b.FillComponentBuyList(componentId, targetCount)), EnterPkLite: () => InvokeClient(static b => b.EnterPkLite()), // Campaign CH slice CH4 (2026-08-09): command-registry completion. IsUsingTurbineChat: () => ReadClient(static b => b.IsUsingTurbineChat(), false), SetChatTitle: title => InvokeClient(b => b.SetChatTitle(title)), SetSingleCharacterOption: (optionId, value) => InvokeClient(b => b.SetSingleCharacterOption(optionId, value)), AddPlayerPermission: name => InvokeClient(b => b.AddPlayerPermission(name)), RemovePlayerPermission: name => InvokeClient(b => b.RemovePlayerPermission(name)), RequestAvailableHouses: houseType => InvokeClient(b => b.RequestAvailableHouses(houseType)), RequestChannelIndex: () => InvokeClient(static b => b.RequestChannelIndex()), RequestChannelList: channelId => InvokeClient(b => b.RequestChannelList(channelId)), JoinGmChannel: channelId => InvokeClient(b => b.JoinGmChannel(channelId)), LeaveGmChannel: channelId => InvokeClient(b => b.LeaveGmChannel(channelId)), RecallAllegianceHometown: () => InvokeClient(static b => b.RecallAllegianceHometown()), RequestAllegianceInfo: name => InvokeClient(b => b.RequestAllegianceInfo(name)), AbandonHouse: () => InvokeClient(static b => b.AbandonHouse())); private bool InvokeClient(Action invoke) { lock (_gate) { ClientCommandController.Bindings? bindings = _clientCommands; if (_state != 1 || bindings is null) return false; invoke(bindings); return true; } } private TResult ReadClient( Func read, TResult fallback) { lock (_gate) { ClientCommandController.Bindings? bindings = _clientCommands; return _state == 1 && bindings is not null ? read(bindings) : fallback; } } private bool SendIfActive(Action send) { lock (_gate) { if (_state != 1) return false; send(); return true; } } }