//========= Copyright 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"
#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"
#if defined( REPLAY_ENABLED )
#include "replayserver.h"
#include "replay_internal.h"
#endif
#include "tier2/tier2.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, "Max bandwidth rate allowed on server, 0 == unlimited" );
static ConVar	sv_minrate( "sv_minrate", "3500", FCVAR_REPLICATED, "Min bandwidth rate allowed on server, 0 == unlimited" );
       
       ConVar	sv_maxupdaterate( "sv_maxupdaterate", "100", FCVAR_REPLICATED, "Maximum updates per second that the server will allow" );
	   ConVar	sv_minupdaterate( "sv_minupdaterate", "20", FCVAR_REPLICATED, "Minimum updates per second that the server will allow" );

	   ConVar	sv_stressbots("sv_stressbots", "0", FCVAR_DEVELOPMENTONLY, "If set to 1, the server calculates data and fills packets to bots. Used for perf testing.");
static ConVar	sv_allowdownload ("sv_allowdownload", "1", 0, "Allow clients to download files");
static ConVar	sv_allowupload ("sv_allowupload", "1", 0, "Allow clients to upload customizations files");
	   ConVar	sv_sendtables ( "sv_sendtables", "0", FCVAR_DEVELOPMENTONLY, "Force full sendtable sending path." );
	   

extern ConVar sv_maxreplay;
extern ConVar tv_snapshotrate;
extern ConVar tv_transmitall;
extern ConVar sv_pure_kick_clients;
extern ConVar sv_pure_trace;

// 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 SWDS 
	"rpt",
	"rpt_connect",
	"rpt_password",
	"rpt_screenshot",
	"rpt_download_log",
#endif
	NULL,
};


// Used on the server and on the client to bound its cl_rate cvar.
int ClampClientRate( int nRate )
{
	if ( sv_maxrate.GetInt() > 0 )
	{
		nRate = clamp( nRate, MIN_RATE, sv_maxrate.GetInt() );
	}

	if ( sv_minrate.GetInt() > 0 )
	{
		nRate = clamp( nRate, sv_minrate.GetInt(), MAX_RATE );
	}

	return nRate;
}


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;
}

CGameClient::~CGameClient()
{

}

