namespace AcDream.Core.Chat; /// /// Source/transport classification for a chat channel — distinguishes /// retail's two parallel chat channel pipelines. /// public enum ChatChannelSource { /// Legacy ChatChannel bitflag id rides 0x0147 ChatChannel. Legacy, /// Turbine room id rides 0xF7DE TurbineChat. Turbine, } /// /// Unified info about a chat channel — either a legacy ChatChannel id /// (Fellowship, Allegiance, Vassals, Patron, Monarch, CoVassals) or a /// Turbine room id (General, Trade, LFG, Roleplay, Society*, Olthoi). /// /// /// Mirrors holtburger's ChatChannelInfo /// (references/holtburger/crates/holtburger-core/src/client/types.rs /// lines 63-102). The two retail channel pipelines run side by side — /// legacy ChatChannel for the player-organisation channels and /// TurbineChat for the global community rooms — and a single /// abstraction over both keeps the chat panel and command bus from /// having to special-case the transport at every call site. /// /// /// /// tells callers whether the server /// echoes the client's own outgoing messages back on this channel /// (so the client should suppress its optimistic local echo). Per /// holtburger's predicate at chat.rs::is_self_echo_channel /// (lines 492-507) this is true for the legacy fellowship/vassals/ /// patron/monarch/co-vassals channels — server resends those with an /// empty sender. S1 (CH3 Opus review, 2026-08-09) added /// AllegianceBroadcast to this same group: ACE's GameActionChatChannel /// handler includes the sender as an ordinary member of its real-name /// broadcast — a different mechanism from the other five's empty-sender /// resend, but the same consequence for the client (suppress the local /// echo). Turbine and tells do not echo. /// /// public abstract record ChatChannelInfo(string DisplayName, ChatChannelSource Source) { /// Legacy ChatChannel bitflag id (0x00000800 etc.). public sealed record Legacy(uint ChannelId, string DisplayName) : ChatChannelInfo(DisplayName, ChatChannelSource.Legacy) { public override bool IsSelfEchoChannel() { // Per holtburger: the legacy fellowship + allegiance-tree // channels are the ones the server echoes back to the sender. // Bitflag values from // references/holtburger/.../messages/chat/types.rs::ChatChannel. // // S1 (CH3 Opus review, 2026-08-09) — corrects the original CH3 // filing at research doc §3.7/§5.4: AllegianceBroadcast // (0x02000000) belongs in the `true` group below, NOT the // `false` default. ACE's GameActionChatChannel handler iterates // player.Allegiance.Members, and the sender IS a member, so // they receive their own line back with their REAL name — a // different mechanism from Fellow/Vassals/Patron/Monarch/ // CoVassals' separate ""-sender resend, but the same // consequence: keeping a local optimistic echo double-prints. // Retail agrees: ClientCommunicationSystem::DoAllegianceBroadcast // @0x005761F0 calls Event_ChannelBroadcast(0x2000000, &text) // with no AddTextToScroll of its own. return ChannelId switch { 0x00000800u => true, // Fellow 0x00001000u => true, // Vassals 0x00002000u => true, // Patron 0x00004000u => true, // Monarch 0x01000000u => true, // CoVassals 0x02000000u => true, // AllegianceBroadcast _ => false, }; } } /// /// TurbineChat room. is the runtime channel id /// the server hands out via SetTurbineChatChannels (0x0295). /// classifies the room semantically (General, /// Trade, etc.); chooses the wire dispatch /// (SendToRoomById for outbound, SendToRoomByName for inbound events). /// public sealed record Turbine( uint RoomId, uint ChatType, uint DispatchType, string DisplayName) : ChatChannelInfo(DisplayName, ChatChannelSource.Turbine) { public override bool IsSelfEchoChannel() { // S4 (CH3 Opus review, 2026-08-09): the comment this replaced // was wrong in both directions. ACE's TurbineChatHandler // resends via GetAllOnline() WITH the sender included — there // is no sender exclusion, so the sender's own outgoing line // comes back through the SAME broadcast every other member // gets. Retail's SendTurbineChat @0x0057db10 emits no local // AddTextToScroll on success either way. Production correctly // shows no local optimistic echo for Turbine channels, but NOT // because of this return value — RouteTurbineChat // (LiveSessionCommandRouter / DirectGameRuntimeCommandAdapter) // never calls OnSelfSent for a Turbine send at all, so this // method is currently unread for the Turbine variant (only // Legacy.IsSelfEchoChannel() has a caller). Kept `false` here // since no caller depends on the value either way. return false; } } /// /// True iff the server echoes our own outgoing messages on this /// channel — caller should suppress optimistic local echo to avoid /// double-printing. /// public abstract bool IsSelfEchoChannel(); }