using AcDream.Core.Chat; using AcDream.Core.Net.Messages; using AcDream.Runtime.Session; namespace AcDream.Runtime.Gameplay; /// /// Outcome of . /// public enum TurbineChatGateStatus { /// The send may proceed to the wire. Allowed, /// /// Turbine chat is disabled, or this room's id is still 0 (not /// populated by SetTurbineChatChannels — e.g. no allegiance, no /// society). Retail: "Turbine chat is not available." /// (), raised /// locally with NOTHING sent. /// Unavailable, /// /// The room exists and Turbine chat is enabled, but the player's own /// Hear*Chat option (or, for Olthoi, heritage) is off. Retail: /// WeenieErrorWithString 0x0551 YouAreNotListeningTo_Channel, /// raised locally with NOTHING sent. /// NotListening, } /// One evaluated gate decision, carrying the room/chatType/display /// name a caller needs whether the send proceeds or gets refused. public readonly record struct TurbineChatGateResult( TurbineChatGateStatus Status, uint RoomId, uint ChatType, string DisplayName); /// /// Retail's local pre-send membership gate for a Turbine-room chat channel, /// ported from ClientCommunicationSystem::SendTurbineChat @0x0057db10 /// (Sept 2013 EoR build; research doc /// docs/research/2026-08-09-chat-side-channels-vs-ace.md §4.2/§6.2). /// /// /// Both the graphical (LiveSessionCommandRouter) and headless /// (DirectGameRuntimeCommandAdapter) outbound chat paths call this /// SAME chokepoint so the two hosts can never diverge on which channels are /// joined and which local refusal a blocked send produces. Retail refuses /// LOCALLY (nothing reaches the wire) in exactly two cases — Turbine chat /// off / room id 0, or the player's own Hear*Chat option off — before /// ever building a TurbineChatBlob. /// /// public static class TurbineChatMembershipGate { public static TurbineChatGateResult Evaluate( ChatChannelKindLite kind, TurbineChatState turbineChat, RuntimeCharacterOptionsState options, bool isOlthoiPlayer) { ArgumentNullException.ThrowIfNull(turbineChat); ArgumentNullException.ThrowIfNull(options); // N2 (CH3 Opus review): reuse TurbineChatDisplayNames.Resolve — the // SAME assembly already has this room/chatType-to-display-name // table (LiveSessionEventRouter's inbound path uses it) rather than // maintaining a second copy of the same seven strings here. (uint room, uint chatType) = kind switch { ChatChannelKindLite.Allegiance => ( turbineChat.AllegianceRoom, (uint)TurbineChat.ChatType.Allegiance), ChatChannelKindLite.General => ( turbineChat.GeneralRoom, (uint)TurbineChat.ChatType.General), ChatChannelKindLite.Trade => ( turbineChat.TradeRoom, (uint)TurbineChat.ChatType.Trade), ChatChannelKindLite.Lfg => ( turbineChat.LfgRoom, (uint)TurbineChat.ChatType.Lfg), ChatChannelKindLite.Roleplay => ( turbineChat.RoleplayRoom, (uint)TurbineChat.ChatType.Roleplay), ChatChannelKindLite.Society => ( turbineChat.SocietyRoom, (uint)TurbineChat.ChatType.Society), ChatChannelKindLite.Olthoi => ( turbineChat.OlthoiRoom, (uint)TurbineChat.ChatType.Olthoi), _ => (0u, 0u), }; string name = TurbineChatDisplayNames.Resolve(room, chatType); if (!turbineChat.Enabled || room == 0u) { return new TurbineChatGateResult( TurbineChatGateStatus.Unavailable, room, chatType, name); } bool hearOption = kind switch { ChatChannelKindLite.Allegiance => (options.Options1 & (uint)PlayerDescriptionParser.CharacterOptions1.HearAllegianceChat) != 0u, ChatChannelKindLite.General => (options.Options2 & (uint)PlayerDescriptionParser.CharacterOptions2.HearGeneralChat) != 0u, ChatChannelKindLite.Trade => (options.Options2 & (uint)PlayerDescriptionParser.CharacterOptions2.HearTradeChat) != 0u, ChatChannelKindLite.Lfg => (options.Options2 & (uint)PlayerDescriptionParser.CharacterOptions2.HearLFGChat) != 0u, ChatChannelKindLite.Roleplay => (options.Options2 & (uint)PlayerDescriptionParser.CharacterOptions2.HearRoleplayChat) != 0u, ChatChannelKindLite.Society => (options.Options2 & (uint)PlayerDescriptionParser.CharacterOptions2.HearSocietyChat) != 0u, // Retail's Olthoi entry point passes CPlayerSystem::IsOlthoi() // as its hearOption argument instead of a PlayerModule flag — // there is no "Hear Olthoi chat" toggle, only heritage. ChatChannelKindLite.Olthoi => isOlthoiPlayer, _ => true, }; return hearOption ? new TurbineChatGateResult(TurbineChatGateStatus.Allowed, room, chatType, name) : new TurbineChatGateResult(TurbineChatGateStatus.NotListening, room, chatType, name); } /// /// N3 (CH3 Opus review): the single shared mapping from an evaluated /// to the retail-exact local /// refusal text/type an AddText caller should raise instead of /// sending — collapses the identical /// switch (gate.Status) { case Unavailable: ...; case NotListening: /// ...; } block that used to live independently in both /// LiveSessionCommandRouter.RouteTurbineChat and /// DirectGameRuntimeCommandAdapter.TrySendChannel. Returns /// null when the gate allows the send to proceed to the wire. /// public static (string Text, RetailLogTextType Type)? ResolveRefusalText( TurbineChatGateResult gate) { switch (gate.Status) { case TurbineChatGateStatus.Unavailable: return (ClientTextRefusals.TurbineChatUnavailable, RetailLogTextType.Default); case TurbineChatGateStatus.NotListening: (string? text, RetailLogTextType type) = WeenieErrorMessages.Resolve(0x0551u, gate.DisplayName); return text is not null ? (text, type) : null; default: return null; } } }