bool CGameClient::ProcessClientInfo( CLC_ClientInfo *msg )
{
	CBaseClient::ProcessClientInfo( msg );

	if ( m_bIsHLTV )
	{
		// Likely spoofing, or misconfiguration. Don't let Disconnect pathway believe this is a replay bot, it will
		// asplode.
		m_bIsHLTV = false;
		Disconnect( "ProcessClientInfo: SourceTV can not connect to game directly.\n" );
		return false;
	}

#if defined( REPLAY_ENABLED )
	if ( m_bIsReplay )
	{
		// Likely spoofing, or misconfiguration. Don't let Disconnect pathway believe this is an hltv bot, it will
		// asplode.
		m_bIsReplay = false;
		Disconnect( "ProcessClientInfo: 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::ProcessMove(CLC_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; 


	int totalcmds =msg->m_nBackupCommands + msg->m_nNewCommands;

	// Decrement drop count by held back packet count
	int netdrop = m_NetChannel->GetDropNumber();

	bool ignore = !sv.IsActive();
#ifdef SWDS
	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->m_NetChan->incoming_sequence,
//		m_Client->m_NetChan->incoming_sequence & SV_UPDATE_MASK );
	
	int startbit = msg->m_DataIn.GetNumBitsRead();

	serverGameClients->ProcessUsercmds
	( 
		edict,					// Player edict
		&msg->m_DataIn,
		msg->m_nNewCommands,
		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 ( msg->m_DataIn.IsOverflowed() )
	{
		Disconnect( "ProcessUsercmds:  Overflowed reading usercmd data (check sending and receiving code for mismatches)!\n" );
		return false;
	}

	int endbit = msg->m_DataIn.GetNumBitsRead();

	if ( msg->m_nLength != (endbit-startbit) )
	{
		Disconnect( "ProcessUsercmds:  Incorrect reading frame (check sending and receiving code for mismatches)!\n" );
		return false;
	}

	return true;
}

bool CGameClient::ProcessVoiceData( CLC_VoiceData *msg )
{
	char voiceDataBuffer[4096];
	int bitsRead = msg->m_DataIn.ReadBitsClamped( voiceDataBuffer, msg->m_nLength );

	SV_BroadcastVoiceData( this, Bits2Bytes(bitsRead), voiceDataBuffer, msg->m_xuid );

	return true;
}

bool CGameClient::ProcessCmdKeyValues( CLC_CmdKeyValues *msg )
{
	serverGameClients->ClientCommandKeyValues( edict, msg->GetKeyValues() );
	return true;
}

bool CGameClient::ProcessRespondCvarValue( CLC_RespondCvarValue *msg )
{
	if ( msg->m_iCookie > 0 )
	{
		if ( g_pServerPluginHandler )
			g_pServerPluginHandler->OnQueryCvarValueFinished( msg->m_iCookie, edict, msg->m_eStatusCode, msg->m_szCvarName, msg->m_szCvarValue );
	}
	else
	{
		// Negative cookie means the game DLL asked for the value.
		if ( serverGameDLL && g_iServerGameDLLVersion >= 6 )
		{
#ifdef REL_TO_STAGING_MERGE_TODO
			serverGameDLL->OnQueryCvarValueFinished( msg->m_iCookie, edict, msg->m_eStatusCode, msg->m_szCvarName, msg->m_szCvarValue );
#endif
		}
	}
	
	return true;
}

#include "pure_server.h"
bool CGameClient::ProcessFileCRCCheck( CLC_FileCRCCheck *msg )
{
	// Ignore this message if we're not in pure server mode...
	if ( !sv.IsInPureServerMode() )
		return true;

	char warningStr[1024] = {0};

	// The client may send us files we don't care about, so filter them here
//	if ( !sv.GetPureServerWhitelist()->GetForceMatchList()->IsFileInList( msg->m_szFilename ) )
//		return true;

	// first check against all the other files users have sent
	FileHash_t filehash;
	filehash.m_md5contents = msg->m_MD5;
	filehash.m_crcIOSequence = msg->m_CRCIOs;
	filehash.m_eFileHashType = msg->m_eFileHashType;
	filehash.m_cbFileLen = msg->m_nFileFraction;
	filehash.m_nPackFileNumber = msg->m_nPackFileNumber;
	filehash.m_PackFileID = msg->m_PackFileID;

	const char *path = msg->m_szPathID;
	const char *fileName = msg->m_szFilename;
	if ( g_PureFileTracker.DoesFileMatch( path, fileName, msg->m_nFileFraction, &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 ( sv_pure_kick_clients.GetInt() )
		{
			Disconnect( "%s", 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(), msg->m_szPathID, msg->m_szFilename );
		}
	}

	return true;
}
bool CGameClient::ProcessFileMD5Check( CLC_FileMD5Check *msg )
{
	// Legacy message
	return true;
}


#if defined( REPLAY_ENABLED )
bool CGameClient::ProcessSaveReplay( CLC_SaveReplay *pMsg )
{
	// Don't allow on listen servers
	if ( !sv.IsDedicated() )
		return false;

	if ( !g_pReplay )
		return false;

	g_pReplay->SV_NotifyReplayRequested();

	return true;
}
#endif

void CGameClient::DownloadCustomizations()
{
	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, "game" ) )
			continue; // we already have it

		// we don't have it, request download from client

		m_nCustomFiles[i].reqID = m_NetChannel->RequestFile( hexname.m_Filename );
	}
}

void CGameClient::Connect( const char * szName, int nUserID, INetChannel *pNetChannel, bool bFakePlayer, int clientChallenge )
{
	CBaseClient::Connect( szName, nUserID, pNetChannel, bFakePlayer, clientChallenge );

	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 - server only
	IGameEvent *event = g_GameEventManager.CreateEvent( "player_connect" );
	{
		event->SetInt( "userid", m_UserID );
		event->SetInt( "index", m_nClientSlot );
		event->SetString( "name", m_Name );
		event->SetString("networkid", GetNetworkIDString() ); 	
		event->SetString( "address", m_NetChannel?m_NetChannel->GetAddress():"none" );
		event->SetInt( "bot", m_bFakePlayer?1:0 );
		g_GameEventManager.FireEvent( event, true );
	}

	// the only difference here is we don't send an
	// IP to prevent hackers from doing evil things
	event = g_GameEventManager.CreateEvent( "player_connect_client" );
	if ( event )
	{
		event->SetInt( "userid", m_UserID );
		event->SetInt( "index", m_nClientSlot );
		event->SetString( "name", m_Name );
		event->SetString( "networkid", GetNetworkIDString() );
		event->SetInt( "bot", m_bFakePlayer ? 1 : 0 );
		g_GameEventManager.FireEvent( event );
	}
}

void CGameClient::SetupPackInfo( CFrameSnapshot *pSnapshot )
{
	// 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
#ifndef _XBOX
#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
#endif
	{
		m_PackInfo.m_pTransmitAlways = NULL;
	}

	// Add frame to ClientFrame list 

	int nMaxFrames = MAX_CLIENT_FRAMES;

	if ( sv_maxreplay.GetFloat() > 0 )
	{
		// if the server has replay features enabled, allow a way bigger frame buffer
		nMaxFrames = max ( (float)nMaxFrames, sv_maxreplay.GetFloat() / m_Server->GetTickInterval() );
	}
		
	if ( nMaxFrames < AddClientFrame( m_pCurrentFrame ) )
	{
		// If the client has more than 64 frames, the server will start to eat too much memory.
		RemoveOldestFrame(); 
	}
		
	// Since area to area visibility is determined by each player's PVS, copy
	//  the area network lookups into the ClientPackInfo_t
	m_PackInfo.m_AreasNetworked = 0;
	int areaCount = g_AreasNetworked.Count();
	for ( int j = 0; j < areaCount; j++ )
	{
		m_PackInfo.m_Areas[m_PackInfo.m_AreasNetworked] = g_AreasNetworked[ j ];
		m_PackInfo.m_AreasNetworked++;

		// Msg("CGameClient::SetupPackInfo: too much areas (%i)", areaCount );
		Assert( m_PackInfo.m_AreasNetworked < MAX_WORLD_AREAS );
	}
	
	CM_SetupAreaFloodNums( m_PackInfo.m_AreaFloodNums, &m_PackInfo.m_nMapAreas );
}

void CGameClient::SetupPrevPackInfo()
{
	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 );
	}

	CBaseClient::SetRate( nRate, bForce );
}
void CGameClient::SetUpdateRate(int udpaterate, bool bForce)
{
	if ( !bForce )
	{
		if ( sv_maxupdaterate.GetInt() > 0 )
		{
			udpaterate = clamp( udpaterate, 1, sv_maxupdaterate.GetInt() );
		}

		if ( sv_minupdaterate.GetInt() > 0 )
		{
			udpaterate = clamp( udpaterate, sv_minupdaterate.GetInt(), 100 );
		}
	}

	CBaseClient::SetUpdateRate( udpaterate, bForce );
}


