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

*/
// cl_main.c  -- client main loop

#include "client.h"
#include "../qcommon/webdownload.h"

cvar_t	*cl_freelook;

cvar_t	*cl_stereo_separation;
cvar_t	*cl_stereo;

cvar_t	*rcon_client_password;
cvar_t	*rcon_address;

cvar_t	*cl_timeout;
cvar_t	*cl_maxfps;
cvar_t	*cl_maxpackets;
cvar_t	*cl_compresspackets;
cvar_t	*cl_synchusercmd;
cvar_t	*cl_shownet;

cvar_t	*cl_timedemo;
cvar_t	*cl_demoavi_fps;
cvar_t	*cl_demoavi_scissor;

cvar_t	*lookspring;
cvar_t	*lookstrafe;
cvar_t	*sensitivity;
cvar_t  *m_accel;	// wsw : pb : add mouseaccel
cvar_t  *m_filter;
cvar_t	*in_minMsecs; // wsw : jal : millisecs to wait before asking the system for key/mouse input again

cvar_t	*m_pitch;
cvar_t	*m_yaw;
cvar_t	*m_forward;
cvar_t	*m_side;

//
// userinfo
//
cvar_t	*info_password;
cvar_t	*rate;
#ifdef BATTLEYE
cvar_t	*cl_battleye;
#endif

cvar_t	*cl_masterservers;

// wsw : debug netcode
cvar_t	*cl_debug_serverCmd;

cvar_t	*cl_downloads;
cvar_t	*cl_downloads_from_web;

client_static_t	cls;
client_state_t	cl;

entity_state_t	cl_baselines[MAX_EDICTS];

void CL_RestartMedia( void );
//======================================================================


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

CLIENT RELIABLE COMMAND COMMUNICATION

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

/*
======================
CL_AddReliableCommand

The given command will be transmitted to the server, and is gauranteed to
not have future usercmd_t executed before it is executed
======================
*/
void CL_AddReliableCommand( /*const*/ char *cmd ) {
	int		index;

	if( !cmd || !strlen(cmd) )
		return;

	// if we would be losing an old command that hasn't been acknowledged,
	// we must drop the connection
	if ( cls.reliableSequence - cls.reliableAcknowledge > MAX_RELIABLE_COMMANDS ) {
		Com_Error( ERR_DROP, "Client command overflow" );
	}
	cls.reliableSequence++;
	index = cls.reliableSequence & ( MAX_RELIABLE_COMMANDS - 1 );
	Q_strncpyz( cls.reliableCommands[ index ], cmd, sizeof( cls.reliableCommands[ index ] ) );
}

/*
======================
CL_UpdateClientCommandsToServer

Add the pending commands to the message
======================
*/
void CL_UpdateClientCommandsToServer( msg_t *msg ) {
	int		i;

	//if( cl_debug_serverCmd->integer & 2 )
	//	Com_Printf( "cls.reliableAcknowledge: %i cls.reliableSequence:%i\n", cls.reliableAcknowledge,cls.reliableSequence );

	// write any unacknowledged clientCommands
	for ( i = cls.reliableAcknowledge + 1 ; i <= cls.reliableSequence ; i++ ) {
		if( !strlen( cls.reliableCommands[ i & (MAX_RELIABLE_COMMANDS-1) ] ) ) 
			continue;

		MSG_WriteByte( msg, clc_clientcommand );
		MSG_WriteLong( msg, i );
		MSG_WriteString( msg, cls.reliableCommands[ i & (MAX_RELIABLE_COMMANDS-1) ] );

		if( cl_debug_serverCmd->integer & 2 && cls.state < CA_ACTIVE )
			Com_Printf( "CL_UpdateClientCommandsToServer(%i): %s\n", i, cls.reliableCommands[ i & (MAX_RELIABLE_COMMANDS-1) ] );
	}

	cls.reliableSent = cls.reliableSequence;
}

#ifdef BATTLEYE
void CL_AddBattleyeCommandToMessage( msg_t *msg ) {
	qbyte	*bepacket = NULL;
	int		 bepacketlen = 0;

	if( cl_battleye->integer ) {
		// the server is not acknowledging packets
		if( cls.BE.acknowledgedPacket + BE_UPDATE_BACKUP <= cls.BE.headPacket ) {
			Com_Error( ERR_DROP, "Missing server acknowledge of too many BattlEye packets" );
			return;
		}

		if( msg ) {
			if( cls.BE.lastSentTime > cls.realtime ) // clamp 
				cls.BE.lastSentTime = cls.realtime;

			if( cls.BE.lastSentTime + BE_MIN_RESEND < cls.realtime ) {
				// send the next buffered unacknowledged packet (if any)
				if( cls.BE.headPacket > cls.BE.acknowledgedPacket ) {
					bepacketlen = cls.BE.packetLens[cls.BE.acknowledgedPacket & BE_UPDATE_MASK];
					if( bepacketlen ) {
						bepacket = cls.BE.packets[cls.BE.acknowledgedPacket & BE_UPDATE_MASK];
					}
				}
			}

			MSG_WriteByte( msg, clc_battleye );
			//write svc_battleye acknowledge
			MSG_WriteLong( msg, cls.BE.commandReceived );
			if( bepacket ) {
				MSG_WriteShort( msg, bepacketlen );
				MSG_Write( msg, bepacket, bepacketlen );
				MSG_WriteLong( msg, cls.BE.acknowledgedPacket+1 ); // id of this BE packet
				cls.BE.lastSentTime = cls.realtime;
			} else {
				MSG_WriteShort( msg, 0 ); // no data, just acknowledge of svc_battleye
			}
		}
	}
}
#endif

/*
===================
Cmd_ForwardToServer

adds the current command line as a command to the client message.
things like godmode, noclip, etc, are commands directed to the server,
so when they are typed in at the console, they will need to be forwarded.
===================
*/
void Cmd_ForwardToServer( void )
{
	char	*cmd;

	if( cls.demoplaying )
		return;

	cmd = Cmd_Argv(0);
	if( cls.state <= CA_CONNECTED || *cmd == '-' || *cmd == '+' )
	{
		Com_Printf( "Unknown command \"%s\"\n", cmd );
		return;
	}

	CL_AddReliableCommand( va("%s %s\n", cmd, Cmd_Args()) );
}

/*
==================
CL_ForwardToServer_f
==================
*/
void CL_ForwardToServer_f( void )
{
	if( cls.demoplaying )
		return;

	if( cls.state != CA_CONNECTED && cls.state != CA_ACTIVE )
	{
		Com_Printf( "Can't \"%s\", not connected\n", Cmd_Argv(0) );
		return;
	}

	// don't forward the first argument
	if( Cmd_Argc() > 1 )
	{
		CL_AddReliableCommand( Cmd_Args() );
	}
}

/*
==================
CL_ServerDisconnect_f
==================
*/
void CL_ServerDisconnect_f( void )
{
	char menuparms[MAX_STRING_CHARS];
	int type;
	char reason[MAX_STRING_CHARS];

	type = atoi(Cmd_Argv(1));
	if( type < 0 || type >= DROP_TYPE_TOTAL ) type = DROP_TYPE_GENERAL;

	Q_strncpyz( reason, Cmd_Argv(2), sizeof(reason) );

	CL_Disconnect_f();

	Com_Printf( "Connection was closed by server: %s\n", reason );

	Q_snprintfz( menuparms, sizeof(menuparms), "menu_failed 1 \"%s\" %i \"%s\"", cls.servername, type, reason );

	Cbuf_ExecuteText( EXEC_NOW, menuparms );
}

/*
==================
CL_Quit
==================
*/
void CL_Quit( void )
{
	CL_Disconnect( NULL );
	Com_Quit();
}

/*
==================
CL_Quit_f
==================
*/
void CL_Quit_f( void )
{
	CL_Quit();
}

/*
=======================
CL_SendConnectPacket

We have gotten a challenge from the server, so try and
connect.
======================
*/
void CL_SendConnectPacket( void )
{
	cls.quakePort = Cvar_VariableValue( "qport" );
	userinfo_modified = qfalse;

	Netchan_OutOfBandPrint( NS_CLIENT, cls.serveraddress, "connect %i %i %i \"%s\"\n",
		PROTOCOL_VERSION, cls.quakePort, cls.challenge, Cvar_Userinfo() );
}

