/*
========================
idLobby::BuildMigrationInviteList
========================
*/
void idLobby::BuildMigrationInviteList( bool inviteOldHost )
{
	migrationInfo.invites.Clear();
	
	// Build a list of addresses we will send invites to (gather all unique remote addresses from the session user list)
	for( int i = 0; i < GetNumLobbyUsers(); i++ )
	{
		lobbyUser_t* user = GetLobbyUser( i );
		
		if( !verify( user != NULL ) )
		{
			continue;
		}
		
		if( user->IsDisconnected() )
		{
			continue;
		}
		
		if( IsSessionUserIndexLocal( i ) )
		{
			migrationInfo.ourPingMs = user->pingMs;
			migrationInfo.ourUserId = user->lobbyUserID;
			migrationInfo.persistUntilGameEndsData.ourGameData = user->migrationGameData;
			NET_VERBOSE_PRINT( "^2NET: Migration game data for local user is index %d \n", user->migrationGameData );
			
			continue;		// Only interested in remote users
		}
		
		if( !inviteOldHost && user->peerIndex == -1 )
		{
			continue;		// Don't invite old host if told not to do so
		}
		
		if( FindMigrationInviteIndex( user->address ) == -1 )
		{
			migrationInvite_t invite;
			invite.address			= user->address;
			invite.pingMs			= user->pingMs;
			invite.userId			= user->lobbyUserID;
			invite.migrationGameData = user->migrationGameData;
			invite.lastInviteTime	= 0;
			
			NET_VERBOSE_PRINT( "^2NET: Migration game data for user %s is index %d \n", user->gamertag, user->migrationGameData );
			
			migrationInfo.invites.Append( invite );
		}
	}
}
/*
========================
idLobby::HandleMigrationGameData
========================
*/
void idLobby::HandleMigrationGameData( idBitMsg& msg )
{
	// Receives game migration data from the server. Just save off the raw data. If we ever become host we'll let the game code read
	// that chunk in (we can't do anything with it now anyways: we don't have entities or any server code to read it in to)
	migrationInfo.persistUntilGameEndsData.hasGameData = true;
	
	// Reset each user's migration game data. If we don't receive new data for them in this msg, we don't want to use the old data
	for( int i = 0; i < GetNumLobbyUsers(); i++ )
	{
		lobbyUser_t* u = GetLobbyUser( i );
		if( u != NULL )
		{
			u->migrationGameData = -1;
		}
	}
	
	msg.ReadData( migrationInfo.persistUntilGameEndsData.gameData, sizeof( migrationInfo.persistUntilGameEndsData.gameData ) );
	int numUsers = msg.ReadByte();
	int dataIndex = 0;
	for( int i = 0; i < numUsers; i++ )
	{
		lobbyUserID_t lobbyUserID;
		lobbyUserID.ReadFromMsg( msg );
		lobbyUser_t* user = GetLobbyUser( GetLobbyUserIndexByID( lobbyUserID ) );
		if( user != NULL )
		{
		
			NET_VERBOSE_PRINT( "NET:    Got migration data[%d] for user %s\n", dataIndex, user->gamertag );
			
			user->migrationGameData = dataIndex;
			msg.ReadData( migrationInfo.persistUntilGameEndsData.gameDataUser[ dataIndex ], sizeof( migrationInfo.persistUntilGameEndsData.gameDataUser[ dataIndex ] ) );
			dataIndex++;
		}
	}
}
예제 #3
0
/*
========================
idLobby::InitSessionUsersFromLocalUsers
This functions just defaults the session users to the signin manager local users
========================
*/
void idLobby::InitSessionUsersFromLocalUsers( bool onlineMatch ) {
	assert( lobbyBackend != NULL );

	// First, clear all session users of this session type
	FreeAllUsers();

	// Copy all local users from sign in mgr to the session user list
	for ( int i = 0; i < sessionCB->GetSignInManager().GetNumLocalUsers(); i++ ) {		
		idLocalUser * localUser = sessionCB->GetSignInManager().GetLocalUserByIndex( i );
		
		// Make sure this user can join lobbies
		if ( onlineMatch && !localUser->CanPlayOnline() ) {
			continue;
		}

		lobbyUser_t lobbyUser = CreateLobbyUserFromLocalUser( localUser );

		// Append this new session user to the session user list
		lobbyUser_t * createdUser = AllocUser( lobbyUser );

		// Get the migration game data if this is a migrated hosting
		if ( verify( createdUser != NULL ) && migrationInfo.persistUntilGameEndsData.wasMigratedHost ) {
			createdUser->migrationGameData = migrationInfo.persistUntilGameEndsData.ourGameData;
			NET_VERBOSE_PRINT( "NET: Migration game data set for local user %s at index %d \n", createdUser->gamertag, migrationInfo.persistUntilGameEndsData.ourGameData );
		}		
	}
}
예제 #4
0
/*
========================
idLobby::CreateLobbyUserFromLocalUser
This functions just defaults the session users to the signin manager local users
========================
*/
lobbyUser_t idLobby::CreateLobbyUserFromLocalUser( const idLocalUser * localUser ) {
		
	lobbyUser_t lobbyUser;
	idStr::Copynz( lobbyUser.gamertag, localUser->GetGamerTag(), sizeof( lobbyUser.gamertag ) );
	lobbyUser.peerIndex			= -1;
	lobbyUser.lobbyUserID		= lobbyUserID_t( localUser->GetLocalUserHandle(), lobbyType );	// Generate the lobby using a combination of local user id, and lobby type
	lobbyUser.disconnecting		= false;

	// If we are in a game lobby (or dedicated game state lobby), and we have a party lobby running, assume we can grab the party token from our equivalent user in the party.
	if ( ( lobbyType == TYPE_GAME || lobbyType == TYPE_GAME_STATE ) && sessionCB->GetPartyLobby().IsLobbyActive() ) {
		if ( ( sessionCB->GetPartyLobby().parms.matchFlags & MATCH_REQUIRE_PARTY_LOBBY ) && !( sessionCB->GetPartyLobby().parms.matchFlags & MATCH_PARTY_INVITE_PLACEHOLDER ) ) {
			// copy some things from my party user
			const int myPartyUserIndex = sessionCB->GetPartyLobby().GetLobbyUserIndexByLocalUserHandle( lobbyUser.lobbyUserID.GetLocalUserHandle() );

			if ( verify( myPartyUserIndex >= 0 ) ) {		// Just in case
				lobbyUser_t * myPartyUser = sessionCB->GetPartyLobby().GetLobbyUser( myPartyUserIndex );
				if ( myPartyUser != NULL ) {
					lobbyUser.partyToken = myPartyUser->partyToken;
				}
			}
		}
	}

	lobbyUser.UpdateClientMutableData( localUser );

	NET_VERBOSE_PRINT( "NET: CreateLobbyUserFromLocalUser: party %08x name %s (%s)\n", lobbyUser.partyToken, lobbyUser.gamertag, GetLobbyName() );

	return lobbyUser;
}
/*
========================
idLobbyBackendDirect::StartHosting
========================
*/
void idLobbyBackendDirect::StartHosting( const idMatchParameters & p, float skillLevel, lobbyBackendType_t type ) {
	NET_VERBOSE_PRINT( "idLobbyBackendDirect::StartHosting\n" );

	isLocal = MatchTypeIsLocal( p.matchFlags );
	isHost	= true;

	state	= STATE_READY;
	isLocal = true;
}
/*
========================
idLobbyBackendDirect::JoinFromConnectInfo
========================
*/
void idLobbyBackendDirect::JoinFromConnectInfo( const lobbyConnectInfo_t& connectInfo )
{
	if( lobbyToSessionCB->CanJoinLocalHost() )
	{
		// TODO: "CanJoinLocalHost" == *must* join LocalHost ?!
		Sys_StringToNetAdr( "localhost", &address, true );
		address.port = net_port.GetInteger();
		NET_VERBOSE_PRINT( "NET: idLobbyBackendDirect::JoinFromConnectInfo(): canJoinLocalHost\n" );
	}
	else
	{
		address = connectInfo.netAddr;
		NET_VERBOSE_PRINT( "NET: idLobbyBackendDirect::JoinFromConnectInfo(): %s\n", Sys_NetAdrToString( address ) );
	}
	
	state		= STATE_READY;
	isLocal		= false;
	isHost		= false;
}
예제 #7
0
/*
========================
idLobby::AllocLobbyUserSlotForBot
========================
*/
lobbyUserID_t idLobby::AllocLobbyUserSlotForBot( const char * botName ) {
	lobbyUser_t botSessionUser;
	botSessionUser.peerIndex = peerIndexOnHost;
	botSessionUser.isBot = true;
	botSessionUser.disconnecting = false;
	idStr::Copynz( botSessionUser.gamertag, botName, sizeof( botSessionUser.gamertag ) );

	localUserHandle_t localUserHandle( session->GetSignInManager().GetUniqueLocalUserHandle( botSessionUser.gamertag ) );
	botSessionUser.lobbyUserID = lobbyUserID_t( localUserHandle, lobbyType );

	lobbyUser_t * botUser = NULL;

	int sessionUserIndex = -1;

	// First, try to replace a disconnected user
	for ( int i = 0; i < GetNumLobbyUsers(); ++i ) {
		if ( IsLobbyUserDisconnected( i ) ) {
			lobbyUser_t * user = GetLobbyUser( i );
			if ( verify( user != NULL ) ) {
				*user = botSessionUser;
				botUser = user;
				sessionUserIndex = i;
				break;
			}
		}
	}

	if ( botUser == NULL ) {
		if ( freeUsers.Num() == 0 ) {
			idLib::Warning( "NET: Out Of Session Users - Can't Add Bot %s!", botName );
			return lobbyUserID_t();
		}
		botUser = AllocUser( botSessionUser );
		sessionUserIndex = userList.Num() - 1;
	}

	if ( !verify( botUser != NULL ) ) {
		idLib::Warning( "NET: Can't Find Session Slot For Bot!" );
		return lobbyUserID_t();
	} else {
		NET_VERBOSE_PRINT( "NET: Created Bot %s At Index %d \n", botUser->gamertag, sessionUserIndex );
	}

	SendNewUsersToPeers( peerIndexOnHost, userList.Num() - 1, 1 ); // bot has been added to the lobby user list - update the peers so that they can see the bot too.

	return GetLobbyUser( sessionUserIndex )->lobbyUserID;
}
/*
========================
idSessionLocalWin::Shutdown
========================
*/
void idSessionLocalWin::Shutdown()
{
	NET_VERBOSE_PRINT( "NET: Shutdown\n" );
	idSessionLocal::Shutdown();
	
	MoveToMainMenu();
	
	// Wait until we fully shutdown
	while( localState != STATE_IDLE && localState != STATE_PRESS_START )
	{
		Pump();
	}
	
	if( achievementSystem != NULL )
	{
		achievementSystem->Shutdown();
		delete achievementSystem;
		achievementSystem = NULL;
	}
}
예제 #9
0
/*
========================
idLobby::GetAverageSessionLevel
========================
*/
float idLobby::GetAverageSessionLevel() {
	float	level				= 0.0f;
	int		numActiveMembers	= 0;

	for ( int i = 0; i < GetNumLobbyUsers(); i++ ) {
		const lobbyUser_t * user = GetLobbyUser( i );

		if ( user->IsDisconnected() ) {
			continue;
		}

		level += user->level;
		numActiveMembers++;
	}

	if ( numActiveMembers > 0 ) {
		level /= (float)numActiveMembers;
	}

	float ret = Max( level, 1.0f );
	NET_VERBOSE_PRINT( "NET: GetAverageSessionLevel %g\n", ret );
	return ret;
}
/*
========================
idLobby::UpdateHostMigration
========================
*/
void idLobby::UpdateHostMigration()
{

	int time = Sys_Milliseconds();
	
	// If we are picking a new host, then update that
	if( migrationInfo.state == MIGRATE_PICKING_HOST )
	{
		const int MIGRATION_PICKING_HOST_TIMEOUT_IN_SECONDS = 20;		// FIXME: set back to 5 // Give other hosts 5 seconds
		
		if( time - migrationInfo.migrationStartTime > session->GetTitleStorageInt( "MIGRATION_PICKING_HOST_TIMEOUT_IN_SECONDS", MIGRATION_PICKING_HOST_TIMEOUT_IN_SECONDS ) * 1000 )
		{
			// Just become the host if we haven't heard from a host in awhile
			BecomeHost();
		}
		else
		{
			return;
		}
	}
	
	// See if we are a new migrated host that needs to invite the original members back
	if( migrationInfo.state != MIGRATE_BECOMING_HOST )
	{
		return;
	}
	
	if( lobbyBackend == NULL || lobbyBackend->GetState() != idLobbyBackend::STATE_READY )
	{
		return;
	}
	
	if( state != STATE_IDLE )
	{
		return;
	}
	
	if( !IsHost() )
	{
		return;
	}
	
	const int MIGRATION_TIMEOUT_IN_SECONDS		= 30; // FIXME: setting to 30 for dev purposes. 10 seems more reasonable. Need to make unloading game / loading lobby async
	const int MIGRATION_INVITE_TIME_IN_SECONDS	= 2;
	
	if( migrationInfo.invites.Num() == 0 || time - migrationInfo.migrationStartTime > session->GetTitleStorageInt( "MIGRATION_TIMEOUT_IN_SECONDS", MIGRATION_TIMEOUT_IN_SECONDS ) * 1000 )
	{
		// Either everyone acked, or we timed out, just keep who we have, and stop sending invites
		EndMigration();
		return;
	}
	
	// Send invites to anyone who hasn't responded
	for( int i = 0; i < migrationInfo.invites.Num(); i++ )
	{
		if( time - migrationInfo.invites[i].lastInviteTime < session->GetTitleStorageInt( "MIGRATION_INVITE_TIME_IN_SECONDS", MIGRATION_INVITE_TIME_IN_SECONDS ) * 1000 )
		{
			continue;		// Not enough time passed
		}
		
		// Mark the time
		migrationInfo.invites[i].lastInviteTime = time;
		
		byte buffer[ idPacketProcessor::MAX_PACKET_SIZE - 2 ];
		idBitMsg outmsg( buffer, sizeof( buffer ) );
		
		// Have lobbyBackend fill out msg with connection info
		lobbyConnectInfo_t connectInfo = lobbyBackend->GetConnectInfo();
		connectInfo.WriteToMsg( outmsg );
		
		// Let them know whether or not this was from in game
		outmsg.WriteBool( migrationInfo.persistUntilGameEndsData.wasMigratedGame );
		
		NET_VERBOSE_PRINT( "NET: Sending migration invite to %s\n", migrationInfo.invites[i].address.ToString() );
		
		// Send the migration invite
		SendConnectionLess( migrationInfo.invites[i].address, OOB_MIGRATE_INVITE, outmsg.GetReadData(), outmsg.GetSize() );
	}
}
/*
========================
idLobby::SendMigrationGameData
========================
*/
void idLobby::SendMigrationGameData()
{
	if( net_migration_disable.GetBool() )
	{
		return;
	}
	
	if( sessionCB->GetState() != idSession::INGAME )
	{
		return;
	}
	
	if( !migrationInfo.persistUntilGameEndsData.hasGameData )
	{
		// Haven't been given any migration game data yet
		return;
	}
	
	const int now = Sys_Milliseconds();
	if( nextSendMigrationGameTime > now )
	{
		return;
	}
	
	byte	packetData[ idPacketProcessor::MAX_MSG_SIZE ];
	idBitMsg msg( packetData, sizeof( packetData ) );
	
	// Write global data
	msg.WriteData( &migrationInfo.persistUntilGameEndsData.gameData, sizeof( migrationInfo.persistUntilGameEndsData.gameData ) );
	msg.WriteByte( GetNumLobbyUsers() );
	
	// Write user data
	for( int userIndex = 0; userIndex < GetNumLobbyUsers(); ++userIndex )
	{
		lobbyUser_t* u = GetLobbyUser( userIndex );
		if( u->IsDisconnected() || u->migrationGameData < 0 )
		{
			continue;
		}
		
		u->lobbyUserID.WriteToMsg( msg );
		msg.WriteData( migrationInfo.persistUntilGameEndsData.gameDataUser[ u->migrationGameData ], sizeof( migrationInfo.persistUntilGameEndsData.gameDataUser[ u->migrationGameData ] ) );
	}
	
	// Send to 1 peer
	for( int i = 0; i < peers.Num(); i++ )
	{
		int peerToSend = ( nextSendMigrationGamePeer + i ) % peers.Num();
		
		if( peers[ peerToSend ].IsConnected() && peers[ peerToSend ].loaded )
		{
			if( peers[ peerToSend ].packetProc->NumQueuedReliables() > idPacketProcessor::MAX_RELIABLE_QUEUE / 2 )
			{
				// This is kind of a hack for development so we don't DC clients by sending them too many reliable migration messages
				// when they aren't responding. Doesn't seem like a horrible thing to have in a shipping product but is not necessary.
				NET_VERBOSE_PRINT( "NET: Skipping reliable game migration data msg because client reliable queue is > half full\n" );
				
			}
			else
			{
				if( net_migration_debug.GetBool() )
				{
					idLib::Printf( "NET: Sending migration game data to peer %d. size: %d\n", peerToSend, msg.GetSize() );
				}
				QueueReliableMessage( peerToSend, RELIABLE_MIGRATION_GAME_DATA, msg.GetReadData(), msg.GetSize() );
			}
			break;
		}
	}
	
	// Increment next send time / next send peer
	nextSendMigrationGamePeer++;
	if( nextSendMigrationGamePeer >= peers.Num() )
	{
		nextSendMigrationGamePeer = 0;
	}
	
	nextSendMigrationGameTime = now + MIGRATION_GAME_DATA_INTERVAL_MS;
}
/*
========================
idLobby::PickNewHostInternal
========================
*/
void idLobby::PickNewHostInternal( bool forceMe, bool inviteOldHost )
{

	if( migrationInfo.state == MIGRATE_PICKING_HOST )
	{
		return;		// Already picking new host
	}
	
	idLib::Printf( "PickNewHost: Started picking new host %s.\n", GetLobbyName() );
	
	if( IsHost() )
	{
		idLib::Printf( "PickNewHost: Already host of session %s\n", GetLobbyName() );
		return;
	}
	
	// Find the user with the lowest ping
	int bestUserIndex			= -1;
	int bestPingMs				= 0;
	lobbyUserID_t bestUserId;
	
	for( int i = 0; i < GetNumLobbyUsers(); i++ )
	{
		lobbyUser_t* user = GetLobbyUser( i );
		
		if( !verify( user != NULL ) )
		{
			continue;
		}
		
		if( user->IsDisconnected() )
		{
			continue;
		}
		
		if( user->peerIndex == -1 )
		{
			continue;		// Don't try and pick old host
		}
		
		if( bestUserIndex == -1 || IsBetterHost( user->pingMs, user->lobbyUserID, bestPingMs, bestUserId ) )
		{
			bestUserIndex	= i;
			bestPingMs		= user->pingMs;
			bestUserId		= user->lobbyUserID;
		}
		
		if( user->peerIndex == net_migration_forcePeerAsHost.GetInteger() )
		{
			bestUserIndex	= i;
			bestPingMs		= user->pingMs;
			bestUserId		= user->lobbyUserID;
			break;
		}
	}
	
	// Remember when we first started picking a new host
	migrationInfo.state						= MIGRATE_PICKING_HOST;
	migrationInfo.migrationStartTime		= Sys_Milliseconds();
	
	migrationInfo.persistUntilGameEndsData.wasMigratedGame = sessionCB->GetState() == idSession::INGAME;
	
	if( bestUserIndex == -1 )  	// This can happen if we call PickNewHost on an lobby that was Shutdown
	{
		NET_VERBOSE_PRINT( "MIGRATION: PickNewHost was called on an lobby that was Shutdown\n" );
		BecomeHost();
		return;
	}
	
	NET_VERBOSE_PRINT( "MIGRATION: Chose user index %d (%s) for new host\n", bestUserIndex, GetLobbyUser( bestUserIndex )->gamertag );
	
	bool bestWasLocal = IsSessionUserIndexLocal( bestUserIndex );		// Check before shutting down the lobby
	migrateMsgFlags = parms.matchFlags;						// Save off match parms
	
	// Build invite list
	BuildMigrationInviteList( inviteOldHost );
	
	// If the best user is on this machine, then we become the host now, otherwise, wait for a new host to contact us
	if( forceMe || bestWasLocal )
	{
		BecomeHost();
	}
}
예제 #13
0
/*
========================
idLobby::CheckPeerThrottle
========================
*/
void idLobby::CheckPeerThrottle( int p )
{
	assert( lobbyType == GetActingGameStateLobbyType() );
	
	if( !verify( p >= 0 && p < peers.Num() ) )
	{
		return;
	}
	
	peer_t& peer = peers[p];
	
	if( !peer.IsConnected() )
	{
		return;
	}
	
	if( !IsHost() )
	{
		return;
	}
	
	if( session->GetTitleStorageInt( "net_peer_throttle_mode", net_peer_throttle_mode.GetInteger() ) == 0 )
	{
		return;
	}
	
	if( peer.receivedBps < 0.0f )
	{
		return;
	}
	
	int time = Sys_Milliseconds();
	
	if( !AllPeersHaveBaseState() )
	{
		return;
	}
	
	if( verify( peer.snapProc != NULL ) )
	{
		const int peer_throttle_minSnapSeq = session->GetTitleStorageInt( "net_peer_throttle_minSnapSeq", net_peer_throttle_minSnapSeq.GetInteger() );
		if( peer.snapProc->GetFullSnapBaseSequence() <= idSnapshotProcessor::INITIAL_SNAP_SEQUENCE + peer_throttle_minSnapSeq )
		{
			return;
		}
	}
	
	// This is bps throttling which compares the sent bytes per second to the reported received bps
	float peer_throttle_bps_host_threshold = session->GetTitleStorageFloat( "net_peer_throttle_bps_host_threshold", net_peer_throttle_bps_host_threshold.GetFloat() );
	
	if( peer_throttle_bps_host_threshold > 0.0f )
	{
		int deltaT = idMath::ClampInt( 0, 100, time - peer.receivedThrottleTime );
		if( deltaT > 0 && peer.receivedThrottleTime > 0 && peer.receivedBpsIndex > 0 )
		{
		
			bool throttled = false;
			float sentBps = peer.sentBpsHistory[ peer.receivedBpsIndex % MAX_BPS_HISTORY ];
			
			// Min outgoing rate from server (don't throttle if we are sending < 1k)
			if( sentBps > peer_throttle_bps_host_threshold )
			{
				float pct = peer.receivedBps / idMath::ClampFloat( 0.01f, static_cast<float>( BANDWIDTH_REPORTING_MAX ), sentBps ); // note the receivedBps is implicitly clamped on client end to 10k/sec
				
				/*
				static int lastSeq = 0;
				if ( peer.receivedBpsIndex != lastSeq ) {
					NET_VERBOSE_PRINT( "%ssentBpsHistory[%d] = %.2f   received: %.2f PCT: %.2f \n", ( pct > 1.0f ? "^1" : "" ), peer.receivedBpsIndex, sentBps, peer.receivedBps, pct );
				}
				lastSeq = peer.receivedBpsIndex;
				*/
				
				// Increase throttle time if peer is < % of what we are sending him
				if( pct < session->GetTitleStorageFloat( "net_peer_throttle_bps_peer_threshold_pct", net_peer_throttle_bps_peer_threshold_pct.GetFloat() ) )
				{
					peer.receivedThrottle += ( float )deltaT;
					throttled = true;
					NET_VERBOSE_PRINT( "NET: throttled... %.2f ....pct %.2f  receivedBps %.2f outgoingBps %.2f, peer %i, seq %i\n", peer.receivedThrottle, pct, peer.receivedBps, sentBps, p, peer.snapProc->GetFullSnapBaseSequence() );
				}
			}
			
			if( !throttled )
			{
				float decayRate = session->GetTitleStorageFloat( "net_peer_throttle_bps_decay", net_peer_throttle_bps_decay.GetFloat() );
				
				peer.receivedThrottle = Max<float>( 0.0f, peer.receivedThrottle - ( ( ( float )deltaT ) * decayRate ) );
				//NET_VERBOSE_PRINT("NET: !throttled... %.2f ....receivedBps %.2f outgoingBps %.2f\n", peer.receivedThrottle, peer.receivedBps, sentBps );
			}
			
			float duration = session->GetTitleStorageFloat( "net_peer_throttle_bps_duration", net_peer_throttle_bps_duration.GetFloat() );
			
			if( peer.receivedThrottle > duration )
			{
				peer.maxSnapBps = peer.receivedBps * session->GetTitleStorageFloat( "net_snap_bw_test_throttle_max_scale", net_snap_bw_test_throttle_max_scale.GetFloat() );
				
				int maxRate = common->GetSnapRate() * session->GetTitleStorageInt( "net_peer_throttle_maxSnapRate", net_peer_throttle_maxSnapRate.GetInteger() );
				
				if( peer.throttledSnapRate == 0 )
				{
					peer.throttledSnapRate = common->GetSnapRate() * 2;
				}
				else if( peer.throttledSnapRate < maxRate )
				{
					peer.throttledSnapRate = idMath::ClampInt( common->GetSnapRate(), maxRate, peer.throttledSnapRate + common->GetSnapRate() );
				}
				
				peer.receivedThrottle = 0.0f;	// Start over, so we don't immediately throttle again
			}
		}
		peer.receivedThrottleTime = time;
	}
}