using Sandbox.Engine; 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 { void InstallHandshakeMessages() { AddHandler( On_Handshake_ServerInfo ); AddHandler( On_Handshake_ClientInfo ); AddHandler( On_Handshake_Welcome ); AddHandler( On_Handshake_RequestSnapshot ); AddHandler( On_Handshake_Snapshot ); AddHandler( On_Handshake_MountedVPKs ); AddHandler( On_Handshake_RequestMountedVPKs ); AddHandler( On_Handshake_ClientReady ); AddHandler( On_Handshake_Activate ); AddHandler( On_Handshake_Restart ); AddHandler( On_Kick ); } /// /// Server says hello to the client. It tells the client some basic information about itself. /// The client can determine here whether they still want to join or not. /// async Task On_Handshake_ServerInfo( ServerInfo msg, Connection source, Guid msgId ) { if ( !source.IsHost ) return; IsDeveloperHost = msg.IsDeveloperHost; source.UpdateFrom( msg.Host ); source.State = Connection.ChannelState.LoadingServerInformation; Connection.Local = new LocalConnection( msg.Assigned.Id ) { HandshakeId = msg.HandshakeId, State = Connection.ChannelState.LoadingServerInformation }; // // Tell the server all about ourselves // var output = UserInfo.Local; if ( !source.OnReceiveServerInfo( ref output, msg ) ) { IGameInstanceDll.Current.Disconnect(); return; } log.Trace( $"Server Id is {source.Id}" ); log.Trace( $"Map Name is {msg.MapName}" ); log.Trace( $"Server Name is {msg.ServerName}" ); log.Trace( $"Engine version is {msg.EngineVersion}" ); log.Trace( $"Game Package is {msg.GamePackage}" ); log.Trace( $"My Client ID is {Connection.Local.Id}" ); // This is a bit of a mess, it needs a good cleaning up. If they have a menu package, then load it first. if ( !string.IsNullOrEmpty( msg.GamePackage ) ) { UpdateLoading( $"Loading {msg.GamePackage}" ); log.Trace( $"Loading menu package.. {msg.GamePackage}" ); var flags = GameLoadingFlags.Remote | GameLoadingFlags.Reload; if ( IsDeveloperHost ) flags |= GameLoadingFlags.Developer; if ( !Application.IsStandalone ) { LaunchArguments.Map = msg.MapPackage; await IGameInstanceDll.Current.LoadGamePackageAsync( msg.GamePackage, flags, default ); } } else { log.Trace( $"No game package - must be a developer" ); } if ( IGameInstanceDll.Current is not null ) { // TypeLibrary was probably rebuilt, keep it up to date TypeLibrary = IGameInstanceDll.Current.TypeLibrary; } foreach ( var (k, v) in msg.ServerData ) { Networking.SetData( k, v ); } Networking.MaxPlayers = msg.MaxPlayers; Networking.ServerName = msg.ServerName; Networking.MapName = msg.MapName; InstallStringTables(); log.Trace( $"Fetching Server Data.." ); // // Tell me what I need // UpdateLoading( $"Fetching Server Data" ); source.SendMessage( output with { HandshakeId = msg.HandshakeId } ); } Task On_Handshake_ClientInfo( UserInfo msg, Connection source, Guid msgId ) { if ( source.IsHost ) return Task.CompletedTask; if ( msg.HandshakeId != source.HandshakeId ) return Task.CompletedTask; if ( source.State != Connection.ChannelState.LoadingServerInformation ) { source.Kick( $"Invalid Handshake State {source.State}" ); Log.Info( $"Kicking {source.DisplayName} [{source.SteamId}] Invalid Handshake State {source.State}" ); return Task.CompletedTask; } if ( !source.OnReceiveUserInfo( msg ) ) return Task.CompletedTask; // // Lobbies and steam network connections are trusted so we can take the display name and Steam Id from them // we shouldn't trust any other type of connection... but local TCP we can let slide. // if ( source is SteamLobbyConnection slob ) { var friend = new Friend( slob.Friend.Id ); msg.SteamId = slob.Friend.Id; msg.DisplayName = friend.Name; } Log.Info( $"{msg.DisplayName} [{msg.SteamId}] is connecting" ); var denialReason = ""; source.PreInfo = new ConnectionInfo( null ) { ConnectionId = source.Id, State = source.State }; source.PreInfo.Update( msg ); if ( GameSystem is not null && !GameSystem.AcceptConnection( source, ref denialReason ) ) { Log.Info( $"Kicking {msg.DisplayName} [{msg.SteamId}] - {denialReason}" ); source.Kick( denialReason ); return Task.CompletedTask; } source.PreInfo = null; source.State = Connection.ChannelState.Welcome; //log.Info( $"Client Name is {data.DisplayName}" ); //log.Info( $"Client SteamId is {data.SteamId}" ); //log.Info( $"Client EngineVersion is {data.EngineVersion}" ); var output = new Welcome(); output.HandshakeId = msg.HandshakeId; foreach ( var table in tables ) { table.SendSnapshot( source ); } // // They're connected now dummy // msg.ConnectionTime = DateTime.UtcNow; // // Make their name unique // var displayName = msg.DisplayName; var index = 2; while ( ConnectionInfo.All.Values.Any( x => string.Equals( x.DisplayName, displayName, StringComparison.OrdinalIgnoreCase ) ) ) { displayName = $"{msg.DisplayName} ({index})"; index++; } msg.DisplayName = displayName; // // Add player info to the manager. This will get sent to all the other players // so this player is part of the game now. // { AddConnection( source, msg ); } // // Tell game this guy has connected. This happens after ConnectionInfo so that // everyone can look up their name etc. // GameSystem?.OnConnected( source ); source.SendMessage( output ); return Task.CompletedTask; } async Task On_Handshake_Welcome( Welcome msg, Connection source, Guid msgId ) { if ( !source.IsHost ) return; if ( Connection.Local is null ) throw new UnauthorizedAccessException(); if ( msg.HandshakeId != Connection.Local.HandshakeId ) return; if ( Connection.Local.State != Connection.ChannelState.LoadingServerInformation ) throw new UnauthorizedAccessException(); Connection.Local.State = Connection.ChannelState.Welcome; log.Trace( $"Welcome!" ); UpdateLoading( $"Loading Network Tables" ); await IGameInstanceDll.Current?.LoadNetworkTables( this ); UpdateLoading( $"Init Game System" ); InitializeGameSystem(); log.Trace( $"Game Network System: {GameSystem}" ); // // Here would be a goodish place to send a bunch of CRC's of the loaded state, so // the server can compare and reject if we're loading assemblies wrong (cheater) // UpdateLoading( "Fetching Snapshot" ); var output = new RequestMountedVPKs { HandshakeId = msg.HandshakeId }; source.SendMessage( output ); } Task On_Handshake_RequestMountedVPKs( RequestMountedVPKs msg, Connection source, Guid msgId ) { if ( source.IsHost ) return Task.CompletedTask; if ( msg.HandshakeId != source.HandshakeId ) return Task.CompletedTask; if ( source.State != Connection.ChannelState.Welcome ) { source.Kick( $"Invalid Handshake State {source.State}" ); Log.Info( $"Kicking {source.DisplayName} [{source.SteamId}] Invalid Handshake State {source.State}" ); return Task.CompletedTask; } var output = new MountedVPKsResponse { HandshakeId = msg.HandshakeId }; GameSystem?.GetMountedVPKs( source, ref output ); source.State = Connection.ChannelState.MountVPKs; source.SendMessage( output ); return Task.CompletedTask; } async Task On_Handshake_MountedVPKs( MountedVPKsResponse msg, Connection source, Guid msgId ) { if ( !source.IsHost ) return; if ( Connection.Local is null ) throw new UnauthorizedAccessException(); if ( msg.HandshakeId != Connection.Local.HandshakeId ) return; if ( Connection.Local.State != Connection.ChannelState.Welcome ) throw new UnauthorizedAccessException(); Connection.Local.State = Connection.ChannelState.MountVPKs; await GameSystem?.MountVPKs( source, msg ); var userInfo = UserInfo.Local; var output = new RequestInitialSnapshot(); output.HandshakeId = msg.HandshakeId; output.UserData = userInfo.UserData; source.SendMessage( output ); } Task On_Handshake_RequestSnapshot( RequestInitialSnapshot msg, Connection source, Guid msgId ) { if ( source.IsHost ) return Task.CompletedTask; if ( msg.HandshakeId != source.HandshakeId ) return Task.CompletedTask; if ( source.State != Connection.ChannelState.MountVPKs ) { source.Kick( $"Invalid Handshake State {source.State}" ); Log.Info( $"Kicking {source.DisplayName} [{source.SteamId}] Invalid Handshake State {source.State}" ); return Task.CompletedTask; } if ( msg.UserData is not null ) { source.UpdateUserData( msg.UserData ); } source.State = Connection.ChannelState.Snapshot; log.Trace( $"[{this}] Requesting a snapshot" ); var snapshot = new SnapshotMsg { GameObjectSystems = [], NetworkObjects = new( 64 ) }; GameSystem?.GetSnapshot( source, ref snapshot ); var output = new InitialSnapshotResponse { HandshakeId = source.HandshakeId, Snapshot = snapshot }; source.SendMessage( output ); return Task.CompletedTask; } async Task On_Handshake_Snapshot( InitialSnapshotResponse msg, Connection source, Guid msgId ) { NetworkDebugSystem.Current?.Record( NetworkDebugSystem.MessageType.Snapshot, msg ); if ( !source.IsHost ) return; if ( Connection.Local is null ) throw new UnauthorizedAccessException(); if ( msg.HandshakeId != Connection.Local.HandshakeId ) return; if ( Connection.Local.State != Connection.ChannelState.MountVPKs ) throw new UnauthorizedAccessException(); Connection.Local.State = Connection.ChannelState.Snapshot; UpdateLoading( "Loading Snapshot" ); Log.Trace( $"[{this}] Got a snapshot" ); // // Spawn the scene, which could also lead to calling OnStart, OnEnable on components // which might create new network instances. So from this point we should be considered // live in the game. // if ( GameSystem is not null ) { await GameSystem.SetSnapshotAsync( msg.Snapshot ); } Log.Trace( $"[{this}] Finished loading snapshot" ); var output = new ClientReady(); output.HandshakeId = msg.HandshakeId; source.SendMessage( output ); } Task On_Handshake_ClientReady( ClientReady msg, Connection source, Guid msgId ) { if ( source.IsHost ) return Task.CompletedTask; if ( msg.HandshakeId != source.HandshakeId ) return Task.CompletedTask; Log.Trace( $"[{this}] Client is ready" ); if ( source.State != Connection.ChannelState.Snapshot ) { source.Kick( $"Invalid Handshake State {source.State}" ); Log.Info( $"Kicking {source.DisplayName} [{source.SteamId}] Invalid Handshake State {source.State}" ); return Task.CompletedTask; } source.State = Connection.ChannelState.Connected; // // Tell game this guy is fully active. // GameSystem?.OnJoined( source ); var output = new Activate(); output.HandshakeId = msg.HandshakeId; source.SendMessage( output ); Log.Info( $"{source.DisplayName} [{source.SteamId}] is connected" ); return Task.CompletedTask; } Task On_Handshake_Restart( RestartHandshakeMsg msg, Connection source, Guid msgId ) { if ( source.IsHost ) return Task.CompletedTask; StartHandshake( source ); return Task.CompletedTask; } Task On_Kick( KickMsg msg, Connection source, Guid msgId ) { if ( !source.IsHost ) { // Conna: only the host can kick us. return Task.CompletedTask; } IGameInstanceDll.Current.Disconnect(); IMenuSystem.ShowServerError( "Disconnected", msg.Reason ); Log.Warning( $"Disconnecting - {msg.Reason}" ); return Task.CompletedTask; } Task On_Handshake_Activate( Activate msg, Connection source, Guid msgId ) { if ( !source.IsHost ) return Task.CompletedTask; if ( Connection.Local is null ) throw new UnauthorizedAccessException(); if ( msg.HandshakeId != Connection.Local.HandshakeId ) return Task.CompletedTask; Log.Trace( $"[{this}] I am spawning into the game!" ); LoadingScreen.IsVisible = false; Connection.Local.State = Connection.ChannelState.Connected; source.State = Connection.ChannelState.Connected; return Task.CompletedTask; } }