using System.Buffers.Binary; using System.Globalization; using System.Text.Json; using System.Text.Json.Nodes; namespace CommonwealthOnline.Server; internal static class ProtocolConstants { public const int ProtocolVersion = 2; public const int LegacyProtocolVersion = 1; public const int MaxMessageBytes = 64 * 1024; public const double MaxAbsCoordinate = 10_000_000.0; public const double MaxMovementSpeed = 100_000.0; public const int MaxActionEvents = 16; public const int MaxNpcsPerPacket = 64; public const double ExteriorInterestRadius = 8192.0; public const double MaxNormalMovementSpeed = 2500.0; public const double MovementGraceDistance = 512.0; public const double MaxMovementValidationElapsedSeconds = 5.0; public static readonly HashSet MovementTransitionTypes = new(StringComparer.Ordinal) { "teleport", "cell_change", "worldspace_change", "load", "spawn", "fast_travel" }; public static readonly HashSet AllowedMovementTypes = new(MovementTransitionTypes, StringComparer.Ordinal) { "normal" }; } internal enum Delivery : uint { UnreliableSequenced = 0, ReliableOrdered = 1 } internal readonly record struct EncodedPacket(string PacketType, byte[] Payload, Delivery Delivery); internal static class TransportPolicy { public static Delivery ForPacketType(string packetType) => packetType is "transform" or "npcState" ? Delivery.UnreliableSequenced : Delivery.ReliableOrdered; public static bool IsSnapshot(string packetType) => ForPacketType(packetType) == Delivery.UnreliableSequenced; } internal sealed class PacketCodecException(string message) : Exception(message); internal static class PacketCodec { private static readonly JsonSerializerOptions Compact = new() { WriteIndented = false }; public static EncodedPacket Encode(JsonObject packet) { var type = JsonHelpers.String(packet["type"]); if (string.IsNullOrEmpty(type)) throw new PacketCodecException("packet type must be a non-empty string"); byte[] payload; try { payload = JsonSerializer.SerializeToUtf8Bytes(packet, Compact); } catch (Exception ex) when (ex is JsonException or NotSupportedException) { throw new PacketCodecException($"packet is not JSON serializable: {ex.Message}"); } if (payload.Length > ProtocolConstants.MaxMessageBytes) throw new PacketCodecException("packet exceeds maximum message size"); return new EncodedPacket(type, payload, TransportPolicy.ForPacketType(type)); } public static JsonObject Decode(ReadOnlySpan payload) { if (payload.Length > ProtocolConstants.MaxMessageBytes) throw new PacketCodecException("packet exceeds maximum message size"); JsonNode? node; try { node = JsonNode.Parse(payload, documentOptions: new JsonDocumentOptions { AllowTrailingCommas = false, CommentHandling = JsonCommentHandling.Disallow }); } catch (JsonException ex) { throw new PacketCodecException($"invalid JSON packet: {ex.Message}"); } if (node is not JsonObject packet) throw new PacketCodecException("packet must be a JSON object"); var type = JsonHelpers.String(packet["type"]); if (string.IsNullOrEmpty(type)) throw new PacketCodecException("packet type must be a non-empty string"); return packet; } } internal static class JsonHelpers { public static string? String(JsonNode? node) => node is JsonValue value && value.TryGetValue(out var text) ? text : null; public static bool? Boolean(JsonNode? node) => node is JsonValue value && value.TryGetValue(out var result) ? result : null; public static bool TryUInt32(JsonNode? node, uint min, uint max, out uint result) { result = 0; if (node is not JsonValue value || value.TryGetValue(out _)) return false; if (value.TryGetValue(out var u) && u >= min && u <= max) { result = u; return true; } if (value.TryGetValue(out var l) && l >= min && l <= max) { result = (uint)l; return true; } if (value.TryGetValue(out var d) && double.IsFinite(d) && d == Math.Truncate(d) && d >= min && d <= max) { result = (uint)d; return true; } return false; } public static bool TryDouble(JsonNode? node, double min, double max, out double result) { result = 0; if (node is not JsonValue value || value.TryGetValue(out _)) return false; double d; if (value.TryGetValue(out var direct)) d = direct; else if (value.TryGetValue(out var l)) d = l; else if (value.TryGetValue(out var m)) d = (double)m; else return false; if (!double.IsFinite(d) || d < min || d > max) return false; result = d; return true; } public static bool IsHexFormId(JsonNode? node, bool allowEmpty = false, bool allowZero = true) => String(node) is { } text && IsHexFormId(text, allowEmpty, allowZero); public static bool IsHexFormId(string text, bool allowEmpty = false, bool allowZero = true) { if (allowEmpty && text.Length == 0) return true; if (text.Length is < 1 or > 8) return false; if (!uint.TryParse(text, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out var parsed)) return false; return allowZero || parsed != 0; } public static string NormalizeFormId(string text) => uint.Parse(text, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture).ToString("X8", CultureInfo.InvariantCulture); public static JsonObject CloneObject(JsonObject source) => (JsonObject)source.DeepClone(); public static double UnixTime() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() / 1000.0; } internal sealed class SequenceCounter { private uint _value; public uint Current => _value; public uint Advance() { unchecked { _value++; } if (_value == 0) _value = 1; return _value; } public void Reset() => _value = 0; } internal sealed class SequenceWindow { private uint _lastAccepted; public uint LastAccepted => _lastAccepted; public bool Accept(uint candidate) { if (!IsNewer(candidate, _lastAccepted)) return false; _lastAccepted = candidate; return true; } public void Reset() => _lastAccepted = 0; public static bool IsNewer(uint candidate, uint baseline) { if (candidate == 0) return false; if (baseline == 0) return true; var delta = unchecked(candidate - baseline); return delta > 0 && delta < 0x80000000u; } } internal readonly record struct SnapshotEnvelope(string PacketType, uint Sequence, byte[] Payload); internal static class GnsSnapshotEnvelope { private static ReadOnlySpan Magic => "COG2"u8; private const byte Version = 1; public const int HeaderSize = 12; public static byte[] Encode(string packetType, ReadOnlySpan payload, uint sequence) { var family = packetType switch { "transform" => (byte)1, "npcState" => (byte)2, _ => throw new ArgumentException($"packet type '{packetType}' is not a GNS snapshot family", nameof(packetType)) }; if (sequence == 0) throw new ArgumentOutOfRangeException(nameof(sequence)); if (payload.IsEmpty) throw new ArgumentException("snapshot payload cannot be empty", nameof(payload)); if (HeaderSize + payload.Length > ProtocolConstants.MaxMessageBytes) throw new ArgumentException("snapshot envelope exceeds maximum GNS message size"); var output = new byte[HeaderSize + payload.Length]; Magic.CopyTo(output); output[4] = Version; output[5] = family; BinaryPrimitives.WriteUInt16BigEndian(output.AsSpan(6, 2), 0); BinaryPrimitives.WriteUInt32BigEndian(output.AsSpan(8, 4), sequence); payload.CopyTo(output.AsSpan(HeaderSize)); return output; } public static bool TryDecode(ReadOnlySpan message, out SnapshotEnvelope envelope, out string? error) { envelope = default; error = null; if (message.Length < 4 || !message[..4].SequenceEqual(Magic)) return false; if (message.Length < HeaderSize) { error = "truncated GNS snapshot envelope"; return true; } if (message[4] != Version || BinaryPrimitives.ReadUInt16BigEndian(message.Slice(6, 2)) != 0) { error = "invalid GNS snapshot envelope header"; return true; } var type = message[5] switch { 1 => "transform", 2 => "npcState", _ => null }; if (type is null) { error = "unknown GNS snapshot family"; return true; } var sequence = BinaryPrimitives.ReadUInt32BigEndian(message.Slice(8, 4)); if (sequence == 0) { error = "snapshot sequence zero is reserved"; return true; } var payload = message[HeaderSize..].ToArray(); if (payload.Length == 0) { error = "snapshot envelope payload is empty"; return true; } envelope = new SnapshotEnvelope(type, sequence, payload); return true; } }