/*
=================
CL_CheckForResend

Resend a connect message if the last one has timed out
=================
*/
void CL_CheckForResend( void )
{
	if( cls.demoplaying )
		return;

	// if the local server is running and we aren't then connect
	if( cls.state == CA_DISCONNECTED && Com_ServerState() )
	{
		CL_SetClientState( CA_CONNECTING );
		Q_strncpyz( cls.servername, "localhost", sizeof(cls.servername) );
		NET_StringToAdr( "localhost", &cls.serveraddress );
		if( cls.serveraddress.port == 0 )
			cls.serveraddress.port = BigShort( PORT_SERVER );
	}

	// resend if we haven't gotten a reply yet
	if( cls.state != CA_CONNECTING )
		return;
	
	if( curtime - cls.connect_time < 3000 )
		return;

	cls.connect_count++;
	cls.connect_time = curtime;	// for retransmit requests

	Com_Printf( "Connecting to %s...\n", cls.servername );

	Netchan_OutOfBandPrint( NS_CLIENT, cls.serveraddress, "getchallenge\n" );
}


/*
================
CL_Connect_f

================
*/
void CL_Connect_f( void )
{
	netadr_t serveraddress; // save of address before calling CL_Disconnect

	if( Cmd_Argc() != 2 )
	{
		Com_Printf( "usage: connect <server>\n" );
		return;	
	}

	// do it there cause CL_Disconnect destroy Argv[1]
	// and then we loose the address
	if( !NET_StringToAdr(Cmd_Argv(1), &serveraddress) )
	{
		Com_Printf( "Bad server address\n" );
		return;
	}
	// set it now for connection ui
	cls.serveraddress=serveraddress;
	Q_strncpyz( cls.servername, Cmd_Argv (1), sizeof(cls.servername) );

	if( Com_ServerState () )
	{	// if running a local server, kill it and reissue
		SV_Shutdown( "Server quit\n", qfalse );
	}
	CL_Disconnect( NULL );

	NET_Config( qtrue );		// allow remote

	// maybe not needed
	// set it in case of disconnect overwriting it
	cls.serveraddress=serveraddress;

	if( cls.serveraddress.port == 0 )
		cls.serveraddress.port = BigShort( PORT_SERVER );

	memset( cl.configstrings, 0, sizeof(cl.configstrings) );
	// moved upper due to CL_Disconnect overwriting Cmd_Argv(1)
	//Q_strncpyz( cls.servername, Cmd_Argv (1), sizeof(cls.servername) );
	CL_SetClientState( CA_CONNECTING );
	cls.connect_time = -99999;	// CL_CheckForResend() will fire immediately
	cls.connect_count = 0;
	cls.rejected = qfalse;
	cls.lastPacketReceivedTime = cls.realtime; // reset the timeout limit
}


/*
=====================
CL_Rcon_f

Send the rest of the command line over as
an unconnected command.
=====================
*/
void CL_Rcon_f( void )
{
	char	message[1024];
	int		i;
	netadr_t	to;

	if( cls.demoplaying )
		return;

	if( strlen(rcon_client_password->string) == 0 )
	{
		Com_Printf( "You must set 'rcon_password' before issuing an rcon command.\n" );
		return;
	}

	// wsw : jal : check for msg len abuse (thx to r1Q2)
	if( strlen(Cmd_Args()) + strlen(rcon_client_password->string) + 16 >= sizeof(message) ) 
	{
		Com_Printf( "Length of password + command exceeds maximum allowed length.\n" );
		return;
	}

	message[0] = (qbyte)255;
	message[1] = (qbyte)255;
	message[2] = (qbyte)255;
	message[3] = (qbyte)255;
	message[4] = 0;

	NET_Config( qtrue );		// allow remote

	Q_strncatz( message, "rcon ", sizeof(message) );

	Q_strncatz( message, rcon_client_password->string, sizeof(message) );
	Q_strncatz( message, " ", sizeof(message) );

	for( i = 1; i < Cmd_Argc(); i++ )
	{
		Q_strncatz( message, Cmd_Argv(i), sizeof(message) );
		Q_strncatz( message, " ", sizeof(message) );
	}

	if( cls.state >= CA_CONNECTED )
		to = cls.netchan.remoteAddress;
	else
	{
		if( !strlen(rcon_address->string) )
		{
			Com_Printf( "You must be connected, or set the 'rcon_address' cvar to issue rcon commands\n" );
			return;
		}

		if( rcon_address->modified )
		{
			if( !NET_StringToAdr( rcon_address->string, &cls.rconaddress ) )
			{
				Com_Printf("Bad rcon_address.\n");
				return; // we don't clear modified, so it will whine the next time too
			}
			if( cls.rconaddress.port == 0 )
				cls.rconaddress.port = BigShort( PORT_SERVER );

			rcon_address->modified = qfalse;
		}

		to = cls.rconaddress;
	}

	NET_SendPacket( NS_CLIENT, (int)strlen(message)+1, message, to );
}

/*
=====================
CL_GetClipboardData
=====================
*/
void CL_GetClipboardData( char *string, int size )
{
	char *cbd;

	if( !string || size <= 0 )
		return;

	string[0] = 0;
	cbd = Sys_GetClipboardData();
	if( cbd && cbd[0] )
	{
		Q_strncpyz ( string, cbd, size );
		//Q_free ( cbd );
	}
}

/*
=====================
CL_SetKeyDest
=====================
*/
void CL_SetKeyDest( int key_dest )
{
	if( key_dest < key_game || key_dest > key_menu )
		Com_Error( ERR_DROP, "CL_SetKeyDest: invalid key_dest" );

	if( cls.key_dest != key_dest )
	{
		Key_ClearStates();
		cls.key_dest = key_dest;
	}
}


/*
=====================
CL_SetOldKeyDest
=====================
*/
void CL_SetOldKeyDest( int key_dest )
{
	if( key_dest < key_game || key_dest > key_menu )
		Com_Error( ERR_DROP, "CL_SetKeyDest: invalid key_dest" );
	cls.old_key_dest = key_dest;
}

/*
=====================
CL_ResetServerCount
=====================
*/
void CL_ResetServerCount( void )
{
	cl.servercount = -1;
}

/*
=====================
CL_ClearState

=====================
*/
void CL_ClearState( void )
{
// wipe the entire cl structure
	memset( &cl, 0, sizeof(cl) );
	memset( cl_baselines, 0, sizeof(cl_baselines) );

	//userinfo_modified = qtrue;
	cls.lastExecutedServerCommand = 0;
	cls.reliableAcknowledge = 0;
	cls.reliableSequence = 0;
	cls.reliableSent = 0;
	memset( &cls.reliableCommands, 0, sizeof(cls.reliableCommands) );
#ifdef BATTLEYE
	memset( &cls.BE, 0, sizeof(cls.BE) );
#endif

	//restart realtime and lastPacket times
	cls.realtime = 0;
	cls.lastPacketSentTime = 0;
	cls.lastPacketReceivedTime = 0;
}


//=====================
//CL_SetNext_f
//
//Next is used to set an action which is executed at disconnecting.
//=====================
char cl_NextString[MAX_STRING_CHARS];
void CL_SetNext_f( void ) 
{
	if( Cmd_Argc() < 2 ) {
		Com_Printf( "USAGE: next <commands>\n" );
		return;
	}

	// jalfixme: I'm afraid of this being too powerful, since it basically
	// is allowed to execute everyting. Shall we check for something?
	Q_strncpyz( cl_NextString, Cmd_Args(), sizeof(cl_NextString) );
	Com_Printf( "NEXT: %s\n", cl_NextString );
}


//=====================
//CL_ExecuteNext
//=====================
void CL_ExecuteNext( void ) 
{
	if( !strlen(cl_NextString) )
		return;

	Cbuf_ExecuteText( EXEC_APPEND, cl_NextString );
	memset( cl_NextString, 0, sizeof(cl_NextString) );
}

/*
=====================
CL_Disconnect_SendCommand

Sends a disconnect message to the server
=====================
*/
void CL_Disconnect_SendCommand( void )
{
	// wsw : jal : send the packet 3 times to make sure isn't lost
	CL_AddReliableCommand( "disconnect" );
	CL_WritePacket( NULL );
	CL_AddReliableCommand( "disconnect" );
	CL_WritePacket( NULL );
	CL_AddReliableCommand( "disconnect" );
	CL_WritePacket( NULL );
}

