/*
Copyright (C) 1997-2001 Id Software, Inc.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/
// sv_user.c -- server code for moving users

#include "server.h"

edict_t	*sv_player;

/*
============================================================

USER STRINGCMD EXECUTION

sv_client and sv_player will be valid.
============================================================
*/

/*
================
SV_New_f

Sends the first message from the server to a connected client.
This will be sent on the initial connection and upon each server load.
================
*/
void SV_New_f( void )
{
	char		*gamedir;
	int			playernum;
	edict_t		*ent;
	int			request_confistrings = -1;
	int			sv_bitflags = 0;

	Com_DPrintf( "New() from %s\n", sv_client->name );

	// if in CS_AWAITING we have sended the response packet the new once already,
	// but client might have not got it so we send it again
	if( sv_client->state != CS_CONNECTED && sv_client->state != CS_AWAITING ) {
		Com_Printf( "New not valid -- already spawned\n" );
		return;
	}

	//
	// serverdata needs to go over for all types of servers
	// to make sure the protocol is right, and to set the gamedir
	//
	gamedir = Cvar_VariableString( "fs_gamedir" );
	
	MSG_Clear( &tmpMessage );

	// send the serverdata
	MSG_WriteByte( &tmpMessage, svc_serverdata );
	MSG_WriteLong( &tmpMessage, PROTOCOL_VERSION );
	MSG_WriteLong( &tmpMessage, svs.spawncount );
	MSG_WriteString( &tmpMessage, gamedir );

	playernum = sv_client - svs.clients;
	MSG_WriteShort( &tmpMessage, playernum );

	// send full levelname
	MSG_WriteString( &tmpMessage, sv.name );

	//
	// game server
	// 
	if( sv.state == ss_game )
	{
		// set up the entity for the client
		ent = EDICT_NUM(playernum+1);
		ent->s.number = playernum+1;
		sv_client->edict = ent;
		memset( &sv_client->lastcmd, 0, sizeof(sv_client->lastcmd) );

#ifdef BATTLEYE
		if( sv_battleye->integer )
			sv_bitflags |= 128;
#endif
		MSG_WriteByte( &tmpMessage, sv_bitflags );

#if 1
		// we want it to request configstrings
		request_confistrings = 0;
#else
		// write as many configstrings as we can fit into the message
		{
			int	start = 0;
			int ofs_configstrings = tmpMessage.cursize;
			int	ofs_recover;
			
			MSG_WriteShort( &tmpMessage, 0 );
			
			// write a packet full of data
			while( start < MAX_CONFIGSTRINGS &&
				tmpMessage.cursize < MAX_PACKETLEN * 3 )
			{
				if( sv.configstrings[start][0] ) {
					//SV_SendServerCommand( sv_client, "cs %i \"%s\"", start, sv.configstrings[start] );
					MSG_WriteShort( &tmpMessage, start );
					MSG_WriteString( &tmpMessage, sv.configstrings[start] );
				}
				start++;
			}

			ofs_recover = tmpMessage.cursize;
			tmpMessage.cursize = ofs_configstrings;
			MSG_WriteShort( &tmpMessage, start );
			tmpMessage.cursize = ofs_recover;

			request_confistrings = start;
		}
#endif
	}

	MSG_WriteShort( &tmpMessage, request_confistrings );

	// reset the reliable commands chain
	sv_client->clientCommandExecuted = 0;
	sv_client->reliableAcknowledge = 0;
	sv_client->reliableSequence = 0;
	sv_client->reliableSent = 0;
	memset( &sv_client->reliableCommands, 0, sizeof(sv_client->reliableCommands) );
#ifdef BATTLEYE
	memset( &sv_client->BE, 0, sizeof(sv_client->BE) );
#endif

	SV_SendMessageToClient( sv_client, &tmpMessage );
	Netchan_PushAllFragments( &sv_client->netchan );

	// don't let it send reliable commands until we get the first configstring request
	sv_client->state = CS_AWAITING;
}

