using System; using System.Net; using System.Threading; using System.Threading.Tasks; namespace codelanguage { /// One declared argument of a remote verb. public sealed class RemoteVerbParameter { public string Name { get; set; } = string.Empty; public string Type { get; set; } = string.Empty; } /// A verb advertised by a hosted world for the player's mob type. public sealed class RemoteVerb { public string Name { get; set; } = string.Empty; public string Category { get; set; } = string.Empty; public List Parameters { get; set; } = new(); } /// /// Lightweight client-side network session. It wires a transport connection /// to ClientReplicationManager, keeps authentication/connection state, and /// provides the heartbeat messages and lifecycle events needed by a client /// world renderer such as CraftSeeker. /// public class ClientNetworkSession : IDisposable { private readonly CancellationTokenSource _lifetime = new CancellationTokenSource(); private CancellationTokenSource? _heartbeatCancellation; private Task? _heartbeatTask; private TaskCompletionSource? _pendingLogin; private readonly object _pendingLoginLock = new object(); private readonly object _connectionLifecycleLock = new object(); private Task? _disconnectLifecycleTask; private volatile bool _disconnectLifecycleStarted; public NetworkClient Client { get; } public ClientReplicationManager Replication { get; } public ClientAuthenticationState Authentication { get; } public ClientConnectionState Connection { get; } public event Action? MessageReceived; public event Action? ObjectSpawned; public event Action? ObjectUpdated; public event Action? ObjectDespawned; public event Action? Connected; public event Action? Disconnected; /// Raised when a chunked world resource bundle from the server /// has been fully reassembled and verified. public event Action? ResourceBundleReceived; /// Completed world resource bundle (.rsc) downloaded from the /// server, if any. Also set before /// subscribers attach, so callers can check it after login returns. public byte[]? ResourceBundle { get; private set; } private readonly ResourceBundleTransfer.Receiver _resourceBundleReceiver = new(); /// Verbs advertised by the hosted world for this connection's /// player mob (populated from the LoginResponse). A remote Seeker renders /// these instead of walking its own local TypeRegistry, which does not /// contain the world's types. public IReadOnlyList RemoteVerbs { get; private set; } = Array.Empty(); public ClientNetworkSession( NetworkClient? client = null, ClientReplicationManager? replication = null, ClientConnectionState? connection = null, ClientAuthenticationState? authentication = null) { Client = client ?? new NetworkClient(); Replication = replication ?? new ClientReplicationManager(); Connection = connection ?? new ClientConnectionState(); Authentication = authentication ?? new ClientAuthenticationState(); Client.MessageReceived += HandleTransportMessage; Client.Disconnected += HandleTransportDisconnected; Replication.ObjectSpawned += replicatedObject => InvokeSafely(ObjectSpawned, replicatedObject); Replication.ObjectUpdated += replicatedObject => InvokeSafely(ObjectUpdated, replicatedObject); Replication.ObjectDespawned += objectId => InvokeSafely(ObjectDespawned, objectId); } public bool IsConnected => Connection.IsConnected; public bool IsAuthenticated => Authentication.IsAuthenticated; public int ClientId => Connection.ClientId; public int PlayerObjectId { get; private set; } public TimeSpan HeartbeatInterval { get; set; } = TimeSpan.FromMilliseconds(NetworkConfig.HeartbeatIntervalMs); public async Task ConnectAsync(string host, int port, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(host)) throw new ArgumentException("The server host must be supplied.", nameof(host)); if (port <= 0 || port > 65535) throw new ArgumentOutOfRangeException(nameof(port)); if (Connection.IsConnected) return true; Connection.ServerAddress = host; Connection.ServerPort = port; ResourceBundle = null; _resourceBundleReceiver.Reset(); RemoteVerbs = Array.Empty(); await Client.ConnectAsync(host, port, cancellationToken).ConfigureAwait(false); lock (_connectionLifecycleLock) { _disconnectLifecycleTask = null; _disconnectLifecycleStarted = false; } _heartbeatCancellation?.Dispose(); _heartbeatCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _lifetime.Token); Connection.IsConnected = true; Connection.ConnectedAt = DateTime.UtcNow; Connection.LastHeartbeat = DateTime.UtcNow; InvokeSafely(Connected); _heartbeatTask = HeartbeatLoopAsync(_heartbeatCancellation.Token); return true; } public Task LoginAsync(string username, string password, CancellationToken cancellationToken = default) => LoginCoreAsync(username, password, null, cancellationToken); public Task LoginWithHubSessionAsync(string username, string hubSessionToken, CancellationToken cancellationToken = default) => LoginCoreAsync(username, null, hubSessionToken, cancellationToken); private async Task LoginCoreAsync(string username, string? password, string? hubSessionToken, CancellationToken cancellationToken) { if (!Connection.IsConnected) throw new InvalidOperationException("The client must be connected before logging in."); TaskCompletionSource pendingLogin; lock (_pendingLoginLock) { if (_pendingLogin != null && !_pendingLogin.Task.IsCompleted) throw new InvalidOperationException("A login request is already pending."); _pendingLogin = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); pendingLogin = _pendingLogin; } if (string.IsNullOrWhiteSpace(hubSessionToken)) Client.SendLoginRequest(username, password ?? string.Empty); else Client.SendHubLoginRequest(username, hubSessionToken); NetworkMessage response; try { Task timeoutTask = Task.Delay(NetworkConfig.HeartbeatTimeoutMs, cancellationToken); Task completedTask = await Task.WhenAny(pendingLogin.Task, timeoutTask).ConfigureAwait(false); if (completedTask != pendingLogin.Task) if (cancellationToken.IsCancellationRequested) throw new OperationCanceledException(cancellationToken); else throw new TimeoutException("The login response was not received before the heartbeat timeout expired."); response = await pendingLogin.Task.ConfigureAwait(false); } catch { lock (_pendingLoginLock) { if (_pendingLogin == pendingLogin) _pendingLogin = null; } throw; } lock (_pendingLoginLock) { _pendingLogin = null; } bool success = Client.ApplyLoginResponse(response); if (success) { Connection.IsAuthenticated = true; // Account IDs and transport connection IDs are different namespaces. } else { Connection.IsAuthenticated = false; Authentication.ClearAuthentication(); } return success; } public void HandleMessage(NetworkMessage message) { if (message == null) throw new ArgumentNullException(nameof(message)); HandleTransportMessage(message); } public bool ApplyMessage(NetworkMessage message) { HandleMessage(message); return true; } public void ProcessMessage(NetworkMessage message) => HandleMessage(message); public void ReceiveMessage(NetworkMessage message) => HandleMessage(message); /// Sends a verb/proc invocation to the server for this /// connection's player mob. The result (or an error) arrives as /// ProcReturn/ProcError and is stored in 's RPC /// table under the returned call id. Fire-and-forget is the normal verb /// pattern; callers that need the result can poll GetResponse(callId). public long SendProcedureCall(string procName, params object?[] args) => SendProcedureCall(PlayerObjectId, procName, args); /// Sends a verb/proc invocation targeted at a specific replicated /// object (0 or the player's own id targets the player mob itself). public long SendProcedureCall(int targetObjectId, string procName, params object?[] args) { if (string.IsNullOrWhiteSpace(procName)) throw new ArgumentException("A procedure name is required.", nameof(procName)); if (!Connection.IsAuthenticated) throw new InvalidOperationException("The session must be authenticated before calling world procedures."); long callId = Replication.Rpc.GenerateCallId(); Client.Send(Replication.Rpc.CreateCallMessage(callId, procName, targetObjectId, args ?? Array.Empty())); return callId; } private static IReadOnlyList ParseRemoteVerbs(object? raw) { if (raw is not IEnumerable items) return Array.Empty(); var verbs = new List(); foreach (var item in items) { if (item is Dictionary map) { string name = map.TryGetValue("name", out var nameValue) ? nameValue?.ToString() ?? string.Empty : string.Empty; if (name.Length == 0) continue; var parameters = new List(); if (map.TryGetValue("parameters", out var parameterValue) && parameterValue is IEnumerable parameterItems) { foreach (var parameterItem in parameterItems) { if (parameterItem is not Dictionary parameterMap) continue; string parameterName = parameterMap.TryGetValue("name", out var parameterNameValue) ? parameterNameValue?.ToString() ?? string.Empty : string.Empty; if (parameterName.Length == 0) continue; parameters.Add(new RemoteVerbParameter { Name = parameterName, Type = parameterMap.TryGetValue("type", out var parameterTypeValue) ? parameterTypeValue?.ToString() ?? string.Empty : string.Empty }); } } verbs.Add(new RemoteVerb { Name = name, Category = map.TryGetValue("category", out var categoryValue) ? categoryValue?.ToString() ?? string.Empty : string.Empty, Parameters = parameters }); } else if (item is string text && text.Length > 0) { verbs.Add(new RemoteVerb { Name = text }); } } return verbs; } public void SendHeartbeat() { if (!Connection.IsConnected) return; var message = new NetworkMessage(MessageType.Heartbeat, Connection.ClientId) { Data = new Dictionary { { "timestamp", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }, { "latency", Connection.Latency } } }; if (Connection.ClientId > 0 && message.ClientId == 0) message.ClientId = Connection.ClientId; Client.Send(message); } public void Tick(DateTime now = default) { if (now == default) now = DateTime.UtcNow; if (Client.IsConnected && now - Connection.LastHeartbeat >= HeartbeatInterval) SendHeartbeat(); } public async Task DisconnectAsync() { Task disconnectCore = EnsureDisconnectLifecycleAsync(); await disconnectCore.ConfigureAwait(false); } private Task EnsureDisconnectLifecycleAsync() { lock (_connectionLifecycleLock) { if (_disconnectLifecycleTask == null) { _disconnectLifecycleTask = Task.Run(RunDisconnectLifecycleAsync); } return _disconnectLifecycleTask; } } private async Task RunDisconnectLifecycleAsync() { try { CancellationTokenSource? heartbeatCancellation = _heartbeatCancellation; heartbeatCancellation?.Cancel(); try { Task? heartbeatTask = _heartbeatTask; if (heartbeatTask != null) await heartbeatTask.ConfigureAwait(false); } catch (OperationCanceledException) { } catch (ObjectDisposedException) { } if (heartbeatCancellation != null) { try { heartbeatCancellation.Dispose(); } catch { } if (_heartbeatCancellation == heartbeatCancellation) _heartbeatCancellation = null; } await Client.DisconnectAsync().ConfigureAwait(false); } finally { bool shouldNotify; lock (_connectionLifecycleLock) { shouldNotify = !_disconnectLifecycleStarted; if (shouldNotify) _disconnectLifecycleStarted = true; } if (shouldNotify) { Connection.IsConnected = false; Connection.IsAuthenticated = false; Authentication.ClearAuthentication(); InvokeSafely(Disconnected); } } } public void Dispose() { _heartbeatCancellation?.Cancel(); _heartbeatCancellation?.Dispose(); _heartbeatCancellation = null; _lifetime.Cancel(); try { Client.MessageReceived -= HandleTransportMessage; Client.Disconnected -= HandleTransportDisconnected; } catch { } try { Client.Dispose(); } catch { } } private void HandleTransportMessage(NetworkMessage message) { try { if (message.Type == MessageType.ConnectResponse) { if (message.Data.TryGetValue("clientId", out var id)) Connection.ClientId = AsInt(id); } else if (message.Type == MessageType.LoginResponse) { if (message.Data.TryGetValue("playerObjectId", out var playerId)) PlayerObjectId = AsInt(playerId); RemoteVerbs = ParseRemoteVerbs(message.Data.TryGetValue("verbs", out var verbs) ? verbs : null); bool success = Client.ApplyLoginResponse(message); SetAuthenticationState(success, message); lock (_pendingLoginLock) { _pendingLogin?.TrySetResult(message); } } else if (message.Type == MessageType.Heartbeat) { Connection.LastHeartbeat = DateTime.UtcNow; if (message.Data.TryGetValue("latency", out var latencyValue) && latencyValue != null) Connection.Latency = AsInt(latencyValue); } else if (message.Type is MessageType.ResourceBundleStart or MessageType.ResourceBundleChunk or MessageType.ResourceBundleEnd) { if (_resourceBundleReceiver.Accept(message) && _resourceBundleReceiver.Bundle is byte[] bundle) { ResourceBundle = bundle; InvokeSafely(ResourceBundleReceived, bundle); } } Replication.ApplyMessage(message); MessageReceived?.Invoke(message); } catch { } } private void HandleTransportDisconnected() { _resourceBundleReceiver.Reset(); _ = EnsureDisconnectLifecycleAsync(); } private void SetAuthenticationState(bool success, NetworkMessage response) { if (success) { Authentication.SetAuthenticated( response.Data.TryGetValue("userId", out var userIdValue) ? AsInt(userIdValue) : Authentication.UserId ?? 0, response.Data.TryGetValue("username", out var usernameValue) ? usernameValue?.ToString() ?? string.Empty : Authentication.Username ?? string.Empty, response.Data.TryGetValue("sessionToken", out var tokenValue) ? tokenValue?.ToString() ?? string.Empty : Authentication.SessionToken ?? string.Empty); Connection.IsAuthenticated = true; } else { Authentication.ClearAuthentication(); Connection.IsAuthenticated = false; } } private async Task HeartbeatLoopAsync(CancellationToken cancellationToken) { using var linkedCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _lifetime.Token); CancellationToken linkedToken = linkedCancellation.Token; try { while (!linkedToken.IsCancellationRequested) { await Task.Delay(HeartbeatInterval, linkedToken).ConfigureAwait(false); if (Client.IsConnected) SendHeartbeat(); } } catch (OperationCanceledException) { } catch (ObjectDisposedException) { } } private static void InvokeSafely(Action? handler) { if (handler == null) return; foreach (Action subscriber in handler.GetInvocationList().Cast()) { try { subscriber(); } catch { } } } private static void InvokeSafely(Action? handler, T argument) { if (handler == null) return; foreach (Action subscriber in handler.GetInvocationList().Cast>()) { try { subscriber(argument); } catch { } } } private static int AsInt(object? value) { if (value == null) return 0; if (value is int intValue) return intValue; if (value is long longValue) return unchecked((int)longValue); if (value is double doubleValue) return checked((int)Math.Round(doubleValue)); return int.TryParse(Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture), System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out int parsedValue) ? parsedValue : 0; } } public class ClientSession : ClientNetworkSession { } public class NetworkClientSession : ClientNetworkSession { } public class ClientNetworkRuntime : ClientNetworkSession { } }