/*
=====================
CL_Disconnect

Goes from a connected state to full screen console state
Sends a disconnect message to the server
This is also called on Com_Error, so it shouldn't cause any errors
=====================
*/
void CL_Disconnect( const char *message )
{
	char menuparms[MAX_STRING_CHARS];
	qboolean wasconnecting;

	// We have to shut down webdownloading first
	if( cls.download.web ) {
		cls.download.disconnect = qtrue;
		return;
	}

	if( cls.state == CA_UNINITIALIZED )
		return;
	if( cls.state == CA_DISCONNECTED )
		goto done;

	if( cls.state < CA_LOADING )
		wasconnecting = qtrue;
	else
		wasconnecting = qfalse;

	if( cl_timedemo && cl_timedemo->integer )
	{
		unsigned int	time;
		int		i;

		Com_Printf("\n");
		for( i = 1; i < 100; i++ ) {
			if( cl.timedemo_counts[i] > 0 )
			{
				Com_Printf("%2ims - %7.2ffps: %6.2f%c\n", i, 1000.0/i,
					(cl.timedemo_counts[i]*1.0/cl.timedemo_frames)*100.0, '%');
			}
		}

		Com_Printf("\n");
		time = Sys_Milliseconds() - cl.timedemo_start;
		if( time > 0 )
			Com_Printf( "%i frames, %3.1f seconds: %3.1f fps\n", cl.timedemo_frames,
			time/1000.0, cl.timedemo_frames*1000.0 / time );
	}

	cls.connect_time = 0;
	cls.connect_count = 0;
	cls.rejected = 0;

	SCR_StopCinematic();

	if( cls.demorecording )
		CL_Stop_f();

	if( cls.demoplaying ) {
		CL_DemoCompleted();
	} else {
		CL_Disconnect_SendCommand(); // send a disconnect message to the server
	}

	CL_RestartMedia();

	if( cls.download.filenum )
		CL_StopServerDownload();

	CL_ClearState();
	CL_SetClientState( CA_DISCONNECTED );

	if( message != NULL ) {
		Q_snprintfz( menuparms, sizeof(menuparms), "menu_failed %i \"%s\" %i \"%s\"", (wasconnecting ? 0 : 2 ),
				cls.servername, DROP_TYPE_GENERAL, message );

		Cbuf_ExecuteText( EXEC_NOW, menuparms );
	}

done:
	// drop loading plaque unless this is the initial game start
	if( cls.disable_servercount != -1 )
		SCR_EndLoadingPlaque();	// get rid of loading plaque

	// in case we disconnect while in download phase
	CL_FreeDownloadList();

#ifdef BATTLEYE
	if( cls.runBattlEye )
		cls.runBattlEye = qfalse;
#endif

	CL_ExecuteNext(); // start next action if any is defined
}

void CL_Disconnect_f( void )
{
	// We have to shut down webdownloading first
	if( cls.download.web ) {
		cls.download.disconnect = qtrue;
		return;
	}

	CL_Disconnect( NULL );
	SV_Shutdown( "Owner left the listen server", qfalse );
}

/*
=================
CL_Changing_f

Just sent as a hint to the client that they should
drop to full console
=================
*/
void CL_Changing_f( void )
{
	//ZOID
	//if we are downloading, we don't change!  This so we don't suddenly stop downloading a map
	if( cls.download.filenum || cls.download.web )
		return;

	Com_Printf( "CL:Changing\n" );

	memset( cl.configstrings, 0, sizeof(cl.configstrings) );
	CL_SetClientState( CA_CONNECTED );	// not active anymore, but not disconnected
}


/*
=================
CL_Reconnect_f

The server is changing levels
=================
*/
void CL_Reconnect_f( void )
{
	if( cls.demoplaying )
		return;

	//if we are downloading, we don't change!  This so we don't suddenly stop downloading a map
	if( cls.download.filenum || cls.download.web )
		return;

	cls.connect_count = 0;
	cls.rejected = 0;

	S_StopAllSounds ();
	if( cls.state == CA_CONNECTED ) {
		Com_Printf( "reconnecting...\n" );
		memset( cl.configstrings, 0, sizeof(cl.configstrings) );
		CL_SetClientState( CA_CONNECTED );
		CL_AddReliableCommand( "new" );
		return;
	}

	if( *cls.servername ) {
		if( cls.state >= CA_CONNECTED ) {
			CL_Disconnect( NULL );
			cls.connect_time = curtime - 1500;
		} else
			cls.connect_time = -99999; // fire immediately
		Com_Printf( "reconnecting...\n" );
	}

	CL_SetClientState( CA_CONNECTING );
}

/*
=================
CL_ConnectionlessPacket

Responses to broadcasts, etc
=================
*/
void CL_ConnectionlessPacket( msg_t *msg )
{
	char	*s;
	char	*c;
	
	MSG_BeginReading( msg );
	MSG_ReadLong( msg );	// skip the -1

	s = MSG_ReadStringLine( msg );

	if( !strncmp(s, "getserversResponse\\", 19) )
	{
		Com_Printf( "%s: %s\n", NET_AdrToString(&net_from), "getserversResponse" );
		CL_ParseGetServersResponse( msg );
		return;
	}

	Cmd_TokenizeString( s, qfalse );
	c = Cmd_Argv(0);

	Com_Printf( "%s: %s\n", NET_AdrToString(&net_from), s );

	// server connection
	if( !strcmp(c, "client_connect") )
	{
		if( cls.state == CA_CONNECTED )
		{
			Com_Printf( "Dup connect received.  Ignored.\n" );
			return;
		}
		// these two are from Q3
		if( cls.state != CA_CONNECTING )
		{
			Com_Printf ("client_connect packet while not connecting.  Ignored.\n");
			return;
		}
		if ( !NET_CompareBaseAdr( &net_from, &cls.serveraddress ) ) {
			Com_Printf( "client_connect from a different address.  Ignored.\n" );
			Com_Printf( "Was %s should have been %s\n", NET_AdrToString(&net_from),
					NET_AdrToString(&cls.serveraddress) );
			return;
		}

		cls.rejected = qfalse;

		Netchan_Setup( NS_CLIENT, &cls.netchan, net_from, cls.quakePort );
		CL_AddReliableCommand( "new" );
		memset( cl.configstrings, 0, sizeof(cl.configstrings) );
		CL_SetClientState( CA_CONNECTED );
		return;
	}

	// reject packet, used to inform the client that connection attemp didn't succeed
	if( !strcmp(c, "reject") )
	{
		int			rejectflag;

		if( cls.state != CA_CONNECTING )
		{
			Com_Printf ("reject packet while not connecting.  Ignored.\n");
			return;
		}
		if ( !NET_CompareBaseAdr( &net_from, &cls.serveraddress ) ) {
			Com_Printf( "reject from a different address.  Ignored.\n" );
			Com_Printf( "Was %s should have been %s\n", NET_AdrToString(&net_from),
					NET_AdrToString(&cls.serveraddress) );
			return;
		}

		cls.rejected = qtrue;

		cls.rejecttype = atoi(MSG_ReadStringLine( msg ));
		if( cls.rejecttype < 0 || cls.rejecttype >= DROP_TYPE_TOTAL ) cls.rejecttype = DROP_TYPE_GENERAL;

		rejectflag = atoi(MSG_ReadStringLine( msg ));

		Q_strncpyz(cls.rejectmessage, MSG_ReadStringLine( msg ), sizeof(cls.rejectmessage));
		if( strlen(cls.rejectmessage) > sizeof(cls.rejectmessage)-2 ) {
			cls.rejectmessage[strlen(cls.rejectmessage)-2] = '.';
			cls.rejectmessage[strlen(cls.rejectmessage)-1] = '.';
			cls.rejectmessage[strlen(cls.rejectmessage)] = '.';
		}

		Com_Printf( "Connection refused: %s\n", cls.rejectmessage);
		if( rejectflag & DROP_FLAG_AUTORECONNECT ) {
			Com_Printf( "Automatic reconnecting allowed.\n");
		} else {
			char menuparms[MAX_STRING_CHARS];

			Com_Printf( "Automatic reconnecting not allowed.\n");

			CL_Disconnect( NULL );
			Q_snprintfz( menuparms, sizeof(menuparms), "menu_failed 0 \"%s\" %i \"%s\"",
				cls.servername, cls.rejecttype, cls.rejectmessage );

			Cbuf_ExecuteText( EXEC_NOW, menuparms );
		}

		return;
	}

	// server responding to a status broadcast
	if( !strcmp(c, "info") )
	{
		CL_ParseStatusMessage( msg );
		return;
	}

	// remote command from gui front end
	if( !strcmp(c, "cmd") )
	{
		if( !NET_IsLocalAddress(&net_from) )
		{
			Com_Printf( "Command packet from remote host.  Ignored.\n" );
			return;
		}
		Sys_AppActivate();
		s = MSG_ReadString( msg );
		Cbuf_AddText( s );
		Cbuf_AddText( "\n" );
		return;
	}
	// print command from somewhere
	if( !strcmp(c, "print") )
	{
		// CA_CONNECTING is allowed, because old servers send protocol mismatch connection error message with it
		if( ((cls.state != CA_UNINITIALIZED && cls.state != CA_DISCONNECTED) &&
				NET_CompareBaseAdr( &net_from, &cls.serveraddress )) ||
				(strlen(rcon_address->string) > 0 && NET_CompareBaseAdr( &net_from, &cls.rconaddress )) )
		{
			s = MSG_ReadString( msg );
			Com_Printf( "%s", s );
			return;
		}
		else
		{
			Com_Printf( "Print packet from unknown host.  Ignored.\n" );
			return;
		}
	}

	// ping from somewhere
	if( !strcmp(c, "ping") )
	{
		Netchan_OutOfBandPrint( NS_CLIENT, net_from, "ack" );
		return;
	}

	// challenge from the server we are connecting to
	if( !strcmp(c, "challenge") )
	{
		// these two are from Q3
		if( cls.state != CA_CONNECTING )
		{
			Com_Printf ("challenge packet while not connecting.  Ignored.\n");
			return;
		}
		if ( !NET_CompareBaseAdr( &net_from, &cls.serveraddress ) ) {
			Com_Printf( "challenge from a different address.  Ignored.\n" );
			Com_Printf( "Was %s should have been %s\n", NET_AdrToString(&net_from),
					NET_AdrToString(&cls.serveraddress) );
			return;
		}

		cls.challenge = atoi( Cmd_Argv(1) );
		//wsw : r1q2[start]
		//r1: reset the timer so we don't send dup. getchallenges
		cls.connect_time = curtime;
		//wsw : r1q2[end]
		CL_SendConnectPacket();
		return;
	}

	// echo request from server
	if( !strcmp(c, "echo") )
	{
		Netchan_OutOfBandPrint( NS_CLIENT, net_from, "%s", Cmd_Argv(1) );
		return;
	}

	// jal : wsw
	// server responding to a detailed info broadcast
	if( !strcmp(c, "infoResponse") )
	{
		CL_ParseGetInfoResponse( msg );
		return;
	}

	Com_Printf( "Unknown command.\n" );
}