//==================
//SV_Configstrings_f
//==================
void SV_Configstrings_f( void )
{
	int			start;

	if( sv_client->state == CS_AWAITING ) {
		Com_DPrintf( "Start Configstrings() from %s\n", sv_client->name );
		sv_client->state = CS_CONNECTED;
	} else
		Com_DPrintf( "Configstrings() from %s\n", sv_client->name );

	if( sv_client->state != CS_CONNECTED )
	{
		Com_Printf( "configstrings not valid -- already spawned\n" );
		return;
	}

	// handle the case of a level changing while a client was connecting
	if( atoi(Cmd_Argv(1)) != svs.spawncount )
	{
		Com_Printf( "SV_Configstrings_f from different level\n" );
		SV_New_f();
		return;
	}
	
	start = atoi(Cmd_Argv(2));
	if( start < 0 ) {
		start = 0;
	}
	
	// write a packet full of data
	while( start < MAX_CONFIGSTRINGS &&
		sv_client->reliableSequence - sv_client->reliableAcknowledge < MAX_RELIABLE_COMMANDS - 8 )
	{
		if( sv.configstrings[start][0] ) {
			SV_SendServerCommand( sv_client, "cs %i \"%s\"", start, sv.configstrings[start] );
		}
		start++;
	}

	// send next command
	if( start == MAX_CONFIGSTRINGS ) {
		SV_SendServerCommand( sv_client, "cmd baselines %i 0", svs.spawncount );
	} else {
		SV_SendServerCommand( sv_client, "cmd configstrings %i %i", svs.spawncount, start );
	}
}

#define MAX_BASELINE_MSGLEN 2000

//==================
//SV_Baselines_f
//==================
void SV_Baselines_f( void )
{
	int		start;
	entity_state_t	nullstate;
	entity_state_t	*base;

	Com_DPrintf( "Baselines() from %s\n", sv_client->name );

	if( sv_client->state != CS_CONNECTED ) {
		Com_Printf( "baselines not valid -- already spawned\n" );
		return;
	}

	// handle the case of a level changing while a client was connecting
	if( atoi(Cmd_Argv(1)) != svs.spawncount ) {
		Com_Printf( "SV_Baselines_f from different level\n" );
		SV_New_f();
		return;
	}

	start = atoi(Cmd_Argv(2));
	if( start < 0 )
		start = 0;

	memset( &nullstate, 0, sizeof(nullstate) );

	// write a packet full of data
	MSG_Clear( &tmpMessage );
	
	while( tmpMessage.cursize < FRAGMENT_SIZE * 3
		&& start < MAX_EDICTS )
	{
		base = &sv.baselines[start];
		if( base->modelindex || base->sound || base->effects )
		{
			MSG_WriteByte( &tmpMessage, svc_spawnbaseline );
			MSG_WriteDeltaEntity( &nullstate, base, &tmpMessage, qtrue, qtrue );
		}
		start++;
	}
	
	// send next command
	if( start == MAX_EDICTS ) {
		SV_SendServerCommand( sv_client, "precache %i", svs.spawncount );
	} else {	
		SV_SendServerCommand( sv_client, "cmd baselines %i %i", svs.spawncount, start );
	}
	
	SV_AddReliableCommandsToMessage( sv_client, &tmpMessage );
	SV_SendMessageToClient( sv_client, &tmpMessage );
}

/*
==================
SV_Begin_f
==================
*/
void SV_Begin_f( void )
{
	Com_DPrintf( "Begin() from %s\n", sv_client->name );

	// wsw : r1q2[start] : could be abused to respawn or cause spam/other mod-specific problems
	if( sv_client->state != CS_CONNECTED )
	{
		if( dedicated->integer )
			Com_Printf( "SV_Begin_f: 'Begin' from already spawned client: %s.\n", sv_client->name );
		SV_DropClient( sv_client, DROP_TYPE_GENERAL, "Already spawned client" );
		return;
	}
	// wsw : r1q2[end]

	// handle the case of a level changing while a client was connecting
	if( atoi(Cmd_Argv(1)) != svs.spawncount )
	{
		Com_Printf( "SV_Begin_f from different level\n" );
		SV_New_f();
		return;
	}

	sv_client->state = CS_SPAWNED;

	// call the game begin function
	ge->ClientBegin( sv_player );

	Cbuf_InsertFromDefer();
}

