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