//=================
// CL_ProcessPacket
//=================
qboolean CL_ProcessPacket( netchan_t *netchan, msg_t *msg ) {
	if( !Netchan_Process( netchan, msg ) )
		return qfalse;		// wasn't accepted for some reason

	{
		int			sequence, sequence_ack;
		int			zerror;

		// now if compressed, expand it
		MSG_BeginReading( msg );
		sequence = MSG_ReadLong( msg );
		sequence_ack = MSG_ReadLong( msg ); 
		if( msg->compressed ) {
			zerror = Netchan_DecompressMessage( msg );
			if( zerror < 0 ) { // compression error. Drop the packet
				Com_DPrintf( "SV_ProcessPacket: Compression error %i. Dropping packet\n", zerror );
				return qfalse;
			}
		}
	}

	return qtrue;
}

/*
=================
CL_ReadPackets
=================
*/
void CL_ReadPackets( void )
{
	static msg_t		msg;
	static qbyte		msgData[MAX_MSGLEN];

	MSG_Init( &msg, msgData, sizeof(msgData) );
	MSG_Clear( &msg );

	while( NET_GetPacket(NS_CLIENT, &net_from, &msg) )
	{
		//
		// remote command packet
		//
		if( *(int *)msg.data == -1 )
		{
			CL_ConnectionlessPacket( &msg );
			continue;
		}

		if( cls.state == CA_DISCONNECTED || cls.state == CA_CONNECTING )
			continue;		// dump it if not connected

		if( msg.cursize < 8 )
		{
			//wsw : r1q2[start]
			//r1: delegated to DPrintf (someone could spam your console with crap otherwise)
			Com_DPrintf( "%s: Runt packet\n", NET_AdrToString(&net_from) );
			//wsw : r1q2[end]
			continue;
		}

		//
		// packet from server
		//
		if( !NET_CompareAdr(&net_from, &cls.netchan.remoteAddress) )
		{
			Com_DPrintf ("%s:sequenced packet without connection\n"
				,NET_AdrToString(&net_from) );
			continue;
		}
		if( !CL_ProcessPacket(&cls.netchan, &msg) )
			continue;		// wasn't accepted for some reason

		CL_ParseServerMessage( &msg );
		cls.lastPacketReceivedTime = cls.realtime;
	}

	// not expected, but could happen if svs.realtime is cleared and lastPacketReceivedTime is not
	if( cls.lastPacketReceivedTime > cls.realtime )
		cls.lastPacketReceivedTime = cls.realtime;

	// check timeout
	if( cls.state >= CA_CONNECTED && !cl.cin.time && cls.lastPacketReceivedTime ) {
		if( cls.lastPacketReceivedTime + cl_timeout->value*1000 < cls.realtime )
		{
			if( ++cl.timeoutcount > 5 )	// timeoutcount saves debugger
			{
				Com_Printf( "\nServer connection timed out.\n" );
				CL_Disconnect( "Connection timed out" );
				return;
			}
		}
	}
	else
		cl.timeoutcount = 0;
	
}

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

/*
==============
CL_Userinfo_f
==============
*/
void CL_Userinfo_f( void )
{
	Com_Printf( "User info settings:\n" );
	Info_Print( Cvar_Userinfo() );
}

int precache_check; // for autodownload of precache items
int precache_spawncount;
int precache_tex;

#define PLAYER_MULT 5

// ENV_CNT is map load
#define ENV_CNT (CS_PLAYERINFOS + MAX_CLIENTS * PLAYER_MULT)
#define TEXTURE_CNT (ENV_CNT+1)