void CGameClient::UpdateUserSettings()
{
	// set voice loopback
	m_bVoiceLoopback = m_ConVars->GetInt( "voice_loopback", 0 ) != 0;

	CBaseClient::UpdateUserSettings();

	// Give entity dll a chance to look at the changes.
	// Do this after CBaseClient::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;
} */


/*
===================
SV_FullClientUpdate

sends all the info about *cl to *sb
===================
*/

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 );
	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 );
	}
#ifndef _XBOX
	if ( IsHLTV() )
	{	
		hltv->Changelevel();
	}

#if defined( REPLAY_ENABLED )
	if ( IsReplay() )
	{
		replay->Changelevel();
	}
#endif
#endif
	CBaseClient::Inactivate();

	m_Sounds.Purge();
	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 CBaseClient::UpdateAcknowledgedFramecount( tick );
}



void CGameClient::Clear()
{
#ifndef _XBOX
	if ( m_bIsHLTV && hltv )
	{
		hltv->Shutdown();
	}
	
#if defined( REPLAY_ENABLED )
	if ( m_bIsReplay && replay )
	{
		replay->Shutdown();
	}
#endif
#endif

	CBaseClient::Clear();

	// free all frames
	DeleteClientFrames( -1 );

	m_Sounds.Purge();
	m_VoiceStreams.ClearAll();
	m_VoiceProximity.ClearAll();
	edict = NULL;
	m_pViewEntity = NULL;
	m_bVoiceLoopback = false;
	m_LastMovementTick = 0;
	m_nSoundSequence = 0;
#if defined( REPLAY_ENABLED )
	m_flLastSaveReplayTime = host_time;
#endif
}

