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();
}