using System.Net; using System.Text; using System.Text.Json.Nodes; using CommonwealthOnline.Server; namespace CommonwealthOnline.Server.Tests; internal static class Program { private static int _passed; private static int _failed; public static async Task Main() { await Run("transport policy", TestTransportPolicy); await Run("packet codec", TestPacketCodec); await Run("snapshot sequence", TestSnapshotSequence); await Run("snapshot envelope", TestSnapshotEnvelope); await Run("player state validation", TestPlayerStateValidation); await Run("npc authority epochs", TestNpcAuthority); await Run("interest filtering", TestInterestFiltering); await Run("config compatibility", TestConfigCompatibility); await Run("ban persistence", TestBanStore); await Run("authoritative server ids and interest", TestAuthoritativeServer); await Run("durable player state relay", TestPlayerStateRelay); await Run("npc authority validation", TestNpcAuthorityIntegration); await Run("combat interest routing", TestCombatInterest); Console.WriteLine($"C# server tests: {_passed} passed, {_failed} failed"); return _failed == 0 ? 0 : 1; } private static async Task Run(string name, Func test) { try { await test(); _passed++; Console.WriteLine($"PASS {name}"); } catch (Exception ex) { _failed++; Console.Error.WriteLine($"FAIL {name}: {ex.Message}"); } } private static Task TestTransportPolicy() { Equal(Delivery.UnreliableSequenced, TransportPolicy.ForPacketType("transform")); Equal(Delivery.UnreliableSequenced, TransportPolicy.ForPacketType("npcState")); Equal(Delivery.ReliableOrdered, TransportPolicy.ForPacketType("playerState")); Equal(Delivery.ReliableOrdered, TransportPolicy.ForPacketType("futureControl")); return Task.CompletedTask; } private static Task TestPacketCodec() { var packet = new JsonObject { ["type"] = "playerState", ["characterName"] = "Nomad" }; var encoded = PacketCodec.Encode(packet); Equal("playerState", encoded.PacketType); Equal(Delivery.ReliableOrdered, encoded.Delivery); var decoded = PacketCodec.Decode(encoded.Payload); Equal("Nomad", JsonHelpers.String(decoded["characterName"])); Throws(() => PacketCodec.Encode(new JsonObject { ["type"] = "x", ["blob"] = new string('a', ProtocolConstants.MaxMessageBytes + 1) })); Throws(() => PacketCodec.Decode("[]"u8)); return Task.CompletedTask; } private static Task TestSnapshotSequence() { var window = new SequenceWindow(); True(window.Accept(1)); True(window.Accept(3)); False(window.Accept(2)); False(window.Accept(3)); True(SequenceWindow.IsNewer(1, uint.MaxValue)); var counter = new SequenceCounter(); Equal(1u, counter.Advance()); return Task.CompletedTask; } private static Task TestSnapshotEnvelope() { var payload = Encoding.UTF8.GetBytes("{\"type\":\"transform\"}"); var wire = GnsSnapshotEnvelope.Encode("transform", payload, 7); Equal(ProtocolConstants.MaxMessageBytes >= wire.Length, true); True(GnsSnapshotEnvelope.TryDecode(wire, out var envelope, out var error)); Equal(null, error); Equal("transform", envelope.PacketType); Equal(7u, envelope.Sequence); SequenceEqual(payload, envelope.Payload); False(GnsSnapshotEnvelope.TryDecode(payload, out _, out _)); return Task.CompletedTask; } private static Task TestPlayerStateValidation() { var state = new JsonObject { ["type"] = "playerState", ["characterName"] = "Nomad", ["equippedItems"] = new JsonArray(new JsonObject { ["slot"] = "RightHand", ["formId"] = "ABC" }), ["appearance"] = new JsonObject { ["version"] = 4, ["raceFormId"] = "13746", ["hairColorFormId"] = "123", ["facialHairColorFormId"] = "124", ["complexionFormId"] = "125", ["height"] = 1.0, ["isFemale"] = false, ["morphWeight"] = new JsonObject { ["thin"] = 0.0, ["muscular"] = 0.0, ["large"] = 0.0 }, ["bodyTintColor"] = new JsonObject { ["r"] = 255, ["g"] = 255, ["b"] = 255, ["a"] = 255 } }, ["actionEvents"] = new JsonArray(new JsonObject { ["sequence"] = 1, ["type"] = 3, ["eventName"] = "fireSingle" }) }; var normalized = ProtocolValidation.NormalizePlayerState(state); NotNull(normalized); Equal("00000123", JsonHelpers.String(((JsonObject)normalized!["appearance"]!)["hairColorFormId"])); var bad = (JsonObject)state.DeepClone(); ((JsonArray)bad["actionEvents"]!)[0]!["eventName"] = "bogus"; Equal(null, ProtocolValidation.NormalizePlayerState(bad)); return Task.CompletedTask; } private static Task TestNpcAuthority() { var manager = new NpcAuthorityManager(); var scope = new ScopeKey("00000001", ""); var changes = manager.Reconcile(new[] { (2u, scope), (1u, scope) }); Equal(1, changes.Count); Equal(1u, changes[0].PlayerId); Equal(1u, changes[0].Epoch); True(manager.Authorize(1, scope, 1)); changes = manager.Reconcile(Array.Empty<(uint, ScopeKey)>()); Equal(2u, changes[0].Epoch); changes = manager.Reconcile(new[] { (3u, scope) }); Equal(3u, changes[0].Epoch); False(manager.Authorize(1, scope, 1)); True(manager.Authorize(3, scope, 3)); return Task.CompletedTask; } private static Task TestInterestFiltering() { var a = Transform("00000001", "000000AA", 0, 0); var sameCell = Transform("00000001", "000000BB", 999999, 999999); var near = Transform("00000002", "000000AA", 1000, 1000); var far = Transform("00000003", "000000AA", 20000, 0); var otherWorld = Transform("00000004", "000000BB", 0, 0); True(ProtocolValidation.StatesShareInterest(a, sameCell)); True(ProtocolValidation.StatesShareInterest(a, near)); False(ProtocolValidation.StatesShareInterest(a, far)); False(ProtocolValidation.StatesShareInterest(a, otherWorld)); return Task.CompletedTask; } private static Task TestConfigCompatibility() { var dir = TempDir(); try { var path = Path.Combine(dir, "commonwealth-server.json"); File.WriteAllText(path, "{\"host\":\"0.0.0.0\",\"port\":7777,\"max_players\":16,\"admin_port\":7779,\"enable_gns_transport\":\"false\"}"); var options = ServerOptions.Load(path); False(options.EnableGnsTransport); Equal(7777, options.Port); Equal(0, options.Validate().Count); } finally { Directory.Delete(dir, true); } return Task.CompletedTask; } private static Task TestBanStore() { var dir = TempDir(); try { var path = Path.Combine(dir, "bans.json"); var store = new BanStore(path); store.Ban("127.0.0.1", "test"); var reloaded = new BanStore(path); Equal("test", reloaded.GetBan("127.0.0.1")?.Reason); True(reloaded.Unban("127.0.0.1")); Equal(0, new BanStore(path).List().Count); } finally { Directory.Delete(dir, true); } return Task.CompletedTask; } private static async Task TestAuthoritativeServer() { var fixture = CreateServerFixture(); await using var server = fixture.Server; var a = new MemoryConnection("a", 31001); var b = new MemoryConnection("b", 31002); True(await server.AcceptConnectionAsync(a, CancellationToken.None)); True(await server.AcceptConnectionAsync(b, CancellationToken.None)); await Send(server, a, new JsonObject { ["type"] = "hello", ["protocolVersion"] = 2 }); await Send(server, b, new JsonObject { ["type"] = "hello", ["protocolVersion"] = 2 }); var aId = ReadyId(a); var bId = ReadyId(b); True(aId != bId); await Send(server, a, Transform("00000001", "000000AA", 0, 0, "spawn")); await Send(server, b, Transform("00000002", "000000BB", 0, 0, "spawn")); a.Clear(); b.Clear(); await Send(server, a, Transform("00000001", "000000AA", 1, 0)); False(b.SentPackets.Any(p => JsonHelpers.String(p["type"]) == "transform" && JsonHelpers.TryUInt32(p["playerId"], 1, uint.MaxValue, out var id) && id == aId)); } private static async Task TestPlayerStateRelay() { var fixture = CreateServerFixture(); await using var server = fixture.Server; var a = new MemoryConnection("a", 32001); var b = new MemoryConnection("b", 32002); await Activate(server, a, b); await Send(server, a, Transform("00000001", "000000AA", 0, 0, "spawn")); await Send(server, b, Transform("00000002", "000000BB", 0, 0, "spawn")); a.Clear(); b.Clear(); await Send(server, a, new JsonObject { ["type"] = "playerState", ["characterName"] = "Nomad", ["actionEvents"] = new JsonArray(new JsonObject { ["sequence"] = 1, ["type"] = 3, ["eventName"] = "fireSingle" }) }); var relay = b.SentPackets.Single(p => JsonHelpers.String(p["type"]) == "playerState"); Equal("Nomad", JsonHelpers.String(relay["characterName"])); False(relay.ContainsKey("actionEvents")); } private static async Task TestNpcAuthorityIntegration() { var fixture = CreateServerFixture(); await using var server = fixture.Server; var a = new MemoryConnection("a", 33001); True(await server.AcceptConnectionAsync(a, CancellationToken.None)); await Send(server, a, new JsonObject { ["type"] = "hello", ["protocolVersion"] = 2 }); await Send(server, a, Transform("00000010", "", 0, 0, "spawn")); var authority = a.SentPackets.Last(p => JsonHelpers.String(p["type"]) == "npcAuthority"); JsonHelpers.TryUInt32(authority["authorityEpoch"], 1, uint.MaxValue, out var epoch); a.Clear(); await Send(server, a, new JsonObject { ["type"] = "npcState", ["authorityEpoch"] = epoch, ["authorityCellId"] = "00000010", ["authorityWorldspaceId"] = "", ["npcs"] = new JsonArray(new JsonObject { ["sourceFormId"] = "00000020", ["cellId"] = "00000010", ["worldspaceId"] = "", ["x"] = 1.0, ["y"] = 2.0, ["z"] = 3.0, ["angleZ"] = 0.0 }) }); var stats = server.GetCoreStats(); True(JsonHelpers.TryUInt32(stats["npcStatePacketsReceived"], 0, uint.MaxValue, out var received) && received == 1); await Send(server, a, new JsonObject { ["type"] = "npcState", ["authorityEpoch"] = Math.Max(1u, epoch - 1), ["authorityCellId"] = "00000010", ["authorityWorldspaceId"] = "", ["npcs"] = new JsonArray() }); stats = server.GetCoreStats(); True(JsonHelpers.TryUInt32(stats["npcAuthorityRejects"], 0, uint.MaxValue, out var rejects) && rejects >= 1); } private static async Task TestCombatInterest() { var fixture = CreateServerFixture(); await using var server = fixture.Server; var a = new MemoryConnection("a", 34001); var b = new MemoryConnection("b", 34002); await Activate(server, a, b); var bId = ReadyId(b); await Send(server, a, Transform("00000100", "000000AA", 0, 0, "spawn")); await Send(server, b, Transform("00000200", "000000BB", 0, 0, "spawn")); a.Clear(); b.Clear(); await Send(server, a, new JsonObject { ["type"] = "combatHit", ["targetPlayerId"] = bId, ["sequence"] = 1, ["damage"] = 10.0 }); False(b.SentPackets.Any(p => JsonHelpers.String(p["type"]) == "combatHit")); var stats = server.GetCoreStats(); True(JsonHelpers.TryUInt32(stats["packetsRejected"], 0, uint.MaxValue, out var rejected) && rejected >= 1); } private static (AuthoritativeServer Server, string Dir) CreateServerFixture() { var dir = TempDir(); var path = Path.Combine(dir, "commonwealth-server.json"); var options = new ServerOptions { ConfigPath = path, Host = "127.0.0.1", Port = 7777, AdminPort = 7779, MaxPlayers = 32 }; return (new AuthoritativeServer(options), dir); } private static async Task Activate(AuthoritativeServer server, params MemoryConnection[] connections) { foreach (var connection in connections) { True(await server.AcceptConnectionAsync(connection, CancellationToken.None)); await Send(server, connection, new JsonObject { ["type"] = "hello", ["protocolVersion"] = 2 }); } } private static uint ReadyId(MemoryConnection connection) { var ready = connection.SentPackets.Last(p => JsonHelpers.String(p["type"]) == "sessionReady"); True(JsonHelpers.TryUInt32(ready["playerId"], 1, uint.MaxValue, out var id)); return id; } private static Task Send(AuthoritativeServer server, MemoryConnection connection, JsonObject packet) => server.HandleMessageAsync(connection, PacketCodec.Encode(packet).Payload, CancellationToken.None); private static JsonObject Transform(string cell, string world, double x, double y, string movement = "normal") => new() { ["type"] = "transform", ["x"] = x, ["y"] = y, ["z"] = 0.0, ["angleZ"] = 0.0, ["cellId"] = cell, ["worldspaceId"] = world, ["movementType"] = movement }; private static string TempDir() { var path = Path.Combine(Path.GetTempPath(), "co-csharp-tests-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(path); return path; } private static void True(bool value) { if (!value) throw new Exception("expected true"); } private static void False(bool value) { if (value) throw new Exception("expected false"); } private static void NotNull(object? value) { if (value is null) throw new Exception("expected non-null"); } private static void Equal(T expected, T actual) where T : notnull { if (!EqualityComparer.Default.Equals(expected, actual)) throw new Exception($"expected {expected}, got {actual}"); } private static void SequenceEqual(byte[] expected, byte[] actual) { if (!expected.AsSpan().SequenceEqual(actual)) throw new Exception("byte sequences differ"); } private static void Throws(Action action) where T : Exception { try { action(); } catch (T) { return; } throw new Exception($"expected {typeof(T).Name}"); } private sealed class MemoryConnection : IGameConnection { private readonly object _gate = new(); private readonly List _sent = new(); private int _closed; public MemoryConnection(string name, int port) { ConnectionKey = "memory:" + name; RemoteEndpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), port); } public string ConnectionKey { get; } public string TransportName => "memory"; public IPEndPoint RemoteEndpoint { get; } public bool IsClosed => Volatile.Read(ref _closed) != 0; public IReadOnlyList SentPackets { get { lock (_gate) return _sent.Select(JsonHelpers.CloneObject).ToArray(); } } public ValueTask SendAsync(EncodedPacket packet, CancellationToken cancellationToken = default) { if (IsClosed) return ValueTask.FromResult(SendOutcome.NotConnected); lock (_gate) _sent.Add(PacketCodec.Decode(packet.Payload)); return ValueTask.FromResult(SendOutcome.Sent); } public ValueTask DisconnectAsync(int reason, string debug) { Interlocked.Exchange(ref _closed, 1); return ValueTask.CompletedTask; } public ValueTask DisposeAsync() => DisconnectAsync(0, "dispose"); public void Clear() { lock (_gate) _sent.Clear(); } } }