void CGameClient::Reconnect( void )
{
	// If the client was connected before, tell the game .dll to disconnect him/her.
	sv.RemoveClientFromGame( this );

	CBaseClient::Reconnect();
}

void CGameClient::Disconnect( const char *fmt, ... )
{
	va_list		argptr;
	char		reason[1024];

	if ( m_nSignonState == SIGNONSTATE_NONE )
		return;	// no recursion

	va_start (argptr,fmt);
	Q_vsnprintf (reason, sizeof( reason ), fmt,argptr);
	va_end (argptr);

	// 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", reason );
		event->SetString("name", GetClientName() );
		event->SetString("networkid", GetNetworkIDString() ); 
		event->SetInt( "bot", m_bFakePlayer?1:0 );
		g_GameEventManager.FireEvent( event );
	}

	m_Server->RemoveClientFromGame( this );

	CBaseClient::Disconnect( "%s", reason );
}

bool CGameClient::SetSignonState(int state, int spawncount)
{
	if ( state == SIGNONSTATE_CONNECTED )
	{
		if ( !CheckConnect() )
			return false;

		m_NetChannel->SetTimeout( SIGNON_TIME_OUT ); // allow 5 minutes to load map
		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 );

#ifdef _XBOX
		// to save memory on the XBOX reduce reliable buffer size from 96 to 8 kB
		m_NetChannel->SetMaxBufferSize( true, 8*1024 );
#endif
	}

	return CBaseClient::SetSignonState( state, spawncount );
}

void CGameClient::SendSound( SoundInfo_t &sound, bool isReliable )
{
#if defined( REPLAY_ENABLED )
	if ( IsFakeClient() && !IsHLTV() && !IsReplay() )
#else
	if ( IsFakeClient() && !IsHLTV() )
#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 )
	{
		SVC_Sounds	sndmsg;
		char		buffer[32];

		m_nSoundSequence = ( m_nSoundSequence + 1 ) & SOUND_SEQNUMBER_MASK;	// increase own sound sequence counter
		sound.nSequenceNumber = 0; // don't transmit nSequenceNumber for reliable sounds

		sndmsg.m_DataOut.StartWriting(buffer, sizeof(buffer) );
		sndmsg.m_nNumSounds = 1;
		sndmsg.m_bReliableSound = true;
				
		SoundInfo_t	defaultSound; defaultSound.SetDefault();

		sound.WriteDelta( &defaultSound, sndmsg.m_DataOut );

		// send reliable sound as single message
		SendNetMsg( sndmsg, true );
		return;
	}

	sound.nSequenceNumber = m_nSoundSequence;

	m_Sounds.AddToTail( sound );	// queue sounds until snapshot is send
}

