using System.Collections.Concurrent; using System.Net; using System.Text.Json.Nodes; namespace CommonwealthOnline.Server; internal sealed class AuthoritativeServer : IServerIngress, IAsyncDisposable { private const int MaxPacketsPerSecond = 120; private const int MaxConnectAttempts = 8; private const double ConnectAttemptWindowSeconds = 10.0; private const double ClientIdleTimeoutSeconds = 60.0; private const double ClientHandshakeTimeoutSeconds = 10.0; private readonly ServerOptions _options; private readonly BanStore _banStore; private readonly object _gate = new(); private readonly Dictionary _clients = new(StringComparer.Ordinal); private readonly Dictionary _lastPlayerStateByPlayerId = new(); private readonly Dictionary _lastNpcStateByScope = new(); private readonly NpcAuthorityManager _npcAuthority = new(); private readonly Dictionary> _connectAttempts = new(StringComparer.Ordinal); private readonly Dictionary _stats = new(StringComparer.Ordinal); private readonly Dictionary _serverWorldState = new(StringComparer.Ordinal); private readonly CancellationTokenSource _shutdown = new(); private readonly Task _maintenanceTask; private JsonObject? _lastLegacyNpcState; private uint _nextPlayerId = 1; private uint? _worldStateHostPlayerId; private readonly double _startedAt = JsonHelpers.UnixTime(); public AuthoritativeServer(ServerOptions options) { _options = options; _banStore = new BanStore(options.BansPath); foreach (var name in new[] { "clientsConnected", "clientsDisconnected", "pendingConnectionsRejected", "packetsReceived", "packetsSent", "packetsBroadcast", "packetsRejected", "rateLimitedPackets", "transformPacketsReceived", "transformPacketsBroadcast", "transformPacketsInterestFiltered", "playerStatePacketsReceived", "playerStatePacketsBroadcast", "movementPacketsRejected", "movementCorrectionsSent", "worldStatePacketsReceived", "worldStatePacketsBroadcast", "npcStatePacketsReceived", "npcStatePacketsBroadcast", "npcAuthorityChanges", "npcAuthorityRejects", "combatHitsReceived", "combatHitsRouted", "worldStateHostPacketsBroadcast", "serverWorldStatePacketsBroadcast", "sessionEndedPacketsSent", "bannedConnectionsRejected", "disconnectPacketsBroadcast", "protocolV2Connections", "legacyConnections" }) _stats[name] = 0; _maintenanceTask = Task.Run(() => MaintenanceLoopAsync(_shutdown.Token)); } public event Action? LogMessage; public void Log(string message, string level = "info") { if (!ShouldLog(level)) return; LogMessage?.Invoke(message, level); } private bool ShouldLog(string level) { static int Rank(string value) => value switch { "debug" => 10, "info" => 20, "warning" => 30, "error" => 40, _ => 20 }; return Rank(level) >= Rank(_options.LogVerbosity); } public async Task AcceptConnectionAsync(IGameConnection connection, CancellationToken cancellationToken) { var ip = connection.RemoteEndpoint.Address.ToString(); var ban = _banStore.GetBan(ip); if (ban is not null) { Increment("bannedConnectionsRejected"); await SendDirectSessionEndedAsync(connection, "banned", ban.Value.Reason, cancellationToken).ConfigureAwait(false); await connection.DisconnectAsync(0, "Banned"); return false; } if (!AllowConnectAttempt(ip)) { Increment("pendingConnectionsRejected"); await SendDirectSessionEndedAsync(connection, "rate_limited", "Too many connection attempts.", cancellationToken).ConfigureAwait(false); await connection.DisconnectAsync(0, "Connection attempt rate limited"); return false; } ClientSession client; lock (_gate) { var maxPending = Math.Max(16, _options.MaxPlayers * 2); var pending = _clients.Values.Count(x => !x.GameplayActive); if (pending >= maxPending) client = null!; else { client = new ClientSession(connection, _nextPlayerId++); _clients[connection.ConnectionKey] = client; } } if (client is null) { Increment("pendingConnectionsRejected"); await SendDirectSessionEndedAsync(connection, "rate_limited", "Too many pending connections.", cancellationToken).ConfigureAwait(false); await connection.DisconnectAsync(0, "Too many pending connections"); return false; } var capabilities = new JsonArray("interest-v1", "hello-v2", "bounded-framing", "rate-limit-v1", "movement-correction-v1", "npc-authority-epoch-v1", "player-state-v1"); if (connection.TransportName == "gns") { capabilities.Add("gns-message-transport-v1"); capabilities.Add("gns-snapshot-sequence-v1"); } var welcome = new JsonObject { ["type"] = "welcome", ["playerId"] = client.PlayerId, ["serverTime"] = JsonHelpers.UnixTime(), ["serverName"] = _options.ServerName, ["serverDescription"] = _options.ServerDescription, ["protocolVersion"] = ProtocolConstants.ProtocolVersion, ["capabilities"] = capabilities }; Log($"{connection.TransportName.ToUpperInvariant()} accept: {client.Label} (provisional player {client.PlayerId})", "debug"); if (!await SendPacketAsync(client, welcome, false, cancellationToken).ConfigureAwait(false)) { await DisconnectClientAsync(client).ConfigureAwait(false); return false; } return true; } public async Task HandleMessageAsync(IGameConnection connection, ReadOnlyMemory payload, CancellationToken cancellationToken) { var client = FindByConnection(connection); if (client is null) { await connection.DisconnectAsync(0, "Unknown session"); return; } if (!await AllowPacketAsync(client).ConfigureAwait(false)) return; client.RecordReceived(); Increment("packetsReceived"); JsonObject packet; try { packet = PacketCodec.Decode(payload.Span); } catch (PacketCodecException ex) { Reject(client, ex.Message); return; } await DispatchPacketAsync(client, packet, cancellationToken).ConfigureAwait(false); } public async Task HandleTransportRejectAsync(IGameConnection connection, string reason, bool warning = false) { var client = FindByConnection(connection); if (client is null) return; if (!await AllowPacketAsync(client).ConfigureAwait(false)) return; client.RecordReceived(); Increment("packetsReceived"); Reject(client, reason, warning); } public Task HandleConnectionClosedAsync(IGameConnection connection) { var client = FindByConnection(connection); return client is null ? Task.CompletedTask : DisconnectClientAsync(client); } public async Task EndSessionForTransportAsync(IGameConnection connection, string code, string reason) { var client = FindByConnection(connection); if (client is null) { await SendDirectSessionEndedAsync(connection, code, reason, CancellationToken.None).ConfigureAwait(false); await connection.DisconnectAsync(0, reason); return; } await EndSessionAsync(client, code, reason).ConfigureAwait(false); } private async Task DispatchPacketAsync(ClientSession client, JsonObject packet, CancellationToken cancellationToken) { var packetType = JsonHelpers.String(packet["type"]); if (packetType == "hello") { await HandleHelloAsync(client, packet, cancellationToken).ConfigureAwait(false); return; } if (packetType == "keepAlive") return; if (!client.GameplayActive) { if (packetType is not ("transform" or "worldState" or "npcState" or "combatHit")) { Reject(client, "Gameplay packet received before session activation"); return; } if (!await ActivateClientAsync(client, ProtocolConstants.LegacyProtocolVersion, cancellationToken).ConfigureAwait(false)) { await EndSessionAsync(client, "server_full", "Server is full.").ConfigureAwait(false); return; } Log($"Legacy client player {client.PlayerId} activated without hello; client upgrade recommended.", "warning"); } switch (packetType) { case "transform": await HandleTransformAsync(client, packet, cancellationToken).ConfigureAwait(false); break; case "playerState": await HandlePlayerStateAsync(client, packet, cancellationToken).ConfigureAwait(false); break; case "worldState": await HandleWorldStateAsync(client, packet, cancellationToken).ConfigureAwait(false); break; case "npcState": await HandleNpcStateAsync(client, packet, cancellationToken).ConfigureAwait(false); break; case "combatHit": await HandleCombatHitAsync(client, packet, cancellationToken).ConfigureAwait(false); break; default: Reject(client, $"Unknown packet type: {packetType ?? ""}"); break; } } private async Task HandleHelloAsync(ClientSession client, JsonObject packet, CancellationToken cancellationToken) { if (!JsonHelpers.TryUInt32(packet["protocolVersion"], 1, ushort.MaxValue, out var version) || version != ProtocolConstants.ProtocolVersion) { await EndSessionAsync(client, "protocol_mismatch", $"Server requires protocol {ProtocolConstants.ProtocolVersion}.").ConfigureAwait(false); return; } if (!await ActivateClientAsync(client, (int)version, cancellationToken).ConfigureAwait(false)) await EndSessionAsync(client, "server_full", "Server is full.").ConfigureAwait(false); } private async Task ActivateClientAsync(ClientSession client, int protocolVersion, CancellationToken cancellationToken) { bool becameHost; lock (_gate) { if (client.GameplayActive) return true; if (_clients.Values.Count(x => x.GameplayActive) >= _options.MaxPlayers) return false; if (!client.Activate(protocolVersion)) return true; _stats["clientsConnected"]++; _stats[protocolVersion >= ProtocolConstants.ProtocolVersion ? "protocolV2Connections" : "legacyConnections"]++; becameHost = _worldStateHostPlayerId is null; if (becameHost) _worldStateHostPlayerId = client.PlayerId; } Log($"Client connected: {client.Label} (player {client.PlayerId}, protocol {protocolVersion})"); var ready = new JsonObject { ["type"] = "sessionReady", ["playerId"] = client.PlayerId, ["protocolVersion"] = protocolVersion, ["serverProtocolVersion"] = ProtocolConstants.ProtocolVersion, ["worldStateHostPlayerId"] = _worldStateHostPlayerId, ["serverTime"] = JsonHelpers.UnixTime() }; if (!await SendPacketAsync(client, ready, false, cancellationToken).ConfigureAwait(false)) return false; await SendExistingTransformsAsync(client, cancellationToken).ConfigureAwait(false); await SendExistingPlayerStatesAsync(client, cancellationToken).ConfigureAwait(false); await SendExistingNpcStateAsync(client, cancellationToken).ConfigureAwait(false); if (becameHost) await BroadcastWorldStateHostAsync(client.PlayerId, cancellationToken).ConfigureAwait(false); return true; } private async Task HandleTransformAsync(ClientSession client, JsonObject packet, CancellationToken cancellationToken) { var normalized = ProtocolValidation.NormalizeTransform(packet); if (normalized is null) { Reject(client, "Malformed transform"); return; } if (client.ProtocolVersion >= ProtocolConstants.ProtocolVersion) foreach (var field in new[] { "equippedItems", "appearance", "actionEvents", "characterName" }) normalized.Remove(field); var previous = client.TransformAnchor(); var acceptedMonotonic = MonotonicClock.Now; var movement = ProtocolValidation.ValidateMovement(previous.Transform, previous.Monotonic, normalized, acceptedMonotonic); if (!movement.Accepted) { Increment("movementPacketsRejected"); Reject(client, movement.Reason); await SendPositionCorrectionAsync(client, movement.Reason, cancellationToken).ConfigureAwait(false); return; } var previousScope = AuthorityScopeFromTransform(previous.Transform); normalized["playerId"] = client.PlayerId; normalized["serverTime"] = JsonHelpers.UnixTime(); client.RecordTransform(normalized, acceptedMonotonic); Increment("transformPacketsReceived"); await BroadcastTransformAsync(client, normalized, cancellationToken).ConfigureAwait(false); await ReconcileNpcAuthorityAsync(cancellationToken).ConfigureAwait(false); var currentScope = AuthorityScopeFromTransform(normalized); if (currentScope is not null && currentScope != previousScope) await SendNpcAuthorityForClientAsync(client, currentScope.Value, cancellationToken).ConfigureAwait(false); } private async Task HandlePlayerStateAsync(ClientSession client, JsonObject packet, CancellationToken cancellationToken) { if (client.ProtocolVersion < ProtocolConstants.ProtocolVersion) { Reject(client, "playerState requires Protocol V2", false); return; } var normalized = ProtocolValidation.NormalizePlayerState(packet); if (normalized is null) { Reject(client, "Malformed playerState"); return; } normalized["playerId"] = client.PlayerId; normalized["serverTime"] = JsonHelpers.UnixTime(); var durable = new[] { "equippedItems", "appearance", "characterName" }; lock (_gate) { if (durable.Any(normalized.ContainsKey)) { var cached = _lastPlayerStateByPlayerId.TryGetValue(client.PlayerId, out var existing) ? JsonHelpers.CloneObject(existing) : new JsonObject(); cached["type"] = "playerState"; cached["playerId"] = client.PlayerId; cached["serverTime"] = normalized["serverTime"]?.DeepClone(); foreach (var field in durable) if (normalized.ContainsKey(field)) cached[field] = normalized[field]?.DeepClone(); cached.Remove("actionEvents"); _lastPlayerStateByPlayerId[client.PlayerId] = cached; } _stats["playerStatePacketsReceived"]++; } await BroadcastPlayerStateAsync(client, normalized, cancellationToken).ConfigureAwait(false); } private async Task HandleWorldStateAsync(ClientSession client, JsonObject packet, CancellationToken cancellationToken) { lock (_gate) if (_worldStateHostPlayerId != client.PlayerId) { RejectLocked(client, "worldState from non-authority client", false); return; } var normalized = ProtocolValidation.NormalizeWorldState(packet); if (normalized is null) { Reject(client, "Malformed worldState"); return; } normalized["playerId"] = client.PlayerId; normalized["serverTime"] = JsonHelpers.UnixTime(); Increment("worldStatePacketsReceived"); await BroadcastWorldStateAsync(client, normalized, cancellationToken).ConfigureAwait(false); } private async Task HandleNpcStateAsync(ClientSession client, JsonObject packet, CancellationToken cancellationToken) { var normalized = ProtocolValidation.NormalizeNpcState(packet); if (normalized is null) { Reject(client, "Malformed npcState"); return; } if (client.ProtocolVersion >= ProtocolConstants.ProtocolVersion) { var scope = AuthorityScopeFromPacket(normalized); if (scope is null || !JsonHelpers.TryUInt32(normalized["authorityEpoch"], 1, uint.MaxValue, out var epoch)) { Increment("npcAuthorityRejects"); Reject(client, "Protocol V2 npcState missing valid authority scope/epoch"); return; } lock (_gate) { if (!_npcAuthority.Authorize(client.PlayerId, scope.Value, epoch)) { _stats["npcAuthorityRejects"]++; RejectLocked(client, "Stale or unauthorized npcState authority epoch"); return; } } var npcs = (JsonArray)normalized["npcs"]!; foreach (var node in npcs) { var npc = (JsonObject)node!; if (JsonHelpers.String(npc["cellId"]) != scope.Value.CellId || (JsonHelpers.String(npc["worldspaceId"]) ?? string.Empty) != scope.Value.WorldspaceId) { Increment("npcAuthorityRejects"); Reject(client, "npcState contains NPCs outside declared authority scope"); return; } } normalized["authorityCellId"] = scope.Value.CellId; normalized["authorityWorldspaceId"] = scope.Value.WorldspaceId; lock (_gate) _lastNpcStateByScope[scope.Value] = JsonHelpers.CloneObject(normalized); } else { lock (_gate) if (_worldStateHostPlayerId != client.PlayerId) { RejectLocked(client, "Legacy npcState from non-authority client", false); return; } lock (_gate) _lastLegacyNpcState = JsonHelpers.CloneObject(normalized); } normalized["playerId"] = client.PlayerId; normalized["serverTime"] = JsonHelpers.UnixTime(); normalized["fullReplace"] = true; Increment("npcStatePacketsReceived"); await BroadcastNpcStateAsync(client, normalized, cancellationToken).ConfigureAwait(false); } private async Task HandleCombatHitAsync(ClientSession client, JsonObject packet, CancellationToken cancellationToken) { var normalized = ProtocolValidation.NormalizeCombatHit(packet); if (normalized is null) { Reject(client, "Malformed combatHit"); return; } JsonHelpers.TryUInt32(normalized["sequence"], 1, uint.MaxValue, out var sequence); if (sequence <= client.LastCombatSequence) { Reject(client, "Duplicate or out-of-order combat sequence", false); return; } client.LastCombatSequence = sequence; normalized["playerId"] = client.PlayerId; normalized["serverTime"] = JsonHelpers.UnixTime(); Increment("combatHitsReceived"); await RouteCombatHitAsync(client, normalized, cancellationToken).ConfigureAwait(false); } private async Task AllowPacketAsync(ClientSession client) { var now = MonotonicClock.Now; if (now - client.RateWindowStart >= 1.0) { if (client.RateWindowCount <= MaxPacketsPerSecond) client.RateViolations = Math.Max(0, client.RateViolations - 1); client.RateWindowStart = now; client.RateWindowCount = 0; client.RateWindowBlocked = false; } client.RateWindowCount++; if (client.RateWindowCount <= MaxPacketsPerSecond) return true; Increment("rateLimitedPackets"); if (!client.RateWindowBlocked) { client.RateWindowBlocked = true; client.RateViolations++; Log($"Packet-rate limit exceeded by player {client.PlayerId} ({client.RateViolations}/3 windows)", "warning"); } if (client.RateViolations >= 3) await EndSessionAsync(client, "rate_limited", "Sustained packet-rate limit exceeded.").ConfigureAwait(false); return false; } private bool AllowConnectAttempt(string ip) { var now = MonotonicClock.Now; var cutoff = now - ConnectAttemptWindowSeconds; lock (_gate) { if (!_connectAttempts.TryGetValue(ip, out var queue)) _connectAttempts[ip] = queue = new Queue(); while (queue.Count > 0 && queue.Peek() < cutoff) queue.Dequeue(); queue.Enqueue(now); return queue.Count <= MaxConnectAttempts; } } private async Task SendPositionCorrectionAsync(ClientSession client, string reason, CancellationToken cancellationToken) { var previous = client.TransformAnchor().Transform; if (previous is null) return; var packet = new JsonObject { ["type"] = "positionCorrection", ["reason"] = reason[..Math.Min(reason.Length, 160)], ["x"] = previous["x"]?.DeepClone(), ["y"] = previous["y"]?.DeepClone(), ["z"] = previous["z"]?.DeepClone(), ["angleZ"] = previous["angleZ"]?.DeepClone(), ["cellId"] = previous["cellId"]?.DeepClone(), ["worldspaceId"] = previous["worldspaceId"]?.DeepClone(), ["serverTime"] = JsonHelpers.UnixTime() }; if (await SendPacketAsync(client, packet, false, cancellationToken).ConfigureAwait(false)) Increment("movementCorrectionsSent"); else await DisconnectClientAsync(client).ConfigureAwait(false); } private async Task SendExistingTransformsAsync(ClientSession target, CancellationToken cancellationToken) { var targetTransform = target.TransformAnchor().Transform; ClientSession[] peers; lock (_gate) peers = _clients.Values.Where(x => x.GameplayActive && x.Connection.ConnectionKey != target.Connection.ConnectionKey && x.LastTransform is not null).ToArray(); var sent = 0; foreach (var peer in peers) { var transform = peer.TransformAnchor().Transform; if (!ProtocolValidation.StatesShareInterest(transform, targetTransform)) continue; if (transform is null) continue; transform["serverTime"] = JsonHelpers.UnixTime(); if (!await SendPacketAsync(target, transform, true, cancellationToken).ConfigureAwait(false)) break; sent++; } Add("transformPacketsBroadcast", sent); } private async Task SendExistingPlayerStatesAsync(ClientSession target, CancellationToken cancellationToken) { if (target.ProtocolVersion < ProtocolConstants.ProtocolVersion) return; JsonObject[] packets; lock (_gate) packets = _lastPlayerStateByPlayerId.Where(x => x.Key != target.PlayerId).Select(x => JsonHelpers.CloneObject(x.Value)).ToArray(); var sent = 0; foreach (var packet in packets) { packet["serverTime"] = JsonHelpers.UnixTime(); if (!await SendPacketAsync(target, packet, true, cancellationToken).ConfigureAwait(false)) break; sent++; } Add("playerStatePacketsBroadcast", sent); } private async Task BroadcastTransformAsync(ClientSession sender, JsonObject packet, CancellationToken cancellationToken) { ClientSession[] recipients; lock (_gate) recipients = _clients.Values.Where(x => x.GameplayActive && x.Connection.ConnectionKey != sender.Connection.ConnectionKey).ToArray(); var sent = 0; var filtered = 0; foreach (var recipient in recipients) { if (!ProtocolValidation.StatesShareInterest(packet, recipient.TransformAnchor().Transform)) { filtered++; continue; } if (await SendPacketAsync(recipient, packet, true, cancellationToken).ConfigureAwait(false)) sent++; else await DisconnectClientAsync(recipient).ConfigureAwait(false); } Add("transformPacketsBroadcast", sent); Add("transformPacketsInterestFiltered", filtered); } private async Task BroadcastPlayerStateAsync(ClientSession sender, JsonObject packet, CancellationToken cancellationToken) { ClientSession[] recipients; lock (_gate) recipients = _clients.Values.Where(x => x.GameplayActive && x.Connection.ConnectionKey != sender.Connection.ConnectionKey && x.ProtocolVersion >= ProtocolConstants.ProtocolVersion).ToArray(); var durable = new[] { "equippedItems", "appearance", "characterName" }; var senderTransform = sender.TransformAnchor().Transform; var senderScopeKnown = ProtocolValidation.ScopeFromState(senderTransform) is not null; var sent = 0; foreach (var recipient in recipients) { var relay = JsonHelpers.CloneObject(packet); if (relay.ContainsKey("actionEvents")) { var targetTransform = recipient.TransformAnchor().Transform; var targetScopeKnown = ProtocolValidation.ScopeFromState(targetTransform) is not null; if (!senderScopeKnown || !targetScopeKnown || !ProtocolValidation.StatesShareInterest(senderTransform, targetTransform)) relay.Remove("actionEvents"); } if (!relay.ContainsKey("actionEvents") && !durable.Any(relay.ContainsKey)) continue; if (await SendPacketAsync(recipient, relay, true, cancellationToken).ConfigureAwait(false)) sent++; else await DisconnectClientAsync(recipient).ConfigureAwait(false); } Add("playerStatePacketsBroadcast", sent); } private async Task BroadcastWorldStateAsync(ClientSession sender, JsonObject packet, CancellationToken cancellationToken) { var sent = await BroadcastAsync(packet, sender, false, cancellationToken).ConfigureAwait(false); Add("worldStatePacketsBroadcast", sent); } private JsonObject NpcPacketForRecipient(JsonObject packet, ClientSession recipient) { var target = recipient.TransformAnchor().Transform; if (target is null) return JsonHelpers.CloneObject(packet); var clean = JsonHelpers.CloneObject(packet); var output = new JsonArray(); if (packet["npcs"] is JsonArray npcs) foreach (var node in npcs) if (node is JsonObject npc && ProtocolValidation.StatesShareInterest(npc, target)) output.Add(npc.DeepClone()); clean["npcs"] = output; return clean; } private async Task BroadcastNpcStateAsync(ClientSession sender, JsonObject packet, CancellationToken cancellationToken) { ClientSession[] recipients; lock (_gate) recipients = _clients.Values.Where(x => x.GameplayActive && x.Connection.ConnectionKey != sender.Connection.ConnectionKey).ToArray(); var sent = 0; foreach (var recipient in recipients) { if (await SendPacketAsync(recipient, NpcPacketForRecipient(packet, recipient), true, cancellationToken).ConfigureAwait(false)) sent++; else await DisconnectClientAsync(recipient).ConfigureAwait(false); } Add("npcStatePacketsBroadcast", sent); } private async Task SendExistingNpcStateAsync(ClientSession client, CancellationToken cancellationToken) { JsonObject? snapshot; if (client.ProtocolVersion >= ProtocolConstants.ProtocolVersion) { var scope = AuthorityScopeFromTransform(client.TransformAnchor().Transform); lock (_gate) snapshot = scope is not null && _lastNpcStateByScope.TryGetValue(scope.Value, out var stored) ? JsonHelpers.CloneObject(stored) : null; } else lock (_gate) snapshot = _lastLegacyNpcState is null ? null : JsonHelpers.CloneObject(_lastLegacyNpcState); if (snapshot is null || (JsonHelpers.TryUInt32(snapshot["playerId"], 0, uint.MaxValue, out var owner) && owner == client.PlayerId)) return; var scoped = NpcPacketForRecipient(snapshot, client); scoped["serverTime"] = JsonHelpers.UnixTime(); if (await SendPacketAsync(client, scoped, true, cancellationToken).ConfigureAwait(false)) Increment("npcStatePacketsBroadcast"); } private async Task RouteCombatHitAsync(ClientSession sender, JsonObject packet, CancellationToken cancellationToken) { JsonHelpers.TryUInt32(packet["targetPlayerId"], 1, uint.MaxValue, out var targetId); if (targetId == sender.PlayerId) { Reject(sender, "Self-targeted combatHit", false); return; } ClientSession? recipient; lock (_gate) recipient = _clients.Values.FirstOrDefault(x => x.GameplayActive && x.PlayerId == targetId); if (recipient is null) { Reject(sender, $"Combat target {targetId} is not connected", false); return; } if (!ProtocolValidation.StatesShareInterest(sender.TransformAnchor().Transform, recipient.TransformAnchor().Transform)) { Reject(sender, $"Combat target {targetId} is outside interest scope", true); return; } if (await SendPacketAsync(recipient, packet, false, cancellationToken).ConfigureAwait(false)) Increment("combatHitsRouted"); else await DisconnectClientAsync(recipient).ConfigureAwait(false); } private async Task ReconcileNpcAuthorityAsync(CancellationToken cancellationToken) { IReadOnlyList changes; lock (_gate) { var players = _clients.Values.Where(x => x.GameplayActive && x.ProtocolVersion >= ProtocolConstants.ProtocolVersion) .Select(x => (x.PlayerId, Scope: AuthorityScopeFromTransform(x.TransformAnchor().Transform))) .Where(x => x.Scope is not null).Select(x => (x.PlayerId, x.Scope!.Value)); changes = _npcAuthority.Reconcile(players); foreach (var change in changes) _lastNpcStateByScope.Remove(change.Scope); } foreach (var change in changes) { var packet = AuthorityPacket(change.PlayerId, change.Epoch, change.Scope); await BroadcastAsync(packet, null, true, cancellationToken).ConfigureAwait(false); Increment("npcAuthorityChanges"); Log($"NPC authority scope {change.Scope.CellId}/{(string.IsNullOrEmpty(change.Scope.WorldspaceId) ? "" : change.Scope.WorldspaceId)}: {change.PreviousPlayerId} -> {change.PlayerId}, epoch {change.Epoch}"); } } private async Task SendNpcAuthorityForClientAsync(ClientSession client, ScopeKey scope, CancellationToken cancellationToken) { if (client.ProtocolVersion < ProtocolConstants.ProtocolVersion) return; AuthorityAssignment? assignment; lock (_gate) assignment = _npcAuthority.Get(scope); if (assignment is null) return; if (!await SendPacketAsync(client, AuthorityPacket(assignment.Value.PlayerId, assignment.Value.Epoch, scope), false, cancellationToken).ConfigureAwait(false)) await DisconnectClientAsync(client).ConfigureAwait(false); } private static JsonObject AuthorityPacket(uint playerId, uint epoch, ScopeKey scope) => new() { ["type"] = "npcAuthority", ["authorityPlayerId"] = playerId, ["authorityEpoch"] = epoch, ["authorityCellId"] = scope.CellId, ["authorityWorldspaceId"] = scope.WorldspaceId, ["serverTime"] = JsonHelpers.UnixTime() }; private static ScopeKey? AuthorityScopeFromTransform(JsonObject? transform) { if (transform is null) return null; var cell = JsonHelpers.String(transform["cellId"]); var world = JsonHelpers.String(transform["worldspaceId"]) ?? string.Empty; return cell is null || !JsonHelpers.IsHexFormId(cell, false, false) || !JsonHelpers.IsHexFormId(world, true, true) ? null : new ScopeKey(JsonHelpers.NormalizeFormId(cell), world.Length == 0 ? string.Empty : JsonHelpers.NormalizeFormId(world)); } private static ScopeKey? AuthorityScopeFromPacket(JsonObject packet) { var cell = JsonHelpers.String(packet["authorityCellId"]); var world = JsonHelpers.String(packet["authorityWorldspaceId"]) ?? string.Empty; return cell is null || !JsonHelpers.IsHexFormId(cell, false, false) || !JsonHelpers.IsHexFormId(world, true, true) ? null : new ScopeKey(JsonHelpers.NormalizeFormId(cell), world.Length == 0 ? string.Empty : JsonHelpers.NormalizeFormId(world)); } private async Task BroadcastAsync(JsonObject packet, ClientSession? exclude, bool v2Only, CancellationToken cancellationToken) { ClientSession[] recipients; lock (_gate) recipients = _clients.Values.Where(x => x.GameplayActive && (exclude is null || x.Connection.ConnectionKey != exclude.Connection.ConnectionKey) && (!v2Only || x.ProtocolVersion >= ProtocolConstants.ProtocolVersion)).ToArray(); var sent = 0; foreach (var recipient in recipients) { if (await SendPacketAsync(recipient, packet, true, cancellationToken).ConfigureAwait(false)) sent++; else await DisconnectClientAsync(recipient).ConfigureAwait(false); } return sent; } private async Task BroadcastWorldStateHostAsync(uint playerId, CancellationToken cancellationToken) { var sent = await BroadcastAsync(new JsonObject { ["type"] = "worldStateHost", ["worldStateHostPlayerId"] = playerId, ["serverTime"] = JsonHelpers.UnixTime() }, null, false, cancellationToken).ConfigureAwait(false); Add("worldStateHostPacketsBroadcast", sent); } private async Task BroadcastDisconnectAsync(ClientSession client) { var sent = await BroadcastAsync(new JsonObject { ["type"] = "disconnect", ["playerId"] = client.PlayerId, ["serverTime"] = JsonHelpers.UnixTime() }, null, false, CancellationToken.None).ConfigureAwait(false); Add("disconnectPacketsBroadcast", sent); } private async Task SendPacketAsync(ClientSession client, JsonObject packet, bool broadcast, CancellationToken cancellationToken) { EncodedPacket encoded; try { encoded = PacketCodec.Encode(packet); } catch (PacketCodecException) { return false; } var result = await client.Connection.SendAsync(encoded, cancellationToken).ConfigureAwait(false); var success = result == SendOutcome.Sent || (encoded.Delivery == Delivery.UnreliableSequenced && result is SendOutcome.Dropped or SendOutcome.Backpressure); if (!success) return false; client.RecordSent(broadcast); lock (_gate) { _stats["packetsSent"]++; if (broadcast) _stats["packetsBroadcast"]++; } return true; } private async Task SendDirectSessionEndedAsync(IGameConnection connection, string code, string reason, CancellationToken cancellationToken) { var packet = new JsonObject { ["type"] = "sessionEnded", ["code"] = code, ["reason"] = reason ?? string.Empty, ["serverTime"] = JsonHelpers.UnixTime() }; try { var result = await connection.SendAsync(PacketCodec.Encode(packet), cancellationToken).ConfigureAwait(false); if (result == SendOutcome.Sent) lock (_gate) { _stats["sessionEndedPacketsSent"]++; _stats["packetsSent"]++; } } catch { } } private async Task EndSessionAsync(ClientSession client, string code, string reason) { var packet = new JsonObject { ["type"] = "sessionEnded", ["code"] = code, ["reason"] = reason ?? string.Empty, ["serverTime"] = JsonHelpers.UnixTime() }; if (await SendPacketAsync(client, packet, false, CancellationToken.None).ConfigureAwait(false)) Increment("sessionEndedPacketsSent"); await DisconnectClientAsync(client).ConfigureAwait(false); } private async Task DisconnectClientAsync(ClientSession client) { bool removed; bool wasActive; bool wasHost; lock (_gate) { wasActive = client.GameplayActive; wasHost = wasActive && _worldStateHostPlayerId == client.PlayerId; removed = _clients.Remove(client.Connection.ConnectionKey); if (removed) _lastPlayerStateByPlayerId.Remove(client.PlayerId); if (removed && wasActive) _stats["clientsDisconnected"]++; } if (!removed) return; await client.Connection.DisconnectAsync(0, "Commonwealth Online disconnect"); if (!wasActive) { Log($"Closed pending/probe connection: {client.Label}", "debug"); return; } Log($"Client disconnected: {client.Label} (player {client.PlayerId})"); await BroadcastDisconnectAsync(client).ConfigureAwait(false); await ReconcileNpcAuthorityAsync(CancellationToken.None).ConfigureAwait(false); if (wasHost) await ReassignWorldStateHostAsync().ConfigureAwait(false); } private async Task ReassignWorldStateHostAsync() { uint? newHost; lock (_gate) { var active = _clients.Values.Where(x => x.GameplayActive).ToArray(); if (active.Length == 0) { _worldStateHostPlayerId = null; _lastLegacyNpcState = null; return; } newHost = active.Min(x => x.PlayerId); _worldStateHostPlayerId = newHost; _lastLegacyNpcState = null; } await BroadcastWorldStateHostAsync(newHost!.Value, CancellationToken.None).ConfigureAwait(false); Log($"Reassigned world-state host to player {newHost.Value}."); } private void Reject(ClientSession client, string reason, bool warning = true) { lock (_gate) RejectLocked(client, reason, warning); } private void RejectLocked(ClientSession client, string reason, bool warning = true) { _stats["packetsRejected"]++; Log($"Rejected packet from {client.Label} (player {client.PlayerId}): {reason}", warning ? "warning" : "debug"); } private ClientSession? FindByConnection(IGameConnection connection) { lock (_gate) return _clients.TryGetValue(connection.ConnectionKey, out var client) ? client : null; } private void Increment(string name) { lock (_gate) _stats[name]++; } private void Add(string name, long amount) { lock (_gate) _stats[name] += amount; } private async Task MaintenanceLoopAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { try { await Task.Delay(500, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { break; } ClientSession[] snapshot; lock (_gate) snapshot = _clients.Values.ToArray(); var now = JsonHelpers.UnixTime(); foreach (var client in snapshot) { if (!client.GameplayActive && now - client.ConnectedAt > ClientHandshakeTimeoutSeconds) { Log($"Handshake timeout: {client.Label}", "warning"); await DisconnectClientAsync(client).ConfigureAwait(false); } else if (client.GameplayActive && client.LastPacketAt is { } last && now - last > ClientIdleTimeoutSeconds) { Log($"Idle timeout: player {client.PlayerId}", "warning"); await DisconnectClientAsync(client).ConfigureAwait(false); } } } } public JsonObject GetCoreStats() { lock (_gate) { var active = _clients.Values.Count(x => x.GameplayActive); var stats = new JsonObject(); foreach (var pair in _stats) stats[pair.Key] = pair.Value; stats["host"] = _options.Host; stats["port"] = _options.Port; stats["serverName"] = _options.ServerName; stats["serverDescription"] = _options.ServerDescription; stats["maxPlayers"] = _options.MaxPlayers; stats["isRunning"] = true; stats["startedAt"] = _startedAt; stats["uptimeSeconds"] = JsonHelpers.UnixTime() - _startedAt; stats["connectedClients"] = active; stats["pendingConnections"] = _clients.Count - active; stats["nextPlayerId"] = _nextPlayerId; stats["protocolVersion"] = ProtocolConstants.ProtocolVersion; stats["npcAuthorityScopes"] = _npcAuthority.Assignments.Count; return stats; } } public JsonObject GetAdminStats() { var core = GetCoreStats(); return new JsonObject { ["is_running"] = true, ["host"] = _options.Host, ["port"] = _options.Port.ToString(), ["server_name"] = _options.ServerName, ["server_description"] = _options.ServerDescription, ["uptime_seconds"] = core["uptimeSeconds"]?.DeepClone(), ["connected_clients"] = core["connectedClients"]?.DeepClone(), ["packets_received"] = core["packetsReceived"]?.DeepClone(), ["packets_sent"] = core["packetsSent"]?.DeepClone(), ["transform_packets_received"] = core["transformPacketsReceived"]?.DeepClone(), ["transform_packets_broadcast"] = core["transformPacketsBroadcast"]?.DeepClone(), ["world_state_packets_received"] = core["worldStatePacketsReceived"]?.DeepClone(), ["world_state_packets_broadcast"] = core["worldStatePacketsBroadcast"]?.DeepClone() }; } public JsonArray GetClientSnapshots() { lock (_gate) return new JsonArray(_clients.Values.Where(x => x.GameplayActive).Select(x => (JsonNode)x.Snapshot()).ToArray()); } public JsonArray GetAdminClients() { lock (_gate) { return new JsonArray(_clients.Values.Where(x => x.GameplayActive).Select(client => { var snapshot = client.Snapshot(); var endpoint = client.Label; return (JsonNode)new JsonObject { ["player_id"] = client.PlayerId, ["address"] = endpoint, ["label"] = endpoint, ["connected_at"] = client.ConnectedAt, ["packets_sent"] = client.PacketsSent, ["packets_received"] = client.PacketsReceived, ["last_transform"] = snapshot["lastTransform"]?.DeepClone() }; }).ToArray()); } } public IReadOnlyList ListBans() => _banStore.List(); public async Task<(bool Ok, string Message, JsonObject? Data)> KickAsync(uint playerId, string reason) { ClientSession? client; lock (_gate) client = _clients.Values.FirstOrDefault(x => x.GameplayActive && x.PlayerId == playerId); if (client is null) return (false, $"No connected player with id {playerId}", null); var data = new JsonObject { ["playerId"] = playerId, ["ip"] = client.RemoteEndpoint.Address.ToString(), ["code"] = "kicked", ["reason"] = reason ?? string.Empty }; await EndSessionAsync(client, "kicked", reason ?? string.Empty).ConfigureAwait(false); return (true, $"Kicked player {playerId}.", data); } public async Task<(bool Ok, string Message, JsonObject? Data)> BanPlayerAsync(uint playerId, string reason) { ClientSession? client; lock (_gate) client = _clients.Values.FirstOrDefault(x => x.GameplayActive && x.PlayerId == playerId); return client is null ? (false, $"No connected player with id {playerId}", null) : await BanIpAsync(client.RemoteEndpoint.Address.ToString(), reason).ConfigureAwait(false); } public async Task<(bool Ok, string Message, JsonObject? Data)> BanIpAsync(string ip, string reason) { BanEntry entry; try { entry = _banStore.Ban(ip, reason ?? string.Empty); } catch (ArgumentException ex) { return (false, ex.Message, null); } ClientSession[] sessions; lock (_gate) sessions = _clients.Values.Where(x => x.RemoteEndpoint.Address.ToString() == entry.Ip).ToArray(); foreach (var session in sessions) await EndSessionAsync(session, "banned", entry.Reason).ConfigureAwait(false); var data = new JsonObject { ["ip"] = entry.Ip, ["reason"] = entry.Reason, ["bannedAt"] = entry.BannedAt, ["sessionsEnded"] = sessions.Length }; Log($"Banned IP {entry.Ip}{(string.IsNullOrEmpty(entry.Reason) ? string.Empty : $" (reason: {entry.Reason})")}"); return (true, $"Banned IP {entry.Ip}.", data); } public (bool Ok, string Message) Unban(string ip) { var removed = _banStore.Unban(ip); if (removed) Log($"Unbanned IP {ip}"); return removed ? (true, $"Unbanned IP {ip}.") : (false, $"IP {ip} is not banned."); } public async Task SetServerTimeAsync(string hhmm) { if (WorldStatePresets.HhmmToGameHour(hhmm) is null) return false; var normalized = hhmm.Trim().PadLeft(4, '0'); lock (_gate) _serverWorldState["timeHHmm"] = normalized; await BroadcastServerWorldStateAsync().ConfigureAwait(false); return true; } public async Task SetServerWeatherAsync(string value) { string normalized; try { normalized = WorldStatePresets.NormalizeWeatherConsoleArg(value); } catch { return false; } lock (_gate) { _serverWorldState["weatherConsoleArg"] = normalized; _serverWorldState["weatherFormId"] = WorldStatePresets.RelayWeatherFormId(normalized); } await BroadcastServerWorldStateAsync().ConfigureAwait(false); return true; } private async Task BroadcastServerWorldStateAsync() { Dictionary snapshot; lock (_gate) snapshot = new Dictionary(_serverWorldState, StringComparer.Ordinal); var packets = new List(); if (snapshot.TryGetValue("timeHHmm", out var time)) packets.Add(new JsonObject { ["type"] = "serverWorldState", ["timeHHmm"] = time, ["serverTime"] = JsonHelpers.UnixTime() }); if (snapshot.TryGetValue("weatherConsoleArg", out var weather)) { var packet = new JsonObject { ["type"] = "serverWorldState", ["weatherConsoleArg"] = weather, ["serverTime"] = JsonHelpers.UnixTime() }; if (snapshot.TryGetValue("weatherFormId", out var form)) packet["weatherFormId"] = form; packets.Add(packet); } var sent = 0; foreach (var packet in packets) sent += await BroadcastAsync(packet, null, false, CancellationToken.None).ConfigureAwait(false); Add("serverWorldStatePacketsBroadcast", sent); } public async ValueTask DisposeAsync() { _shutdown.Cancel(); try { await _maintenanceTask.ConfigureAwait(false); } catch { } ClientSession[] clients; lock (_gate) { clients = _clients.Values.ToArray(); _clients.Clear(); } foreach (var client in clients) await client.Connection.DisposeAsync(); _shutdown.Dispose(); } }