//========= Copyright 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(POSIX) #include /* #include #include // for HZ */ #include #include #elif defined(_X360) #else #error "Includes for CPU usage calcs here" #endif #include "filesystem_engine.h" #include "baseserver.h" #include "sysexternal.h" #include "quakedef.h" #include "host.h" #include "netmessages.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 #include #include #include #include #include #include #include #include "tier0/icommandline.h" #include "sv_steamauth.h" #include "tier0/vcrmode.h" #include "sv_ipratelimit.h" #include "cl_steamauth.h" #include "sv_filter.h" #if defined( _X360 ) #include "xbox/xbox_win32stubs.h" #endif // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" CThreadFastMutex g_svInstanceBaselineMutex; extern CGlobalVars g_ServerGlobalVariables; static ConVar sv_max_queries_sec( "sv_max_queries_sec", "3.0", 0, "Maximum queries per second to respond to from a single IP address." ); static ConVar sv_max_queries_window( "sv_max_queries_window", "30", 0, "Window over which to average queries per second averages." ); static ConVar sv_max_queries_sec_global( "sv_max_queries_sec_global", "3000", 0, "Maximum queries per second to respond to from anywhere." ); static ConVar sv_max_connects_sec( "sv_max_connects_sec", "2.0", 0, "Maximum connections per second to respond to from a single IP address." ); static ConVar sv_max_connects_window( "sv_max_connects_window", "4", 0, "Window over which to average connections per second averages." ); // This defaults to zero so that somebody spamming the server with packets cannot lock out other clients. static ConVar sv_max_connects_sec_global( "sv_max_connects_sec_global", "0", 0, "Maximum connections per second to respond to from anywhere." ); static CIPRateLimit s_queryRateChecker( &sv_max_queries_sec, &sv_max_queries_window, &sv_max_queries_sec_global ); static CIPRateLimit s_connectRateChecker( &sv_max_connects_sec, &sv_max_connects_window, &sv_max_connects_sec_global ); // 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 int SortServerTags( char* const *p1, char* const *p2 ) { return ( Q_strcmp( *p1, *p2 ) > 0 ); } static void ServerTagsCleanUp( void ) { 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 sv_tags.SetValue( tmptags ); TagList.PurgeAndDeleteElements(); } } static void SvTagsChangeCallback( IConVar *pConVar, const char *pOldValue, float flOldValue ) { // We're going to modify the sv_tags convar here, which will cause this to be called again. Prevent recursion. static bool bTagsChangeCallback = false; if ( bTagsChangeCallback ) return; bTagsChangeCallback = true; ServerTagsCleanUp(); ConVarRef var( pConVar ); if ( Steam3Server().SteamGameServer() ) { Steam3Server().SteamGameServer()->SetGameTags( var.GetString() ); } bTagsChangeCallback = false; } ConVar sv_region( "sv_region","-1", FCVAR_NONE, "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_password( "sv_password", "", FCVAR_NOTIFY | FCVAR_PROTECTED | FCVAR_DONTRECORD, "Server password for entry into multiplayer games" ); ConVar sv_tags( "sv_tags", "", FCVAR_NOTIFY, "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", 0, "Overrides the max players reported to prospective clients" ); ConVar sv_alternateticks( "sv_alternateticks", ( IsX360() ) ? "1" : "0", FCVAR_SPONLY, "If set, server only simulates entities on even numbered ticks.\n" ); ConVar sv_allow_wait_command( "sv_allow_wait_command", "1", FCVAR_REPLICATED, "Allow or disallow the wait command on clients connected to this server." ); ConVar sv_allow_color_correction( "sv_allow_color_correction", "1", FCVAR_REPLICATED, "Allow or disallow clients to use color correction on this server." ); extern CNetworkStringTableContainer *networkStringTableContainerServer; extern ConVar sv_stressbots; int g_CurGameServerID = 1; // #define ALLOW_DEBUG_DEDICATED_SERVER_OUTSIDE_STEAM bool AllowDebugDedicatedServerOutsideSteam() { #if defined( ALLOW_DEBUG_DEDICATED_SERVER_OUTSIDE_STEAM ) return true; #else return false; #endif } static void SetMasterServerKeyValue( ISteamGameServer *pUpdater, 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" ) ) { pUpdater->SetKeyValue( var.GetName(), "1" ); } else { pUpdater->SetKeyValue( var.GetName(), "0" ); } } else { pUpdater->SetKeyValue( var.GetName(), var.GetString() ); } if ( Steam3Server().BIsActive() ) { sv.RecalculateTags(); } } static void ServerNotifyVarChangeCallback( IConVar *pConVar, const char *pOldValue, float flOldValue ) { if ( !pConVar->IsFlagSet( FCVAR_NOTIFY ) ) return; ISteamGameServer *pUpdater = Steam3Server().SteamGameServer(); if ( !pUpdater ) { // This will force it to send all the rules whenever the master server updater is there. sv.SetMasterServerRulesDirty(); return; } SetMasterServerKeyValue( pUpdater, pConVar ); } ////////////////////////////////////////////////////////////////////// // Construction/Destruction ////////////////////////////////////////////////////////////////////// CBaseServer::CBaseServer() { // 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_szSkyname[0] = 0; m_Password[0] = 0; V_memset( worldmapMD5.bits, 0, MD5_DIGEST_LENGTH ); serverclasses = serverclassbits = 0; m_nMaxclients = m_nSpawnCount = 0; m_flTickInterval = 0.03; m_nUserid = 0; m_nNumConnections = 0; m_bIsDedicated = false; m_fCPUPercent = 0; m_fStartTime = 0; m_fLastCPUCheckTime = 0; m_bMasterServerRulesDirty = true; m_flLastMasterServerUpdateTime = 0; m_CurrentRandomNonce = 0; m_LastRandomNonce = 0; m_flLastRandomNumberGenerationTime = -3.0f; // force it to calc first frame m_bReportNewFakeClients = true; m_flPausedTimeEnd = -1.f; } CBaseServer::~CBaseServer() { } /* ================ SV_CheckChallenge Make sure connecting client is not spoofing ================ */ bool CBaseServer::CheckChallengeNr( netadr_t &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() ) return true; uint64 challenge = ((uint64)adr.GetIPNetworkByteOrder() << 32) + m_CurrentRandomNonce; CRC32_t hash; CRC32_Init( &hash ); CRC32_ProcessBuffer( &hash, &challenge, sizeof(challenge) ); CRC32_Final( &hash ); if ( (int)hash == nChallengeValue ) return true; // try with the old random nonce challenge &= 0xffffffff00000000ull; challenge += m_LastRandomNonce; hash = 0; CRC32_Init( &hash ); CRC32_ProcessBuffer( &hash, &challenge, sizeof(challenge) ); CRC32_Final( &hash ); if ( (int)hash == nChallengeValue ) return true; return false; } 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 } } #define MAX_REUSE_PER_IP 5 // 5 outstanding connect request within timeout window, to account for NATs /* ================ CheckIPConnectionReuse Determine if this IP requesting the connect is connecting too often ================ */ bool CBaseServer::CheckIPConnectionReuse( netadr_t &adr ) { int nSimultaneouslyConnections = 0; for ( int slot = 0 ; slot < m_Clients.Count() ; slot++ ) { CBaseClient *client = m_Clients[slot]; // if the user is connected but not fully in AND the addr's match if ( client->IsConnected() && !client->IsActive() && !client->IsFakeClient() && adr.CompareAdr ( client->m_NetChannel->GetRemoteAddress(), true ) ) { nSimultaneouslyConnections++; } } if ( nSimultaneouslyConnections > MAX_REUSE_PER_IP ) { Msg ("Too many connect packets from %s\n", adr.ToString( true ) ); return false; // too many connect packets!!!! } return true; // this IP is okay } 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; } /* ================ 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 ( netadr_t &adr, int protocol, int challenge, int clientChallenge, int authProtocol, const char *name, const char *password, const char *hashedCDkey, int cdKeyLen ) { COM_TimestampedLog( "CBaseServer::ConnectClient" ); if ( !IsActive() ) { return NULL; } if ( !name || !password || !hashedCDkey ) { return NULL; } // Make sure protocols match up if ( !CheckProtocol( adr, protocol, clientChallenge ) ) { return NULL; } if ( !CheckChallengeNr( adr, challenge ) ) { RejectConnection( adr, clientChallenge, "#GameUI_ServerRejectBadChallenge" ); 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, clientChallenge, "#GameUI_ServerRejectLANRestrict"); return NULL; } #endif if ( !CheckPassword( adr, password, name ) ) { // failed ConMsg ( "%s: password failed.\n", adr.ToString() ); // Special rejection handler. RejectConnection( adr, clientChallenge, "#GameUI_ServerRejectBadPassword" ); return NULL; } } COM_TimestampedLog( "CBaseServer::ConnectClient: GetFreeClient" ); CBaseClient *client = GetFreeClient( adr ); if ( !client ) { RejectConnection( adr, clientChallenge, "#GameUI_ServerRejectServerFull" ); return NULL; // no free slot found } int nNextUserID = GetNextUserID(); if ( !CheckChallengeType( client, nNextUserID, adr, authProtocol, hashedCDkey, cdKeyLen, clientChallenge ) ) // we use the client pointer to track steam requests { return NULL; } ISteamGameServer *pSteamGameServer = Steam3Server().SteamGameServer(); if ( !pSteamGameServer && authProtocol == PROTOCOL_STEAM ) { Warning("NULL ISteamGameServer in ConnectClient. Steam authentication may fail.\n"); } if ( Filter_IsUserBanned( client->GetNetworkID() ) ) { // Need to make sure the master server is updated with the rejected connection because // we called Steam3Server().NotifyClientConnect() in CheckChallengeType() above. if ( pSteamGameServer && authProtocol == PROTOCOL_STEAM ) pSteamGameServer->SendUserDisconnect( client->m_SteamID ); RejectConnection( adr, clientChallenge, "#GameUI_ServerRejectBanned" ); return NULL; } #if !defined( _HLTVTEST ) && !defined( _REPLAYTEST ) if ( !FinishCertificateCheck( adr, authProtocol, hashedCDkey, clientChallenge ) ) { // Need to make sure the master server is updated with the rejected connection because // we called Steam3Server().NotifyClientConnect() in CheckChallengeType() above. if ( pSteamGameServer && authProtocol == PROTOCOL_STEAM ) pSteamGameServer->SendUserDisconnect( client->m_SteamID ); return NULL; } #endif COM_TimestampedLog( "CBaseServer::ConnectClient: NET_CreateNetChannel" ); // create network channel INetChannel * netchan = NET_CreateNetChannel( m_Socket, &adr, adr.ToString(), client ); if ( !netchan ) { // Need to make sure the master server is updated with the rejected connection because // we called Steam3Server().NotifyClientConnect() in CheckChallengeType() above. if ( pSteamGameServer && authProtocol == PROTOCOL_STEAM ) pSteamGameServer->SendUserDisconnect( client->m_SteamID ); RejectConnection( adr, clientChallenge, "#GameUI_ServerRejectFailedChannel" ); return NULL; } // setup netchannl settings netchan->SetChallengeNr( challenge ); COM_TimestampedLog( "CBaseServer::ConnectClient: client->Connect" ); // make sure client is reset and clear client->Connect( name, nNextUserID, netchan, false, clientChallenge ); m_nUserid = nNextUserID; m_nNumConnections++; // 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 { ALIGN4 char msg_buffer[MAX_ROUTABLE_PAYLOAD] ALIGN4_POST; bf_write msg( msg_buffer, sizeof(msg_buffer) ); msg.WriteLong( CONNECTIONLESS_HEADER ); msg.WriteByte( S2C_CONNECTION ); msg.WriteLong( clientChallenge ); msg.WriteString( "0000000000" ); // pad out NET_SendPacket ( NULL, m_Socket, adr, msg.GetData(), msg.GetNumBytesWritten() ); } // 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 } if ( netchan && !netchan->IsLoopback() ) ConMsg("Client \"%s\" connected (%s).\n", client->GetClientName(), netchan->GetAddress() ); return client; } /* ================ RequireValidChallenge Return true if this server query must provide a valid challenge number ================ */ bool CBaseServer::RequireValidChallenge( netadr_t &adr ) { if ( sv_enableoldqueries.GetBool() == true ) { return false; // don't enforce challenge numbers } return true; } /* ================ ValidChallenge Return true if this challenge number is correct for this host (for server queries) ================ */ bool CBaseServer::ValidChallenge( netadr_t & adr, int challengeNr ) { if ( !IsActive() ) // Must be running a server. return false ; if ( !IsMultiplayer() ) // ignore in single player return false ; if ( RequireValidChallenge( adr) ) { if ( !CheckChallengeNr( adr, challengeNr ) ) { ReplyServerChallenge( adr ); return false; } } return true; } bool CBaseServer::ValidInfoChallenge( netadr_t & adr, const char *nugget ) { if ( !IsActive() ) // Must be running a server. return false ; if ( !IsMultiplayer() ) // ignore in single player return false ; if ( IsReplay() ) return false; if ( RequireValidChallenge( adr) ) { if ( Q_stricmp( nugget, A2S_KEY_STRING ) ) // if the string isn't equal then fail out { return false; } } return true; } bool CBaseServer::ProcessConnectionlessPacket(netpacket_t * packet) { bf_read msg = packet->message; // handy shortcut char c = msg.ReadChar(); if ( c== 0 ) { return false; } switch ( c ) { case A2S_GETCHALLENGE : { int clientChallenge = msg.ReadLong(); ReplyChallenge( packet->from, clientChallenge ); } break; case A2S_SERVERQUERY_GETCHALLENGE: ReplyServerChallenge( packet->from ); break; case C2S_CONNECT : { char cdkey[STEAM_KEYSIZE]; char name[256]; char password[256]; char productVersion[32]; int protocol = msg.ReadLong(); int authProtocol = msg.ReadLong(); int challengeNr = msg.ReadLong(); int clientChallenge = msg.ReadLong(); // pull the challenge number check early before we do any expensive processing on the connect if ( !CheckChallengeNr( packet->from, challengeNr ) ) { RejectConnection( packet->from, clientChallenge, "#GameUI_ServerRejectBadChallenge" ); break; } // rate limit the connections if ( !s_connectRateChecker.CheckIP( packet->from ) ) return false; msg.ReadString( name, sizeof(name) ); msg.ReadString( password, sizeof(password) ); msg.ReadString( productVersion, sizeof(productVersion) ); // bool bClientPlugins = ( msg.ReadByte() > 0 ); // There's a magic number we use in the steam.inf in P4 that we don't update. // We can use this to detect if they are running out of P4, and if so, don't do any version // checking. const char *pszVersionInP4 = "2000"; const char *pszVersionString = GetSteamInfIDVersionInfo().szVersionString; if ( V_strcmp( pszVersionString, pszVersionInP4 ) && V_strcmp( productVersion, pszVersionInP4 ) ) { int nVersionCheck = Q_strncmp( pszVersionString, productVersion, V_strlen( pszVersionString ) ); if ( nVersionCheck < 0 ) { RejectConnection( packet->from, clientChallenge, "#GameUI_ServerRejectOldVersion" ); break; } if ( nVersionCheck > 0 ) { RejectConnection( packet->from, clientChallenge, "#GameUI_ServerRejectNewVersion" ); break; } } // if ( Steam3Server().BSecure() && bClientPlugins ) // { // RejectConnection( packet->from, "Cannot connect to a secure server while plug-ins are\nloaded on your client\n" ); // break; // } if ( authProtocol == PROTOCOL_STEAM ) { int keyLen = msg.ReadShort(); if ( keyLen < 0 || keyLen > sizeof(cdkey) ) { RejectConnection( packet->from, clientChallenge, "#GameUI_ServerRejectBadSteamKey" ); break; } msg.ReadBytes( cdkey, keyLen ); ConnectClient( packet->from, protocol, challengeNr, clientChallenge, authProtocol, name, password, cdkey, keyLen ); // cd key is actually a raw encrypted key } else { msg.ReadString( cdkey, sizeof(cdkey) ); ConnectClient( packet->from, protocol, challengeNr, clientChallenge, authProtocol, name, password, cdkey, strlen(cdkey) ); } } break; default: { // rate limit the more expensive server query packets if ( !s_queryRateChecker.CheckIP( packet->from ) ) return false; // We don't understand it, let the master server updater at it. if ( Steam3Server().SteamGameServer() && Steam3Server().IsMasterServerUpdaterSharingGameSocket() ) { Steam3Server().SteamGameServer()->HandleIncomingPacket( packet->message.GetBasePointer(), packet->message.TotalBytesAvailable(), packet->from.GetIPHostByteOrder(), packet->from.GetPort() ); // This is where it will usually want to respond to something immediately by sending some // packets, so check for that immediately. ForwardPacketsFromMasterServerUpdater(); } } break; } return true; } int CBaseServer::GetNumFakeClients() const { int count = 0; for ( int i = 0; i < m_Clients.Count(); i++ ) { if ( m_Clients[i]->IsFakeClient() ) { count++; } } return count; } /* ================== void SV_CountPlayers Counts number of connections. Clients includes regular connections ================== */ int CBaseServer::GetNumClients( void ) const { int count = 0; for (int i=0 ; i < m_Clients.Count() ; i++ ) { if ( m_Clients[ i ]->IsConnected() ) { count++; } } return count; } /* ================== void SV_CountPlayers Counts number of HLTV and Replay connections. Clients includes regular connections ================== */ int CBaseServer::GetNumProxies( void ) const { int count = 0; for (int i=0 ; i < m_Clients.Count() ; i++ ) { #if defined( REPLAY_ENABLED ) if ( m_Clients[ i ]->IsConnected() && (m_Clients[ i ]->IsHLTV() || m_Clients[ i ]->IsReplay() ) ) #else if ( m_Clients[ i ]->IsConnected() && m_Clients[ i ]->IsHLTV() ) #endif { count++; } } return count; } int CBaseServer::GetNumPlayers() { int count = 0; if ( !GetUserInfoTable()) { return 0; } const int maxPlayers = GetUserInfoTable()->GetNumStrings(); for ( int i=0; i < maxPlayers; i++ ) { const player_info_t *pi = (const player_info_t *) m_pUserInfoTable->GetStringUserData( i, NULL ); if ( !pi ) continue; if ( pi->fakeplayer ) continue; // don't count bots count++; } return count; } bool CBaseServer::GetPlayerInfo( int nClientIndex, player_info_t *pinfo ) { if ( !pinfo ) return false; if ( nClientIndex < 0 || !GetUserInfoTable() || nClientIndex >= GetUserInfoTable()->GetNumStrings() ) { Q_memset( pinfo, 0, sizeof( player_info_t ) ); return false; } player_info_t *pi = (player_info_t*) GetUserInfoTable()->GetStringUserData( nClientIndex, NULL ); if ( !pi ) { Q_memset( pinfo, 0, sizeof( player_info_t ) ); return false; } Q_memcpy( pinfo, pi, sizeof( player_info_t ) ); // Fixup from network order (little endian) CByteswap byteswap; byteswap.SetTargetBigEndian( false ); byteswap.SwapFieldsToTargetEndian( pinfo ); return true; } void CBaseServer::UserInfoChanged( int nClientIndex ) { player_info_t pi; bool oldlock = networkStringTableContainerServer->Lock( false ); if ( m_Clients[ nClientIndex ]->FillUserInfo( pi ) ) { // Fixup to little endian for networking CByteswap byteswap; byteswap.SetTargetBigEndian( false ); byteswap.SwapFieldsToTargetEndian( &pi ); // update user info settings m_pUserInfoTable->SetStringUserData( nClientIndex, sizeof(pi), &pi ); } else { // delete user data settings m_pUserInfoTable->SetStringUserData( nClientIndex, 0, NULL ); } networkStringTableContainerServer->Lock( oldlock ); } void CBaseServer::FillServerInfo(SVC_ServerInfo &serverinfo) { static char gamedir[MAX_OSPATH]; Q_FileBase( com_gamedir, gamedir, sizeof( gamedir ) ); serverinfo.m_nProtocol = PROTOCOL_VERSION; serverinfo.m_nServerCount = GetSpawnCount(); V_memcpy( serverinfo.m_nMapMD5.bits, worldmapMD5.bits, MD5_DIGEST_LENGTH ); serverinfo.m_nMaxClients = GetMaxClients(); serverinfo.m_nMaxClasses = serverclasses; serverinfo.m_bIsDedicated = IsDedicated(); #ifdef _WIN32 serverinfo.m_cOS = 'W'; #else serverinfo.m_cOS = 'L'; #endif // HACK to signal that the server is "new" serverinfo.m_cOS = tolower( serverinfo.m_cOS ); serverinfo.m_fTickInterval = GetTickInterval(); serverinfo.m_szGameDir = gamedir; serverinfo.m_szMapName = GetMapName(); serverinfo.m_szSkyName = m_szSkyname; serverinfo.m_szHostName = GetName(); serverinfo.m_bIsHLTV = IsHLTV(); #if defined( REPLAY_ENABLED ) serverinfo.m_bIsReplay = IsReplay(); #endif } /* ================= SVC_GetChallenge Returns a challenge number that can be used in a subsequent client_connect command. We do this to prevent denial of service attacks that flood the server with invalid connection IPs. With a challenge, they must give a valid IP address. ================= */ void CBaseServer::ReplyChallenge(netadr_t &adr, int clientChallenge ) { ALIGN4 char buffer[STEAM_KEYSIZE+32] ALIGN4_POST; bf_write msg(buffer,sizeof(buffer)); // get a free challenge number int challengeNr = GetChallengeNr( adr ); int authprotocol = GetChallengeType( adr ); msg.WriteLong( CONNECTIONLESS_HEADER ); msg.WriteByte( S2C_CHALLENGE ); msg.WriteLong( S2C_MAGICVERSION ); // This makes it so we can detect that this server is correct msg.WriteLong( challengeNr ); // Server to client challenge msg.WriteLong( clientChallenge ); // Client to server challenge to ensure our reply is what they asked msg.WriteLong( authprotocol ); #if !defined( NO_STEAM ) //#ifndef _XBOX if ( authprotocol == PROTOCOL_STEAM ) { msg.WriteShort( 0 ); // steam2 encryption key not there anymore CSteamID steamID = Steam3Server().GetGSSteamID(); uint64 unSteamID = steamID.ConvertToUint64(); msg.WriteBytes( &unSteamID, sizeof(unSteamID) ); msg.WriteByte( Steam3Server().BSecure() ); } #else msg.WriteShort( 1 ); msg.WriteByte( 0 ); uint64 unSteamID = 0; msg.WriteBytes( &unSteamID, sizeof(unSteamID) ); msg.WriteByte( 0 ); #endif msg.WriteString( "000000" ); // padding bytes NET_SendPacket( NULL, m_Socket, adr, msg.GetData(), msg.GetNumBytesWritten() ); } /* ================= ReplyServerChallenge Returns a challenge number that can be used in a subsequent server query commands. We do this to prevent DDoS attacks via bandwidth amplification. ================= */ void CBaseServer::ReplyServerChallenge(netadr_t &adr) { ALIGN4 char buffer[16] ALIGN4_POST; bf_write msg(buffer,sizeof(buffer)); // get a free challenge number int challengeNr = GetChallengeNr( adr ); msg.WriteLong( CONNECTIONLESS_HEADER ); msg.WriteByte( S2C_CHALLENGE ); msg.WriteLong( challengeNr ); NET_SendPacket( NULL, m_Socket, adr, msg.GetData(), msg.GetNumBytesWritten() ); } const char *CBaseServer::GetName( void ) const { return host_name.GetString(); } int CBaseServer::GetChallengeType(netadr_t &adr) { if ( AllowDebugDedicatedServerOutsideSteam() ) return PROTOCOL_HASHEDCDKEY; #ifndef SWDS // don't auth SP games or local mp games if steam isn't running if ( Host_IsSinglePlayerGame() || ( !Steam3Client().SteamUser() && !IsDedicated() )) { return PROTOCOL_HASHEDCDKEY; } else #endif { return PROTOCOL_STEAM; } } int CBaseServer::GetChallengeNr (netadr_t &adr) { uint64 challenge = ((uint64)adr.GetIPNetworkByteOrder() << 32) + m_CurrentRandomNonce; CRC32_t hash; CRC32_Init( &hash ); CRC32_ProcessBuffer( &hash, &challenge, sizeof(challenge) ); CRC32_Final( &hash ); return (int)hash; } void CBaseServer::GetNetStats( float &avgIn, float &avgOut ) { avgIn = avgOut = 0.0f; for (int i = 0; i < m_Clients.Count(); i++ ) { CBaseClient *cl = m_Clients[ i ]; // Fake clients get killed in here. if ( cl->IsFakeClient() ) continue; if ( !cl->IsConnected() ) continue; INetChannel *netchan = cl->GetNetChannel(); avgIn += netchan->GetAvgData(FLOW_INCOMING); avgOut += netchan->GetAvgData(FLOW_OUTGOING); } } void CBaseServer::CalculateCPUUsage( void ) { if ( !sv_stats.GetBool() ) { return; } tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ ); float curtime = Sys_FloatTime(); if ( m_fStartTime == 0 ) // record when we started { m_fStartTime = curtime; } if( curtime > m_fLastCPUCheckTime + 1 ) // only do this every 1 second { #if defined ( _WIN32 ) static float lastAvg=0; static __int64 lastTotalTime=0,lastNow=0; HANDLE handle; FILETIME creationTime, exitTime, kernelTime, userTime, nowTime; __int64 totalTime,now; handle = GetCurrentProcess(); // get CPU time GetProcessTimes (handle, &creationTime, &exitTime, &kernelTime, &userTime); GetSystemTimeAsFileTime(&nowTime); if ( lastNow == 0 ) { memcpy(&lastNow, &creationTime, sizeof(__int64)); } memcpy(&totalTime, &userTime, sizeof(__int64)); memcpy(&now, &kernelTime, sizeof(__int64)); totalTime+=now; memcpy(&now, &nowTime, sizeof(__int64)); m_fCPUPercent = (double)(totalTime-lastTotalTime)/(double)(now-lastNow); // now save this away for next time if ( curtime > lastAvg+5 ) // only do it every 5 seconds, so we keep a moving average { memcpy(&lastNow,&nowTime,sizeof(__int64)); memcpy(&lastTotalTime,&totalTime,sizeof(__int64)); lastAvg=m_fLastCPUCheckTime; } #elif defined ( POSIX ) static struct rusage s_lastUsage; static float s_lastAvg = 0; struct rusage currentUsage; if ( getrusage( RUSAGE_SELF, ¤tUsage ) == 0 ) { double flTimeDiff = (double)( currentUsage.ru_utime.tv_sec - s_lastUsage.ru_utime.tv_sec ) + (double)(( currentUsage.ru_utime.tv_usec - s_lastUsage.ru_utime.tv_usec ) / 1000000); m_fCPUPercent = flTimeDiff / ( m_fLastCPUCheckTime - s_lastAvg ); // now save this away for next time if( m_fLastCPUCheckTime > s_lastAvg + 5) { s_lastUsage = currentUsage; s_lastAvg = m_fLastCPUCheckTime; } } // limit checking :) if( m_fCPUPercent > 0.9999 ) m_fCPUPercent = 0.9999; if( m_fCPUPercent < 0 ) m_fCPUPercent = 0; #else #error #endif m_fLastCPUCheckTime = curtime; } } //----------------------------------------------------------------------------- // Purpose: Prepare for level transition, etc. //----------------------------------------------------------------------------- void CBaseServer::InactivateClients( void ) { for (int i = 0; i < m_Clients.Count(); i++ ) { CBaseClient *cl = m_Clients[ i ]; // Fake clients get killed in here. #if defined( REPLAY_ENABLED ) if ( cl->IsFakeClient() && !cl->IsHLTV() && !cl->IsReplay() ) #else if ( cl->IsFakeClient() && !cl->IsHLTV() ) #endif { // If we don't do this, it'll have a bunch of extra steam IDs for unauthenticated users. Steam3Server().NotifyClientDisconnect( cl ); cl->Clear(); continue; } else if ( !cl->IsConnected() ) { continue; } cl->Inactivate(); } } void CBaseServer::ReconnectClients( void ) { for (int i=0 ; i< m_Clients.Count() ; i++ ) { CBaseClient *cl = m_Clients[i]; if ( cl->IsConnected() ) { cl->m_nSignonState = SIGNONSTATE_CONNECTED; NET_SignonState signon( cl->m_nSignonState, -1 ); cl->SendNetMsg( signon ); } } } /* ================== SV_CheckTimeouts If a packet has not been received from a client in sv_timeout.GetFloat() seconds, drop the conneciton. When a client is normally dropped, the CSVClient goes into a zombie state for a few seconds to make sure any final reliable message gets resent if necessary ================== */ void CBaseServer::CheckTimeouts (void) { VPROF_BUDGET( "CBaseServer::CheckTimeouts", VPROF_BUDGETGROUP_OTHER_NETWORKING ); // Don't timeout in _DEBUG builds int i; #if !defined( _DEBUG ) for (i=0 ; i< m_Clients.Count() ; i++ ) { IClient *cl = m_Clients[ i ]; if ( cl->IsFakeClient() || !cl->IsConnected() ) continue; INetChannel *netchan = cl->GetNetChannel(); if ( !netchan ) continue; if ( netchan->IsTimedOut() ) { cl->Disconnect( CLIENTNAME_TIMED_OUT, cl->GetClientName() ); } } #endif for (i=0 ; i< m_Clients.Count() ; i++ ) { IClient *cl = m_Clients[ i ]; if ( cl->IsFakeClient() || !cl->IsConnected() ) continue; if ( cl->GetNetChannel() && cl->GetNetChannel()->IsOverflowed() ) { cl->Disconnect( "Client %d overflowed reliable channel.", i ); } } } // ================== // check if clients update thier user setting (convars) and call // ================== void CBaseServer::UpdateUserSettings(void) { VPROF_BUDGET( "CBaseServer::UpdateUserSettings", VPROF_BUDGETGROUP_OTHER_NETWORKING ); for (int i=0 ; i< m_Clients.Count() ; i++ ) { CBaseClient *cl = m_Clients[ i ]; cl->CheckFlushNameChange(); if ( cl->m_bConVarsChanged ) { cl->UpdateUserSettings(); } } } // ================== // check if clients need the serverinfo packet sent // ================== void CBaseServer::SendPendingServerInfo() { VPROF_BUDGET( "CBaseServer::SendPendingServerInfo", VPROF_BUDGETGROUP_OTHER_NETWORKING ); for (int i=0 ; i< m_Clients.Count() ; i++ ) { CBaseClient *cl = m_Clients[ i ]; if ( cl->m_bSendServerInfo ) { cl->SendServerInfo(); } } } // compresses a packed entity, returns data & bits const char *CBaseServer::CompressPackedEntity(ServerClass *pServerClass, const char *data, int &bits) { ALIGN4 static char s_packedData[MAX_PACKEDENTITY_DATA] ALIGN4_POST; bf_write writeBuf( "CompressPackedEntity", s_packedData, sizeof( s_packedData ) ); const void *pBaselineData = NULL; int nBaselineBits = 0; Assert( pServerClass != NULL ); GetClassBaseline( pServerClass, &pBaselineData, &nBaselineBits ); nBaselineBits *= 8; Assert( pBaselineData != NULL ); SendTable_WriteAllDeltaProps( pServerClass->m_pTable, pBaselineData, nBaselineBits, data, bits, -1, &writeBuf ); //overwrite in bits with out bits bits = writeBuf.GetNumBitsWritten(); return s_packedData; } // uncompresses a const char* CBaseServer::UncompressPackedEntity(PackedEntity *pPackedEntity, int &bits) { UnpackedDataCache_t *pdc = framesnapshotmanager->GetCachedUncompressedEntity( pPackedEntity ); if ( pdc->bits > 0 ) { // found valid uncompressed version in cache bits= pdc->bits; return pdc->data; } // not in cache, so uncompress it const void *pBaseline; int nBaselineBytes = 0; GetClassBaseline( pPackedEntity->m_pServerClass, &pBaseline, &nBaselineBytes ); Assert( pBaseline != NULL ); // store this baseline in u.m_pUpdateBaselines bf_read oldBuf( "UncompressPackedEntity1", pBaseline, nBaselineBytes ); bf_read newBuf( "UncompressPackedEntity2", pPackedEntity->GetData(), Bits2Bytes(pPackedEntity->GetNumBits()) ); bf_write outBuf( "UncompressPackedEntity3", pdc->data, MAX_PACKEDENTITY_DATA ); Assert( pPackedEntity->m_pClientClass ); RecvTable_MergeDeltas( pPackedEntity->m_pClientClass->m_pRecvTable, &oldBuf, &newBuf, &outBuf ); bits = pdc->bits = outBuf.GetNumBitsWritten(); return pdc->data; } /* ================ SV_CheckProtocol Make sure connecting client is using proper protocol ================ */ bool CBaseServer::CheckProtocol( netadr_t &adr, int nProtocol, int clientChallenge ) { if ( nProtocol != PROTOCOL_VERSION ) { // Client is newer than server if ( nProtocol > PROTOCOL_VERSION ) { RejectConnection( adr, clientChallenge, "#GameUI_ServerRejectOldProtocol" ); } else // Server is newer than client { RejectConnection( adr, clientChallenge, "#GameUI_ServerRejectNewProtocol" ); } return false; } // Success return true; } /* ================ SV_CheckKeyInfo Determine if client is outside appropriate address range ================ */ bool CBaseServer::CheckChallengeType( CBaseClient * client, int nNewUserID, netadr_t & adr, int nAuthProtocol, const char *pchLogonCookie, int cbCookie, int clientChallenge ) { if ( AllowDebugDedicatedServerOutsideSteam() ) return true; // Check protocol ID if ( ( nAuthProtocol <= 0 ) || ( nAuthProtocol > PROTOCOL_LASTVALID ) ) { RejectConnection( adr, clientChallenge, "#GameUI_ServerRejectInvalidConnection"); return false; } if ( ( nAuthProtocol == PROTOCOL_HASHEDCDKEY ) && (Q_strlen( pchLogonCookie ) <= 0 || Q_strlen(pchLogonCookie) != 32 ) ) { RejectConnection( adr, clientChallenge, "#GameUI_ServerRejectInvalidCertLen" ); return false; } Assert( !IsReplay() ); if ( IsHLTV() ) { // Don't authenticate spectators or add them to the // player list in the singleton Steam3Server() Assert( nAuthProtocol == PROTOCOL_HASHEDCDKEY ); Assert( !client->m_SteamID.IsValid() ); } else if ( nAuthProtocol == PROTOCOL_STEAM ) { // Dev hack to allow 360/Steam PC cross platform play // int ip0 = 207; // int ip1 = 173; // int ip2 = 179; // int ip3Min = 230; // int ip3Max = 245; // // if ( adr.ip[0] == ip0 && // adr.ip[1] == ip1 && // adr.ip[2] == ip2 && // adr.ip[3] >= ip3Min && // adr.ip[3] <= ip3Max ) // { // return true; // } client->SetSteamID( CSteamID() ); // set an invalid SteamID // Convert raw certificate back into data if ( cbCookie <= 0 || cbCookie >= STEAM_KEYSIZE ) { RejectConnection( adr, clientChallenge, "#GameUI_ServerRejectInvalidSteamCertLen" ); return false; } netadr_t checkAdr = adr; if ( adr.GetType() == NA_LOOPBACK || adr.IsLocalhost() ) { checkAdr.SetIP( net_local_adr.GetIPHostByteOrder() ); } if ( !Steam3Server().NotifyClientConnect( client, nNewUserID, checkAdr, pchLogonCookie, cbCookie ) && !Steam3Server().BLanOnly() ) // the userID isn't alloc'd yet so we need to fill it in manually { RejectConnection( adr, clientChallenge, "#GameUI_ServerRejectSteam" ); return false; } // // Any rejections below this must call SendUserDisconnect // // Now that we have auth'd with steam, client->GetSteamID() is now valid and we can verify against the GC lobby bool bHasGCLobby = g_iServerGameDLLVersion >= 8 && serverGameDLL->GetServerGCLobby(); if ( bHasGCLobby ) { if ( !serverGameDLL->GetServerGCLobby()->SteamIDAllowedToConnect( client->m_SteamID ) ) { ISteamGameServer *pSteamGameServer = Steam3Server().SteamGameServer(); if ( pSteamGameServer ) pSteamGameServer->SendUserDisconnect( client->m_SteamID); RejectConnection( adr, clientChallenge, "#GameUI_ServerRejectMustUseMatchmaking" ); return false; } } } else { if ( !Steam3Server().NotifyLocalClientConnect( client ) ) // the userID isn't alloc'd yet so we need to fill it in manually { RejectConnection( adr, clientChallenge, "#GameUI_ServerRejectGS" ); return false; } } return true; } bool CBaseServer::CheckIPRestrictions( const netadr_t &adr, int nAuthProtocol ) { // Determine if client is outside appropriate address range if ( adr.IsLoopback() ) return true; // X360TBD: network if ( IsX360() ) return true; // allow other users if they're on the same ip range if ( Steam3Server().BLanOnly() ) { // allow connection, if client is in the same subnet if ( adr.CompareClassBAdr( net_local_adr ) ) return true; // allow connection, if client has a private IP if ( adr.IsReservedAdr() ) return true; // reject connection return false; } return true; } void CBaseServer::SetMasterServerRulesDirty() { m_bMasterServerRulesDirty = true; } bool CBaseServer::CheckPassword( netadr_t &adr, const char *password, const char *name ) { const char *server_password = GetPassword(); if ( !server_password ) return true; // no password set if ( adr.IsLocalhost() || adr.IsLoopback() ) { return true; // local client can always connect } int iServerPassLen = Q_strlen(server_password); if ( iServerPassLen != Q_strlen(password) ) { return false; // different length cannot be equal } if ( Q_strncmp( password, server_password, iServerPassLen ) == 0) { return true; // passwords are equal } return false; // all test failed } float CBaseServer::GetTime() const { return m_nTickCount * m_flTickInterval; } float CBaseServer::GetFinalTickTime() const { return (m_nTickCount + (host_frameticks - host_currentframetick)) * m_flTickInterval; } void CBaseServer::DisconnectClient(IClient *client, const char *reason ) { client->Disconnect( reason ); } void CBaseServer::Clear( void ) { if ( m_StringTables ) { m_StringTables->RemoveAllTables(); m_StringTables = NULL; } m_pInstanceBaselineTable = NULL; m_pLightStyleTable = NULL; m_pUserInfoTable = NULL; m_pServerStartupTable = NULL; m_State = ss_dead; m_nTickCount = 0; Q_memset( m_szMapname, 0, sizeof( m_szMapname ) ); Q_memset( m_szSkyname, 0, sizeof( m_szSkyname ) ); V_memset( worldmapMD5.bits, 0, MD5_DIGEST_LENGTH ); MEM_ALLOC_CREDIT(); // Use a different limit on the signon buffer, so we can save some memory in SP (for xbox). if ( IsMultiplayer() || IsDedicated() ) { m_SignonBuffer.EnsureCapacity( NET_MAX_PAYLOAD ); } else { m_SignonBuffer.EnsureCapacity( 16384 ); } m_Signon.StartWriting( m_SignonBuffer.Base(), m_SignonBuffer.Count() ); m_Signon.SetDebugName( "m_Signon" ); serverclasses = 0; serverclassbits = 0; m_LastRandomNonce = m_CurrentRandomNonce = 0; m_flPausedTimeEnd = -1.f; } /* ================ SV_RejectConnection Rejects connection request and sends back a message ================ */ void CBaseServer::RejectConnection( const netadr_t &adr, int clientChallenge, const char *s ) { ALIGN4 char msg_buffer[MAX_ROUTABLE_PAYLOAD] ALIGN4_POST; bf_write msg( msg_buffer, sizeof(msg_buffer) ); msg.WriteLong( CONNECTIONLESS_HEADER ); msg.WriteByte( S2C_CONNREJECT ); msg.WriteLong( clientChallenge ); msg.WriteString( s ); NET_SendPacket ( NULL, m_Socket, adr, msg.GetData(), msg.GetNumBytesWritten() ); } void CBaseServer::SetPaused(bool paused) { if ( !IsPausable() ) { return; } if ( !IsActive() ) return; if ( paused ) { m_State = ss_paused; } else { m_State = ss_active; } SVC_SetPause setpause( paused ); BroadcastMessage( setpause ); } //----------------------------------------------------------------------------- // Purpose: General initialization of the server //----------------------------------------------------------------------------- void CBaseServer::Init (bool bIsDedicated) { m_nMaxclients = 0; m_nSpawnCount = 0; m_nUserid = 1; m_nNumConnections = 0; m_bIsDedicated = bIsDedicated; m_Socket = NS_SERVER; m_Signon.SetDebugName( "m_Signon" ); g_pCVar->InstallGlobalChangeCallback( ServerNotifyVarChangeCallback ); SetMasterServerRulesDirty(); Clear(); } INetworkStringTable *CBaseServer::GetInstanceBaselineTable( void ) { if ( m_pInstanceBaselineTable == NULL ) { m_pInstanceBaselineTable = m_StringTables->FindTable( INSTANCE_BASELINE_TABLENAME ); } return m_pInstanceBaselineTable; } INetworkStringTable *CBaseServer::GetLightStyleTable( void ) { if ( m_pLightStyleTable == NULL ) { m_pLightStyleTable= m_StringTables->FindTable( LIGHT_STYLES_TABLENAME ); } return m_pLightStyleTable; } INetworkStringTable *CBaseServer::GetUserInfoTable( void ) { if ( m_pUserInfoTable == NULL ) { if ( m_StringTables == NULL ) { return NULL; } m_pUserInfoTable = m_StringTables->FindTable( USER_INFO_TABLENAME ); } return m_pUserInfoTable; } bool CBaseServer::GetClassBaseline( ServerClass *pClass, void const **pData, int *pDatalen ) { if ( sv_instancebaselines.GetInt() ) { ErrorIfNot( pClass->m_InstanceBaselineIndex != INVALID_STRING_INDEX, ("SV_GetInstanceBaseline: missing instance baseline for class '%s'", pClass->m_pNetworkName) ); AUTO_LOCK( g_svInstanceBaselineMutex ); *pData = GetInstanceBaselineTable()->GetStringUserData( pClass->m_InstanceBaselineIndex, pDatalen ); return *pData != NULL; } else { static char dummy[1] = {0}; *pData = dummy; *pDatalen = 1; return true; } } bool CBaseServer::ShouldUpdateMasterServer() { // If the game server itself is ever running, then it's the one who gets to update the master server. // (SourceTV will not update it in this case). return true; } void CBaseServer::CheckMasterServerRequestRestart() { if ( !Steam3Server().SteamGameServer() || !Steam3Server().SteamGameServer()->WasRestartRequested() ) return; // Connection was rejected by the HLMaster (out of date version) // hack, vgui console looks for this string; Msg("%cMasterRequestRestart\n", 3); #ifndef _WIN32 if (CommandLine()->FindParm(AUTO_RESTART)) { Msg("Your server will be restarted on map change.\n"); Log("Your server will be restarted on map change.\n"); SetRestartOnLevelChange( true ); } #endif if ( sv.IsDedicated() ) // under linux assume steam { Msg("Your server needs to be restarted in order to receive the latest update.\n"); Log("Your server needs to be restarted in order to receive the latest update.\n"); } else { Msg("Your server is out of date. Please update and restart.\n"); } } void CBaseServer::UpdateMasterServer() { VPROF_BUDGET( "CBaseServer::UpdateMasterServer", VPROF_BUDGETGROUP_OTHER_NETWORKING ); if ( !ShouldUpdateMasterServer() ) return; if ( !Steam3Server().SteamGameServer() ) return; // Only update every so often. double flCurTime = Plat_FloatTime(); if ( flCurTime - m_flLastMasterServerUpdateTime < MASTER_SERVER_UPDATE_INTERVAL ) return; m_flLastMasterServerUpdateTime = flCurTime; ForwardPacketsFromMasterServerUpdater(); CheckMasterServerRequestRestart(); if ( NET_IsDedicated() && sv_region.GetInt() == -1 ) { sv_region.SetValue( 255 ); // HACK!HACK! undo me once we want to enforce regions //Log_Printf( "You must set sv_region in your server.cfg or use +sv_region on the command line\n" ); //Con_Printf( "You must set sv_region in your server.cfg or use +sv_region on the command line\n" ); //Cbuf_AddText( "quit\n" ); //return; } static bool bUpdateMasterServers = !CommandLine()->FindParm( "-nomaster" ); if ( !bUpdateMasterServers ) return; bool bActive = IsActive() && IsMultiplayer(); if ( serverGameDLL && serverGameDLL->ShouldHideServer() ) bActive = false; Steam3Server().SteamGameServer()->EnableHeartbeats( bActive ); if ( !bActive ) return; UpdateMasterServerRules(); UpdateMasterServerPlayers(); Steam3Server().SendUpdatedServerDetails(); } void CBaseServer::UpdateMasterServerRules() { // Only do this if the rules vars are dirty. if ( !m_bMasterServerRulesDirty ) return; ISteamGameServer *pUpdater = Steam3Server().SteamGameServer(); if ( !pUpdater ) return; pUpdater->ClearAllKeyValues(); // Need to respond with game directory, game name, and any server variables that have been set that // effect rules. Also, probably need a hook into the .dll to respond with additional rule information. ConCommandBase *var; for ( var = g_pCVar->GetCommands() ; var ; var=var->GetNext() ) { if ( !(var->IsFlagSet( FCVAR_NOTIFY ) ) ) continue; if ( var->IsCommand() ) continue; ConVar *pConVar = static_cast< ConVar* >( var ); if ( !pConVar ) continue; SetMasterServerKeyValue( pUpdater, pConVar ); } if ( Steam3Server().SteamGameServer() ) { RecalculateTags(); } // Ok.. it's all updated, only send incremental updates now until we decide they're all dirty. m_bMasterServerRulesDirty = false; } void CBaseServer::ForwardPacketsFromMasterServerUpdater() { ISteamGameServer *p = Steam3Server().SteamGameServer(); if ( !p ) return; while ( 1 ) { uint32 netadrAddress; uint16 netadrPort; unsigned char packetData[16 * 1024]; int len = p->GetNextOutgoingPacket( packetData, sizeof( packetData ), &netadrAddress, &netadrPort ); if ( len <= 0 ) break; // Send this packet for them.. netadr_t adr( netadrAddress, netadrPort ); NET_SendPacket( NULL, m_Socket, adr, packetData, len ); } } /* ================= SV_ReadPackets Read's packets from clients and executes messages as appropriate. ================= */ void CBaseServer::RunFrame( void ) { VPROF_BUDGET( "CBaseServer::RunFrame", VPROF_BUDGETGROUP_OTHER_NETWORKING ); tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "CBaseServer::RunFrame" ); NET_ProcessSocket( m_Socket, this ); #ifdef LINUX // Process the linux sv lan port if it's open. if ( NET_GetUDPPort( NS_SVLAN ) ) NET_ProcessSocket( NS_SVLAN, this ); #endif CheckTimeouts(); // drop clients that timeed out UpdateUserSettings(); // update client settings SendPendingServerInfo(); // send outstanding signon packets after ALL user settings have been updated CalculateCPUUsage(); // update CPU usage UpdateMasterServer(); if ( m_flLastRandomNumberGenerationTime < 0 || (m_flLastRandomNumberGenerationTime + CHALLENGE_NONCE_LIFETIME) < g_ServerGlobalVariables.realtime ) { m_LastRandomNonce = m_CurrentRandomNonce; // RandomInt maps a uniform distribution on the interval [0,INT_MAX], so make two calls to get the random number. // RandomInt will always return the minimum value if the difference in min and max is greater than or equal to INT_MAX. m_CurrentRandomNonce = ( ( (uint32)RandomInt( 0, 0xFFFF ) ) << 16 ) | RandomInt( 0, 0xFFFF ); m_flLastRandomNumberGenerationTime = g_ServerGlobalVariables.realtime; } // Timed pause - resume game when time expires if ( m_flPausedTimeEnd >= 0.f && m_State == ss_paused && Sys_FloatTime() >= m_flPausedTimeEnd ) { SetPausedForced( false ); } } //----------------------------------------------------------------------------- // Purpose: // Input : *adr - // *pslot - // **ppClient - // Output : int //----------------------------------------------------------------------------- CBaseClient * CBaseServer::GetFreeClient( netadr_t &adr ) { CBaseClient *freeclient = NULL; for ( int slot = 0 ; slot < m_Clients.Count() ; slot++ ) { CBaseClient *client = m_Clients[slot]; if ( client->IsFakeClient() ) continue; if ( client->IsConnected() ) { if ( adr.CompareAdr ( client->m_NetChannel->GetRemoteAddress() ) ) { ConMsg ( "%s:reconnect\n", adr.ToString() ); RemoveClientFromGame( client ); // perform a silent netchannel shutdown, don't send disconnect msg client->m_NetChannel->Shutdown( NULL ); client->m_NetChannel = NULL; client->Clear(); return client; } } else { // use first found free slot if ( !freeclient ) { freeclient = client; } } } if ( !freeclient ) { int count = m_Clients.Count(); if ( count >= m_nMaxclients ) { return NULL; // server full } // we have to create a new client slot freeclient = CreateNewClient( count ); m_Clients.AddToTail( freeclient ); } // Success return freeclient; } void CBaseServer::SendClientMessages ( bool bSendSnapshots ) { VPROF_BUDGET( "SendClientMessages", VPROF_BUDGETGROUP_OTHER_NETWORKING ); for (int i=0; i< m_Clients.Count(); i++ ) { CBaseClient* client = m_Clients[i]; // Update Host client send state... if ( !client->ShouldSendMessages() ) continue; // Connected, but inactive, just send reliable, sequenced info. if ( client->m_NetChannel ) { client->m_NetChannel->Transmit(); client->UpdateSendState(); } else { Msg("Client has no netchannel.\n"); } } } CBaseClient *CBaseServer::CreateFakeClient( const char *name ) { netadr_t adr; // it's an empty address CBaseClient *fakeclient = GetFreeClient( adr ); if ( !fakeclient ) { // server is full return NULL; } INetChannel *netchan = NULL; if ( sv_stressbots.GetBool() ) { netadr_t adrNull( 0, 0 ); // 0.0.0.0:0 signifies a bot. It'll plumb all the way down to winsock calls but it won't make them. netchan = NET_CreateNetChannel( m_Socket, &adrNull, adrNull.ToString(), fakeclient, true ); } // a NULL netchannel signals a fakeclient m_nUserid = GetNextUserID(); m_nNumConnections++; fakeclient->SetReportThisFakeClient( m_bReportNewFakeClients ); fakeclient->Connect( name, m_nUserid, netchan, true, 0 ); // fake some cvar settings //fakeclient->SetUserCVar( "name", name ); // set already by Connect() fakeclient->SetUserCVar( "rate", "30000" ); fakeclient->SetUserCVar( "cl_updaterate", "20" ); fakeclient->SetUserCVar( "cl_interp_ratio", "1.0" ); fakeclient->SetUserCVar( "cl_interp", "0.1" ); fakeclient->SetUserCVar( "cl_interpolate", "0" ); fakeclient->SetUserCVar( "cl_predict", "1" ); fakeclient->SetUserCVar( "cl_predictweapons", "1" ); fakeclient->SetUserCVar( "cl_lagcompensation", "1" ); fakeclient->SetUserCVar( "closecaption","0" ); fakeclient->SetUserCVar( "english", "1" ); fakeclient->SetUserCVar( "cl_clanid", "0" ); fakeclient->SetUserCVar( "cl_team", "blue" ); fakeclient->SetUserCVar( "hud_classautokill", "1" ); fakeclient->SetUserCVar( "tf_medigun_autoheal", "0" ); fakeclient->SetUserCVar( "cl_autorezoom", "1" ); fakeclient->SetUserCVar( "fov_desired", "75" ); fakeclient->SetUserCVar( "tf_remember_lastswitched", "0" ); fakeclient->SetUserCVar( "cl_autoreload", "0" ); fakeclient->SetUserCVar( "tf_remember_activeweapon", "0" ); fakeclient->SetUserCVar( "hud_combattext", "0" ); fakeclient->SetUserCVar( "cl_flipviewmodels", "0" ); // create client in game.dll fakeclient->ActivatePlayer(); fakeclient->m_nSignonTick = m_nTickCount; return fakeclient; } void CBaseServer::Shutdown( void ) { if ( !IsActive() ) return; m_State = ss_dead; // Only drop clients if we have not cleared out entity data prior to this. for( int i=m_Clients.Count()-1; i>=0; i-- ) { CBaseClient * cl = m_Clients[ i ]; if ( cl->IsConnected() ) { cl->Disconnect( "Server shutting down" ); } else { // free any memory do this out side here in case the reason the server is shutting down // is because the listen server client typed disconnect, in which case we won't call // cl->DropClient, but the client might have some frame snapshot references left over, etc. cl->Clear(); } delete cl; m_Clients.Remove( i ); } // Let drop messages go out Sys_Sleep( 100 ); // clear everything Clear(); } //----------------------------------------------------------------------------- // Purpose: Sends text to all active clients // Input : *fmt - // ... - //----------------------------------------------------------------------------- void CBaseServer::BroadcastPrintf (const char *fmt, ...) { va_list argptr; char string[1024]; va_start (argptr,fmt); Q_vsnprintf (string, sizeof( string ), fmt,argptr); va_end (argptr); SVC_Print print( string ); BroadcastMessage( print ); } void CBaseServer::BroadcastMessage( INetMessage &msg, bool onlyActive, bool reliable ) { for ( int i = 0; i < m_Clients.Count(); i++ ) { CBaseClient *cl = m_Clients[ i ]; if ( (onlyActive && !cl->IsActive()) || !cl->IsSpawned() ) { continue; } if ( !cl->SendNetMsg( msg, reliable ) ) { if ( msg.IsReliable() || reliable ) { DevMsg( "BroadcastMessage: Reliable broadcast message overflow for client %s", cl->GetClientName() ); } } } } void CBaseServer::BroadcastMessage( INetMessage &msg, IRecipientFilter &filter ) { if ( filter.IsInitMessage() ) { // This really only applies to the first player to connect, but that works in single player well enought if ( IsActive() ) { ConDMsg( "SV_BroadcastMessage: Init message being created after signon buffer has been transmitted\n" ); } if ( !msg.WriteToBuffer( m_Signon ) ) { Sys_Error( "SV_BroadcastMessage: Init message would overflow signon buffer!\n" ); return; } } else { msg.SetReliable( filter.IsReliable() ); int num = filter.GetRecipientCount(); for ( int i = 0; i < num; i++ ) { int index = filter.GetRecipientIndex( i ); if ( index < 1 || index > m_Clients.Count() ) { Msg( "SV_BroadcastMessage: Recipient Filter for message type %i (reliable: %s, init: %s) with bogus client index (%i) in list of %i clients\n", msg.GetType(), filter.IsReliable() ? "yes" : "no", filter.IsInitMessage() ? "yes" : "no", index, num ); if ( msg.IsReliable() ) Host_Error( "Reliable message (type %i) discarded.", msg.GetType() ); continue; } CBaseClient *cl = m_Clients[ index - 1 ]; if ( !cl->IsSpawned() ) { continue; } if ( !cl->SendNetMsg( msg ) ) { if ( msg.IsReliable() ) { DevMsg( "BroadcastMessage: Reliable filter message overflow for client %s", cl->GetClientName() ); } } } } } //----------------------------------------------------------------------------- // Purpose: Writes events to the client's network buffer // Input : *cl - // *pack - // *msg - //----------------------------------------------------------------------------- static ConVar sv_debugtempentities( "sv_debugtempentities", "0", 0, "Show temp entity bandwidth usage." ); static bool CEventInfo_LessFunc( CEventInfo * const &lhs, CEventInfo * const &rhs ) { return lhs->classID < rhs->classID; } void CBaseServer::WriteTempEntities( CBaseClient *client, CFrameSnapshot *pCurrentSnapshot, CFrameSnapshot *pLastSnapshot, bf_write &buf, int ev_max ) { VPROF_BUDGET( "CBaseServer::WriteTempEntities", VPROF_BUDGETGROUP_OTHER_NETWORKING ); ALIGN4 char data[NET_MAX_PAYLOAD] ALIGN4_POST; SVC_TempEntities msg; msg.m_DataOut.StartWriting( data, sizeof(data) ); bf_write &buffer = msg.m_DataOut; // shortcut CFrameSnapshot *pSnapshot; CEventInfo *pLastEvent = NULL; bool bDebug = sv_debugtempentities.GetBool(); // limit max entities to field bit length ev_max = min( ev_max, ((1<NextSnapshot(); } else { pSnapshot = pCurrentSnapshot; } CUtlRBTree< CEventInfo * > sorted( 0, ev_max, CEventInfo_LessFunc ); // Build list of events sorted by send table classID (makes the delta work better in cases with a lot of the same message type ) while ( pSnapshot && ((int)sorted.Count() < ev_max) ) { for( int i = 0; i < pSnapshot->m_nTempEntities; ++i ) { CEventInfo *event = pSnapshot->m_pTempEntities[ i ]; if ( client->IgnoreTempEntity( event ) ) continue; // event is not seen by this player sorted.Insert( event ); // More space still if ( (int)sorted.Count() >= ev_max ) break; } // stop, we reached our current snapshot if ( pSnapshot == pCurrentSnapshot ) break; // got to next snapshot pSnapshot = framesnapshotmanager->NextSnapshot( pSnapshot ); } if ( sorted.Count() <= 0 ) return; for ( int i = sorted.FirstInorder(); i != sorted.InvalidIndex(); i = sorted.NextInorder( i ) ) { CEventInfo *event = sorted[ i ]; if ( event->fire_delay == 0.0f ) { buffer.WriteOneBit( 0 ); } else { buffer.WriteOneBit( 1 ); buffer.WriteSBitLong( event->fire_delay*100.0f, 8 ); } if ( pLastEvent && pLastEvent->classID == event->classID ) { buffer.WriteOneBit( 0 ); // delta against last temp entity int startBit = bDebug ? buffer.GetNumBitsWritten() : 0; SendTable_WriteAllDeltaProps( event->pSendTable, pLastEvent->pData, pLastEvent->bits, event->pData, event->bits, -1, &buffer ); if ( bDebug ) { int length = buffer.GetNumBitsWritten() - startBit; DevMsg("TE %s delta bits: %i\n", event->pSendTable->GetName(), length ); } } else { // full update, just compressed against zeros in MP buffer.WriteOneBit( 1 ); int startBit = bDebug ? buffer.GetNumBitsWritten() : 0; buffer.WriteUBitLong( event->classID, GetClassBits() ); if ( IsMultiplayer() ) { SendTable_WriteAllDeltaProps( event->pSendTable, NULL, // will write only non-zero elements 0, event->pData, event->bits, -1, &buffer ); } else { // write event with zero properties buffer.WriteBits( event->pData, event->bits ); } if ( bDebug ) { int length = buffer.GetNumBitsWritten() - startBit; DevMsg("TE %s full bits: %i\n", event->pSendTable->GetName(), length ); } } if ( IsMultiplayer() ) { // in single player, don't used delta compression, lastEvent remains NULL pLastEvent = event; } } // set num entries msg.m_nNumEntries = sorted.Count(); msg.WriteToBuffer( buf ); } void CBaseServer::SetMaxClients( int number ) { m_nMaxclients = clamp( number, 1, ABSOLUTE_PLAYER_LIMIT ); } extern ConVar tv_enable; //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseServer::RecalculateTags( void ) { if ( IsHLTV() || IsReplay() ) return; // We're going to modify the sv_tags convar here, which will cause this to be called again. Prevent recursion. static bool bRecalculatingTags = false; if ( bRecalculatingTags ) return; bRecalculatingTags = true; // Games without this interface will have no tagged cvars besides "increased_maxplayers" if ( serverGameTags ) { KeyValues *pKV = new KeyValues( "GameTags" ); serverGameTags->GetTaggedConVarList( pKV ); KeyValues *p = pKV->GetFirstSubKey(); while ( p ) { ConVar *pConVar = g_pCVar->FindVar( p->GetString("convar") ); if ( pConVar ) { const char *pszDef = pConVar->GetDefault(); const char *pszCur = pConVar->GetString(); if ( Q_strcmp( pszDef, pszCur ) ) { AddTag( p->GetString("tag") ); } else { RemoveTag( p->GetString("tag") ); } } p = p->GetNextKey(); } pKV->deleteThis(); } // Check maxplayers int minmaxplayers = 1; int maxmaxplayers = ABSOLUTE_PLAYER_LIMIT; int defaultmaxplayers = 1; serverGameClients->GetPlayerLimits( minmaxplayers, maxmaxplayers, defaultmaxplayers ); int nMaxReportedClients = GetMaxClients() - GetNumProxies(); if ( sv_visiblemaxplayers.GetInt() > 0 && sv_visiblemaxplayers.GetInt() < nMaxReportedClients ) { nMaxReportedClients = sv_visiblemaxplayers.GetInt(); } if ( nMaxReportedClients > defaultmaxplayers ) { AddTag( "increased_maxplayers" ); } else { RemoveTag( "increased_maxplayers" ); } #if defined( REPLAY_ENABLED ) ConVarRef replay_enable( "replay_enable", true ); if ( replay_enable.IsValid() && replay_enable.GetBool() ) { AddTag( "replays" ); } else { RemoveTag( "replays" ); } #endif bRecalculatingTags = false; } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseServer::AddTag( const char *pszTag ) { CUtlVector TagList; V_SplitString( sv_tags.GetString(), ",", TagList ); for ( int i = 0; i < TagList.Count(); i++ ) { // Already in the tag list? if ( !Q_stricmp(TagList[i],pszTag) ) return; } TagList.PurgeAndDeleteElements(); // Append it char tmptags[MAX_TAG_STRING_LENGTH]; tmptags[0] = '\0'; Q_strncpy( tmptags, pszTag, MAX_TAG_STRING_LENGTH ); Q_strncat( tmptags, ",", MAX_TAG_STRING_LENGTH ); Q_strncat( tmptags, sv_tags.GetString(), MAX_TAG_STRING_LENGTH ); sv_tags.SetValue( tmptags ); } //----------------------------------------------------------------------------- // Purpose: //----------------------------------------------------------------------------- void CBaseServer::RemoveTag( const char *pszTag ) { const char *pszTags = sv_tags.GetString(); if ( !pszTags || !pszTags[0] ) return; char tmptags[MAX_TAG_STRING_LENGTH]; tmptags[0] = '\0'; CUtlVector TagList; bool bFoundIt = false; V_SplitString( sv_tags.GetString(), ",", TagList ); for ( int i = 0; i < TagList.Count(); i++ ) { // Keep any tags other than the specified one if ( Q_stricmp(TagList[i],pszTag) ) { Q_strncat( tmptags, TagList[i], MAX_TAG_STRING_LENGTH ); Q_strncat( tmptags, ",", MAX_TAG_STRING_LENGTH ); } else { bFoundIt = true; } } TagList.PurgeAndDeleteElements(); // Didn't find it in our list? if ( !bFoundIt ) return; sv_tags.SetValue( tmptags ); } //----------------------------------------------------------------------------- // Purpose: Server-only override (ignores sv_pausable). Can be on a timer. //----------------------------------------------------------------------------- void CBaseServer::SetPausedForced( bool bPaused, float flDuration /*= -1.f*/ ) { if ( !IsActive() ) return; m_State = ( bPaused ) ? ss_paused : ss_active; m_flPausedTimeEnd = ( bPaused && flDuration > 0.f ) ? Sys_FloatTime() + flDuration : -1.f; SVC_SetPauseTimed setpause( bPaused, m_flPausedTimeEnd ); BroadcastMessage( setpause ); }