using System.Runtime.CompilerServices; using System.Threading; using Sandbox.Network; namespace Sandbox; /// /// A connection, usually to a server or a client. /// [Expose, ActionGraphIgnore] public abstract partial class Connection { internal abstract void InternalSend( ByteStream stream, NetFlags flags ); internal abstract void InternalRecv( NetworkSystem.MessageHandler handler ); internal abstract void InternalClose( int closeCode, string closeReason ); /// /// Called when we receive from this . /// /// Our outgoing data. /// /// Whether or not we want to continue to connect. internal virtual bool OnReceiveServerInfo( ref UserInfo userInfo, ServerInfo serverInfo ) { return true; } /// /// Called when we receive from this . /// /// /// Whether or not we want to allow this connection internal virtual bool OnReceiveUserInfo( UserInfo info ) { return true; } /// /// Get whether this connection has a specific permission. /// public virtual bool HasPermission( string permission ) { // The host has every permission by default. return IsHost; } /// /// This connection's unique identifier. /// [ActionGraphInclude] public Guid Id { get; protected set; } /// /// A unique identifier that is set when the connection starts handshaking. This identifier will /// be passed into all handshake messages, so that if a new handshaking process starts while one /// is already active, old handshake messages will be ignored. /// internal Guid HandshakeId { get; set; } /// /// The this connection belongs to. /// internal NetworkSystem System { get; set; } /// /// An array of Pvs sources from this connection. /// internal Vector3[] VisibilityOrigins = []; /// /// Calculate the closest distance (squared) to a position based on the Pvs sources from /// this . /// [MethodImpl( MethodImplOptions.AggressiveInlining )] public float DistanceSquared( Vector3 position ) { if ( VisibilityOrigins == null || VisibilityOrigins.Length == 0 ) return float.PositiveInfinity; var minSq = float.PositiveInfinity; var sources = VisibilityOrigins; var count = sources.Length; for ( var i = 0; i < count; i++ ) { var source = VisibilityOrigins[i]; var distance = source.DistanceSquared( position ); if ( distance > minSq ) continue; if ( distance == 0f ) return 0f; minSq = distance; } return minSq; } /// /// Calculate the closest distance to a position based on the Pvs sources from /// this . /// [MethodImpl( MethodImplOptions.AggressiveInlining )] internal float Distance( Vector3 position ) { return MathF.Sqrt( DistanceSquared( position ) ); } /// /// Can this connection spawn networked objects? /// public bool CanSpawnObjects { get => IsHost || (Info?.CanSpawnObjects ?? true); set { Assert.True( Networking.IsHost ); var info = Info; if ( info is null ) return; if ( info.CanSpawnObjects == value ) return; info.CanSpawnObjects = value; info.UpdateStringTable(); } } /// /// Can this connection refresh networked objects that they own? /// public bool CanRefreshObjects { get => IsHost || (Info?.CanRefreshObjects ?? true); set { Assert.True( Networking.IsHost ); var info = Info; if ( info is null ) return; if ( info.CanRefreshObjects == value ) return; info.CanRefreshObjects = value; info.UpdateStringTable(); } } public virtual float Latency => 0; [ActionGraphInclude] public virtual string Name => "Unnammed"; [ActionGraphInclude] public virtual float Time => 0.0f; [ActionGraphInclude] public virtual string Address => "unknown"; [ActionGraphInclude] public virtual bool IsHost => false; /// /// True if this channel is still currently connecting. /// [ActionGraphInclude] public bool IsConnecting => State <= ChannelState.Snapshot; /// /// True if this channel is fully connnected and fully logged on. /// [ActionGraphInclude] public bool IsActive => State == ChannelState.Connected; private int _messagesSent; private int _messagesReceived; /// /// How many messages have been sent to this connection? /// public int MessagesSent { get => Interlocked.CompareExchange( ref _messagesSent, 0, 0 ); internal set => Interlocked.Exchange( ref _messagesSent, value ); } /// /// How many messages have been received from this connection? /// public int MessagesRecieved { get => Interlocked.CompareExchange( ref _messagesReceived, 0, 0 ); internal set => Interlocked.Exchange( ref _messagesReceived, value ); } /// /// Kick this from the server. Only the host can kick clients. /// /// The reason to display to this client. public virtual void Kick( string reason ) { Assert.NotNull( System ); if ( !System.IsHost ) return; if ( string.IsNullOrWhiteSpace( reason ) ) reason = "Kicked"; // TODO: do we immediately close this connection locally // instead of waiting for a disconnection message? SendMessage( new KickMsg { Reason = reason } ); } /// /// Log a message to the console for this connection. /// public void SendLog( LogLevel level, string message ) { if ( Local == this || System is null ) { switch ( level ) { case LogLevel.Info: Log.Info( message ); break; case LogLevel.Warn: Log.Warning( message ); break; case LogLevel.Error: Log.Error( message ); break; } return; } var msg = new LogMsg { Level = (byte)level, Message = message }; SendMessage( msg, NetFlags.Reliable ); } internal void InitializeSystem( NetworkSystem parent ) { System = parent; } internal void SendMessage( InternalMessageType type, T t ) { Assert.NotNull( System ); var msg = ByteStream.Create( 256 ); msg.Write( type ); System.Serialize( t, ref msg ); SendRawMessage( msg ); msg.Dispose(); } /// /// Get stats about this connection such as bandwidth usage and how many packets are being /// sent and received. /// public virtual ConnectionStats Stats => default; /// /// Send a message to this connection. /// public void SendMessage( T t ) { SendMessage( t, NetFlags.Reliable ); } internal void SendMessage( T t, NetFlags flags ) { Assert.NotNull( System ); var msg = ByteStream.Create( 256 ); msg.Write( InternalMessageType.Packed ); System.Serialize( t, ref msg ); SendRawMessage( msg, flags ); msg.Dispose(); } internal virtual void SendRawMessage( ByteStream stream, NetFlags flags = NetFlags.Reliable ) { // Note: this is basically quater of k_cbMaxSteamNetworkingSocketsMessageSizeSend var maxChunkSize = 128 * 1024; var isReliableMessage = (flags & NetFlags.Reliable) != 0; if ( !isReliableMessage || stream.Length < maxChunkSize ) { InternalSend( stream, flags ); return; } // // Split messages into multiple parts, this should hardly ever happen. // var chunkHeader = 32; var chunks = (stream.Length / (float)maxChunkSize).CeilToInt(); Log.Trace( $"splitting {stream.Length} bytes into {chunks} {maxChunkSize}b chunks" ); for ( int i = 0; i < chunks; i++ ) { using ByteStream chunkMessage = ByteStream.Create( maxChunkSize + chunkHeader ); chunkMessage.Write( InternalMessageType.Chunk ); chunkMessage.Write( (uint)i ); chunkMessage.Write( (uint)chunks ); chunkMessage.Write( stream, i * maxChunkSize, maxChunkSize ); Log.Trace( $"Chunk {i + 1} is {chunkMessage.Length}b" ); InternalSend( chunkMessage, flags ); chunkMessage.Dispose(); } } /// /// This is called on a worker thread and should handle any threaded processing of messages. /// internal virtual void ProcessMessagesInThread() { } internal void GetIncomingMessages( NetworkSystem.MessageHandler handler ) { InternalRecv( handler ); } internal void Close( int reasonCode, string reasonString ) { InternalClose( reasonCode, reasonString ); } ChannelState InternalState { get; set; } /// /// Current internal progression of this connection. /// internal ChannelState State { get { return Info?.State ?? InternalState; } set { InternalState = value; var info = Info; if ( info is null ) return; if ( info.State == value ) return; info.State = value; info.UpdateStringTable(); } } internal enum ChannelState { Unconnected, LoadingServerInformation, Welcome, MountVPKs, Snapshot, Connected, } public override string ToString() => Name; /// /// Generate an ID for this connection. This is called by the server to allocate /// the connection an identifier. We're avoiding sequential, allocated ids because /// who needs to deal with that bullshit. /// internal void GenerateConnectionId() { Id = Guid.NewGuid(); } /// /// Update this channel's info. Usually called from the host. /// internal void UpdateFrom( ChannelInfo host ) { Id = host.Id; } /// /// Called once a second. /// internal virtual void Tick( NetworkSystem parent ) { } /// /// The ping of this connection (in milliseconds.) /// [ActionGraphInclude] public float Ping => Info?.Ping ?? 0f; /// /// The server has worked out the round trip time on a connection using the heartbeat. /// We want to keep a sliding window of this timing and use it to predict the latency. /// internal void UpdateRtt( float rtt ) { Info?.UpdatePing( (rtt * 1000f * 0.5f).CeilToInt() ); } ConnectionInfo Info => FindConnectionInfo( Id ) ?? PreInfo; /// /// The connection info before connection is added /// internal ConnectionInfo PreInfo { get; set; } [ActionGraphInclude] public string DisplayName => Info?.DisplayName ?? "Unknown Player"; [ActionGraphInclude] public SteamId SteamId => Info?.SteamId ?? default; /// /// The Id of the party that this user is a part of. This can be used to compare to other users to /// group them into parties. /// [ActionGraphInclude] public SteamId PartyId => Info?.PartyId ?? default; public DateTimeOffset ConnectionTime => Info?.ConnectionTime ?? default; [ActionGraphInclude] public string GetUserData( string key ) => Info?.GetUserData( key ) ?? default; /// /// New, updated UserInfo data arrived. Replace our old data with this. /// internal void UpdateUserData( Dictionary userData ) { Info?.UpdateUserData( userData ); } /// /// Set or update an individual UserInfo data key. /// /// /// internal void SetUserData( string key, string value ) { Info?.SetUserData( key, value ); } }