using Sandbox.Internal;
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
{
readonly Logger log;
///
/// Are we the owner of this network system? True if we're hosting
/// the server, or we're the current owner of a p2p system.
///
public bool IsHost { get; private set; }
///
/// Has this network system been disconnected?
///
public bool IsDisconnected { get; private set; }
internal bool IsDisconnecting { get; set; }
public LobbyConfig Config { get; internal init; }
public ConnectionInfoManager ConnectionInfo { get; }
public HostStats HostStats { get; private set; }
public string DebugName { get; }
///
/// Whether the host is busy right now. This can be used to determine if
/// the host can be changed.
///
internal bool IsHostBusy
{
get
{
if ( IsHandshaking() )
return false;
return GameSystem?.IsHostBusy ?? true;
}
}
public override string ToString() => DebugName;
public NetworkSystem( string debugName, TypeLibrary library )
{
DebugName = debugName;
TypeLibrary = library;
IsDeveloperHost = Application.IsEditor;
ConnectionInfo = new( this );
log = new( $"NetworkSystem/{debugName}" );
log.Trace( "Initialized" );
InstallHandshakeMessages();
AddHandler( InternalMessageType.TableSnapshot, TableMessage );
AddHandler( InternalMessageType.TableUpdated, TableMessage );
AddHandler( OnTargetedInternalMessage );
AddHandler( OnTargetedMessage );
AddHandler( OnServerCommand );
AddHandler( OnUserInfoUpdate );
AddHandler( OnReceiveHostStats );
AddHandler( OnReconnectMsg );
AddHandler( OnReceiveServerData );
AddHandler( OnReceiveServerName );
AddHandler( OnReceiveMapName );
AddHandler( OnLogMsg );
}
///
/// We have received a log message from another client.
///
void OnLogMsg( LogMsg msg, Connection source, Guid msgId )
{
// Only the host can put shit in our console.
if ( !source.IsHost )
return;
switch ( (LogLevel)msg.Level )
{
case LogLevel.Error:
Log.Error( msg.Message );
break;
case LogLevel.Warn:
Log.Warning( msg.Message );
break;
case LogLevel.Info:
Log.Info( msg.Message );
break;
}
}
///
/// We have received a UserInfo ConVar value update from a client.
///
void OnUserInfoUpdate( UserInfoUpdate msg, Connection source, Guid msgId )
{
if ( !Networking.IsHost )
return;
var command = ConVarSystem.Find( msg.Command );
if ( command is null ) return;
if ( !command.IsUserInfo ) return;
source.SetUserData( msg.Command, msg.Value );
}
///
/// We have received a console command from a client that should be run on the server.
///
void OnServerCommand( ServerCommand msg, Connection source, Guid msgId )
{
// It's not meant for us if we're not the host.
if ( !Networking.IsHost )
return;
var command = ConVarSystem.Find( msg.Command );
if ( command is null ) return;
if ( !command.IsConCommand ) return;
if ( !command.IsServer && !command.IsAdmin ) return;
if ( command.IsCheat && !Game.CheatsEnabled ) return;
var oldCaller = Command.Caller;
Command.Caller = source;
try
{
command.Run( msg.Args );
}
finally
{
Command.Caller = oldCaller;
}
}
///
/// We have received network / performance stats from the server.
///
void OnReceiveHostStats( HostStats data, Connection source, Guid msgId )
{
// We should only receive host stats from the host, obviously.
if ( !source.IsHost )
return;
HostStats = data;
}
///
/// We have received a changed server name.
///
void OnReceiveServerName( ServerNameMsg data, Connection source, Guid msgId )
{
if ( !source.IsHost )
{
Log.Warning( "Got ServerNameMsg - but not from host!" );
return;
}
Networking.ServerName = data.Name;
}
///
/// We have received a changed map name.
///
void OnReceiveMapName( MapNameMsg data, Connection source, Guid msgId )
{
if ( !source.IsHost )
{
Log.Warning( "Got MapNameMsg - but not from host!" );
return;
}
Networking.MapName = data.Name;
}
///
/// We have received changed data from the server.
///
void OnReceiveServerData( ServerDataMsg data, Connection source, Guid msgId )
{
if ( !source.IsHost )
{
Log.Warning( "Got ServerDataMsg - but not from host!" );
return;
}
Networking.SetData( data.Name, data.Value );
}
///
/// The server has told us to reconnect
///
void OnReconnectMsg( ReconnectMsg data, Connection source, Guid msgId )
{
if ( !source.IsHost )
{
Log.Warning( "Got ReconnectMsg - but not from host!" );
return;
}
Networking.StartReconnecting( data );
}
///
/// We have received a message intended for a different connection.
///
void OnTargetedInternalMessage( TargetedInternalMessage data, Connection source, Guid msgId )
{
// A targeted message is only trusted from the host or if the sender is saying he's the sender
if ( data.SenderId != source.Id && !source.IsHost )
{
Log.Warning( $"Connection {source.Id} tried to send a TargetedMessage with invalid SenderId {data.SenderId}" );
source.Kick( "Invalid TargetedMessage.SenderId" ); // If we're the host, kick them
return;
}
// This targeted message is intended for us!
if ( data.TargetId == Guid.Empty || data.TargetId == Connection.Local.Id )
{
var senderConnection = Connection.Find( data.SenderId );
senderConnection ??= source;
var msg = new NetworkMessage
{
Source = senderConnection,
Data = ByteStream.CreateReader( data.Data )
};
try
{
HandleIncomingMessage( msg );
}
catch ( Exception e )
{
Log.Warning( e );
}
msg.Data.Dispose();
}
else
{
// It's not for us, let's have a look to see if we have a connection with this id and forward it to them.
var target = FindConnection( data.TargetId );
target?.SendMessage( data, (NetFlags)data.Flags );
}
}
///
/// We have received a message intended for a different connection.
///
void OnTargetedMessage( TargetedMessage data, Connection source, Guid msgId )
{
// A targeted message is only trusted from the host or if the sender is saying he's the sender
if ( data.SenderId != source.Id && !source.IsHost )
{
Log.Warning( $"Connection {source.Id} tried to send a TargetedMessage with invalid SenderId {data.SenderId}" );
source.Kick( "Invalid TargetedMessage.SenderId" ); // If we're the host, kick them
return;
}
// This targeted message is intended for us!
if ( data.TargetId == Guid.Empty || data.TargetId == Connection.Local.Id )
{
var senderConnection = Connection.Find( data.SenderId );
senderConnection ??= source;
object messageData = data.Message;
if ( messageData is byte[] arr )
{
var stream = ByteStream.CreateReader( arr );
var type = stream.Read();
if ( type == InternalMessageType.Packed )
messageData = TypeLibrary.FromBytes