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.
1735 lines
43 KiB
1735 lines
43 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: baseclient.cpp: implementation of the CBaseClient class. |
|
// |
|
//===========================================================================// |
|
|
|
|
|
#include "client_pch.h" |
|
#include "tier0/etwprof.h" |
|
#include "eiface.h" |
|
#include "baseclient.h" |
|
#include "server.h" |
|
#include "host.h" |
|
#include "networkstringtable.h" |
|
#include "framesnapshot.h" |
|
#include "GameEventManager.h" |
|
#include "LocalNetworkBackdoor.h" |
|
#include "dt_send_eng.h" |
|
#ifndef SWDS |
|
#include "vgui_baseui_interface.h" |
|
#endif |
|
#include "sv_remoteaccess.h" // NotifyDedicatedServerUI() |
|
#include "MapReslistGenerator.h" |
|
#include "sv_steamauth.h" |
|
#include "matchmaking.h" |
|
#include "iregistry.h" |
|
#include "sv_main.h" |
|
#include "hltvserver.h" |
|
#include <ctype.h> |
|
#if defined( REPLAY_ENABLED ) |
|
#include "replay_internal.h" |
|
#endif |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
extern IServerGameDLL *serverGameDLL; |
|
extern ConVar tv_enable; |
|
|
|
ConVar sv_namechange_cooldown_seconds( "sv_namechange_cooldown_seconds", "30.0", FCVAR_NONE, "When a client name change is received, wait N seconds before allowing another name change" ); |
|
ConVar sv_netspike_on_reliable_snapshot_overflow( "sv_netspike_on_reliable_snapshot_overflow", "0", FCVAR_NONE, "If nonzero, the server will dump a netspike trace if a client is dropped due to reliable snapshot overflow" ); |
|
ConVar sv_netspike_sendtime_ms( "sv_netspike_sendtime_ms", "0", FCVAR_NONE, "If nonzero, the server will dump a netspike trace if it takes more than N ms to prepare a snapshot to a single client. This feature does take some CPU cycles, so it should be left off when not in use." ); |
|
ConVar sv_netspike_output( "sv_netspike_output", "1", FCVAR_NONE, "Where the netspike data be written? Sum of the following values: 1=netspike.txt, 2=ordinary server log" ); |
|
|
|
////////////////////////////////////////////////////////////////////// |
|
// Construction/Destruction |
|
////////////////////////////////////////////////////////////////////// |
|
|
|
CBaseClient::CBaseClient() |
|
{ |
|
// init all pointers |
|
m_NetChannel = NULL; |
|
m_ConVars = NULL; |
|
m_Server = NULL; |
|
m_pBaseline = NULL; |
|
m_bIsHLTV = false; |
|
#if defined( REPLAY_ENABLED ) |
|
m_bIsReplay = false; |
|
#endif |
|
m_bConVarsChanged = false; |
|
m_bInitialConVarsSet = false; |
|
m_bSendServerInfo = false; |
|
m_bFullyAuthenticated = false; |
|
m_fTimeLastNameChange = 0.0; |
|
m_szPendingNameChange[0] = '\0'; |
|
m_bReportFakeClient = true; |
|
m_iTracing = 0; |
|
m_bPlayerNameLocked = false; |
|
} |
|
|
|
CBaseClient::~CBaseClient() |
|
{ |
|
} |
|
|
|
|
|
void CBaseClient::SetRate(int nRate, bool bForce ) |
|
{ |
|
if ( m_NetChannel ) |
|
m_NetChannel->SetDataRate( nRate ); |
|
} |
|
|
|
int CBaseClient::GetRate( void ) const |
|
{ |
|
if ( m_NetChannel ) |
|
{ |
|
return m_NetChannel->GetDataRate(); |
|
} |
|
else |
|
{ |
|
return 0; |
|
} |
|
} |
|
|
|
bool CBaseClient::FillUserInfo( player_info_s &userInfo ) |
|
{ |
|
Q_memset( &userInfo, 0, sizeof(userInfo) ); |
|
|
|
if ( !m_Name[0] || !IsConnected() ) |
|
return false; // inactive user, no more data available |
|
|
|
Q_strncpy( userInfo.name, GetClientName(), MAX_PLAYER_NAME_LENGTH ); |
|
V_strcpy_safe( userInfo.guid, GetNetworkIDString() ); |
|
userInfo.friendsID = m_nFriendsID; |
|
Q_strncpy( userInfo.friendsName, m_FriendsName, sizeof(m_FriendsName) ); |
|
userInfo.userID = GetUserID(); |
|
userInfo.fakeplayer = IsFakeClient(); |
|
userInfo.ishltv = IsHLTV(); |
|
#if defined( REPLAY_ENABLED ) |
|
userInfo.isreplay = IsReplay(); |
|
#endif |
|
|
|
for( int i=0; i< MAX_CUSTOM_FILES; i++ ) |
|
userInfo.customFiles[i] = m_nCustomFiles[i].crc; |
|
|
|
userInfo.filesDownloaded = m_nFilesDownloaded; |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Send text to client |
|
// Input : *fmt - |
|
// ... - |
|
//----------------------------------------------------------------------------- |
|
void CBaseClient::ClientPrintf (const char *fmt, ...) |
|
{ |
|
if ( !m_NetChannel ) |
|
{ |
|
return; |
|
} |
|
|
|
va_list argptr; |
|
char string[1024]; |
|
|
|
va_start (argptr,fmt); |
|
Q_vsnprintf (string, sizeof( string ), fmt,argptr); |
|
va_end (argptr); |
|
|
|
SVC_Print print(string); |
|
m_NetChannel->SendNetMsg( print ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Send text to client |
|
// Input : *fmt - |
|
// ... - |
|
//----------------------------------------------------------------------------- |
|
bool CBaseClient::SendNetMsg(INetMessage &msg, bool bForceReliable) |
|
{ |
|
if ( !m_NetChannel ) |
|
{ |
|
return true; |
|
} |
|
|
|
int nStartBit = m_NetChannel->GetNumBitsWritten( msg.IsReliable() || bForceReliable ); |
|
bool bret = m_NetChannel->SendNetMsg( msg, bForceReliable ); |
|
if ( IsTracing() ) |
|
{ |
|
int nBits = m_NetChannel->GetNumBitsWritten( msg.IsReliable() || bForceReliable ) - nStartBit; |
|
TraceNetworkMsg( nBits, "NetMessage %s", msg.GetName() ); |
|
} |
|
return bret; |
|
} |
|
|
|
char const *CBaseClient::GetUserSetting(char const *pchCvar) const |
|
{ |
|
if ( !m_ConVars || !pchCvar || !pchCvar[0] ) |
|
{ |
|
return ""; |
|
} |
|
|
|
const char * value = m_ConVars->GetString( pchCvar, "" ); |
|
|
|
if ( value[0]==0 ) |
|
{ |
|
// check if this var even existed |
|
if ( m_ConVars->GetDataType( pchCvar ) == KeyValues::TYPE_NONE ) |
|
{ |
|
DevMsg( "GetUserSetting: cvar '%s' unknown.\n", pchCvar ); |
|
} |
|
} |
|
|
|
return value; |
|
} |
|
|
|
void CBaseClient::SetUserCVar( const char *pchCvar, const char *value) |
|
{ |
|
if ( !pchCvar || !value ) |
|
return; |
|
|
|
// Name is handled differently |
|
if ( !Q_stricmp( pchCvar, "name") ) |
|
{ |
|
//Msg("CBaseClient::SetUserCVar[index=%d]('name', '%s')\n", m_nClientSlot, value ); |
|
ClientRequestNameChange( value ); |
|
return; |
|
} |
|
|
|
m_ConVars->SetString( pchCvar, value ); |
|
} |
|
|
|
void CBaseClient::SetUpdateRate(int udpaterate, bool bForce) |
|
{ |
|
udpaterate = clamp( udpaterate, 1, 100 ); |
|
|
|
m_fSnapshotInterval = 1.0f / udpaterate; |
|
} |
|
|
|
int CBaseClient::GetUpdateRate(void) const |
|
{ |
|
if ( m_fSnapshotInterval > 0 ) |
|
return (int)(1.0f/m_fSnapshotInterval); |
|
else |
|
return 0; |
|
} |
|
|
|
void CBaseClient::FreeBaselines() |
|
{ |
|
if ( m_pBaseline ) |
|
{ |
|
m_pBaseline->ReleaseReference(); |
|
m_pBaseline = NULL; |
|
} |
|
|
|
m_nBaselineUpdateTick = -1; |
|
m_nBaselineUsed = 0; |
|
m_BaselinesSent.ClearAll(); |
|
} |
|
|
|
void CBaseClient::Clear() |
|
{ |
|
// Throw away any residual garbage in the channel. |
|
if ( m_NetChannel ) |
|
{ |
|
m_NetChannel->Shutdown("Disconnect by server.\n"); |
|
m_NetChannel = NULL; |
|
} |
|
|
|
if ( m_ConVars ) |
|
{ |
|
m_ConVars->deleteThis(); |
|
m_ConVars = NULL; |
|
} |
|
|
|
FreeBaselines(); |
|
|
|
// This used to be a memset, but memset will screw up any embedded classes |
|
// and we want to preserve some things like index. |
|
m_nSignonState = SIGNONSTATE_NONE; |
|
m_nDeltaTick = -1; |
|
m_nSignonTick = 0; |
|
m_nStringTableAckTick = 0; |
|
m_pLastSnapshot = NULL; |
|
m_nForceWaitForTick = -1; |
|
m_bFakePlayer = false; |
|
m_bIsHLTV = false; |
|
#if defined( REPLAY_ENABLED ) |
|
m_bIsReplay = false; |
|
#endif |
|
m_fNextMessageTime = 0; |
|
m_fSnapshotInterval = 0; |
|
m_bReceivedPacket = false; |
|
m_UserID = 0; |
|
m_Name[0] = 0; |
|
m_nFriendsID = 0; |
|
m_FriendsName[0] = 0; |
|
m_nSendtableCRC = 0; |
|
m_nBaselineUpdateTick = -1; |
|
m_nBaselineUsed = 0; |
|
m_nFilesDownloaded = 0; |
|
m_bConVarsChanged = false; |
|
m_bSendServerInfo = false; |
|
m_bFullyAuthenticated = false; |
|
m_fTimeLastNameChange = 0.0; |
|
m_szPendingNameChange[0] = '\0'; |
|
|
|
Q_memset( m_nCustomFiles, 0, sizeof(m_nCustomFiles) ); |
|
} |
|
|
|
bool CBaseClient::SetSignonState(int state, int spawncount) |
|
{ |
|
MDLCACHE_COARSE_LOCK_(g_pMDLCache); |
|
switch( m_nSignonState ) |
|
{ |
|
case SIGNONSTATE_CONNECTED : // client is connected, leave client in this state and let SendPendingSignonData do the rest |
|
m_bSendServerInfo = true; |
|
break; |
|
|
|
case SIGNONSTATE_NEW : // client got server info, send prespawn datam_Client->SendServerInfo() |
|
if ( !SendSignonData() ) |
|
return false; |
|
|
|
break; |
|
|
|
case SIGNONSTATE_PRESPAWN : SpawnPlayer(); |
|
break; |
|
|
|
case SIGNONSTATE_SPAWN : ActivatePlayer(); |
|
break; |
|
|
|
case SIGNONSTATE_FULL : OnSignonStateFull(); |
|
break; |
|
|
|
case SIGNONSTATE_CHANGELEVEL: break; |
|
|
|
} |
|
|
|
return true; |
|
} |
|
|
|
void CBaseClient::Reconnect( void ) |
|
{ |
|
ConMsg("Forcing client reconnect (%i)\n", m_nSignonState ); |
|
|
|
m_NetChannel->Clear(); |
|
|
|
m_nSignonState = SIGNONSTATE_CONNECTED; |
|
|
|
NET_SignonState signon( m_nSignonState, -1 ); |
|
m_NetChannel->SendNetMsg( signon ); |
|
} |
|
|
|
void CBaseClient::Inactivate( void ) |
|
{ |
|
FreeBaselines(); |
|
|
|
m_nDeltaTick = -1; |
|
m_nSignonTick = 0; |
|
m_nStringTableAckTick = 0; |
|
m_pLastSnapshot = NULL; |
|
m_nForceWaitForTick = -1; |
|
|
|
m_nSignonState = SIGNONSTATE_CHANGELEVEL; |
|
|
|
if ( m_NetChannel ) |
|
{ |
|
// don't do that for fakeclients |
|
m_NetChannel->Clear(); |
|
|
|
if ( NET_IsMultiplayer() ) |
|
{ |
|
NET_SignonState signon( m_nSignonState, m_Server->GetSpawnCount() ); |
|
SendNetMsg( signon ); |
|
|
|
// force sending message now |
|
m_NetChannel->Transmit(); |
|
} |
|
} |
|
|
|
// don't receive event messages anymore |
|
g_GameEventManager.RemoveListener( this ); |
|
} |
|
|
|
//--------------------------------------------------------------------------- |
|
// Purpose: Determine whether or not a character should be ignored in a player's name. |
|
//--------------------------------------------------------------------------- |
|
inline bool BIgnoreCharInName ( unsigned char cChar, bool bIsFirstCharacter ) |
|
{ |
|
// Don't copy '%' or '~' chars across |
|
// Don't copy '#' chars across if they would go into the first position in the name |
|
// Don't allow color codes ( less than COLOR_MAX ) |
|
return cChar == '%' || cChar == '~' || cChar < 0x09 || ( bIsFirstCharacter && cChar == '#' ); |
|
} |
|
|
|
void ValidateName( char *pszName, int nBuffSize ) |
|
{ |
|
if ( !pszName ) |
|
return; |
|
|
|
// did we get an empty string for the name? |
|
if ( Q_strlen( pszName ) <= 0 ) |
|
{ |
|
Q_snprintf( pszName, nBuffSize, "unnamed" ); |
|
} |
|
else |
|
{ |
|
Q_RemoveAllEvilCharacters( pszName ); |
|
|
|
const unsigned char *pChar = (unsigned char *)pszName; |
|
|
|
// also skip characters we're going to ignore |
|
while ( *pChar && ( isspace(*pChar) || BIgnoreCharInName( *pChar, true ) ) ) |
|
{ |
|
++pChar; |
|
} |
|
|
|
// did we get all the way to the end of the name without a non-whitespace character? |
|
if ( *pChar == '\0' ) |
|
{ |
|
Q_snprintf( pszName, nBuffSize, "unnamed" ); |
|
} |
|
} |
|
} |
|
|
|
void CBaseClient::SetName(const char * playerName) |
|
{ |
|
char name[MAX_PLAYER_NAME_LENGTH]; |
|
Q_strncpy( name, playerName, sizeof(name) ); |
|
|
|
// Clear any pending name change |
|
m_szPendingNameChange[0] = '\0'; |
|
|
|
// quick check to make sure the name isn't empty or full of whitespace |
|
ValidateName( name, sizeof(name) ); |
|
|
|
if ( Q_strncmp( name, m_Name, sizeof(m_Name) ) == 0 ) |
|
return; // didn't change |
|
|
|
int i; |
|
int dupc = 1; |
|
char *p, *val; |
|
|
|
char newname[MAX_PLAYER_NAME_LENGTH]; |
|
|
|
// remove evil char '%' |
|
char *pFrom = (char *)name; |
|
char *pTo = m_Name; |
|
char *pLimit = &m_Name[sizeof(m_Name)-1]; |
|
|
|
while ( *pFrom && pTo < pLimit ) |
|
{ |
|
// Don't copy '%' or '~' chars across |
|
// Don't copy '#' chars across if they would go into the first position in the name |
|
// Don't allow color codes ( less than COLOR_MAX ) |
|
if ( !BIgnoreCharInName( *pFrom, pTo == &m_Name[0] ) ) |
|
{ |
|
*pTo++ = *pFrom; |
|
} |
|
|
|
pFrom++; |
|
} |
|
*pTo = 0; |
|
|
|
Assert( m_Name[ 0 ] != '\0' ); // this should've been caught by ValidateName |
|
if ( m_Name[ 0 ] == '\0' ) |
|
{ |
|
V_strncpy( m_Name, "unnamed", sizeof(m_Name) ); |
|
} |
|
|
|
val = m_Name; |
|
|
|
// Don't care about duplicate names on the xbox. It can only occur when a player |
|
// is reconnecting after crashing, and we don't want to ever show the (X) then. |
|
if ( !IsX360() ) |
|
{ |
|
// Check to see if another user by the same name exists |
|
while ( true ) |
|
{ |
|
for ( i = 0; i < m_Server->GetClientCount(); i++ ) |
|
{ |
|
IClient *client = m_Server->GetClient( i ); |
|
|
|
if( !client->IsConnected() || client == this ) |
|
continue; |
|
|
|
// If it's 2 bots they're allowed to have matching names, otherwise there's a conflict |
|
if( !Q_stricmp( client->GetClientName(), val ) && !( IsFakeClient() && client->IsFakeClient() ) ) |
|
{ |
|
CBaseClient *pClient = dynamic_cast< CBaseClient* >( client ); |
|
if ( IsFakeClient() && pClient ) |
|
{ |
|
// We're a bot so we get to keep the name... change the other guy |
|
pClient->m_Name[ 0 ] = '\0'; |
|
pClient->SetName( val ); |
|
} |
|
else |
|
{ |
|
break; |
|
} |
|
} |
|
} |
|
|
|
if (i >= m_Server->GetClientCount()) |
|
break; |
|
|
|
p = val; |
|
|
|
if (val[0] == '(') |
|
{ |
|
if (val[2] == ')') |
|
{ |
|
p = val + 3; |
|
} |
|
else if (val[3] == ')') //assumes max players is < 100 |
|
{ |
|
p = val + 4; |
|
} |
|
} |
|
|
|
Q_snprintf(newname, sizeof(newname), "(%d)%-.*s", dupc++, MAX_PLAYER_NAME_LENGTH - 4, p ); |
|
Q_strncpy(m_Name, newname, sizeof(m_Name)); |
|
|
|
val = m_Name; |
|
} |
|
} |
|
|
|
m_ConVars->SetString( "name", m_Name ); |
|
m_bConVarsChanged = true; |
|
|
|
m_Server->UserInfoChanged( m_nClientSlot ); |
|
} |
|
|
|
void CBaseClient::ActivatePlayer() |
|
{ |
|
COM_TimestampedLog( "CBaseClient::ActivatePlayer" ); |
|
|
|
// tell server to update the user info table (if not already done) |
|
m_Server->UserInfoChanged( m_nClientSlot ); |
|
|
|
m_nSignonState = SIGNONSTATE_FULL; |
|
MapReslistGenerator().OnPlayerSpawn(); |
|
#ifndef _XBOX |
|
// update the UI |
|
NotifyDedicatedServerUI("UpdatePlayers"); |
|
#endif |
|
} |
|
|
|
void CBaseClient::SpawnPlayer( void ) |
|
{ |
|
COM_TimestampedLog( "CBaseClient::SpawnPlayer" ); |
|
|
|
if ( !IsFakeClient() ) |
|
{ |
|
// free old baseline snapshot |
|
FreeBaselines(); |
|
|
|
// create baseline snapshot for real clients |
|
m_pBaseline = framesnapshotmanager->CreateEmptySnapshot( 0, MAX_EDICTS ); |
|
} |
|
|
|
// Set client clock to match server's |
|
NET_Tick tick( m_Server->GetTick(), host_frametime_unbounded, host_frametime_stddeviation ); |
|
SendNetMsg( tick, true ); |
|
|
|
// Spawned into server, not fully active, though |
|
m_nSignonState = SIGNONSTATE_SPAWN; |
|
NET_SignonState signonState (m_nSignonState, m_Server->GetSpawnCount() ); |
|
SendNetMsg( signonState ); |
|
} |
|
|
|
bool CBaseClient::SendSignonData( void ) |
|
{ |
|
COM_TimestampedLog( " CBaseClient::SendSignonData" ); |
|
#ifndef SWDS |
|
EngineVGui()->UpdateProgressBar(PROGRESS_SENDSIGNONDATA); |
|
#endif |
|
|
|
if ( m_Server->m_Signon.IsOverflowed() ) |
|
{ |
|
Host_Error( "Signon buffer overflowed %i bytes!!!\n", m_Server->m_Signon.GetNumBytesWritten() ); |
|
return false; |
|
} |
|
|
|
m_NetChannel->SendData( m_Server->m_Signon ); |
|
|
|
m_nSignonState = SIGNONSTATE_PRESPAWN; |
|
NET_SignonState signonState( m_nSignonState, m_Server->GetSpawnCount() ); |
|
|
|
return m_NetChannel->SendNetMsg( signonState ); |
|
} |
|
|
|
void CBaseClient::Connect( const char * szName, int nUserID, INetChannel *pNetChannel, bool bFakePlayer, int clientChallenge ) |
|
{ |
|
COM_TimestampedLog( "CBaseClient::Connect" ); |
|
#ifndef SWDS |
|
EngineVGui()->UpdateProgressBar(PROGRESS_SIGNONCONNECT); |
|
#endif |
|
Clear(); |
|
|
|
m_ConVars = new KeyValues("userinfo"); |
|
m_bInitialConVarsSet = false; |
|
|
|
m_UserID = nUserID; |
|
|
|
SetName( szName ); |
|
m_fTimeLastNameChange = 0.0; |
|
|
|
m_bFakePlayer = bFakePlayer; |
|
m_NetChannel = pNetChannel; |
|
|
|
if ( m_NetChannel && m_Server && m_Server->IsMultiplayer() ) |
|
{ |
|
m_NetChannel->SetCompressionMode( true ); |
|
} |
|
|
|
m_clientChallenge = clientChallenge; |
|
|
|
m_nSignonState = SIGNONSTATE_CONNECTED; |
|
|
|
if ( bFakePlayer ) |
|
{ |
|
// Hidden fake players and the HLTV/Replay bot will get removed by CSteam3Server::SendUpdatedServerDetails. |
|
Steam3Server().NotifyLocalClientConnect( this ); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Drops client from server, with explanation |
|
// Input : *cl - |
|
// crash - |
|
// *fmt - |
|
// ... - |
|
//----------------------------------------------------------------------------- |
|
void CBaseClient::Disconnect( const char *fmt, ... ) |
|
{ |
|
va_list argptr; |
|
char string[1024]; |
|
|
|
if ( m_nSignonState == SIGNONSTATE_NONE ) |
|
return; // no recursion |
|
|
|
#if !defined( SWDS ) && defined( ENABLE_RPT ) |
|
SV_NotifyRPTOfDisconnect( m_nClientSlot ); |
|
#endif |
|
|
|
#ifndef _XBOX |
|
Steam3Server().NotifyClientDisconnect( this ); |
|
#endif |
|
m_nSignonState = SIGNONSTATE_NONE; |
|
|
|
// clear user info |
|
m_Server->UserInfoChanged( m_nClientSlot ); |
|
|
|
va_start (argptr,fmt); |
|
Q_vsnprintf (string, sizeof( string ), fmt,argptr); |
|
va_end (argptr); |
|
|
|
ConMsg("Dropped %s from server (%s)\n", GetClientName(), string ); |
|
|
|
// remove the client as listener |
|
g_GameEventManager.RemoveListener( this ); |
|
|
|
// Send the remaining reliable buffer so the client finds out the server is shutting down. |
|
if ( m_NetChannel ) |
|
{ |
|
m_NetChannel->Shutdown( string ) ; |
|
m_NetChannel = NULL; |
|
} |
|
|
|
Clear(); // clear state |
|
#ifndef _XBOX |
|
NotifyDedicatedServerUI("UpdatePlayers"); |
|
#endif |
|
Steam3Server().SendUpdatedServerDetails(); // Update the master server. |
|
} |
|
|
|
void CBaseClient::FireGameEvent( IGameEvent *event ) |
|
{ |
|
tmZoneFiltered( TELEMETRY_LEVEL0, 50, TMZF_NONE, "%s", __FUNCTION__ ); |
|
|
|
char buffer_data[MAX_EVENT_BYTES]; |
|
|
|
SVC_GameEvent eventMsg; |
|
|
|
eventMsg.m_DataOut.StartWriting( buffer_data, sizeof(buffer_data) ); |
|
|
|
// create bitstream from KeyValues |
|
if ( g_GameEventManager.SerializeEvent( event, &eventMsg.m_DataOut ) ) |
|
{ |
|
if ( m_NetChannel ) |
|
{ |
|
bool bSent = m_NetChannel->SendNetMsg( eventMsg ); |
|
if ( !bSent ) |
|
DevMsg("GameEventManager: failed to send event '%s'.\n", event->GetName() ); |
|
} |
|
} |
|
else |
|
{ |
|
DevMsg("GameEventManager: failed to serialize event '%s'.\n", event->GetName() ); |
|
} |
|
} |
|
|
|
bool CBaseClient::SendServerInfo( void ) |
|
{ |
|
COM_TimestampedLog( " CBaseClient::SendServerInfo" ); |
|
|
|
// supporting smaller stack |
|
byte *buffer = (byte *)MemAllocScratch( NET_MAX_PAYLOAD ); |
|
|
|
bf_write msg( "SV_SendServerinfo->msg", buffer, NET_MAX_PAYLOAD ); |
|
|
|
// Only send this message to developer console, or multiplayer clients. |
|
if ( developer.GetBool() || m_Server->IsMultiplayer() ) |
|
{ |
|
char devtext[ 2048 ]; |
|
int curplayers = m_Server->GetNumClients(); |
|
|
|
Q_snprintf( devtext, sizeof( devtext ), |
|
"\n%s\nMap: %s\nPlayers: %i / %i\nBuild: %d\nServer Number: %i\n\n", |
|
serverGameDLL->GetGameDescription(), |
|
m_Server->GetMapName(), |
|
curplayers, m_Server->GetMaxClients(), |
|
build_number(), |
|
m_Server->GetSpawnCount() ); |
|
|
|
SVC_Print printMsg( devtext ); |
|
|
|
printMsg.WriteToBuffer( msg ); |
|
} |
|
|
|
SVC_ServerInfo serverinfo; // create serverinfo message |
|
|
|
serverinfo.m_nPlayerSlot = m_nClientSlot; // own slot number |
|
|
|
m_Server->FillServerInfo( serverinfo ); // fill rest of info message |
|
|
|
serverinfo.WriteToBuffer( msg ); |
|
|
|
if ( IsX360() && serverinfo.m_nMaxClients > 1 ) |
|
{ |
|
Msg( "Telling clients to connect" ); |
|
g_pMatchmaking->TellClientsToConnect(); |
|
} |
|
|
|
// send first tick |
|
m_nSignonTick = m_Server->m_nTickCount; |
|
|
|
NET_Tick signonTick( m_nSignonTick, 0, 0 ); |
|
signonTick.WriteToBuffer( msg ); |
|
|
|
// write stringtable baselines |
|
#ifndef SHARED_NET_STRING_TABLES |
|
m_Server->m_StringTables->WriteBaselines( msg ); |
|
#endif |
|
|
|
// Write replicated ConVars to non-listen server clients only |
|
if ( !m_NetChannel->IsLoopback() ) |
|
{ |
|
NET_SetConVar convars; |
|
Host_BuildConVarUpdateMessage( &convars, FCVAR_REPLICATED, true ); |
|
|
|
convars.WriteToBuffer( msg ); |
|
} |
|
|
|
m_bSendServerInfo = false; |
|
|
|
// send signon state |
|
m_nSignonState = SIGNONSTATE_NEW; |
|
NET_SignonState signonMsg( m_nSignonState, m_Server->GetSpawnCount() ); |
|
signonMsg.WriteToBuffer( msg ); |
|
|
|
// send server info as one data block |
|
if ( !m_NetChannel->SendData( msg ) ) |
|
{ |
|
MemFreeScratch(); |
|
Disconnect("Server info data overflow"); |
|
return false; |
|
} |
|
|
|
COM_TimestampedLog( " CBaseClient::SendServerInfo(finished)" ); |
|
|
|
MemFreeScratch(); |
|
|
|
return true; |
|
} |
|
|
|
CClientFrame *CBaseClient::GetDeltaFrame( int nTick ) |
|
{ |
|
Assert( 0 ); // derive moe |
|
return NULL; // CBaseClient has no delta frames |
|
} |
|
|
|
void CBaseClient::WriteGameSounds(bf_write &buf) |
|
{ |
|
// CBaseClient has no events |
|
} |
|
|
|
void CBaseClient::ConnectionStart(INetChannel *chan) |
|
{ |
|
REGISTER_NET_MSG( Tick ); |
|
REGISTER_NET_MSG( StringCmd ); |
|
REGISTER_NET_MSG( SetConVar ); |
|
REGISTER_NET_MSG( SignonState ); |
|
|
|
REGISTER_CLC_MSG( ClientInfo ); |
|
REGISTER_CLC_MSG( Move ); |
|
REGISTER_CLC_MSG( VoiceData ); |
|
REGISTER_CLC_MSG( BaselineAck ); |
|
REGISTER_CLC_MSG( ListenEvents ); |
|
|
|
REGISTER_CLC_MSG( RespondCvarValue ); |
|
REGISTER_CLC_MSG( FileCRCCheck ); |
|
REGISTER_CLC_MSG( FileMD5Check ); |
|
|
|
#if defined( REPLAY_ENABLED ) |
|
REGISTER_CLC_MSG( SaveReplay ); |
|
#endif |
|
|
|
REGISTER_CLC_MSG( CmdKeyValues ); |
|
} |
|
|
|
bool CBaseClient::ProcessTick( NET_Tick *msg ) |
|
{ |
|
m_NetChannel->SetRemoteFramerate( msg->m_flHostFrameTime, msg->m_flHostFrameTimeStdDeviation ); |
|
return UpdateAcknowledgedFramecount( msg->m_nTick ); |
|
} |
|
|
|
bool CBaseClient::ProcessStringCmd( NET_StringCmd *msg ) |
|
{ |
|
ExecuteStringCommand( msg->m_szCommand ); |
|
return true; |
|
} |
|
|
|
bool CBaseClient::ProcessSetConVar( NET_SetConVar *msg ) |
|
{ |
|
for ( int i=0; i<msg->m_ConVars.Count(); i++ ) |
|
{ |
|
const char *name = msg->m_ConVars[i].name; |
|
const char *value = msg->m_ConVars[i].value; |
|
|
|
// Discard any convar change request if contains funky characters |
|
bool bFunky = false; |
|
for (const char *s = name ; *s != '\0' ; ++s ) |
|
{ |
|
if ( !V_isalnum(*s) && *s != '_' ) |
|
{ |
|
bFunky = true; |
|
break; |
|
} |
|
} |
|
if ( bFunky ) |
|
{ |
|
Msg( "Ignoring convar change request for variable '%s' from client %s; invalid characters in the variable name\n", name, GetClientName() ); |
|
continue; |
|
} |
|
|
|
// "name" convar is handled differently |
|
if ( V_stricmp( name, "name" ) == 0 ) |
|
{ |
|
ClientRequestNameChange( value ); |
|
continue; |
|
} |
|
|
|
// The initial set of convars must contain all client convars that are flagged userinfo. This is a simple fix to |
|
// exploits that send bogus data later, and catches bugs (why are new userinfo convars appearing later?) |
|
if ( m_bInitialConVarsSet && !m_ConVars->FindKey( name ) ) |
|
{ |
|
#ifndef _DEBUG // warn all the time in debug build |
|
static double s_dblLastWarned = 0.0; |
|
double dblTimeNow = Plat_FloatTime(); |
|
if ( dblTimeNow - s_dblLastWarned > 10 ) |
|
#endif |
|
{ |
|
#ifndef _DEBUG |
|
s_dblLastWarned = dblTimeNow; |
|
#endif |
|
Warning( "Client \"%s\" userinfo ignored: \"%s\" = \"%s\"\n", |
|
this->GetClientName(), name, value ); |
|
} |
|
continue; |
|
} |
|
|
|
m_ConVars->SetString( name, value ); |
|
|
|
// DevMsg( 1, " UserInfo update %s: %s = %s\n", m_Client->m_Name, name, value ); |
|
} |
|
|
|
m_bConVarsChanged = true; |
|
m_bInitialConVarsSet = true; |
|
|
|
return true; |
|
} |
|
|
|
bool CBaseClient::ProcessSignonState( NET_SignonState *msg) |
|
{ |
|
if ( msg->m_nSignonState == SIGNONSTATE_CHANGELEVEL ) |
|
{ |
|
return true; // ignore this message |
|
} |
|
|
|
if ( msg->m_nSignonState > SIGNONSTATE_CONNECTED ) |
|
{ |
|
if ( msg->m_nSpawnCount != m_Server->GetSpawnCount() ) |
|
{ |
|
Reconnect(); |
|
return true; |
|
} |
|
} |
|
|
|
// client must acknowledge our current state, otherwise start again |
|
if ( msg->m_nSignonState != m_nSignonState ) |
|
{ |
|
Reconnect(); |
|
return true; |
|
} |
|
|
|
return SetSignonState( msg->m_nSignonState, msg->m_nSpawnCount ); |
|
} |
|
|
|
bool CBaseClient::ProcessClientInfo( CLC_ClientInfo *msg ) |
|
{ |
|
if ( m_nSignonState != SIGNONSTATE_NEW ) |
|
{ |
|
Warning( "Dropping ClientInfo packet from client not in appropriate state\n" ); |
|
return false; |
|
} |
|
|
|
m_nSendtableCRC = msg->m_nSendTableCRC; |
|
|
|
// Protect against spoofed packets claiming to be HLTV clients |
|
if ( ( hltv && hltv->IsTVRelay() ) || tv_enable.GetBool() ) |
|
{ |
|
m_bIsHLTV = msg->m_bIsHLTV; |
|
} |
|
else |
|
{ |
|
m_bIsHLTV = false; |
|
} |
|
|
|
#if defined( REPLAY_ENABLED ) |
|
m_bIsReplay = msg->m_bIsReplay; |
|
#endif |
|
|
|
m_nFilesDownloaded = 0; |
|
m_nFriendsID = msg->m_nFriendsID; |
|
Q_strncpy( m_FriendsName, msg->m_FriendsName, sizeof(m_FriendsName) ); |
|
|
|
for ( int i=0; i<MAX_CUSTOM_FILES; i++ ) |
|
{ |
|
m_nCustomFiles[i].crc = msg->m_nCustomFiles[i]; |
|
m_nCustomFiles[i].reqID = 0; |
|
} |
|
|
|
if ( msg->m_nServerCount != m_Server->GetSpawnCount() ) |
|
{ |
|
Reconnect(); // client still in old game, reconnect |
|
} |
|
|
|
return true; |
|
} |
|
|
|
bool CBaseClient::ProcessBaselineAck( CLC_BaselineAck *msg ) |
|
{ |
|
if ( msg->m_nBaselineTick != m_nBaselineUpdateTick ) |
|
{ |
|
// This occurs when there are multiple ack's queued up for processing from a client. |
|
return true; |
|
} |
|
|
|
if ( msg->m_nBaselineNr != m_nBaselineUsed ) |
|
{ |
|
DevMsg("CBaseClient::ProcessBaselineAck: wrong baseline nr received (%i)\n", msg->m_nBaselineTick ); |
|
return true; |
|
} |
|
|
|
Assert( m_pBaseline ); |
|
|
|
// copy ents send as full updates this frame into baseline stuff |
|
CClientFrame *frame = GetDeltaFrame( m_nBaselineUpdateTick ); |
|
if ( frame == NULL ) |
|
{ |
|
// Will get here if we have a lot of packet loss and finally receive a stale ack from |
|
// remote client. Our "window" could be well beyond what it's acking, so just ignore the ack. |
|
return true; |
|
} |
|
|
|
CFrameSnapshot *pSnapshot = frame->GetSnapshot(); |
|
|
|
if ( pSnapshot == NULL ) |
|
{ |
|
// TODO if client lags for a couple of seconds the snapshot is lost |
|
// fix: don't remove snapshots that are labled a possible basline candidates |
|
// or: send full update |
|
DevMsg("CBaseClient::ProcessBaselineAck: invalid frame snapshot (%i)\n", m_nBaselineUpdateTick ); |
|
return false; |
|
} |
|
|
|
int index = m_BaselinesSent.FindNextSetBit( 0 ); |
|
|
|
while ( index >= 0 ) |
|
{ |
|
// get new entity |
|
PackedEntityHandle_t hNewEntity = pSnapshot->m_pEntities[index].m_pPackedData; |
|
if ( hNewEntity == INVALID_PACKED_ENTITY_HANDLE ) |
|
{ |
|
DevMsg("CBaseClient::ProcessBaselineAck: invalid packet handle (%i)\n", index ); |
|
return false; |
|
} |
|
|
|
PackedEntityHandle_t hOldEntity = m_pBaseline->m_pEntities[index].m_pPackedData; |
|
|
|
if ( hOldEntity != INVALID_PACKED_ENTITY_HANDLE ) |
|
{ |
|
// remove reference before overwriting packed entity |
|
framesnapshotmanager->RemoveEntityReference( hOldEntity ); |
|
} |
|
|
|
// increase reference |
|
framesnapshotmanager->AddEntityReference( hNewEntity ); |
|
|
|
// copy entity handle, class & serial number to |
|
m_pBaseline->m_pEntities[index] = pSnapshot->m_pEntities[index]; |
|
|
|
// go to next entity |
|
index = m_BaselinesSent.FindNextSetBit( index + 1 ); |
|
} |
|
|
|
m_pBaseline->m_nTickCount = m_nBaselineUpdateTick; |
|
|
|
// flip used baseline flag |
|
m_nBaselineUsed = (m_nBaselineUsed==1)?0:1; |
|
|
|
m_nBaselineUpdateTick = -1; // ready to update baselines again |
|
|
|
return true; |
|
} |
|
|
|
bool CBaseClient::ProcessListenEvents( CLC_ListenEvents *msg ) |
|
{ |
|
// first remove the client as listener |
|
g_GameEventManager.RemoveListener( this ); |
|
|
|
for ( int i=0; i < MAX_EVENT_NUMBER; i++ ) |
|
{ |
|
if ( msg->m_EventArray.Get(i) ) |
|
{ |
|
CGameEventDescriptor *descriptor = g_GameEventManager.GetEventDescriptor( i ); |
|
|
|
if ( descriptor ) |
|
{ |
|
g_GameEventManager.AddListener( this, descriptor, CGameEventManager::CLIENTSTUB ); |
|
} |
|
else |
|
{ |
|
DevMsg("ProcessListenEvents: game event %i not found.\n", i ); |
|
return false; |
|
} |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
|
|
extern int GetNetSpikeValue(); |
|
|
|
void CBaseClient::StartTrace( bf_write &msg ) |
|
{ |
|
|
|
// Should we be tracing? |
|
m_Trace.m_nMinWarningBytes = 0; |
|
if ( !IsHLTV() && !IsReplay() && !IsFakeClient() ) |
|
m_Trace.m_nMinWarningBytes = GetNetSpikeValue(); |
|
if ( m_iTracing < 2 ) |
|
{ |
|
if ( m_Trace.m_nMinWarningBytes <= 0 && sv_netspike_sendtime_ms.GetFloat() <= 0.0f ) |
|
{ |
|
m_iTracing = 0; |
|
return; |
|
} |
|
|
|
m_iTracing = 1; |
|
} |
|
m_Trace.m_nStartBit = msg.GetNumBitsWritten(); |
|
m_Trace.m_nCurBit = m_Trace.m_nStartBit; |
|
m_Trace.m_StartSendTime = Plat_FloatTime(); |
|
} |
|
|
|
#define SERVER_PACKETS_LOG "netspike.txt" |
|
|
|
void CBaseClient::EndTrace( bf_write &msg ) |
|
{ |
|
if ( m_iTracing == 0 ) |
|
return; |
|
VPROF_BUDGET( "CBaseClient::EndTrace", VPROF_BUDGETGROUP_OTHER_NETWORKING ); |
|
|
|
int bits = m_Trace.m_nCurBit - m_Trace.m_nStartBit; |
|
float flElapsedMs = ( Plat_FloatTime() - m_Trace.m_StartSendTime ) * 1000.0; |
|
int nBitThreshold = m_Trace.m_nMinWarningBytes << 3; |
|
if ( m_iTracing < 2 // not forced |
|
&& ( nBitThreshold <= 0 || bits < nBitThreshold ) // didn't exceed data threshold |
|
&& ( sv_netspike_sendtime_ms.GetFloat() <= 0.0f || flElapsedMs < sv_netspike_sendtime_ms.GetFloat() ) ) // didn't exceed time threshold |
|
{ |
|
m_Trace.m_Records.RemoveAll(); |
|
m_iTracing = 0; |
|
return; |
|
} |
|
|
|
CUtlBuffer logData( 0, 0, CUtlBuffer::TEXT_BUFFER ); |
|
|
|
logData.Printf( "%f/%d Player [%s][%d][adr:%s] was sent a datagram %d bits (%8.3f bytes), took %.2fms\n", |
|
realtime, |
|
host_tickcount, |
|
GetClientName(), |
|
GetPlayerSlot(), |
|
GetNetChannel()->GetAddress(), |
|
bits, (float)bits / 8.0f, |
|
flElapsedMs |
|
); |
|
|
|
// Write header line to the log if we aren't writing the whole thing |
|
if ( ( sv_netspike_output.GetInt() & 2 ) == 0 ) |
|
Log("netspike: %s", logData.String() ); |
|
|
|
for ( int i = 0 ; i < m_Trace.m_Records.Count() ; ++i ) |
|
{ |
|
Spike_t &sp = m_Trace.m_Records[ i ]; |
|
logData.Printf( "%64.64s : %8d bits (%8.3f bytes)\n", sp.m_szDesc, sp.m_nBits, (float)sp.m_nBits / 8.0f ); |
|
} |
|
|
|
if ( sv_netspike_output.GetInt() & 1 ) |
|
COM_LogString( SERVER_PACKETS_LOG, logData.String() ); |
|
if ( sv_netspike_output.GetInt() & 2 ) |
|
Log( "%s", logData.String() ); |
|
ETWMark1S( "netspike", logData.String() ); |
|
m_Trace.m_Records.RemoveAll(); |
|
m_iTracing = 0; |
|
} |
|
|
|
void CBaseClient::TraceNetworkData( bf_write &msg, char const *fmt, ... ) |
|
{ |
|
if ( !IsTracing() ) |
|
return; |
|
VPROF_BUDGET( "CBaseClient::TraceNetworkData", VPROF_BUDGETGROUP_OTHER_NETWORKING ); |
|
char buf[ 64 ]; |
|
va_list argptr; |
|
va_start( argptr, fmt ); |
|
Q_vsnprintf( buf, sizeof( buf ), fmt, argptr ); |
|
va_end( argptr ); |
|
|
|
Spike_t t; |
|
Q_strncpy( t.m_szDesc, buf, sizeof( t.m_szDesc ) ); |
|
t.m_nBits = msg.GetNumBitsWritten() - m_Trace.m_nCurBit; |
|
m_Trace.m_Records.AddToTail( t ); |
|
m_Trace.m_nCurBit = msg.GetNumBitsWritten(); |
|
} |
|
|
|
void CBaseClient::TraceNetworkMsg( int nBits, char const *fmt, ... ) |
|
{ |
|
if ( !IsTracing() ) |
|
return; |
|
VPROF_BUDGET( "CBaseClient::TraceNetworkMsg", VPROF_BUDGETGROUP_OTHER_NETWORKING ); |
|
char buf[ 64 ]; |
|
va_list argptr; |
|
va_start( argptr, fmt ); |
|
Q_vsnprintf( buf, sizeof( buf ), fmt, argptr ); |
|
va_end( argptr ); |
|
|
|
Spike_t t; |
|
Q_strncpy( t.m_szDesc, buf, sizeof( t.m_szDesc ) ); |
|
t.m_nBits = nBits; |
|
m_Trace.m_Records.AddToTail( t ); |
|
} |
|
|
|
void CBaseClient::SendSnapshot( CClientFrame *pFrame ) |
|
{ |
|
// never send the same snapshot twice |
|
if ( m_pLastSnapshot == pFrame->GetSnapshot() ) |
|
{ |
|
m_NetChannel->Transmit(); |
|
return; |
|
} |
|
|
|
// if we send a full snapshot (no delta-compression) before, wait until client |
|
// received and acknowledge that update. don't spam client with full updates |
|
if ( m_nForceWaitForTick > 0 ) |
|
{ |
|
// just continue transmitting reliable data |
|
m_NetChannel->Transmit(); |
|
return; |
|
} |
|
|
|
VPROF_BUDGET( "SendSnapshot", VPROF_BUDGETGROUP_OTHER_NETWORKING ); |
|
tmZoneFiltered( TELEMETRY_LEVEL0, 50, TMZF_NONE, "%s", __FUNCTION__ ); |
|
|
|
bool bFailedOnce = false; |
|
write_again: |
|
bf_write msg( "CBaseClient::SendSnapshot", m_SnapshotScratchBuffer, sizeof( m_SnapshotScratchBuffer ) ); |
|
|
|
TRACE_PACKET( ( "SendSnapshot(%d)\n", pFrame->tick_count ) ); |
|
|
|
// now create client snapshot packet |
|
CClientFrame *deltaFrame = GetDeltaFrame( m_nDeltaTick ); // NULL if delta_tick is not found |
|
if ( !deltaFrame ) |
|
{ |
|
// We need to send a full update and reset the instanced baselines |
|
OnRequestFullUpdate(); |
|
} |
|
|
|
// send tick time |
|
NET_Tick tickmsg( pFrame->tick_count, host_frametime_unbounded, host_frametime_stddeviation ); |
|
|
|
StartTrace( msg ); |
|
|
|
tickmsg.WriteToBuffer( msg ); |
|
|
|
if ( IsTracing() ) |
|
{ |
|
TraceNetworkData( msg, "NET_Tick" ); |
|
} |
|
|
|
#ifndef SHARED_NET_STRING_TABLES |
|
// in LocalNetworkBackdoor mode we updated the stringtables already in SV_ComputeClientPacks() |
|
if ( !g_pLocalNetworkBackdoor ) |
|
{ |
|
// Update shared client/server string tables. Must be done before sending entities |
|
m_Server->m_StringTables->WriteUpdateMessage( this, GetMaxAckTickCount(), msg ); |
|
} |
|
#endif |
|
|
|
int nDeltaStartBit = 0; |
|
if ( IsTracing() ) |
|
{ |
|
nDeltaStartBit = msg.GetNumBitsWritten(); |
|
} |
|
|
|
// send entity update, delta compressed if deltaFrame != NULL |
|
m_Server->WriteDeltaEntities( this, pFrame, deltaFrame, msg ); |
|
|
|
if ( IsTracing() ) |
|
{ |
|
int nBits = msg.GetNumBitsWritten() - nDeltaStartBit; |
|
TraceNetworkMsg( nBits, "Total Delta" ); |
|
} |
|
|
|
// send all unreliable temp entities between last and current frame |
|
// send max 64 events in multi player, 255 in SP |
|
int nMaxTempEnts = m_Server->IsMultiplayer() ? 64 : 255; |
|
m_Server->WriteTempEntities( this, pFrame->GetSnapshot(), m_pLastSnapshot.GetObject(), msg, nMaxTempEnts ); |
|
|
|
if ( IsTracing() ) |
|
{ |
|
TraceNetworkData( msg, "Temp Entities" ); |
|
} |
|
|
|
WriteGameSounds( msg ); |
|
|
|
// write message to packet and check for overflow |
|
if ( msg.IsOverflowed() ) |
|
{ |
|
bool bWasTracing = IsTracing(); |
|
if ( bWasTracing ) |
|
{ |
|
TraceNetworkMsg( 0, "Finished [delta %s]", deltaFrame ? "yes" : "no" ); |
|
EndTrace( msg ); |
|
} |
|
|
|
if ( !deltaFrame ) |
|
{ |
|
|
|
if ( !bWasTracing ) |
|
{ |
|
|
|
// Check for debugging by dumping a snapshot |
|
if ( sv_netspike_on_reliable_snapshot_overflow.GetBool() ) |
|
{ |
|
if ( !bFailedOnce ) // shouldn't be necessary, but just in case |
|
{ |
|
Warning(" RELIABLE SNAPSHOT OVERFLOW! Triggering trace to see what is so large\n" ); |
|
bFailedOnce = true; |
|
m_iTracing = 2; |
|
goto write_again; |
|
} |
|
m_iTracing = 0; |
|
} |
|
} |
|
|
|
// if this is a reliable snapshot, drop the client |
|
Disconnect( "ERROR! Reliable snapshot overflow." ); |
|
return; |
|
} |
|
else |
|
{ |
|
// unreliable snapshots may be dropped |
|
ConMsg ("WARNING: msg overflowed for %s\n", m_Name); |
|
msg.Reset(); |
|
} |
|
} |
|
|
|
// remember this snapshot |
|
m_pLastSnapshot = pFrame->GetSnapshot(); |
|
|
|
// Don't send the datagram to fakeplayers unless sv_stressbots is on (which will make m_NetChannel non-null). |
|
if ( m_bFakePlayer && !m_NetChannel ) |
|
{ |
|
m_nDeltaTick = pFrame->tick_count; |
|
m_nStringTableAckTick = m_nDeltaTick; |
|
return; |
|
} |
|
|
|
bool bSendOK; |
|
|
|
// is this is a full entity update (no delta) ? |
|
if ( !deltaFrame ) |
|
{ |
|
VPROF_BUDGET( "SendSnapshot Transmit Full", VPROF_BUDGETGROUP_OTHER_NETWORKING ); |
|
|
|
// transmit snapshot as reliable data chunk |
|
bSendOK = m_NetChannel->SendData( msg ); |
|
bSendOK = bSendOK && m_NetChannel->Transmit(); |
|
|
|
// remember this tickcount we send the reliable snapshot |
|
// so we can continue sending other updates if this has been acknowledged |
|
m_nForceWaitForTick = pFrame->tick_count; |
|
} |
|
else |
|
{ |
|
VPROF_BUDGET( "SendSnapshot Transmit Delta", VPROF_BUDGETGROUP_OTHER_NETWORKING ); |
|
|
|
// just send it as unreliable snapshot |
|
bSendOK = m_NetChannel->SendDatagram( &msg ) > 0; |
|
} |
|
|
|
if ( bSendOK ) |
|
{ |
|
if ( IsTracing() ) |
|
{ |
|
TraceNetworkMsg( 0, "Finished [delta %s]", deltaFrame ? "yes" : "no" ); |
|
EndTrace( msg ); |
|
} |
|
} |
|
else |
|
{ |
|
Disconnect( "ERROR! Couldn't send snapshot." ); |
|
} |
|
} |
|
|
|
bool CBaseClient::ExecuteStringCommand( const char *pCommand ) |
|
{ |
|
if ( !pCommand || !pCommand[0] ) |
|
return false; |
|
|
|
if ( !Q_stricmp( pCommand, "demorestart" ) ) |
|
{ |
|
DemoRestart(); |
|
// trick, dont return true, so serverGameClients gets this command too |
|
return false; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
void CBaseClient::DemoRestart() |
|
{ |
|
|
|
} |
|
|
|
bool CBaseClient::ShouldSendMessages( void ) |
|
{ |
|
if ( !IsConnected() ) |
|
return false; |
|
|
|
// if the reliable message overflowed, drop the client |
|
if ( m_NetChannel && m_NetChannel->IsOverflowed() ) |
|
{ |
|
m_NetChannel->Reset(); |
|
Disconnect ("%s overflowed reliable buffer\n", m_Name ); |
|
return false; |
|
} |
|
|
|
// check, if it's time to send the next packet |
|
bool bSendMessage = m_fNextMessageTime <= net_time ; |
|
|
|
if ( !bSendMessage && !IsActive() ) |
|
{ |
|
// if we are in signon modem instantly reply if |
|
// we got a answer and have reliable data waiting |
|
if ( m_bReceivedPacket && m_NetChannel && m_NetChannel->HasPendingReliableData() ) |
|
{ |
|
bSendMessage = true; |
|
} |
|
} |
|
|
|
if ( bSendMessage && m_NetChannel && !m_NetChannel->CanPacket() ) |
|
{ |
|
// we would like to send a message, but bandwidth isn't available yet |
|
// tell netchannel that we are choking a packet |
|
m_NetChannel->SetChoked(); |
|
// Record an ETW event to indicate that we are throttling. |
|
ETWThrottled(); |
|
bSendMessage = false; |
|
} |
|
|
|
return bSendMessage; |
|
} |
|
|
|
void CBaseClient::UpdateSendState( void ) |
|
{ |
|
// wait for next incoming packet |
|
m_bReceivedPacket = false; |
|
|
|
// in single player mode always send messages |
|
if ( !m_Server->IsMultiplayer() && !host_limitlocal.GetFloat() ) |
|
{ |
|
m_fNextMessageTime = net_time; // send ASAP and |
|
m_bReceivedPacket = true; // don't wait for incoming packets |
|
} |
|
else if ( IsActive() ) // multiplayer mode |
|
{ |
|
// snapshot mode: send snapshots frequently |
|
float maxDelta = min ( m_Server->GetTickInterval(), m_fSnapshotInterval ); |
|
float delta = clamp( (float)( net_time - m_fNextMessageTime ), 0.0f, maxDelta ); |
|
m_fNextMessageTime = net_time + m_fSnapshotInterval - delta; |
|
} |
|
else // multiplayer signon mode |
|
{ |
|
if ( m_NetChannel && m_NetChannel->HasPendingReliableData() && |
|
m_NetChannel->GetTimeSinceLastReceived() < 1.0f ) |
|
{ |
|
// if we have pending reliable data send as fast as possible |
|
m_fNextMessageTime = net_time; |
|
} |
|
else |
|
{ |
|
// signon mode: only respond on request or after 1 second |
|
m_fNextMessageTime = net_time + 1.0f; |
|
} |
|
} |
|
} |
|
|
|
void CBaseClient::UpdateUserSettings() |
|
{ |
|
int rate = m_ConVars->GetInt( "rate", DEFAULT_RATE ); |
|
|
|
if ( sv.IsActive() ) |
|
{ |
|
// If we're running a local listen server then set the rate very high |
|
// in order to avoid delays due to network throttling. This allows for |
|
// easier profiling of other issues (it removes most of the frame-render |
|
// time which can otherwise dominate profiles) and saves developer time |
|
// by making maps and models load much faster. |
|
if ( rate == DEFAULT_RATE ) |
|
{ |
|
// Only override the rate if the user hasn't customized it. |
|
// The max rate should be a million or so in order to truly |
|
// eliminate networking delays. |
|
rate = MAX_RATE; |
|
} |
|
} |
|
|
|
// set server to client network rate |
|
SetRate( rate, false ); |
|
|
|
// set server to client update rate |
|
SetUpdateRate( m_ConVars->GetInt( "cl_updaterate", 20), false ); |
|
|
|
SetMaxRoutablePayloadSize( m_ConVars->GetInt( "net_maxroutable", MAX_ROUTABLE_PAYLOAD ) ); |
|
|
|
m_Server->UserInfoChanged( m_nClientSlot ); |
|
|
|
m_bConVarsChanged = false; |
|
} |
|
|
|
void CBaseClient::ClientRequestNameChange( const char *pszNewName ) |
|
{ |
|
// This is called several times. Only show a status message the first time. |
|
bool bShowStatusMessage = ( m_szPendingNameChange[0] == '\0' ); |
|
|
|
V_strcpy_safe( m_szPendingNameChange, pszNewName ); |
|
CheckFlushNameChange( bShowStatusMessage ); |
|
} |
|
|
|
void CBaseClient::CheckFlushNameChange( bool bShowStatusMessage /*= false*/ ) |
|
{ |
|
if ( !IsConnected() ) |
|
return; |
|
|
|
if ( m_szPendingNameChange[0] == '\0' ) |
|
return; |
|
|
|
if ( m_bPlayerNameLocked ) |
|
return; |
|
|
|
// Did they change it back to the original? |
|
if ( !Q_strcmp( m_szPendingNameChange, m_Name ) ) |
|
{ |
|
|
|
// Nothing really pending, they already changed it back |
|
// we had a chance to apply the other one! |
|
m_szPendingNameChange[0] = '\0'; |
|
return; |
|
} |
|
|
|
// Check for throttling name changes |
|
// Don't do it on bots |
|
if ( !IsFakeClient() && IsNameChangeOnCooldown( bShowStatusMessage ) ) |
|
{ |
|
return; |
|
} |
|
|
|
// Set the new name |
|
m_fTimeLastNameChange = Plat_FloatTime(); |
|
SetName( m_szPendingNameChange ); |
|
} |
|
|
|
bool CBaseClient::IsNameChangeOnCooldown( bool bShowStatusMessage /*= false*/ ) |
|
{ |
|
// Check cooldown. The first name change is free |
|
if ( m_fTimeLastNameChange > 0.0 ) |
|
{ |
|
// Too recent? |
|
double timeNow = Plat_FloatTime(); |
|
double dNextChangeTime = m_fTimeLastNameChange + sv_namechange_cooldown_seconds.GetFloat(); |
|
if ( timeNow < dNextChangeTime ) |
|
{ |
|
// Cooldown period still active; throttle the name change |
|
if ( bShowStatusMessage ) |
|
{ |
|
ClientPrintf( "You have changed your name recently, and must wait %i seconds.\n", (int)abs( timeNow - dNextChangeTime ) ); |
|
} |
|
return true; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
void CBaseClient::OnRequestFullUpdate() |
|
{ |
|
VPROF_BUDGET( "CBaseClient::OnRequestFullUpdate", VPROF_BUDGETGROUP_OTHER_NETWORKING ); |
|
|
|
// client requests a full update |
|
m_pLastSnapshot = NULL; |
|
|
|
// free old baseline snapshot |
|
FreeBaselines(); |
|
|
|
// and create new baseline snapshot |
|
m_pBaseline = framesnapshotmanager->CreateEmptySnapshot( 0, MAX_EDICTS ); |
|
|
|
DevMsg("Sending full update to Client %s\n", GetClientName() ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Input : *cl - |
|
//----------------------------------------------------------------------------- |
|
bool CBaseClient::UpdateAcknowledgedFramecount(int tick) |
|
{ |
|
if ( IsFakeClient() ) |
|
{ |
|
// fake clients are always fine |
|
m_nDeltaTick = tick; |
|
m_nStringTableAckTick = tick; |
|
return true; |
|
} |
|
|
|
// are we waiting for full reliable update acknowledge |
|
if ( m_nForceWaitForTick > 0 ) |
|
{ |
|
if ( tick > m_nForceWaitForTick ) |
|
{ |
|
// we should never get here since full updates are transmitted as reliable data now |
|
// Disconnect("Acknowledging reliable snapshot failed.\n"); |
|
return true; |
|
} |
|
else if ( tick == -1 ) |
|
{ |
|
if( !m_NetChannel->HasPendingReliableData() ) |
|
{ |
|
// that's strange: we sent the client a full update, and it was fully received ( no reliable data in waiting buffers ) |
|
// but the client is requesting another full update. |
|
// |
|
// This can happen if they request full updates in succession really quickly (using cl_fullupdate or "record X;stop" quickly). |
|
// There was a bug here where if we just return out, the client will have nuked its entities and we'd send it |
|
// a supposedly uncompressed update but m_nDeltaTick was not -1, so it was delta'd and it'd miss lots of stuff. |
|
// Led to clients getting full spectator mode radar while their player was not a spectator. |
|
ConDMsg("Client forced immediate full update.\n"); |
|
m_nForceWaitForTick = m_nDeltaTick = -1; |
|
OnRequestFullUpdate(); |
|
return true; |
|
} |
|
} |
|
else if ( tick < m_nForceWaitForTick ) |
|
{ |
|
// keep on waiting, do nothing |
|
return true; |
|
} |
|
else // ( tick == m_nForceWaitForTick ) |
|
{ |
|
// great, the client acknowledge the tick we send the full update |
|
m_nForceWaitForTick = -1; |
|
// continue sending snapshots... |
|
} |
|
} |
|
else |
|
{ |
|
if ( m_nDeltaTick == -1 ) |
|
{ |
|
// we still want to send a full update, don't change delta_tick from -1 |
|
return true; |
|
} |
|
|
|
if ( tick == -1 ) |
|
{ |
|
OnRequestFullUpdate(); |
|
} |
|
else |
|
{ |
|
if ( m_nDeltaTick > tick ) |
|
{ |
|
// client already acknowledged new tick and now switch back to older |
|
// thats not allowed since we always delete older frames |
|
Disconnect("Client delta ticks out of order.\n"); |
|
return false; |
|
} |
|
} |
|
} |
|
|
|
// get acknowledged client frame |
|
m_nDeltaTick = tick; |
|
|
|
if ( m_nDeltaTick > -1 ) |
|
{ |
|
m_nStringTableAckTick = m_nDeltaTick; |
|
} |
|
|
|
if ( (m_nBaselineUpdateTick > -1) && (m_nDeltaTick > m_nBaselineUpdateTick) ) |
|
{ |
|
// server sent a baseline update, but it wasn't acknowledged yet so it was probably lost. |
|
m_nBaselineUpdateTick = -1; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: return a string version of the userid |
|
//----------------------------------------------------------------------------- |
|
const char *GetUserIDString( const USERID_t& id ) |
|
{ |
|
static char idstr[ MAX_NETWORKID_LENGTH ]; |
|
|
|
idstr[ 0 ] = 0; |
|
|
|
switch ( id.idtype ) |
|
{ |
|
case IDTYPE_STEAM: |
|
{ |
|
CSteamID nullID; |
|
|
|
if ( Steam3Server().BLanOnly() && nullID == id.steamid ) |
|
{ |
|
V_strcpy_safe( idstr, "STEAM_ID_LAN" ); |
|
} |
|
else if ( nullID == id.steamid ) |
|
{ |
|
V_strcpy_safe( idstr, "STEAM_ID_PENDING" ); |
|
} |
|
else |
|
{ |
|
V_sprintf_safe( idstr, "%s", id.steamid.Render() ); |
|
} |
|
} |
|
break; |
|
case IDTYPE_HLTV: |
|
{ |
|
V_strcpy_safe( idstr, "HLTV" ); |
|
} |
|
break; |
|
case IDTYPE_REPLAY: |
|
{ |
|
V_strcpy_safe( idstr, "REPLAY" ); |
|
} |
|
break; |
|
default: |
|
{ |
|
V_strcpy_safe( idstr, "UNKNOWN" ); |
|
} |
|
break; |
|
} |
|
|
|
return idstr; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: return a string version of the userid |
|
//----------------------------------------------------------------------------- |
|
const char *CBaseClient::GetNetworkIDString() const |
|
{ |
|
if ( IsFakeClient() ) |
|
{ |
|
return "BOT"; |
|
} |
|
|
|
return ( GetUserIDString( GetNetworkID() ) ); |
|
} |
|
|
|
bool CBaseClient::IgnoreTempEntity( CEventInfo *event ) |
|
{ |
|
int iPlayerIndex = GetPlayerSlot()+1; |
|
|
|
return !event->filter.IncludesPlayer( iPlayerIndex ); |
|
} |
|
|
|
const USERID_t CBaseClient::GetNetworkID() const |
|
{ |
|
USERID_t userID; |
|
|
|
userID.steamid = m_SteamID; |
|
userID.idtype = IDTYPE_STEAM; |
|
|
|
return userID; |
|
} |
|
|
|
void CBaseClient::SetSteamID( const CSteamID &steamID ) |
|
{ |
|
m_SteamID = steamID; |
|
} |
|
|
|
void CBaseClient::SetMaxRoutablePayloadSize( int nMaxRoutablePayloadSize ) |
|
{ |
|
if ( m_NetChannel ) |
|
{ |
|
m_NetChannel->SetMaxRoutablePayloadSize( nMaxRoutablePayloadSize ); |
|
} |
|
} |
|
|
|
int CBaseClient::GetMaxAckTickCount() const |
|
{ |
|
int nMaxTick = m_nSignonTick; |
|
if ( m_nDeltaTick > nMaxTick ) |
|
{ |
|
nMaxTick = m_nDeltaTick; |
|
} |
|
if ( m_nStringTableAckTick > nMaxTick ) |
|
{ |
|
nMaxTick = m_nStringTableAckTick; |
|
} |
|
return nMaxTick; |
|
} |
|
|
|
bool CBaseClient::ProcessCmdKeyValues( CLC_CmdKeyValues *msg ) |
|
{ |
|
return true; |
|
} |
|
|
|
void CBaseClient::OnSignonStateFull() |
|
{ |
|
#if defined( REPLAY_ENABLED ) |
|
if ( g_pReplay && g_pServerReplayContext ) |
|
{ |
|
g_pServerReplayContext->CreateSessionOnClient( m_nClientSlot ); |
|
} |
|
#endif |
|
}
|
|
|