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;
|
||||
}
|
||||
}
|
||||
278
tests/AcDream.Core.Net.Tests/Messages/TurbineChatTests.cs
Normal file
278
tests/AcDream.Core.Net.Tests/Messages/TurbineChatTests.cs
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Net.Tests.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Phase I.6: <see cref="TurbineChat"/> codec round-trip tests for all
|
||||
/// three payload variants and the UTF-16LE Turbine string codec.
|
||||
///
|
||||
/// <para>
|
||||
/// Golden fixtures from holtburger
|
||||
/// <c>references/holtburger/crates/holtburger-protocol/src/messages/chat/turbine.rs</c>
|
||||
/// lines 555-639 — generated by ACE's
|
||||
/// <c>SyntheticProtocolTests.GenerateTurbineChatFixtures</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class TurbineChatTests
|
||||
{
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// Round-trip fixtures (parse, then re-serialise, then parse again)
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TryParse_EventSendToRoom_GoldenFixture()
|
||||
{
|
||||
byte[] fixture = HexDecode(
|
||||
"DEF700005E000000010000000100000001000000" +
|
||||
"B5000B0001000000B5000B00000000003E000000" +
|
||||
"020000000541006C006900630065000B68006500" +
|
||||
"6C006C006F00200077006F0072006C0064000C00" +
|
||||
"0000010000500000000002000000");
|
||||
|
||||
var parsed = TurbineChat.TryParse(fixture);
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal(TurbineChat.BlobType.EventBinary, parsed!.Value.BlobType);
|
||||
Assert.Equal(TurbineChat.DispatchType.SendToRoomByName, parsed.Value.DispatchType);
|
||||
Assert.Equal(1u, parsed.Value.TargetType);
|
||||
Assert.Equal(0x000B00B5u, parsed.Value.TargetId);
|
||||
Assert.Equal(1u, parsed.Value.TransportType);
|
||||
Assert.Equal(0x000B00B5u, parsed.Value.TransportId);
|
||||
Assert.Equal(0u, parsed.Value.Cookie);
|
||||
|
||||
var ev = Assert.IsType<TurbineChat.Payload.EventSendToRoom>(parsed.Value.Body);
|
||||
Assert.Equal(2u, ev.RoomId); // TurbineChatChannel.General
|
||||
Assert.Equal("Alice", ev.SenderName);
|
||||
Assert.Equal("hello world", ev.Message);
|
||||
Assert.Equal(0x0Cu, ev.ExtraDataSize);
|
||||
Assert.Equal(0x50000001u, ev.SenderId);
|
||||
Assert.Equal(0, ev.HResult);
|
||||
Assert.Equal(2u, ev.ChatType); // TurbineChatType.General
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_EventSendToRoom_RoundTripsThroughTryParse()
|
||||
{
|
||||
byte[] built = TurbineChat.Build(
|
||||
blobType: TurbineChat.BlobType.EventBinary,
|
||||
dispatchType: TurbineChat.DispatchType.SendToRoomByName,
|
||||
targetType: 1u,
|
||||
targetId: 0x000B00B5u,
|
||||
transportType: 1u,
|
||||
transportId: 0x000B00B5u,
|
||||
cookie: 0u,
|
||||
payload: new TurbineChat.Payload.EventSendToRoom(
|
||||
RoomId: 2u,
|
||||
SenderName: "Alice",
|
||||
Message: "hello world",
|
||||
ExtraDataSize: 0x0Cu,
|
||||
SenderId: 0x50000001u,
|
||||
HResult: 0,
|
||||
ChatType: 2u));
|
||||
|
||||
var parsed = TurbineChat.TryParse(built);
|
||||
Assert.NotNull(parsed);
|
||||
var ev = Assert.IsType<TurbineChat.Payload.EventSendToRoom>(parsed!.Value.Body);
|
||||
Assert.Equal("Alice", ev.SenderName);
|
||||
Assert.Equal("hello world", ev.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_Response_GoldenFixture()
|
||||
{
|
||||
byte[] fixture = HexDecode(
|
||||
"DEF7000038000000050000000100000001000000" +
|
||||
"B5000B0001000000B5000B00000000001800000007000000" +
|
||||
"020000000200000000000000");
|
||||
|
||||
var parsed = TurbineChat.TryParse(fixture);
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal(TurbineChat.BlobType.ResponseBinary, parsed!.Value.BlobType);
|
||||
Assert.Equal(TurbineChat.DispatchType.SendToRoomByName, parsed.Value.DispatchType);
|
||||
var resp = Assert.IsType<TurbineChat.Payload.Response>(parsed.Value.Body);
|
||||
Assert.Equal(7u, resp.ContextId);
|
||||
Assert.Equal(2u, resp.ResponseId);
|
||||
Assert.Equal(2u, resp.MethodId);
|
||||
Assert.Equal(0, resp.HResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_Response_RoundTripsThroughTryParse()
|
||||
{
|
||||
byte[] built = TurbineChat.Build(
|
||||
blobType: TurbineChat.BlobType.ResponseBinary,
|
||||
dispatchType: TurbineChat.DispatchType.SendToRoomByName,
|
||||
targetType: 1u, targetId: 0x000B00B5u,
|
||||
transportType: 1u, transportId: 0x000B00B5u,
|
||||
cookie: 0u,
|
||||
payload: new TurbineChat.Payload.Response(
|
||||
ContextId: 7u, ResponseId: 2u, MethodId: 2u, HResult: 0));
|
||||
|
||||
var parsed = TurbineChat.TryParse(built);
|
||||
Assert.NotNull(parsed);
|
||||
var resp = Assert.IsType<TurbineChat.Payload.Response>(parsed!.Value.Body);
|
||||
Assert.Equal(7u, resp.ContextId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_RequestSendToRoomById_GoldenFixture()
|
||||
{
|
||||
byte[] fixture = HexDecode(
|
||||
"DEF700005D000000030000000200000001000000" +
|
||||
"00000000000000000000000000000000" +
|
||||
"3D000000" +
|
||||
"07000000020000000200000002000000" +
|
||||
"0A7400720061006400650020007300700061006D00" +
|
||||
"0C000000010000500000000003000000");
|
||||
|
||||
var parsed = TurbineChat.TryParse(fixture);
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal(TurbineChat.BlobType.RequestBinary, parsed!.Value.BlobType);
|
||||
Assert.Equal(TurbineChat.DispatchType.SendToRoomById, parsed.Value.DispatchType);
|
||||
var req = Assert.IsType<TurbineChat.Payload.RequestSendToRoomById>(parsed.Value.Body);
|
||||
Assert.Equal(7u, req.ContextId);
|
||||
Assert.Equal(2u, req.RoomId); // TurbineChatChannel.General
|
||||
Assert.Equal("trade spam", req.Message);
|
||||
Assert.Equal(0x0Cu, req.ExtraDataSize);
|
||||
Assert.Equal(0x50000001u, req.SenderId);
|
||||
Assert.Equal(0, req.HResult);
|
||||
Assert.Equal(3u, req.ChatType); // TurbineChatType.Trade
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_RequestSendToRoomById_RoundTripsThroughTryParse()
|
||||
{
|
||||
byte[] built = TurbineChat.Build(
|
||||
blobType: TurbineChat.BlobType.RequestBinary,
|
||||
dispatchType: TurbineChat.DispatchType.SendToRoomById,
|
||||
targetType: 1u, targetId: 0u,
|
||||
transportType: 0u, transportId: 0u,
|
||||
cookie: 0u,
|
||||
payload: new TurbineChat.Payload.RequestSendToRoomById(
|
||||
ContextId: 7u,
|
||||
RoomId: 2u,
|
||||
Message: "trade spam",
|
||||
ExtraDataSize: 0x0Cu,
|
||||
SenderId: 0x50000001u,
|
||||
HResult: 0,
|
||||
ChatType: 3u));
|
||||
|
||||
var parsed = TurbineChat.TryParse(built);
|
||||
Assert.NotNull(parsed);
|
||||
var req = Assert.IsType<TurbineChat.Payload.RequestSendToRoomById>(parsed!.Value.Body);
|
||||
Assert.Equal("trade spam", req.Message);
|
||||
Assert.Equal(2u, req.RoomId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_RequestRejectsNonAceRequestIds()
|
||||
{
|
||||
// Per turbine.rs:642-651: ACE's RequestSendToRoomById is the only
|
||||
// (response_id=2, method_id=2) pair. Other ids are rejected.
|
||||
byte[] fixture = HexDecode(
|
||||
"DEF700005D000000030000000200000001000000" +
|
||||
"00000000000000000000000000000000" +
|
||||
"3D000000" +
|
||||
"07000000020000000200000002000000" +
|
||||
"0A7400720061006400650020007300700061006D00" +
|
||||
"0C000000010000500000000003000000");
|
||||
|
||||
// Mutate response_id (offset 4 + 36 + 4 = 44) to 3 — must reject.
|
||||
BitConverter.GetBytes(3u).CopyTo(fixture, 44);
|
||||
Assert.Null(TurbineChat.TryParse(fixture));
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// UTF-16LE Turbine string codec
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TurbineString_ShortPrefix_RoundTrips()
|
||||
{
|
||||
var buf = new List<byte>();
|
||||
TurbineChat.WriteTurbineString(buf, "Hello");
|
||||
// 1-byte prefix, then 5 UTF-16LE code units = 5 + 1 = 11 bytes.
|
||||
Assert.Equal(11, buf.Count);
|
||||
Assert.Equal(5, buf[0]); // length prefix
|
||||
Assert.Equal(0, 0x80 & buf[0]); // high bit clear
|
||||
|
||||
byte[] data = buf.ToArray();
|
||||
int pos = 0;
|
||||
string roundTrip = TurbineChat.ReadTurbineString(data, ref pos);
|
||||
Assert.Equal("Hello", roundTrip);
|
||||
Assert.Equal(data.Length, pos);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TurbineString_LongPrefix_RoundTrips()
|
||||
{
|
||||
// 200 chars > 0x80 → 2-byte prefix form.
|
||||
string s = new string('a', 200);
|
||||
var buf = new List<byte>();
|
||||
TurbineChat.WriteTurbineString(buf, s);
|
||||
|
||||
// Prefix: 2 bytes; high bit of first byte set.
|
||||
Assert.NotEqual(0, buf[0] & 0x80);
|
||||
// Decoded length = ((b0 & 0x7F) << 8) | b1
|
||||
int decodedLen = ((buf[0] & 0x7F) << 8) | buf[1];
|
||||
Assert.Equal(200, decodedLen);
|
||||
Assert.Equal(2 + 200 * 2, buf.Count);
|
||||
|
||||
byte[] data = buf.ToArray();
|
||||
int pos = 0;
|
||||
string roundTrip = TurbineChat.ReadTurbineString(data, ref pos);
|
||||
Assert.Equal(s, roundTrip);
|
||||
Assert.Equal(data.Length, pos);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TurbineString_NonAscii_RoundTripsAsUtf16LE()
|
||||
{
|
||||
// 'Café' has 4 UTF-16 code units, last with 0x00E9 — verifies
|
||||
// we're using UTF-16LE rather than CP1252 like the rest of the
|
||||
// chat layer.
|
||||
const string s = "Café";
|
||||
var buf = new List<byte>();
|
||||
TurbineChat.WriteTurbineString(buf, s);
|
||||
// Bytes: prefix(1) + UTF-16LE C-a-f-é = 1 + 8 = 9 bytes.
|
||||
Assert.Equal(9, buf.Count);
|
||||
Assert.Equal(4, buf[0]);
|
||||
// 'é' = 0xE9 0x00 in UTF-16LE.
|
||||
Assert.Equal(0xE9, buf[1 + 6]);
|
||||
Assert.Equal(0x00, buf[1 + 7]);
|
||||
|
||||
byte[] data = buf.ToArray();
|
||||
int pos = 0;
|
||||
string roundTrip = TurbineChat.ReadTurbineString(data, ref pos);
|
||||
Assert.Equal(s, roundTrip);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TurbineString_EmptyString_RoundTrips()
|
||||
{
|
||||
var buf = new List<byte>();
|
||||
TurbineChat.WriteTurbineString(buf, "");
|
||||
Assert.Single(buf);
|
||||
Assert.Equal(0, buf[0]);
|
||||
|
||||
byte[] data = buf.ToArray();
|
||||
int pos = 0;
|
||||
Assert.Equal("", TurbineChat.ReadTurbineString(data, ref pos));
|
||||
Assert.Equal(1, pos);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// Helpers
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
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