void CL_RequestNextDownload( void )
{
	char fn[MAX_OSPATH];

	if( cls.state != CA_CONNECTED )
		return;

	if( !cl_downloads->integer && precache_check < ENV_CNT )
		precache_check = ENV_CNT;

//ZOID
	if( precache_check == CS_MODELS ) { // confirm map
		precache_check = CS_MODELS+2; // 0 isn't used
		if( !CL_CheckOrDownloadFile(cl.configstrings[CS_MODELS+1]) )
			return; // started a download
	}

	if( precache_check >= CS_MODELS && precache_check < CS_MODELS+MAX_MODELS ) {
		while( precache_check < CS_MODELS+MAX_MODELS &&
			cl.configstrings[precache_check][0] ) {
			if (cl.configstrings[precache_check][0] == '*' ||
				cl.configstrings[precache_check][0] == '$' || // disable playermodel downloading for now
				cl.configstrings[precache_check][0] == '#') {
				precache_check++;
				continue;
			}

			if( !CL_CheckOrDownloadFile(cl.configstrings[precache_check++]) ) {
				return; // started a download
			}
		}
		precache_check = CS_SOUNDS;
	}

	if( precache_check >= CS_SOUNDS && precache_check < CS_SOUNDS+MAX_SOUNDS ) { 
		if( precache_check == CS_SOUNDS )
			precache_check++; // zero is blank
		while( precache_check < CS_SOUNDS+MAX_SOUNDS &&
			cl.configstrings[precache_check][0] ) {
			if( cl.configstrings[precache_check][0] == '*' ) {
				precache_check++;
				continue;
			}

			Q_strncpyz( fn, cl.configstrings[precache_check++], sizeof(fn) );
			COM_DefaultExtension( fn, ".wav", sizeof(fn) );
			if( !CL_CheckOrDownloadFile(fn) )
				return; // started a download
		}
		precache_check = CS_IMAGES;
	}
	if( precache_check >= CS_IMAGES && precache_check < CS_IMAGES+MAX_IMAGES ) {
		if( precache_check == CS_IMAGES )
			precache_check++; // zero is blank
#if 1
		// precache phase completed
		precache_check = ENV_CNT;
	}
#else
		precache_check = CS_PLAYERINFOS;
	}
	// skins are special, since a player has three things to download:
	// model, weapon model and skin
	// so precache_check is now *3
	if( precache_check >= CS_PLAYERINFOS && precache_check < CS_PLAYERINFOS + MAX_CLIENTS * PLAYER_MULT ) 
	{
		while( precache_check < CS_PLAYERINFOS + MAX_CLIENTS * PLAYER_MULT ) 
		{
			int i, n;
			//char model[MAX_QPATH], skin[MAX_QPATH], *p;
			i = (precache_check - CS_PLAYERINFOS)/PLAYER_MULT;
			n = (precache_check - CS_PLAYERINFOS)%PLAYER_MULT;

			// Vic: disabled for now
#if 1
			precache_check = CS_PLAYERINFOS + (i + 1) * PLAYER_MULT;
#else
			if( !cl.configstrings[CS_PLAYERINFOS+i][0] ) {
				precache_check = CS_PLAYERINFOS + (i + 1) * PLAYER_MULT;
				continue;
			}

			if( (p = strchr( cl.configstrings[CS_PLAYERINFOS+i], '\\') ) != NULL )
			{
				p++;
				Q_strncpyz(model, p, sizeof(model));
				if( ( p = strchr(model, '\\') ) != NULL )
					p++;
			}
			else
				p = cl.configstrings[CS_PLAYERINFOS+i];

			Q_strncpyz(model, p, sizeof(model));
			p = strchr(model, '/');
			if (!p)
				p = strchr( model, '\\' );
			if (p) {
				*p++ = 0;
				Q_strncpyz( skin, p, sizeof(skin) );
			} else
				*skin = 0;

			switch (n) {
			case 0: // model
				Q_snprintfz( fn, sizeof(fn), "players/%s/tris.md2", model );
				if( !CL_CheckOrDownloadFile(fn) ) {
					precache_check = CS_PLAYERINFOS + i * PLAYER_MULT + 1;
					return; // started a download
				}
				n++;
				/*FALL THROUGH*/

			case 1: // weapon model
				Q_snprintfz( fn, sizeof(fn), "players/%s/weapon.md2", model );
				if( !CL_CheckOrDownloadFile(fn) ) {
					precache_check = CS_PLAYERINFOS + i * PLAYER_MULT + 2;
					return; // started a download
				}
				n++;
				/*FALL THROUGH*/

			case 2: // weapon skin
				Q_snprintfz( fn, sizeof(fn), "players/%s/weapon.pcx", model );
				if( !CL_CheckOrDownloadFile(fn) ) {
					precache_check = CS_PLAYERINFOS + i * PLAYER_MULT + 3;
					return; // started a download
				}
				n++;
				/*FALL THROUGH*/

			case 3: // skin
				Q_snprintfz( fn, sizeof(fn), "players/%s/%s.pcx", model, skin );
				if( !CL_CheckOrDownloadFile(fn) ) {
					precache_check = CS_PLAYERINFOS + i * PLAYER_MULT + 4;
					return; // started a download
				}
				n++;
				/*FALL THROUGH*/

			case 4: // skin_i
				Q_snprintfz( fn, sizeof(fn), "players/%s/%s_i.pcx", model, skin );
				if( !CL_CheckOrDownloadFile(fn) ) {
					precache_check = CS_PLAYERINFOS + i * PLAYER_MULT + 5;
					return; // started a download
				}
				// move on to next model
				precache_check = CS_PLAYERINFOS + (i + 1) * PLAYER_MULT;
			}
#endif // 1
		}
		// precache phase completed
		precache_check = ENV_CNT;
	}
#endif // 1
	if( precache_check == ENV_CNT ) {
		unsigned map_checksum;

		// we're done with the download phase, so clear the list
		CL_FreeDownloadList();

		// check memory integrity
		Mem_CheckSentinelsGlobal();

		CM_LoadMap( cl.configstrings[CS_MODELS+1], qtrue, &map_checksum );

		// check memory integrity
		Mem_CheckSentinelsGlobal();

		if( map_checksum != (unsigned)atoi(cl.configstrings[CS_MAPCHECKSUM]) ) {
			Com_Error( ERR_DROP, "Local map version differs from server: %i != '%s'",
				map_checksum, cl.configstrings[CS_MAPCHECKSUM] );
			return;
		}

		precache_check = TEXTURE_CNT;
	}

	if( precache_check == TEXTURE_CNT ) {
		precache_check = TEXTURE_CNT+1;
		precache_tex = 0;
	}

	// confirm existance of textures, download any that don't exist
	if( precache_check == TEXTURE_CNT+1 ) {
		precache_check = TEXTURE_CNT+999;
	}

//ZOID

	// load client game module
	CL_GameModule_Init();
	CL_AddReliableCommand( va("begin %i\n", precache_spawncount) );
}

/*
=================
CL_Precache_f

The server will send this command right
before allowing the client into the server
=================
*/
void CL_Precache_f( void )
{
	if( Cmd_Argc() < 2 )
	{	// demo playback
		unsigned map_checksum;

		// check memory integrity
		Mem_CheckSentinelsGlobal();

		CM_LoadMap( cl.configstrings[CS_MODELS+1], qtrue, &map_checksum );

		// check memory integrity
		Mem_CheckSentinelsGlobal();

		CL_GameModule_Init();

		return;
	}

	precache_check = CS_MODELS;
	precache_spawncount = atoi( Cmd_Argv(1) );

	CL_RequestNextDownload();
}

/*
===============
CL_WriteConfiguration

Writes key bindings, archived cvars and aliases to a config file
===============
*/
static void CL_WriteConfiguration( const char *name, qboolean warn )
{
	int file;

	if( FS_FOpenFile( name, &file, FS_WRITE ) == -1 ) {
		Com_Printf( "Couldn't write %s.\n", name );
		return;
	}

	if( warn ) {
		FS_Printf( file, "// This file is automatically generated by Warsow, do not modify.\n\n" );
	}

	FS_Printf( file, "// key bindings\n" );
	Key_WriteBindings( file );

	FS_Printf( file, "\n// variables\n" );
	Cvar_WriteVariables( file );

	FS_Printf( file, "\n// aliases\n" );
	Cmd_WriteAliases( file );

	FS_FCloseFile( file );
}


/*
===============
CL_WriteConfig_f
===============
*/
void CL_WriteConfig_f( void )
{
	char	name[MAX_QPATH];

	if( Cmd_Argc() != 2 ) {
		Com_Printf( "usage: writeconfig <filename>\n" );
		return;
	}

	Q_strncpyz( name, Cmd_Argv(1), sizeof(name) );
	COM_DefaultExtension( name, ".cfg", sizeof(name) );

	Com_Printf( "Writing %s\n", name );

	CL_WriteConfiguration( name, qfalse );
}


/*
=================
CL_SetClientState
=================
*/
void CL_SetClientState( int state )
{
	cls.state = state;
	Com_SetClientState( state );

	switch( state ) {
		case CA_DISCONNECTED:
			Con_Close();
			Cbuf_ExecuteText( EXEC_NOW, "menu_main" );
			//CL_UIModule_MenuMain ();
			CL_SetKeyDest( key_menu );
//			SCR_UpdateScreen();
			break;
		case CA_CONNECTING:
			Con_Close();
			CL_SetKeyDest( key_game );
//			SCR_UpdateScreen();
			break;
		case CA_CONNECTED:
			Con_Close();
			Cvar_FixCheatVars();
//			SCR_UpdateScreen();
			break;
		case CA_ACTIVE:
			Con_Close();
			CL_SetKeyDest( key_game );
//			SCR_UpdateScreen();
			break;
		default:
			break;
	}
}

