using System; using System.Buffers.Binary; using System.Collections.Generic; using System.Linq; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace GameCult.Networking { /// /// Transfer counters for a CultNet transport connection. /// public sealed class CultNetTransportStats { /// /// Gets the number of payload frames sent through the connection. /// public long FramesSent { get; internal set; } /// /// Gets the number of payload frames received through the connection. /// public long FramesReceived { get; internal set; } /// /// Gets the number of bytes sent, including transport framing. /// public long BytesSent { get; internal set; } /// /// Gets the number of bytes received, including transport framing. /// public long BytesReceived { get; internal set; } internal CultNetTransportStats Snapshot() { return new CultNetTransportStats { FramesSent = FramesSent, FramesReceived = FramesReceived, BytesSent = BytesSent, BytesReceived = BytesReceived }; } } /// /// Payload delivered by a CultNet transport connection. /// public sealed class CultNetTransportFrame { /// /// Gets or sets the logical transport channel. /// public string ChannelId { get; set; } = "schema"; /// /// Gets or sets the raw payload bytes carried by the frame. /// public byte[] Payload { get; set; } = Array.Empty(); } /// /// Options for creating a TCP framed transport profile. /// public sealed class TcpFramedTransportProfileOptions { /// /// Gets or sets the advertised transport id. /// public string TransportId { get; set; } = "tcp-framed"; /// /// Gets or sets the advertised host. /// public string? Host { get; set; } /// /// Gets or sets the advertised port. /// public int? Port { get; set; } /// /// Gets or sets the maximum payload size for the schema channel. /// public int? MaxPayloadBytes { get; set; } /// /// Gets or sets the maximum fragment size for the schema channel. /// public int? MaxFragmentBytes { get; set; } } /// /// Options for creating a CultNet RUDP transport profile. /// public sealed class RudpTransportProfileOptions { /// /// Gets or sets the advertised transport id. /// public string TransportId { get; set; } = "rudp"; /// /// Gets or sets the advertised host. /// public string? Host { get; set; } /// /// Gets or sets the advertised port. /// public int? Port { get; set; } /// /// Gets or sets the maximum payload size for RUDP channels. /// public int? MaxPayloadBytes { get; set; } /// /// Gets or sets the maximum fragment size for RUDP channels. /// public int? MaxFragmentBytes { get; set; } /// /// Gets or sets the maximum pending reliable packet count for RUDP channels. /// public int? MaxPendingReliablePackets { get; set; } } /// /// Helpers for creating CultNet transport profile documents. /// public static class CultNetTransportProfiles { /// /// Creates a profile for the current length-prefixed TCP schema lane. /// public static CultNetTransportProfile CreateTcpFramed( string runtimeId, TcpFramedTransportProfileOptions? options = null) { if (string.IsNullOrWhiteSpace(runtimeId)) throw new ArgumentException("Runtime id is required.", nameof(runtimeId)); options ??= new TcpFramedTransportProfileOptions(); return new CultNetTransportProfile { RuntimeId = runtimeId, Transports = [ new CultNetTransportDescriptor { TransportId = string.IsNullOrWhiteSpace(options.TransportId) ? "tcp-framed" : options.TransportId, Protocol = "tcp_framed", Host = options.Host, Port = options.Port, WireContracts = [CultNetWireContracts.SchemaV0], Channels = [ new CultNetTransportChannel { ChannelId = "schema", Delivery = "reliable", Ordering = "ordered", MaxPayloadBytes = options.MaxPayloadBytes, MaxFragmentBytes = options.MaxFragmentBytes } ] } ] }; } /// /// Creates a profile for the CultNet reliable UDP transport. /// public static CultNetTransportProfile CreateRudp( string runtimeId, RudpTransportProfileOptions? options = null) { if (string.IsNullOrWhiteSpace(runtimeId)) throw new ArgumentException("Runtime id is required.", nameof(runtimeId)); options ??= new RudpTransportProfileOptions(); return new CultNetTransportProfile { RuntimeId = runtimeId, Transports = [ new CultNetTransportDescriptor { TransportId = string.IsNullOrWhiteSpace(options.TransportId) ? "rudp" : options.TransportId, Protocol = "rudp", Host = options.Host, Port = options.Port, WireContracts = [CultNetWireContracts.SchemaV0], Channels = [ new CultNetTransportChannel { ChannelId = "schema", Delivery = "reliable", Ordering = "ordered", MaxPayloadBytes = options.MaxPayloadBytes, MaxFragmentBytes = options.MaxFragmentBytes, MaxPendingReliablePackets = options.MaxPendingReliablePackets }, new CultNetTransportChannel { ChannelId = "latest", Delivery = "unreliable", Ordering = "sequenced", MaxPayloadBytes = options.MaxPayloadBytes, MaxFragmentBytes = options.MaxFragmentBytes, MaxPendingReliablePackets = options.MaxPendingReliablePackets }, new CultNetTransportChannel { ChannelId = "realtime", Delivery = "unreliable", Ordering = "unordered", MaxPayloadBytes = options.MaxPayloadBytes, MaxFragmentBytes = options.MaxFragmentBytes, MaxPendingReliablePackets = options.MaxPendingReliablePackets } ] } ] }; } } /// /// Packet type codes for CultNet reliable UDP transport packets. /// public enum CultNetRudpPacketType : byte { /// Connection request packet. Connect = 1, /// Connection accepted packet. Accept = 2, /// Payload data packet. Data = 3, /// Selective acknowledgement packet. Ack = 4, /// Ping packet. Ping = 5, /// Pong packet. Pong = 6, /// Disconnect packet. Disconnect = 7 } /// /// Binary packet for the CultNet reliable UDP transport. /// public sealed class CultNetRudpPacket { /// /// Gets or sets the packet type. /// public CultNetRudpPacketType PacketType { get; set; } /// /// Gets or sets the connection/session binding id. /// public uint ConnectionId { get; set; } /// /// Gets or sets the packet sequence number. /// public uint Sequence { get; set; } /// /// Gets or sets the latest sequence acknowledged by this packet. /// public uint Ack { get; set; } /// /// Gets or sets the selective acknowledgement mask. /// public uint AckMask { get; set; } /// /// Gets or sets the logical channel id. /// public string ChannelId { get; set; } = string.Empty; /// /// Gets or sets whether the packet participates in reliable delivery. /// public bool Reliable { get; set; } /// /// Gets or sets whether the packet participates in ordered delivery. /// public bool Ordered { get; set; } /// /// Gets or sets whether the packet is a latest-state sequenced packet. /// public bool Sequenced { get; set; } /// /// Gets or sets the fragment id, or zero when unfragmented. /// public ushort FragmentId { get; set; } /// /// Gets or sets the zero-based fragment index. /// public ushort FragmentIndex { get; set; } /// /// Gets or sets the fragment count, or zero when unfragmented. /// public ushort FragmentCount { get; set; } /// /// Gets or sets the transport-neutral payload bytes. /// public byte[] Payload { get; set; } = Array.Empty(); } /// /// Encodes and decodes CultNet reliable UDP packet bytes. /// public static class CultNetRudpPacketCodec { private const int FixedHeaderBytes = 36; private const byte Version = 0; private static readonly byte[] Magic = [0x43, 0x4e, 0x52, 0x30]; /// /// Encodes a RUDP packet into the canonical binary envelope. /// public static byte[] Encode(CultNetRudpPacket packet) { if (packet == null) throw new ArgumentNullException(nameof(packet)); var channelId = Encoding.UTF8.GetBytes(packet.ChannelId ?? string.Empty); if (channelId.Length > 255) { throw new InvalidOperationException("CultNet RUDP channel id cannot exceed 255 UTF-8 bytes."); } var payload = packet.Payload ?? Array.Empty(); var headerBytes = checked(FixedHeaderBytes + channelId.Length); var wire = new byte[checked(headerBytes + payload.Length)]; Magic.CopyTo(wire, 0); wire[4] = Version; wire[5] = (byte)packet.PacketType; wire[6] = EncodeFlags(packet); wire[7] = checked((byte)headerBytes); BinaryPrimitives.WriteUInt32BigEndian(wire.AsSpan(8, 4), packet.ConnectionId); BinaryPrimitives.WriteUInt32BigEndian(wire.AsSpan(12, 4), packet.Sequence); BinaryPrimitives.WriteUInt32BigEndian(wire.AsSpan(16, 4), packet.Ack); BinaryPrimitives.WriteUInt32BigEndian(wire.AsSpan(20, 4), packet.AckMask); BinaryPrimitives.WriteUInt16BigEndian(wire.AsSpan(24, 2), packet.FragmentId); BinaryPrimitives.WriteUInt16BigEndian(wire.AsSpan(26, 2), packet.FragmentIndex); BinaryPrimitives.WriteUInt16BigEndian(wire.AsSpan(28, 2), packet.FragmentCount); BinaryPrimitives.WriteUInt32BigEndian(wire.AsSpan(30, 4), checked((uint)payload.Length)); wire[34] = checked((byte)channelId.Length); wire[35] = 0; channelId.CopyTo(wire.AsSpan(FixedHeaderBytes)); payload.CopyTo(wire.AsSpan(headerBytes)); return wire; } /// /// Decodes a RUDP packet from the canonical binary envelope. /// public static CultNetRudpPacket Decode(byte[] wire) { if (wire == null) throw new ArgumentNullException(nameof(wire)); if (wire.Length < FixedHeaderBytes) { throw new InvalidOperationException("CultNet RUDP packet is shorter than the fixed header."); } for (var index = 0; index < Magic.Length; index++) { if (wire[index] != Magic[index]) { throw new InvalidOperationException("CultNet RUDP packet has the wrong magic."); } } if (wire[4] != Version) { throw new InvalidOperationException($"Unsupported CultNet RUDP packet version {wire[4]}."); } var type = (CultNetRudpPacketType)wire[5]; if (!Enum.IsDefined(typeof(CultNetRudpPacketType), type)) { throw new InvalidOperationException($"Unsupported CultNet RUDP packet type {wire[5]}."); } var headerBytes = wire[7]; var channelIdLength = wire[34]; if (headerBytes != FixedHeaderBytes + channelIdLength) { throw new InvalidOperationException("CultNet RUDP packet header length does not match the channel id length."); } var payloadLength = checked((int)BinaryPrimitives.ReadUInt32BigEndian(wire.AsSpan(30, 4))); if (wire.Length != headerBytes + payloadLength) { throw new InvalidOperationException("CultNet RUDP packet payload length does not match the packet size."); } var flags = wire[6]; var payload = new byte[payloadLength]; Array.Copy(wire, headerBytes, payload, 0, payload.Length); return new CultNetRudpPacket { PacketType = type, Reliable = (flags & 0b0000_0001) != 0, Ordered = (flags & 0b0000_0010) != 0, Sequenced = (flags & 0b0000_0100) != 0, ConnectionId = BinaryPrimitives.ReadUInt32BigEndian(wire.AsSpan(8, 4)), Sequence = BinaryPrimitives.ReadUInt32BigEndian(wire.AsSpan(12, 4)), Ack = BinaryPrimitives.ReadUInt32BigEndian(wire.AsSpan(16, 4)), AckMask = BinaryPrimitives.ReadUInt32BigEndian(wire.AsSpan(20, 4)), FragmentId = BinaryPrimitives.ReadUInt16BigEndian(wire.AsSpan(24, 2)), FragmentIndex = BinaryPrimitives.ReadUInt16BigEndian(wire.AsSpan(26, 2)), FragmentCount = BinaryPrimitives.ReadUInt16BigEndian(wire.AsSpan(28, 2)), ChannelId = Encoding.UTF8.GetString(wire, FixedHeaderBytes, channelIdLength), Payload = payload }; } private static byte EncodeFlags(CultNetRudpPacket packet) { return (byte)( (packet.Reliable ? 0b0000_0001 : 0) | (packet.Ordered ? 0b0000_0010 : 0) | (packet.Sequenced ? 0b0000_0100 : 0) | (packet.FragmentCount > 0 ? 0b0000_1000 : 0)); } } /// /// Payload delivered by the CultNet RUDP reliability state machine. /// public sealed class CultNetRudpDeliveredFrame { /// /// Gets or sets the logical channel id. /// public string ChannelId { get; set; } = string.Empty; /// /// Gets or sets the delivered payload bytes. /// public byte[] Payload { get; set; } = Array.Empty(); /// /// Gets or sets the packet sequence that delivered the frame. /// public uint Sequence { get; set; } } /// /// Result of feeding one packet into a CultNet RUDP session. /// public sealed class CultNetRudpReceiveResult { /// /// Gets or sets delivered frames. /// public IReadOnlyList Delivered { get; set; } = Array.Empty(); /// /// Gets or sets an optional immediate reply packet. /// public CultNetRudpPacket? Reply { get; set; } /// /// Gets or sets whether the packet was a pong response. /// public bool Pong { get; set; } /// /// Gets or sets the pong payload bytes. /// public byte[] PongPayload { get; set; } = Array.Empty(); /// /// Gets or sets whether the remote peer sent a disconnect packet. /// public bool Disconnected { get; set; } /// /// Gets or sets the remote disconnect reason bytes. /// public byte[] DisconnectReason { get; set; } = Array.Empty(); } /// /// Options for the in-memory CultNet RUDP reliability state machine. /// public sealed class CultNetRudpSessionOptions { /// /// Gets or sets the connection/session binding id. /// public uint ConnectionId { get; set; } /// /// Gets or sets the first local packet sequence. /// public uint InitialSequence { get; set; } = 1; /// /// Gets or sets the resend delay in milliseconds. /// public long ResendDelayMs { get; set; } = 250; /// /// Gets or sets the maximum pending reliable packet count. /// public int? MaxPendingReliablePackets { get; set; } } /// /// Send options for RUDP data packets. /// public sealed class CultNetRudpSendOptions { /// /// Gets or sets whether the packet participates in reliable delivery. /// public bool Reliable { get; set; } /// /// Gets or sets whether the packet participates in ordered delivery. /// public bool Ordered { get; set; } /// /// Gets or sets whether the packet is latest-state sequenced. /// public bool Sequenced { get; set; } /// /// Gets or sets the current logical time in milliseconds. /// public long NowMs { get; set; } } /// /// Role for a single-peer CultNet RUDP socket transport. /// public enum CultNetRudpSocketMode { /// /// Initiates the connect packet. /// Client, /// /// Accepts the first connect packet from a remote endpoint. /// Server } /// /// Options for binding a CultNet RUDP session to a UDP socket. /// public sealed class CultNetRudpSocketTransportOptions { /// /// Gets or sets the runtime id advertised by this transport. /// public string RuntimeId { get; set; } = string.Empty; /// /// Gets or sets the bound UDP socket. /// public Socket Socket { get; set; } = null!; /// /// Gets or sets whether this side initiates or accepts the handshake. /// public CultNetRudpSocketMode Mode { get; set; } /// /// Gets or sets the expected remote endpoint. Servers may leave this unset until the first connect packet. /// public EndPoint? RemoteEndPoint { get; set; } /// /// Gets or sets the RUDP connection/session binding id. /// public uint ConnectionId { get; set; } /// /// Gets or sets the first local packet sequence. /// public uint InitialSequence { get; set; } = 1; /// /// Gets or sets the resend delay in milliseconds. /// public long ResendDelayMs { get; set; } = 250; /// /// Gets or sets the advertised transport id. /// public string TransportId { get; set; } = "rudp"; /// /// Gets or sets the maximum payload size for RUDP channels. /// public int? MaxPayloadBytes { get; set; } /// /// Gets or sets the maximum fragment size for RUDP channels. /// public int? MaxFragmentBytes { get; set; } /// /// Gets or sets the maximum pending reliable packet count for RUDP channels. /// public int? MaxPendingReliablePackets { get; set; } } /// /// Socket-free reliability state machine for CultNet RUDP. /// public sealed class CultNetRudpSession { private sealed class PendingReliablePacket { public CultNetRudpPacket Packet { get; set; } = new CultNetRudpPacket(); public long LastSentAtMs { get; set; } } private sealed class PendingOrderedFrame { public CultNetRudpDeliveredFrame Frame { get; set; } = new CultNetRudpDeliveredFrame(); public uint NextSequence { get; set; } } private sealed class FragmentBuffer { public string ChannelId { get; set; } = string.Empty; public bool Ordered { get; set; } public ushort FragmentCount { get; set; } public Dictionary Payloads { get; } = new Dictionary(); public Dictionary Sequences { get; } = new Dictionary(); } private uint _nextSequence; private ushort _nextFragmentId = 1; private readonly int? _maxPendingReliablePackets; private bool _connected; private long? _lastReceivedAtMs; private uint? _highestReceivedSequence; private readonly HashSet _receivedSequences = new HashSet(); private readonly Dictionary _pendingReliable = new Dictionary(); private readonly Dictionary _orderedNextSequenceByChannel = new Dictionary(StringComparer.Ordinal); private readonly Dictionary> _orderedBuffers = new Dictionary>(StringComparer.Ordinal); private readonly Dictionary _fragmentBuffers = new Dictionary(StringComparer.Ordinal); /// /// Initializes a RUDP reliability session. /// public CultNetRudpSession(CultNetRudpSessionOptions options) { if (options == null) throw new ArgumentNullException(nameof(options)); if (options.MaxPendingReliablePackets.HasValue && options.MaxPendingReliablePackets.Value <= 0) { throw new ArgumentOutOfRangeException(nameof(options), "RUDP MaxPendingReliablePackets must be greater than zero."); } ConnectionId = options.ConnectionId; _nextSequence = options.InitialSequence; ResendDelayMs = options.ResendDelayMs; _maxPendingReliablePackets = options.MaxPendingReliablePackets; } /// /// Gets the connection/session binding id. /// public uint ConnectionId { get; } /// /// Gets the resend delay in milliseconds. /// public long ResendDelayMs { get; } /// /// Gets whether the session has completed the connect/accept handshake. /// public bool Connected => _connected; /// /// Gets the logical time of the last received packet. /// public long? LastReceivedAtMs => _lastReceivedAtMs; /// /// Gets reliable packet sequences awaiting acknowledgement. /// public IReadOnlyList PendingReliableSequences => _pendingReliable.Keys.OrderBy(value => value).ToArray(); /// /// Creates a reliable ordered connect packet. /// public CultNetRudpPacket CreateConnect(long nowMs = 0, byte[]? payload = null) { EnsureReliableCapacity(1); var packet = CreatePacket(CultNetRudpPacketType.Connect, "control", payload ?? Array.Empty(), reliable: true, ordered: true, sequenced: false); TrackReliable(packet, nowMs); return packet; } /// /// Accepts a connect packet and returns a reliable ordered accept packet. /// public CultNetRudpPacket AcceptConnect(CultNetRudpPacket packet, long nowMs = 0, byte[]? payload = null) { RequireConnection(packet); if (packet.PacketType != CultNetRudpPacketType.Connect) { throw new InvalidOperationException($"Expected RUDP connect packet, got {packet.PacketType}."); } EnsureReliableCapacity(1); RememberReceived(packet.Sequence); _connected = true; var response = CreatePacket(CultNetRudpPacketType.Accept, "control", payload ?? Array.Empty(), reliable: true, ordered: true, sequenced: false); TrackReliable(response, nowMs); return response; } /// /// Creates a data packet. /// public CultNetRudpPacket Send(string channelId, byte[] payload, CultNetRudpSendOptions? options = null) { return SendMany(channelId, payload, options).First(); } /// /// Creates one or more data packets, fragmenting when requested. /// public IReadOnlyList SendMany(string channelId, byte[] payload, CultNetRudpSendOptions? options = null, int? maxFragmentBytes = null) { if (!_connected) { throw new InvalidOperationException("Cannot send RUDP data before the session is connected."); } options ??= new CultNetRudpSendOptions(); payload ??= Array.Empty(); if (maxFragmentBytes.HasValue && maxFragmentBytes.Value <= 0) { throw new ArgumentOutOfRangeException(nameof(maxFragmentBytes), "RUDP maxFragmentBytes must be greater than zero."); } if (maxFragmentBytes.HasValue && payload.Length > maxFragmentBytes.Value) { var fragmentCount = (payload.Length + maxFragmentBytes.Value - 1) / maxFragmentBytes.Value; if (fragmentCount > ushort.MaxValue) { throw new InvalidOperationException("RUDP payload requires more than 65535 fragments."); } EnsureReliableCapacity(options.Reliable ? fragmentCount : 0); var fragmentId = AllocateFragmentId(); var packets = new List(); for (var index = 0; index < fragmentCount; index++) { var start = index * maxFragmentBytes.Value; var length = Math.Min(maxFragmentBytes.Value, payload.Length - start); var chunk = new byte[length]; Array.Copy(payload, start, chunk, 0, length); var fragmentPacket = CreatePacket( CultNetRudpPacketType.Data, channelId, chunk, options.Reliable, options.Ordered, options.Sequenced, fragmentId, (ushort)index, (ushort)fragmentCount); if (fragmentPacket.Reliable) { TrackReliable(fragmentPacket, options.NowMs); } packets.Add(fragmentPacket); } return packets; } EnsureReliableCapacity(options.Reliable ? 1 : 0); var packet = CreatePacket( CultNetRudpPacketType.Data, channelId, payload, options.Reliable, options.Ordered, options.Sequenced); if (packet.Reliable) { TrackReliable(packet, options.NowMs); } return new[] { packet }; } /// /// Applies a remote packet to the session. /// public CultNetRudpReceiveResult Receive(CultNetRudpPacket packet, long nowMs = 0) { RequireConnection(packet); ApplyAcknowledgements(packet); _lastReceivedAtMs = nowMs; var expectedSequenceIfUninitialized = _highestReceivedSequence.HasValue ? _highestReceivedSequence.Value + 1 : packet.Sequence; if (packet.PacketType == CultNetRudpPacketType.Accept) { RememberReceived(packet.Sequence); _connected = true; return new CultNetRudpReceiveResult(); } if (packet.PacketType == CultNetRudpPacketType.Ping) { RememberReceived(packet.Sequence); return new CultNetRudpReceiveResult { Reply = CreatePacket( CultNetRudpPacketType.Pong, "control", packet.Payload ?? Array.Empty(), reliable: false, ordered: false, sequenced: false) }; } if (packet.PacketType == CultNetRudpPacketType.Ack || packet.PacketType == CultNetRudpPacketType.Pong) { RememberReceived(packet.Sequence); return new CultNetRudpReceiveResult { Pong = packet.PacketType == CultNetRudpPacketType.Pong, PongPayload = packet.PacketType == CultNetRudpPacketType.Pong ? packet.Payload ?? Array.Empty() : Array.Empty() }; } if (packet.PacketType == CultNetRudpPacketType.Disconnect) { RememberReceived(packet.Sequence); _connected = false; return new CultNetRudpReceiveResult { Disconnected = true, DisconnectReason = packet.Payload ?? Array.Empty() }; } if (packet.PacketType != CultNetRudpPacketType.Data) { return new CultNetRudpReceiveResult(); } var duplicate = _receivedSequences.Contains(packet.Sequence); RememberReceived(packet.Sequence); if (duplicate) { return new CultNetRudpReceiveResult(); } var reassembled = Reassemble(packet); if (reassembled == null) { return new CultNetRudpReceiveResult(); } return new CultNetRudpReceiveResult { Delivered = reassembled.Ordered ? DeliverOrdered(reassembled.Frame, reassembled.NextSequence, expectedSequenceIfUninitialized) : new[] { reassembled.Frame } }; } /// /// Creates a packet carrying the current acknowledgement state. /// public CultNetRudpPacket CreateAck() { return CreatePacket(CultNetRudpPacketType.Ack, "control", Array.Empty(), reliable: false, ordered: false, sequenced: false); } /// /// Creates a packet carrying a keepalive ping payload. /// public CultNetRudpPacket CreatePing(byte[]? payload = null) { return CreatePacket(CultNetRudpPacketType.Ping, "control", payload ?? Array.Empty(), reliable: false, ordered: false, sequenced: false); } /// /// Creates a packet carrying a transport-level disconnect reason. /// public CultNetRudpPacket CreateDisconnect(byte[]? reason = null) { _connected = false; return CreatePacket(CultNetRudpPacketType.Disconnect, "control", reason ?? Array.Empty(), reliable: false, ordered: false, sequenced: false); } /// /// Marks the session disconnected when no packet has arrived within the timeout window. /// public bool CheckTimeout(long nowMs, long timeoutMs) { if (!_connected || !_lastReceivedAtMs.HasValue) { return false; } if (nowMs - _lastReceivedAtMs.Value <= timeoutMs) { return false; } _connected = false; return true; } /// /// Returns reliable packets due for resend at the supplied logical time. /// public IReadOnlyList DueResends(long nowMs) { var due = new List(); foreach (var pending in _pendingReliable.Values) { if (nowMs - pending.LastSentAtMs >= ResendDelayMs) { pending.LastSentAtMs = nowMs; due.Add(ClonePacket(pending.Packet)); } } return due.OrderBy(packet => packet.Sequence).ToArray(); } private CultNetRudpPacket CreatePacket( CultNetRudpPacketType packetType, string channelId, byte[] payload, bool reliable, bool ordered, bool sequenced) { return CreatePacket(packetType, channelId, payload, reliable, ordered, sequenced, 0, 0, 0); } private CultNetRudpPacket CreatePacket( CultNetRudpPacketType packetType, string channelId, byte[] payload, bool reliable, bool ordered, bool sequenced, ushort fragmentId, ushort fragmentIndex, ushort fragmentCount) { var sequence = _nextSequence; _nextSequence = checked(_nextSequence + 1); var (ack, ackMask) = AckState(); return new CultNetRudpPacket { PacketType = packetType, ConnectionId = ConnectionId, Sequence = sequence, Ack = ack, AckMask = ackMask, ChannelId = channelId, Reliable = reliable, Ordered = ordered, Sequenced = sequenced, FragmentId = fragmentId, FragmentIndex = fragmentIndex, FragmentCount = fragmentCount, Payload = payload ?? Array.Empty() }; } private void TrackReliable(CultNetRudpPacket packet, long nowMs) { _pendingReliable[packet.Sequence] = new PendingReliablePacket { Packet = ClonePacket(packet), LastSentAtMs = nowMs }; } private void EnsureReliableCapacity(int packetCount) { if (packetCount == 0 || !_maxPendingReliablePackets.HasValue) { return; } if (_pendingReliable.Count + packetCount > _maxPendingReliablePackets.Value) { throw new InvalidOperationException("RUDP reliable send queue is full."); } } private void ApplyAcknowledgements(CultNetRudpPacket packet) { _pendingReliable.Remove(packet.Ack); for (var bit = 0; bit < 32; bit++) { if ((packet.AckMask & (1u << bit)) != 0 && packet.Ack > bit) { _pendingReliable.Remove(packet.Ack - (uint)bit - 1); } } } private void RememberReceived(uint sequence) { _receivedSequences.Add(sequence); if (!_highestReceivedSequence.HasValue || sequence > _highestReceivedSequence.Value) { _highestReceivedSequence = sequence; } } private (uint Ack, uint AckMask) AckState() { var ack = _highestReceivedSequence ?? 0; uint ackMask = 0; for (var bit = 0; bit < 32; bit++) { if (ack > bit && _receivedSequences.Contains(ack - (uint)bit - 1)) { ackMask |= 1u << bit; } } return (ack, ackMask); } private sealed class ReassembledFrame { public CultNetRudpDeliveredFrame Frame { get; set; } = new CultNetRudpDeliveredFrame(); public bool Ordered { get; set; } public uint NextSequence { get; set; } } private ReassembledFrame? Reassemble(CultNetRudpPacket packet) { if (packet.FragmentCount == 0) { return new ReassembledFrame { Frame = new CultNetRudpDeliveredFrame { ChannelId = packet.ChannelId, Payload = packet.Payload ?? Array.Empty(), Sequence = packet.Sequence }, Ordered = packet.Ordered, NextSequence = packet.Sequence + 1 }; } if (packet.FragmentId == 0) { throw new InvalidOperationException("RUDP fragmented packet must have a non-zero fragment id."); } if (packet.FragmentIndex >= packet.FragmentCount) { throw new InvalidOperationException("RUDP fragment index must be lower than fragment count."); } var key = $"{packet.ChannelId}\0{packet.FragmentId}"; if (!_fragmentBuffers.TryGetValue(key, out var buffer)) { buffer = new FragmentBuffer { ChannelId = packet.ChannelId, Ordered = packet.Ordered, FragmentCount = packet.FragmentCount }; _fragmentBuffers[key] = buffer; } if (buffer.FragmentCount != packet.FragmentCount || buffer.Ordered != packet.Ordered) { throw new InvalidOperationException("RUDP fragment metadata changed within a fragment set."); } buffer.Payloads[packet.FragmentIndex] = packet.Payload?.ToArray() ?? Array.Empty(); buffer.Sequences[packet.FragmentIndex] = packet.Sequence; if (buffer.Payloads.Count < packet.FragmentCount) { return null; } var payloadLength = buffer.Payloads.Values.Sum(chunk => chunk.Length); var payload = new byte[payloadLength]; var offset = 0; for (ushort index = 0; index < packet.FragmentCount; index++) { var chunk = buffer.Payloads[index]; Array.Copy(chunk, 0, payload, offset, chunk.Length); offset += chunk.Length; } var sequences = buffer.Sequences.Values.ToArray(); _fragmentBuffers.Remove(key); return new ReassembledFrame { Frame = new CultNetRudpDeliveredFrame { ChannelId = buffer.ChannelId, Payload = payload, Sequence = sequences.Min() }, Ordered = buffer.Ordered, NextSequence = sequences.Max() + 1 }; } private IReadOnlyList DeliverOrdered( CultNetRudpDeliveredFrame frame, uint nextSequenceAfterFrame, uint expectedSequenceIfUninitialized) { if (!_orderedNextSequenceByChannel.TryGetValue(frame.ChannelId, out var next)) { next = Math.Min(expectedSequenceIfUninitialized, frame.Sequence); _orderedNextSequenceByChannel[frame.ChannelId] = next; } if (frame.Sequence < next) { return Array.Empty(); } if (frame.Sequence > next) { if (!_orderedBuffers.TryGetValue(frame.ChannelId, out var buffer)) { buffer = new SortedDictionary(); _orderedBuffers[frame.ChannelId] = buffer; } buffer[frame.Sequence] = new PendingOrderedFrame { Frame = frame, NextSequence = nextSequenceAfterFrame }; return Array.Empty(); } _orderedNextSequenceByChannel[frame.ChannelId] = nextSequenceAfterFrame; return new[] { frame }.Concat(DrainOrdered(frame.ChannelId)).ToArray(); } private IReadOnlyList DrainOrdered(string channelId) { var delivered = new List(); if (!_orderedBuffers.TryGetValue(channelId, out var buffer)) { return delivered; } while (_orderedNextSequenceByChannel.TryGetValue(channelId, out var next) && buffer.TryGetValue(next, out var pending)) { buffer.Remove(next); delivered.Add(pending.Frame); _orderedNextSequenceByChannel[channelId] = pending.NextSequence; } return delivered; } private ushort AllocateFragmentId() { var fragmentId = _nextFragmentId; _nextFragmentId++; if (_nextFragmentId == 0) { _nextFragmentId = 1; } return fragmentId; } private void RequireConnection(CultNetRudpPacket packet) { if (packet.ConnectionId != ConnectionId) { throw new InvalidOperationException($"RUDP packet connection id {packet.ConnectionId} does not match {ConnectionId}."); } } private static CultNetRudpPacket ClonePacket(CultNetRudpPacket packet) { return new CultNetRudpPacket { PacketType = packet.PacketType, ConnectionId = packet.ConnectionId, Sequence = packet.Sequence, Ack = packet.Ack, AckMask = packet.AckMask, ChannelId = packet.ChannelId, Reliable = packet.Reliable, Ordered = packet.Ordered, Sequenced = packet.Sequenced, FragmentId = packet.FragmentId, FragmentIndex = packet.FragmentIndex, FragmentCount = packet.FragmentCount, Payload = packet.Payload?.ToArray() ?? Array.Empty() }; } } /// /// Single-peer UDP socket binding for the CultNet RUDP reliability session. /// public sealed class CultNetRudpSocketTransportConnection : IDisposable { private readonly Socket _socket; private readonly CultNetRudpSession _session; private readonly CultNetRudpSocketMode _mode; private readonly int? _maxFragmentBytes; private readonly CultNetTransportStats _stats = new CultNetTransportStats(); private readonly Queue _deliveredFrames = new Queue(); private readonly Queue _pongPayloads = new Queue(); private EndPoint? _remoteEndPoint; private bool _disposed; /// /// Initializes a UDP socket binding around a CultNet RUDP session. /// public CultNetRudpSocketTransportConnection(CultNetRudpSocketTransportOptions options) { if (options == null) throw new ArgumentNullException(nameof(options)); if (string.IsNullOrWhiteSpace(options.RuntimeId)) throw new ArgumentException("Runtime id is required.", nameof(options)); _socket = options.Socket ?? throw new ArgumentNullException(nameof(options.Socket)); _mode = options.Mode; _remoteEndPoint = options.RemoteEndPoint; _maxFragmentBytes = options.MaxFragmentBytes; _session = new CultNetRudpSession(new CultNetRudpSessionOptions { ConnectionId = options.ConnectionId, InitialSequence = options.InitialSequence, ResendDelayMs = options.ResendDelayMs, MaxPendingReliablePackets = options.MaxPendingReliablePackets }); var local = _socket.LocalEndPoint as IPEndPoint; Profile = CultNetTransportProfiles.CreateRudp( options.RuntimeId, new RudpTransportProfileOptions { TransportId = options.TransportId, Host = local?.Address.ToString(), Port = local?.Port, MaxPayloadBytes = options.MaxPayloadBytes, MaxFragmentBytes = options.MaxFragmentBytes, MaxPendingReliablePackets = options.MaxPendingReliablePackets }); } /// /// Gets the profile this connection implements. /// public CultNetTransportProfile Profile { get; } /// /// Gets whether the RUDP handshake has completed. /// public bool Connected => _session.Connected; /// /// Gets a snapshot of the current transfer counters. /// public CultNetTransportStats Stats => _stats.Snapshot(); /// /// Gets the last transport-level remote disconnect reason, if one was received. /// public byte[]? DisconnectReason { get; private set; } /// /// Attempts to dequeue the next received pong payload. /// public bool TryDequeuePongPayload(out byte[] payload) { if (_pongPayloads.Count > 0) { payload = _pongPayloads.Dequeue(); return true; } payload = Array.Empty(); return false; } /// /// Sends the client connect packet. /// public void Connect(byte[]? payload = null) { if (_mode != CultNetRudpSocketMode.Client) { throw new InvalidOperationException("Only a client RUDP socket transport can initiate connect."); } SendPacket(_session.CreateConnect(NowMs(), payload ?? Array.Empty())); } /// /// Sends the client connect packet with a UTF-8 payload. /// public void Connect(string payload) { Connect(Encoding.UTF8.GetBytes(payload ?? string.Empty)); } /// /// Sends the client connect packet and polls until the handshake completes or times out. /// public bool ConnectAndWait( byte[]? payload = null, TimeSpan? timeout = null, TimeSpan? pollInterval = null) { Connect(payload); return AwaitConnected(timeout, pollInterval); } /// /// Sends the client connect packet with a UTF-8 payload and polls until the handshake completes or times out. /// public bool ConnectAndWait( string payload, TimeSpan? timeout = null, TimeSpan? pollInterval = null) { return ConnectAndWait(Encoding.UTF8.GetBytes(payload ?? string.Empty), timeout, pollInterval); } /// /// Polls the transport until the RUDP handshake completes or times out. /// public bool AwaitConnected(TimeSpan? timeout = null, TimeSpan? pollInterval = null) { var deadline = DateTimeOffset.UtcNow.Add(timeout ?? TimeSpan.FromSeconds(1)); var interval = pollInterval ?? TimeSpan.FromMilliseconds(5); while (DateTimeOffset.UtcNow < deadline) { _ = ReceiveOnce(); PollResends(); if (Connected) { return true; } Thread.Sleep(interval); } return Connected; } /// /// Sends a logical transport frame through the RUDP session. /// public void Send(string channelId, byte[] payload) { foreach (var packet in _session.SendMany(channelId, payload, ChannelSendOptions(channelId), _maxFragmentBytes)) { SendPacket(packet); } _stats.FramesSent++; } /// /// Sends a reliable ordered schema-channel payload. /// public void SendSchema(byte[] payload) { Send("schema", payload); } /// /// Sends a reliable ordered schema-channel UTF-8 payload. /// public void SendSchema(string payload) { SendSchema(Encoding.UTF8.GetBytes(payload ?? string.Empty)); } /// /// Sends a CultNet schema-v0 message on the reliable ordered schema channel. /// public void SendSchemaMessage(TMessage message) where TMessage : ICultNetSchemaMessage { if (message == null) throw new ArgumentNullException(nameof(message)); Send("schema", CultNetSchemaMessageSerialization.Serialize(message)); } /// /// Sends an unreliable sequenced latest-state payload. /// public void SendLatest(byte[] payload) { Send("latest", payload); } /// /// Sends an unreliable sequenced latest-state UTF-8 payload. /// public void SendLatest(string payload) { SendLatest(Encoding.UTF8.GetBytes(payload ?? string.Empty)); } /// /// Sends an unreliable unordered realtime payload. /// public void SendRealtime(byte[] payload) { Send("realtime", payload); } /// /// Sends an unreliable unordered realtime UTF-8 payload. /// public void SendRealtime(string payload) { SendRealtime(Encoding.UTF8.GetBytes(payload ?? string.Empty)); } /// /// Attempts to receive the next CultNet schema-v0 message. /// public ICultNetSchemaMessage? ReceiveSchemaMessageOnce() { while (true) { var frame = ReceiveOnce(); if (frame == null) { return null; } if (!string.Equals(frame.ChannelId, "schema", StringComparison.Ordinal)) { continue; } return CultNetSchemaMessageSerialization.Deserialize(frame.Payload); } } /// /// Attempts to receive the next CultNet schema-v0 message of the requested type. /// public TMessage? ReceiveSchemaMessageOnce() where TMessage : class, ICultNetSchemaMessage { return ReceiveSchemaMessageOnce() as TMessage; } /// /// Polls until a delivered transport frame matches the predicate or the timeout expires. /// public CultNetTransportFrame? ReceiveUntil( TimeSpan timeout, Func? predicate = null, TimeSpan? pollInterval = null) { var deadline = DateTimeOffset.UtcNow.Add(timeout); var interval = pollInterval ?? TimeSpan.FromMilliseconds(5); predicate ??= _ => true; while (DateTimeOffset.UtcNow < deadline) { var frame = ReceiveOnce(); if (frame != null && predicate(frame)) { return frame; } PollResends(); Thread.Sleep(interval); } return null; } /// /// Polls until a schema-channel payload arrives or the timeout expires. /// public byte[]? ReceiveSchema(TimeSpan timeout, TimeSpan? pollInterval = null) { return ReceiveUntil( timeout, frame => string.Equals(frame.ChannelId, "schema", StringComparison.Ordinal), pollInterval)?.Payload; } /// /// Polls until a CultNet schema-v0 message arrives or the timeout expires. /// public ICultNetSchemaMessage? ReceiveSchemaMessage(TimeSpan timeout, TimeSpan? pollInterval = null) { var payload = ReceiveSchema(timeout, pollInterval); return payload == null ? null : CultNetSchemaMessageSerialization.Deserialize(payload); } /// /// Polls until a CultNet schema-v0 message of the requested type arrives or the timeout expires. /// public TMessage? ReceiveSchemaMessage(TimeSpan timeout, TimeSpan? pollInterval = null) where TMessage : class, ICultNetSchemaMessage { return ReceiveSchemaMessage(timeout, pollInterval) as TMessage; } /// /// Sends a transport-level disconnect packet. /// public void Disconnect(byte[]? reason = null) { SendPacket(_session.CreateDisconnect(reason ?? Array.Empty())); } /// /// Sends a transport-level ping packet. /// public void Ping(byte[]? payload = null) { SendPacket(_session.CreatePing(payload ?? Array.Empty())); } /// /// Checks whether the session has exceeded the receive timeout. /// public bool CheckTimeout(long timeoutMs) { return _session.CheckTimeout(NowMs(), timeoutMs); } /// /// Polls the UDP socket once and returns the next delivered transport frame if one is available. /// public CultNetTransportFrame? ReceiveOnce() { if (_deliveredFrames.Count > 0) { return _deliveredFrames.Dequeue(); } var buffer = new byte[65535]; EndPoint remote = new IPEndPoint(IPAddress.Any, 0); int received; try { received = _socket.ReceiveFrom(buffer, ref remote); } catch (SocketException error) when ( error.SocketErrorCode == SocketError.WouldBlock || error.SocketErrorCode == SocketError.TimedOut) { return null; } _stats.BytesReceived += received; if (_remoteEndPoint == null) { _remoteEndPoint = remote; } else if (!_remoteEndPoint.Equals(remote)) { return null; } var wire = new byte[received]; Array.Copy(buffer, wire, received); var packet = CultNetRudpPacketCodec.Decode(wire); if (_mode == CultNetRudpSocketMode.Server && packet.PacketType == CultNetRudpPacketType.Connect) { SendPacket(_session.AcceptConnect(packet, NowMs())); return null; } var result = _session.Receive(packet, NowMs()); if (result.Reply != null) { SendPacket(result.Reply); } if (result.Pong) { _pongPayloads.Enqueue(result.PongPayload); } if (result.Disconnected) { DisconnectReason = result.DisconnectReason; return null; } foreach (var frame in result.Delivered) { _deliveredFrames.Enqueue(new CultNetTransportFrame { ChannelId = frame.ChannelId, Payload = frame.Payload }); _stats.FramesReceived++; } var delivered = _deliveredFrames.Count > 0 ? _deliveredFrames.Dequeue() : null; if (packet.PacketType == CultNetRudpPacketType.Accept || delivered != null) { SendPacket(_session.CreateAck()); } return delivered; } /// /// Sends any reliable packets whose resend timers are due. /// public void PollResends() { foreach (var packet in _session.DueResends(NowMs())) { SendPacket(packet); } } /// public void Dispose() { if (_disposed) { return; } _disposed = true; _socket.Dispose(); } private void SendPacket(CultNetRudpPacket packet) { if (_remoteEndPoint == null) { throw new InvalidOperationException("RUDP socket transport does not have a remote endpoint."); } var wire = CultNetRudpPacketCodec.Encode(packet); var sent = _socket.SendTo(wire, _remoteEndPoint); _stats.BytesSent += sent; } private static CultNetRudpSendOptions ChannelSendOptions(string channelId) { return string.Equals(channelId, "schema", StringComparison.Ordinal) ? new CultNetRudpSendOptions { Reliable = true, Ordered = true, NowMs = NowMs() } : string.Equals(channelId, "latest", StringComparison.Ordinal) ? new CultNetRudpSendOptions { Sequenced = true, NowMs = NowMs() } : new CultNetRudpSendOptions { NowMs = NowMs() }; } private static long NowMs() { return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); } } /// /// Transport connection for the current TCP length-prefixed CultNet schema lane. /// public sealed class TcpFramedTransportConnection { private const int HeaderBytes = 4; private readonly Stream _stream; private readonly CultNetTransportStats _stats = new CultNetTransportStats(); /// /// Initializes a TCP framed transport over an existing stream. /// public TcpFramedTransportConnection(Stream stream, CultNetTransportProfile profile) { _stream = stream ?? throw new ArgumentNullException(nameof(stream)); Profile = profile ?? throw new ArgumentNullException(nameof(profile)); } /// /// Gets the profile this connection implements. /// public CultNetTransportProfile Profile { get; } /// /// Gets a snapshot of the current transfer counters. /// public CultNetTransportStats Stats => _stats.Snapshot(); /// /// Sends a schema-channel payload. /// public async Task SendAsync(string channelId, byte[] payload, CancellationToken cancellationToken = default) { if (!string.Equals(channelId, "schema", StringComparison.Ordinal)) { throw new InvalidOperationException($"tcp_framed transport only supports the schema channel, got \"{channelId}\"."); } if (payload == null) throw new ArgumentNullException(nameof(payload)); var header = new byte[HeaderBytes]; BinaryPrimitives.WriteUInt32BigEndian(header, checked((uint)payload.Length)); await _stream.WriteAsync(header, cancellationToken).ConfigureAwait(false); await _stream.WriteAsync(payload, cancellationToken).ConfigureAwait(false); await _stream.FlushAsync(cancellationToken).ConfigureAwait(false); _stats.BytesSent += HeaderBytes + payload.Length; _stats.FramesSent++; } /// /// Receives the next schema-channel payload. /// public async Task ReceiveAsync(CancellationToken cancellationToken = default) { var header = new byte[HeaderBytes]; await ReadExactlyAsync(header, cancellationToken).ConfigureAwait(false); var payloadLength = checked((int)BinaryPrimitives.ReadUInt32BigEndian(header)); var payload = new byte[payloadLength]; await ReadExactlyAsync(payload, cancellationToken).ConfigureAwait(false); _stats.BytesReceived += HeaderBytes + payloadLength; _stats.FramesReceived++; return new CultNetTransportFrame { ChannelId = "schema", Payload = payload }; } private async Task ReadExactlyAsync(byte[] buffer, CancellationToken cancellationToken) { var offset = 0; while (offset < buffer.Length) { var read = await _stream.ReadAsync(buffer.AsMemory(offset, buffer.Length - offset), cancellationToken) .ConfigureAwait(false); if (read == 0) { throw new EndOfStreamException(); } offset += read; } } } }