//=============================================================================


/*
==================
SV_NextDownload_f

Responds to reliable nextdl packet with unreliable download packet
If nextdl packet's offet information is negative, download will be stopped
==================
*/
void SV_NextDownload_f( void )
{
	int		blocksize;
	int		offset;

	if( !sv_uploads->integer || !sv_uploads_from_server->integer )
		return;

	if( strlen(sv_client->downloadname) == 0 ) {
		Com_Printf( "nextdl message for client with no download active from %s\n", sv_client->name );
		return;
	}

	if( Q_stricmp(sv_client->downloadname, Cmd_Argv(1)) ) {
		Com_Printf( "nextdl message for wrong filename from %s\n", sv_client->name );
		return;
	}

	offset = atoi(Cmd_Argv(2));

	if( offset >= sv_client->downloadsize ) {
		Com_Printf( "nextdl message with too big offset from %s\n", sv_client->name );
		return;
	}
	if( offset == -1 ) {
		Com_Printf( "Upload of %s to %s completed\n", sv_client->downloadname, sv_client->name );
		if( sv_client->download ) {
			FS_FreeFile( sv_client->download );
			sv_client->download = NULL;
		}
		sv_client->downloadtimeout = 0;
		sv_client->downloadname[0] = 0;
		sv_client->downloadsize = 0;
		return;
	}
	if( offset < 0 ) {
		Com_Printf( "Upload of %s to %s failed\n", sv_client->downloadname, sv_client->name );
		if( sv_client->download ) {
			FS_FreeFile( sv_client->download );
			sv_client->download = NULL;
		}
		sv_client->downloadtimeout = 0;
		sv_client->downloadname[0] = 0;
		sv_client->downloadsize = 0;
		return;
	}

	if( !sv_client->download ) {
		char	fullname[MAX_OSPATH];

		Com_Printf( "Starting server upload of %s to %s\n", sv_client->downloadname, sv_client->name );

		Q_snprintfz( fullname, sizeof(fullname), "%s/%s", FS_Gamedir(), sv_client->downloadname );
		FS_LoadAbsoluteFile( fullname, (void **)&sv_client->download, NULL, 0 );
		if( !sv_client->download ) {
			sv_client->downloadname[0] = 0;
			sv_client->downloadsize = 0;
			Com_Printf( "Error loading %s for uploading\n", fullname );
			return;
		}
	}

	MSG_Clear( &tmpMessage );
	SV_AddReliableCommandsToMessage( sv_client, &tmpMessage );

	blocksize = sv_client->downloadsize - offset;
	// jalfixme: addapt download to user rate setting and sv_maxrate setting.
	if( blocksize > FRAGMENT_SIZE * 2 )
		blocksize = FRAGMENT_SIZE * 2;
	if( offset + blocksize > sv_client->downloadsize )
		blocksize = sv_client->downloadsize - offset;

	MSG_WriteByte( &tmpMessage, svc_download );
	MSG_WriteString( &tmpMessage, sv_client->downloadname );
	MSG_WriteLong( &tmpMessage, offset );
	MSG_WriteLong( &tmpMessage, blocksize );
	MSG_Write( &tmpMessage, sv_client->download + offset, blocksize );
	SV_SendMessageToClient( sv_client, &tmpMessage );

	sv_client->downloadtimeout = svs.realtime + 10000;
}

/*
==================
SV_DenyDownload

Helper function for generating initdownload packets for denying download
==================
*/
static void SV_DenyDownload( const char *reason )
{
	// size -1 is used to signal that it's refused
	// URL field is used for deny reason
	MSG_Clear( &tmpMessage );
	SV_SendServerCommand( sv_client, "initdownload \"%s\" %i %u %i \"%s\"", "", -1, 0, qfalse, reason ? reason : "" );
	SV_AddReliableCommandsToMessage( sv_client, &tmpMessage );
	SV_SendMessageToClient( sv_client, &tmpMessage );
}

