2535 lines
72 KiB
C++
2535 lines
72 KiB
C++
//===== Copyright © 1996-2005, Valve Corporation, All rights reserved. ======//
|
|
//
|
|
// Purpose:
|
|
//
|
|
//===========================================================================//
|
|
|
|
#include "server_pch.h"
|
|
#include "framesnapshot.h"
|
|
#include "checksum_engine.h"
|
|
#include "sv_main.h"
|
|
#include "GameEventManager.h"
|
|
#include "networkstringtable.h"
|
|
#include "demo.h"
|
|
#include "PlayerState.h"
|
|
#include "tier0/vprof.h"
|
|
#include "sv_packedentities.h"
|
|
#include "LocalNetworkBackdoor.h"
|
|
#include "testscriptmgr.h"
|
|
#include "hltvserver.h"
|
|
#if defined( REPLAY_ENABLED )
|
|
#include "replayserver.h"
|
|
#endif
|
|
#include "pr_edict.h"
|
|
#include "logofile_shared.h"
|
|
#include "dt_send_eng.h"
|
|
#include "sv_plugin.h"
|
|
#include "download.h"
|
|
#include "cmodel_engine.h"
|
|
#include "tier1/commandbuffer.h"
|
|
#include "gl_cvars.h"
|
|
#include "tier2/tier2.h"
|
|
#include "matchmaking/imatchframework.h"
|
|
#include "audio/public/vox.h" // TERROR: for net_showreliablesounds
|
|
#include "SoundEmitterSystem/isoundemittersystembase.h"
|
|
#include "ihltv.h"
|
|
#include "tier1/utlstringtoken.h"
|
|
|
|
// memdbgon must be the last include file in a .cpp file!!!
|
|
#include "tier0/memdbgon.h"
|
|
|
|
extern CNetworkStringTableContainer *networkStringTableContainerServer;
|
|
|
|
static ConVar sv_timeout( "sv_timeout", "65", 0, "After this many seconds without a message from a client, the client is dropped" );
|
|
static ConVar sv_maxrate( "sv_maxrate", "0", FCVAR_REPLICATED | FCVAR_RELEASE, "Max bandwidth rate allowed on server, 0 == unlimited", true, 0, true, MAX_RATE );
|
|
static ConVar sv_minrate( "sv_minrate", STRINGIFY( MIN_RATE ), FCVAR_REPLICATED | FCVAR_RELEASE, "Min bandwidth rate allowed on server, 0 == unlimited", true, 0, true, MAX_RATE );
|
|
|
|
ConVar sv_maxupdaterate( "sv_maxupdaterate", "64", FCVAR_REPLICATED | FCVAR_RELEASE, "Maximum updates per second that the server will allow" ); // we need to be able to set max rate to 128
|
|
ConVar sv_minupdaterate( "sv_minupdaterate", "64", FCVAR_REPLICATED | FCVAR_RELEASE, "Minimum updates per second that the server will allow" );
|
|
|
|
ConVar sv_stressbots("sv_stressbots", "0", FCVAR_RELEASE, "If set to 1, the server calculates data and fills packets to bots. Used for perf testing.");
|
|
ConVar sv_replaybots( "sv_replaybots", "1", FCVAR_RELEASE, "If set to 1, the server records data needed to replay network stream from bot's perspective" );
|
|
static ConVar sv_allowdownload ("sv_allowdownload", "1", FCVAR_RELEASE, "Allow clients to download files");
|
|
static ConVar sv_allowupload ("sv_allowupload", "1", FCVAR_RELEASE, "Allow clients to upload customizations files");
|
|
ConVar sv_sendtables ( "sv_sendtables", "0", FCVAR_DEVELOPMENTONLY, "Force full sendtable sending path." );
|
|
#if HLTV_REPLAY_ENABLED
|
|
ConVar spec_replay_enable( "spec_replay_enable", "0", FCVAR_RELEASE|FCVAR_REPLICATED, "Enable Killer Replay, requires hltv server running." );
|
|
#endif
|
|
ConVar spec_replay_message_time( "spec_replay_message_time", "9.5", FCVAR_RELEASE | FCVAR_REPLICATED, "How long to show the message about Killer Replay after death. The best setting is a bit shorter than spec_replay_autostart_delay + spec_replay_leadup_time + spec_replay_winddown_time" );
|
|
ConVar spec_replay_rate_limit( "spec_replay_rate_limit", "3", FCVAR_RELEASE | FCVAR_REPLICATED, "Minimum allowable pause between replay requests in seconds" );
|
|
|
|
static ConVar ss_voice_hearpartner( "ss_voice_hearpartner", "0", 0, "Route voice between splitscreen players on same system." );
|
|
|
|
ConVar sv_max_dropped_packets_to_process( "sv_max_dropped_packets_to_process", "10", FCVAR_RELEASE, "Max dropped packets to process. Lower settings prevent lagged players from simulating too far in the past. Setting of 0 disables cap." );
|
|
|
|
ConVar cl_allowdownload( "cl_allowdownload", "1", FCVAR_ARCHIVE, "Client downloads customization files" );
|
|
|
|
static ConVar sv_quota_stringcmdspersecond( "sv_quota_stringcmdspersecond", "40", FCVAR_RELEASE, "How many string commands per second clients are allowed to submit, 0 to disallow all string commands" );
|
|
|
|
// TERROR:
|
|
static ConVar net_showreliablesounds( "net_showreliablesounds", "0", FCVAR_CHEAT );
|
|
ConVar replay_debug( "replay_debug", "0", FCVAR_RELEASE | FCVAR_REPLICATED);
|
|
|
|
ConVar sv_allow_legacy_cmd_execution_from_client( "sv_allow_legacy_cmd_execution_from_client", "0", FCVAR_RELEASE, "Enables old concommand execution behavior allowing remote clients to run any command not explicitly flagged as disallowed." );
|
|
|
|
extern ConVar sv_maxreplay;
|
|
extern ConVar tv_snapshotrate;
|
|
extern ConVar tv_transmitall;
|
|
extern ConVar sv_pure_kick_clients;
|
|
extern ConVar sv_pure_trace;
|
|
|
|
#if defined( REPLAY_ENABLED )
|
|
extern ConVar replay_snapshotrate;
|
|
extern ConVar replay_transmitall;
|
|
#endif
|
|
|
|
// static ConVar sv_failuretime( "sv_failuretime", "0.5", 0, "After this long without a packet from client, don't send any more until client starts sending again" );
|
|
|
|
static const char * s_clcommands[] =
|
|
{
|
|
"status",
|
|
"pause",
|
|
"setpause",
|
|
"unpause",
|
|
"ping",
|
|
"rpt_server_enable",
|
|
"rpt_client_enable",
|
|
#ifndef DEDICATED
|
|
"rpt",
|
|
"rpt_connect",
|
|
"rpt_password",
|
|
"rpt_screenshot",
|
|
"rpt_download_log",
|
|
#endif
|
|
"ss_connect",
|
|
"ss_disconnect",
|
|
#if defined( REPLAY_ENABLED )
|
|
"request_replay_demo",
|
|
#endif
|
|
NULL,
|
|
};
|
|
|
|
|
|
// Used on the server and on the client to bound its cl_rate cvar.
|
|
int ClampClientRate( int nRate )
|
|
{
|
|
// Apply mod specific clamps
|
|
if ( sv_maxrate.GetInt() > 0 )
|
|
{
|
|
nRate = MIN( nRate, sv_maxrate.GetInt() );
|
|
}
|
|
|
|
if ( sv_minrate.GetInt() > 0 )
|
|
{
|
|
nRate = MAX( nRate, sv_minrate.GetInt() );
|
|
}
|
|
|
|
// Apply overall clamp
|
|
nRate = clamp( nRate, MIN_RATE, MAX_RATE );
|
|
|
|
return nRate;
|
|
}
|
|
|
|
// Validate minimum number of required clients to be connected to a server
|
|
enum ValidateMinRequiredClients_t
|
|
{
|
|
VALIDATE_SPAWN,
|
|
VALIDATE_DISCONNECT
|
|
};
|
|
void SV_ValidateMinRequiredClients( ValidateMinRequiredClients_t eReason )
|
|
{
|
|
// FIXME: This gives false positives for drops and disconnects. (kwd)
|
|
return;
|
|
|
|
if ( !IsX360() && !sv.IsDedicatedForXbox() )
|
|
return;
|
|
|
|
static ConVarRef s_director_min_start_players( "director_min_start_players", true );
|
|
if ( !s_director_min_start_players.IsValid() )
|
|
return;
|
|
|
|
int numRequiredByDirector = s_director_min_start_players.GetInt();
|
|
if ( numRequiredByDirector <= 0 )
|
|
return;
|
|
|
|
switch ( eReason )
|
|
{
|
|
case VALIDATE_SPAWN:
|
|
{
|
|
// If at least one client has already spawned in the server, if there is a required
|
|
// minimum number of players and some of the players are not yet connected to server
|
|
// then most likely they dropped out or failed to connect, need to lower the minimum
|
|
// number of required players and proceed with game.
|
|
|
|
int numConnected = sv.GetClientCount();
|
|
int numInState = 0;
|
|
|
|
// Determine how many clients are above signon state NEW
|
|
for ( int j = 0 ; j < numConnected ; j++ )
|
|
{
|
|
IClient *client = sv.GetClient( j );
|
|
if ( !client )
|
|
continue;
|
|
|
|
if ( !client->IsSpawned() )
|
|
continue;
|
|
|
|
++ numInState;
|
|
}
|
|
|
|
if ( numRequiredByDirector > numInState )
|
|
{
|
|
s_director_min_start_players.SetValue( numInState );
|
|
ConMsg( "SV_ValidateMinRequiredClients: spawn: lowered min start players to %d.\n",
|
|
numInState );
|
|
}
|
|
}
|
|
break;
|
|
|
|
case VALIDATE_DISCONNECT:
|
|
{
|
|
// If somebody disconnects from the server we decrement the minimum number of players required
|
|
-- numRequiredByDirector;
|
|
s_director_min_start_players.SetValue( numRequiredByDirector );
|
|
ConMsg( "SV_ValidateMinRequiredClients: disconnect: lowered min start players to %d.\n",
|
|
numRequiredByDirector );
|
|
}
|
|
break;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
CGameClient::CGameClient(int slot, CBaseServer *pServer )
|
|
{
|
|
Clear();
|
|
|
|
m_nClientSlot = slot;
|
|
m_nEntityIndex = slot+1;
|
|
m_Server = pServer;
|
|
m_pCurrentFrame = NULL;
|
|
m_bIsInReplayMode = false;
|
|
|
|
// NULL out data we'll never use.
|
|
memset( &m_PrevPackInfo, 0, sizeof( m_PrevPackInfo ) );
|
|
m_PrevPackInfo.m_pTransmitEdict = &m_PrevTransmitEdict;
|
|
m_flTimeClientBecameFullyConnected = -1.0f;
|
|
m_flLastClientCommandQuotaStart = -1.0f;
|
|
m_numClientCommandsInQuota = 0;
|
|
}
|
|
|
|
CGameClient::~CGameClient()
|
|
{
|
|
|
|
}
|
|
|
|
bool CGameClient::CLCMsg_ClientInfo( const CCLCMsg_ClientInfo& msg )
|
|
{
|
|
BaseClass::CLCMsg_ClientInfo( msg );
|
|
|
|
if ( m_bIsHLTV )
|
|
{
|
|
Disconnect( "CLCMsg_ClientInfo: SourceTV can not connect to game directly.\n" );
|
|
return false;
|
|
}
|
|
|
|
#if defined( REPLAY_ENABLED )
|
|
if ( m_bIsReplay )
|
|
{
|
|
Disconnect( "CLCMsg_ClientInfo: Replay can not connect to game directly.\n" );
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
if ( sv_allowupload.GetBool() )
|
|
{
|
|
// download all missing customizations files from this client;
|
|
DownloadCustomizations();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CGameClient::CLCMsg_Move( const CCLCMsg_Move& msg )
|
|
{
|
|
// Don't process usercmds until the client is active. If we do, there can be weird behavior
|
|
// like the game trying to send reliable messages to the client and having those messages discarded.
|
|
if ( !IsActive() )
|
|
return true;
|
|
|
|
if ( m_LastMovementTick == sv.m_nTickCount )
|
|
{
|
|
// Only one movement command per frame, someone is cheating.
|
|
return true;
|
|
}
|
|
|
|
m_LastMovementTick = sv.m_nTickCount;
|
|
|
|
INetChannel *netchan = sv.GetBaseUserForSplitClient( this )->m_NetChannel;
|
|
int totalcmds = msg.num_backup_commands() + msg.num_new_commands();
|
|
|
|
// Decrement drop count by held back packet count
|
|
int netdrop = netchan->GetDropNumber();
|
|
|
|
// Dropped packet count is reported by clients
|
|
if ( sv_max_dropped_packets_to_process.GetInt() )
|
|
netdrop = Clamp( netdrop, 0, sv_max_dropped_packets_to_process.GetInt() );
|
|
|
|
bool ignore = !sv.IsActive();
|
|
#ifdef DEDICATED
|
|
bool paused = sv.IsPaused();
|
|
#else
|
|
bool paused = sv.IsPaused() || ( !sv.IsMultiplayer() && Con_IsVisible() );
|
|
#endif
|
|
|
|
// Make sure player knows of correct server time
|
|
g_ServerGlobalVariables.curtime = sv.GetTime();
|
|
g_ServerGlobalVariables.frametime = host_state.interval_per_tick;
|
|
|
|
// COM_Log( "sv.log", " executing %i move commands from client starting with command %i(%i)\n",
|
|
// numcmds,
|
|
// m_Client->netchan->incoming_sequence,
|
|
// m_Client->netchan->incoming_sequence & SV_UPDATE_MASK );
|
|
|
|
bf_read DataIn( &msg.data()[0], msg.data().size() );
|
|
|
|
serverGameClients->ProcessUsercmds
|
|
(
|
|
edict, // Player edict
|
|
&DataIn,
|
|
msg.num_new_commands(),
|
|
totalcmds, // Commands in packet
|
|
netdrop, // Number of dropped commands
|
|
ignore, // Don't actually run anything
|
|
paused // Run, but don't actually do any movement
|
|
);
|
|
|
|
|
|
if ( DataIn.IsOverflowed() )
|
|
{
|
|
Disconnect( "ProcessUsercmds: Overflowed reading usercmd data (check sending and receiving code for mismatches)!\n" );
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CGameClient::CLCMsg_VoiceData( const CCLCMsg_VoiceData& msg )
|
|
{
|
|
serverGameClients->ClientVoice( edict );
|
|
|
|
SV_BroadcastVoiceData( this, msg );
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CGameClient::CLCMsg_CmdKeyValues( const CCLCMsg_CmdKeyValues& msg )
|
|
{
|
|
KeyValues *keyvalues = CmdKeyValuesHelper::CLCMsg_GetKeyValues( msg );
|
|
KeyValues::AutoDelete autodelete_keyvalues( keyvalues );
|
|
|
|
serverGameClients->ClientCommandKeyValues( edict, keyvalues );
|
|
|
|
if ( IsX360() || NET_IsDedicatedForXbox() )
|
|
{
|
|
// See if a player was removed
|
|
if ( !Q_stricmp( keyvalues->GetName(), "OnPlayerRemovedFromSession" ) )
|
|
{
|
|
XUID xuid = keyvalues->GetUint64( "xuid", 0ull );
|
|
for ( int iPlayerIndex = 0; iPlayerIndex < sv.GetClientCount(); iPlayerIndex++ )
|
|
{
|
|
CBaseClient *pClient = (CBaseClient *) sv.GetClient( iPlayerIndex );
|
|
if ( !pClient || !xuid || pClient->GetClientXuid() != xuid )
|
|
continue;
|
|
|
|
pClient->Disconnect( "Player removed from host session\n" );
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
MEM_ALLOC_CREDIT();
|
|
KeyValues *pEvent = new KeyValues( "Server::CmdKeyValues" );
|
|
pEvent->AddSubKey( autodelete_keyvalues.Detach() );
|
|
pEvent->SetPtr( "edict", edict );
|
|
g_pMatchFramework->GetEventsSubscription()->BroadcastEvent( pEvent );
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CGameClient::CLCMsg_HltvReplay( const CCLCMsg_HltvReplay &msg )
|
|
{
|
|
int nRequest = msg.request();
|
|
if ( nRequest == REPLAY_EVENT_STUCK_NEED_FULL_UPDATE )
|
|
{
|
|
if ( m_nForceWaitForTick > 0 )
|
|
{
|
|
// <sergiy> if we are indeed waiting for tick confirmation, the client may indeed be stuck. Let them have another update. This is prone to a mildly annoying attack: client can go into a loop requesting updates, which will raise server's CPU usage and traffic and may cause server to skip ticks on high-load casual servers. So later on, I should probably rate-limit this. But hopefully I'll find why the client gets stuck before too long.
|
|
UpdateAcknowledgedFramecount( -1 );
|
|
}
|
|
}
|
|
else if ( nRequest )
|
|
{
|
|
ClientReplayEventParams_t params( nRequest );
|
|
if ( params.m_flSlowdownRate > 0.01f && params.m_flSlowdownRate < 10.0f && params.m_flSlowdownLength > 0.01f && params.m_flSlowdownLength <= 5.0f )
|
|
{
|
|
// keep defaults in suspicious cases
|
|
params.m_flSlowdownRate = msg.slowdown_rate();
|
|
params.m_flSlowdownLength = msg.slowdown_length();
|
|
}
|
|
params.m_nPrimaryTargetEntIndex = msg.primary_target_ent_index();
|
|
params.m_flEventTime = msg.event_time();
|
|
serverGameClients->ClientReplayEvent( edict, params );
|
|
}
|
|
else
|
|
{
|
|
if ( IsHltvReplay() )
|
|
m_HltvReplayStats.nUserCancels++;
|
|
StopHltvReplay();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CGameClient::SVCMsg_UserMessage( const CSVCMsg_UserMessage &msg )
|
|
{
|
|
serverGameClients->ClientSvcUserMessage( edict, msg.msg_type(), msg.passthrough(), msg.msg_data().size(), &msg.msg_data()[0] );
|
|
return true;
|
|
}
|
|
|
|
bool CGameClient::CLCMsg_RespondCvarValue( const CCLCMsg_RespondCvarValue& msg )
|
|
{
|
|
if ( msg.cookie() > 0 )
|
|
{
|
|
if ( g_pServerPluginHandler )
|
|
g_pServerPluginHandler->OnQueryCvarValueFinished( ( EQueryCvarValueStatus )msg.cookie(), edict, ( EQueryCvarValueStatus )msg.status_code(), msg.name().c_str(), msg.value().c_str() );
|
|
}
|
|
else
|
|
{
|
|
// Negative cookie means the game DLL asked for the value.
|
|
if ( serverGameDLL && g_bServerGameDLLGreaterThanV5 )
|
|
{
|
|
#ifdef REL_TO_STAGING_MERGE_TODO
|
|
serverGameDLL->OnQueryCvarValueFinished( msg.cookie(), edict, msg.status_code(), msg.name().c_str(), msg.value().c_str() );
|
|
#endif
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#include "pure_server.h"
|
|
bool CGameClient::CLCMsg_FileCRCCheck( const CCLCMsg_FileCRCCheck& msg )
|
|
{
|
|
// Ignore this message if we're not in pure server mode...
|
|
if ( !sv.IsInPureServerMode() )
|
|
return true;
|
|
|
|
char warningStr[1024] = {0};
|
|
|
|
// first check against all the other files users have sent
|
|
FileHash_t filehash;
|
|
V_memcpy( filehash.m_md5contents.bits, msg.md5().c_str(), MD5_DIGEST_LENGTH );
|
|
filehash.m_crcIOSequence = msg.crc();
|
|
filehash.m_eFileHashType = msg.file_hash_type();
|
|
filehash.m_cbFileLen = msg.file_len();
|
|
filehash.m_nPackFileNumber = msg.pack_file_number();
|
|
filehash.m_PackFileID = msg.pack_file_id();
|
|
|
|
const char *path = CCLCMsg_FileCRCCheck_t::GetPath( msg );
|
|
const char *fileName = CCLCMsg_FileCRCCheck_t::GetFileName( msg );
|
|
if ( g_PureFileTracker.DoesFileMatch( path, fileName, msg.file_fraction(), &filehash, GetNetworkID() ) )
|
|
{
|
|
// track successful file
|
|
}
|
|
else
|
|
{
|
|
V_snprintf( warningStr, sizeof( warningStr ), "Pure server: file [%s]\\%s does not match the server's file.", path, fileName );
|
|
}
|
|
// still ToDo:
|
|
// 1. make sure the user sends some files
|
|
// 2. make sure the user doesnt skip any files
|
|
// 3. make sure the user sends the right files...
|
|
|
|
if ( warningStr[0] )
|
|
{
|
|
if ( serverGameDLL )
|
|
{
|
|
serverGameDLL->OnPureServerFileValidationFailure( edict, path, fileName, filehash.m_crcIOSequence, filehash.m_eFileHashType,
|
|
filehash.m_cbFileLen, filehash.m_nPackFileNumber, filehash.m_PackFileID );
|
|
}
|
|
|
|
if ( sv_pure_kick_clients.GetInt() )
|
|
{
|
|
Disconnect( warningStr );
|
|
}
|
|
else
|
|
{
|
|
ClientPrintf( "Warning: %s\n", warningStr );
|
|
if ( sv_pure_trace.GetInt() >= 1 )
|
|
{
|
|
Msg( "[%s] %s\n", GetNetworkIDString(), warningStr );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( sv_pure_trace.GetInt() == 2 )
|
|
{
|
|
Msg( "Pure server CRC check: client %s passed check for [%s]\\%s\n", GetClientName(), path, fileName );
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
void CGameClient::DownloadCustomizations()
|
|
{
|
|
if ( !cl_allowdownload.GetBool() )
|
|
return; // client doesn't want to download any customizations
|
|
|
|
for ( int i=0; i<MAX_CUSTOM_FILES; i++ )
|
|
{
|
|
if ( m_nCustomFiles[i].crc == 0 )
|
|
continue; // slot not used
|
|
|
|
CCustomFilename hexname( m_nCustomFiles[i].crc );
|
|
|
|
if ( g_pFileSystem->FileExists( hexname.m_Filename ) )
|
|
continue; // we already have it
|
|
|
|
// we don't have it, request download from client
|
|
|
|
m_nCustomFiles[i].reqID = m_NetChannel->RequestFile( hexname.m_Filename, false );
|
|
}
|
|
}
|
|
|
|
void CGameClient::Connect(const char * szName, int nUserID, INetChannel *pNetChannel, bool bFakePlayer, CrossPlayPlatform_t clientPlatform, const CMsg_CVars *pVecCvars /*= NULL*/)
|
|
{
|
|
BaseClass::Connect( szName, nUserID, pNetChannel, bFakePlayer, clientPlatform, pVecCvars );
|
|
|
|
edict = EDICT_NUM( m_nEntityIndex );
|
|
|
|
// init PackInfo
|
|
m_PackInfo.m_pClientEnt = edict;
|
|
m_PackInfo.m_nPVSSize = sizeof( m_PackInfo.m_PVS );
|
|
|
|
// fire global game event
|
|
IGameEvent *event = g_GameEventManager.CreateEvent( "player_connect" );
|
|
{
|
|
event->SetInt( "userid", m_UserID );
|
|
event->SetInt( "index", m_nClientSlot );
|
|
event->SetString( "name", m_Name );
|
|
event->SetUint64( "xuid", GetClientXuid() );
|
|
event->SetString( "networkid", GetNetworkIDString() );
|
|
// event->SetString( "address", m_NetChannel?m_NetChannel->GetAddress():"none" );
|
|
event->SetInt( "bot", m_bFakePlayer?1:0 );
|
|
g_GameEventManager.FireEvent( event );
|
|
}
|
|
}
|
|
|
|
static ConVar sv_maxclientframes( "sv_maxclientframes", "128" );
|
|
static ConVar sv_extra_client_connect_time( "sv_extra_client_connect_time", "15.0", 0,
|
|
"Seconds after client connect during which extra frames are buffered to prevent non-delta'd update" );
|
|
|
|
void CGameClient::SetupPackInfo( CFrameSnapshot *pSnapshot )
|
|
{
|
|
Assert( !IsHltvReplay() );
|
|
// Compute Vis for each client
|
|
m_PackInfo.m_nPVSSize = (GetCollisionBSPData()->numclusters + 7) / 8;
|
|
serverGameClients->ClientSetupVisibility( (edict_t *)m_pViewEntity,
|
|
m_PackInfo.m_pClientEnt, m_PackInfo.m_PVS, m_PackInfo.m_nPVSSize );
|
|
|
|
// This is the frame we are creating, i.e., the next
|
|
// frame after the last one that the client acknowledged
|
|
|
|
m_pCurrentFrame = AllocateFrame();
|
|
m_pCurrentFrame->Init( pSnapshot );
|
|
|
|
m_PackInfo.m_pTransmitEdict = &m_pCurrentFrame->transmit_entity;
|
|
|
|
// if this client is the HLTV or Replay client, add the nocheck PVS bit array
|
|
// normal clients don't need that extra array
|
|
#if defined( REPLAY_ENABLED )
|
|
if ( IsHLTV() || IsReplay() )
|
|
#else
|
|
if ( IsHLTV() )
|
|
#endif
|
|
{
|
|
// the hltv client doesn't has a ClientFrame list
|
|
m_pCurrentFrame->transmit_always = new CBitVec<MAX_EDICTS>;
|
|
m_PackInfo.m_pTransmitAlways = m_pCurrentFrame->transmit_always;
|
|
}
|
|
else
|
|
{
|
|
m_PackInfo.m_pTransmitAlways = NULL;
|
|
}
|
|
|
|
// Add frame to ClientFrame list
|
|
|
|
int nMaxFrames = MAX( sv_maxclientframes.GetInt(), MAX_CLIENT_FRAMES );
|
|
|
|
// Only do this on dedicated servers (360 dedicated servers are !IsX360 so this check isn't strictly necessary)
|
|
// and non-x360 am servers due to concerns over memory growth on the consoles.
|
|
if ( ( !IsX360() || sv.IsDedicated() ) &&
|
|
( m_flTimeClientBecameFullyConnected != -1.0f ) &&
|
|
( realtime - m_flTimeClientBecameFullyConnected ) < sv_extra_client_connect_time.GetFloat() )
|
|
{
|
|
// For 15 seconds, the max will go from 128 (default) to 450 (assuming 0.0333 world tick interval)
|
|
// or to 960 (assuming 0.015625, 64 fps).
|
|
// In practice during changelevel on 360 I've seen it get up to 210 or so which is only 60% greater
|
|
// than the 128 frame max default, so 450 seems like it should capture all cases.
|
|
nMaxFrames = MAX( nMaxFrames, (int)( sv_extra_client_connect_time.GetFloat() / m_Server->GetTickInterval() ) );
|
|
// Msg( "Allowing up to %d frames for player for %f more seconds\n", nMaxFrames, realtime - m_flTimeClientBecameFullyConnected );
|
|
}
|
|
|
|
if ( sv_maxreplay.GetFloat() > 0 )
|
|
{
|
|
// if the server has replay features enabled, allow a way bigger frame buffer
|
|
nMaxFrames = MAX( nMaxFrames, sv_maxreplay.GetFloat() / m_Server->GetTickInterval() );
|
|
}
|
|
|
|
// During the startup period we retain additional frames. Once nMaxFrames drops we need to
|
|
// purge the extra frames or else we may permanently be using too much memory.
|
|
int frameCount = AddClientFrame( m_pCurrentFrame );
|
|
while ( nMaxFrames < frameCount )
|
|
{
|
|
// If the client has more than nMaxFrames frames, the server will start to eat too much memory.
|
|
RemoveOldestFrame();
|
|
--frameCount;
|
|
}
|
|
|
|
m_PackInfo.m_AreasNetworked = 0;
|
|
int areaCount = g_AreasNetworked.Count();
|
|
for ( int j = 0; j < areaCount; j++ )
|
|
{
|
|
// Msg("CGameClient::SetupPackInfo: too much areas (%i)", areaCount );
|
|
AssertOnce( m_PackInfo.m_AreasNetworked < MAX_WORLD_AREAS );
|
|
|
|
if ( m_PackInfo.m_AreasNetworked >= MAX_WORLD_AREAS )
|
|
break;
|
|
|
|
m_PackInfo.m_Areas[m_PackInfo.m_AreasNetworked] = g_AreasNetworked[ j ];
|
|
m_PackInfo.m_AreasNetworked++;
|
|
}
|
|
|
|
|
|
CM_SetupAreaFloodNums( m_PackInfo.m_AreaFloodNums, &m_PackInfo.m_nMapAreas );
|
|
}
|
|
|
|
ConVar spec_replay_rate_base( "spec_replay_rate_base", "1", FCVAR_RELEASE | FCVAR_REPLICATED, "Base time scale of Killer Replay.Experimental." );
|
|
|
|
void CGameClient::SetupHltvFrame( int nServerTick )
|
|
{
|
|
Assert( m_nHltvReplayDelay && m_pHltvReplayServer );
|
|
int nReplayTick = nServerTick - m_nHltvReplayDelay;
|
|
|
|
CClientFrame *pFrame = m_pHltvReplayServer->ExpandAndGetClientFrame( nReplayTick, false );
|
|
if ( !pFrame )
|
|
return;
|
|
|
|
m_pCurrentFrame = pFrame;
|
|
}
|
|
|
|
void CGameClient::SetupPrevPackInfo()
|
|
{
|
|
Assert( !IsHltvReplay() );
|
|
memcpy( &m_PrevTransmitEdict, m_PackInfo.m_pTransmitEdict, sizeof( m_PrevTransmitEdict ) );
|
|
|
|
// Copy the relevant fields into m_PrevPackInfo.
|
|
m_PrevPackInfo.m_AreasNetworked = m_PackInfo.m_AreasNetworked;
|
|
memcpy( m_PrevPackInfo.m_Areas, m_PackInfo.m_Areas, sizeof( m_PackInfo.m_Areas[0] ) * m_PackInfo.m_AreasNetworked );
|
|
|
|
m_PrevPackInfo.m_nPVSSize = m_PackInfo.m_nPVSSize;
|
|
memcpy( m_PrevPackInfo.m_PVS, m_PackInfo.m_PVS, m_PackInfo.m_nPVSSize );
|
|
|
|
m_PrevPackInfo.m_nMapAreas = m_PackInfo.m_nMapAreas;
|
|
memcpy( m_PrevPackInfo.m_AreaFloodNums, m_PackInfo.m_AreaFloodNums, m_PackInfo.m_nMapAreas * sizeof( m_PackInfo.m_nMapAreas ) );
|
|
}
|
|
|
|
|
|
/*
|
|
================
|
|
CheckRate
|
|
|
|
Make sure channel rate for active client is within server bounds
|
|
================
|
|
*/
|
|
void CGameClient::SetRate(int nRate, bool bForce )
|
|
{
|
|
if ( !bForce )
|
|
{
|
|
nRate = ClampClientRate( nRate );
|
|
}
|
|
|
|
BaseClass::SetRate( nRate, bForce );
|
|
}
|
|
void CGameClient::SetUpdateRate( float fUpdateRate, bool bForce )
|
|
{
|
|
if ( !bForce )
|
|
{
|
|
if ( CHLTVServer *hltv = GetAnyConnectedHltvServer() )
|
|
{
|
|
// Clients connected to our HLTV server will receive updates at tv_snapshotrate
|
|
fUpdateRate = hltv->GetSnapshotRate();
|
|
}
|
|
else
|
|
{
|
|
if ( sv_maxupdaterate.GetFloat() > 0 )
|
|
{
|
|
fUpdateRate = clamp( fUpdateRate, 1, sv_maxupdaterate.GetFloat() );
|
|
}
|
|
|
|
if ( sv_minupdaterate.GetInt() > 0 )
|
|
{
|
|
fUpdateRate = clamp( fUpdateRate, sv_minupdaterate.GetFloat(), 128.0f );
|
|
}
|
|
}
|
|
}
|
|
|
|
BaseClass::SetUpdateRate( fUpdateRate, bForce );
|
|
}
|
|
|
|
|
|
void CGameClient::UpdateUserSettings()
|
|
{
|
|
// set voice loopback
|
|
m_bVoiceLoopback = m_ConVars->GetInt( "voice_loopback", 0 ) != 0;
|
|
|
|
BaseClass::UpdateUserSettings();
|
|
|
|
// Give entity dll a chance to look at the changes.
|
|
// Do this after BaseClass::UpdateUserSettings() so name changes like prepending a (1)
|
|
// take effect before the server dll sees the name.
|
|
g_pServerPluginHandler->ClientSettingsChanged( edict );
|
|
}
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: A File has been received, if it's a logo, send it on to any other players who need it
|
|
// and return true, otherwise, return false
|
|
// Input : *cl -
|
|
// *filename -
|
|
// Output : Returns true on success, false on failure.
|
|
/*-----------------------------------------------------------------------------
|
|
bool CGameClient::ProcessIncomingLogo( const char *filename )
|
|
{
|
|
char crcfilename[ 512 ];
|
|
char logohex[ 16 ];
|
|
Q_binarytohex( (byte *)&logo, sizeof( logo ), logohex, sizeof( logohex ) );
|
|
|
|
Q_snprintf( crcfilename, sizeof( crcfilename ), "materials/decals/downloads/%s.vtf", logohex );
|
|
|
|
// It's not a logo file?
|
|
if ( Q_strcasecmp( filename, crcfilename ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// First, make sure crc is valid
|
|
CRC32_t check;
|
|
CRC_File( &check, crcfilename );
|
|
if ( check != logo )
|
|
{
|
|
ConMsg( "Incoming logo file didn't match player's logo CRC, ignoring\n" );
|
|
// Still note that it was a logo!
|
|
return true;
|
|
}
|
|
|
|
// Okay, looks good, see if any other players need this logo file
|
|
SV_SendLogo( check );
|
|
return true;
|
|
} */
|
|
|
|
bool CGameClient::IsHearingClient( int index ) const
|
|
{
|
|
#if defined( REPLAY_ENABLED )
|
|
if ( IsHLTV() || IsReplay() )
|
|
#else
|
|
if ( IsHLTV() )
|
|
#endif
|
|
return true;
|
|
|
|
if ( index == GetPlayerSlot() )
|
|
return m_bVoiceLoopback;
|
|
|
|
CGameClient *pClient = sv.Client( index );
|
|
|
|
// Don't send voice from one splitscreen partner to another on the same box
|
|
if ( !ss_voice_hearpartner.GetBool() &&
|
|
IsSplitScreenPartner( pClient ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return pClient->m_VoiceStreams.Get( GetPlayerSlot() ) != 0;
|
|
}
|
|
|
|
bool CGameClient::IsProximityHearingClient( int index ) const
|
|
{
|
|
CGameClient *pClient = sv.Client( index );
|
|
return pClient->m_VoiceProximity.Get( GetPlayerSlot() ) != 0;
|
|
}
|
|
|
|
void CGameClient::Inactivate( void )
|
|
{
|
|
if ( edict && !edict->IsFree() )
|
|
{
|
|
m_Server->RemoveClientFromGame( this );
|
|
}
|
|
|
|
if ( IsHLTV() )
|
|
{
|
|
if ( CHLTVServer *hltv = GetAnyConnectedHltvServer() )
|
|
{
|
|
hltv->Changelevel( true );
|
|
}
|
|
}
|
|
|
|
m_nHltvReplayDelay = 0;
|
|
m_pHltvReplayServer = NULL;
|
|
m_nHltvReplayStopAt = 0;
|
|
m_nHltvReplayStartAt = 0;
|
|
m_nHltvLastSendTick = 0; // last send tick, don't send ticks twice
|
|
|
|
#if defined( REPLAY_ENABLED )
|
|
if ( IsReplay() )
|
|
{
|
|
replay->Changelevel();
|
|
}
|
|
#endif
|
|
|
|
BaseClass::Inactivate();
|
|
|
|
m_Sounds.Purge();
|
|
ConVarRef voice_verbose( "voice_verbose" );
|
|
if ( voice_verbose.GetBool() )
|
|
{
|
|
Msg( "* CGameClient::Inactivate: Clearing m_VoiceStreams/m_VoiceProximity for %s (%s)\n", GetClientName(), GetNetChannel() ? GetNetChannel()->GetAddress() : "null" );
|
|
}
|
|
m_VoiceStreams.ClearAll();
|
|
m_VoiceProximity.ClearAll();
|
|
|
|
|
|
DeleteClientFrames( -1 ); // delete all
|
|
}
|
|
|
|
bool CGameClient::UpdateAcknowledgedFramecount(int tick)
|
|
{
|
|
// free old client frames which won't be used anymore
|
|
if ( tick != m_nDeltaTick )
|
|
{
|
|
// delta tick changed, free all frames smaller than tick
|
|
int removeTick = tick;
|
|
|
|
if ( sv_maxreplay.GetFloat() > 0 )
|
|
removeTick -= (sv_maxreplay.GetFloat() / m_Server->GetTickInterval() ); // keep a replay buffer
|
|
|
|
if ( removeTick > 0 )
|
|
{
|
|
DeleteClientFrames( removeTick );
|
|
}
|
|
}
|
|
|
|
return BaseClass::UpdateAcknowledgedFramecount( tick );
|
|
}
|
|
|
|
|
|
|
|
void CGameClient::Clear()
|
|
{
|
|
if ( m_bIsHLTV )
|
|
{
|
|
if ( CHLTVServer *hltv = GetAnyConnectedHltvServer() )
|
|
{
|
|
hltv->Shutdown();
|
|
}
|
|
}
|
|
|
|
#if defined( REPLAY_ENABLED )
|
|
if ( m_bIsReplay )
|
|
{
|
|
replay->Shutdown();
|
|
}
|
|
#endif
|
|
|
|
BaseClass::Clear();
|
|
|
|
m_HltvQueuedMessages.PurgeAndDeleteElements();
|
|
|
|
// free all frames
|
|
DeleteClientFrames( -1 );
|
|
|
|
m_Sounds.Purge();
|
|
ConVarRef voice_verbose( "voice_verbose" );
|
|
if ( voice_verbose.GetBool() )
|
|
{
|
|
Msg( "* CGameClient::Clear: Clearing m_VoiceStreams/m_VoiceProximity for %s (%s)\n", GetClientName(), GetNetChannel() ? GetNetChannel()->GetAddress() : "null" );
|
|
}
|
|
m_VoiceStreams.ClearAll();
|
|
m_VoiceProximity.ClearAll();
|
|
edict = NULL;
|
|
m_pViewEntity = NULL;
|
|
m_bVoiceLoopback = false;
|
|
m_LastMovementTick = 0;
|
|
m_nSoundSequence = 0;
|
|
m_flTimeClientBecameFullyConnected = -1.0f;
|
|
m_flLastClientCommandQuotaStart = -1.0f;
|
|
m_numClientCommandsInQuota = 0;
|
|
m_nHltvReplayDelay = 0;
|
|
m_pHltvReplayServer = NULL;
|
|
m_nHltvReplayStopAt = 0;
|
|
m_nHltvReplayStartAt = 0;
|
|
m_nHltvLastSendTick = 0;
|
|
m_flHltvLastReplayRequestTime = -spec_replay_message_time.GetFloat();
|
|
}
|
|
|
|
void CGameClient::Reconnect( void )
|
|
{
|
|
// If the client was connected before, tell the game .dll to disconnect him/her.
|
|
sv.RemoveClientFromGame( this );
|
|
|
|
BaseClass::Reconnect();
|
|
}
|
|
|
|
void CGameClient::PerformDisconnection( const char *pReason )
|
|
{
|
|
// notify other clients of player leaving the game
|
|
// send the username and network id so we don't depend on the CBasePlayer pointer
|
|
IGameEvent *event = g_GameEventManager.CreateEvent( "player_disconnect" );
|
|
|
|
if ( event )
|
|
{
|
|
event->SetInt("userid", GetUserID() );
|
|
event->SetString("reason", pReason );
|
|
event->SetString("name", GetClientName() );
|
|
event->SetUint64("xuid", GetClientXuid() );
|
|
event->SetString("networkid", GetNetworkIDString() );
|
|
g_GameEventManager.FireEvent( event );
|
|
}
|
|
|
|
m_Server->RemoveClientFromGame( this );
|
|
|
|
int nDisconnectSignonState = GetSignonState();
|
|
BaseClass::PerformDisconnection( pReason );
|
|
if ( nDisconnectSignonState >= SIGNONSTATE_NEW )
|
|
{
|
|
SV_ValidateMinRequiredClients( VALIDATE_DISCONNECT );
|
|
}
|
|
|
|
m_nHltvReplayDelay = 0;
|
|
m_pHltvReplayServer = NULL;
|
|
m_nHltvReplayStopAt = 0;
|
|
m_nHltvReplayStartAt = 0;
|
|
m_nHltvLastSendTick = 0;
|
|
}
|
|
|
|
HltvReplayStats_t m_DisconnectedClientsHltvReplayStats;
|
|
|
|
void CGameClient::Disconnect( const char *fmt )
|
|
{
|
|
// Remember what state we had when "Disconnect" got called
|
|
int nDisconnectSignonState = GetSignonState();
|
|
|
|
if ( nDisconnectSignonState == SIGNONSTATE_NONE )
|
|
return; // no recursion
|
|
|
|
m_DisconnectedClientsHltvReplayStats += m_HltvReplayStats;
|
|
m_HltvReplayStats.Reset();
|
|
|
|
BaseClass::Disconnect( fmt );
|
|
}
|
|
|
|
bool CGameClient::ProcessSignonStateMsg( int state, int spawncount )
|
|
{
|
|
if ( state == SIGNONSTATE_SPAWN || state == SIGNONSTATE_CHANGELEVEL )
|
|
{
|
|
StopHltvReplay();
|
|
}
|
|
else if ( state == SIGNONSTATE_CONNECTED )
|
|
{
|
|
if ( !CheckConnect() )
|
|
return false;
|
|
|
|
// Allow long enough time-out to load a map
|
|
float flTimeout = SIGNON_TIME_OUT;
|
|
if ( sv.IsDedicatedForXbox() )
|
|
flTimeout = SIGNON_TIME_OUT_360;
|
|
|
|
m_NetChannel->SetTimeout( flTimeout );
|
|
m_NetChannel->SetFileTransmissionMode( false );
|
|
m_NetChannel->SetMaxBufferSize( true, NET_MAX_PAYLOAD );
|
|
}
|
|
else if ( state == SIGNONSTATE_NEW )
|
|
{
|
|
if ( !sv.IsMultiplayer() )
|
|
{
|
|
// local client as received and create string tables,
|
|
// now link server tables to client tables
|
|
SV_InstallClientStringTableMirrors();
|
|
}
|
|
}
|
|
else if ( state == SIGNONSTATE_FULL )
|
|
{
|
|
if ( sv.m_bLoadgame )
|
|
{
|
|
// If this game was loaded from savegame, finish restoring game now
|
|
sv.FinishRestore();
|
|
}
|
|
|
|
m_NetChannel->SetTimeout( sv_timeout.GetFloat() ); // use smaller timeout limit
|
|
m_NetChannel->SetFileTransmissionMode( true );
|
|
|
|
g_pServerPluginHandler->ClientFullyConnect( edict );
|
|
}
|
|
|
|
return BaseClass::ProcessSignonStateMsg( state, spawncount );
|
|
}
|
|
|
|
void CGameClient::SendSound( SoundInfo_t &sound, bool isReliable )
|
|
{
|
|
#if defined( REPLAY_ENABLED )
|
|
if ( IsFakeClient() && !IsHLTV() && !IsReplay() && !IsSplitScreenUser() )
|
|
#else
|
|
if ( IsFakeClient() && !IsHLTV() && !IsSplitScreenUser() )
|
|
#endif
|
|
{
|
|
return; // dont send sound messages to bots
|
|
}
|
|
|
|
// don't send sound messages while client is replay mode
|
|
if ( m_bIsInReplayMode )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// reliable sounds are send as single messages
|
|
if ( isReliable )
|
|
{
|
|
CSVCMsg_Sounds_t *sndmsg = new CSVCMsg_Sounds_t;
|
|
|
|
m_nSoundSequence = ( m_nSoundSequence + 1 ) & SOUND_SEQNUMBER_MASK; // increase own sound sequence counter
|
|
sound.nSequenceNumber = 0; // don't transmit nSequenceNumber for reliable sounds
|
|
|
|
sndmsg->set_reliable_sound( true );
|
|
|
|
sound.WriteDelta( NULL, *sndmsg, sv.GetFinalTickTime() );
|
|
|
|
if ( net_showreliablesounds.GetBool() )
|
|
{
|
|
const char *name = "<Unknown>";
|
|
if ( sound.bIsSentence )
|
|
{
|
|
name = VOX_SentenceNameFromIndex( sound.nSoundNum );
|
|
}
|
|
else
|
|
{
|
|
if( sound.nFlags & SND_IS_SCRIPTHANDLE )
|
|
{
|
|
name = sound.pszName;
|
|
}
|
|
else
|
|
{
|
|
name = sv.GetSound( sound.nSoundNum );
|
|
}
|
|
}
|
|
Warning( "reliable%s %s %d/%d/%d/%s\n",
|
|
((sound.nFlags & SND_STOP) != 0)?" stop":"",
|
|
(sound.bIsSentence)?"sentence":"sound",
|
|
sound.nEntityIndex, sound.nChannel, sound.nSoundNum, name );
|
|
}
|
|
|
|
// send reliable sound as single message
|
|
SendNetMsg( *sndmsg, true );
|
|
|
|
if ( m_nHltvReplayDelay )
|
|
m_HltvQueuedMessages.AddToTail( sndmsg );
|
|
else
|
|
delete sndmsg;
|
|
|
|
return;
|
|
}
|
|
|
|
sound.nSequenceNumber = m_nSoundSequence;
|
|
|
|
m_Sounds.AddToTail( sound ); // queue sounds until snapshot is send
|
|
}
|
|
|
|
void CGameClient::WriteGameSounds( bf_write &buf, int nMaxSounds )
|
|
{
|
|
if ( m_Sounds.Count() <= 0 )
|
|
return;
|
|
|
|
CSVCMsg_Sounds_t msg;
|
|
|
|
msg.SetReliable( false );
|
|
int nSoundCount = FillSoundsMessage( msg, nMaxSounds );
|
|
msg.WriteToBuffer( buf );
|
|
|
|
if ( IsTracing() )
|
|
{
|
|
TraceNetworkData( buf, "Sounds [count=%d]", nSoundCount );
|
|
}
|
|
}
|
|
|
|
static ConVar sv_sound_discardextraunreliable( "sv_sound_discardextraunreliable", "1" );
|
|
|
|
int CGameClient::FillSoundsMessage(CSVCMsg_Sounds &msg, int nMaxSounds )
|
|
{
|
|
int i, count = m_Sounds.Count();
|
|
|
|
// Discard events if we have too many to signal with 8 bits
|
|
if ( count > nMaxSounds )
|
|
count = nMaxSounds;
|
|
|
|
// Nothing to send
|
|
if ( !count )
|
|
return 0;
|
|
|
|
SoundInfo_t defaultSound;
|
|
SoundInfo_t *pDeltaSound = &defaultSound;
|
|
|
|
msg.set_reliable_sound( false );
|
|
|
|
float finalTickTime = m_Server->GetFinalTickTime();
|
|
for ( i = 0 ; i < count; i++ )
|
|
{
|
|
SoundInfo_t &sound = m_Sounds[ i ];
|
|
sound.WriteDelta( pDeltaSound, msg, finalTickTime );
|
|
pDeltaSound = &m_Sounds[ i ];
|
|
}
|
|
|
|
// remove added events from list
|
|
if ( sv_sound_discardextraunreliable.GetBool() )
|
|
{
|
|
if ( m_Sounds.Count() != count )
|
|
{
|
|
DevMsg( 2, "Warning! Dropped %i unreliable sounds for client %s.\n" , m_Sounds.Count() - count, m_Name );
|
|
}
|
|
m_Sounds.RemoveAll();
|
|
}
|
|
else
|
|
{
|
|
int remove = m_Sounds.Count() - ( count + nMaxSounds );
|
|
if ( remove > 0 )
|
|
{
|
|
DevMsg( 2, "Warning! Dropped %i unreliable sounds for client %s.\n" , remove, m_Name );
|
|
count+= remove;
|
|
}
|
|
|
|
if ( count > 0 )
|
|
{
|
|
m_Sounds.RemoveMultiple( 0, count );
|
|
}
|
|
}
|
|
|
|
Assert( m_Sounds.Count() <= nMaxSounds );
|
|
|
|
return msg.sounds_size();
|
|
}
|
|
|
|
bool CGameClient::CheckConnect( void )
|
|
{
|
|
// Allow the game dll to reject this client.
|
|
char szRejectReason[128];
|
|
Q_strncpy( szRejectReason, "Connection rejected by game", sizeof( szRejectReason ) );
|
|
|
|
if ( !g_pServerPluginHandler->ClientConnect( edict, m_Name, m_NetChannel->GetAddress(), szRejectReason, sizeof( szRejectReason ) ) )
|
|
{
|
|
// Reject the connection and drop the client.
|
|
Disconnect( szRejectReason );
|
|
return false;
|
|
}
|
|
|
|
return BaseClass::CheckConnect();
|
|
}
|
|
|
|
void CGameClient::ActivatePlayer( void )
|
|
{
|
|
BaseClass::ActivatePlayer();
|
|
|
|
COM_TimestampedLog( "CGameClient::ActivatePlayer -start" );
|
|
|
|
// call the spawn function
|
|
if ( !sv.m_bLoadgame )
|
|
{
|
|
g_ServerGlobalVariables.curtime = sv.GetTime();
|
|
|
|
COM_TimestampedLog( "g_pServerPluginHandler->ClientPutInServer" );
|
|
|
|
g_pServerPluginHandler->ClientPutInServer( edict, m_Name );
|
|
}
|
|
|
|
COM_TimestampedLog( "g_pServerPluginHandler->ClientActive" );
|
|
|
|
g_pServerPluginHandler->ClientActive( edict, sv.m_bLoadgame );
|
|
|
|
COM_TimestampedLog( "g_pServerPluginHandler->ClientSettingsChanged" );
|
|
|
|
g_pServerPluginHandler->ClientSettingsChanged( edict );
|
|
|
|
COM_TimestampedLog( "GetTestScriptMgr()->CheckPoint" );
|
|
|
|
GetTestScriptMgr()->CheckPoint( "client_connected" );
|
|
|
|
// don't send signonstate to client, client will switch to FULL as soon
|
|
// as the first full entity update packets has been received
|
|
|
|
// fire a activate event
|
|
IGameEvent *event = g_GameEventManager.CreateEvent( "player_activate" );
|
|
|
|
if ( event )
|
|
{
|
|
event->SetInt( "userid", GetUserID() );
|
|
g_GameEventManager.FireEvent( event );
|
|
}
|
|
|
|
COM_TimestampedLog( "CGameClient::ActivatePlayer -end" );
|
|
|
|
// We'll let them have additional snapshots so the remote can connect w/o requiring a second
|
|
// non-delta update (since the client gets the uncompressed world, then does a bunch of loading
|
|
// which can take 7-10 seconds, then ack's a NET_Tick/delta tick which is way out of date by then
|
|
m_flTimeClientBecameFullyConnected = realtime;
|
|
}
|
|
|
|
bool CGameClient::SendSignonData( void )
|
|
{
|
|
bool bClientHasdifferentTables = false;
|
|
|
|
if ( sv.m_FullSendTables.IsOverflowed() )
|
|
{
|
|
Host_Error( "Send Table signon buffer overflowed %i bytes!!!\n", sv.m_FullSendTables.GetNumBytesWritten() );
|
|
return false;
|
|
}
|
|
|
|
if ( SendTable_GetCRC() != (CRC32_t)0 )
|
|
{
|
|
bClientHasdifferentTables = m_nSendtableCRC != SendTable_GetCRC();
|
|
}
|
|
|
|
#ifdef _DEBUG
|
|
if ( sv_sendtables.GetInt() == 2 )
|
|
{
|
|
// force sending class tables, for debugging
|
|
bClientHasdifferentTables = true;
|
|
}
|
|
#endif
|
|
|
|
// Write the send tables & class infos if needed
|
|
if ( bClientHasdifferentTables )
|
|
{
|
|
if ( sv_sendtables.GetBool() )
|
|
{
|
|
// send client class table descriptions so it can rebuild tables
|
|
ConDMsg("Client sent different SendTable CRC, sending full tables.\n" );
|
|
m_NetChannel->SendData( sv.m_FullSendTables );
|
|
}
|
|
else
|
|
{
|
|
Disconnect( "Server uses different class tables" );
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// use your class infos, CRC is correct
|
|
CSVCMsg_ClassInfo_t classmsg;
|
|
classmsg.set_create_on_client( true );
|
|
m_NetChannel->SendNetMsg( classmsg );
|
|
}
|
|
|
|
if ( !BaseClass::SendSignonData() )
|
|
return false;
|
|
|
|
m_nSoundSequence = 1; // reset sound sequence numbers after signon block
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void CGameClient::SpawnPlayer( void )
|
|
{
|
|
SV_ValidateMinRequiredClients( VALIDATE_SPAWN );
|
|
|
|
// run the entrance script
|
|
if ( sv.m_bLoadgame )
|
|
{ // loaded games are fully inited already
|
|
// if this is the last client to be connected, unpause
|
|
sv.SetPaused( false );
|
|
}
|
|
else
|
|
{
|
|
// set up the edict
|
|
Assert( serverGameEnts );
|
|
serverGameEnts->FreeContainingEntity( edict );
|
|
InitializeEntityDLLFields( edict );
|
|
}
|
|
|
|
// restore default client entity and turn off replay mdoe
|
|
m_nEntityIndex = m_nClientSlot+1;
|
|
m_bIsInReplayMode = false;
|
|
|
|
// set view entity
|
|
CSVCMsg_SetView_t setView;
|
|
setView.set_entity_index( m_nEntityIndex );
|
|
SendNetMsg( setView );
|
|
|
|
BaseClass::SpawnPlayer();
|
|
}
|
|
|
|
CClientFrame *CGameClient::GetDeltaFrame( int nTick )
|
|
{
|
|
Assert ( !IsHLTV() ); // has no ClientFrames
|
|
#if defined( REPLAY_ENABLED )
|
|
Assert ( !IsReplay() ); // has no ClientFrames
|
|
#endif
|
|
|
|
if ( m_bIsInReplayMode )
|
|
{
|
|
int followEntity;
|
|
|
|
serverGameClients->GetReplayDelay( edict, followEntity );
|
|
|
|
Assert( followEntity > 0 );
|
|
|
|
CGameClient *pFollowEntity = sv.Client( followEntity-1 );
|
|
|
|
if ( pFollowEntity )
|
|
return pFollowEntity->GetClientFrame( nTick );
|
|
}
|
|
|
|
return GetClientFrame( nTick );
|
|
}
|
|
|
|
void CGameClient::WriteViewAngleUpdate()
|
|
{
|
|
//
|
|
// send the current viewpos offset from the view entity
|
|
//
|
|
// a fixangle might get lost in a dropped packet. Oh well.
|
|
|
|
if ( IsFakeClient() && !IsSplitScreenUser() )
|
|
return;
|
|
|
|
Assert( serverGameClients );
|
|
CPlayerState *pl = serverGameClients->GetPlayerState( edict );
|
|
Assert( pl || IsSplitScreenUser() );
|
|
if ( !pl )
|
|
return;
|
|
|
|
if ( pl->fixangle != FIXANGLE_NONE )
|
|
{
|
|
if ( pl->fixangle == FIXANGLE_RELATIVE )
|
|
{
|
|
CSVCMsg_FixAngle_t fixAngle;
|
|
fixAngle.set_relative( true );
|
|
fixAngle.mutable_angle()->set_x( pl->anglechange.x );
|
|
fixAngle.mutable_angle()->set_y( pl->anglechange.y );
|
|
fixAngle.mutable_angle()->set_z( pl->anglechange.z );
|
|
m_NetChannel->SendNetMsg( fixAngle );
|
|
pl->anglechange.Init(); // clear
|
|
}
|
|
else
|
|
{
|
|
CSVCMsg_FixAngle_t fixAngle;
|
|
fixAngle.set_relative( false );
|
|
fixAngle.mutable_angle()->set_x( pl->v_angle.x );
|
|
fixAngle.mutable_angle()->set_y( pl->v_angle.y );
|
|
fixAngle.mutable_angle()->set_z( pl->v_angle.z );
|
|
m_NetChannel->SendNetMsg( fixAngle );
|
|
}
|
|
|
|
pl->fixangle = FIXANGLE_NONE;
|
|
}
|
|
}
|
|
|
|
/*
|
|
===================
|
|
SV_ValidateClientCommand
|
|
|
|
Determine if passed in user command is valid.
|
|
===================
|
|
*/
|
|
bool CGameClient::IsEngineClientCommand( const CCommand &args ) const
|
|
{
|
|
if ( args.ArgC() == 0 )
|
|
return false;
|
|
|
|
for ( int i = 0; s_clcommands[i] != NULL; ++i )
|
|
{
|
|
if ( !Q_strcasecmp( args[0], s_clcommands[i] ) )
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool CGameClient::SendNetMsg( INetMessage &msg, bool bForceReliable, bool bVoice )
|
|
{
|
|
if ( m_bIsHLTV )
|
|
{
|
|
if ( CHLTVServer* hltv = GetAnyConnectedHltvServer() )
|
|
{// pass this message to HLTV
|
|
return hltv->SendNetMsg( msg, bForceReliable, bVoice );
|
|
}
|
|
else
|
|
{
|
|
Warning("HLTV client has no HLTV server connected\n");
|
|
return false;
|
|
}
|
|
}
|
|
#if defined( REPLAY_ENABLED )
|
|
if ( m_bIsReplay )
|
|
{
|
|
// pass this message to replay
|
|
return replay->SendNetMsg( msg, bForceReliable, bVoice );
|
|
}
|
|
#endif
|
|
if ( IsHltvReplay() )
|
|
{
|
|
if ( msg.GetType() != svc_VoiceData ) // let the voice messages through
|
|
{
|
|
Assert( !bVoice );
|
|
bool bResult = true;
|
|
|
|
if ( msg.GetType() == svc_UserMessage )
|
|
{
|
|
// chat: see UTIL_SayText2Filter(), Say_Host() and "player_say" GameMessage
|
|
CSVCMsg_UserMessage_t &userMessageHeader = ( CSVCMsg_UserMessage_t & )msg;
|
|
// Only send through those user messages that require real-time timeline on the client.
|
|
switch ( userMessageHeader.msg_type() )
|
|
{
|
|
case 22: // CS_UM_RadioText
|
|
case 5: //CS_UM_SayText
|
|
case 6: // CS_UM_SayText2
|
|
case 7: // CS_UM_TextMsg
|
|
case 18: // CS_UM_RawAudio
|
|
// mark this message as real-time and send with that flag, to distinguish it from the replay messages
|
|
userMessageHeader.set_passthrough( 1 );
|
|
bResult = BaseClass::SendNetMsg( msg, bForceReliable, bVoice );
|
|
userMessageHeader.clear_passthrough();
|
|
break;
|
|
}
|
|
}
|
|
|
|
return bResult; // just ignore all (other) new messages: we'll take them from hltv later if needed
|
|
}
|
|
Assert( bVoice );
|
|
}
|
|
|
|
return BaseClass::SendNetMsg( msg, bForceReliable, bVoice );
|
|
}
|
|
|
|
static CUtlStringToken s_HltvUnskippableEvents[] = {
|
|
"round_start",
|
|
"begin_new_match",
|
|
"game_newmap"
|
|
};
|
|
|
|
static CUtlStringToken s_HltvQueueableEvents[] = {
|
|
"teamplay_round_start","teamplay_round_end",
|
|
MakeStringToken( "endmatch_cmm_start_reveal_items" ),
|
|
"announce_phase_end",
|
|
"cs_match_end_restart",
|
|
"round_freeze_end"
|
|
};
|
|
|
|
static CUtlStringToken s_HltvPassThroughRealtimeEvents[] = {
|
|
"teamplay_broadcast_audio",
|
|
"player_chat",
|
|
"player_say",
|
|
"player_death",
|
|
"round_mvp",
|
|
"round_end"
|
|
};
|
|
|
|
|
|
bool IsInList( const char *pEventName, const char **ppList, int nListCount )
|
|
{
|
|
for ( int i = 0; i < nListCount; ++i )
|
|
{
|
|
if ( !V_strcmp( pEventName, ppList[ i ] ) )
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool IsInList( CUtlStringToken eventName, const CUtlStringToken *pTokenList, int nListCount )
|
|
{
|
|
for ( int i = 0; i < nListCount; ++i )
|
|
{
|
|
if ( eventName == pTokenList[ i ] )
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void CGameClient::FireGameEvent( IGameEvent *event )
|
|
{
|
|
if ( IsHltvReplay() )
|
|
{
|
|
const char *pEventName = event->GetName(); // please don't fold the string variable, it's useful for debugging
|
|
CUtlStringToken eventName = MakeStringToken( pEventName );
|
|
|
|
if ( IsInList( eventName, s_HltvQueueableEvents, ARRAYSIZE( s_HltvQueueableEvents ) ) )
|
|
{
|
|
if ( event->IsReliable() ) // skip this event if it's not reliable
|
|
{
|
|
CSVCMsg_GameEvent_t *pEventMsg = new CSVCMsg_GameEvent_t;
|
|
if ( g_GameEventManager.SerializeEvent( event, pEventMsg ) )
|
|
{
|
|
m_HltvQueuedMessages.AddToTail( pEventMsg );
|
|
}
|
|
else
|
|
{
|
|
delete pEventMsg;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
else if ( IsInList( eventName, s_HltvUnskippableEvents, ARRAYSIZE( s_HltvUnskippableEvents ) ) )
|
|
{
|
|
Msg( "%s (%d) is skipping replay in progress (%d/%d) due to event %s\n", m_Name, m_UserID, sv.GetTick() - m_nHltvReplayDelay - m_nHltvReplayStartAt, m_nHltvReplayStopAt - m_nHltvReplayStartAt, pEventName );
|
|
StopHltvReplay();
|
|
return BaseClass::FireGameEvent( event );
|
|
}
|
|
else if ( IsInList( eventName, s_HltvPassThroughRealtimeEvents, ARRAYSIZE( s_HltvPassThroughRealtimeEvents ) ) )
|
|
{
|
|
return BaseClass::FireGameEvent( event, true ); // mark the event as real-time pass-through so that on the other end the client knows it's happening in real time, and it's not a replayed-back event
|
|
}
|
|
else
|
|
{
|
|
// For now, ignore game events by default, if they are sent while we're in a replay.
|
|
return;
|
|
}
|
|
}
|
|
|
|
return BaseClass::FireGameEvent( event );
|
|
}
|
|
|
|
bool CGameClient::ExecuteStringCommand( const char *pCommandString )
|
|
{
|
|
// first let the baseclass handle it
|
|
if ( BaseClass::ExecuteStringCommand( pCommandString ) )
|
|
return true;
|
|
|
|
// Determine whether the command is appropriate
|
|
CCommand args;
|
|
if ( !args.Tokenize( pCommandString, kCommandSrcNetClient ) )
|
|
return false;
|
|
|
|
if ( args.ArgC() == 0 )
|
|
return false;
|
|
|
|
// Disallow all string commands from client
|
|
// Special case for 0 here, as we don't kick in this case.
|
|
int cmdQuota = sv_quota_stringcmdspersecond.GetInt();
|
|
if ( cmdQuota == 0 )
|
|
return false;
|
|
|
|
// Client is about to execute a string command, check if we need to reset quota
|
|
if ( realtime - m_flLastClientCommandQuotaStart >= 1.0 )
|
|
{
|
|
// reset quota
|
|
m_flLastClientCommandQuotaStart = realtime;
|
|
m_numClientCommandsInQuota = 0;
|
|
}
|
|
++ m_numClientCommandsInQuota;
|
|
if ( m_numClientCommandsInQuota > cmdQuota )
|
|
{
|
|
// Disconnect player for Denial-of-service attack
|
|
|
|
// REI: Remove this define when we unify trunk/staging (trunk uses enum reasons for disconnection)
|
|
// REI: See network_connection.proto for where this is supposed to come from
|
|
#define NETWORK_DISCONNECT_SERVER_DOS "#GameUI_Disconnect_TooManyCommands"
|
|
Disconnect( NETWORK_DISCONNECT_SERVER_DOS );
|
|
return false;
|
|
}
|
|
|
|
if ( IsEngineClientCommand( args ) )
|
|
{
|
|
Cmd_ExecuteCommand( CBUF_SERVER, args, m_nClientSlot );
|
|
return true;
|
|
}
|
|
|
|
// FIXME: This logic seem strange; why can't we just go through Cmd_ExecuteCommand? We should check for
|
|
// permission (cheat, sponly, gamedll since we are coming from client code) there, right?
|
|
const ConCommandBase *pCommand = g_pCVar->FindCommandBase( args[ 0 ] );
|
|
if ( pCommand && pCommand->IsCommand() && pCommand->IsFlagSet( FCVAR_GAMEDLL ) )
|
|
{
|
|
// Allow cheat commands in singleplayer, debug, or multiplayer with sv_cheats on
|
|
// NOTE: Don't bother with rpt stuff; commands that matter there shouldn't have FCVAR_GAMEDLL set
|
|
if ( pCommand->IsFlagSet( FCVAR_CHEAT ) )
|
|
{
|
|
if ( sv.IsMultiplayer() && !CanCheat() )
|
|
return false;
|
|
}
|
|
else if ( !sv_allow_legacy_cmd_execution_from_client.GetBool() && !pCommand->IsFlagSet( FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS )
|
|
#if !defined DEDICATED
|
|
&& ( sv.IsDedicated() || m_nClientSlot != GetBaseLocalClient().m_nPlayerSlot )
|
|
#endif
|
|
)
|
|
{
|
|
#if DEVELOPMENT_ONLY
|
|
Warning( "WARNING: Client sent concommand %s which is not flagged as executable, ignoring\n" );
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
if ( pCommand->IsFlagSet( FCVAR_SPONLY ) )
|
|
{
|
|
if ( sv.IsMultiplayer() )
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
// REI 7/25/2016:
|
|
// I added this here; this state is normally set by Cmd_ExecuteCommand when executing
|
|
// a kCmdSrcNetClient command.
|
|
//
|
|
// Is there a reason this code path goes directly to Cmd_Dispatch instead
|
|
// of the usual path through Cmd_ExecuteCommand?
|
|
cmd_clientslot = m_nClientSlot;
|
|
|
|
g_pServerPluginHandler->SetCommandClient( m_nClientSlot );
|
|
Cmd_Dispatch( pCommand, args );
|
|
}
|
|
else
|
|
{
|
|
g_pServerPluginHandler->ClientCommand( edict, args ); // TODO pass client id and string
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
extern ConVar sv_multiplayer_maxsounds;
|
|
|
|
bool CGameClient::SendSnapshot( CClientFrame * pFrame )
|
|
{
|
|
if ( IsHltvReplay() )
|
|
{
|
|
return SendHltvReplaySnapshot( pFrame );
|
|
}
|
|
|
|
if ( m_bIsHLTV )
|
|
{
|
|
SNPROF( "SendSnapshot - HLTV" );
|
|
|
|
CHLTVServer *hltv = GetAnyConnectedHltvServer();
|
|
// pack sounds to one message
|
|
if ( m_Sounds.Count() > 0 )
|
|
{
|
|
CSVCMsg_Sounds_t sounds;
|
|
sounds.SetReliable( false );
|
|
FillSoundsMessage( sounds, m_Server->IsMultiplayer() ? sv_multiplayer_maxsounds.GetInt() : 255 );
|
|
hltv->SendNetMsg( sounds );
|
|
}
|
|
|
|
int maxEnts = tv_transmitall.GetBool()?255:64;
|
|
CSVCMsg_TempEntities_t tempentsmsg;
|
|
hltv->WriteTempEntities( this, pFrame->GetSnapshot(), m_pLastSnapshot.GetObject(), tempentsmsg, maxEnts );
|
|
if ( tempentsmsg.num_entries() )
|
|
{
|
|
tempentsmsg.WriteToBuffer( *hltv->GetBuffer( HLTV_BUFFER_TEMPENTS ) );
|
|
}
|
|
|
|
// add snapshot to HLTV server frame list
|
|
hltv->AddNewDeltaFrame( pFrame );
|
|
|
|
// remember this snapshot
|
|
m_pLastSnapshot = pFrame->GetSnapshot();
|
|
|
|
Assert( !GetHltvReplayDelay() ); // hltv master client shouldn't be in killer replay mode
|
|
// fake acknowledgement, remove ClientFrame reference immediately
|
|
UpdateAcknowledgedFramecount( pFrame->tick_count );
|
|
|
|
return true;
|
|
}
|
|
|
|
#if defined( REPLAY_ENABLED )
|
|
if ( m_bIsReplay )
|
|
{
|
|
SNPROF( "SendSnapshot - Replay" );
|
|
|
|
char *buf = (char *)_alloca( NET_MAX_PAYLOAD );
|
|
|
|
// pack sounds to one message
|
|
if ( m_Sounds.Count() > 0 )
|
|
{
|
|
CSVCMsg_Sounds_t sounds;
|
|
|
|
sounds.SetReliable( false );
|
|
FillSoundsMessage( sounds, m_Server->IsMultiplayer() ? sv_multiplayer_maxsounds.GetInt() : 255 );
|
|
replay->SendNetMsg( sounds );
|
|
}
|
|
|
|
int maxEnts = replay_transmitall.GetBool()?255:64;
|
|
replay->WriteTempEntities( this, pFrame->GetSnapshot(), m_pLastSnapshot.GetObject(), *replay->GetBuffer( REPLAY_BUFFER_TEMPENTS ), maxEnts );
|
|
|
|
// add snapshot to Replay server frame list
|
|
replay->AddNewFrame( pFrame );
|
|
|
|
// remember this snapshot
|
|
m_pLastSnapshot = pFrame->GetSnapshot();
|
|
|
|
// fake acknowledgement, remove ClientFrame reference immediately
|
|
UpdateAcknowledgedFramecount( pFrame->tick_count );
|
|
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
// update client viewangles update
|
|
WriteViewAngleUpdate();
|
|
|
|
bool bRet;
|
|
|
|
bRet = BaseClass::SendSnapshot( pFrame );
|
|
|
|
if ( bRet )
|
|
{
|
|
// Send messages that were queued up during replay - they need fully created entities, which is why I'm sending them after SendSnapshot
|
|
for ( INetMessage * pMessage : m_HltvQueuedMessages )
|
|
{
|
|
if ( m_NetChannel )
|
|
{
|
|
if ( !m_NetChannel->SendNetMsg( *pMessage, true ) ) // we only queue reliable messages
|
|
break;
|
|
}
|
|
}
|
|
m_HltvQueuedMessages.PurgeAndDeleteElements();
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
if ( IsFakeClient() )
|
|
{
|
|
Assert( !GetHltvReplayDelay() ); // fake clients should not be in "killer replay" mode
|
|
// fake acknowledgement, remove ClientFrame reference immediately
|
|
UpdateAcknowledgedFramecount( pFrame->tick_count );
|
|
}
|
|
}
|
|
|
|
return bRet;
|
|
}
|
|
|
|
//ConVar replay_hltv_voice( "replay_hltv_voice", "0", FCVAR_RELEASE );
|
|
|
|
bool CGameClient::SendHltvReplaySnapshot( CClientFrame * pFrame )
|
|
{
|
|
Assert( IsHltvReplay() );
|
|
VPROF_BUDGET( "CGameClient::SendHltvReplaySnapshot", "HLTV" );
|
|
|
|
byte buf[ NET_MAX_PAYLOAD ];
|
|
bf_write msg( "CGameClient::SendHltvReplaySnapshot", buf, sizeof( buf ) );
|
|
|
|
// 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_pLastSnapshot == pFrame->GetSnapshot() )
|
|
{
|
|
// never send the same snapshot twice
|
|
m_NetChannel->Transmit();
|
|
return false;
|
|
}
|
|
|
|
if ( m_nForceWaitForTick > 0 )
|
|
{
|
|
// just continue transmitting reliable data
|
|
Assert( !m_bFakePlayer ); // Should never happen
|
|
m_NetChannel->Transmit();
|
|
return false;
|
|
}
|
|
|
|
CClientFrame *pDeltaFrame = m_pHltvReplayServer->GetDeltaFrame( m_nDeltaTick ); // NULL if delta_tick is not found
|
|
CHLTVFrame *pLastFrame = ( CHLTVFrame* )m_pHltvReplayServer->GetDeltaFrame( m_nHltvLastSendTick );
|
|
|
|
if ( pLastFrame )
|
|
{
|
|
// start first frame after last send
|
|
pLastFrame = ( CHLTVFrame* )pLastFrame->m_pNext;
|
|
}
|
|
|
|
// add all reliable messages between ]lastframe,currentframe]
|
|
// add all tempent & sound messages between ]lastframe,currentframe]
|
|
while ( pLastFrame && pLastFrame->tick_count <= pFrame->tick_count )
|
|
{
|
|
m_NetChannel->SendData( pLastFrame->m_Messages[ HLTV_BUFFER_RELIABLE ], true );
|
|
|
|
if ( pDeltaFrame )
|
|
{
|
|
// if we send entities delta compressed, also send unreliable data
|
|
m_NetChannel->SendData( pLastFrame->m_Messages[ HLTV_BUFFER_UNRELIABLE ], false );
|
|
// we skip the voice messages here because we don't want to hear the 10-second-delayed voice.
|
|
// we might want to send the real-time voice though
|
|
// if ( replay_hltv_voice.GetBool() )
|
|
// {
|
|
// int nVoiceBits = pLastFrame->m_Messages[ HLTV_BUFFER_VOICE ].m_nDataBits;
|
|
// if ( nVoiceBits > 0 )
|
|
// {
|
|
// //Msg( "replaying voice: %d bits\n", nVoiceBits );
|
|
// m_NetChannel->SendData( pLastFrame->m_Messages[ HLTV_BUFFER_VOICE ], false ); // we separate voice, even though it's simply more unreliable data, because we don't send it in replay
|
|
// }
|
|
// }
|
|
}
|
|
|
|
pLastFrame = ( CHLTVFrame* )pLastFrame->m_pNext;
|
|
}
|
|
|
|
// now create client snapshot packet
|
|
|
|
// send tick time
|
|
CNETMsg_Tick_t tickmsg( pFrame->tick_count, host_frameendtime_computationduration, host_frametime_stddeviation, host_framestarttime_stddeviation );
|
|
tickmsg.set_hltv_replay_flags( 1 );
|
|
tickmsg.WriteToBuffer( msg );
|
|
|
|
// Update shared client/server string tables. Must be done before sending entities
|
|
m_Server->m_StringTables->WriteUpdateMessage( NULL, GetMaxAckTickCount(), msg );
|
|
|
|
// TODO delta cache whole snapshots, not just packet entities. then use net_Align
|
|
// send entity update, delta compressed if deltaFrame != NULL
|
|
{
|
|
CSVCMsg_PacketEntities_t packetmsg;
|
|
m_pHltvReplayServer->WriteDeltaEntities( this, pFrame, pDeltaFrame, packetmsg );
|
|
|
|
packetmsg.WriteToBuffer( msg );
|
|
}
|
|
|
|
// write message to packet and check for overflow
|
|
if ( msg.IsOverflowed() )
|
|
{
|
|
if ( !pDeltaFrame )
|
|
{
|
|
// if this is a reliable snapshot, drop the client
|
|
//Disconnect( NETWORK_DISCONNECT_SNAPSHOTOVERFLOW );
|
|
|
|
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
// unreliable snapshots may be dropped
|
|
ConMsg( "WARNING: msg overflowed for %s\n", m_Name );
|
|
msg.Reset();
|
|
}
|
|
}
|
|
|
|
// remember this snapshot
|
|
m_pLastSnapshot = pFrame->GetSnapshot();
|
|
m_nHltvLastSendTick = pFrame->tick_count;
|
|
|
|
// Don't send the datagram to fakeplayers
|
|
if ( m_bFakePlayer )
|
|
{
|
|
m_nDeltaTick = pFrame->tick_count;
|
|
return true;
|
|
}
|
|
|
|
bool bSendOK;
|
|
|
|
// is this is a full entity update (no delta) ?
|
|
if ( !pDeltaFrame )
|
|
{
|
|
if ( replay_debug.GetInt() > 10 )
|
|
Msg( "HLTV send full frame %d: %d bytes\n", pFrame->tick_count, ( msg.m_iCurBit + 7 ) / 8 );
|
|
// 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
|
|
{
|
|
if ( replay_debug.GetInt() > 10 )
|
|
Msg( "HLTV send %d-delta of frame %d: %d bytes\n", pFrame->tick_count - pDeltaFrame->tick_count, pFrame->tick_count, ( msg.m_iCurBit + 7 ) / 8 );
|
|
// just send it as unreliable snapshot
|
|
bSendOK = m_NetChannel->SendDatagram( &msg ) > 0;
|
|
}
|
|
|
|
if ( !bSendOK )
|
|
{
|
|
Disconnect( "Snapshot error" );
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
bool CGameClient::CanStartHltvReplay()
|
|
{
|
|
CActiveHltvServerIterator hltv;
|
|
if ( hltv && !IsFakeClient() )
|
|
{
|
|
int nOldestHltvTick = hltv->GetOldestTick();
|
|
return ( nOldestHltvTick > 0 ); // we should have some ticks in history to proceed successfully
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void CGameClient::ResetReplayRequestTime()
|
|
{
|
|
m_flHltvLastReplayRequestTime = -spec_replay_message_time.GetFloat();
|
|
}
|
|
|
|
bool CGameClient::StartHltvReplay( const HltvReplayParams_t ¶ms )
|
|
{
|
|
m_HltvReplayStats.nStartRequests++;
|
|
CActiveHltvServerIterator hltv;
|
|
if ( hltv && !IsFakeClient() )
|
|
{
|
|
if ( !params.m_bAbortCurrentReplay && m_nHltvReplayDelay )
|
|
{
|
|
// we're already in replay, do not abort it to start a new one
|
|
DevMsg( "Hltv Replay failure: already in replay\n" );
|
|
m_HltvReplayStats.nFailedReplays[ HltvReplayStats_t::FAILURE_ALREADY_IN_REPLAY ]++;
|
|
return false;
|
|
}
|
|
|
|
int nServerTick = sv.GetTick();
|
|
float flRealTime = Plat_FloatTime();
|
|
if ( fabsf( flRealTime - m_flHltvLastReplayRequestTime ) <= spec_replay_rate_limit.GetFloat() )
|
|
{
|
|
DevMsg( "Hltv Replay failure: requests are rate limited to no more than 1 per %g seconds\n", spec_replay_rate_limit.GetFloat() );
|
|
m_HltvReplayStats.nFailedReplays[ HltvReplayStats_t::FAILURE_TOO_FREQUENT ]++;
|
|
return false;
|
|
}
|
|
|
|
int nOldestHltvTick = hltv->GetOldestTick();
|
|
if ( nOldestHltvTick > 0 ) // we should have some ticks in history to proceed successfully
|
|
{
|
|
// GetMaxAckTickCount() cannot be older than signon tick, and I don't know how much logic quietly relies on it, so to make it easier on myself I just won't allow reaching further into the past than the signon time, at least for the first iteration of replay
|
|
int nOldestClientTick = Max( m_nSignonTick, nOldestHltvTick );
|
|
|
|
float flTickInterval = sv.GetTickInterval();
|
|
int nDesiredReplayDelay = params.m_flDelay / flTickInterval, nNewReplayDelay = nDesiredReplayDelay;
|
|
int nNewReplayStopAt = nServerTick + params.m_flStopAt / flTickInterval;
|
|
|
|
Assert( hltv->m_CurrentFrame->tick_count >= hltv->m_nFirstTick ); // first known tick should have happened at or before the time of the current recorded HLTV frame
|
|
if ( nServerTick - nNewReplayDelay < nOldestClientTick )
|
|
{
|
|
nNewReplayDelay = nServerTick - nOldestClientTick;
|
|
}
|
|
|
|
if ( nNewReplayDelay <= 0 || nNewReplayStopAt <= nServerTick - nNewReplayDelay )
|
|
{
|
|
m_HltvReplayStats.nFailedReplays[ HltvReplayStats_t::FAILURE_NO_FRAME ]++;
|
|
nNewReplayDelay = nNewReplayStopAt = 0;
|
|
m_pCurrentFrame = NULL;
|
|
}
|
|
else
|
|
{
|
|
m_pCurrentFrame = hltv->ExpandAndGetClientFrame( nServerTick - nNewReplayDelay, false );
|
|
if ( m_pCurrentFrame )
|
|
{
|
|
nNewReplayDelay = nServerTick - m_pCurrentFrame->tick_count;
|
|
}
|
|
else
|
|
{
|
|
m_HltvReplayStats.nFailedReplays[ HltvReplayStats_t::FAILURE_NO_FRAME2 ]++;
|
|
nNewReplayDelay = nNewReplayStopAt = 0;
|
|
}
|
|
}
|
|
|
|
if ( !m_pCurrentFrame || abs( nNewReplayDelay - nDesiredReplayDelay ) > 64 )
|
|
{
|
|
DevMsg( "Hltv replay delay %u cannot match the requested delay %u\n", nNewReplayDelay, nDesiredReplayDelay );
|
|
m_HltvReplayStats.nFailedReplays[ HltvReplayStats_t::FAILURE_CANNOT_MATCH_DELAY ]++;
|
|
nNewReplayDelay = nNewReplayStopAt = 0; // couldn't find anything decently approaching the desired delay in history
|
|
}
|
|
|
|
// now commit all the changes if needed
|
|
m_nHltvReplayStopAt = nNewReplayStopAt;
|
|
m_nHltvReplayStartAt = nServerTick;
|
|
if ( nNewReplayDelay != m_nHltvReplayDelay )
|
|
{
|
|
CSVCMsg_HltvReplay_t msg;
|
|
msg.set_delay( nNewReplayDelay );
|
|
msg.set_primary_target( params.m_nPrimaryTargetEntIndex );
|
|
msg.set_replay_stop_at( nNewReplayStopAt );
|
|
msg.set_replay_start_at( m_nHltvReplayStartAt );
|
|
|
|
if ( params.m_flSlowdownRate > 1.0f / 16.0f && params.m_flSlowdownBeginAt + 0.125f < params.m_flSlowdownEndAt )
|
|
{
|
|
m_flHltvReplaySlowdownRate = params.m_flSlowdownRate;
|
|
m_nHltvReplaySlowdownBeginAt = Max<int>( nServerTick - nNewReplayDelay, nServerTick + params.m_flSlowdownBeginAt / flTickInterval );
|
|
m_nHltvReplaySlowdownEndAt = Max<int>( m_nHltvReplaySlowdownBeginAt, nServerTick + params.m_flSlowdownEndAt / flTickInterval );
|
|
msg.set_replay_slowdown_rate( m_flHltvReplaySlowdownRate );
|
|
msg.set_replay_slowdown_begin( m_nHltvReplaySlowdownBeginAt );
|
|
msg.set_replay_slowdown_end( m_nHltvReplaySlowdownEndAt );
|
|
}
|
|
else
|
|
{
|
|
m_flHltvReplaySlowdownRate = 1.0f;
|
|
m_nHltvReplaySlowdownBeginAt = 0;
|
|
m_nHltvReplaySlowdownEndAt = 0;
|
|
}
|
|
|
|
SendNetMsg( msg, true );
|
|
//if( nNewReplayDelay) ExecuteStringCommand( "spectate" );
|
|
|
|
if ( replay_debug.GetBool() )
|
|
Msg( "Start HLMV Replay at %d, delay %d, until %d\n", nServerTick, nNewReplayDelay, nNewReplayStopAt );
|
|
m_nHltvReplayDelay = nNewReplayDelay;
|
|
m_pHltvReplayServer = hltv;
|
|
m_nDeltaTick = -1;
|
|
if ( m_nStringTableAckTick > nServerTick - nNewReplayDelay )
|
|
m_nStringTableAckTick = 0; // need to reset the stringtables, as they were updated in the future relative to the delayed stream
|
|
m_pLastSnapshot = NULL;
|
|
m_nHltvLastSendTick = 0;
|
|
FreeBaselines();
|
|
// all these data become invalid once we start sending HLTV packets from the past
|
|
m_PackInfo.Reset();
|
|
m_PrevPackInfo.Reset();
|
|
m_pCurrentFrame = NULL;
|
|
DeleteClientFrames( -1 ); // Should we clean up all the frames? Seems logical, as we'll never need them
|
|
m_flHltvLastReplayRequestTime = flRealTime;
|
|
m_HltvReplayStats.nSuccessfulStarts++;
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DevMsg( "Hltv Replay failure: HLTV frame is not ready\n" );
|
|
m_HltvReplayStats.nFailedReplays[ HltvReplayStats_t::FAILURE_FRAME_NOT_READY ]++;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
CBaseClient *CGameClient::GetPropCullClient()
|
|
{
|
|
return GetHltvReplayDelay() ? m_pHltvReplayServer->m_MasterClient : this;
|
|
}
|
|
|
|
|
|
static char s_HltvReplayBuffers[ 8 ][ 256 ];
|
|
uint s_nLastHltvReplayBuffer = 0;
|
|
|
|
const char *HltvReplayStats_t::AsString()const
|
|
{
|
|
if ( !nSuccessfulStarts && !nStartRequests && !nFullReplays && !nUserCancels && !nStopRequests )
|
|
{
|
|
return "";
|
|
}
|
|
|
|
s_nLastHltvReplayBuffer++;
|
|
if ( s_nLastHltvReplayBuffer >= ARRAYSIZE( s_HltvReplayBuffers ) )
|
|
s_nLastHltvReplayBuffer = 0;
|
|
char *pBuffer = s_HltvReplayBuffers[ s_nLastHltvReplayBuffer ];
|
|
|
|
CUtlString fails = ":";
|
|
int nTotalFailures = 0;
|
|
for ( int i = 0; i < NUM_FAILURES; ++i )
|
|
{
|
|
if ( i )
|
|
fails += ",";
|
|
|
|
if ( nFailedReplays[ i ] )
|
|
{
|
|
fails.Append( CFmtStr( "%u", nFailedReplays[ i ] ) );
|
|
nTotalFailures += nFailedReplays[ i ];
|
|
}
|
|
}
|
|
|
|
if ( nNetAbortReplays )
|
|
{
|
|
fails.Append( CFmtStr( "[%u!]", nNetAbortReplays ) );
|
|
nTotalFailures += nNetAbortReplays;
|
|
}
|
|
|
|
if ( !nTotalFailures )
|
|
fails = "";
|
|
|
|
V_snprintf( pBuffer, sizeof( s_HltvReplayBuffers[ s_nLastHltvReplayBuffer ] ), "%u/%u started, %u full, %u cancels / %u stops, %u fails%s", nSuccessfulStarts, nStartRequests, nFullReplays, nUserCancels, nStopRequests, nTotalFailures, fails.Get() );
|
|
|
|
return pBuffer;
|
|
}
|
|
|
|
|
|
void CGameClient::StepHltvReplayStatus( int nServerTick )
|
|
{
|
|
if ( IsHltvReplay() )
|
|
{
|
|
if ( m_nHltvLastSendTick >= m_nHltvReplayStopAt )
|
|
{
|
|
m_HltvReplayStats.nFullReplays++;
|
|
StopHltvReplay();
|
|
}
|
|
else if ( m_nForceWaitForTick > 0 )
|
|
{
|
|
if ( !m_pCurrentFrame || m_pCurrentFrame->tick_count >= m_nHltvReplayStopAt )
|
|
{
|
|
// client doesn't respond or there's no current frame -both indicating some problem. And we're past the time alotted for replay - we should just abort.
|
|
Msg( "Client %d (eidx %d, user id %d) %s - aborting wait for ack for tick %d, stopping replay at %d>=%d\n", m_nClientSlot, m_nEntityIndex, m_UserID, m_Name, m_nForceWaitForTick, m_pCurrentFrame ? m_pCurrentFrame->tick_count : 0, m_nHltvReplayStopAt );
|
|
m_HltvReplayStats.nNetAbortReplays++;
|
|
StopHltvReplay();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void CGameClient::StopHltvReplay()
|
|
{
|
|
if ( IsHltvReplay() )
|
|
{
|
|
m_HltvReplayStats.nStopRequests++;
|
|
if ( m_nHltvLastSendTick < m_nHltvReplayStopAt )
|
|
m_HltvReplayStats.nAbortStopRequests++;
|
|
m_nHltvReplayStopAt = 0;
|
|
m_nHltvReplayDelay = 0;
|
|
m_nDeltaTick = -1;
|
|
m_nForceWaitForTick = -1;
|
|
m_pLastSnapshot = NULL; // it doesn't matter what last snapshot we sent; we need to send a full frame update
|
|
m_nHltvLastSendTick = 0;
|
|
FreeBaselines();
|
|
m_pCurrentFrame = NULL;
|
|
DeleteClientFrames( -1 ); // Should we clean up all the frames? Seems logical, as we'll never need them
|
|
Assert( CountClientFrames() == 0 ); // we shouldn't have used the client frame manager to send HLTV stream to client
|
|
}
|
|
// just in case, send the end-of-hltv replay message even if we are not in replay
|
|
{
|
|
CSVCMsg_HltvReplay_t msg;
|
|
SendNetMsg( msg, true );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// This function contains all the logic to determine if we should send a datagram
|
|
// to a particular client
|
|
//-----------------------------------------------------------------------------
|
|
|
|
bool CGameClient::ShouldSendMessages( void )
|
|
{
|
|
if ( m_bIsHLTV )
|
|
{
|
|
// calc snapshot interval
|
|
if ( CHLTVServer *hltv = GetAnyConnectedHltvServer() )
|
|
{
|
|
int nSnapshotInterval = 1.0f / ( m_Server->GetTickInterval() * hltv->GetSnapshotRate() );
|
|
// I am the HLTV client, record every nSnapshotInterval tick
|
|
return ( sv.m_nTickCount >= ( hltv->m_nLastTick + nSnapshotInterval ) );
|
|
}
|
|
else
|
|
{
|
|
return false; // something is wrong, it'll assert in GetAnyConnectedHltvServer()..
|
|
}
|
|
}
|
|
|
|
#if defined( REPLAY_ENABLED )
|
|
if ( m_bIsReplay )
|
|
{
|
|
// calc snapshot interval
|
|
int nSnapshotInterval = 1.0f / ( m_Server->GetTickInterval() * replay_snapshotrate.GetFloat() );
|
|
|
|
// I am the Replay client, record every nSnapshotInterval tick
|
|
return ( sv.m_nTickCount >= (replay->m_nLastTick + nSnapshotInterval) );
|
|
}
|
|
#endif
|
|
|
|
// If sv_stressbots is true, then treat a bot more like a regular client and do deltas and such for it.
|
|
if( !sv_replaybots.GetBool() && IsFakeClient() )
|
|
{
|
|
if ( !sv_stressbots.GetBool() )
|
|
return false;
|
|
}
|
|
|
|
return BaseClass::ShouldSendMessages();
|
|
}
|
|
|
|
void CGameClient::FileReceived( const char *fileName, unsigned int transferID, bool bIsReplayDemoFile /* = false */ )
|
|
{
|
|
//check if file is one of our requested custom files
|
|
for ( int i=0; i<MAX_CUSTOM_FILES; i++ )
|
|
{
|
|
if ( m_nCustomFiles[i].reqID == transferID )
|
|
{
|
|
m_nFilesDownloaded++;
|
|
|
|
// broadcast update to other clients so they start downlaoding this file
|
|
m_Server->UserInfoChanged( m_nClientSlot );
|
|
return;
|
|
}
|
|
}
|
|
|
|
Msg( "CGameClient::FileReceived: %s not wanted.\n", fileName );
|
|
}
|
|
|
|
void CGameClient::FileRequested(const char *fileName, unsigned int transferID, bool bIsReplayDemoFile /* = false */ )
|
|
{
|
|
DevMsg( "File '%s' requested from client %s.\n", fileName, m_NetChannel->GetAddress() );
|
|
|
|
if ( sv_allowdownload.GetBool() )
|
|
{
|
|
m_NetChannel->SendFile( fileName, transferID, bIsReplayDemoFile );
|
|
}
|
|
else
|
|
{
|
|
m_NetChannel->DenyFile( fileName, transferID, bIsReplayDemoFile );
|
|
}
|
|
}
|
|
|
|
void CGameClient::FileDenied(const char *fileName, unsigned int transferID, bool bIsReplayDemoFile /* = false */ )
|
|
{
|
|
ConMsg( "Downloading file '%s' from client %s failed.\n", fileName, GetClientName() );
|
|
}
|
|
|
|
void CGameClient::FileSent(const char *fileName, unsigned int transferID, bool bIsReplayDemoFile /* = false */ )
|
|
{
|
|
ConMsg( "Sent file '%s' to client %s.\n", fileName, GetClientName() );
|
|
}
|
|
|
|
void CGameClient::PacketStart(int incoming_sequence, int outgoing_acknowledged)
|
|
{
|
|
for ( int i = 1; i < host_state.max_splitscreen_players; ++i )
|
|
{
|
|
if ( !m_SplitScreenUsers[ i ] )
|
|
continue;
|
|
|
|
m_SplitScreenUsers[ i ]->PacketStart( incoming_sequence, outgoing_acknowledged );
|
|
}
|
|
|
|
// make sure m_LastMovementTick != sv.tickcount
|
|
m_LastMovementTick = ( sv.m_nTickCount - 1 );
|
|
|
|
host_client = this;
|
|
|
|
// During connection, only respond if client sends a packet
|
|
m_bReceivedPacket = true;
|
|
}
|
|
|
|
void CGameClient::PacketEnd()
|
|
{
|
|
// Fix up clock in case prediction/etc. code reset it.
|
|
g_ServerGlobalVariables.frametime = host_state.interval_per_tick;
|
|
}
|
|
|
|
void CGameClient::ConnectionClosing(const char *reason)
|
|
{
|
|
SV_RedirectEnd();
|
|
|
|
Disconnect( (reason!=NULL)?reason:"Connection closing" );
|
|
}
|
|
|
|
void CGameClient::ConnectionCrashed(const char *reason)
|
|
{
|
|
if ( m_Name[0] && IsConnected() )
|
|
{
|
|
SV_RedirectEnd();
|
|
|
|
Disconnect( (reason!=NULL)?reason:"Connection lost" );
|
|
}
|
|
}
|
|
|
|
CClientFrame *CGameClient::GetSendFrame()
|
|
{
|
|
CClientFrame *pFrame = m_pCurrentFrame;
|
|
|
|
// just return if replay is disabled
|
|
if ( sv_maxreplay.GetFloat() <= 0 || IsHltvReplay() )
|
|
return pFrame;
|
|
|
|
int followEntity;
|
|
|
|
int delayTicks = serverGameClients->GetReplayDelay( edict, followEntity );
|
|
|
|
bool isInReplayMode = ( delayTicks > 0 );
|
|
|
|
if ( isInReplayMode != m_bIsInReplayMode )
|
|
{
|
|
// force a full update when modes are switched
|
|
m_nDeltaTick = -1;
|
|
|
|
m_bIsInReplayMode = isInReplayMode;
|
|
|
|
if ( isInReplayMode )
|
|
{
|
|
m_nEntityIndex = followEntity;
|
|
}
|
|
else
|
|
{
|
|
m_nEntityIndex = m_nClientSlot+1;
|
|
}
|
|
}
|
|
|
|
Assert( (m_nClientSlot+1 == m_nEntityIndex) || isInReplayMode );
|
|
|
|
if ( isInReplayMode )
|
|
{
|
|
CGameClient *pFollowPlayer = sv.Client( followEntity-1 );
|
|
|
|
if ( !pFollowPlayer )
|
|
return NULL;
|
|
|
|
pFrame = pFollowPlayer->GetClientFrame( sv.GetTick() - delayTicks, false );
|
|
|
|
if ( !pFrame )
|
|
return NULL;
|
|
|
|
if ( m_pLastSnapshot == pFrame->GetSnapshot() )
|
|
return NULL;
|
|
}
|
|
|
|
return pFrame;
|
|
}
|
|
|
|
bool CGameClient::IgnoreTempEntity( CEventInfo *event )
|
|
{
|
|
// in replay mode replay all temp entities
|
|
if ( m_bIsInReplayMode )
|
|
return false;
|
|
|
|
return BaseClass::IgnoreTempEntity( event );
|
|
}
|
|
|
|
|
|
const CCheckTransmitInfo* CGameClient::GetPrevPackInfo()
|
|
{
|
|
Assert( !IsHltvReplay() ); // we don't maintain this data during Hltv-fed replay
|
|
return &m_PrevPackInfo;
|
|
}
|
|
|
|
// This code is useful for verifying that the networking of soundinfo_t stuff isn't borked.
|
|
#if 0
|
|
|
|
#include "vstdlib/random.h"
|
|
|
|
class CTestSoundInfoNetworking
|
|
{
|
|
public:
|
|
|
|
CTestSoundInfoNetworking();
|
|
|
|
void RunTest();
|
|
|
|
private:
|
|
|
|
void CreateRandomSounds( int nCount );
|
|
void CreateRandomSound( SoundInfo_t &si );
|
|
void Compare( const SoundInfo_t &s1, const SoundInfo_t &s2 );
|
|
|
|
CUtlVector< SoundInfo_t > m_Sounds;
|
|
|
|
CUtlVector< SoundInfo_t > m_Received;
|
|
};
|
|
|
|
static CTestSoundInfoNetworking g_SoundTest;
|
|
|
|
CON_COMMAND( st, "sound test" )
|
|
{
|
|
int nCount = 1;
|
|
if ( args.ArgC() >= 2 )
|
|
{
|
|
nCount = clamp( Q_atoi( args.Arg( 1 ) ), 1, 100000 );
|
|
}
|
|
|
|
for ( int i = 0 ; i < nCount; ++i )
|
|
{
|
|
if ( !( i % 100 ) && i > 0 )
|
|
{
|
|
Msg( "Running test %d %f %% done\n",
|
|
i, 100.0f * (float)i/(float)nCount );
|
|
}
|
|
g_SoundTest.RunTest();
|
|
}
|
|
}
|
|
CTestSoundInfoNetworking::CTestSoundInfoNetworking()
|
|
{
|
|
}
|
|
|
|
void CTestSoundInfoNetworking::CreateRandomSound( SoundInfo_t &si )
|
|
{
|
|
int entindex = RandomInt( 0, MAX_EDICTS - 1 );
|
|
int channel = RandomInt( 0, 7 );
|
|
int soundnum = RandomInt( 0, MAX_SOUNDS - 1 );
|
|
Vector org = RandomVector( -16383, 16383 );
|
|
Vector dir = RandomVector( -1.0f, 1.0f );
|
|
float flVolume = RandomFloat( 0.1f, 1.0f );
|
|
bool bLooping = RandomInt( 0, 100 ) < 5;
|
|
int nPitch = RandomInt( 0, 100 ) < 5 ? RandomInt( 95, 105 ) : 100;
|
|
Vector lo = RandomInt( 0, 100 ) < 5 ? RandomVector( -16383, 16383 ) : org;
|
|
int speaker = RandomInt( 0, 100 ) < 2 ? RandomInt( 0, MAX_EDICTS - 1 ) : -1;
|
|
soundlevel_t level = soundlevel_t(RandomInt( 70, 150 ));
|
|
|
|
si.Set( entindex, channel, "foo.wav", org, dir, flVolume, level, bLooping, nPitch, lo, speaker );
|
|
|
|
si.nFlags = ( 1 << RandomInt( 0, 6 ) );
|
|
si.nSoundNum = soundnum;
|
|
si.bIsSentence = RandomInt( 0, 1 );
|
|
si.bIsAmbient = RandomInt( 0, 1 );
|
|
si.fDelay = RandomInt( 0, 100 ) < 2 ? RandomFloat( -0.1, 0.1f ) : 0.0f;
|
|
}
|
|
|
|
void CTestSoundInfoNetworking::CreateRandomSounds( int nCount )
|
|
{
|
|
m_Sounds.Purge();
|
|
m_Sounds.EnsureCount( nCount );
|
|
|
|
for ( int i = 0; i < nCount; ++i )
|
|
{
|
|
SoundInfo_t &si = m_Sounds[ i ];
|
|
CreateRandomSound( si );
|
|
}
|
|
}
|
|
|
|
void CTestSoundInfoNetworking::RunTest()
|
|
{
|
|
int m_nSoundSequence = 0;
|
|
|
|
CreateRandomSounds( 512 );
|
|
|
|
SoundInfo_t defaultSound; defaultSound.SetDefault();
|
|
SoundInfo_t *pDeltaSound = &defaultSound;
|
|
|
|
CSVCMsg_Sounds_t msg;
|
|
|
|
msg.set_reliable_sound( false );
|
|
msg.SetReliable( false );
|
|
|
|
for ( int i = 0 ; i < m_Sounds.Count(); i++ )
|
|
{
|
|
SoundInfo_t &sound = m_Sounds[ i ];
|
|
sound.WriteDelta( pDeltaSound, msg.m_DataOut );
|
|
pDeltaSound = &m_Sounds[ i ];
|
|
}
|
|
|
|
// Now read them out
|
|
defaultSound.SetDefault();
|
|
pDeltaSound = &defaultSound;
|
|
|
|
msg.m_DataIn.StartReading( buf, msg.m_DataOut.GetNumBytesWritten(), 0, msg.m_DataOut.GetNumBitsWritten() );
|
|
|
|
SoundInfo_t sound;
|
|
|
|
for ( int i=0; i<msg.m_nNumSounds; i++ )
|
|
{
|
|
sound.ReadDelta( pDeltaSound, msg.m_DataIn );
|
|
|
|
pDeltaSound = &sound; // copy delta values
|
|
|
|
if ( msg.m_bReliableSound )
|
|
{
|
|
// client is incrementing the reliable sequence numbers itself
|
|
m_nSoundSequence = ( m_nSoundSequence + 1 ) & SOUND_SEQNUMBER_MASK;
|
|
|
|
Assert ( sound.nSequenceNumber == 0 );
|
|
|
|
sound.nSequenceNumber = m_nSoundSequence;
|
|
}
|
|
|
|
// Add no ambient sounds to sorted queue, will be processed after packet has been completly parsed
|
|
// CL_AddSound( sound );
|
|
m_Received.AddToTail( sound );
|
|
}
|
|
|
|
|
|
// Now validate them
|
|
for ( int i = 0 ; i < msg.m_nNumSounds; ++i )
|
|
{
|
|
SoundInfo_t &server = m_Sounds[ i ];
|
|
SoundInfo_t &client = m_Received[ i ];
|
|
|
|
Compare( server, client );
|
|
}
|
|
|
|
m_Sounds.Purge();
|
|
m_Received.Purge();
|
|
}
|
|
|
|
void CTestSoundInfoNetworking::Compare( const SoundInfo_t &s1, const SoundInfo_t &s2 )
|
|
{
|
|
bool bSndStop = s2.nFlags == SND_STOP;
|
|
|
|
if ( !bSndStop && s1.nSequenceNumber != s2.nSequenceNumber )
|
|
{
|
|
Msg( "seq number mismatch %d %d\n", s1.nSequenceNumber, s2.nSequenceNumber );
|
|
}
|
|
|
|
|
|
if ( s1.nEntityIndex != s2.nEntityIndex )
|
|
{
|
|
Msg( "ent mismatch %d %d\n", s1.nEntityIndex, s2.nEntityIndex );
|
|
}
|
|
|
|
if ( s1.nChannel != s2.nChannel )
|
|
{
|
|
Msg( "channel mismatch %d %d\n", s1.nChannel, s2.nChannel );
|
|
}
|
|
|
|
Vector d;
|
|
|
|
d = s1.vOrigin - s2.vOrigin;
|
|
|
|
if ( !bSndStop && d.Length() > 32.0f )
|
|
{
|
|
Msg( "origin mismatch [%f] (%f %f %f) != (%f %f %f)\n", d.Length(), s1.vOrigin.x, s1.vOrigin.y, s1.vOrigin.z, s2.vOrigin.x, s2.vOrigin.y, s2.vOrigin.z );
|
|
}
|
|
|
|
// Vector vDirection;
|
|
float delta = fabs( s1.fVolume - s2.fVolume );
|
|
|
|
if ( !bSndStop && delta > 1.0f )
|
|
{
|
|
Msg( "vol mismatch %f %f\n", s1.fVolume, s2.fVolume );
|
|
}
|
|
|
|
|
|
if ( !bSndStop && s1.Soundlevel != s2.Soundlevel )
|
|
{
|
|
Msg( "sndlvl mismatch %d %d\n", s1.Soundlevel, s2.Soundlevel );
|
|
}
|
|
|
|
// bLooping;
|
|
|
|
if ( s1.bIsSentence != s2.bIsSentence )
|
|
{
|
|
Msg( "sentence mismatch %d %d\n", s1.bIsSentence ? 1 : 0, s2.bIsSentence ? 1 : 0 );
|
|
}
|
|
if ( s1.bIsAmbient != s2.bIsAmbient )
|
|
{
|
|
Msg( "ambient mismatch %d %d\n", s1.bIsAmbient ? 1 : 0, s2.bIsAmbient ? 1 : 0 );
|
|
}
|
|
|
|
if ( !bSndStop && s1.nPitch != s2.nPitch )
|
|
{
|
|
Msg( "pitch mismatch %d %d\n", s1.nPitch, s2.nPitch );
|
|
}
|
|
|
|
// Vector vListenerOrigin;
|
|
|
|
if ( s1.nFlags != s2.nFlags )
|
|
{
|
|
Msg( "flags mismatch %d %d\n", s1.nFlags, s2.nFlags );
|
|
}
|
|
|
|
if ( s1.nSoundNum != s2.nSoundNum )
|
|
{
|
|
Msg( "soundnum mismatch %d %d\n", s1.nSoundNum, s2.nSoundNum );
|
|
}
|
|
|
|
delta = fabs( s1.fDelay - s2.fDelay );
|
|
|
|
if ( !bSndStop && delta > 0.020f )
|
|
{
|
|
Msg( "delay mismatch %f %f\n", s1.fDelay, s2.fDelay );
|
|
}
|
|
|
|
|
|
if ( !bSndStop && s1.nSpeakerEntity != s2.nSpeakerEntity )
|
|
{
|
|
Msg( "speakerentity mismatch %d %d\n", s1.nSpeakerEntity, s2.nSpeakerEntity );
|
|
}
|
|
}
|
|
|
|
#endif
|