void CGameClient::WriteGameSounds( bf_write &buf )
{
	if ( m_Sounds.Count() <= 0 )
		return;

	char data[NET_MAX_PAYLOAD];
	SVC_Sounds msg;
	msg.m_DataOut.StartWriting( data, sizeof(data) );
	
	int nSoundCount = FillSoundsMessage( msg );
	msg.WriteToBuffer( buf );

	if ( IsTracing() )
	{
		TraceNetworkData( buf, "Sounds [count=%d]", nSoundCount );
	}
}

int	CGameClient::FillSoundsMessage(SVC_Sounds &msg)
{
	int i, count = m_Sounds.Count();

	// send max 64 sound in multiplayer per snapshot, 255 in SP
	int max = m_Server->IsMultiplayer() ? 32 : 255;

	// Discard events if we have too many to signal with 8 bits
	if ( count > max )
		count = max;

	// Nothing to send
	if ( !count )
		return 0;

	SoundInfo_t defaultSound; defaultSound.SetDefault();
	SoundInfo_t *pDeltaSound = &defaultSound;
	
	msg.m_nNumSounds = count;
	msg.m_bReliableSound = false;
	msg.SetReliable( false );

	Assert( msg.m_DataOut.GetNumBitsLeft() > 0 );

	for ( i = 0 ; i < count; i++ )
	{
		SoundInfo_t &sound = m_Sounds[ i ];
		sound.WriteDelta( pDeltaSound, msg.m_DataOut );
		pDeltaSound = &m_Sounds[ i ];
	}

	// remove added events from list
	int remove = m_Sounds.Count() - ( count + max );

	if ( remove > 0 )
	{
		DevMsg("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() <= max ); // keep ev_max temp ent for next update

	return msg.m_nNumSounds;
}



bool CGameClient::CheckConnect( void )
{
	// Allow the game dll to reject this client.
	char szRejectReason[128];
	Q_strncpy( szRejectReason, "Connection rejected by game\n", sizeof( szRejectReason ) );

	if ( !g_pServerPluginHandler->ClientConnect( edict, m_Name, m_NetChannel->GetAddress(), szRejectReason, sizeof( szRejectReason ) ) )
	{
		// Reject the connection and drop the client.
		Disconnect( szRejectReason, m_Name );
		return false;
	}

	return true;
}

void CGameClient::ActivatePlayer( void )
{
	CBaseClient::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" );
}

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
		SVC_ClassInfo classmsg( true, m_Server->serverclasses );
		m_NetChannel->SendNetMsg( classmsg );
	}

	if ( !CBaseClient::SendSignonData()	)
		return false;

	m_nSoundSequence = 1; // reset sound sequence numbers after signon block

	return true;
}


void CGameClient::SpawnPlayer( void )
{
	// 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
    SVC_SetView setView( m_nEntityIndex );
	SendNetMsg( setView );

	

	CBaseClient::SpawnPlayer();

	// notify that the player is spawning
	serverGameClients->ClientSpawned( edict );
}