/*
==================
SV_BeginDownload_f

Responds to reliable download packet with reliable initdownload packet
==================
*/
void SV_BeginDownload_f( void )
{
	static char	*allowed_directories[] = { "maps/", "sounds/", NULL };
	char		*name, *pakname;
	qboolean	allowed, found;
	char		tempname[MAX_QPATH];
	int			i;
	unsigned	checksum;

	if( !sv_uploads->integer || (!sv_uploads_from_server->integer && (strlen(sv_uploads_baseurl->string) == 0)) ) {
		SV_DenyDownload( "Downloading is not allowed on this server" );
		return;
	}

	name = Cmd_Argv(1);

	// hacked by zoid to allow more conrol over download
	// first off, no .. or global allow check
	if( strstr (name, "..") || *name == '.'  || *name == '/' || strchr (name, '\"')
			|| *name == '$' // deny player models by now
			|| !strstr (name, "/") ) // MUST be in a subdirectory
	{
		SV_DenyDownload("Invalid filename");
		return;
	}
	
	// only allow downloads from certain subdirectories
	allowed = qfalse;
	for( i = 0; allowed_directories[i] != NULL; i++ ) {
		if( Q_strnicmp(name, allowed_directories[i], strlen(allowed_directories[i])) ) {
			allowed = qtrue;
			break;
		}
	}
	if( !allowed) {
		SV_DenyDownload("Invalid file location");
		return;
	}

	// only allow files that are in precache list
	found = qfalse;
	for( i = CS_MODELS; i < CS_IMAGES+MAX_IMAGES; i++ ) {
		if( strlen(sv.configstrings[i]) == 0 )
			continue;

		if( i == CS_MODELS || i == CS_SOUNDS || i == CS_IMAGES ) // empty fields
			continue;

		if( i == CS_MODELS+1 ) // map
		{
			if( !Q_stricmp( sv.configstrings[i], name ) ) {
				found = qtrue;
				break;
			}
			continue;
		}
		else if( i < CS_MODELS+MAX_MODELS )
		{
			if (sv.configstrings[i][0] == '*' ||
				sv.configstrings[i][0] == '$' || // disable playermodel downloading for now
				sv.configstrings[i][0] == '#') {
				continue;
			}
			if( !Q_stricmp( sv.configstrings[i], name ) ) {
				found = qtrue;
				break;
			}
			continue;
		}
		else if( i < CS_SOUNDS+MAX_SOUNDS )
		{
			if( sv.configstrings[i][0] == '*' ) // sexed sounds
				continue;
			Q_strncpyz( tempname, sv.configstrings[i], sizeof(tempname) );
			COM_DefaultExtension( tempname, ".wav", sizeof(tempname) );
			if( !Q_stricmp( tempname, name ) ) {
				found = qtrue;
				break;
			}
			continue;
		}
		else if( i < CS_IMAGES+MAX_IMAGES )
		{
			// disabled for now
			continue;
		}
	}

	if( !found ) {
		SV_DenyDownload("File is not in precache list");
		return;
	}

	if( FS_FOpenFile ( name, NULL, FS_READ ) == -1 ) {
		SV_DenyDownload("Server doesn't have this file");
		return;
	}

	// check if file is inside a PAK
	pakname = FS_PakNameForFile( name );
	if( !pakname ) {
		SV_DenyDownload("Not inside pak file");
		return;
	}

	if( sv_client->download ) {
		FS_FreeFile( sv_client->download );
		sv_client->download = NULL;
	}

	sv_client->downloadsize = FS_LoadAbsoluteFile( pakname, NULL, NULL, 0 );
	if( sv_client->downloadsize <= 0 )
	{
		Com_Printf( "Error getting size of %s for uploading\n", sv_client->downloadname );
		sv_client->downloadname[0] = 0;
		sv_client->downloadsize = 0;
		SV_DenyDownload( "Error getting file size" );
		return;
	}

	checksum = SV_GetCachedPakChecksum(pakname);

	// remove gamedir
	if( strlen(pakname) > 2 )
		pakname += 2;
	while( pakname && *pakname != '/' ) {
		pakname++;
	}
	if( *pakname == '/' )
		pakname++;

	if( !Q_stricmp("data0.pk3", pakname) ) {
		if( sv_client->download ) {
			FS_FreeFile( sv_client->download );
			sv_client->download = NULL;
		}
		sv_client->downloadtimeout = 0;
		sv_client->downloadname[0] = 0;
		sv_client->downloadsize = 0;
		SV_DenyDownload("Can't download data0.pk3");
		return;
	}

	Q_strncpyz( sv_client->downloadname, pakname, sizeof(sv_client->downloadname) );

	Com_Printf( "Offering %s to %s\n", sv_client->downloadname, sv_client->name );

	// start the download
	MSG_Clear( &tmpMessage );
	SV_SendServerCommand( sv_client, "initdownload \"%s\" %i %u %i \"%s\"", sv_client->downloadname,
			sv_client->downloadsize, checksum, (sv_uploads_from_server->integer != 0),
			(strlen(sv_uploads_baseurl->string) > 0) ? va("%s/%s", sv_uploads_baseurl->string, pakname) : "" );
	SV_AddReliableCommandsToMessage( sv_client, &tmpMessage );
	SV_SendMessageToClient( sv_client, &tmpMessage );
}


