|
|
//========= 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 <time.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <netinet/in.h>
#elif defined(LINUX)
#include <time.h>
#include <sys/sysinfo.h>
#include <asm/param.h> // for HZ
#include <netinet/in.h>
#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 <proto_oob.h>
#include <vstdlib/random.h>
#include <irecipientfilter.h>
#include <keyvalues.h>
#include <tier0/vprof.h>
#include <cdll_int.h>
#include <eiface.h>
#include <client_class.h>
#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<char*> 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.Count() ; i++) { if ( adr.CompareAdr(m_ServerQueryChallenges[i].adr, true) ) // base adr only
{ if (nChallengeValue != m_ServerQueryChallenges[i].challenge) { return false; }
if ( net_time > ( 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 ? "<ip hidden>" : netchan->GetAddress() ); }
// Once someone is successfully on the server, it'll hibernate when client count goes to zero
m_flReservationExpiryTime = 0.0f; m_flTimeLastClientLeft = -1.0f;
return client; }
/*
================ RequireValidChallenge
Return true if this server query must provide a valid challenge number ================ */ bool CBaseServer::RequireValidChallenge( const ns_address &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( const ns_address & 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( const ns_address & adr, const char *nugget ) { if ( !IsActive() ) // Must be running a server.
return false ;
if ( !IsMultiplayer() ) // ignore in single player
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) { // NOTE: msg is copy-constructed from packet->message, so reading
// from "msg" will not advance read-pointer in "packet->message"!!!
// ... and vice-versa, hence passing "packet" to a nested function
// will make that function receive the original unprocessed message.
// [ this differs from client-side connectionless packet processing ]
if ( !CheckConnectionLessRateLimits( packet->from ) ) { return false; }
bf_read msg = packet->message; // handy shortcut
char c = msg.ReadChar();
if ( !IsActive() || !Host_ShouldRun() ) { // Server should not be processing most of the traffic in this
// state except client LAN searches
if ( IsDedicated() ) return true;
if ( c != 0 ) return true; }
switch ( c ) { case A2A_PING : NET_OutOfBandPrintf (packet->source, packet->from, "%c00000000000000", A2A_ACK ); break; case A2A_PRINT : // Don't spew to server console, this was being used to spam the server console since we publish server public IP addresses...
break;
case A2A_ACK : ConMsg ("A2A_ACK from %s\n", ns_address_render( packet->from ).String() ); break;
case A2S_GETCHALLENGE : #if !defined(NO_STEAM)
// Drop packet if we don't yet have our Steam ID
// because we're still logging on
if ( !Steam3Server().BHasLogonResult() ) break; #endif
ReplyChallenge( packet->from, msg ); break; case A2S_SERVERQUERY_GETCHALLENGE: ReplyServerChallenge( packet->from ); break;
#if ENGINE_CONNECT_VIA_MMS
case C2S_VALIDATE_SESSION: { int protocol = msg.ReadLong(); uint64 uiXuid = msg.ReadLongLong(); uint64 uiSessionId = msg.ReadLongLong(); Msg( "C2S_VALIDATE_SESSION from %llx, session %llx, protocol %d.\n", uiXuid, uiSessionId, protocol );
if ( GetHostVersion() == protocol ) { extern void HostValidateSessionImpl(); HostValidateSessionImpl(); } } break; #endif
case C2S_CONNECT : { char cdkey[STEAM_KEYSIZE]; char name[256] = {}; char password[256] = {};
int protocol = msg.ReadLong(); int authProtocol = msg.ReadLong(); int challengeNr = msg.ReadLong();
msg.ReadString( name, sizeof(name) ); msg.ReadString( password, sizeof(password) );
//decode split screen player info from the message
int numPlayers = msg.ReadByte(); if ( numPlayers > host_state.max_splitscreen_players || numPlayers < 0 ) { // Make sure we reject connection early, otherwise it will trigger Disconnect
// freeing client inside ConnectClient and cause memory access violation
DevMsg( "Rejecting connection request from %s, players = %u.\n", ns_address_render( packet->from ).String(), numPlayers ); RejectConnection( packet->from, "No more split screen slots!" ); break; }
struct AutoCleanupVectorPlayers_t : public CUtlVector< CCLCMsg_SplitPlayerConnect_t * > { ~AutoCleanupVectorPlayers_t() { PurgeAndDeleteElements(); } } splitScreenPlayers; if( numPlayers > 0 ) { for( int playerCount = 0; playerCount < numPlayers; ++ playerCount ) // get CLC_SplitPlayerConnect msg for all players even if only one is connecting
{ msg.ReadVarInt32(); //the packet type.
CCLCMsg_SplitPlayerConnect_t *pSplitPlayerConnect = new CCLCMsg_SplitPlayerConnect_t; splitScreenPlayers.AddToTail( pSplitPlayerConnect ); if ( !pSplitPlayerConnect->ReadFromBuffer( msg ) ) { numPlayers = -1; // Trigger an error
} else if ( pSplitPlayerConnect->convars().cvars_size() ) { // Make sure convars are expanded using dictionary
for ( int iCV = 0; iCV < pSplitPlayerConnect->convars().cvars_size(); ++ iCV ) { CMsg_CVars::CVar *convar = pSplitPlayerConnect->mutable_convars()->mutable_cvars( iCV ); NetMsgExpandCVarUsingDictionary( convar ); } } } }
if ( numPlayers < 0 ) { // Catch malformed user info
DevMsg( "Rejecting connection request from %s, malformed userinfo.\n", ns_address_render( packet->from ).String() ); RejectConnection( packet->from, "No more split screen slots!" ); break; }
bool isClientLowViolence = msg.ReadOneBit() != 0;
uint64 nReservationCookie = msg.ReadLongLong();
CrossPlayPlatform_t clientPlatform = (CrossPlayPlatform_t)msg.ReadByte(); if( (clientPlatform == CROSSPLAYPLATFORM_UNKNOWN) || (clientPlatform > CROSSPLAYPLATFORM_LAST) ) { DevMsg( "Rejecting connection request from %s, client's cross-play platform is unrecognized.\n", ns_address_render( packet->from ).String() ); RejectConnection( packet->from, "Invalid cross-play platform id\n" ); break; }
bool bIsLocalConnection = packet->from.IsLocalhost() || packet->from.IsLoopback();
// On a listen server, local player always gets on regardless of cookie
if ( IsDedicated() || !bIsLocalConnection ) { if ( IsExclusiveToLobbyConnections() && m_nReservationCookie != nReservationCookie ) { DevMsg( "Rejecting connection request from %s, client's reservation cookie %llx does not match servers's cookie %llx.\n", ns_address_render( packet->from ).String(), nReservationCookie, m_nReservationCookie ); RejectConnection( packet->from, "#Valve_Reject_Connect_From_Lobby" ); break; }
// if this server is currently reserved, only allow connection if the client provides the cookie the server was reserved with
if ( IsReserved() && m_nReservationCookie != nReservationCookie ) { DevMsg( "Rejecting connection request from %s (reservation cookie 0x%llx), server is reserved with reservation cookie 0x%llx " "for %.1f more seconds\n", ns_address_render( packet->from ).String(), nReservationCookie, m_nReservationCookie, ( m_flReservationExpiryTime - net_time ) ); RejectConnection( packet->from, "#Valve_Reject_Reserved_For_Lobby" ); break; }
// Check if we are running with a special flag and not advertise to Steam
if ( !V_strcmp( g_pLaunchOptions->GetFirstSubKey()->GetNextKey()->GetNextKey()->GetString(), "server_is_unavailable" ) ) { DevMsg( "Rejecting connection request from %s (reservation cookie 0x%llx), server is reserved with reservation cookie 0x%llx " "for %.1f more seconds, running with tag server_is_unavailable\n", ns_address_render( packet->from ).String(), nReservationCookie, m_nReservationCookie, ( m_flReservationExpiryTime - net_time ) ); RejectConnection( packet->from, "#Valve_Reject_Workshop_Loading" ); break; } }
//
// Get client certificate
//
const byte *pbNetEncryptPrivateKey = NULL; int cbNetEncryptPrivateKey = 0; bool bNetEncryptPrivateKey = NET_CryptGetNetworkCertificate( k_ENetworkCertificate_PrivateKey, &pbNetEncryptPrivateKey, &cbNetEncryptPrivateKey ); // Read whether the client would like to use encryption key?
byte *pbClientPlainKey = NULL; int nEncryptionKeyIndex = msg.ReadLong();
// Peek into client-supplied speculative accountid
static bool s_bExternalCryptKeys = ( CommandLine()->CheckParm( "-externalnetworkcryptkey" ) != NULL ); AccountID_t unAccountIDfor3rdParties = 0; if ( s_bExternalCryptKeys && splitScreenPlayers.Count() ) { const CMsg_CVars& convars = splitScreenPlayers[ 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, "accountid" ) ) continue;
if ( unAccountIDfor3rdParties ) { unAccountIDfor3rdParties = 0; break; }
unAccountIDfor3rdParties = Q_atoi( value ); } }
// Community-servers feature to manage encryption keys in their secure clients
if ( s_bExternalCryptKeys ) { bool bClientWantsToUseCryptKey = ( nEncryptionKeyIndex != 0 ); bNetEncryptPrivateKey = g_pServerPluginHandler->BNetworkCryptKeyCheckRequired( packet->from.GetIP(), packet->from.GetPort(), unAccountIDfor3rdParties, bClientWantsToUseCryptKey ); }
if ( nEncryptionKeyIndex ) { int numEncryptedBytes = msg.ReadLong(); if ( ( numEncryptedBytes > 0 ) && ( numEncryptedBytes <= 256 ) ) { byte *pbEncryptedKey = ( byte * ) stackalloc( numEncryptedBytes ); msg.ReadBytes( pbEncryptedKey, numEncryptedBytes );
// Community-servers feature to manage encryption keys in their secure clients
if ( s_bExternalCryptKeys ) { // Client can send whatever gibberish they want, plugins manage the encryption keys
byte chPlainKey[ 1024 ] = {}; if ( g_pServerPluginHandler->BNetworkCryptKeyValidate( packet->from.GetIP(), packet->from.GetPort(), unAccountIDfor3rdParties, nEncryptionKeyIndex, numEncryptedBytes, pbEncryptedKey, chPlainKey ) ) { pbClientPlainKey = ( byte * ) stackalloc( NET_CRYPT_KEY_LENGTH ); Q_memcpy( pbClientPlainKey, chPlainKey, NET_CRYPT_KEY_LENGTH ); } } else { //
// Server will send the certificate and its signature down to the client
//
byte chPlainKey[ 1024 ] = {}; if ( bNetEncryptPrivateKey && NET_CryptVerifyClientSessionKey( serverGameDLL->IsValveDS(), pbNetEncryptPrivateKey, cbNetEncryptPrivateKey, pbEncryptedKey, numEncryptedBytes, chPlainKey, Q_ARRAYSIZE( chPlainKey ) ) ) { pbClientPlainKey = ( byte * ) stackalloc( NET_CRYPT_KEY_LENGTH ); Q_memcpy( pbClientPlainKey, chPlainKey, NET_CRYPT_KEY_LENGTH ); } } } }
if ( ( this == &sv ) && bNetEncryptPrivateKey && !pbClientPlainKey ) { DevMsg( "Rejecting connection request from %s, no certificate.\n", ns_address_render( packet->from ).String() ); RejectConnection( packet->from, "Invalid certificate\n" ); break; }
if ( authProtocol == PROTOCOL_STEAM ) { int keyLen = msg.ReadShort(); if ( keyLen < 0 || keyLen > sizeof(cdkey) ) { RejectConnection( packet->from, "Invalid Steam key length\n" ); break; } msg.ReadBytes( cdkey, keyLen );
ConnectClient( packet->from, protocol, challengeNr, authProtocol, name, password, cdkey, keyLen, splitScreenPlayers, isClientLowViolence, clientPlatform, pbClientPlainKey, nEncryptionKeyIndex ); // cd key is actually a raw encrypted key
} else { msg.ReadString( cdkey, sizeof(cdkey) ); ConnectClient( packet->from, protocol, challengeNr, authProtocol, name, password, cdkey, strlen(cdkey), splitScreenPlayers, isClientLowViolence, clientPlatform, pbClientPlainKey, nEncryptionKeyIndex ); }
} break; case A2A_CUSTOM : // TODO fire game event with string and adr so 3rd party tool can get it
break;
#if ENGINE_CONNECT_VIA_MMS
case A2S_RESERVE : ReplyReservationRequest( packet->from, msg ); break; #endif
case A2S_RESERVE_CHECK : ReplyReservationCheckRequest( packet->from, msg ); break;
case A2S_PING: { if ( msg.ReadLong() == GetHostVersion() ) { uint32 token = msg.ReadLong(); char buffer[64]; bf_write msg(buffer,sizeof(buffer));
msg.WriteLong( CONNECTIONLESS_HEADER ); msg.WriteByte( S2A_PING_RESPONSE ); msg.WriteLong( GetHostVersion() ); msg.WriteLong( token );
NET_SendPacket( NULL, m_Socket, packet->from, msg.GetData(), msg.GetNumBytesWritten() ); }
break; }
case 0 : { // Feed into matchmaking
KeyValues *notify = new KeyValues( "OnNetLanConnectionlessPacket" ); notify->SetPtr( "rawpkt", packet ); g_pMatchFramework->GetEventsSubscription()->BroadcastEvent( notify ); } break;
case A2S_INFO: // Handle A2S_INFO ourselves, we are concealing SteamID of the game server and report players differently
if ( g_bSteamMasterHeartbeatsEnabled ) { extern ConVar host_info_show; switch ( host_info_show.GetInt() ) { case 2: goto steam3server_oob_packet_handler;
case 1: { // Validate challenge
char nugget[ 64 ]; if ( !msg.ReadString( nugget, sizeof( nugget ) ) ) break; if ( !ValidInfoChallenge( packet->from, nugget ) ) break;
// Respond
extern AppId_t g_unSteamAppID; AppId_t appIdResponse = g_unSteamAppID ? g_unSteamAppID : 730; CUtlBuffer buf; buf.EnsureCapacity( MAX_ROUTABLE_PAYLOAD );
buf.PutUnsignedInt( LittleDWord( CONNECTIONLESS_HEADER ) ); buf.PutUnsignedChar( S2A_INFO_SRC ); buf.PutUnsignedChar( 17 ); // Hardcoded protocol version number
extern ConVar host_name_store; buf.PutString( host_name_store.GetBool() ? GetName() : "Counter-Strike: Global Offensive" ); buf.PutString( GetMapName() ); buf.PutString( "csgo" ); buf.PutString( "Counter-Strike: Global Offensive" );
// The next field is a 16-bit version of the AppID. If our AppID < 65536,
// then let's go ahead and put in in there, to maximize compatibility
// with old clients who might be only using this field but not the new one.
// However, if our AppID won't fit, there's no way we can be compatible,
// anyway, so just put in a zero, which is better than a bogus AppID.
uint16 usAppIdShort = ( uint16 ) appIdResponse; buf.PutShort( LittleWord( usAppIdShort ) );
// player info
buf.PutUnsignedChar( GetNumPlayers() ); buf.PutUnsignedChar( GetMaxHumanPlayers() ); buf.PutUnsignedChar( MAX( 0, GetNumFakeClients() - GetNumProxies() ) );
// NOTE: This key's meaning is changed in the new version. Since we send gameport and specport,
// it knows whether we're running SourceTV or not. Then it only needs to know if we're a dedicated or listen server.
if ( IsDedicated() ) buf.PutUnsignedChar( 'd' ); // d = dedicated server
else buf.PutUnsignedChar( 'l' ); // l = listen server
#if defined(_WIN32)
buf.PutUnsignedChar( 'w' ); #elif defined(OSX)
buf.PutUnsignedChar( 'm' ); #else // LINUX?
buf.PutUnsignedChar( 'l' ); #endif
// Password?
buf.PutUnsignedChar( GetPassword() ? 1 : 0 ); buf.PutUnsignedChar( Steam3Server().BSecure() ? 1 : 0 ); buf.PutString( GetHostVersionString() );
//
// NEW DATA.
//
// Write a byte with some flags that describe what is to follow.
byte nNewFlags = 0; nNewFlags |= S2A_EXTRA_DATA_HAS_GAME_PORT;
// if ( GetSteamID().IsValid() )
// nNewFlags |= S2A_EXTRA_DATA_HAS_STEAMID;
extern bool CanShowHostTvStatus(); CHLTVServer *hltv = NULL; for ( CActiveHltvServerIterator it; it; it.Next() ) { if ( CanShowHostTvStatus() ) { hltv = it; // pass spectator data (port and name)
nNewFlags |= S2A_EXTRA_DATA_HAS_SPECTATOR_DATA; } break; }
if ( m_GameType.String()[ 0 ] != '\0' ) nNewFlags |= S2A_EXTRA_DATA_HAS_GAMETAG_DATA;
nNewFlags |= S2A_EXTRA_DATA_GAMEID;
buf.PutUnsignedChar( nNewFlags );
// Write the rest of the data.
if ( nNewFlags & S2A_EXTRA_DATA_HAS_GAME_PORT ) { buf.PutShort( LittleWord( GetUDPPort() ) ); }
if ( nNewFlags & S2A_EXTRA_DATA_HAS_SPECTATOR_DATA ) { buf.PutShort( LittleWord( hltv->GetUDPPort() ) ); buf.PutString( host_name_store.GetBool() ? GetName() : "Counter-Strike: Global Offensive" ); }
if ( nNewFlags & S2A_EXTRA_DATA_HAS_GAMETAG_DATA ) { buf.PutString( m_GameType.String() ); }
if ( nNewFlags & S2A_EXTRA_DATA_GAMEID ) { // !FIXME! Is there a reason we aren't using the other half
// of this field? Shouldn't we put the game mod ID in there, too?
// We have the game dir.
buf.PutInt64( LittleQWord( CGameID( appIdResponse ).ToUint64() ) ); }
NET_SendPacket( NULL, m_Socket, packet->from, ( unsigned char * ) buf.Base(), buf.TellPut() ); } break; } } break;
case A2S_PLAYER: // Handle A2S_PLAYER ourselves, we are concealing SteamID of the game server and report players differently
if ( g_bSteamMasterHeartbeatsEnabled ) { extern ConVar host_players_show; switch ( host_players_show.GetInt() ) { case 2: goto steam3server_oob_packet_handler; case 1: { CUtlBuffer buf; buf.EnsureCapacity( MAX_ROUTABLE_PAYLOAD );
buf.PutUnsignedInt( LittleDWord( CONNECTIONLESS_HEADER ) ); buf.PutUnsignedChar( S2A_PLAYER ); buf.PutUnsignedChar( 1 ); buf.PutUnsignedChar( 0 ); buf.PutString( "Max Players" ); buf.PutInt( GetMaxHumanPlayers() );
if ( m_fStartTime == 0 ) // record when we started
{ m_fStartTime = Sys_FloatTime(); } float flTime = Sys_FloatTime() - m_fStartTime; float flLittleTime; LittleFloat( &flLittleTime, &flTime ); buf.PutFloat( flLittleTime );
NET_SendPacket( NULL, m_Socket, packet->from, ( unsigned char * ) buf.Base(), buf.TellPut() ); } break; } } break;
case A2S_RULES: if ( g_bSteamMasterHeartbeatsEnabled ) { extern ConVar host_rules_show; if ( host_rules_show.GetBool() ) goto steam3server_oob_packet_handler; } break;
default: { static bool s_bEnableLegacyPackets = !!CommandLine()->FindParm( "-enablelegacypackets" ); if ( !s_bEnableLegacyPackets ) break;
steam3server_oob_packet_handler:
// We don't understand it, let the master server updater at it.
if ( packet->from.IsType< netadr_t >() && Steam3Server().SteamGameServer() && Steam3Server().IsMasterServerUpdaterSharingGameSocket() ) { const netadr_t &adr = packet->from.AsType< netadr_t>(); Steam3Server().SteamGameServer()->HandleIncomingPacket( packet->message.GetBasePointer(), packet->message.TotalBytesAvailable(), adr.GetIPHostByteOrder(), adr.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]->IsConnected() && 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 ( m_Clients[ i ]->IsConnected() && (m_Clients[ i ]->IsHLTV() #if defined( REPLAY_ENABLED )
|| m_Clients[ i ]->IsReplay() #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 ); // WARNING: using raw bytes access to player info instead of GetPlayerInfo to save server CPU when
// responding to server info queries. Structure not byteswapped, must use GetPlayerInfo.
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 || !GetUserInfoTable() ) return false;
if ( nClientIndex < 0 || 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 (big endian)
CByteswap byteswap; byteswap.SetTargetBigEndian( true ); byteswap.SwapFieldsToTargetEndian( pinfo );
return true; }
void CBaseServer::UserInfoChanged( int nClientIndex ) { if ( !m_pUserInfoTable ) return;
player_info_t pi;
bool oldlock = networkStringTableContainerServer->Lock( false ); if ( m_Clients[ nClientIndex ]->FillUserInfo( pi ) ) { // Fixup to network order (big endian)
CByteswap byteswap; byteswap.SetTargetBigEndian( true ); 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(CSVCMsg_ServerInfo &serverinfo) { char gamedir[MAX_OSPATH]; Q_FileBase( com_gamedir, gamedir, sizeof( gamedir ) );
serverinfo.set_protocol( GetHostVersion() ); serverinfo.set_server_count( GetSpawnCount() ); serverinfo.set_map_crc( worldmapCRC ); serverinfo.set_client_crc( clientDllCRC ); serverinfo.set_string_table_crc( CRC32_ConvertToUnsignedLong( &stringTableCRC ) ); serverinfo.set_max_clients( GetMaxClients() ); serverinfo.set_max_classes( serverclasses ); serverinfo.set_is_dedicated( IsDedicated() ); #ifdef _WIN32
serverinfo.set_c_os( 'W' ); #else
serverinfo.set_c_os( 'L' ); #endif
// HACK to signal that the server is "new"
serverinfo.set_c_os( tolower( serverinfo.c_os() ) );
serverinfo.set_tick_interval( GetTickInterval() ); serverinfo.set_game_dir( gamedir ); serverinfo.set_map_name( GetMapName() ); serverinfo.set_map_group_name( GetMapGroupName() ); serverinfo.set_sky_name( m_szSkyname ); extern ConVar host_name_store; serverinfo.set_host_name( host_name_store.GetBool() ? GetName() : "Counter-Strike: Global Offensive" ); serverinfo.set_is_hltv( IsHLTV() ); serverinfo.set_is_redirecting_to_proxy_relay( false ); char szMapPath[MAX_PATH]; V_ComposeFileName( "maps", GetMapName(), szMapPath, sizeof(szMapPath) ); serverinfo.set_ugc_map_id( serverGameDLL->GetUGCMapFileID( szMapPath ) );
#if defined( REPLAY_ENABLED )
serverinfo.set_is_replay( IsReplay() ); #endif
// Don't expose server public IP in the server info, the client is already connected, so there's no reason to store it either
// if( Steam3Server().SteamGameServer() == NULL )
// serverinfo.set_public_ip( NULL );
// else
// serverinfo.set_public_ip( Steam3Server().SteamGameServer()->GetPublicIP() );
}
/*
================= 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( const ns_address &adr, bf_read &inmsg ) { if ( !CanAcceptChallengesFrom( adr ) ) // Silently swallow the challenge request
return;
// Check if we are running with a special flag and not advertise to Steam
if ( !V_strcmp( g_pLaunchOptions->GetFirstSubKey()->GetNextKey()->GetNextKey()->GetString(), "server_is_unavailable" ) ) { DevMsg( "Server running with server_is_unavailable, ignoring challenge from %s\n", ns_address_render( adr ).String() ); return; }
char ALIGN4 buffer[512] ALIGN4_POST; bf_write msg(buffer,sizeof(buffer));
char context[ 256 ] = { 0 }; inmsg.ReadString( context, sizeof( context ) );
// get a free challenge number
int challengeNr = GetChallengeNr( adr ); int authprotocol = GetChallengeType( adr );
msg.WriteLong( CONNECTIONLESS_HEADER ); msg.WriteByte( S2C_CHALLENGE ); msg.WriteLong( challengeNr ); msg.WriteLong( authprotocol );
bool bWroteInfo = false; uint64 ullSteamIDGS = 0ull;
#if !defined( NO_STEAM )
ullSteamIDGS = Steam3Server().GetGSSteamID().ConvertToUint64(); if ( authprotocol == PROTOCOL_STEAM ) { msg.WriteShort( 0 ); // steam2 encryption key not there anymore
msg.WriteLongLong( ullSteamIDGS ); msg.WriteByte( Steam3Server().BSecure() ); bWroteInfo = true; } #endif
if ( !bWroteInfo ) { msg.WriteShort( 1 ); msg.WriteLongLong( ullSteamIDGS ); msg.WriteByte( 0 ); }
bool bReloadServerMap = false;
if ( StringHasPrefix( context, "connect" ) ) { bool bIsLocalConnection = adr.IsLoopback() || adr.IsLocalhost(); // A local player never has to enter a PW
bool bWillRequirePassword = ( GetPassword() != NULL ) && !bIsLocalConnection;
// Validate the challenge if the challenge was passed as part of connect
// packet:
unsigned long nChallenge = 0; char const *pszValidate = context + Q_strlen( "connect" ); if ( pszValidate[0] == '0' && pszValidate[1] == 'x' ) { char chValidateChallenge[ 9 ] = {0}; Q_strncpy( chValidateChallenge, pszValidate + 2, sizeof( chValidateChallenge ) ); nChallenge = strtoul( chValidateChallenge, NULL, 16 ); }
bool bAllowDC = !serverGameDLL->IsValveDS(); // Official DS direct connect disabled
if ( bAllowDC ) bAllowDC = serverGameDLL->ShouldAllowDirectConnect(); // let the ongoing game overrule direct connect
if ( bAllowDC ) { //
// Game server must be logged on with a persistent GSLT unless LAN case
//
static const bool s_bAllowLanWhitelist = !CommandLine()->FindParm( "-ignorelanwhitelist" ); bool bWhitelistedClient = bIsLocalConnection // localhost/loopback
|| ( s_bAllowLanWhitelist && ( adr.IsReservedAdr() // LAN RFC 1918
|| ( ( adr.GetAddressType() == NSAT_NETADR ) && ( adr.GetIP() == Steam3Server().GetPublicIP() ) ) // same public IP (pinhole NAT or same host in DMZ)
) ); bool bPersistentGameServerAccount = !sv_lan.GetBool() && !Steam3Server().BLanOnly() && Steam3Server().GetGSSteamID().IsValid() && Steam3Server().GetGSSteamID().BPersistentGameServerAccount(); if ( IsDedicated() && !bWhitelistedClient && !bPersistentGameServerAccount ) bAllowDC = false; // just fail direct connect
}
if ( IsExclusiveToLobbyConnections() && !GetReservationCookie() ) { // Server is in the state when it requires lobby connection, but
// is unreserved.
// For untrusted clients: we need to tell them to retry with
// a valid challenge
if ( nChallenge != ( unsigned long ) challengeNr ) { msg.WriteString( "connect-retry" ); } // For trusted clients: give them a grace period to establish
// the required lobby and submit a reservation request
else { if ( !bAllowDC ) { if ( serverGameDLL->IsValveDS() ) { // Do not allow direct-connect to Valve dedicated servers
msg.WriteString( "connect-matchmaking-only" ); DevMsg( "Cannot direct-connect to Valve CS:GO Server\n" ); } else { // Do not allow direct-connect to this community server
msg.WriteString( "connect-lan-only" ); DevMsg( "Cannot direct-connect to community CS:GO Server, visit http://steamcommunity.com/dev/managegameservers\n" );
static bool s_bPrintedInfo = false; if ( sv.IsDedicated() && !s_bPrintedInfo ) { s_bPrintedInfo = true;
Warning( "****************************************************\n" ); Warning( "* Client connections are restricted to LAN only. *\n" ); Warning( "* Double-check your sv_setsteamaccount setting. *\n" ); Warning( "* To create a game server account go to *\n" ); Warning( "* http://steamcommunity.com/dev/managegameservers *\n" ); Warning( "****************************************************\n" ); } } } else { #if ENGINE_CONNECT_VIA_MMS
msg.WriteString( "connect-granted" ); m_flTimeReservationGraceStarted = net_time; m_adrReservationGraceStarted = adr;
DevMsg( "Server requires lobby reservation and is unreserved: granting reservation grace period to %s\n", ns_address_render( adr ).String() ); #else
if ( m_pnReservationCookieSession && *m_pnReservationCookieSession ) { // Reserve in place
m_flReservationExpiryTime = net_time + sv_mmqueue_reservation_timeout.GetFloat(); SetReservationCookie( *m_pnReservationCookieSession, "[R] Connect from %s", ns_address_render( adr ).String() ); bReloadServerMap = true; // Reply with whatever context was requested
msg.WriteString( context ); } else { DevMsg( "Server is not connected to matchmaking backend, ignoring challenge from %s\n", ns_address_render( adr ).String() ); return; } #endif
} } } else { // Otherwise just reply with whatever context was requested
msg.WriteString( context ); }
char const *pszLobbyType = ""; if ( IsExclusiveToLobbyConnections() ) { pszLobbyType = "public"; if ( sv_steamgroup_exclusive.GetBool() || GetPassword() ) { pszLobbyType = "friends"; } }
msg.WriteLong( GetHostVersion() ); msg.WriteString( pszLobbyType );
msg.WriteByte( bWillRequirePassword ? 1 : 0 );
#if ENGINE_CONNECT_VIA_MMS
if ( !bAllowDC ) { msg.WriteLongLong( (uint64)-1 ); msg.WriteByte( 0 ); } else { if ( !IsHLTV() ) { msg.WriteLongLong( sv.GetReservationCookie() ); } else { msg.WriteLongLong( 0 ); } static ConVar *dcFriendsReqd = cvar->FindVar( "sv_dc_friends_reqd" ); if ( !IsHLTV() && dcFriendsReqd ) { msg.WriteByte( dcFriendsReqd->GetInt() ); } else { msg.WriteByte( 0 ); } } #else
msg.WriteLongLong( ( uint64 ) -1 ); msg.WriteByte( 0 ); #endif
msg.WriteByte( serverGameDLL->IsValveDS() ? 1 : 0 ); //
// Server will send the certificate and its signature down to the client
//
const byte *pbNetEncryptPublic = NULL; int cbNetEncryptPublic = 0; const byte *pbNetEncryptSignature = NULL; int cbNetEncryptSignature = 0; bool bServerRequiresEncryptedChannels = ( ( this == &sv ) && NET_CryptGetNetworkCertificate( k_ENetworkCertificate_PublicKey, &pbNetEncryptPublic, &cbNetEncryptPublic ) && NET_CryptGetNetworkCertificate( k_ENetworkCertificate_Signature, &pbNetEncryptSignature, &cbNetEncryptSignature ) ); msg.WriteByte( bServerRequiresEncryptedChannels ? 1 : 0 ); if ( bServerRequiresEncryptedChannels ) { msg.WriteLong( cbNetEncryptPublic ); msg.WriteBytes( pbNetEncryptPublic, cbNetEncryptPublic ); msg.WriteLong( cbNetEncryptSignature ); msg.WriteBytes( pbNetEncryptSignature, cbNetEncryptSignature ); } } else { msg.WriteString( context ); }
NET_SendPacket( NULL, m_Socket, adr, msg.GetData(), msg.GetNumBytesWritten() );
//
// Force the map to be reloaded upon first connection
//
if ( bReloadServerMap && IsDedicated() ) { Cbuf_AddText( CBUF_SERVER, CFmtStr( "nextlevel %s\n", GetMapName() ) ); // gamerules will clean it up when they construct for the next map
Cbuf_AddText( CBUF_SERVER, CFmtStr( "map %s reserved\n", GetMapName() ) ); Cbuf_Execute(); } }
/*
================= 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(const ns_address &adr) { char buffer[16]; 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() ); }
//-----------------------------------------------------------------------------
// Purpose: encrypts an 8-byte sequence
//-----------------------------------------------------------------------------
static inline void Decrypt8ByteSequence( IceKey& cipher, const unsigned char *cipherText, unsigned char *plainText ) { cipher.decrypt( cipherText, plainText ); }
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
static void DecryptBuffer( IceKey& cipher, unsigned char *bufData, uint bufferSize) { unsigned char *cipherText = bufData; unsigned char *plainText = bufData; uint bytesEncrypted = 0;
while (bytesEncrypted < bufferSize) { // encrypt 8 byte section
Decrypt8ByteSequence( cipher, cipherText, plainText ); bytesEncrypted += 8; cipherText += 8; plainText += 8; } }
//-----------------------------------------------------------------------------
// Purpose: Replies to a reservation request sent by the client
//-----------------------------------------------------------------------------
void CBaseServer::ReplyReservationRequest( const ns_address &adr, bf_read &msgIn ) { if ( msgIn.ReadLong() != GetHostVersion() ) return;
byte decrypted[ 1024 ];
bool bAccepted = false; static char const * s_pchTournamentServer = CommandLine()->ParmValue( "-tournament", ( char const * ) NULL ); if ( s_pchTournamentServer ) { DevMsg( "Reservation request from %s rejected: server running in tournament mode for '%s'\n", ns_address_render( adr ).String(), s_pchTournamentServer ); } else if ( IsReserved() ) { // Special case -- server is reserved, but takes too long
// to switch the map, client is retrying meanwhile, we don't want
// to blow them off with a failure code!
if ( m_ReservationStatus.m_bActive && m_ReservationStatus.m_bSuccess && m_ReservationStatus.m_Remote.CompareAdr( adr ) ) { DevMsg( "Ignoring follow-up reservation request from %s: server reservation transition in progress (for %d more seconds)\n", ns_address_render( adr ).String(), (int) ( m_flReservationExpiryTime - net_time ) ); SendReservationStatus( kEReservationStatusPending ); return; }
DevMsg( "Reservation request from %s rejected: server already reserved (for %d more seconds)\n", ns_address_render( adr ).String(), (int) ( m_flReservationExpiryTime - net_time ) ); } else if ( GetNumClients() - GetNumFakeClients() > 0 ) { DevMsg( "Reservation request from %s rejected: server not empty\n", ns_address_render( adr ).String() ); } else if ( !V_strcmp( g_pLaunchOptions->GetFirstSubKey()->GetNextKey()->GetNextKey()->GetString(), "server_is_unavailable" ) ) { DevMsg( "Reservation request from %s rejected: server is running with flag server_is_unavailable\n", ns_address_render( adr ).String() ); } else { bool bOkay = true; if ( !IsX360() && !IsDedicatedForXbox() ) { int nChallengeNr = GetChallengeNr( adr ); if ( !CanAcceptChallengesFrom( adr ) ) { DevMsg( "Reservation request from address %s, but challenges exclusive for %s\n", ns_address_render( adr ).String(), m_adrReservationGraceStarted.ToString() ); bOkay = false; } else if ( !nChallengeNr ) { DevMsg( "Reservation request from unknown address %s\n", ns_address_render( adr ).String() ); bOkay = false; } else { int payloadSize = msgIn.ReadLong(); if ( payloadSize <= 0 || payloadSize > sizeof( decrypted ) || ( payloadSize % 8 ) ) { DevMsg( "ReplyReservationRequest: Reservation request with bogus payload size from %s [%d bytes]\n", ns_address_render( adr ).String(), payloadSize ); bOkay = false; } else { IceKey cipher(1); /* medium encryption level */ unsigned char ucEncryptionKey[8] = { 0 }; *( int * )&ucEncryptionKey[ 0 ] = LittleDWord( nChallengeNr ^ 0x5ef8ce12 ); *( int * )&ucEncryptionKey[ 4 ] = LittleDWord( nChallengeNr ^ 0xaa98e42c );
cipher.set( ucEncryptionKey );
msgIn.ReadBytes( decrypted, payloadSize ); // Try and decrypt it
DecryptBuffer( cipher, decrypted, payloadSize );
// Rewind and use decrypted payload
msgIn.StartReading( decrypted, payloadSize );
unsigned int nMagic = msgIn.ReadLong(); if ( nMagic == 0xfeedbeef ) { bOkay = true; } else { Msg( "ReplyReservationRequest: Reservation request with bogus payload data from %s [%d bytes]\n", ns_address_render( adr ).String(), payloadSize ); } } } }
if ( bOkay ) { //
// Parse the incoming message
//
uint64 nReservationCookie; int nSettingsSize;
nReservationCookie = msgIn.ReadLongLong(); nSettingsSize = msgIn.ReadLong();
float flExpiryTime = sv_reservation_timeout.GetFloat();
DevMsg( "Reservation request from %s accepted: server reserved with reservation cookie 0x%llx for %.1f seconds\n", ns_address_render( adr ).String(), nReservationCookie, flExpiryTime ); DevMsg( " settings size = %d\n", nSettingsSize );
//
// Establish reservation time
//
bAccepted = true;
// This time will be held until the first client connects, at which time the reservation will stay
// in effect until the last client leaves the server
m_flReservationExpiryTime = net_time + flExpiryTime;
SetReservationCookie( nReservationCookie, "ReplyReservationRequest" );
//
// Process game settings
//
m_numGameSlots = 0; if ( nSettingsSize > 0 ) { KeyValues *pKV = new KeyValues(""); KeyValues::AutoDelete autodelete_pKV( pKV ); byte tmp[MAX_OOB_KEYVALUES]; Assert( nSettingsSize <= sizeof( tmp ) );
if ( nSettingsSize <= sizeof( tmp ) ) { // convert from binary to keyvalues
CUtlBuffer buf; msgIn.ReadBytes( tmp, nSettingsSize ); buf.Put( tmp, nSettingsSize ); pKV->ReadAsBinary( buf );
if ( sv_reservation_tickrate_adjustment.GetInt() > 0 ) { float fDesiredTickInterval = 1.0f / sv_reservation_tickrate_adjustment.GetInt(); // quantize tick intervals to the nearest N / 512 fraction
float fNumerator = floorf(fDesiredTickInterval * 512.0f + 0.5f); host_state.interval_per_tick = fNumerator / 512.0f; host_state.interval_per_tick = clamp( host_state.interval_per_tick, MINIMUM_TICK_INTERVAL, MAXIMUM_TICK_INTERVAL ); DevMsg( "Reservation tickrate adjustment to %.0f ticks %.6f ms\n", floorf( 1.0f/host_state.interval_per_tick ), host_state.interval_per_tick ); if ( host_state.interval_per_tick < MINIMUM_TICK_INTERVAL || host_state.interval_per_tick > MAXIMUM_TICK_INTERVAL ) { Sys_Error( "GetTickInterval returned bogus tick interval (%f)[%f to %f is valid range]", host_state.interval_per_tick, MINIMUM_TICK_INTERVAL, MAXIMUM_TICK_INTERVAL ); }
extern ConVar sv_maxupdaterate; extern ConVar sv_maxcmdrate; sv_maxupdaterate.SetValue(1.0f / host_state.interval_per_tick); sv_maxcmdrate.SetValue(1.0f / host_state.interval_per_tick); }
// let the game know the settings
serverGameDLL->ApplyGameSettings( pKV ); // adjust the game slots
m_numGameSlots = pKV->GetInt( "members/numSlots", 0 ); #ifdef PORTAL2 // HACK: PORTAL2 uses maxclients instead of GAMERULES
SetMaxClients( m_numGameSlots ); #endif
} } } }
m_ReservationStatus.m_bActive = true; m_ReservationStatus.m_bSuccess = bAccepted; m_ReservationStatus.m_Remote = adr;
// Send the status right away
SendReservationStatus( bAccepted ? kEReservationStatusPending : kEReservationStatusRejected ); }
void CBaseServer::ReplyReservationCheckRequest( const ns_address &adr, bf_read &msgIn ) { if ( msgIn.ReadLong() != GetHostVersion() ) return;
uint32 token = msgIn.ReadLong(); uint32 uiReservationStage = msgIn.ReadLong(); uint64 nReservationCookie= msgIn.ReadLongLong(); bool reservationMatch = ( nReservationCookie == GetReservationCookie() ); uint64 uiClientSteamID = msgIn.ReadLongLong(); uint8 uiAwaitingClients = 0x7F; // large positive int8 number
if ( uiReservationStage && reservationMatch && uiClientSteamID && sv_mmqueue_reservation.GetString()[0] == 'Q' ) { DevMsg( "Reservation from client %u: %u\n", uint32( uiClientSteamID ), uiReservationStage ); // For queue reservation client ID must match
bool bThisClientShouldJoin = false, bThisClientJustConfirmed = false; uint8 uiActualAwaitingClients = 0; for ( int k = 0; k < m_arrReservationPlayers.Count(); ++ k ) { QueueMatchPlayer_t &qmp = m_arrReservationPlayers[k]; if ( qmp.m_uiAccountID == CSteamID( uiClientSteamID ).GetAccountID() ) { bThisClientShouldJoin = true; qmp.m_adr = adr; qmp.m_uiToken = token; if ( qmp.m_uiReservationStage < uiReservationStage ) bThisClientJustConfirmed = true; qmp.m_uiReservationStage = uiReservationStage; } if ( qmp.m_uiReservationStage < uiReservationStage ) uiActualAwaitingClients += 1; }
// Reservation matches, report to this client how many more clients we are awaiting
if ( bThisClientShouldJoin ) { uiAwaitingClients = uiActualAwaitingClients;
if ( bThisClientJustConfirmed && serverGameDLL ) { // Report to the GC the new level of confirmations
CUtlVector< uint32 > arrConfirmedAccounts; arrConfirmedAccounts.EnsureCapacity( m_arrReservationPlayers.Count() ); uint32 uiMinReservationLevel = 0xFFFFu; for ( int jj = 0; jj < m_arrReservationPlayers.Count(); ++ jj ) { if ( m_arrReservationPlayers[jj].m_uiReservationStage < uiMinReservationLevel ) uiMinReservationLevel = m_arrReservationPlayers[jj].m_uiReservationStage; } for ( int jj = 0; jj < m_arrReservationPlayers.Count(); ++ jj ) { if ( ( m_arrReservationPlayers[jj].m_uiReservationStage >= 2 ) || ( m_arrReservationPlayers[jj].m_uiReservationStage > uiMinReservationLevel ) ) arrConfirmedAccounts.AddToTail( m_arrReservationPlayers[jj].m_uiAccountID ); } DevMsg( "Match start status: %u/%u\n", uiMinReservationLevel, arrConfirmedAccounts.Count() ); serverGameDLL->ReportGCQueuedMatchStart( uiMinReservationLevel, arrConfirmedAccounts.Base(), arrConfirmedAccounts.Count() );
if ( !uiActualAwaitingClients ) { // Extend the reservation a little bit more since the match is now confirmed to be starting
m_flReservationExpiryTime = net_time + sv_mmqueue_reservation_extended_timeout.GetFloat(); DevMsg( "Reservation time extended +%d sec\n", sv_mmqueue_reservation_extended_timeout.GetInt() ); } } } }
if ( uiReservationStage && reservationMatch && uiClientSteamID && sv_mmqueue_reservation.GetString()[ 0 ] == 'G' ) { DevMsg( "Reservation from in-progress client %u: %u\n", uint32( uiClientSteamID ), uiReservationStage );
// Reservation matches, report to this client how many more clients we are awaiting
uiAwaitingClients = 0;
// Extend the reservation a little bit more since the match is now confirmed to be starting
m_flReservationExpiryTime = net_time + sv_mmqueue_reservation_extended_timeout.GetFloat(); DevMsg( "Reservation time extended +%d sec\n", sv_mmqueue_reservation_extended_timeout.GetInt() ); }
//
// Send response to this client
//
char buffer[64]; bf_write msg(buffer,sizeof(buffer));
msg.WriteLong( CONNECTIONLESS_HEADER ); msg.WriteByte( S2A_RESERVE_CHECK_RESPONSE ); msg.WriteLong( GetHostVersion() ); msg.WriteLong( token ); msg.WriteLong( uiReservationStage ); msg.WriteByte( uiAwaitingClients ); NET_SendPacket( NULL, m_Socket, adr, msg.GetData(), msg.GetNumBytesWritten() );
//
// Send response to all clients too when this packet is really important
//
if ( !uiAwaitingClients ) { // Make sure we send this notification to all clients to force them to connect
// to our server as soon as everybody confirmed
for ( int jj = 0; jj < m_arrReservationPlayers.Count(); ++ jj ) { char buffer[64]; bf_write msg(buffer,sizeof(buffer));
msg.WriteLong( CONNECTIONLESS_HEADER ); msg.WriteByte( S2A_RESERVE_CHECK_RESPONSE ); msg.WriteLong( GetHostVersion() ); msg.WriteLong( m_arrReservationPlayers[jj].m_uiToken ); msg.WriteLong( uiReservationStage ); msg.WriteByte( uiAwaitingClients );
NET_SendPacket( NULL, m_Socket, m_arrReservationPlayers[jj].m_adr, msg.GetData(), msg.GetNumBytesWritten() ); } } }
void CBaseServer::ClearReservationStatus() { m_ReservationStatus.m_bActive = false; }
void CBaseServer::FlagForSteamIDReuseAfterShutdown() { m_flFlagForSteamIDReuseAfterShutdownTime = Plat_FloatTime(); }
void CBaseServer::SendReservationStatus( EReservationStatus_t kEReservationStatus ) { if ( !m_ReservationStatus.m_bActive ) { return; }
char buffer[32]; bf_write msg(buffer,sizeof(buffer));
msg.WriteLong( CONNECTIONLESS_HEADER ); msg.WriteByte( S2A_RESERVE_RESPONSE ); msg.WriteLong( GetHostVersion() ); msg.WriteByte( kEReservationStatus ); msg.WriteOneBit( ( serverGameDLL && serverGameDLL->IsValveDS() ) ? 1 : 0 ); // whether we are running ValveDS
msg.WriteLong( m_numGameSlots ); // number of game slots actually set on the server
DevMsg( "Reservation response to %s: %u, server reserved for %d more seconds\n", ns_address_render( m_ReservationStatus.m_Remote ).String(), kEReservationStatus, (int) ( m_flReservationExpiryTime - net_time ) );
NET_SendPacket( NULL, m_Socket, m_ReservationStatus.m_Remote, msg.GetData(), msg.GetNumBytesWritten() );
if ( kEReservationStatus != kEReservationStatusPending ) ClearReservationStatus(); }
const char *CBaseServer::GetName( void ) const { return host_name.GetString(); }
bool UseCDKeyAuth() { #ifdef NO_STEAM
return true; #endif
// if we are physically on a 360 (360 listen server) or we are on a PC dedicated server for 360 clients, don't require Steam auth
if ( IsX360() || NET_IsDedicatedForXbox() ) return true;
// for single player games that don't use Steam features, don't require Steam auth
if ( !serverGameDLL->ShouldPreferSteamAuth() && Host_IsSinglePlayerGame() ) return true;
#ifndef DEDICATED
// for listen servers, if the Steam interface is not available (Steam not running), OK to not require Steam auth
if ( !sv.IsDedicated() && !Steam3Client().SteamUser() ) return true; #endif
// require Steam auth
return false; }
int CBaseServer::GetChallengeType( const ns_address &adr) { if ( UseCDKeyAuth() ) { return PROTOCOL_HASHEDCDKEY; } else { return PROTOCOL_STEAM; } }
int CBaseServer::GetChallengeNr ( const ns_address &adr) { int oldest = 0; float oldestTime = FLT_MAX; // see if we already have a challenge for this ip
for (int i = 0 ; i < m_ServerQueryChallenges.Count() ; i++) { if ( adr.CompareAdr (m_ServerQueryChallenges[i].adr, true) ) { // reuse challenge number, but update time
m_ServerQueryChallenges[i].time = net_time; return m_ServerQueryChallenges[i].challenge; } if (m_ServerQueryChallenges[i].time < oldestTime) { // remember oldest challenge
oldestTime = m_ServerQueryChallenges[i].time; oldest = i; } }
if ( m_ServerQueryChallenges.Count() > MAX_CHALLENGES ) { m_ServerQueryChallenges.FastRemove( oldest ); }
int newEntry = m_ServerQueryChallenges.AddToTail(); // note the 0x0FFF of the top 16 bits, so that -1 will never be sent as a challenge
m_ServerQueryChallenges[newEntry].challenge = (RandomInt(0,0x0FFF) << 16) | RandomInt(0,0xFFFF); m_ServerQueryChallenges[newEntry].adr = adr; m_ServerQueryChallenges[newEntry].time = net_time; return m_ServerQueryChallenges[newEntry].challenge; }
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; }
if(m_fStartTime==0) // record when we started
{ m_fStartTime=Sys_FloatTime(); }
if( Sys_FloatTime () > 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(Sys_FloatTime () > 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 ( _PS3 )
// FAKE
m_fCPUPercent = 0.1; #elif defined ( LINUX )
// FAKE
m_fCPUPercent = 0.1; #elif defined ( POSIX )
/*
// linux CPU % code here :)
static int32 lastrunticks,lastcputicks; static float lastAvg=0;
struct sysinfo infos; int32 dummy; int length; char statFile[PATH_MAX]; int32 now = time(NULL); int32 ctime,stime,start_time; FILE *pFile; int32 runticks,cputicks;
snprintf(statFile,PATH_MAX,"/proc/%i/stat",getpid()); // we can't use FS_Open() cause its outside our dir
pFile = fopen(statFile, "r"); if ( pFile == NULL ) { goto end; } sysinfo(&infos);
fscanf(pFile, "%d %s %c %d %d %d %d %d %lu %lu \
%lu %lu %lu %ld %ld %ld %ld %ld %ld %lu \ %lu %ld %lu %lu %lu %lu %lu %lu %lu %lu \ %lu %lu %lu %lu %lu %lu", &dummy,statFile,&dummy,&dummy,&dummy,&dummy, &dummy,&dummy,&dummy,&dummy, // end of first line
&dummy,&dummy,&dummy,&ctime,&stime, &dummy,&dummy,&dummy,&dummy,&dummy, // end of second
&start_time,&dummy,&dummy,&dummy,&dummy, &dummy,&dummy,&dummy,&dummy,&dummy, // end of third
&dummy,&dummy,&dummy,&dummy,&dummy,&dummy); fclose(pFile);
runticks = infos.uptime*HZ -start_time; // time the process has been running
cputicks = stime+ctime; if(lastcputicks==0) { lastcputicks=cputicks; }
if(lastrunticks==0) { lastrunticks=runticks; } else { m_fCPUPercent = (float)(cputicks-lastcputicks)/(float)(runticks-lastrunticks); }
*/ //ConMsg("%f %li %li %li %li\n",cpuPercent,
// cputicks,(cputicks-lastcputicks),
// (runticks-lastrunticks),runticks);
static struct rusage s_lastUsage; static float 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 - lastAvg);
// now save this away for next time
if( m_fLastCPUCheckTime > lastAvg+5) { s_lastUsage = currentUsage; lastAvg = m_fLastCPUCheckTime; } } // limit checking :)
if( m_fCPUPercent > 0.999 ) m_fCPUPercent = 0.999; if( m_fCPUPercent < 0 ) m_fCPUPercent = 0; end:
#else
#error
#endif
m_fLastCPUCheckTime=Sys_FloatTime(); } }
//-----------------------------------------------------------------------------
// 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 (but split screen users don't)
if ( cl->IsFakeClient() && !cl->IsSplitScreenUser() && !cl->IsHLTV() #if defined( REPLAY_ENABLED )
&& !cl->IsReplay() #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->SetSignonState( SIGNONSTATE_CONNECTED ); CNETMsg_SignonState_t signon( cl->GetSignonState(), -1 ); cl->FillSignOnFullServerInfo( signon ); 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) { int i; 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; // Don't timeout in _DEBUG builds
#if !defined( _DEBUG )
if ( netchan->IsTimedOut() ) { if ( this == &sv ) { if ( CGameClient *pGameClient = dynamic_cast< CGameClient * >( cl ) ) { serverGameDLL->OnEngineClientNetworkEvent( pGameClient->edict, pGameClient->m_SteamID.ConvertToUint64(), INetChannelInfo::k_ENetworkEventType_TimedOut, NULL ); } } cl->Disconnect( CFmtStr( "%s timed out", cl->GetClientName() ) ); } else #endif
// Handle steam socket disconnection
if ( netchan->IsRemoteDisconnected() ) { cl->Disconnect( CFmtStr( "%s disconnected", cl->GetClientName() ) ); } else if ( netchan->IsOverflowed() ) { cl->Disconnect( CFmtStr( "%s overflowed reliable channel", cl->GetClientName() ) ); } } }
// ==================
// check if clients update thier user setting (convars) and call
// ==================
void CBaseServer::UpdateUserSettings(void) { for (int i=0 ; i< m_Clients.Count() ; i++ ) { CBaseClient *cl = m_Clients[ i ];
if ( cl->m_bConVarsChanged ) { cl->UpdateUserSettings(); } } }
// ==================
// check if clients need the serverinfo packet sent
// ==================
void CBaseServer::SendPendingServerInfo() { for (int i=0 ; i< m_Clients.Count() ; i++ ) { CBaseClient *cl = m_Clients[ i ];
if ( cl->m_bSendServerInfo ) { cl->SendServerInfo(); } } }
void CBaseServer::OnSteamServerLogonSuccess( uint32 externalIP ) { for (int i=0 ; i< m_Clients.Count() ; i++ ) { CBaseClient *cl = m_Clients[ i ]; cl->OnSteamServerLogonSuccess( externalIP ); } }
/*
================ SV_CheckProtocol
Make sure connecting client is using proper protocol ================ */ bool CBaseServer::CheckProtocol( const ns_address &adr, int nProtocol ) { if ( nProtocol != GetHostVersion() ) { // Client is newer than server
if ( nProtocol > GetHostVersion() ) { RejectConnection( adr, "This server is using an older protocol ( %i ) than your client ( %i ).\n", GetHostVersion(), nProtocol ); } else // Server is newer than client
{ RejectConnection( adr, "This server is using a newer protocol ( %i ) than your client ( %i ).\n", GetHostVersion(), nProtocol ); } return false; }
// Success
return true; }
/*
================ SV_CheckKeyInfo
Determine if client is outside appropriate address range ================ */ bool CBaseServer::CheckChallengeType( CBaseClient * client, int nNewUserID, const ns_address &adr, int nAuthProtocol, const char *pchLogonCookie, int cbCookie ) { // Check protocol ID
if ( ( nAuthProtocol <= 0 ) || ( nAuthProtocol > PROTOCOL_LASTVALID ) ) { RejectConnection( adr, "Invalid connection.\n"); return false; }
if ( ( nAuthProtocol == PROTOCOL_HASHEDCDKEY ) && (Q_strlen( pchLogonCookie ) <= 0 || Q_strlen(pchLogonCookie) != 32 ) ) { RejectConnection( adr, "Invalid authentication certificate length.\n" ); return false; }
if ( IsDedicatedForXbox() ) { return true; }
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, "STEAM certificate length error! %i/%i\n", cbCookie, STEAM_KEYSIZE ); return false; } ns_address checkAdr = adr; if ( adr.IsLoopback() || adr.IsLocalhost() ) { checkAdr.AsType<netadr_t>().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, "STEAM validation rejected\n" ); return false; } } else { if ( !Steam3Server().NotifyLocalClientConnect( client ) ) // the userID isn't alloc'd yet so we need to fill it in manually
{ RejectConnection( adr, "GSCreateLocalUser failed\n" ); return false; } }
return true; }
bool CBaseServer::CheckIPRestrictions( const ns_address &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() ) { if ( !adr.IsType<netadr_t>() ) return false;
// allow connection, if client is in the same subnet
if ( adr.AsType<netadr_t>().CompareClassBAdr( net_local_adr ) ) return true;
// allow connection, if client has a private IP
if ( adr.AsType<netadr_t>().IsReservedAdr() ) return true; // reject connection
return false; }
return true; }
void CBaseServer::SetMasterServerRulesDirty() { m_bMasterServerRulesDirty = true; }
bool CBaseServer::CheckPassword( const ns_address &adr, const char *password, const char *name ) { // If a server is reserved, then we ignore PW checks
// if ( GetReservationCookie() != 0ull )
// return true;
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::ClearBaselineHandles( void ) { FOR_EACH_MAP_FAST( m_BaselineHandles, i ) { g_pSerializedEntities->ReleaseSerializedEntity( m_BaselineHandles[ i ] ); } m_BaselineHandles.Purge(); }
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;
ClearBaselineHandles(); m_State = ss_dead; m_nTickCount = 0; Q_memset( m_szMapname, 0, sizeof( m_szMapname ) ); Q_memset( m_szBaseMapname, 0, sizeof( m_szBaseMapname ) ); Q_memset( m_szMapGroupName, 0, sizeof( m_szMapGroupName ) ); Q_memset( m_szSkyname, 0, sizeof( m_szSkyname ) );
clientDllCRC = 0; worldmapCRC = 0; stringTableCRC = 0;
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; }
/*
================ SV_RejectConnection
Rejects connection request and sends back a message ================ */ void CBaseServer::RejectConnection( const ns_address &adr, const char *fmt, ... ) { va_list argptr; char text[1024];
va_start (argptr, fmt); Q_vsnprintf ( text, sizeof( text ), fmt, argptr); va_end (argptr);
NET_OutOfBandPrintf( m_Socket, adr, "%c%s", S2C_CONNREJECT, text );
Warning( "RejectConnection: %s - %s\n", ns_address_render( adr ).String(), text ); }
void CBaseServer::SetPaused( bool paused ) { // pause request can be rejected, unpause needs to fall through
if ( m_State != ss_paused && !IsPausable() ) { return; }
if ( !IsActive() ) return;
if ( paused ) { m_State = ss_paused; } else { m_State = ss_active; }
CSVCMsg_SetPause_t setpause; setpause.set_paused( paused ); BroadcastMessage( setpause ); }
void CBaseServer::SetTimescale( float flTimescale ) { m_flTimescale = flTimescale; }
//-----------------------------------------------------------------------------
// Purpose: General initialization of the server
//-----------------------------------------------------------------------------
void CBaseServer::Init( bool bIsDedicated ) { m_nMaxclients = 0; m_nSpawnCount = 0; m_nUserid = 1; m_bIsDedicated = bIsDedicated; m_bIsDedicatedForXbox = bIsDedicated && ( CommandLine()->FindParm( "-xlsp" ) != 0 ); m_bIsDedicatedForPS3 = bIsDedicated && ( CommandLine()->FindParm( "-ps3ds" ) != 0 );
m_Socket = NS_SERVER; m_Signon.SetDebugName( "m_Signon" );
if ( !g_pKVrulesConvars ) { g_pKVrulesConvars = new KeyValues( "NotifyRulesCvars" ); if ( !g_pKVrulesConvars->LoadFromFile( g_pFullFileSystem, "gamerulescvars.txt", "MOD" ) ) { Warning( "Failed to load gamerulescvars.txt, game rules cvars might not be reported to management tools.\n" ); } } 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, SerializedEntityHandle_t *pHandle ) { if ( sv_instancebaselines.GetInt() ) { ErrorIfNot( pClass->m_InstanceBaselineIndex != INVALID_STRING_INDEX, ("SV_GetInstanceBaseline: missing instance baseline for class '%s'", pClass->m_pNetworkName) );
AUTO_LOCK_FM( g_svInstanceBaselineMutex ); int nSlot = m_BaselineHandles.Find( pClass->m_InstanceBaselineIndex ); ErrorIfNot( nSlot != m_BaselineHandles.InvalidIndex(), ( "CBaseServer::GetClassBaseline: for %s failed\n", pClass->GetName() ) );
*pHandle = m_BaselineHandles[ nSlot ]; Assert( *pHandle != SERIALIZED_ENTITY_HANDLE_INVALID ); return *pHandle != SERIALIZED_ENTITY_HANDLE_INVALID; }
*pHandle = SERIALIZED_ENTITY_HANDLE_INVALID; 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 ( !( CommandLine()->FindParm( "-fake_stale_server" ) ) ) { 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 is out of date and will be shutdown during hibernation or changelevel, whichever comes first.\n"); Log_Msg( LOG_SERVER_LOG, "Your server is out of date and will be shutdown during hibernation or changelevel, whichever comes first.\n");
SetRestartOnLevelChange( true );
Cbuf_AddText(CBUF_SERVER, "sv_shutdown\n"); Cbuf_Execute(); } #endif
#ifdef _WIN32
if (g_pFileSystem->IsSteam()) #else
else if ( 1 ) // under linux assume steam
#endif
{ Msg("Your server needs to be restarted in order to receive the latest update.\n"); Log_Msg( LOG_SERVER_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"); }
if ( serverGameDLL && serverGameDLL->IsValveDS() && !CommandLine()->FindParm( "-noautoupdate" ) ) { // Valve DS always posts sv_shutdown when out of date
Cbuf_AddText( CBUF_SERVER, "sv_shutdown\n" ); Cbuf_Execute(); } }
void CBaseServer::UpdateMasterServer() { if ( !ShouldUpdateMasterServer() ) return;
if ( !Steam3Server().SteamGameServer() ) return; // Call this every frame
ForwardPacketsFromMasterServerUpdater(); // Only update every so often.
double flCurTime = Plat_FloatTime(); if ( flCurTime - m_flLastMasterServerUpdateTime < MASTER_SERVER_UPDATE_INTERVAL ) return;
m_flLastMasterServerUpdateTime = flCurTime;
// If we are not in legacy mode then make sure server tags still get updated every so often.
// In legacy mode this is done by heartbeat code
UpdateGameData();
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 ) { bool bActive = IsActive() && IsMultiplayer() && g_bEnableMasterServerUpdater && !IsSinglePlayerGame(); if ( ShouldHideFromMasterServer() ) bActive = false;
g_bSteamMasterHeartbeatsEnabled = bActive; Steam3Server().SteamGameServer()->EnableHeartbeats( bActive );
if ( bActive ) { UpdateMasterServerRules(); UpdateMasterServerPlayers(); Steam3Server().SendUpdatedServerDetails(); } } if ( serverGameDLL && Steam3Server().GetGSSteamID().IsValid() ) serverGameDLL->UpdateGCInformation(); }
void CBaseServer::UpdateMasterServerRules() { // Only do this if the rules vars are dirty.
if ( !m_bMasterServerRulesDirty ) return;
ISteamGameServer *pGameServer = Steam3Server().SteamGameServer(); if ( !pGameServer ) return; pGameServer->ClearAllKeyValues(); // Need to respond with game directory, game name, and any server variables that have been set that
// affect rules. Also, probably need a hook into the .dll to respond with additional rule information.
ICvar::Iterator iter( g_pCVar ); for ( iter.SetFirst() ; iter.IsValid() ; iter.Next() ) { ConCommandBase *var = iter.Get();
if ( !(var->IsFlagSet( FCVAR_NOTIFY ) ) ) continue;
ConVar *pConVar = dynamic_cast< ConVar* >( var ); if ( !pConVar ) continue;
if ( !g_pKVrulesConvars->GetBool( pConVar->GetName() ) ) continue;
SetMasterServerKeyValue( pGameServer, 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. ================= */
#define PARENT_PROCESS_UPDATE_INTERVAL ( 10.0 ) // every 10 seconds
void UpdateParentProcess( void ) { #ifdef _LINUX
static double s_flNextParentProcessUpdate = -1; float flNow = Sys_FloatTime(); if ( flNow >= s_flNextParentProcessUpdate ) { s_flNextParentProcessUpdate = flNow + PARENT_PROCESS_UPDATE_INTERVAL; char sbuf[2048]; sprintf( sbuf, "status map=%s;players=%d", sv.GetMapName(), sv.GetNumClients() ); SendStringToParentProcess( sbuf ); } #endif
}
void CBaseServer::RunFrame( void ) { VPROF_BUDGET( "CBaseServer::RunFrame", VPROF_BUDGETGROUP_OTHER_NETWORKING );
NET_ProcessSocket( m_Socket, this );
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 ( IsChildProcess() ) { UpdateParentProcess(); // sned status to the parent process occasionally
}
if ( m_bMasterServerRulesDirty ) { m_bMasterServerRulesDirty = false; RecalculateTags(); }
ProcessSplitScreenDisconnects(); }
void CBaseServer::ProcessVoice( void ) { //This call will improve voice transmission with a slowed host_timescale. But will also send data to the clients too frequently depending on network throttling.
//It can cause prediction errors when not throttled. There is no throttling for listen server loopback connections. Thusly, listen server hosts have prediction errors.
//SendClientMessages( false );
}
CBaseClient * CBaseServer::GetFreeClient( const ns_address &adr ) { CBaseClient *client = GetFreeClientInternal( adr ); return client; }
//-----------------------------------------------------------------------------
// Purpose:
// Input : *adr -
// *pslot -
// **ppClient -
// Output : int
//-----------------------------------------------------------------------------
CBaseClient * CBaseServer::GetFreeClientInternal( const ns_address &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", ns_address_render( adr ).String() );
for ( int k = host_state.max_splitscreen_players; k -- > 1; ) { // Processing split screen users in reverse order
CBaseClient *& pSsUser = client->m_SplitScreenUsers[ k ]; if ( !pSsUser ) continue;
RemoveClientFromGame( pSsUser );
// perform a silent netchannel shutdown, don't send disconnect msg
pSsUser->m_NetChannel->Shutdown( NULL ); pSsUser->m_NetChannel = NULL;
pSsUser->Clear();
// Mark the split screen slot as empty
pSsUser = NULL; }
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 ); SNPROF("SendClientMessages"); 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) { ns_address adr; // it's an empty address
adr.Clear(); // sets NA_NULL
CBaseClient *fakeclient = GetFreeClient( adr ); if ( !fakeclient ) { // server is full
return NULL; }
INetChannel *netchan = NULL; if ( sv_stressbots.GetBool() ) { netchan = NET_CreateNetChannel( m_Socket, &adr, ns_address_render( adr ).String(), fakeclient, NULL, true ); }
// a NULL netchannel signals a fakeclient
m_nUserid = GetNextUserID(); fakeclient->Connect( name, m_nUserid, netchan, true, CROSSPLAYPLATFORM_THISPLATFORM );
// fake some cvar settings
//fakeclient->SetUserCVar( "name", name ); // set already by Connect()
fakeclient->SetUserCVar( "rate", CFmtStr( "%d", DEFAULT_RATE ) ); fakeclient->SetUserCVar( "cl_updaterate", CFmtStr( "%d", (int)( 1.0f / host_state.interval_per_tick ) ) ); fakeclient->SetUserCVar( "cl_cmdrate", CFmtStr( "%d", (int)( 1.0f / host_state.interval_per_tick ) ) ); 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" );
// create client in game.dll
fakeclient->ActivatePlayer();
fakeclient->m_nSignonTick = m_nTickCount; return fakeclient; }
void CBaseServer::Shutdown( void ) { g_pCVar->RemoveGlobalChangeCallback( ServerNotifyVarChangeCallback );
if ( !IsActive() ) return;
m_State = ss_dead;
CUtlVector< CBaseClient * > vecDelete;
// Let reliable messages go out
for ( int retry = 0; retry < 3; ++ retry ) { for ( int i = 0; i < m_Clients.Count(); ++ i ) { CBaseClient *cl = m_Clients[i]; if ( INetChannel *pChannel = cl->GetNetChannel() ) { pChannel->Transmit(); } } Sys_Sleep( 10 ); }
// 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() ) { // Hack, but this forces instant cleanup
if ( cl->IsSplitScreenUser() ) { cl->m_bSplitScreenUser = false; }
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(); }
vecDelete.AddToTail( cl );
m_Clients.Remove( i ); }
// Let drop messages go out
Sys_Sleep( 100 );
#if !defined( _X360 ) && !defined( NO_STEAM )
if ( !IsHLTV() ) { if ( m_flFlagForSteamIDReuseAfterShutdownTime && ( Plat_FloatTime() - m_flFlagForSteamIDReuseAfterShutdownTime < 1.0 ) ) ;// Server was flagged for shutdown and SteamID reuse, don't LogOff
else Steam3Server().DeactivateAndLogoff();
// Reset the shutdown flag
m_flFlagForSteamIDReuseAfterShutdownTime = 0; } #endif
// Let drop messages go out
Sys_Sleep( 100 );
for ( int i = 0; i < vecDelete.Count(); ++i ) { delete vecDelete[ i ]; }
// clear everthing
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);
CSVCMsg_Print_t print; print.set_text( 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\n", 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." );
// 8 KB should be far more than is needed -- in fact less than 100 bytes seems to be sufficient
const int kTempEntityBufferSize = 8192;
void CBaseServer::WriteTempEntities( CBaseClient *client, CFrameSnapshot *pCurrentSnapshot, CFrameSnapshot *pLastSnapshot, CSVCMsg_TempEntities_t &msg, int ev_max ) { msg.Clear();
// allocate the temp buffer for the temp ents
char ALIGN4 tempEntityData[kTempEntityBufferSize] ALIGN4_POST; bf_write buffer( &tempEntityData[0], ARRAYSIZE(tempEntityData) );
bool bDebug = sv_debugtempentities.GetBool();
// Container which calls ReleaseReference on all snapshots in list on exit of function scope
CReferencedSnapshotList snapshotlist; // Builds list and calls AddReference on each item in list (uses a mutex to be thread safe)
framesnapshotmanager->BuildSnapshotList( pCurrentSnapshot, pLastSnapshot, CFrameSnapshotManager::knDefaultSnapshotSet, snapshotlist );
//keep count of the number of entities that we write out (since some can be omitted by the client)
int32 nNumEntitiesWritten = 0; CEventInfo *pLastEvent = NULL;
// Build list of events sorted by send table classID (makes the delta work better in cases with a lot of the same message type )
for ( int nSnapShotIndex = 0; nSnapShotIndex < snapshotlist.m_vecSnapshots.Count(); ++nSnapShotIndex ) { CFrameSnapshot *pSnapshot = snapshotlist.m_vecSnapshots[ nSnapShotIndex ];
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
//we are writing this entity so update our count so the receiver knows how many to parse
nNumEntitiesWritten++;
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->m_Packed, event->m_Packed, -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() );
// Will write all non-zero fields
SendTable_WriteAllDeltaProps( event->pSendTable, SERIALIZED_ENTITY_HANDLE_INVALID, event->m_Packed, -1, &buffer );
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; } } }
//don't do any more work if we didn't write anything out
if( nNumEntitiesWritten <= 0 ) return;
if ( buffer.IsOverflowed() ) { Warning( "WriteTempOverflow! Discarding all ents!\n" ); return; }
// Copy the data to the message buffer. This copying is unfortunate but at least
// it has a cost that is proportional to the message size. The alternative is to
// initially set the buffer size larger and then resize it down, but the initial
// setting of the buffer size will require zeroing all of its bytes which is
// more expensive (as seen on CS:GO server profiles on Linux).
const int nBytesWritten = Bits2Bytes( buffer.GetNumBitsWritten() ); msg.set_entity_data( &tempEntityData[0], nBytesWritten );
// set num entries
msg.set_num_entries( nNumEntitiesWritten ); }
void CBaseServer::SetMaxClients( int number ) { m_nMaxclients = clamp( number, 1, ABSOLUTE_PLAYER_LIMIT ); }
//-----------------------------------------------------------------------------
// 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 nHumans; int nMaxHumans; int nBots;
GetMasterServerPlayerCounts( nHumans, nMaxHumans, nBots );
if ( nMaxHumans > maxmaxplayers ) { AddTag( "increased_maxplayers" ); } else { RemoveTag( "increased_maxplayers" ); }
if ( g_bLowViolence ) { AddTag( "low_violence" ); } else { RemoveTag( "low_violence" ); }
// Group name
const char *pszGroupName = sv_steamgroup.GetString(); if ( !pszGroupName || !pszGroupName[0] ) { RemoveTag( "*grp:", true ); } else { char chGroupNameBuf[64] = {0}; Q_snprintf( chGroupNameBuf, sizeof( chGroupNameBuf ) - 2, "%si", pszGroupName ); AddTag( "*grp:", chGroupNameBuf ); }
bRecalculatingTags = false; }
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CBaseServer::AddTag( const char *pszTag, const char *pszSubTagValue ) { CSplitString TagList( sv_tags.GetString(), "," ); for ( int i = 0; i < TagList.Count(); i++ ) { if ( pszSubTagValue ) { // See if the subtag matches
if ( StringHasPrefix( TagList[i], pszTag ) ) { // Ok, see if the tag value matches
if ( Q_stricmp(TagList[i]+strlen(pszTag),pszSubTagValue) == 0 ) return;
// They have a subtag specified, but it's wrong. Remove it.
RemoveTag( pszTag, true ); } } else { // Already in the tag list?
if ( !Q_stricmp(TagList[i],pszTag) ) return; } }
// Append it
char tmptags[MAX_TAG_STRING_LENGTH]; tmptags[0] = '\0'; Q_strncpy( tmptags, pszTag, MAX_TAG_STRING_LENGTH ); if ( pszSubTagValue ) { Q_strncat( tmptags, pszSubTagValue, 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, bool bSubTag ) { const char *pszTags = sv_tags.GetString(); if ( !pszTags || !pszTags[0] ) return;
char tmptags[MAX_TAG_STRING_LENGTH]; tmptags[0] = '\0';
CSplitString TagList ( sv_tags.GetString(), "," ); bool bFoundIt = false; for ( int i = 0; i < TagList.Count(); i++ ) { bool bMatched = false; if ( bSubTag ) { bMatched = StringHasPrefix( TagList[i], pszTag ); } else { bMatched = Q_stricmp(TagList[i],pszTag) == 0; }
// Keep any tags other than the specified one
if ( !bMatched ) { Q_strncat( tmptags, TagList[i], MAX_TAG_STRING_LENGTH ); Q_strncat( tmptags, ",", MAX_TAG_STRING_LENGTH ); } else { bFoundIt = true; } }
// Didn't find it in our list?
if ( !bFoundIt ) return;
sv_tags.SetValue( tmptags ); }
CBaseClient *CBaseServer::CreateSplitClient( const CMsg_CVars& vecUserInfo, CBaseClient *pAttachedTo ) { // 0.0.0.0:0 signifies a split client. It'll plumb all the way down to winsock calls but it won't make them.
ns_address adr; adr.Clear(); CBaseClient *pSplitClient = GetFreeClient( adr ); if ( !pSplitClient ) { // server is full
return NULL; }
INetChannel *netchan = NULL; // [ NET ENCRYPT ] Split clients don't configure an encryption key because the master channel will network
netchan = NET_CreateNetChannel( m_Socket, &adr, ns_address_render( adr ).String(), pSplitClient, NULL, true );
m_nUserid = GetNextUserID();
// Name will be pulled from vecUserInfo!!!
pSplitClient->Connect( "split", m_nUserid, netchan, true, pAttachedTo->m_ClientPlatform, &vecUserInfo ); Assert( pSplitClient->m_bFakePlayer == true ); pSplitClient->m_bSplitScreenUser = true; pSplitClient->m_pAttachedTo = pAttachedTo; pSplitClient->m_nSignonTick = m_nTickCount;
pSplitClient->m_bSplitAllowFastDisconnect = true;
if ( !pSplitClient->CheckConnect() ) { pSplitClient->m_bSplitAllowFastDisconnect = false; return NULL; }
pSplitClient->m_bSplitAllowFastDisconnect = false;
return pSplitClient; }
CBaseClient *CBaseServer::GetBaseUserForSplitClient( CBaseClient *pSplitUser ) { if ( pSplitUser->m_pAttachedTo ) return pSplitUser->m_pAttachedTo;
return pSplitUser; }
void CBaseServer::QueueSplitScreenDisconnect( CBaseClient *pSplitHost, CBaseClient *pSplitUser ) { SplitDisconnect_t disc; disc.m_pUser = pSplitHost; disc.m_pSplit = pSplitUser;
m_QueuedForDisconnect.AddToTail( disc ); }
void CBaseServer::ProcessSplitScreenDisconnects() { // Destroy it
for ( int i = 0; i < m_QueuedForDisconnect.Count(); ++i ) { SplitDisconnect_t &disc = m_QueuedForDisconnect[ i ]; CBaseClient *pClient = disc.m_pUser; for ( int j = 0; j < host_state.max_splitscreen_players; ++j ) { if ( pClient->m_SplitScreenUsers[ j ] != disc.m_pSplit ) continue;
pClient->m_SplitScreenUsers[ j ] = NULL; disc.m_pSplit->m_bSplitAllowFastDisconnect = true; disc.m_pSplit->Disconnect( "leaving splitscreen" ); disc.m_pSplit->m_bSplitScreenUser = false; } } m_QueuedForDisconnect.Purge(); }
// Exposing state of the server to the client code
ConVar sv_hosting_lobby( "sv_hosting_lobby", "0", FCVAR_DEVELOPMENTONLY | FCVAR_REPLICATED );
void CBaseServer::UpdateReservedState() { if ( ( m_flReservationExpiryTime && !IsReserved() ) || ( m_flReservationExpiryTime < net_time ) ) { m_flReservationExpiryTime = 0.0f; sv.UpdateHibernationState(); } if ( m_flTimeLastClientLeft != -1.0f ) { sv.UpdateHibernationState(); } }
uint64 CBaseServer::GetReservationCookie() const { return m_nReservationCookie; }
bool CBaseServer::ReserveServerForQueuedGame( char const *szReservationPayload ) { if ( !szReservationPayload ) return false; switch ( szReservationPayload[0] ) { case 'Q': // Queued competitive, locked game from the start
case 'G': // Joinable in progress game
break; case 'R': sscanf( szReservationPayload + 1, "%p", &m_pnReservationCookieSession ); return true;
default: return false; }
extern bool sv_ShutDown_WasRequested(); if ( sv_ShutDown_WasRequested() ) { Warning( "Rejecting reservation because sv_shutdown was requested.\n" ); return false; // don't accept new reservations if we are in sv_shutdown mode
}
uint64 uiReservationCookie = 0; uint64 uiMatchID = 0; int32 bReserve = 0; sscanf( szReservationPayload+1, "%llx,%llx,%d:", &uiReservationCookie, &uiMatchID, &bReserve ); if ( !uiReservationCookie || !uiMatchID ) return false;
m_nMatchId = uiMatchID;
if ( bReserve ) { if ( !IsReserved() || ( GetReservationCookie() == uiReservationCookie ) ) { m_flReservationExpiryTime = net_time + sv_mmqueue_reservation_timeout.GetFloat(); sv_mmqueue_reservation.SetValue( szReservationPayload ); SetReservationCookie( uiReservationCookie, "ReserveServerForQueuedGame: %s", szReservationPayload ); return true; } else return false; } else { if ( IsReserved() && ( GetReservationCookie() == uiReservationCookie ) ) { Unreserve(); return true; } else return !IsReserved(); } }
void CBaseServer::SetReservationCookie( uint64 uiCookie, char const *pchReasonFormat, ... ) { if ( uiCookie != m_nReservationCookie ) { char reason[ 256 ] = { 0 }; va_list argptr; va_start( argptr, pchReasonFormat ); Q_vsnprintf( reason, sizeof( reason ), pchReasonFormat, argptr ); va_end( argptr ); ConColorMsg( Color( 255, 0, 255, 255 ), "-> Reservation cookie %llx: reason %s\n", uiCookie, reason );
if ( !uiCookie ) { sv_mmqueue_reservation.SetValue( "" ); m_arrReservationPlayers.RemoveAll(); } else if ( StringHasPrefix( sv_mmqueue_reservation.GetString(), CFmtStr( "Q%llx,", uiCookie ) ) ) { // Set reservation players
m_arrReservationPlayers.RemoveAll(); for ( char const *pszPrev = sv_mmqueue_reservation.GetString(), *pszNext = pszPrev; ( pszNext = strchr( pszPrev, '[' ) ) != NULL; pszPrev = pszNext + 1 ) { uint32 uiAccountId = 0; sscanf( pszNext, "[%x]", &uiAccountId ); if ( uiAccountId ) { QueueMatchPlayer_t qmp; qmp.m_uiAccountID = uiAccountId; qmp.m_uiToken = 0; qmp.m_uiReservationStage = 0; m_arrReservationPlayers.AddToTail( qmp ); } }
// Set the number of expected humans
m_numGameSlots = m_arrReservationPlayers.Count();
// Tournament servers need additional slots for casters
static char const * s_pchTournamentServer = CommandLine()->ParmValue( "-tournament", ( char const * ) NULL ); if ( s_pchTournamentServer ) { int numCasters = 0; for ( char const *pszPrev = sv_mmqueue_reservation.GetString(), *pszNext = pszPrev; ( pszNext = strchr( pszPrev, '{' ) ) != NULL; pszPrev = pszNext + 1 ) { uint32 uiAccountId = 0; sscanf( pszNext, "{%x}", &uiAccountId ); if ( uiAccountId ) ++ numCasters; }
// Allow launch parameter to limit number of caster slots
static int s_nTournamentExtraCastersSlots = CommandLine()->ParmValue( "-tournament_extra_casters_slots", 2 ); numCasters = MAX( 0, MIN( s_nTournamentExtraCastersSlots, numCasters ) ); m_numGameSlots += numCasters; } } else if ( StringHasPrefix( sv_mmqueue_reservation.GetString(), CFmtStr( "G%llx,", uiCookie ) ) ) { // Set reservation players
m_arrReservationPlayers.RemoveAll();
// Set the number of expected humans
m_numGameSlots = 0; } else { // Set exposed reservation variable
sv_mmqueue_reservation.SetValue( CFmtStr( "0x%llx", uiCookie ) ); } }
m_nReservationCookie = uiCookie;
UpdateGameData();
// Expose the current reservation state via a replicated convar to clients
sv_hosting_lobby.SetValue( IsReserved() ); }
void CBaseServer::Unreserve() { if ( IsReserved() ) { Msg( "Server was reserved for %d more seconds. Reservation cleared.\n", (int) ( m_flReservationExpiryTime - net_time ) ); m_flReservationExpiryTime = 0.0f; sv.UpdateHibernationState(); } else { Msg( "Server is not currently reserved.\n" ); }
UpdateGameData(); }
char const *CBaseServer::GetGameType() const { return m_GameType.String(); }
char const *CBaseServer::GetGameData() const { return m_GameData.Base(); }
int CBaseServer::GetGameDataVersion() const { return m_GameDataVersion; }
void CBaseServer::ClearTagStrings() { m_GameType = ""; }
static void BuildTokenList( char const *pchString, char chDelim, CUtlVector< CUtlString > &list ) { // Have to split up the string
int len = Q_strlen( pchString ); char *szCritBuf = (char *)stackalloc( len + 1 ); Q_strncpy( szCritBuf, pchString, len + 1 ); char *pszTok = strchr( szCritBuf, chDelim ); char *pszPrevTok = szCritBuf;
while ( pszTok && pszPrevTok ) { // Save character
char szTemp = *pszTok; *pszTok = 0; if ( *pszPrevTok ) { list.AddToTail( CUtlString( pszPrevTok ) ); } // Restore and advance to next token after delim
*pszTok = szTemp; pszPrevTok = pszTok + 1; pszTok = strchr( pszPrevTok, chDelim ); }
// Close off any trailing token w/o a delim at the end
if ( pszPrevTok && *pszPrevTok ) { list.AddToTail( CUtlString( pszPrevTok ) ); } }
void CBaseServer::AddTagString( CUtlString &dest, char const *pchString ) { if ( !pchString || !*pchString ) return;
char *chDelim = ",";
if ( Q_strstr( pchString, chDelim ) ) { CUtlVector< CUtlString > list; BuildTokenList( pchString, *chDelim, list ); for ( int i = 0; i < list.Count(); ++i ) { AddTagString( dest, list[ i ].String() ); } return; }
if ( !dest.IsEmpty() ) { dest += chDelim; } dest += pchString; }
void CBaseServer::UpdateGameType() { ClearTagStrings();
CUtlString tags;
if ( serverGameDLL ) { char szMatchMakingTags[ 1024 ] = { 0 }; serverGameDLL->GetMatchmakingTags( szMatchMakingTags, sizeof( szMatchMakingTags ) ); if ( szMatchMakingTags[ 0 ] ) { AddTagString( m_GameType, szMatchMakingTags ); } }
bool bHaveAnyClients = GetNumHumanPlayers() > 0; if ( !bHaveAnyClients ) { AddTagString( m_GameType, "empty" ); }
static ConVarRef var( "sv_tags" ); if ( var.IsValid() && var.GetString()[ 0 ] ) { AddTagString( m_GameType, var.GetString() ); }
// Is this server "secure"?
#if !defined( NO_STEAM ) && !defined( _GAMECONSOLE )
{ AddTagString( m_GameType, Steam3Server().BSecure() ? "secure" : "insecure" ); } #endif
// if ( IsDedicated() && serverGameDLL->IsValveDS() && !IsX360() && !IsDedicatedForXbox() &&
// !bHaveAnyClients &&
// !IsReserved() && !( GetNumClients() - GetNumFakeClients() ) &&
// !sv_steamgroup_exclusive.GetBool() )
// {
// AddTagString( va( "*sv_search_key_%s%d", sv_search_key.GetString(), GetHostVersion() ) );
// }
if ( Steam3Server().SteamGameServer() ) { Steam3Server().SteamGameServer()->SetGameTags( m_GameType.String() ); } }
void CBaseServer::UpdateGameData() { UpdateGameType();
const int nPacketSize = MAX_ROUTABLE_PAYLOAD - 128; // The packet has to fit into NET_SendPacket and there's some data sent before the tags too
// Remember old data
CUtlVector< char > oldGameData; oldGameData.EnsureCapacity( m_GameData.Count() ); oldGameData.AddMultipleToTail( m_GameData.Count(), m_GameData.Base() );
//
// Generate new data
m_GameData.RemoveAll(); m_GameData.EnsureCapacity( nPacketSize ); m_GameData.AddMultipleToTail( nPacketSize ); memset( m_GameData.Base(), 0, m_GameData.Count() );
// Add search key
CUtlString utlKey; if ( serverGameDLL && IsDedicated() && !IsX360() && !IsDedicatedForXbox() && ( GetNumHumanPlayers() <= 0 ) && !IsReserved() && !( GetNumClients() - GetNumFakeClients() ) && !sv_steamgroup_exclusive.GetBool() && !GetPassword() ) { AddTagString( utlKey, CFmtStr( "%skey:%s%d", serverGameDLL->IsValveDS() ? "v" : "c", sv_search_key.GetString(), GetHostVersion() ) ); }
if ( utlKey.Length() > m_GameData.Count() - 3 ) { Warning( "GameData: sv_search_key too long, cannot advertise server!\n" ); return; }
// Group name
CUtlString utlGroups; const char *pszGroupName = sv_steamgroup.GetString(); { CUtlVector< CUtlString > list; char *chDelim = ","; BuildTokenList( pszGroupName, *chDelim, list );
for ( int i = 0; i < list.Count(); ++i ) { AddTagString( utlGroups, CFmtStr( "grp:%si", list[ i ].String() ) ); } }
if ( utlKey.Length() + utlGroups.Length() > m_GameData.Count() - 3 ) { Warning( "GameData: Too many Steam groups set for sv_steamgroup, not advertising Steam groups affiliation.\n" ); utlGroups.Purge(); }
if ( serverGameDLL ) { serverGameDLL->GetMatchmakingGameData( m_GameData.Base(), m_GameData.Count() - 1 - utlKey.Length() - utlGroups.Length() - 2 ); }
int nLen = Q_strlen( m_GameData.Base() ); char *pszWrite = m_GameData.Base() + nLen; int numBytes = m_GameData.Count() - nLen;
Q_snprintf( pszWrite, numBytes - 1, "%s%s%s%s", nLen ? "," : "", utlGroups.Get(), utlGroups.Length() ? "," : "", utlKey.Get() );
// Always update Steam
if ( Steam3Server().SteamGameServer() ) { Steam3Server().SteamGameServer()->SetGameData( m_GameData.Base() ); }
// Increment data version if changed
if ( oldGameData.Count() != m_GameData.Count() || memcmp( oldGameData.Base(), m_GameData.Base(), m_GameData.Count() ) ) { ++ m_GameDataVersion; } }
bool CBaseServer::IsPlayingSoloAgainstBots() const { if ( sv.IsActive() && sv.IsMultiplayer() ) { int nNumHumanPlayers = sv.GetNumClients() - sv.GetNumFakeClients(); if ( nNumHumanPlayers == 1 ) { return true; } } return false; }
bool CBaseServer::IsExclusiveToLobbyConnections() const { #ifndef NO_STEAM
if ( !IsDedicated() ) return false;
#if !defined( CSTRIKE15 )
// We are switching CStrike to always have lobbies associated with servers for community matchmaking
if ( !sv_allow_lobby_connect_only.GetBool() ) return false; #endif
if ( sv_lan.GetBool() ) return false; return true; #else
return false; #endif
}
// CON_COMMAND( sv_unreserve, "Clears any lobby reservation for this server\n" )
// {
// sv.Unreserve();
// }
bool CBaseServer::ShouldHideServer() const { if ( serverGameDLL && serverGameDLL->ShouldHideServer() ) { return true; }
return false; }
// If a server is hidden from master server it won't show up on public internet (server browser, steam server list)
// but it can still respond to LAN info, players, rules queries etc.
bool CBaseServer::ShouldHideFromMasterServer() const { // UNDONE: MATCHMAKING: Left4Dead keeps passworded, listen and cheat servers off the master server. TF2 does not.
extern ConVar sv_cheats; if ( !IsDedicated() && (!IsHLTV()) ) { return true; }
return ShouldHideServer(); }
void CBaseServer::OnPasswordChanged() { if ( IsActive() ) { Cbuf_AddText( CBUF_SERVER, "heartbeat\n" ); } }
int CBaseServer::GetMaxClients() const { return m_nMaxclients; }
int CBaseServer::GetMaxHumanPlayers() const { if ( serverGameClients ) { int nMaxHuman = serverGameClients->GetMaxHumanPlayers(); if ( nMaxHuman != -1 ) { return nMaxHuman; } }
return GetMaxClients(); }
int CBaseServer::GetNumHumanPlayers( void ) const { int count = 0;
for (int i=0 ; i < m_Clients.Count() ; i++ ) { if ( m_Clients[ i ]->IsHumanPlayer() ) { count++; } }
return count; }
void CBaseServer::GetMasterServerPlayerCounts( int &nHumans, int &nMaxHumanSlots, int &nBots ) { // count active users
nHumans = GetNumHumanPlayers(); nMaxHumanSlots = GetMaxHumanPlayers(); nBots = sv.GetNumFakeClients();
if ( sv_visiblemaxplayers.GetInt() > 0 ) { nMaxHumanSlots = sv_visiblemaxplayers.GetInt(); }
extern bool CanShowHostTvStatus(); if ( !CanShowHostTvStatus() && ( nBots > 0 ) ) { for ( CActiveHltvServerIterator hltv; hltv; hltv.Next() ) { nBots--; // reduce the bot count by HLTV bot
} } }
void CBaseServer::ShowTags() const { Msg( "Tags:\n" ); Msg( "Public : %s\n", GetGameType() ); Msg( "Private: %s\n", GetGameData() ); }
CON_COMMAND( sv_showtags, "Describe current gametags." ) { sv.ShowTags(); }
const ConVar &GetIndexedConVar( const ConVar &cv, int nIndex ) { if ( nIndex == 0 ) return cv; const ConVar *pIndexedCV = g_pCVar->FindVar( va( "%s%d", cv.GetBaseName(), nIndex ) ); if ( pIndexedCV ) return *pIndexedCV; else return cv; }
|