CClientFrame *CGameClient::GetDeltaFrame( int nTick )
{
#ifndef _XBOX
	Assert ( !IsHLTV() ); // has no ClientFrames
#if defined( REPLAY_ENABLED )
	Assert ( !IsReplay() );  // has no ClientFrames
#endif
#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() )
		return;

	Assert( serverGameClients );
	CPlayerState *pl = serverGameClients->GetPlayerState( edict );
	Assert( pl );

	if ( pl && pl->fixangle != FIXANGLE_NONE	 )
	{
		if ( pl->fixangle == FIXANGLE_RELATIVE		 )
		{
			SVC_FixAngle fixAngle( true, pl->anglechange );
			m_NetChannel->SendNetMsg( fixAngle );
			pl->anglechange.Init(); // clear
		}
		else
		{
			SVC_FixAngle fixAngle(false, pl->v_angle );
			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)
{
#ifndef _XBOX
	if ( m_bIsHLTV )
	{
		// pass this message to HLTV
		return hltv->SendNetMsg( msg, bForceReliable );
	}
#if defined( REPLAY_ENABLED )
	if ( m_bIsReplay )
	{
		// pass this message to replay
		return replay->SendNetMsg( msg, bForceReliable );
	}
#endif
#endif
	return CBaseClient::SendNetMsg( msg, bForceReliable);
}

bool CGameClient::ExecuteStringCommand( const char *pCommandString )
{
	// first let the baseclass handle it
	if ( CBaseClient::ExecuteStringCommand( pCommandString ) )
		return true;
	
	// Determine whether the command is appropriate
	CCommand args;
	if ( !args.Tokenize( pCommandString ) )
		return false;

	if ( args.ArgC() == 0 )
		return false;

	if ( IsEngineClientCommand( args ) )
	{
		Cmd_ExecuteCommand( args, src_client, m_nClientSlot );
		return true;
	}
	
	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;
		}

		if ( pCommand->IsFlagSet( FCVAR_SPONLY ) )
		{
			if ( sv.IsMultiplayer() )
			{
				return false;
			}
		}

		// Don't allow clients to execute commands marked as development only.
		if ( pCommand->IsFlagSet( FCVAR_DEVELOPMENTONLY ) )
		{
			return false;
		}

		g_pServerPluginHandler->SetCommandClient( m_nClientSlot );
		Cmd_Dispatch( pCommand, args );
	}
	else
	{
		g_pServerPluginHandler->ClientCommand( edict, args ); // TODO pass client id and string
	}

	return true;
}

void CGameClient::SendSnapshot( CClientFrame * pFrame )
{
	if ( m_bIsHLTV )
	{
#ifndef SHARED_NET_STRING_TABLES
		// copy string updates from server to hltv stringtable
		networkStringTableContainerServer->DirectUpdate( GetMaxAckTickCount() );
#endif
		char *buf = (char *)_alloca( NET_MAX_PAYLOAD );

		// pack sounds to one message
		if ( m_Sounds.Count() > 0 )
		{
			SVC_Sounds sounds;
			sounds.m_DataOut.StartWriting( buf, NET_MAX_PAYLOAD );

			FillSoundsMessage( sounds );
			hltv->SendNetMsg( sounds );
		}

		int maxEnts = tv_transmitall.GetBool()?255:64;
		hltv->WriteTempEntities( this, pFrame->GetSnapshot(), m_pLastSnapshot.GetObject(), *hltv->GetBuffer( HLTV_BUFFER_TEMPENTS ), maxEnts );

		// add snapshot to HLTV server frame list
		hltv->AddNewFrame( pFrame );

		// remember this snapshot
		m_pLastSnapshot = pFrame->GetSnapshot(); 

		// fake acknowledgement, remove ClientFrame reference immediately 
		UpdateAcknowledgedFramecount( pFrame->tick_count );
		
		return;
	}

#if defined( REPLAY_ENABLED )
	if ( m_bIsReplay )
	{
#ifndef SHARED_NET_STRING_TABLES
		// copy string updates from server to replay stringtable
		networkStringTableContainerServer->DirectUpdate( GetMaxAckTickCount() );
#endif
		char *buf = (char *)_alloca( NET_MAX_PAYLOAD );

		// pack sounds to one message
		if ( m_Sounds.Count() > 0 )
		{
			SVC_Sounds sounds;
			sounds.m_DataOut.StartWriting( buf, NET_MAX_PAYLOAD );

			FillSoundsMessage( sounds );
			replay->SendNetMsg( sounds );
		}

		int maxEnts = 255;
		replay->WriteTempEntities( this, pFrame->GetSnapshot(), m_pLastSnapshot.GetObject(), *replay->GetBuffer( REPLAY_BUFFER_TEMPENTS ), maxEnts );

		// add snapshot to Replay server frame list
		if ( replay->AddNewFrame( pFrame ) )
		{
			// remember this snapshot
			m_pLastSnapshot = pFrame->GetSnapshot(); 

			// fake acknowledgement, remove ClientFrame reference immediately 
			UpdateAcknowledgedFramecount( pFrame->tick_count );
		}

		return;
	}
#endif

	// update client viewangles update
	WriteViewAngleUpdate();

	CBaseClient::SendSnapshot( pFrame );
}

