using Steamworks.Data; using System; namespace Steamworks { public class ConnectionManager { /// /// An optional interface to use instead of deriving /// public IConnectionManager? Interface { get; set; } /// /// The actual connection we're managing /// public Connection Connection; /// /// The last received ConnectionInfo /// public ConnectionInfo ConnectionInfo { get; internal set; } public bool Connected = false; public bool Connecting = true; public string ConnectionName { get => Connection.ConnectionName; set => Connection.ConnectionName = value; } public long UserData { get => Connection.UserData; set => Connection.UserData = value; } public void Close( bool linger = false, int reasonCode = 0, string debugString = "Closing Connection" ) { Connection.Close( linger, reasonCode, debugString ); } public override string ToString() => Connection.ToString(); public virtual void OnConnectionChanged( ConnectionInfo info ) { ConnectionInfo = info; // // Some notes: // - Update state before the callbacks, in case an exception is thrown // - ConnectionState.None happens when a connection is destroyed, even if it was already disconnected (ClosedByPeer / ProblemDetectedLocally) // switch ( info.State ) { case ConnectionState.Connecting: if ( !Connecting && !Connected ) { Connecting = true; OnConnecting( info ); } break; case ConnectionState.Connected: if ( Connecting && !Connected ) { Connecting = false; Connected = true; OnConnected( info ); } break; case ConnectionState.ClosedByPeer: case ConnectionState.ProblemDetectedLocally: case ConnectionState.None: if ( Connecting || Connected ) { Connecting = false; Connected = false; OnDisconnected( info ); } break; } } /// /// We're trying to connect! /// public virtual void OnConnecting( ConnectionInfo info ) { Interface?.OnConnecting( info ); } /// /// Client is connected. They move from connecting to Connections /// public virtual void OnConnected( ConnectionInfo info ) { Interface?.OnConnected( info ); } /// /// The connection has been closed remotely or disconnected locally. Check data.State for details. /// public virtual void OnDisconnected( ConnectionInfo info ) { Interface?.OnDisconnected( info ); } public unsafe int Receive( int bufferSize = 32, bool receiveToEnd = true ) { if (SteamNetworkingSockets.Internal is null) { return 0; } if ( bufferSize < 1 || bufferSize > 256 ) throw new ArgumentOutOfRangeException( nameof( bufferSize ) ); int totalProcessed = 0; NetMsg** messageBuffer = stackalloc NetMsg*[bufferSize]; while ( true ) { int processed = SteamNetworkingSockets.Internal.ReceiveMessagesOnConnection( Connection, new IntPtr( &messageBuffer[0] ), bufferSize ); totalProcessed += processed; try { for ( int i = 0; i < processed; i++ ) { ReceiveMessage( ref messageBuffer[i] ); } } catch { for ( int i = 0; i < processed; i++ ) { if ( messageBuffer[i] != null ) { NetMsg.InternalRelease( messageBuffer[i] ); } } throw; } // // Keep going if receiveToEnd and we filled the buffer // if ( !receiveToEnd || processed < bufferSize ) break; } return totalProcessed; } /// /// Sends a message to multiple connections. /// /// The connections to send the message to. /// The number of connections to send the message to, to allow reusing the connections array. /// Pointer to the message data. /// Size of the message data. /// Flags to control delivery of the message. /// An optional array to hold the results of sending the messages for each connection. public unsafe void SendMessages( Connection[] connections, int connectionCount, IntPtr ptr, int size, SendType sendType = SendType.Reliable, Result[]? results = null ) { if ( connections == null ) throw new ArgumentNullException( nameof( connections ) ); if ( connectionCount < 0 || connectionCount > connections.Length ) throw new ArgumentException( "`connectionCount` must be between 0 and `connections.Length`", nameof( connectionCount ) ); if ( results != null && connectionCount > results.Length ) throw new ArgumentException( "`results` must have at least `connectionCount` entries", nameof( results ) ); if ( connectionCount > 1024 ) // restricting this because we stack allocate based on this value throw new ArgumentOutOfRangeException( nameof( connectionCount ) ); if ( ptr == IntPtr.Zero ) throw new ArgumentNullException( nameof( ptr ) ); if ( size == 0 ) throw new ArgumentException( "`size` cannot be zero", nameof( size ) ); if ( SteamNetworkingSockets.Internal is null ) return; if ( connectionCount == 0 ) return; // SendMessages does not make a copy of the data. We will need to copy because we don't want to force the caller to keep the pointer valid. // 1. We don't want a copy per message. They all refer to the same data. This is the benefit of using Broadcast vs. many sends. // 2. We need to use unmanaged memory. Managed memory may move around and invalidate pointers so it's not an option. // 3. We'll use a reference counter and custom free() function to release this unmanaged memory. var copyPtr = BufferManager.Get( size, connectionCount ); Buffer.MemoryCopy( (void*)ptr, (void*)copyPtr, size, size ); var messages = stackalloc NetMsg*[connectionCount]; var messageNumberOrResults = stackalloc long[results != null ? connectionCount : 0]; for ( var i = 0; i < connectionCount; i++ ) { messages[i] = SteamNetworkingUtils.AllocateMessage(); messages[i]->Connection = connections[i]; messages[i]->Flags = sendType; messages[i]->DataPtr = copyPtr; messages[i]->DataSize = size; messages[i]->FreeDataPtr = BufferManager.FreeFunctionPointer; } SteamNetworkingSockets.Internal.SendMessages( connectionCount, messages, messageNumberOrResults ); if (results == null) return; for ( var i = 0; i < connectionCount; i++ ) { if ( messageNumberOrResults[i] < 0 ) { results[i] = (Result)( -messageNumberOrResults[i] ); } else { results[i] = Result.OK; } } } /// /// Ideally should be using an IntPtr version unless you're being really careful with the byte[] array and /// you're not creating a new one every frame (like using .ToArray()) /// public unsafe void SendMessages( Connection[] connections, int connectionCount, byte[] data, SendType sendType = SendType.Reliable, Result[]? results = null ) { fixed ( byte* ptr = data ) { SendMessages( connections, connectionCount, (IntPtr)ptr, data.Length, sendType, results ); } } /// /// Ideally should be using an IntPtr version unless you're being really careful with the byte[] array and /// you're not creating a new one every frame (like using .ToArray()) /// public unsafe void SendMessages( Connection[] connections, int connectionCount, byte[] data, int offset, int length, SendType sendType = SendType.Reliable, Result[]? results = null ) { fixed ( byte* ptr = data ) { SendMessages( connections, connectionCount, (IntPtr)ptr + offset, length, sendType, results ); } } /// /// This creates a ton of garbage - so don't do anything with this beyond testing! /// public void SendMessages( Connection[] connections, int connectionCount, string str, SendType sendType = SendType.Reliable, Result[]? results = null ) { var bytes = Utility.Utf8NoBom.GetBytes( str ); SendMessages( connections, connectionCount, bytes, sendType, results ); } internal unsafe void ReceiveMessage( ref NetMsg* msg ) { try { OnMessage( msg->DataPtr, msg->DataSize, msg->RecvTime, msg->MessageNumber, msg->Channel ); } finally { // // Releases the message // NetMsg.InternalRelease( msg ); msg = null; } } public virtual void OnMessage( IntPtr data, int size, long messageNum, long recvTime, int channel ) { Interface?.OnMessage( data, size, messageNum, recvTime, channel ); } } }