feat(net+chat): #19 TurbineChat (0xF7DE) codec + ChatChannelInfo + SetTurbineChatChannels parser
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>
This commit is contained in:
parent
f14296c75f
commit
ca968fc766
11 changed files with 1604 additions and 8 deletions
61
tests/AcDream.Core.Tests/Chat/ChatChannelInfoTests.cs
Normal file
61
tests/AcDream.Core.Tests/Chat/ChatChannelInfoTests.cs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
using AcDream.Core.Chat;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Phase I.6: <see cref="ChatChannelInfo"/> + <see cref="ChatChannelSource"/>
|
||||
/// behaviour. Mirrors holtburger's <c>is_self_echo_channel</c> predicate
|
||||
/// (chat.rs:492-507) — only the legacy fellowship + allegiance-tree
|
||||
/// channels echo the sender's own messages back with empty sender.
|
||||
/// </summary>
|
||||
public sealed class ChatChannelInfoTests
|
||||
{
|
||||
[Fact]
|
||||
public void Legacy_FellowshipBitflag_IsSelfEcho()
|
||||
{
|
||||
var c = new ChatChannelInfo.Legacy(0x00000800u, "Fellowship");
|
||||
Assert.Equal(ChatChannelSource.Legacy, c.Source);
|
||||
Assert.Equal("Fellowship", c.DisplayName);
|
||||
Assert.True(c.IsSelfEchoChannel());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x00001000u)] // Vassals
|
||||
[InlineData(0x00002000u)] // Patron
|
||||
[InlineData(0x00004000u)] // Monarch
|
||||
[InlineData(0x01000000u)] // CoVassals
|
||||
public void Legacy_AllegianceTreeBitflags_AreSelfEcho(uint id)
|
||||
{
|
||||
var c = new ChatChannelInfo.Legacy(id, "Allegiance");
|
||||
Assert.True(c.IsSelfEchoChannel(), $"channel 0x{id:X8} should self-echo");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Legacy_AllegianceBroadcastBitflag_IsNOTSelfEcho()
|
||||
{
|
||||
// 0x02000000 AllegianceBroadcast is the read-only motd channel —
|
||||
// server does not echo client messages back on it (per holtburger
|
||||
// chat.rs:492-507 predicate, only Fellow+Vassals+Patron+Monarch+CoVassals).
|
||||
var c = new ChatChannelInfo.Legacy(0x02000000u, "AllegianceBroadcast");
|
||||
Assert.False(c.IsSelfEchoChannel());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Turbine_IsNeverSelfEcho()
|
||||
{
|
||||
// Turbine rooms don't echo — caller must emit local optimistic echo.
|
||||
var t = new ChatChannelInfo.Turbine(
|
||||
RoomId: 0x12345678u, ChatType: 2u, DispatchType: 2u, DisplayName: "General");
|
||||
Assert.Equal(ChatChannelSource.Turbine, t.Source);
|
||||
Assert.Equal("General", t.DisplayName);
|
||||
Assert.False(t.IsSelfEchoChannel());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Legacy_UnknownBitflag_IsNotSelfEcho()
|
||||
{
|
||||
var c = new ChatChannelInfo.Legacy(0xCAFEF00Du, "WatCha");
|
||||
Assert.False(c.IsSelfEchoChannel());
|
||||
}
|
||||
}
|
||||
84
tests/AcDream.Core.Tests/Chat/TurbineChatStateTests.cs
Normal file
84
tests/AcDream.Core.Tests/Chat/TurbineChatStateTests.cs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
using AcDream.Core.Chat;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Phase I.6: <see cref="TurbineChatState"/> behaviour.
|
||||
/// </summary>
|
||||
public sealed class TurbineChatStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void InitialState_DisabledAndZeroRooms_NextContextIdStartsAt1()
|
||||
{
|
||||
var s = new TurbineChatState();
|
||||
Assert.False(s.Enabled);
|
||||
Assert.Equal(0u, s.GeneralRoom);
|
||||
Assert.Equal(0u, s.TradeRoom);
|
||||
Assert.Equal(0u, s.AllegianceRoom);
|
||||
Assert.Equal(1u, s.NextContextId());
|
||||
// After consuming one cookie, the next is 2
|
||||
Assert.Equal(2u, s.NextContextId());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnChannelsReceived_PopulatesAllFieldsAndEnables()
|
||||
{
|
||||
var s = new TurbineChatState();
|
||||
s.OnChannelsReceived(
|
||||
allegianceRoom: 0xA0,
|
||||
generalRoom: 0xA1,
|
||||
tradeRoom: 0xA2,
|
||||
lfgRoom: 0xA3,
|
||||
roleplayRoom: 0xA4,
|
||||
olthoiRoom: 0xA5,
|
||||
societyRoom: 0xA6,
|
||||
societyCelestialHandRoom: 0xA7,
|
||||
societyEldrytchWebRoom: 0xA8,
|
||||
societyRadiantBloodRoom: 0xA9);
|
||||
|
||||
Assert.True(s.Enabled);
|
||||
Assert.Equal(0xA0u, s.AllegianceRoom);
|
||||
Assert.Equal(0xA1u, s.GeneralRoom);
|
||||
Assert.Equal(0xA2u, s.TradeRoom);
|
||||
Assert.Equal(0xA3u, s.LfgRoom);
|
||||
Assert.Equal(0xA4u, s.RoleplayRoom);
|
||||
Assert.Equal(0xA5u, s.OlthoiRoom);
|
||||
Assert.Equal(0xA6u, s.SocietyRoom);
|
||||
Assert.Equal(0xA7u, s.SocietyCelestialHandRoom);
|
||||
Assert.Equal(0xA8u, s.SocietyEldrytchWebRoom);
|
||||
Assert.Equal(0xA9u, s.SocietyRadiantBloodRoom);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NextContextId_IsMonotonicAndStartsAt1()
|
||||
{
|
||||
var s = new TurbineChatState();
|
||||
Assert.Equal(1u, s.NextContextId());
|
||||
Assert.Equal(2u, s.NextContextId());
|
||||
Assert.Equal(3u, s.NextContextId());
|
||||
Assert.Equal(4u, s.NextContextId());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoomFor_ReturnsConfiguredRoom()
|
||||
{
|
||||
var s = new TurbineChatState();
|
||||
s.OnChannelsReceived(
|
||||
allegianceRoom: 1, generalRoom: 2, tradeRoom: 3,
|
||||
lfgRoom: 4, roleplayRoom: 5, olthoiRoom: 6,
|
||||
societyRoom: 7, societyCelestialHandRoom: 8,
|
||||
societyEldrytchWebRoom: 9, societyRadiantBloodRoom: 10);
|
||||
|
||||
Assert.Equal(1u, s.RoomFor(ChatChannelKindLite.Allegiance));
|
||||
Assert.Equal(2u, s.RoomFor(ChatChannelKindLite.General));
|
||||
Assert.Equal(3u, s.RoomFor(ChatChannelKindLite.Trade));
|
||||
Assert.Equal(4u, s.RoomFor(ChatChannelKindLite.Lfg));
|
||||
Assert.Equal(5u, s.RoomFor(ChatChannelKindLite.Roleplay));
|
||||
Assert.Equal(6u, s.RoomFor(ChatChannelKindLite.Olthoi));
|
||||
Assert.Equal(7u, s.RoomFor(ChatChannelKindLite.Society));
|
||||
Assert.Equal(8u, s.RoomFor(ChatChannelKindLite.SocietyCelestialHand));
|
||||
Assert.Equal(9u, s.RoomFor(ChatChannelKindLite.SocietyEldrytchWeb));
|
||||
Assert.Equal(10u, s.RoomFor(ChatChannelKindLite.SocietyRadiantBlood));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue