feat(battle-node): EngineIoFrame parse/encode for EIO3 packets
This commit is contained in:
21
SVSim.BattleNode/Wire/EngineIoFrame.cs
Normal file
21
SVSim.BattleNode/Wire/EngineIoFrame.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
namespace SVSim.BattleNode.Wire;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Engine.IO v3 packet in WebSocket transport mode. Wire form: <c><digit><payload></c>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record EngineIoFrame(EngineIoPacketType Type, string Payload)
|
||||||
|
{
|
||||||
|
public static EngineIoFrame Parse(string raw)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(raw))
|
||||||
|
throw new ArgumentException("Empty EIO frame", nameof(raw));
|
||||||
|
var typeChar = raw[0];
|
||||||
|
if (typeChar < '0' || typeChar > '6')
|
||||||
|
throw new ArgumentException($"Invalid EIO type char '{typeChar}'", nameof(raw));
|
||||||
|
var type = (EngineIoPacketType)(typeChar - '0');
|
||||||
|
var payload = raw.Length > 1 ? raw.Substring(1) : string.Empty;
|
||||||
|
return new EngineIoFrame(type, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Encode() => $"{(int)Type}{Payload}";
|
||||||
|
}
|
||||||
22
SVSim.BattleNode/Wire/EngineIoHandshake.cs
Normal file
22
SVSim.BattleNode/Wire/EngineIoHandshake.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace SVSim.BattleNode.Wire;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Payload of an EIO3 Open packet. Sent by the server to the client immediately after the WS upgrade.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record EngineIoHandshake(
|
||||||
|
[property: JsonPropertyName("sid")] string Sid,
|
||||||
|
[property: JsonPropertyName("upgrades")] string[] Upgrades,
|
||||||
|
[property: JsonPropertyName("pingInterval")] int PingInterval,
|
||||||
|
[property: JsonPropertyName("pingTimeout")] int PingTimeout)
|
||||||
|
{
|
||||||
|
// Wire-key casing here is bare camelCase — NOT EmulatedEntrypoint's snake_case policy.
|
||||||
|
private static readonly JsonSerializerOptions Options = new()
|
||||||
|
{
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
|
};
|
||||||
|
|
||||||
|
public string ToJson() => JsonSerializer.Serialize(this, Options);
|
||||||
|
}
|
||||||
12
SVSim.BattleNode/Wire/EngineIoPacketType.cs
Normal file
12
SVSim.BattleNode/Wire/EngineIoPacketType.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace SVSim.BattleNode.Wire;
|
||||||
|
|
||||||
|
public enum EngineIoPacketType
|
||||||
|
{
|
||||||
|
Open = 0,
|
||||||
|
Close = 1,
|
||||||
|
Ping = 2,
|
||||||
|
Pong = 3,
|
||||||
|
Message = 4,
|
||||||
|
Upgrade = 5,
|
||||||
|
Noop = 6,
|
||||||
|
}
|
||||||
51
SVSim.UnitTests/BattleNode/Wire/EngineIoFrameTests.cs
Normal file
51
SVSim.UnitTests/BattleNode/Wire/EngineIoFrameTests.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
using NUnit.Framework;
|
||||||
|
using SVSim.BattleNode.Wire;
|
||||||
|
|
||||||
|
namespace SVSim.UnitTests.BattleNode.Wire;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class EngineIoFrameTests
|
||||||
|
{
|
||||||
|
[TestCase("0{\"sid\":\"abc\"}", EngineIoPacketType.Open, "{\"sid\":\"abc\"}")]
|
||||||
|
[TestCase("2", EngineIoPacketType.Ping, "")]
|
||||||
|
[TestCase("3", EngineIoPacketType.Pong, "")]
|
||||||
|
[TestCase("4hello", EngineIoPacketType.Message, "hello")]
|
||||||
|
public void Parse_RecognizesTypeAndPayload(string raw, EngineIoPacketType expectedType, string expectedPayload)
|
||||||
|
{
|
||||||
|
var frame = EngineIoFrame.Parse(raw);
|
||||||
|
|
||||||
|
Assert.That(frame.Type, Is.EqualTo(expectedType));
|
||||||
|
Assert.That(frame.Payload, Is.EqualTo(expectedPayload));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(EngineIoPacketType.Open, "{\"sid\":\"abc\"}", "0{\"sid\":\"abc\"}")]
|
||||||
|
[TestCase(EngineIoPacketType.Pong, "", "3")]
|
||||||
|
[TestCase(EngineIoPacketType.Message, "2[\"msg\",{}]", "42[\"msg\",{}]")]
|
||||||
|
public void Encode_EmitsTypeDigitFollowedByPayload(EngineIoPacketType type, string payload, string expected)
|
||||||
|
{
|
||||||
|
var frame = new EngineIoFrame(type, payload);
|
||||||
|
|
||||||
|
Assert.That(frame.Encode(), Is.EqualTo(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Parse_EmptyInput_Throws()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(() => EngineIoFrame.Parse(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Parse_NonDigitFirstChar_Throws()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(() => EngineIoFrame.Parse("xfoo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void EngineIoHandshake_SerializesWithCamelCaseKeys()
|
||||||
|
{
|
||||||
|
var hs = new EngineIoHandshake("abc123", Array.Empty<string>(), 25000, 60000);
|
||||||
|
|
||||||
|
Assert.That(hs.ToJson(),
|
||||||
|
Is.EqualTo("{\"sid\":\"abc123\",\"upgrades\":[],\"pingInterval\":25000,\"pingTimeout\":60000}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user