/*
=================
CL_InitMedia
=================
*/
void CL_InitMedia( void )
{
	if( cls.mediaInitialized )
		return;
	if( cls.state == CA_UNINITIALIZED )
		return;

	cls.mediaInitialized = qtrue;

	// restart renderer
	R_Restart();

	// free all sounds
	S_FreeSounds();

	// register console font and background
	SCR_RegisterConsoleMedia();

	// load user interface
	CL_UIModule_Init();

	// check memory integrity
	Mem_CheckSentinelsGlobal();

	S_SoundsInMemory();
}

/*
=================
CL_ShutdownMedia
=================
*/
void CL_ShutdownMedia( void )
{
	if( !cls.mediaInitialized )
		return;

	cls.mediaInitialized = qfalse;

	// shutdown cgame
	CL_GameModule_Shutdown();

	// shutdown user interface
	CL_UIModule_Shutdown();

	// stop and free all sounds
	S_StopAllSounds();
	S_FreeSounds();

	// wsw : jalfonts
	SCR_ShutDownConsoleMedia();
}

/*
=================
CL_RestartMedia
=================
*/
void CL_RestartMedia( void )
{
	CL_ShutdownMedia();
	CL_InitMedia();
}


/*
==================
CL_ShowIP_f - wsw : jal : taken from Q3 (it only shows the ip when server was started)
==================
*/
void CL_ShowIP_f( void ) {
	Sys_ShowIP();
}

/*
=================
CL_InitLocal
=================
*/
void CL_InitLocal( void )
{
	cvar_t *color;

	cls.state = CA_DISCONNECTED;
	Com_SetClientState( CA_DISCONNECTED );
	cls.realtime = Sys_Milliseconds();
#ifdef BATTLEYE
	cls.runBattlEye = qfalse;
#endif

//
// register our variables
//
	cl_stereo_separation =	Cvar_Get( "cl_stereo_separation", "0.4", CVAR_ARCHIVE );
	cl_stereo =				Cvar_Get( "cl_stereo", "0", CVAR_ARCHIVE );

	cl_maxfps =				Cvar_Get( "cl_maxfps", "90", CVAR_ARCHIVE);
	cl_maxpackets =			Cvar_Get( "cl_maxpackets", "60", CVAR_ARCHIVE);
	cl_compresspackets =	Cvar_Get( "cl_compresspackets", "0", CVAR_ARCHIVE);
	cl_synchusercmd =		Cvar_Get( "cl_synchusercmd", "1", CVAR_ARCHIVE);

	cl_upspeed =			Cvar_Get( "cl_upspeed", "400", 0 );
	cl_forwardspeed =		Cvar_Get( "cl_forwardspeed", "400", 0 );
	cl_sidespeed =			Cvar_Get( "cl_sidespeed", "400", 0 );
	
	cl_yawspeed =			Cvar_Get( "cl_yawspeed", "140", 0 );
	cl_pitchspeed =			Cvar_Get( "cl_pitchspeed", "150", 0 );
	cl_anglespeedkey =		Cvar_Get( "cl_anglespeedkey", "1.5", 0 );

	cl_run =				Cvar_Get( "cl_run", "1", CVAR_ARCHIVE);
	cl_freelook =			Cvar_Get( "cl_freelook", "1", CVAR_ARCHIVE );
	lookspring =			Cvar_Get( "lookspring", "0", CVAR_ARCHIVE );
	lookstrafe =			Cvar_Get( "lookstrafe", "0", CVAR_ARCHIVE );
	sensitivity =			Cvar_Get( "sensitivity", "3", CVAR_ARCHIVE );
	m_accel =				Cvar_Get( "m_accel", "0", CVAR_ARCHIVE );	// wsw : pb : add mouseaccel
	m_filter =				Cvar_Get( "m_filter", "0", CVAR_ARCHIVE );
	in_minMsecs =			Cvar_Get( "in_minmsecs", "5", CVAR_ARCHIVE ); //  wsw : jal : key/mouse input frametime

	m_pitch =				Cvar_Get( "m_pitch", "0.022", CVAR_ARCHIVE );
	m_yaw =					Cvar_Get( "m_yaw", "0.022", CVAR_ARCHIVE );
	m_forward =				Cvar_Get( "m_forward", "1", CVAR_ARCHIVE );
	m_side =				Cvar_Get( "m_side", "1", CVAR_ARCHIVE );

	cl_masterservers =		Cvar_Get( "masterservers", DEFAULT_MASTER_SERVERS_IPS, 0 );
	
	cl_shownet =			Cvar_Get( "cl_shownet", "0", 0 );
	cl_timeout =			Cvar_Get( "cl_timeout", "120", 0 );
	cl_timedemo =			Cvar_Get( "timedemo", "0", CVAR_CHEAT );
	cl_demoavi_fps =		Cvar_Get( "cl_demoavi_fps", "25", CVAR_ARCHIVE );
	cl_demoavi_scissor =	Cvar_Get( "cl_demoavi_scissor", "0", CVAR_ARCHIVE );

	rcon_client_password =	Cvar_Get( "rcon_password", "", 0 );
	rcon_address =			Cvar_Get( "rcon_address", "", 0 );

	// wsw : debug netcode
	cl_debug_serverCmd =	Cvar_Get( "cl_debug_serverCmd", "0", CVAR_ARCHIVE|CVAR_CHEAT );

	cl_downloads =			Cvar_Get( "cl_downloads", "1", CVAR_ARCHIVE );
	cl_downloads_from_web =	Cvar_Get( "cl_downloads_from_web", "1", CVAR_ARCHIVE );

	//
	// userinfo
	//
	info_password =			Cvar_Get( "password", "", CVAR_USERINFO );
	rate =					Cvar_Get( "rate", "25000", CVAR_USERINFO | CVAR_ARCHIVE );	// FIXME

	Cvar_Get( "name", "player", CVAR_USERINFO | CVAR_ARCHIVE );
	Cvar_Get( "model", DEFAULT_PLAYERMODEL, CVAR_USERINFO | CVAR_ARCHIVE ); // wsw : jal
	Cvar_Get( "skin", DEFAULT_PLAYERSKIN, CVAR_USERINFO | CVAR_ARCHIVE ); // wsw : jal
	Cvar_Get( "hand", "0", CVAR_USERINFO | CVAR_ARCHIVE );
	Cvar_Get( "fov", "90", CVAR_USERINFO | CVAR_ARCHIVE );
	// wsw : jal 
	color = Cvar_Get( "color", "", CVAR_ARCHIVE|CVAR_USERINFO );
	if( COM_ReadColorRGBString( color->string ) == -1 ) { // first time create a random color for it
		time_t long_time;   // random isn't working fine at this point.
		struct tm *newtime;	// so we get the user local time and use some values from there
		time( &long_time ); 
		newtime = localtime( &long_time ); 
		long_time *= newtime->tm_sec * newtime->tm_min * newtime->tm_wday;
		Cvar_Set( "color", va("%i %i %i", (long_time)&0xff, ((long_time)>>8)&0xff, ((long_time)>>16)&0xff) );
	}

#ifdef BATTLEYE
#ifdef WIN32
	cl_battleye =			Cvar_Get( "cl_battleye", "1", CVAR_USERINFO | CVAR_ARCHIVE );
#else
	cl_battleye =			Cvar_Get( "cl_battleye", "0", CVAR_USERINFO | CVAR_READONLY );
#endif
#endif

	//
	// register our commands
	//
	Cmd_AddCommand( "cmd", CL_ForwardToServer_f );
	Cmd_AddCommand( "requestservers", CL_GetServers_f );
	Cmd_AddCommand( "getinfo", CL_QueryGetInfoMessage_f ); // wsw : jal : ask for server info
	Cmd_AddCommand( "userinfo", CL_Userinfo_f );
	Cmd_AddCommand( "disconnect", CL_Disconnect_f );
	Cmd_AddCommand( "record", CL_Record_f );
	Cmd_AddCommand( "stop", CL_Stop_f );
	Cmd_AddCommand( "quit", CL_Quit_f );
	Cmd_AddCommand( "connect", CL_Connect_f );
	Cmd_AddCommand( "reconnect", CL_Reconnect_f );
	Cmd_AddCommand( "rcon", CL_Rcon_f );
	Cmd_AddCommand( "download", CL_Download_f );
	Cmd_AddCommand( "writeconfig", CL_WriteConfig_f );
	Cmd_AddCommand( "showip", CL_ShowIP_f ); // jal : wsw : print our ip
	Cmd_AddCommand( "demo", CL_PlayDemo_f );
	Cmd_AddCommand( "demoavi", CL_PlayDemoToAvi_f );
	Cmd_AddCommand( "cinematic", CL_PlayCinematic_f );
	Cmd_AddCommand( "next", CL_SetNext_f );
	Cmd_AddCommand( "pingserver", CL_PingServer_f );
#ifdef DEMOCAM // -- PLX
	Cmd_AddCommand( "demopause", CL_PauseDemo_f );
#endif

}

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

