//========= Copyright 1996-2005, Valve Corporation, All rights reserved. ============// // // Purpose: // // $NoKeywords: $ // //=============================================================================// // baseserver.cpp: implementation of the CBaseServer class. // ////////////////////////////////////////////////////////////////////// #if defined(_WIN32) && !defined(_X360) #include "winlite.h" // FILETIME #elif defined(OSX) || defined(CYGWIN) #include #include #include #include #elif defined(LINUX) #include #include #include // for HZ #include #elif defined(_X360) #elif defined(_PS3) #else #error "Includes for CPU usage calcs here" #endif #include "filesystem_engine.h" #include "baseserver.h" #include "hltvserver.h" #include "sysexternal.h" #include "quakedef.h" #include "host.h" #include "netmessages.h" #include "master.h" #include "sys.h" #include "framesnapshot.h" #include "sv_packedentities.h" #include "dt_send_eng.h" #include "dt_recv_eng.h" #include "networkstringtable.h" #include "sys_dll.h" #include "host_cmd.h" #include "sv_steamauth.h" #include "SteamUserIDValidation.h" #include #include #include #include #include #include #include #include #include "tier0/icommandline.h" #include "sv_steamauth.h" #include "sv_ipratelimit.h" #include "cl_steamauth.h" #include "fmtstr.h" #if defined( _X360 ) #include "xbox/xbox_win32stubs.h" #endif #include "mathlib/IceKey.H" #include "matchmaking/imatchframework.h" #include "tier2/tier2.h" #include "fmtstr.h" #include "sv_plugin.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" CThreadFastMutex g_svInstanceBaselineMutex; // BUGBUG: JAY: Leaving this here for some of the matchmaking code. I don't want to delete the code or enable it // in other games yet. (came over in the merge and will be rationalized later) #define IsLeft4Dead() false // Give new data to Steam's master server updater every N seconds. // This is NOT how often packets are sent to master servers, only how often the // game server talks to Steam's master server updater (which is on the game server's // machine, not the Steam servers). #define MASTER_SERVER_UPDATE_INTERVAL 2.0 // Steam has a matching one in matchmakingtypes.h #define MAX_TAG_STRING_LENGTH 128 bool g_bSteamMasterHeartbeatsEnabled = false; int SortServerTags( char* const *p1, char* const *p2 ) { return ( Q_strcmp( *p1, *p2 ) > 0 ); } struct NoReentry_t { NoReentry_t( int *pn ) : m_pn( pn ) { ++ *m_pn; } ~NoReentry_t() { -- *m_pn; } int *m_pn; }; static void ServerTagsCleanUp( void ) { static int s_nNoReentry = 0; if ( s_nNoReentry ) return; NoReentry_t noReentry( &s_nNoReentry ); CUtlVector TagList; ConVarRef sv_tags( "sv_tags" ); if ( sv_tags.IsValid() ) { int i; char tmptags[MAX_TAG_STRING_LENGTH]; tmptags[0] = '\0'; V_SplitString( sv_tags.GetString(), ",", TagList ); // make a pass on the tags to eliminate preceding whitespace and empty tags for ( i = 0; i < TagList.Count(); i++ ) { if ( i > 0 ) { Q_strncat( tmptags, ",", MAX_TAG_STRING_LENGTH ); } char *pChar = TagList[i]; while ( *pChar && *pChar == ' ' ) { pChar++; } // make sure we don't have an empty string (all spaces or ,,) if ( *pChar ) { Q_strncat( tmptags, pChar, MAX_TAG_STRING_LENGTH ); } } // reset our lists and sort the tags TagList.PurgeAndDeleteElements(); V_SplitString( tmptags, ",", TagList ); TagList.Sort( SortServerTags ); tmptags[0] = '\0'; // create our new, sorted list of tags for ( i = 0; i < TagList.Count(); i++ ) { if ( i > 0 ) { Q_strncat( tmptags, ",", MAX_TAG_STRING_LENGTH ); } Q_strncat( tmptags, TagList[i], MAX_TAG_STRING_LENGTH ); } // set our convar and purge our list if ( Q_strcmp( tmptags, sv_tags.GetString() ) ) { sv_tags.SetValue( tmptags ); } TagList.PurgeAndDeleteElements(); } } static void SvTagsChangeCallback( IConVar *pConVar, const char *pOldValue, float flOldValue ) { sv.UpdateGameData(); if ( sv.IsActive() ) { Cbuf_AddText( CBUF_SERVER, "heartbeat\n" ); } ServerTagsCleanUp(); } static void SvGameDataChangeCallback( IConVar *pConVar, const char *pOldValue, float flOldValue ) { // TODO: sv.UpdateGameData(); if ( sv.IsActive() ) { Cbuf_AddText( CBUF_SERVER, "heartbeat\n" ); } } extern ConVar sv_search_key; extern ConVar sv_lan; extern ConVar cl_hideserverip; ConVar sv_region( "sv_region","-1", FCVAR_NONE | FCVAR_RELEASE, "The region of the world to report this server in." ); static ConVar sv_instancebaselines( "sv_instancebaselines", "1", FCVAR_DEVELOPMENTONLY, "Enable instanced baselines. Saves network overhead." ); static ConVar sv_stats( "sv_stats", "1", 0, "Collect CPU usage stats" ); static ConVar sv_enableoldqueries( "sv_enableoldqueries", "0", 0, "Enable support for old style (HL1) server queries" ); static ConVar sv_reservation_tickrate_adjustment( "sv_reservation_tickrate_adjustment", "0", FCVAR_RELEASE, "Adjust server tickrate upon reservation" ); static void SvPasswordChangeCallback( IConVar *pConVar, const char *pOldValue, float flOldValue ) { ConVarRef cvref( pConVar ); bool bOldPassword = ( pOldValue && pOldValue[0] && Q_stricmp( pOldValue, "none" ) ); char const *pNewValue = cvref.GetString(); bool bNewPassword = ( pNewValue && pNewValue[0] && Q_stricmp( pNewValue, "none" ) ); if ( ( sv.GetNumHumanPlayers() > 0 ) || sv.IsReserved() ) { if ( !bOldPassword && bNewPassword ) { Msg( "Cannot require sv_password when server is already reserved or clients connected!\n" ); cvref.SetValue( "" ); } } sv.OnPasswordChanged(); } static ConVar sv_password( "sv_password", "", FCVAR_NOTIFY | FCVAR_PROTECTED | FCVAR_DONTRECORD | FCVAR_RELEASE, "Server password for entry into multiplayer games", SvPasswordChangeCallback ); ConVar sv_tags( "sv_tags", "", FCVAR_NOTIFY | FCVAR_RELEASE, "Server tags. Used to provide extra information to clients when they're browsing for servers. Separate tags with a comma.", SvTagsChangeCallback ); ConVar sv_visiblemaxplayers( "sv_visiblemaxplayers", "-1", FCVAR_RELEASE, "Overrides the max players reported to prospective clients" ); ConVar sv_alternateticks( "sv_alternateticks", ( IsX360() ) ? "1" : "0", FCVAR_RELEASE, "If set, server only simulates entities on even numbered ticks.\n" ); ConVar sv_allow_wait_command( "sv_allow_wait_command", "1", FCVAR_REPLICATED | FCVAR_RELEASE, "Allow or disallow the wait command on clients connected to this server." ); #if !defined( CSTRIKE15 ) // We are switching CStrike to always have lobbies associated with servers for community matchmaking ConVar sv_allow_lobby_connect_only( "sv_allow_lobby_connect_only", "1", FCVAR_RELEASE, "If set, players may only join this server from matchmaking lobby, may not connect directly." ); #endif static ConVar sv_reservation_timeout( "sv_reservation_timeout", "45", FCVAR_RELEASE, "Time in seconds before lobby reservation expires.", true, 5.0f, true, 180.0f ); static ConVar sv_reservation_grace( "sv_reservation_grace", "5", 0, "Time in seconds given for a lobby reservation.", true, 3.0f, true, 30.0f ); ConVar sv_steamgroup( "sv_steamgroup", "", FCVAR_NOTIFY | FCVAR_RELEASE, "The ID of the steam group that this server belongs to. You can find your group's ID on the admin profile page in the steam community.", SvGameDataChangeCallback ); ConVar sv_steamgroup_exclusive( "sv_steamgroup_exclusive", "0", FCVAR_RELEASE, "If set, only members of Steam group will be able to join the server when it's empty, public people will be able to join the server only if it has players." ); static void SvMmQueueReservationChanged( IConVar *pConVar, const char *pOldValue, float flOldValue ) { if ( serverGameDLL ) serverGameDLL->UpdateGCInformation(); } ConVar sv_mmqueue_reservation( "sv_mmqueue_reservation", "", FCVAR_DEVELOPMENTONLY | FCVAR_DONTRECORD, "Server queue reservation", SvMmQueueReservationChanged ); ConVar sv_mmqueue_reservation_timeout( "sv_mmqueue_reservation_timeout", "21", FCVAR_DEVELOPMENTONLY, "Time in seconds before mmqueue reservation expires.", true, 5.0f, true, 180.0f ); ConVar sv_mmqueue_reservation_extended_timeout( "sv_mmqueue_reservation_extended_timeout", "21", FCVAR_DEVELOPMENTONLY, "Extended time in seconds before mmqueue reservation expires.", true, 5.0f, true, 180.0f ); extern CNetworkStringTableContainer *networkStringTableContainerServer; extern ConVar sv_stressbots; int g_CurGameServerID = 1; static void SetMasterServerKeyValue( ISteamGameServer *pGameServer, IConVar *pConVar ) { ConVarRef var( pConVar ); // For protected cvars, don't send the string if ( var.IsFlagSet( FCVAR_PROTECTED ) ) { // If it has a value string and the string is not "none" if ( ( strlen( var.GetString() ) > 0 ) && stricmp( var.GetString(), "none" ) ) { pGameServer->SetKeyValue( var.GetName(), "1" ); } else { pGameServer->SetKeyValue( var.GetName(), "0" ); } } else { pGameServer->SetKeyValue( var.GetName(), var.GetString() ); } if ( Steam3Server().BIsActive() ) { sv.RecalculateTags(); } } static KeyValues *g_pKVrulesConvars = NULL; static void ServerNotifyVarChangeCallback( IConVar *pConVar, const char *pOldValue, float flOldValue ) { if ( !pConVar->IsFlagSet( FCVAR_NOTIFY ) ) return; if ( !g_pKVrulesConvars->GetBool( pConVar->GetName() ) ) return; ISteamGameServer *pGameServer = Steam3Server().SteamGameServer(); if ( !pGameServer ) { // This will force it to send all the rules whenever the master server updater is there. sv.SetMasterServerRulesDirty(); return; } SetMasterServerKeyValue( pGameServer, pConVar ); } ////////////////////////////////////////////////////////////////////// // Construction/Destruction ////////////////////////////////////////////////////////////////////// CBaseServer::CBaseServer() : m_ServerQueryChallenges( 0, 1024 ), // start with 1K of entries, and alloc in 1K chunks m_BaselineHandles( DefLessFunc( int ) ), m_flFlagForSteamIDReuseAfterShutdownTime( 0 ) { // Just get a unique ID to talk to the steam master server updater. m_bRestartOnLevelChange = false; m_StringTables = NULL; m_pInstanceBaselineTable = NULL; m_pLightStyleTable = NULL; m_pUserInfoTable = NULL; m_pServerStartupTable = NULL; m_pDownloadableFileTable = NULL; m_fLastCPUCheckTime = 0; m_fStartTime = 0; m_fCPUPercent = 0; m_Socket = NS_SERVER; m_nTickCount = 0; m_szMapname[0] = 0; m_szBaseMapname[0] = 0; m_szMapGroupName[0] = 0; m_szSkyname[0] = 0; m_Password[0] = 0; worldmapCRC = 0; clientDllCRC = 0; stringTableCRC = 0; serverclasses = serverclassbits = 0; m_nMaxclients = m_nSpawnCount = 0; m_flTickInterval = 0.03; m_flTimescale = 1.0f; m_nUserid = 0; m_bIsDedicated = false; m_bIsDedicatedForXbox = false; m_bIsDedicatedForPS3 = false; m_fCPUPercent = 0; m_fLastCPUCheckTime = 0; m_bMasterServerRulesDirty = true; m_flLastMasterServerUpdateTime = 0; m_nReservationCookie = 0; m_pnReservationCookieSession = NULL; m_flReservationExpiryTime = -1.0f; m_flTimeLastClientLeft = -1.0f; m_numGameSlots = 0; m_flTimeReservationGraceStarted = -1.0f; m_GameDataVersion = 0; m_nMatchId = 0; } CBaseServer::~CBaseServer() { ClearBaselineHandles(); } /* ================ SV_CheckChallenge Make sure connecting client is not spoofing ================ */ bool CBaseServer::CheckChallengeNr( const ns_address &adr, int nChallengeValue ) { // See if the challenge is valid // Don't care if it is a local address. if ( adr.IsLoopback() ) return true; // X360TBD: network if ( IsX360() || IsDedicatedForXbox() ) return true; for (int i=0 ; i ( m_ServerQueryChallenges[i].time+ CHALLENGE_LIFETIME) ) // allow challenge values to last for 1 hour { m_ServerQueryChallenges.FastRemove(i); ConMsg( "Old challenge from %s.\n", ns_address_render( adr ).String() ); return false; } return true; } // clean up any old entries if ( net_time > ( m_ServerQueryChallenges[i].time+ CHALLENGE_LIFETIME) ) { m_ServerQueryChallenges.FastRemove(i); i--; // backup one as we just shifted the whole vector back by the deleted element } } if ( nChallengeValue != -1 ) { ConDMsg( "No challenge from %s.\n", ns_address_render( adr ).String() ); // this is a common message } return false; } bool CBaseServer::CanAcceptChallengesFrom( const ns_address &adrFrom ) const { // check timeout if ( m_flTimeReservationGraceStarted < 0 ) return true; if ( ( net_time - m_flTimeReservationGraceStarted ) > sv_reservation_grace.GetFloat() ) return true; // otherwise can only accept from a single address return adrFrom.CompareAdr( m_adrReservationGraceStarted ); } const char *CBaseServer::GetPassword() const { const char *password = sv_password.GetString(); // if password is empty or "none", return NULL if ( !password[0] || !Q_stricmp(password, "none" ) ) { return NULL; } return password; } void CBaseServer::SetPassword(const char *password) { if ( password != NULL ) { Q_strncpy( m_Password, password, sizeof(m_Password) ); } else { m_Password[0] = 0; // clear password } } int CBaseServer::GetNextUserID() { // Note: we'll usually exit on the first pass of this loop.. for ( int i=0; i < m_Clients.Count()+1; i++ ) { int nTestID = (m_nUserid + i + 1) % SHRT_MAX; // Make sure no client has this user ID. int iClient; for ( iClient=0; iClient < m_Clients.Count(); iClient++ ) { if ( m_Clients[iClient]->GetUserID() == nTestID ) break; } // Ok, no client has this ID, so return it. if ( iClient == m_Clients.Count() ) return nTestID; } Assert( !"GetNextUserID: can't find a unique ID." ); return m_nUserid + 1; } bool CBaseServer::IsSinglePlayerGame() const { #if !defined( DEDICATED ) if ( sv.IsDedicated() ) { return false; } if ( m_nMaxclients <= 1 ) return true; #if !defined( PORTAL2 ) && !defined( CSTRIKE15 ) // // Portal 2 offline splitscreen games should NOT be paused // transition movies during loading rely on player think functions // See bugbait 81253: https://bugbait.valvesoftware.com/show_bug.cgi?id=81253 // if ( IMatchSession *pIMatchSession = g_pMatchFramework->GetMatchSession() ) { if ( KeyValues *pSettings = pIMatchSession->GetSessionSettings() ) { char const *szNetwork = pSettings->GetString( "system/network", "" ); if ( szNetwork && !Q_stricmp( szNetwork, "offline" ) ) return true; } } #endif #endif return false; } /* ================ SV_ConnectClient Initializes a CSVClient for a new net connection. This will only be called once for a player each game, not once for each level change. ================ */ IClient *CBaseServer::ConnectClient ( const ns_address &adr, int protocol, int challenge, int authProtocol, const char *name, const char *password, const char *hashedCDkey, int cdKeyLen, CUtlVector< CCLCMsg_SplitPlayerConnect_t * > & splitScreenClients, bool isClientLowViolence, CrossPlayPlatform_t clientPlatform, const byte *pbEncryptionKey, int nEncryptionKeyIndex ) { ns_address_render sAdr( adr ); #ifdef IHV_DEMO if ( !adr.IsLoopback() ) { Warning( "This demo version only works as a listen server. Ignoring connection request from %s\n", adr.ToString() ); return NULL; } else { DevMsg( "IHV Demo Version - Connected to loopback client.\n"); } #endif COM_TimestampedLog( "CBaseServer::ConnectClient" ); if ( !IsActive() ) { DevMsg( "Server not active, ignoring %s\n", sAdr.String() ); return NULL; } if ( !name || !password || !hashedCDkey ) { DevMsg( "Bad auth data from %s\n", sAdr.String() ); return NULL; } // Make sure protocols match up if ( !CheckProtocol( adr, protocol ) ) { DevMsg( "Protocol error from %s\n", sAdr.String() ); return NULL; } if ( !CheckChallengeNr( adr, challenge ) ) { RejectConnection( adr, "Bad challenge.\n"); return NULL; } bool bIsLocalConnection = adr.IsLocalhost() || adr.IsLoopback(); #ifndef NO_STEAM if ( IsExclusiveToLobbyConnections() && !IsReserved() && !bIsLocalConnection ) { RejectConnection( adr, "Server only accepting connections from game lobby %s %d.\n", sAdr.String(), challenge ); return NULL; } #endif // Listen server level background map is always a single // player map, don't allow shenanigans or mayhem. // Also, if the user started a "single player" campaign, don't let anyone else join if ( !IsDedicated() && !bIsLocalConnection ) { if ( sv.IsLevelMainMenuBackground() ) { RejectConnection( adr, "#Valve_Reject_Background_Map" ); return NULL; } if ( IsSinglePlayerGame() ) { RejectConnection( adr, "#Valve_Reject_Single_Player" ); return NULL; } if ( ShouldHideServer() ) { // Right now, hidden means commentary, "solo" or background map (l4d), the former of which are covered above. RejectConnection( adr, "#Valve_Reject_Hidden_Game" ); return NULL; } } // SourceTV checks password & restrictions later once we know // if its a normal spectator client or a relay proxy if ( !IsHLTV() && !IsReplay() ) { #ifndef NO_STEAM // LAN servers restrict to class b IP addresses if ( !CheckIPRestrictions( adr, authProtocol ) ) { RejectConnection( adr, "#Valve_Reject_LAN_Game"); return NULL; } #endif if ( !CheckPassword( adr, password, name ) ) { // failed ConMsg ( "%s: password failed.\n", sAdr.String() ); // Special rejection handler. RejectConnection( adr, "#Valve_Reject_Bad_Password" ); return NULL; } } if ( m_numGameSlots ) { int numSlotsRequested = splitScreenClients.Count(); if ( GetNumClients() - GetNumFakeClients() + numSlotsRequested > m_numGameSlots ) { bool bClientReconnecting = false; for ( int slot = 0 ; slot < m_Clients.Count() ; slot++ ) { CBaseClient *client = m_Clients[slot]; if ( client->IsFakeClient() ) continue; if ( client->IsConnected() && adr.CompareAdr ( client->m_NetChannel->GetRemoteAddress() ) ) { ConMsg ( "%s:reconnect\n", sAdr.String() ); bClientReconnecting = true; break; } } if ( ( numSlotsRequested == 1 ) && bClientReconnecting ) { // Allow client to proceed with the connection normally and take over their self } else { RejectConnection( adr, "#Valve_Reject_Server_Full" ); return NULL; // cannot accept, exceeding game mode slot count } } } // Let a GOTV server redirect the client immediately without further // handshake and netchannel work if the redirect is known to be required // at this point. ns_address netAdrRedirect; if ( GetRedirectAddressForConnectClient( adr, splitScreenClients, &netAdrRedirect ) ) { if ( netAdrRedirect.IsValid() ) RejectConnection( adr, "ConnectRedirectAddress:%s\n", ns_address_render( netAdrRedirect ).String() ); return NULL; } COM_TimestampedLog( "CBaseServer::ConnectClient: GetFreeClient" ); CBaseClient *client = GetFreeClient( adr ); if ( !client ) { RejectConnection( adr, "#Valve_Reject_Server_Full" ); return NULL; // no free slot found } int nNextUserID = GetNextUserID(); if ( !CheckChallengeType( client, nNextUserID, adr, authProtocol, hashedCDkey, cdKeyLen ) ) // we use the client pointer to track steam requests { return NULL; } #ifndef _HLTVTEST #ifndef _REPLAYTEST if ( !FinishCertificateCheck( adr, authProtocol, hashedCDkey ) ) { return NULL; } #endif #endif // Make sure client user info carries correct cookie bool bValidatedUserInfo = false; if ( GetReservationCookie() != 0u ) { if ( splitScreenClients.Count() ) { const CMsg_CVars& convars = splitScreenClients[0]->convars(); for ( int i = 0; i< convars.cvars_size(); ++i ) { const char *cvname = NetMsgGetCVarUsingDictionary( convars.cvars(i) ); const char *value = convars.cvars(i).value().c_str(); if ( stricmp( cvname, "cl_session" ) ) continue; uint64 uid; if ( sscanf( value, "$%llx", &uid ) != 1 ) { Warning( "failed to parse session id %s\n", value ); } else { if ( uid == GetReservationCookie() ) bValidatedUserInfo = true; else Warning( "mismatching cookie from %s, client %llx, server %llx!\n", sAdr.String(), uid, GetReservationCookie() ); } } } } else { bValidatedUserInfo = true; } // If we failed to validate user info - reject if ( !bValidatedUserInfo ) { RejectConnection( adr, "Invalid user info.\n" ); return NULL; } // Final validation chance by server.dll if ( char const *szGameServerError = serverGameDLL->ClientConnectionValidatePreNetChan( ( this == &sv ), sAdr.String(), authProtocol, client->m_SteamID.ConvertToUint64() ) ) { RejectConnection( adr, "%s", szGameServerError ); return NULL; } COM_TimestampedLog( "CBaseServer::ConnectClient: NET_CreateNetChannel" ); // Fix the empty name from FCVAR_USERINFO data // for ( int k = 0; !name[ 0 ] && ( k < splitScreenPlayers.Count() ); ++k ) char const *pchClientConnectionName = name; if ( !pchClientConnectionName[ 0 ] && splitScreenClients.Count() ) { for ( int iCvar = 0; iCvar < splitScreenClients[ 0 ]->convars().cvars().size(); ++iCvar ) { CMsg_CVars::CVar const &rCvarInfo = splitScreenClients[ 0 ]->convars().cvars( iCvar ); if ( !V_strcmp( "name", NetMsgGetCVarUsingDictionary( rCvarInfo ) ) ) { pchClientConnectionName = rCvarInfo.value().c_str(); break; } } } // Use an override for connection name pchClientConnectionName = serverGameClients->ClientNameHandler( client->m_SteamID.ConvertToUint64(), pchClientConnectionName ); // create network channel // Encryption keys for the client must have been received previously INetChannel * netchan = NET_CreateNetChannel( m_Socket, &adr, pchClientConnectionName, client, pbEncryptionKey, false ); if ( !netchan ) { RejectConnection( adr, "Failed to create net channel.\n"); return NULL; } // setup netchannl settings netchan->SetChallengeNr( challenge ); COM_TimestampedLog( "CBaseServer::ConnectClient: client->Connect" ); // make sure client is reset and clear client->Connect( pchClientConnectionName, nNextUserID, netchan, false, // real client clientPlatform, (splitScreenClients.Count() > 0) ? &splitScreenClients[0]->convars() : NULL ); // userinfo if supplied client->m_bLowViolence = isClientLowViolence; m_nUserid = nNextUserID; // Will get reset from userinfo, but this value comes from sv_updaterate ( the default ) client->m_fSnapshotInterval = 1.0f/20.0f; client->m_fNextMessageTime = net_time + client->m_fSnapshotInterval; // Force a full delta update on first packet. client->m_nDeltaTick = -1; client->m_nSignonTick = 0; client->m_nStringTableAckTick = 0; client->m_pLastSnapshot = NULL; // Tell client connection worked, now use netchannels NET_OutOfBandPrintf ( m_Socket, adr, "%c.%08X.0000.0000.0000.0000.", S2C_CONNECTION, nEncryptionKeyIndex ); // Set up client structure. if ( authProtocol == PROTOCOL_HASHEDCDKEY ) { // use hased CD key as player GUID Q_strncpy ( client->m_GUID, hashedCDkey, SIGNED_GUID_LEN ); client->m_GUID[SIGNED_GUID_LEN] = '\0'; } else if ( authProtocol == PROTOCOL_STEAM ) { // StartSteamValidation() above initialized the clients networkid } //now process the split screen clients who came in with this client for( int playerIndex = 1; playerIndex < splitScreenClients.Count(); ++ playerIndex ) { ConMsg( "Processing Split Screen connection packet.\n" ); client->CLCMsg_SplitPlayerConnect( *splitScreenClients[ playerIndex ] ); } if ( netchan && !netchan->IsLoopback() ) { ConMsg("Client \"%s\" connected (%s).\n", client->GetClientName(), cl_hideserverip.GetInt()>0 ? "