/*
========================
idLobby::ApplySnapshotDeltaInternal
========================
*/
bool idLobby::ApplySnapshotDeltaInternal( int p, int snapshotNumber )
{
	assert( lobbyType == GetActingGameStateLobbyType() );
	
	if( !verify( p >= 0 && p < peers.Num() ) )
	{
		return false;
	}
	
	peer_t& peer = peers[p];
	
	if( !peer.IsConnected() )
	{
		return false;
	}
	
	// on the server, player = peer number + 1, this only works as long as we don't support clients joining and leaving during game
	// on the client, always 0
	bool result = peer.snapProc->ApplySnapshotDelta( IsHost() ? p + 1 : 0, snapshotNumber );
	
	if( result && IsHost() && peer.snapProc->HasPendingSnap() )
	{
		// Send more of the pending snap if we have one for this peer.
		// The reason we can do this, is because we know more about this peers base state now.
		// And since we maxed out the optimal snap delta size, we'll now be able
		// to send more data, since we assume we'll get better and better delta compression as
		// our version of this peers base state approaches parity with the peers actual state.
		
		// We don't send immediately, since we have to coordinate sending snaps for all peers in same place considering jobs.
		peer.needToSubmitPendingSnap = true;
		NET_VERBOSESNAPSHOT_PRINT( "NET: Sent more unsent snapshot data to peer %d for snapshot %d\n", p, snapshotNumber );
	}
	
	return result;
}
/*
========================
idSnapshotProcessor::ApplySnapshotDelta
Apply a snapshot delta to our current basestate, and make that the new base.
We can remove all deltas that refer to the basetate we just removed.
========================
*/
bool idSnapshotProcessor::ApplySnapshotDelta( int visIndex, int snapshotNumber )
{

	NET_VERBOSESNAPSHOT_PRINT_LEVEL( 6, va( "idSnapshotProcessor::ApplySnapshotDelta snapshotNumber: %d\n", snapshotNumber ) );
	
	// Sanity check deltas
	SanityCheckDeltas();
	
	// dump any deltas older than the acknoweledged snapshot, which should only happen if there is packet loss
	deltas.RemoveOlderThan( snapshotNumber );
	
	if( deltas.Num() == 0 || deltas.ItemSequence( 0 ) != snapshotNumber )
	{
		// this means the snapshot was either already acknowledged or came out of order
		// On the server, this can happen because the client is continuously/redundantly sending acks
		// Once the server has ack'd a certain base sequence, it will need to ignore all the redundant ones.
		// On the client, this will only happen due to out of order, or dropped packets.
		
		if( !common->IsServer() )
		{
			// these should be printed every time on the clients
			// printing on server is not useful / results in tons of spam
			if( deltas.Num() == 0 )
			{
				NET_VERBOSESNAPSHOT_PRINT( "NET: Got snapshot but ignored... deltas.Num(): %d snapshotNumber: %d \n", deltas.Num(), snapshotNumber );
			}
			else
			{
				NET_VERBOSESNAPSHOT_PRINT( "NET: Got snapshot but ignored... deltas.ItemSequence( 0 ): %d != snapshotNumber: %d \n   ", deltas.ItemSequence( 0 ), snapshotNumber );
				
				for( int i = 0; i < deltas.Num(); i++ )
				{
					NET_VERBOSESNAPSHOT_PRINT( "%d ", deltas.ItemSequence( i ) );
				}
				NET_VERBOSESNAPSHOT_PRINT( "\n" );
				
			}
		}
		return false;
	}
	
	int deltaSequence		= 0;
	int deltaBaseSequence	= 0;
	
	PeekDeltaSequence( ( const char* )deltas.ItemData( 0 ), deltas.ItemLength( 0 ), deltaSequence, deltaBaseSequence );
	
	assert( deltaSequence == snapshotNumber );		// Make sure compressed sequence number matches that in data queue
	assert( baseSequence == deltaBaseSequence );	// If this delta isn't based off of our currently ack'd basestate, something is trashed...
	assert( deltaSequence > baseSequence );
	
	if( baseSequence != deltaBaseSequence )
	{
		// NOTE - This should no longer happen with recent fixes.
		// We should probably disconnect from the server if this happens. (packets are trashed most likely)
		NET_VERBOSESNAPSHOT_PRINT( "NET: Got snapshot %d but baseSequence does not match. baseSequence: %d deltaBaseSequence: %d. \n", snapshotNumber, baseSequence, deltaBaseSequence );
		return false;
	}
	
	// Apply this delta to our base state
	if( ApplyDeltaToSnapshot( baseState, ( const char* )deltas.ItemData( 0 ), deltas.ItemLength( 0 ), visIndex ) )
	{
		lastFullSnapBaseSequence = deltaSequence;
	}
	
	baseSequence = deltaSequence;		// This is now our new base sequence
	
	// Remove deltas that we no longer need
	RemoveDeltasForOldBaseSequence();
	
	// Sanity check deltas
	SanityCheckDeltas();
	
	return true;
}
/*
========================
idSnapshotProcessor::ReceiveSnapshotDelta
NOTE: we use ReadDeltaForJob twice, once to build the same base as the server (based on server acks, down ApplySnapshotDelta), and another time to apply the snapshot we just received
could we avoid the double apply by keeping outSnap cached in memory and avoid rebuilding it from a delta when the next one comes around?
========================
*/
bool idSnapshotProcessor::ReceiveSnapshotDelta( const byte* deltaData, int deltaLength, int visIndex, int& outSeq, int& outBaseSeq, idSnapShot& outSnap, bool& fullSnap )
{

	fullSnap = false;
	
	int deltaSequence		= 0;
	int deltaBaseSequence	= 0;
	
	// Get the sequence of this delta, and the base sequence it is delta'd from
	PeekDeltaSequence( ( const char* )deltaData, deltaLength, deltaSequence, deltaBaseSequence );
	
	//idLib::Printf("Incoming snapshot: %i, %i\n", deltaSequence, deltaBaseSequence );
	
	if( deltaSequence <= snapSequence )
	{
		NET_VERBOSESNAPSHOT_PRINT( "Rejecting old delta: %d (snapSequence: %d \n", deltaSequence, snapSequence );
		return false;		// Completely reject older out of order deltas
	}
	
	// Bring the base state up to date with the basestate this delta was compared to
	ApplySnapshotDelta( visIndex, deltaBaseSequence );
	
	// Once we get here, our base state should be caught up to that of the server
	assert( baseSequence == deltaBaseSequence );
	
	// Save the new delta
	if( net_skipClientDeltaAppend.GetBool() || !deltas.Append( deltaSequence, deltaData, deltaLength ) )
	{
		// This can happen if the delta queues get desync'd between the server and client.
		// With recent fixes, this should be extremely rare, or impossible.
		// Just in case this happens, we can recover by assuming we didn't even receive this delta.
		idLib::Printf( "NET: ReceiveSnapshotDelta: No room to append delta %d/%d \n", deltaSequence, deltaBaseSequence );
		return false;
	}
	
	// Update our snapshot sequence number to the newer one we just got (now that it's safe)
	snapSequence = deltaSequence;
	
	if( deltas.Num() > 10 )
	{
		NET_VERBOSESNAPSHOT_PRINT( "NET: ReceiveSnapshotDelta: deltas.Num() > 10: %d\n   ", deltas.Num() );
		for( int i = 0; i < deltas.Num(); i++ )
		{
			NET_VERBOSESNAPSHOT_PRINT( "%d ", deltas.ItemSequence( i ) );
		}
		NET_VERBOSESNAPSHOT_PRINT( "\n" );
	}
	
	
	if( baseSequence != deltaBaseSequence )
	{
		// NOTE - With recent fixes, this should no longer be possible unless the delta is trashed
		// We should probably disconnect from the server when this happens now.
		static bool failed = false;
		if( !failed )
		{
			idLib::Printf( "NET: incorrect base state? not sure how this can happen... baseSequence: %d  deltaBaseSequence: %d \n", baseSequence, deltaBaseSequence );
		}
		failed = true;
		return false;
	}
	
	// Copy out the current deltas sequence values to caller
	outSeq		= deltaSequence;
	outBaseSeq	= deltaBaseSequence;
	
	if( baseSequence < 50 && net_debugBaseStates.GetBool() )
	{
		idLib::Printf( "NET: Proper basestate...  baseSequence: %d  deltaBaseSequence: %d \n", baseSequence, deltaBaseSequence );
	}
	
	// Make a copy of the basestate the server used to create this delta, and then apply and return it
	outSnap = baseState;
	
	fullSnap = ApplyDeltaToSnapshot( outSnap, ( const char* )deltaData, deltaLength, visIndex );
	
	// We received a new delta
	return true;
}
/*
========================
idSnapShot::WriteObject
========================
*/
void idSnapShot::WriteObject( idFile* file, int visIndex, objectState_t* newState, objectState_t* oldState, int& lastobjectNum )
{
	assert( newState != NULL || oldState != NULL );
	
	bool visChange		= false; // visibility changes will be signified with a 0xffff state size
	bool visSendState	= false; // the state is sent when an entity is no longer stale
	
	// Compute visibility changes
	// (we need to do this before writing out object id, because we may not need to write out the id if we early out)
	// (when we don't write out the id, we assume this is an "ack" when we deserialize the objects)
	if( newState != NULL && oldState != NULL )
	{
		// Check visibility
		assert( newState->objectNum == oldState->objectNum );
		
		if( visIndex > 0 )
		{
			bool oldVisible = ( oldState->visMask & ( 1 << visIndex ) ) != 0;
			bool newVisible = ( newState->visMask & ( 1 << visIndex ) ) != 0;
			
			// Force visible if we need to either create or destroy this object
			newVisible |= ( newState->buffer.Size() == 0 ) != ( oldState->buffer.Size() == 0 );
			
			if( !oldVisible && !newVisible )
			{
				// object is stale and ack'ed for this client, write nothing (see 'same object' below)
				return;
			}
			else if( oldVisible && !newVisible )
			{
				NET_VERBOSESNAPSHOT_PRINT( "object %d to client %d goes stale\n", newState->objectNum, visIndex );
				visChange = true;
				visSendState = false;
			}
			else if( !oldVisible && newVisible )
			{
				NET_VERBOSESNAPSHOT_PRINT( "object %d to client %d no longer stale\n", newState->objectNum, visIndex );
				visChange = true;
				visSendState = true;
			}
		}
		
		// Same object, write a delta (never early out during vis changes)
		if( !visChange && newState->buffer.Size() == oldState->buffer.Size() &&
				( ( newState->buffer.Ptr() == oldState->buffer.Ptr() ) || memcmp( newState->buffer.Ptr(), oldState->buffer.Ptr(), newState->buffer.Size() ) == 0 ) )
		{
			// same state, write nothing
			return;
		}
	}
	
	// Get the id of the object we are writing out
	uint16 objectNum;
	if( newState != NULL )
	{
		objectNum = newState->objectNum;
	}
	else if( oldState != NULL )
	{
		objectNum = oldState->objectNum;
	}
	else
	{
		objectNum = 0;
	}
	
	assert( objectNum == 0 || objectNum > lastobjectNum );
	
	// Write out object id (using delta)
	uint16 objectDelta = objectNum - lastobjectNum;
	file->WriteBig( objectDelta );
	lastobjectNum = objectNum;
	
	if( newState == NULL )
	{
		// Deleted, write 0 size
		assert( oldState != NULL );
		file->WriteBig<objectSize_t>( 0 );
	}
	else if( oldState == NULL )
	{
		// New object, write out full state
		assert( newState != NULL );
		// delta against an empty snap
		file->WriteBig( newState->buffer.Size() );
		file->Write( newState->buffer.Ptr(), newState->buffer.Size() );
	}
	else
	{
		// Compare to last object
		assert( newState != NULL && oldState != NULL );
		assert( newState->objectNum == oldState->objectNum );
		
		if( visChange )
		{
			// fake size indicates vis state change
			// NOTE: we may still send a real size and a state below, for 'no longer stale' transitions
			// TMP: send 0xFFFF for going stale and 0xFFFF - 1 for no longer stale
			file->WriteBig<objectSize_t>( visSendState ? SIZE_NOT_STALE : SIZE_STALE );
		}
		if( !visChange || visSendState )
		{
		
			objectSize_t compareSize = Min( newState->buffer.Size(), oldState->buffer.Size() );		// Get the number of bytes that overlap
			
			file->WriteBig( newState->buffer.Size() );										// Write new size
			
			// Compare bytes that overlap
			for( objectSize_t b = 0; b < compareSize; b++ )
			{
				file->WriteBig<byte>( ( 0xFF + 1 + newState->buffer[b] - oldState->buffer[b] ) & 0xFF );
			}
			
			// Write leftover
			if( newState->buffer.Size() > compareSize )
			{
				file->Write( newState->buffer.Ptr() + oldState->buffer.Size(), newState->buffer.Size() - compareSize );
			}
		}
	}
	
#ifdef SNAPSHOT_CHECKSUMS
	if( ( !visChange || visSendState ) && newState != NULL )
	{
		assert( newState->buffer.Size() > 0 );
		unsigned int checksum = MD5_BlockChecksum( newState->buffer.Ptr(), newState->buffer.Size() );
		file->WriteBig( checksum );
	}
#endif
}
/*
========================
idSnapShot::ReadDelta
========================
*/
bool idSnapShot::ReadDelta( idFile* file, int visIndex )
{

	file->ReadBig( time );
	
	int objectNum = 0;
	uint16 delta = 0;
	while( file->ReadBig( delta ) == sizeof( delta ) )
	{
		objectNum += delta;
		if( objectNum >= 0xFFFF )
		{
			// full delta
			return true;
		}
		objectState_t& state = FindOrCreateObjectByID( objectNum );
		objectSize_t newsize = 0;
		file->ReadBig( newsize );
		
		if( newsize == SIZE_STALE )
		{
			NET_VERBOSESNAPSHOT_PRINT( "read delta: object %d goes stale\n", objectNum );
			// sanity
			bool oldVisible = ( state.visMask & ( 1 << visIndex ) ) != 0;
			if( !oldVisible )
			{
				NET_VERBOSESNAPSHOT_PRINT( "ERROR: unexpected already stale\n" );
			}
			state.visMask &= ~( 1 << visIndex );
			state.stale = true;
			// We need to make sure we haven't freed stale objects.
			assert( state.buffer.Size() > 0 );
			// no more data
			continue;
		}
		else if( newsize == SIZE_NOT_STALE )
		{
			NET_VERBOSESNAPSHOT_PRINT( "read delta: object %d no longer stale\n", objectNum );
			// sanity
			bool oldVisible = ( state.visMask & ( 1 << visIndex ) ) != 0;
			if( oldVisible )
			{
				NET_VERBOSESNAPSHOT_PRINT( "ERROR: unexpected not stale\n" );
			}
			state.visMask |= ( 1 << visIndex );
			state.stale = false;
			// the latest state is packed in, get the new size and continue reading the new state
			file->ReadBig( newsize );
		}
		
		if( newsize == 0 )
		{
			// object deleted
			state.buffer._Release();
		}
		else
		{
			objectBuffer_t newbuffer( newsize );
			objectSize_t compareSize = Min( newsize, state.buffer.Size() );
			
			for( objectSize_t i = 0; i < compareSize; i++ )
			{
				uint8 delta = 0;
				file->ReadBig<byte>( delta );
				newbuffer[i] = state.buffer[i] + delta;
			}
			
			if( newsize > compareSize )
			{
				file->Read( newbuffer.Ptr() + compareSize, newsize - compareSize );
			}
			
			state.buffer = newbuffer;
			state.changedCount++;
		}
		
#ifdef SNAPSHOT_CHECKSUMS
		if( state.buffer.Size() > 0 )
		{
			unsigned int checksum = 0;
			file->ReadBig( checksum );
			assert( checksum == MD5_BlockChecksum( state.buffer.Ptr(), state.buffer.Size() ) );
		}
#endif
	}
	
	// partial delta
	return false;
}
/*
========================
idSnapShot::ReadDeltaForJob
========================
*/
bool idSnapShot::ReadDeltaForJob( const char* deltaMem, int deltaSize, int visIndex, idSnapShot* templateStates )
{

	bool report = net_verboseSnapshotReport.GetBool();
	net_verboseSnapshotReport.SetBool( false );
	
	lzwCompressionData_t		lzwData;
	idZeroRunLengthCompressor	rleCompressor;
	idLZWCompressor				lzwCompressor( &lzwData );
	int bytesRead = 0; // how many uncompressed bytes we read in. Used to figure out compression ratio
	
	lzwCompressor.Start( ( uint8* )deltaMem, deltaSize );
	
	// Skip past sequence and baseSequence
	int sequence		= 0;
	int baseSequence	= 0;
	
	lzwCompressor.ReadAgnostic( sequence );
	lzwCompressor.ReadAgnostic( baseSequence );
	lzwCompressor.ReadAgnostic( time );
	bytesRead += sizeof( int ) * 3;
	
	int objectNum = 0;
	uint16 delta = 0;
	
	
	while( lzwCompressor.ReadAgnostic( delta, true ) == sizeof( delta ) )
	{
		bytesRead += sizeof( delta );
		
		objectNum += delta;
		if( objectNum >= 0xFFFF )
		{
			// full delta
			if( net_verboseSnapshotCompression.GetBool() )
			{
				float compRatio = static_cast<float>( deltaSize ) / static_cast<float>( bytesRead );
				idLib::Printf( "Snapshot (%d/%d). ReadSize: %d DeltaSize: %d Ratio: %.3f\n", sequence, baseSequence, bytesRead, deltaSize, compRatio );
			}
			return true;
		}
		
		objectState_t& state = FindOrCreateObjectByID( objectNum );
		
		objectSize_t newsize = 0;
		lzwCompressor.ReadAgnostic( newsize );
		bytesRead += sizeof( newsize );
		
		if( newsize == SIZE_STALE )
		{
			NET_VERBOSESNAPSHOT_PRINT( "read delta: object %d goes stale\n", objectNum );
			// sanity
			bool oldVisible = ( state.visMask & ( 1 << visIndex ) ) != 0;
			if( !oldVisible )
			{
				NET_VERBOSESNAPSHOT_PRINT( "ERROR: unexpected already stale\n" );
			}
			state.visMask &= ~( 1 << visIndex );
			state.stale = true;
			// We need to make sure we haven't freed stale objects.
			assert( state.buffer.Size() > 0 );
			// no more data
			continue;
		}
		else if( newsize == SIZE_NOT_STALE )
		{
			NET_VERBOSESNAPSHOT_PRINT( "read delta: object %d no longer stale\n", objectNum );
			// sanity
			bool oldVisible = ( state.visMask & ( 1 << visIndex ) ) != 0;
			if( oldVisible )
			{
				NET_VERBOSESNAPSHOT_PRINT( "ERROR: unexpected not stale\n" );
			}
			state.visMask |= ( 1 << visIndex );
			state.stale = false;
			// the latest state is packed in, get the new size and continue reading the new state
			lzwCompressor.ReadAgnostic( newsize );
			bytesRead += sizeof( newsize );
		}
		
		objectState_t* 	objTemplateState = templateStates->FindObjectByID( objectNum );
		
		if( newsize == 0 )
		{
			// object deleted: reset state now so next one to use it doesn't have old data
			state.deleted = false;
			state.stale = false;
			state.changedCount = 0;
			state.expectedSequence = 0;
			state.visMask = 0;
			state.buffer._Release();
			state.createdFromTemplate = false;
			
			if( objTemplateState != NULL && objTemplateState->buffer.Size() && objTemplateState->expectedSequence < baseSequence )
			{
				idLib::PrintfIf( net_ssTemplateDebug.GetBool(), "Clearing old template state[%d] [%d<%d]\n", objectNum, objTemplateState->expectedSequence, baseSequence );
				objTemplateState->deleted = false;
				objTemplateState->stale = false;
				objTemplateState->changedCount = 0;
				objTemplateState->expectedSequence = 0;
				objTemplateState->visMask = 0;
				objTemplateState->buffer._Release();
			}
			
		}
		else
		{
		
			// new state?
			bool debug = false;
			if( state.buffer.Size() == 0 )
			{
				state.createdFromTemplate = true;
				// Brand new state
				if( objTemplateState != NULL && objTemplateState->buffer.Size() > 0 && sequence >= objTemplateState->expectedSequence )
				{
					idLib::PrintfIf( net_ssTemplateDebug.GetBool(), "\nAdding basestate for new object %d (for SS %d/%d. obj base created in ss %d) deltaSize: %d\n", objectNum, sequence, baseSequence, objTemplateState->expectedSequence, deltaSize );
					state.buffer = objTemplateState->buffer;
					
					if( net_ssTemplateDebug.GetBool() )
					{
						state.Print( "SPAWN STATE" );
						debug = true;
						PrintAlign( "DELTA STATE" );
					}
				}
				else if( net_ssTemplateDebug.GetBool() )
				{
					idLib::Printf( "\nNew snapobject[%d] in snapshot %d/%d but no basestate found locally so creating new\n", objectNum, sequence, baseSequence );
				}
			}
			else
			{
				state.createdFromTemplate = false;
			}
			
			// the buffer shrank or stayed the same
			objectBuffer_t newbuffer( newsize );
			rleCompressor.Start( NULL, &lzwCompressor, newsize );
			objectSize_t compareSize = Min( state.buffer.Size(), newsize );
			for( objectSize_t i = 0; i < compareSize; i++ )
			{
				byte b = rleCompressor.ReadByte();
				newbuffer[i] = state.buffer[i] + b;
				
				if( debug && InDebugRange( i ) )
				{
					idLib::Printf( "%02X", b );
				}
			}
			// Catch leftover
			if( newsize > compareSize )
			{
				rleCompressor.ReadBytes( newbuffer.Ptr() + compareSize, newsize - compareSize );
				
				if( debug )
				{
					for( objectSize_t i = compareSize; i < newsize; i++ )
					{
						if( InDebugRange( i ) )
						{
							idLib::Printf( "%02X", newbuffer[i] );
						}
					}
				}
				
			}
			state.buffer = newbuffer;
			state.changedCount = sequence;
			bytesRead += sizeof( byte ) * newsize;
			if( debug )
			{
				idLib::Printf( "\n" );
				state.Print( "NEW STATE" );
			}
			
			if( report )
			{
				idLib::Printf( "    Obj %d Compressed: Size %d \n", objectNum, rleCompressor.CompressedSize() );
			}
		}
#ifdef SNAPSHOT_CHECKSUMS
		extern uint32 SnapObjChecksum( const uint8 * data, int length );
		if( state.buffer.Size() > 0 )
		{
			uint32 checksum = 0;
			lzwCompressor.ReadAgnostic( checksum );
			bytesRead += sizeof( checksum );
			if( !verify( checksum == SnapObjChecksum( state.buffer.Ptr(), state.buffer.Size() ) ) )
			{
				idLib::Error( " Invalid snapshot checksum" );
			}
		}
#endif
	}
	// partial delta
	return false;
}
/*
========================
idLobby::SendCompletedPendingSnap
========================
*/
void idLobby::SendCompletedPendingSnap( int p )
{

	assert( lobbyType == GetActingGameStateLobbyType() );
	
	int time = Sys_Milliseconds();
	
	peer_t& peer = peers[p];
	
	if( !peer.IsConnected() )
	{
		return;
	}
	
	if( peer.snapProc == NULL || !peer.snapProc->PendingSnapReadyToSend() )
	{
		return;
	}
	
	// If we have a pending snap ready to send, we better have a pending snap
	assert( peer.snapProc->HasPendingSnap() );
	
	// Get the snap data blob now, even if we don't send it.
	// This is somewhat wasteful, but we have to do this to keep the snap job pipe ready to keep doing work
	// If we don't do this, this peer will cause other peers to be starved of snapshots, when they may very well be ready to send a snap
	byte buffer[ MAX_SNAP_SIZE ];
	int maxLength = sizeof( buffer ) - peer.packetProc->GetReliableDataSize() - 128;
	
	int size = peer.snapProc->GetPendingSnapDelta( buffer, maxLength );
	
	if( !CanSendMoreData( p ) )
	{
		return;
	}
	
	// Can't send anymore snapshots until all fragments are sent
	if( peer.packetProc->HasMoreFragments() )
	{
		return;
	}
	
	// If the peer doesn't have the latest resource list, send it to him before sending any new snapshots
	if( SendResources( p ) )
	{
		return;
	}
	
	int timeFromJobSub = time - peer.lastSnapJobTime;
	int timeFromLastSend = time - peer.lastSnapTime;
	
	if( timeFromLastSend > 0 )
	{
		peer.snapHz = 1000.0f / ( float )timeFromLastSend;
	}
	else
	{
		peer.snapHz = 0.0f;
	}
	
	if( net_snapshot_send_warntime.GetInteger() > 0 && peer.lastSnapTime != 0 && net_snapshot_send_warntime.GetInteger() < timeFromLastSend )
	{
		idLib::Printf( "NET: Took %d ms to send peer %d snapshot\n", timeFromLastSend, p );
	}
	
	if( peer.throttleSnapsForXSeconds != 0 )
	{
		if( time < peer.throttleSnapsForXSeconds )
		{
			return;
		}
		
		// If we were trying to recover ping, see if we succeeded
		if( peer.recoverPing != 0 )
		{
			if( peer.lastPingRtt >= peer.recoverPing )
			{
				peer.failedPingRecoveries++;
			}
			else
			{
				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 )
				{
					// If throttling recovered the ping
					int maxRate = common->GetSnapRate() * session->GetTitleStorageInt( "net_peer_throttle_maxSnapRate", net_peer_throttle_maxSnapRate.GetInteger() );
					peer.throttledSnapRate = idMath::ClampInt( common->GetSnapRate(), maxRate, peer.throttledSnapRate + common->GetSnapRate() );
				}
			}
		}
		
		peer.throttleSnapsForXSeconds = 0;
	}
	
	peer.lastSnapTime = time;
	
	if( size != 0 )
	{
		if( size > 0 )
		{
			NET_VERBOSESNAPSHOT_PRINT_LEVEL( 3, va( "NET: (peer %d) Sending snapshot %d delta'd against %d. Since JobSub: %d Since LastSend: %d. Size: %d\n", p, peer.snapProc->GetSnapSequence(), peer.snapProc->GetBaseSequence(), timeFromJobSub, timeFromLastSend, size ) );
			ProcessOutgoingMsg( p, buffer, size, false, 0 );
		}
		else if( size < 0 )  	// Size < 0 indicates the delta buffer filled up
		{
			// There used to be code here that would disconnect peers if they were in game and filled up the buffer
			// This was causing issues in the playtests we were running (Doom 4 MP) and after some conversation
			// determined that it was not needed since a timeout mechanism has been added since
			ProcessOutgoingMsg( p, buffer, -size, false, 0 );
			if( peer.snapProc != NULL )
			{
				NET_VERBOSESNAPSHOT_PRINT( "NET: (peerNum: %d - name: %s) Resending last snapshot delta %d because his delta list filled up. Since JobSub: %d Since LastSend: %d Delta Size: %d\n", p, GetPeerName( p ), peer.snapProc->GetSnapSequence(), timeFromJobSub, timeFromLastSend, size );
			}
		}
	}
	
	// We calculate what our outgoing rate was for each sequence, so we can have a relative comparison
	// for when the client reports what his downstream was in the same timeframe
	if( IsHost() && peer.snapProc != NULL && peer.snapProc->GetSnapSequence() > 0 )
	{
		//NET_VERBOSE_PRINT("^8  %i Rate: %.2f   SnapSeq: %d GetBaseSequence: %d\n", lastAppendedSequence, peer.packetProc->GetOutgoingRateBytes(), peer.snapProc->GetSnapSequence(), peer.snapProc->GetBaseSequence() );
		peer.sentBpsHistory[ peer.snapProc->GetSnapSequence() % MAX_BPS_HISTORY ] = peer.packetProc->GetOutgoingRateBytes();
	}
}