//==================
//CL_Frame
//==================
void CL_AdjustServerTime( void )
{
	if( !cl.curFrame || !cl.curFrame->valid )
		cl.serverTimeDelta = 0;

	if( cls.realtime + cl.serverTimeDelta < 0 ) { // should never happen
		cl.serverTimeDelta = 0;
	}
	cl.serverTime = cls.realtime + cl.serverTimeDelta;
}

/*
==================
CL_SendCommand
==================
*/
void CL_SendCommand( void )
{
	// build a command even if not connected
	cl.cmdNum = cls.netchan.outgoingSequence & CMD_MASK; // store last sent command number for prediction
	cl.cmd_time[cls.netchan.outgoingSequence & CMD_MASK] = cls.realtime;

	// wsw : jal : cinematics are client side
	if( cl.cmds[cl.cmdNum].buttons 
		&& cl.cin.time > 0 && cls.realtime - cl.cin.time > 1000 )
	{	// skip the rest of the cinematic
		SCR_StopCinematic();
		SCR_FinishCinematic();
		SCR_UpdateScreen();
	}

	// send intentions now
	CL_WritePacket( &cl.cmds[cl.cmdNum] );

	// init the new command (outgoingSequence was bumped at CL_WritePacket if connected)
	memset( &cl.cmds[cls.netchan.outgoingSequence & CMD_MASK], 0, sizeof(usercmd_t) );
	CL_UpdateUserCommand();
}

#ifndef NEWFPSTIMER
/*
==================
CL_MinFrameFrame
==================
*/
double CL_MinFrameFrame( void )
{
	if( !cl_timedemo->integer )
	{
		if( cls.state == CA_CONNECTED )
			return 0.1;			// don't flood packets out while connecting
		if( cl_maxfps->integer )
			return 1.0 / (double)cl_maxfps->integer;	
	}

	return 0;
}

/*
==================
CL_MinPacketFrame
==================
*/
double CL_MinPacketFrame( void )
{
	int	maxpackets;

	if( cl_timedemo->integer || cls.demoplaying )
		return CL_MinFrameFrame();

	if( cls.state == CA_CONNECTED )
		return 0.1; 		// don't flood packets out while connecting

	maxpackets = cl_maxpackets->integer;

	// don't let people abuse cl_maxpackets
	if( cl_maxpackets->integer < 10 ) {
		maxpackets = 10;
	} else if( cl_maxpackets->integer > 90 && 
		!Sys_IsLANAddress( cls.netchan.remoteAddress ) ) {
		maxpackets = 90;
	}

	// return the smaller between maxpackets and maxfps
	if( cl_maxfps->integer && (cl_maxfps->integer < maxpackets) )
		return CL_MinFrameFrame();
		
	return 1.0 / (double)maxpackets;
}
#endif // NEWFPSTIMER

//==================
//CL_Netchan_Transmit
//==================
void CL_Netchan_Transmit( msg_t *msg ) {
	//int zerror;
	// if we got here with unsent fragments, fire them all now
	Netchan_PushAllFragments( &cls.netchan );

#ifdef BATTLEYE
	CL_AddBattleyeCommandToMessage( msg );
#endif
	// do not enable client compression until I fix the compression+fragmentation rare case bug
	//if( cl_compresspackets->integer ) {
	//	zerror = Netchan_CompressMessage( msg );
	//	if( zerror < 0 ) {  // it's compression error, just send uncompressed
	//		Com_DPrintf( "CL_Netchan_Transmit (ignoring compression): Compression error %i\n", zerror );
	//	}
	//}

	Netchan_Transmit( &cls.netchan, msg );
	cls.lastPacketSentTime = cls.realtime;
}


//#ifdef NEWFPSTIMER
void CL_NetSendUserCommand( int msec ) {
	static int minMsec = 1, allMsec = 0, extraMsec = 0;
	int maxpackets;

	if( cls.state == CA_CONNECTED )
		maxpackets = 10; 		// don't flood packets out while connecting

	maxpackets = cl_maxpackets->integer;
	// don't let people abuse cl_maxpackets
	//if( !Sys_IsLANAddress( cls.netchan.remoteAddress ) )
	clamp( maxpackets, 10, 90 );

	if( !cl_timedemo->integer && !cls.demoplaying ) {
		minMsec = 1000 / maxpackets;
	} else {
		minMsec = 1;
	}

	if( minMsec > extraMsec )  // remove, from min frametime, the extra time we spent in last frame
		minMsec -= extraMsec;

	allMsec += msec;
	if( allMsec < minMsec ) {
		return;
	}

	extraMsec = allMsec - minMsec;
	if( extraMsec > minMsec ) {
		//Com_Printf( "Dropped %i full net frame\n", (int)(extraMsec / minMsec) );
		extraMsec = minMsec - 1;
	}

	allMsec = 0;

	// resend a connection request if necessary
	CL_CheckForResend();
	CL_CheckDownloadTimeout();

	// send a new user command message to the server
	CL_SendCommand();
}
//#endif
/*
==================
CL_NetFrame
==================
*/
void CL_NetFrame( int msec )
{
#ifdef NEWFPSTIMER
	static int minMsec = 1, allMsec = 0, extraMsec = 0;
	int maxpackets;
#else
	static double	extrapackettime = 0.001;
	static double	truepackettime;
	double	minpackettime;
#endif
	// read packets from server
	
	if( msec > 5000 ) // if in the debugger last frame, don't timeout
		cls.lastPacketReceivedTime = cls.realtime;

	if( cls.demoplaying )
		CL_ReadDemoPackets();  // fetch results from demo file
	else
		CL_ReadPackets(); // fetch results from server

	// if we have any unsent fragment
	if( cls.netchan.unsentFragments )
		Netchan_TransmitNextFragment( &cls.netchan );

#ifdef NEWFPSTIMER
	if( !cl_synchusercmd->integer ) 
	{
		if( cls.state == CA_CONNECTED )
			maxpackets = 10; 		// don't flood packets out while connecting

		maxpackets = cl_maxpackets->integer;
		// don't let people abuse cl_maxpackets
		//if( !Sys_IsLANAddress( cls.netchan.remoteAddress ) )
		clamp( maxpackets, 10, 90 );
		if( cl_maxfps->integer && (cl_maxfps->integer < maxpackets) )
			maxpackets = cl_maxfps->integer;

		if( !cl_timedemo->integer && !cls.demoplaying ) {
			minMsec = 1000 / maxpackets;
		} else {
			minMsec = 1;
		}

		if( minMsec > extraMsec )  // remove, from min frametime, the extra time we spent in last frame
			minMsec -= extraMsec;

		allMsec += msec;
		if( allMsec < minMsec ) {
			return;
		}

		extraMsec = allMsec - minMsec;
		if( extraMsec > minMsec ) {
			//Com_Printf( "Dropped %i full net frame\n", (int)(extraMsec / minMsec) );
			extraMsec = minMsec - 1;
		}

		allMsec = 0;

		// resend a connection request if necessary
		CL_CheckForResend();
		CL_CheckDownloadTimeout();

		// send a new user command message to the server
		CL_SendCommand();
	}

#else
	if( !cl_synchusercmd->integer )
	{
		// see if it's time to send a new command to the server
		extrapackettime += msec * 0.001;
		minpackettime = CL_MinPacketFrame();
		if( extrapackettime > minpackettime )
		{
			// decide the simulation time
			truepackettime = extrapackettime - 0.001;
			if( truepackettime < minpackettime )
				truepackettime = minpackettime;
			extrapackettime -= truepackettime;

			// resend a connection request if necessary
			CL_CheckForResend();
			CL_CheckDownloadTimeout();

			// send a new user command message to the server
			CL_SendCommand();
		}// else if( !cls.netchan.unsentFragments ) {
		//	NET_Sleep( (minpackettime - extrapackettime)*1000 );
		//}
	}
#endif
}