//-----------------------------------------------------------------------------
// This function contains all the logic to determine if we should send a datagram
// to a particular client
//-----------------------------------------------------------------------------

bool CGameClient::ShouldSendMessages( void )
{
#ifndef _XBOX
	if ( m_bIsHLTV )
	{
		// calc snapshot interval
		int nSnapshotInterval = 1.0f / ( m_Server->GetTickInterval() * tv_snapshotrate.GetFloat() );

		// I am the HLTV client, record every nSnapshotInterval tick
		return ( sv.m_nTickCount >= (hltv->m_nLastTick + nSnapshotInterval) );
	}
	
#if defined( REPLAY_ENABLED )
	if ( m_bIsReplay )
	{
		const float replay_snapshotrate = 16.0f;

		// calc snapshot interval
		int nSnapshotInterval = 1.0f / ( m_Server->GetTickInterval() * replay_snapshotrate );

		// I am the Replay client, record every nSnapshotInterval tick
		return ( sv.m_nTickCount >= (replay->m_nLastTick + nSnapshotInterval) );
	}
#endif
#endif
	// If sv_stressbots is true, then treat a bot more like a regular client and do deltas and such for it.
	if( IsFakeClient() )
	{
		if ( !sv_stressbots.GetBool() )
			return false;
	}

	return CBaseClient::ShouldSendMessages();
}

void CGameClient::FileReceived( const char *fileName, unsigned int transferID )
{
	//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 )
{
	DevMsg( "File '%s' requested from client %s.\n", fileName, m_NetChannel->GetAddress() );

	if ( sv_allowdownload.GetBool() )
	{
		m_NetChannel->SendFile( fileName, transferID );
	}
	else
	{
		m_NetChannel->DenyFile( fileName, transferID );
	}
}

void CGameClient::FileDenied(const char *fileName, unsigned int transferID )
{
	ConMsg( "Downloading file '%s' from client %s failed.\n", fileName, GetClientName() );
}

void CGameClient::FileSent( const char *fileName, unsigned int transferID )
{
	ConMsg( "Sent file '%s' to client %s.\n", fileName, GetClientName() );
}

void CGameClient::PacketStart(int incoming_sequence, int 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)
{
#ifndef _XBOX
	SV_RedirectEnd ();
#endif
	// Check for printf format tokens in this reason string. Crash exploit.
	Disconnect ( (reason && !strchr( reason, '%' ) ) ? reason : "Connection closing" );	
}

void CGameClient::ConnectionCrashed(const char *reason)
{
	if ( m_Name[0] && IsConnected() )
	{
		DebuggerBreakIfDebugging_StagingOnly();

#ifndef _XBOX
		SV_RedirectEnd ();
#endif
		// Check for printf format tokens in this reason string. Crash exploit.
		Disconnect ( (reason && !strchr( reason, '%' ) ) ? reason : "Connection lost" );	
	}
}

CClientFrame *CGameClient::GetSendFrame()
{
	CClientFrame *pFrame = m_pCurrentFrame;

	// just return if replay is disabled
	if ( sv_maxreplay.GetFloat() <= 0 )
		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 CBaseClient::IgnoreTempEntity( event );
}


const CCheckTransmitInfo* CGameClient::GetPrevPackInfo()
{
	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;

	SVC_Sounds	msg;

	char *buf = (char *)_alloca( NET_MAX_PAYLOAD );

	msg.m_DataOut.StartWriting( buf, NET_MAX_PAYLOAD );

	msg.m_nNumSounds = m_Sounds.Count();
	msg.m_bReliableSound = false;
	msg.SetReliable( false );

	Assert( msg.m_DataOut.GetNumBitsLeft() > 0 );

	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 );
	}

	if ( !bSndStop && s1.nSpecialDSP != s2.nSpecialDSP )
	{
		Msg( "special dsp mismatch %d %d\n", s1.nSpecialDSP, s2.nSpecialDSP );
	}

	// 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