Full port of holtburger's TurbineChat sidecar wire path: - TurbineChat.cs: 0xF7DE codec with three payload variants (EventSendToRoom S->C, RequestSendToRoomById C->S, Response). 10-field outer header (size_first/blob_type/dispatch_type/ target_type/target_id/transport_type/transport_id/cookie/ size_second + payload). - UTF-16LE turbine string codec with 1-or-2 byte variable-length prefix (high bit on first byte signals 2-byte form). Mirrors holtburger's read_turbine_string / write_turbine_string at references/holtburger/.../messages/chat/turbine.rs:502-544. - SetTurbineChatChannels.cs: 0x0295 GameEvent sub-opcode parser (10 x u32 channel ids). Wired through GameEventDispatcher in WorldSession ctor; routes to GameEventWiring + TurbineChatState. - ChatChannelInfo.cs (Core): unified record union with Legacy (channel id + name) and Turbine (room id + chat type + dispatch type + name) variants, plus IsSelfEchoChannel predicate (Tells = false, channels = true so optimistic echo is suppressed where the server will echo). - TurbineChatState.cs (Core): Enabled flag + 10 cached room ids + NextContextId() cookie counter starting at 1. - WorldSession adds TurbineChatReceived + TurbineChannelsReceived events; SendTurbineChatTo outbound builds RequestSendToRoomById + sends through SendGameAction. ProcessDatagram dispatches 0xF7DE at the top level. - GameWindow constructs TurbineChatState, subscribes inbound EventSendToRoom -> ChatLog.OnChannelBroadcast; extends I.3's SendChatCmd handler to route Turbine kinds (General/Trade/Lfg/ Roleplay/Society/Olthoi) through TurbineChat first, fall back to legacy ChatChannel send when state.Enabled == false. Round-trip golden fixtures from holtburger source verified for all three payload variants + UTF-16LE strings (short + long prefix + non-ASCII Cafe + empty) + SetTurbineChatChannels. 26 new tests: - TurbineChatTests, SetTurbineChatChannelsTests in Core.Net.Tests - ChatChannelInfoTests, TurbineChatStateTests in Core.Tests Solution total: 960 green (243 Core.Net + 625 Core + 92 UI). ACE doesn't run a TurbineChat server, so codec is "ready when needed" for retail-server-emulating setups. Legacy ChatChannel fallback continues to work for current ACE-against-acdream play. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
94 lines
3.7 KiB
C#
94 lines
3.7 KiB
C#
namespace AcDream.Core.Chat;
|
|
|
|
/// <summary>
|
|
/// Source/transport classification for a chat channel — distinguishes
|
|
/// retail's two parallel chat channel pipelines.
|
|
/// </summary>
|
|
public enum ChatChannelSource
|
|
{
|
|
/// <summary>Legacy <c>ChatChannel</c> bitflag id rides 0x0147 ChatChannel.</summary>
|
|
Legacy,
|
|
|
|
/// <summary>Turbine room id rides 0xF7DE TurbineChat.</summary>
|
|
Turbine,
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
///
|
|
/// <para>
|
|
/// Mirrors holtburger's <c>ChatChannelInfo</c>
|
|
/// (<c>references/holtburger/crates/holtburger-core/src/client/types.rs</c>
|
|
/// lines 63-102). The two retail channel pipelines run side by side —
|
|
/// legacy <c>ChatChannel</c> for the player-organisation channels and
|
|
/// <c>TurbineChat</c> 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.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// <see cref="IsSelfEchoChannel"/> 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 <c>chat.rs::is_self_echo_channel</c>
|
|
/// (lines 492-507) this is true ONLY for the legacy fellowship/vassals/
|
|
/// patron/monarch/co-vassals channels — server resends those with
|
|
/// empty sender. Turbine and tells do not echo.
|
|
/// </para>
|
|
/// </summary>
|
|
public abstract record ChatChannelInfo(string DisplayName, ChatChannelSource Source)
|
|
{
|
|
/// <summary>Legacy <c>ChatChannel</c> bitflag id (0x00000800 etc.).</summary>
|
|
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
|
|
// with an empty sender field. Bitflag values from
|
|
// references/holtburger/.../messages/chat/types.rs::ChatChannel.
|
|
return ChannelId switch
|
|
{
|
|
0x00000800u => true, // Fellow
|
|
0x00001000u => true, // Vassals
|
|
0x00002000u => true, // Patron
|
|
0x00004000u => true, // Monarch
|
|
0x01000000u => true, // CoVassals
|
|
_ => false,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// TurbineChat room. <see cref="RoomId"/> is the runtime channel id
|
|
/// the server hands out via <c>SetTurbineChatChannels</c> (0x0295).
|
|
/// <see cref="ChatType"/> classifies the room semantically (General,
|
|
/// Trade, etc.); <see cref="DispatchType"/> chooses the wire dispatch
|
|
/// (SendToRoomById for outbound, SendToRoomByName for inbound events).
|
|
/// </summary>
|
|
public sealed record Turbine(
|
|
uint RoomId,
|
|
uint ChatType,
|
|
uint DispatchType,
|
|
string DisplayName)
|
|
: ChatChannelInfo(DisplayName, ChatChannelSource.Turbine)
|
|
{
|
|
public override bool IsSelfEchoChannel()
|
|
{
|
|
// Turbine rooms do NOT echo the sender's own messages back.
|
|
// The client must emit its own optimistic local echo to give
|
|
// the player feedback that the message was sent.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// True iff the server echoes our own outgoing messages on this
|
|
/// channel — caller should suppress optimistic local echo to avoid
|
|
/// double-printing.
|
|
/// </summary>
|
|
public abstract bool IsSelfEchoChannel();
|
|
}
|