using Sandbox.Engine;
using Sandbox.Internal;
using System.IO;
namespace Sandbox.Network;
///
/// A network system is a bunch of connections that people can send messages
/// over. Right now it can be a dedicated server, a listen server, a pure client,
/// or a p2p system.
///
internal partial class NetworkSystem
{
internal delegate void MessageHandler( NetworkMessage msg );
internal delegate void TypedMessageHandler( InternalMessageType type, NetworkMessage msg );
internal delegate void TypdMessageHandler( object t, Connection msg, Guid guid );
internal delegate void TypedMessageHandler( T message, Connection msg, Guid guid );
internal delegate Task TypedMessageHandlerAsync( T message, Connection source, Guid guid );
public ref struct NetworkMessage
{
public Connection Source;
public ByteStream Data;
}
readonly Dictionary messageHandlers = new();
readonly Dictionary typeMessageHandlers = new();
internal void AddHandler( InternalMessageType message, TypedMessageHandler handler )
{
messageHandlers[message] = handler;
}
internal void AddHandler( Action handler )
{
typeMessageHandlers[(typeof( T ))] = ( o, channel, g ) => handler( (T)o, channel, g );
}
internal void AddHandler( Func handler )
{
typeMessageHandlers[(typeof( T ))] = ( o, channel, g ) =>
{
_ = ExceptionWrapAsync( async () => await handler( (T)o, channel, g ) );
};
}
async Task ExceptionWrapAsync( Func method )
{
try
{
await method();
}
catch ( Exception e )
{
IGameInstanceDll.Current?.Disconnect();
Log.Warning( e );
Log.Warning( "Disconnected - Connection has crashed!" );
}
}
///
/// Process any incoming or outgoing messages. This would usually be called on a worker thread unless
/// threaded networking is disabled.
///
internal void ProcessMessagesInThread()
{
foreach ( var socket in Sockets )
{
socket?.ProcessMessagesInThread();
}
if ( Connection is null )
return;
lock ( Connection )
{
Connection.ProcessMessagesInThread();
}
}
void HandleIncomingMessages()
{
Assert.NotNull( sockets, "Socket list is null" ); // should be impossible
// This network system only exists in the game.
using var gameScope = GameSystem?.Push();
foreach ( var socket in sockets )
{
socket?.GetIncomingMessages( HandleIncomingMessage );
}
Connection?.GetIncomingMessages( HandleIncomingMessage );
}
MemoryStream chunkStream;
void HandleIncomingMessage( NetworkMessage msg )
{
// Conna: If this message is not from the host and we're still connecting, ignore it.
if ( !IsHost && !msg.Source.IsHost && Connection.Local.IsConnecting )
{
return;
}
var type = msg.Data.Read();
if ( type == InternalMessageType.HeartbeatPing )
{
OnHeartbeatPingMessage( msg.Data, msg.Source );
return;
}
if ( type is InternalMessageType.DeltaSnapshot
or InternalMessageType.DeltaSnapshotAck
or InternalMessageType.DeltaSnapshotCluster
or InternalMessageType.DeltaSnapshotClusterAck )
{
var dataCount = msg.Data.Read();
var bs = msg.Data.ReadByteStream( dataCount );
OnDeltaSnapshotMessage( type, bs, msg.Source );
bs.Dispose();
return;
}
if ( type == InternalMessageType.ClientTick )
{
OnReceiveClientTick( msg.Data, msg.Source );
return;
}
if ( type == InternalMessageType.SetCullState )
{
OnReceiveCullStateChange( msg.Data, msg.Source );
return;
}
if ( type == InternalMessageType.HeartbeatPong )
{
OnHeartbeatPongMessage( msg.Data, msg.Source );
return;
}
if ( type == InternalMessageType.Chunk )
{
var index = msg.Data.Read();
var total = msg.Data.Read();
if ( index < 0 ) throw new InvalidDataException();
if ( index + 1 > total ) throw new InvalidDataException();
if ( total <= 1 ) throw new InvalidDataException();
if ( total > 1024 ) throw new InvalidDataException();
if ( index == 0 )
{
chunkStream = new MemoryStream();
}
unsafe
{
Log.Trace( $"Reading Chunk {index + 1} of {total} (chunk is {msg.Data.ReadRemaining}b)" );
//
// This can happen when leaving a lobby (usually during connect), and then rejoining it..
// getting packets sennt during previous connection. Maybe need smarter headers to avoid it.
//
Assert.NotNull( chunkStream, $"Reading chunk {index + 1} but not started a chunk!" );
chunkStream.Write( msg.Data.GetRemainingBytes() );
Log.Trace( $"Total chuunk stream is now {chunkStream.Length}b long" );
}
if ( index + 1 == total )
{
var constructedMessage = new NetworkMessage();
constructedMessage.Source = msg.Source;
constructedMessage.Data = ByteStream.CreateReader( chunkStream.ToArray() ); //todo make suck less
chunkStream = null;
HandleIncomingMessage( constructedMessage );
constructedMessage.Data.Dispose();
}
return;
}
var responseTo = Guid.Empty;
var requestGuid = Guid.Empty;
if ( type == InternalMessageType.Request )
{
requestGuid = msg.Data.Read();
type = msg.Data.Read();
}
if ( type == InternalMessageType.Response )
{
responseTo = msg.Data.Read();
type = msg.Data.Read();
}
if ( type == InternalMessageType.Packed )
{
object obj = default;
//
// This can error if we're getting a message containing types that are
// defined in an assemblly we didn't recieve yet (because we're connecting)
// so just ignore these exceptions, but output an error for now so we know it's happening.
//
try
{
obj = TypeLibrary.FromBytes