void CL_TimedemoStats( void ) {
	if( cl_timedemo->integer ) {
		static int lasttime = 0;
		if( lasttime != 0 )
		{
			if( curtime - lasttime >= 100 )
				cl.timedemo_counts[99]++;
			else
				cl.timedemo_counts[curtime-lasttime]++;
		}
		lasttime = curtime;
	}
}

#ifdef _DEBUG
static void CL_LogStats( void ) {
	static unsigned int	lasttimecalled = 0;
	if( log_stats->integer )
	{
		if( cls.state == CA_ACTIVE )
		{
			if ( !lasttimecalled )
			{
				lasttimecalled = Sys_Milliseconds();
				if ( log_stats_file )
					FS_Printf( log_stats_file, "0\n" );
			}
			else
			{
				unsigned int now = Sys_Milliseconds();

				if ( log_stats_file )
					FS_Printf( log_stats_file, "%u\n", now - lasttimecalled );
				lasttimecalled = now;
			}
		}
	}
}
#endif

/*
==================
CL_Frame
==================
*/
void CL_Frame( int msec )
{
#ifdef NEWFPSTIMER
	static int minMsec = 1, allMsec = 0, extraMsec = 0;
#else
	static double		extratime = 0.001;
	static double		trueframetime;
	double				minframetime;
#endif
	if( dedicated->integer )
		return;

	cls.realtime += msec;

#ifdef NEWFPSTIMER
	CL_UserInputFrame();
	CL_AdjustServerTime();
	CL_NetFrame( msec );

	// demoavi
	if( cls.demoavi && msec && cls.state == CA_ACTIVE ) {
		R_WriteAviFrame( cls.demoavi_frame++, cl_demoavi_scissor->integer );
		// fixed time for next frame
		msec = (1000.0 / (double)cl_demoavi_fps->integer) * Cvar_VariableValue( "timescale" );
		if( msec < 1 ) {
			msec = 1;
		}
	}

	cls.demoplay_framemsecs += msec;

	if( cl_maxfps->integer > 0 && !cl_timedemo->integer ) {
		minMsec = 1000 / cl_maxfps->integer;
	} else {
		minMsec = 1;
	}

	if( minMsec > extraMsec )  // remove, from min frametime, the extra time we spent in last frame
		minMsec -= extraMsec;

	allMsec += msec;
	if( allMsec < minMsec ) {
		return;
	}

	if( cl_synchusercmd->integer )
		CL_NetSendUserCommand( allMsec );
	cls.frametime = (float)allMsec * 0.001f;
	cls.trueframetime = cls.frametime;

	extraMsec = allMsec - minMsec;
	if( extraMsec > minMsec ) {
		//Com_Printf( "Lost %i full frames\n", (int)(extraMsec / minMsec) );
		extraMsec = minMsec - 1;
	}

	allMsec = 0;

	CL_TimedemoStats();
#else

	CL_UserInputFrame();
	CL_AdjustServerTime();
	CL_NetFrame( msec );

	// demoavi
	if( cls.demoavi && msec && cls.state == CA_ACTIVE ) {
		R_WriteAviFrame( cls.demoavi_frame++, cl_demoavi_scissor->integer );
		// fixed time for next frame
		msec = (1000.0 / (double)cl_demoavi_fps->integer) * Cvar_VariableValue( "timescale" );
		if( msec < 1 ) {
			msec = 1;
		}
	}

	cls.demoplay_framemsecs += msec;

	extratime += msec * 0.001;

	minframetime = CL_MinFrameFrame();
	if( extratime < minframetime )
		return;

	CL_TimedemoStats();

	if( cl_synchusercmd->integer )
		CL_NetSendUserCommand( (unsigned int)(extratime * 1000) );

	// decide the simulation time
	trueframetime = extratime - 0.001;
	if( trueframetime < minframetime )
		trueframetime = minframetime;
	extratime -= trueframetime;

	cls.frametime = trueframetime;
	cls.trueframetime = trueframetime;
#endif
	// allow rendering DLL change
	VID_CheckChanges();

	// update the screen
	if( host_speeds->integer )
		time_before_ref = Sys_Milliseconds();
	SCR_UpdateScreen();
	if( host_speeds->integer )
		time_after_ref = Sys_Milliseconds();

	// update audio
	if( cls.state != CA_ACTIVE || cl.cin.time > 0 )
		S_Update( vec3_origin, vec3_origin, vec3_origin, vec3_origin );

	// advance local effects for next frame
	SCR_RunCinematic();
	SCR_RunConsole();

	cls.framecount++;
#ifdef _DEBUG
	CL_LogStats();
#endif
}


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

/*
===============
CL_CheckForUpdate

retrieve a file with the last version umber on a web server, compare with current version
display a message box in case the user need to update
===============
*/
#define CHECKUPDATE_URL "http://www.warsow.net/"
#define CHECKUPDATE_FILE "warsow_last_version.txt"

void CL_CheckForUpdate(void)
{
	char		url[MAX_STRING_CHARS];
	qboolean	success;
	float	    local_version, net_version;
	FILE		*f;

	// step one get the last version file
	Com_Printf("Checking for Warsow update.\n");

	Q_snprintfz( url, sizeof(url), CHECKUPDATE_URL CHECKUPDATE_FILE );
	success = Web_Get( url, NULL, "warsow_last_version.txt", qfalse, 2, NULL );

	if(!success)
		return;

	// got the file
	// this look stupid but is the safe way to do it
	local_version=atof(va("%4.3f",VERSION));	

	f=fopen(CHECKUPDATE_FILE,"r");

	if(f==NULL)
	{
		Com_Printf("Fail to open last version file.\n");
		return;
	}

	if(fscanf(f,"%f",&net_version)!=1)
	{
		// error
		fclose(f);
		Com_Printf("Fail to parse last version file.\n");
		return;
	}

	// we have the version
	//Com_Printf("CheckForUpdate: local: %f net: %f\n", local_version, net_version);
	if(net_version>local_version)
	{	char cmd[1024];

		// you should update
		Com_Printf("Warsow version %4.3f is available.\n", net_version);
		Q_snprintfz(cmd, 1024, "menu_msgbox \"Version %4.3f of Warsow is available\"", net_version);
		Cbuf_ExecuteText(EXEC_APPEND, cmd );
	}
	else if(net_version==local_version)
	{
		Com_Printf("This Warsow version is up-to-date.\n");
	}

	fclose(f);

	// cleanup
	FS_RemoveFile(CHECKUPDATE_FILE);
}

/*
====================
CL_Init
====================
*/
void CL_Init( void )
{
	if( dedicated->integer )
		return;		// nothing running on the client

	// all archived variables will now be loaded

	Con_Init();

#ifndef VID_INITFIRST
	S_Init();	
	VID_Init();
#else
	VID_Init();
	S_Init();	// sound must be initialized after window is created
#endif

	cls.lastExecutedServerCommand = 0;
	cls.reliableAcknowledge = 0;
	cls.reliableSequence = 0;
	cls.reliableSent = 0;
	memset( &cls.reliableCommands, 0, sizeof(cls.reliableCommands) );
#ifdef BATTLEYE
	memset( &cls.BE, 0, sizeof(cls.BE) );
#endif

	RoQ_Init();

	SCR_InitScreen();
	cls.disable_screen = qtrue;	// don't draw yet

	CL_InitLocal();
	CL_InitInput();

	CL_InitMedia();
	Cbuf_ExecuteText( EXEC_NOW, "menu_main" );

	// check for update
	CL_CheckForUpdate( );
}


/*
===============
CL_Shutdown

FIXME: this is a callback from Sys_Quit and Com_Error.  It would be better
to run quit through here before the final handoff to the sys code.
===============
*/
void CL_Shutdown( void )
{
	static qboolean isdown = qfalse;

	if( cls.state == CA_UNINITIALIZED )
		return;
	if( isdown )
		return;

	isdown = qtrue;

	CL_WriteConfiguration( "config.cfg", qtrue );

	CL_UIModule_Shutdown();
	CL_GameModule_Shutdown();
	S_Shutdown();
	CL_ShutdownInput();
	VID_Shutdown();
}