//============================================================================


/*
=================
SV_Disconnect_f

The client is going to disconnect, so remove the connection immediately
=================
*/
void SV_Disconnect_f( void )
{
//	SV_EndRedirect();
	SV_DropClient( sv_client, DROP_TYPE_GENERAL, "User disconnected" );
}


/*
==================
SV_ShowServerinfo_f

Dumps the serverinfo info string
==================
*/
void SV_ShowServerinfo_f( void )
{
	Info_Print( Cvar_Serverinfo() );
}


void SV_Nextserver( void )
{
	char	*v;

	//ZOID, ss_pic can be nextserver'd in coop mode
	if( sv.state == ss_game )
		return;		// can't nextserver while playing a normal game

	svs.spawncount++;	// make sure another doesn't sneak in
	v = Cvar_VariableString("nextserver");
	if( !v[0] )
		Cbuf_AddText( "killserver\n" );
	else
	{
		Cbuf_AddText(v);
		Cbuf_AddText("\n");
	}
	Cvar_Set("nextserver","");
}

/*
==================
SV_Nextserver_f

A cinematic has completed or been aborted by a client, so move
to the next server,
==================
*/
void SV_Nextserver_f( void )
{
	if( atoi(Cmd_Argv(1)) != svs.spawncount ) {
		Com_DPrintf( "Nextserver() from wrong level, from %s\n", sv_client->name );
		return;		// leftover from last server
	}

	Com_DPrintf( "Nextserver() from %s\n", sv_client->name );

	SV_Nextserver();
}

/*
==================
SV_UserinfoCommand_f
==================
*/
void SV_UserinfoCommand_f( void )
{
	char	*uinfo;
	uinfo = Cmd_Argv(1);

	if( Cmd_Argc() > 2 ) { // invalid ?
		Com_DPrintf( "WARNING: SV_ParseUserinfoCommand_f: userinfo string with more than 1 tokem received from client %s\n", sv_client->name );
	}

	 // empty userinfo received
	if( !uinfo || !uinfo[0] ) {
		if( !strlen(sv_client->userinfo) )
			SV_DropClient( sv_client, DROP_TYPE_GENERAL, "Empty userinfo received" );

		return;
	}
	
	Q_strncpyz( sv_client->userinfo, uinfo, sizeof(sv_client->userinfo) );
	SV_UserinfoChanged( sv_client );
}

typedef struct
{
	char	*name;
	void	(*func) (void);
} ucmd_t;

ucmd_t ucmds[] =
{
	// auto issued
	{ "new", SV_New_f },
	{ "configstrings", SV_Configstrings_f },
	{ "baselines", SV_Baselines_f },
	{ "begin", SV_Begin_f },
	{ "nextserver", SV_Nextserver_f },
	{ "disconnect", SV_Disconnect_f },
	{ "usri", SV_UserinfoCommand_f },

	// issued by hand at client consoles	
	{ "info", SV_ShowServerinfo_f },

	{ "download", SV_BeginDownload_f },
	{ "nextdl", SV_NextDownload_f },

	{ NULL, NULL }
};

