using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; namespace GameCult.Networking { /// /// Minimal client port for request/response services that speak CultNet schema-v0 messages. /// public interface ICultNetSchemaClient : IDisposable { /// /// Gets whether the underlying transport reports an established connection. /// bool Connected { get; } /// /// Connects the transport to a remote endpoint. /// void Connect(string host, int port); /// /// Sends one schema-v0 message through the transport. /// void SendCultNet(T message) where T : ICultNetSchemaMessage; /// /// Adds a callback for one schema-v0 response type. /// void OnCultNet(Action callback) where T : ICultNetSchemaMessage; } /// /// LiteNetLib-backed implementation of the CultNet schema client port. /// public sealed class LiteNetLibCultNetSchemaClient : ICultNetSchemaClient { private readonly Client _client; /// /// Creates a LiteNetLib schema client adapter. /// public LiteNetLibCultNetSchemaClient( ClientSecurityOptions? security = null, Action? configureClient = null) { _client = new Client(security ?? ClientSecurityOptions.Development()) { AllowUnverifiedCultNetMessages = true }; configureClient?.Invoke(_client); } /// public bool Connected => _client.Connected; /// public void Connect(string host, int port) { _client.Connect(host, port); } /// public void SendCultNet(T message) where T : ICultNetSchemaMessage { _client.SendCultNet(message); } /// public void OnCultNet(Action callback) where T : ICultNetSchemaMessage { _client.OnCultNet(callback); } /// public void Dispose() { _client.Dispose(); } } /// /// RUDP-backed implementation of the CultNet schema client port. /// public sealed class RudpCultNetSchemaClient : ICultNetSchemaClient { private readonly string _runtimeId; private readonly uint _connectionId; private readonly string _connectPayload; private readonly int _maxFragmentBytes; private readonly long _resendDelayMs; private readonly ConcurrentDictionary> _handlers = new(); private readonly object _handlerLock = new(); private CultNetRudpSocketTransportConnection? _transport; private Thread? _pumpThread; private volatile bool _disposed; /// /// Creates a RUDP schema client adapter. /// public RudpCultNetSchemaClient( string runtimeId = "csharp-rudp-schema-client", uint connectionId = 0x43554c54, string connectPayload = "cultnet-schema-rudp", int maxFragmentBytes = 1024, long resendDelayMs = 25) { if (string.IsNullOrWhiteSpace(runtimeId)) throw new ArgumentException("Runtime id is required.", nameof(runtimeId)); _runtimeId = runtimeId; _connectionId = connectionId; _connectPayload = connectPayload; _maxFragmentBytes = maxFragmentBytes; _resendDelayMs = resendDelayMs; } /// public bool Connected => _transport?.Connected == true; /// public void Connect(string host, int port) { if (_disposed) throw new ObjectDisposedException(nameof(RudpCultNetSchemaClient)); if (string.IsNullOrWhiteSpace(host)) throw new ArgumentException("Host is required.", nameof(host)); if (port <= 0 || port > 65535) throw new ArgumentOutOfRangeException(nameof(port)); var remoteAddress = IPAddress.TryParse(host, out var parsed) ? parsed : Dns.GetHostAddresses(host)[0]; var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); socket.Bind(new IPEndPoint(IPAddress.Any, 0)); socket.ReceiveTimeout = 20; _transport = new CultNetRudpSocketTransportConnection(new CultNetRudpSocketTransportOptions { RuntimeId = _runtimeId, Mode = CultNetRudpSocketMode.Client, Socket = socket, RemoteEndPoint = new IPEndPoint(remoteAddress, port), ConnectionId = _connectionId, TransportId = "schema-rudp", MaxFragmentBytes = _maxFragmentBytes, ResendDelayMs = _resendDelayMs }); _transport.Connect(Encoding.UTF8.GetBytes(_connectPayload)); _pumpThread = new Thread(Pump) { IsBackground = true, Name = "CultNet RUDP schema client pump" }; _pumpThread.Start(); } /// public void SendCultNet(T message) where T : ICultNetSchemaMessage { if (message == null) throw new ArgumentNullException(nameof(message)); var transport = _transport ?? throw new InvalidOperationException("RUDP schema client is not connected."); transport.SendSchemaMessage(message); } /// public void OnCultNet(Action callback) where T : ICultNetSchemaMessage { if (callback == null) throw new ArgumentNullException(nameof(callback)); lock (_handlerLock) { var handlers = _handlers.GetOrAdd(typeof(T), _ => new List()); handlers.Add(callback); } } /// public void Dispose() { _disposed = true; _transport?.Dispose(); } private void Pump() { while (!_disposed) { var transport = _transport; if (transport == null) { return; } ICultNetSchemaMessage? message; try { message = transport.ReceiveSchemaMessageOnce(); transport.PollResends(); } catch (ObjectDisposedException) when (_disposed) { return; } catch (SocketException) when (_disposed) { return; } if (message != null) { Dispatch(message); } Thread.Sleep(5); } } private void Dispatch(ICultNetSchemaMessage message) { List? handlers; lock (_handlerLock) { if (!_handlers.TryGetValue(message.GetType(), out var registered)) { return; } handlers = registered.ToList(); } foreach (var handler in handlers) { handler.DynamicInvoke(message); } } } /// /// Factory helpers for schema client adapters. /// public static class CultNetSchemaClients { /// /// Creates the default C# LiteNetLib schema client adapter. /// public static ICultNetSchemaClient CreateLiteNetLib( ClientSecurityOptions? security = null, Action? configureClient = null) { return new LiteNetLibCultNetSchemaClient(security, configureClient); } /// /// Creates the C# RUDP schema client adapter. /// public static ICultNetSchemaClient CreateRudp( string runtimeId = "csharp-rudp-schema-client", uint connectionId = 0x43554c54, string connectPayload = "cultnet-schema-rudp", int maxFragmentBytes = 1024, long resendDelayMs = 25) { return new RudpCultNetSchemaClient(runtimeId, connectionId, connectPayload, maxFragmentBytes, resendDelayMs); } /// /// Creates the default schema client adapter for an advertised endpoint URI. /// public static ICultNetSchemaClient CreateForEndpoint( string endpoint, ClientSecurityOptions? security = null, Action? configureClient = null) { if (string.IsNullOrWhiteSpace(endpoint)) { throw new ArgumentException("Endpoint must be non-empty.", nameof(endpoint)); } if (!Uri.TryCreate(endpoint, UriKind.Absolute, out var uri)) { throw new FormatException($"CultNet endpoint '{endpoint}' must be an absolute URI."); } if (string.Equals(uri.Scheme, "rudp", StringComparison.OrdinalIgnoreCase)) { return CreateRudp(); } if (string.Equals(uri.Scheme, "cultnet", StringComparison.OrdinalIgnoreCase)) { return CreateLiteNetLib(security, configureClient); } throw new FormatException($"CultNet endpoint '{endpoint}' must use cultnet://host:port or rudp://host:port."); } } }