Files
2025-06-17 16:38:11 +03:00

278 lines
8.6 KiB
C#

using Steamworks.Data;
using System;
namespace Steamworks
{
public class ConnectionManager
{
/// <summary>
/// An optional interface to use instead of deriving
/// </summary>
public IConnectionManager? Interface { get; set; }
/// <summary>
/// The actual connection we're managing
/// </summary>
public Connection Connection;
/// <summary>
/// The last received ConnectionInfo
/// </summary>
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;
}
}
/// <summary>
/// We're trying to connect!
/// </summary>
public virtual void OnConnecting( ConnectionInfo info )
{
Interface?.OnConnecting( info );
}
/// <summary>
/// Client is connected. They move from connecting to Connections
/// </summary>
public virtual void OnConnected( ConnectionInfo info )
{
Interface?.OnConnected( info );
}
/// <summary>
/// The connection has been closed remotely or disconnected locally. Check data.State for details.
/// </summary>
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;
}
/// <summary>
/// Sends a message to multiple connections.
/// </summary>
/// <param name="connections">The connections to send the message to.</param>
/// <param name="connectionCount">The number of connections to send the message to, to allow reusing the connections array.</param>
/// <param name="ptr">Pointer to the message data.</param>
/// <param name="size">Size of the message data.</param>
/// <param name="sendType">Flags to control delivery of the message.</param>
/// <param name="results">An optional array to hold the results of sending the messages for each connection.</param>
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;
}
}
}
/// <summary>
/// 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())
/// </summary>
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 );
}
}
/// <summary>
/// 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())
/// </summary>
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 );
}
}
/// <summary>
/// This creates a ton of garbage - so don't do anything with this beyond testing!
/// </summary>
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 );
}
}
}