/*
==================
SV_ExecuteUserCommand
==================
*/
char *Cmd_MacroExpandString(char *text);
void SV_ExecuteUserCommand( char *s )
{
	ucmd_t	*u;

	// wsw : r1q2[start] : catch attempted command expansions
	if( strchr(s, '$') )
	{
		char *teststring = Cmd_MacroExpandString(s);
		if( !teststring )
			return;

		if( strcmp(teststring, s) ) {
			Com_Printf( "Exploit filtered: Client %s[%s] attempted macro-expansion\n", sv_client->name, NET_AdrToString(&sv_client->netchan.remoteAddress) );
			return;
		}
	}
	// wsw : r1q2[end]

	// wsw : r1q2[start] : catch end-of-message exploit
	if( strchr (s, '\xFF') )
	{
		if( dedicated->integer )
			Com_Printf( "%s [%s] Dropped. Found end-of-message inside command string\n", sv_client->name, NET_AdrToString(&sv_client->netchan.remoteAddress) );
		SV_DropClient( sv_client, DROP_TYPE_GENERAL, "Found end-of-message inside command string" );
		return;
	}
	// wsw : r1q2[end]

	Cmd_TokenizeString( s, qfalse );

	sv_player = sv_client->edict;

	for( u=ucmds ; u->name ; u++ ) {
		if( !strcmp(Cmd_Argv(0), u->name) )
		{
			u->func();
			break;
		}
	}

	// wsw : r1q2: don't pass commands to game before being fully connected
	if( sv_client->state < CS_SPAWNED )
		return;

	if( !u->name && sv.state == ss_game )
		ge->ClientCommand(sv_player);
}

/*
===========================================================================

USER CMD EXECUTION

===========================================================================
*/



void SV_ClientThink( client_t *cl, usercmd_t *cmd )
{
	cl->commandMsec -= cmd->msec;

	if( cl->commandMsec < 0 && sv_enforcetime->integer )
	{
		Com_Printf/*Com_DPrintf*/( "commandMsec underflow from %s\n", cl->name );
		return;
	}

	ge->ClientThink( cl->edict, cmd );
}

#ifdef BATTLEYE
void SV_ParseCLCBattleye( client_t *client, msg_t *msg ) {
	static qbyte tempdata[BE_MAX_PACKET_SIZE+4];
	short len;
	unsigned int	id;

	client->BE.acknowledgedPacket = MSG_ReadLong( msg );
	len = MSG_ReadShort( msg );
	if( len > 0 && len <= BE_MAX_PACKET_SIZE ) {
		MSG_ReadData( msg, tempdata+4, len );
		id = MSG_ReadLong( msg );
		if( id > client->BE.commandReceived ) {
			client->BE.commandReceived = id;
			// pass this packet to BE Master
			tempdata[0] = 0; // packet type
			tempdata[1] = client - svs.clients; // player id
			*(short*)(tempdata+2) = len;
			if( sv_battleye->integer && client->battleye ) // should never happen, but checking doesn't hurt
				SV_BE_SendToMaster( tempdata, len+4 );
		}
	}
	else if( len != 0 )
		SV_DropClient( client, DROP_TYPE_GENERAL, "Invalid BattlEye packet size" );
}
#endif


#define	MAX_STRINGCMDS	8
#define	MAX_USERINFO_UPDATES	8	// wsw : r1q2 : limit userinfo update per packet
/*
===================
SV_ExecuteClientMessage

The current message is parsed for the given client
===================
*/
void SV_ExecuteClientMessage( client_t *client, msg_t *msg )
{
	int		c;
	char	*s;
	usercmd_t	nullcmd;
	usercmd_t	oldest, oldcmd, newcmd;
	int		net_drop;
	int		checksum, calculatedChecksum;
	int		checksumIndex;
	qboolean	move_issued;
	int		lastframe;
	int		userinfoCount;	// wsw : r1q2

	if( !msg )
		return;

	sv_client = client;
	sv_player = sv_client->edict;

	// only allow one move command
	move_issued = qfalse;
	userinfoCount = 0;	// wsw : r1q2
	while( 1 )
	{
		if( msg->readcount > msg->cursize )
		{
			Com_Printf( "SV_ReadClientMessage: badread\n" );
			SV_DropClient( client, DROP_TYPE_GENERAL, "SV_ReadClientMessage: bad message from client" );
			return;
		}	

		c = MSG_ReadByte( msg );
		if( c == -1 )
			break;
				
		switch( c )
		{
		default:
			Com_Printf( "SV_ReadClientMessage: unknown command char\n" );
			SV_DropClient( client, DROP_TYPE_GENERAL, "SV_ReadClientMessage: unknown command char" );
			return;
						
		case clc_nop:
			break;

		case clc_move:
			{
				if( move_issued )
					return;		// someone is trying to cheat...
				
				move_issued = qtrue;
				checksumIndex = msg->readcount;
				checksum = MSG_ReadByte( msg );
				lastframe = MSG_ReadLong( msg );

				memset( &nullcmd, 0, sizeof(nullcmd) );
				MSG_ReadDeltaUsercmd( msg, &nullcmd, &oldest );
				MSG_ReadDeltaUsercmd( msg, &oldest, &oldcmd );
				MSG_ReadDeltaUsercmd( msg, &oldcmd, &newcmd );

				// calc ping
				if( lastframe != client->lastframe ) {
					client->lastframe = lastframe;
					if( client->lastframe > 0 ) {
						int timeonclient = newcmd.serverTimeStamp - client->frames[lastframe & UPDATE_MASK].sentTimeStamp;
						int roundtriprealtime = svs.realtime - client->frames[lastframe & UPDATE_MASK].sentTimeStamp;
						client->frame_latency[client->lastframe&(LATENCY_COUNTS-1)] = roundtriprealtime - timeonclient;
					}
				}
				
				if( client->state != CS_SPAWNED )
				{
					client->lastframe = -1;
					break;
				}
				
				// if the checksum fails, ignore the rest of the packet
				calculatedChecksum = COM_BlockSequenceCRCByte (
					msg->data + checksumIndex + 1,
					msg->readcount - checksumIndex - 1,
					client->netchan.incomingSequence );
				
				if( calculatedChecksum != checksum )
				{
					Com_DPrintf( "Failed command checksum for %s (%d != %d)/%d\n", 
						client->name, calculatedChecksum, checksum, 
						client->netchan.incomingSequence );
					return;
				}
				
				net_drop = client->netchan.dropped;
				if( net_drop < 20 )
				{
					while( net_drop > 2 )
					{
						SV_ClientThink( client, &client->lastcmd );
						
						net_drop--;
					}
					if( net_drop > 1 )
						SV_ClientThink( client, &oldest );
					
					if( net_drop > 0 )
						SV_ClientThink( client, &oldcmd );
					
				}
				SV_ClientThink( client, &newcmd );

				client->lastcmd = newcmd;
			}
			break;

		case clc_svcack:
			{
				int	cmdNum = MSG_ReadLong( msg );
				if( cmdNum < 0 ) {
					SV_DropClient( client, DROP_TYPE_GENERAL, "SV_ExecuteClientMessage: bad server command acknowledged" );
					return;
				}
				client->reliableAcknowledge = cmdNum;
			}
			break;
			
		case clc_clientcommand:
			{
				int	cmdNum;
				cmdNum = MSG_ReadLong( msg );
				if( cmdNum <= client->clientCommandExecuted ) {
					s = MSG_ReadString( msg ); // read but ignore
					continue;
				}
				client->clientCommandExecuted = cmdNum;
				s = MSG_ReadString( msg );
				SV_ExecuteUserCommand(s);
				if( client->state == CS_ZOMBIE )
					return;	// disconnect command
			}
			break;
#ifdef BATTLEYE
		case clc_battleye:
			SV_ParseCLCBattleye( client, msg );
			break;
#endif
		}
	}
}


