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
|
|
@ -0,0 +1,96 @@
|
|||
using System;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Net.Tests.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Phase I.6: <see cref="SetTurbineChatChannels"/> 10-u32 GameEvent
|
||||
/// payload parser.
|
||||
///
|
||||
/// <para>
|
||||
/// Golden fixture taken from holtburger
|
||||
/// <c>references/holtburger/crates/holtburger-protocol/src/messages/chat/turbine.rs</c>
|
||||
/// lines 654-679 (test <c>set_turbine_chat_channels_fixture</c>) — itself
|
||||
/// generated by ACE's <c>SyntheticProtocolTests.GenerateTurbineChatFixtures</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class SetTurbineChatChannelsTests
|
||||
{
|
||||
[Fact]
|
||||
public void TryParse_HoltburgerGoldenFixture_RoundTrips()
|
||||
{
|
||||
// Full game-message body from turbine.rs:655-657:
|
||||
// B0F70000 – outer 0xF7B0 GameEvent opcode
|
||||
// 00000000 – guid 0
|
||||
// 21000000 – sequence 0x21
|
||||
// 95020000 – eventType 0x0295 (SetTurbineChatChannels)
|
||||
// <40-byte payload>
|
||||
byte[] full = HexDecode(
|
||||
"B0F7000000000000210000009502000078563412" +
|
||||
"020000000300000004000000050000000A000000" +
|
||||
"09000000070000000800000009000000");
|
||||
|
||||
// The parser consumes only the 40-byte payload after the
|
||||
// GameEventEnvelope header (16 bytes).
|
||||
var env = GameEventEnvelope.TryParse(full);
|
||||
Assert.NotNull(env);
|
||||
Assert.Equal(GameEventType.SetTurbineChatChannels, env!.Value.EventType);
|
||||
|
||||
var parsed = SetTurbineChatChannels.TryParse(env.Value.Payload.Span);
|
||||
Assert.NotNull(parsed);
|
||||
// Allegiance: 0x12345678 (Unknown id used in fixture).
|
||||
Assert.Equal(0x12345678u, parsed!.Value.AllegianceRoom);
|
||||
// General..Olthoi
|
||||
Assert.Equal(2u, parsed.Value.GeneralRoom);
|
||||
Assert.Equal(3u, parsed.Value.TradeRoom);
|
||||
Assert.Equal(4u, parsed.Value.LfgRoom);
|
||||
Assert.Equal(5u, parsed.Value.RoleplayRoom);
|
||||
Assert.Equal(10u, parsed.Value.OlthoiRoom);
|
||||
// Society = SocietyRadiantBlood (id 9) per fixture
|
||||
Assert.Equal(9u, parsed.Value.SocietyRoom);
|
||||
// Sub-society rooms 7, 8, 9
|
||||
Assert.Equal(7u, parsed.Value.SocietyCelestialHandRoom);
|
||||
Assert.Equal(8u, parsed.Value.SocietyEldrytchWebRoom);
|
||||
Assert.Equal(9u, parsed.Value.SocietyRadiantBloodRoom);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serialize_RoundTripsThroughTryParse()
|
||||
{
|
||||
var original = new SetTurbineChatChannels.Parsed(
|
||||
AllegianceRoom: 0x100,
|
||||
GeneralRoom: 0x200,
|
||||
TradeRoom: 0x300,
|
||||
LfgRoom: 0x400,
|
||||
RoleplayRoom: 0x500,
|
||||
OlthoiRoom: 0x600,
|
||||
SocietyRoom: 0x700,
|
||||
SocietyCelestialHandRoom: 0x800,
|
||||
SocietyEldrytchWebRoom: 0x900,
|
||||
SocietyRadiantBloodRoom: 0xA00);
|
||||
|
||||
byte[] payload = SetTurbineChatChannels.Serialize(original);
|
||||
Assert.Equal(SetTurbineChatChannels.PayloadSize, payload.Length);
|
||||
|
||||
var roundTripped = SetTurbineChatChannels.TryParse(payload);
|
||||
Assert.NotNull(roundTripped);
|
||||
Assert.Equal(original, roundTripped!.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_TruncatedPayload_ReturnsNull()
|
||||
{
|
||||
// 39 bytes — one short of the 40-byte minimum.
|
||||
Assert.Null(SetTurbineChatChannels.TryParse(new byte[39]));
|
||||
}
|
||||
|
||||
private static byte[] HexDecode(string hex)
|
||||
{
|
||||
if ((hex.Length & 1) != 0) throw new ArgumentException("hex length must be even");
|
||||
byte[] result = new byte[hex.Length / 2];
|
||||
for (int i = 0; i < result.Length; i++)
|
||||
result[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue