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

Doom 3 GPL Source Code
Copyright (C) 1999-2011 id Software LLC, a ZeniMax Media company.

This file is part of the Doom 3 GPL Source Code ("Doom 3 Source Code").

Doom 3 Source Code 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 3 of the License, or
(at your option) any later version.

Doom 3 Source Code 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 Doom 3 Source Code.  If not, see <http://www.gnu.org/licenses/>.

In addition, the Doom 3 Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 Source Code.  If not, please request a copy in writing from id Software at the address below.

If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.

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

#include "sys/platform.h"
#include "idlib/LangDict.h"
#include "framework/async/AsyncNetwork.h"
#include "framework/Licensee.h"
#include "framework/Game.h"
#include "framework/Session_local.h"
#include "sound/sound.h"

#include "framework/async/AsyncClient.h"

const int SETUP_CONNECTION_RESEND_TIME	= 1000;
const int EMPTY_RESEND_TIME				= 500;
const int PREDICTION_FAST_ADJUST		= 4;

/*
==================
idAsyncClient::idAsyncClient
==================
*/
idAsyncClient::idAsyncClient( void ) {
	guiNetMenu = NULL;
	updateState = UPDATE_NONE;
	Clear();
}

/*
==================
idAsyncClient::Clear
==================
*/
void idAsyncClient::Clear( void ) {
	active = false;
	realTime = 0;
	clientTime = 0;
	clientId = 0;
	clientDataChecksum = 0;
	clientNum = 0;
	clientState = CS_DISCONNECTED;
	clientPrediction = 0;
	clientPredictTime = 0;
	serverId = 0;
	serverChallenge = 0;
	serverMessageSequence = 0;
	lastConnectTime = -9999;
	lastEmptyTime = -9999;
	lastPacketTime = -9999;
	lastSnapshotTime = -9999;
	snapshotGameFrame = 0;
	snapshotGameTime = 0;
	snapshotSequence = 0;
	gameInitId = GAME_INIT_ID_INVALID;
	gameFrame = 0;
	gameTimeResidual = 0;
	gameTime = 0;
	memset( userCmds, 0, sizeof( userCmds ) );
	backgroundDownload.completed = true;
	lastRconTime = 0;
	showUpdateMessage = false;
	lastFrameDelta = 0;

	dlRequest = -1;
	dlCount = -1;
	memset( dlChecksums, 0, sizeof( int ) * MAX_PURE_PAKS );
	currentDlSize = 0;
	totalDlSize = 0;
}

/*
==================
idAsyncClient::Shutdown
==================
*/
void idAsyncClient::Shutdown( void ) {
	guiNetMenu = NULL;
	updateMSG.Clear();
	updateURL.Clear();
	updateFile.Clear();
	updateFallback.Clear();
	backgroundDownload.url.url.Clear();
	dlList.Clear();
}

/*
==================
idAsyncClient::InitPort
==================
*/
bool idAsyncClient::InitPort( void ) {
	// if this is the first time we connect to a server, open the UDP port
	if ( !clientPort.GetPort() ) {
		if ( !clientPort.InitForPort( PORT_ANY ) ) {
			common->Printf( "Couldn't open client network port.\n" );
			return false;
		}
	}
	// maintain it valid between connects and ui manager reloads
	guiNetMenu = uiManager->FindGui( "guis/netmenu.gui", true, false, true );

	return true;
}

/*
==================
idAsyncClient::ClosePort
==================
*/
void idAsyncClient::ClosePort( void ) {
	clientPort.Close();
}

/*
==================
idAsyncClient::ClearPendingPackets
==================
*/
void idAsyncClient::ClearPendingPackets( void ) {
	int			size;
	byte		msgBuf[MAX_MESSAGE_SIZE];
	netadr_t	from;

	while( clientPort.GetPacket( from, msgBuf, size, sizeof( msgBuf ) ) ) {
	}
}

/*
==================
idAsyncClient::HandleGuiCommandInternal
==================
*/
const char* idAsyncClient::HandleGuiCommandInternal( const char *cmd ) {
	if ( !idStr::Cmp( cmd, "abort" ) || !idStr::Cmp( cmd, "pure_abort" ) ) {
		common->DPrintf( "connection aborted\n" );
		cmdSystem->BufferCommandText( CMD_EXEC_NOW, "disconnect" );
		return "";
	} else {
		common->DWarning( "idAsyncClient::HandleGuiCommand: unknown cmd %s", cmd );
	}
	return NULL;
}

/*
==================
idAsyncClient::HandleGuiCommand
==================
*/
const char* idAsyncClient::HandleGuiCommand( const char *cmd ) {
	return idAsyncNetwork::client.HandleGuiCommandInternal( cmd );
}

/*
==================
idAsyncClient::ConnectToServer
==================
*/
void idAsyncClient::ConnectToServer( const netadr_t adr ) {
	// shutdown any current game. that includes network disconnect
	session->Stop();

	if ( !InitPort() ) {
		return;
	}

	if ( cvarSystem->GetCVarBool( "net_serverDedicated" ) ) {
		common->Printf( "Can't connect to a server as dedicated\n" );
		return;
	}

	// trash any currently pending packets
	ClearPendingPackets();

	serverAddress = adr;

	// clear the client state
	Clear();

	// get a pseudo random client id, but don't use the id which is reserved for connectionless packets
	clientId = Sys_Milliseconds() & CONNECTIONLESS_MESSAGE_ID_MASK;

	// calculate a checksum on some of the essential data used
	clientDataChecksum = declManager->GetChecksum();

	// start challenging the server
	clientState = CS_CHALLENGING;

	active = true;

	guiNetMenu = uiManager->FindGui( "guis/netmenu.gui", true, false, true );
	guiNetMenu->SetStateString( "status", va( common->GetLanguageDict()->GetString( "#str_06749" ), Sys_NetAdrToString( adr ) ) );
	session->SetGUI( guiNetMenu, HandleGuiCommand );
}

/*
==================
idAsyncClient::Reconnect
==================
*/
void idAsyncClient::Reconnect( void ) {
	ConnectToServer( serverAddress );
}

/*
==================
idAsyncClient::ConnectToServer
==================
*/
void idAsyncClient::ConnectToServer( const char *address ) {
	int serverNum;
	netadr_t adr;

	if ( idStr::IsNumeric( address ) ) {
		serverNum = atoi( address );
		if ( serverNum < 0 || serverNum >= serverList.Num() ) {
			session->MessageBox( MSG_OK, va( common->GetLanguageDict()->GetString( "#str_06733" ), serverNum ), common->GetLanguageDict()->GetString( "#str_06735" ), true );
			return;
		}
		adr = serverList[ serverNum ].adr;
	} else {
		if ( !Sys_StringToNetAdr( address, &adr, true ) ) {
			session->MessageBox( MSG_OK, va( common->GetLanguageDict()->GetString( "#str_06734" ), address ), common->GetLanguageDict()->GetString( "#str_06735" ), true );
			return;
		}
	}
	if ( !adr.port ) {
		adr.port = PORT_SERVER;
	}

	common->Printf( "\"%s\" resolved to %s\n", address, Sys_NetAdrToString( adr ) );

	ConnectToServer( adr );
}

/*
==================
idAsyncClient::DisconnectFromServer
==================
*/
void idAsyncClient::DisconnectFromServer( void ) {
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	if ( clientState >= CS_CONNECTED ) {
		// if we were actually connected, clear the pure list
		fileSystem->ClearPureChecksums();

		// send reliable disconnect to server
		msg.Init( msgBuf, sizeof( msgBuf ) );
		msg.WriteByte( CLIENT_RELIABLE_MESSAGE_DISCONNECT );
		msg.WriteString( "disconnect" );

		if ( !channel.SendReliableMessage( msg ) ) {
			common->Error( "client->server reliable messages overflow\n" );
		}

		SendEmptyToServer( true );
		SendEmptyToServer( true );
		SendEmptyToServer( true );
	}

	if ( clientState != CS_PURERESTART ) {
		channel.Shutdown();
		clientState = CS_DISCONNECTED;
	}

	active = false;
}

/*
==================
idAsyncClient::GetServerInfo
==================
*/
void idAsyncClient::GetServerInfo( const netadr_t adr ) {
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	if ( !InitPort() ) {
		return;
	}

	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
	msg.WriteString( "getInfo" );
	msg.WriteInt( serverList.GetChallenge() );	// challenge

	clientPort.SendPacket( adr, msg.GetData(), msg.GetSize() );
}

/*
==================
idAsyncClient::GetServerInfo
==================
*/
void idAsyncClient::GetServerInfo( const char *address ) {
	netadr_t	adr;

	if ( address && *address != '\0' ) {
		if ( !Sys_StringToNetAdr( address, &adr, true ) ) {
			common->Printf( "Couldn't get server address for \"%s\"\n", address );
			return;
		}
	} else if ( active ) {
		adr = serverAddress;
	} else if ( idAsyncNetwork::server.IsActive() ) {
		// used to be a Sys_StringToNetAdr( "localhost", &adr, true ); and send a packet over loopback
		// but this breaks with net_ip ( typically, for multi-homed servers )
		idAsyncNetwork::server.PrintLocalServerInfo();
		return;
	} else {
		common->Printf( "no server found\n" );
		return;
	}

	if ( !adr.port ) {
		adr.port = PORT_SERVER;
	}

	GetServerInfo( adr );
}

/*
==================
idAsyncClient::GetLANServers
==================
*/
void idAsyncClient::GetLANServers( void ) {
	int			i;
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];
	netadr_t	broadcastAddress;

	if ( !InitPort() ) {
		return;
	}

	idAsyncNetwork::LANServer.SetBool( true );

	serverList.SetupLANScan();

	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
	msg.WriteString( "getInfo" );
	msg.WriteInt( serverList.GetChallenge() );

	broadcastAddress.type = NA_BROADCAST;
	for ( i = 0; i < MAX_SERVER_PORTS; i++ ) {
		broadcastAddress.port = PORT_SERVER + i;
		clientPort.SendPacket( broadcastAddress, msg.GetData(), msg.GetSize() );
	}
}

/*
==================
idAsyncClient::GetNETServers
==================
*/
void idAsyncClient::GetNETServers( void ) {
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	idAsyncNetwork::LANServer.SetBool( false );

	// NetScan only clears GUI and results, not the stored list
	serverList.Clear( );
	serverList.NetScan( );
	serverList.StartServers( true );

	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
	msg.WriteString( "getServers" );
	msg.WriteInt( ASYNC_PROTOCOL_VERSION );
	msg.WriteString( cvarSystem->GetCVarString( "fs_game" ) );
	msg.WriteBits( cvarSystem->GetCVarInteger( "gui_filter_password" ), 2 );
	msg.WriteBits( cvarSystem->GetCVarInteger( "gui_filter_players" ), 2 );
	msg.WriteBits( cvarSystem->GetCVarInteger( "gui_filter_gameType" ), 2 );

	netadr_t adr;
	if ( idAsyncNetwork::GetMasterAddress( 0, adr ) ) {
		clientPort.SendPacket( adr, msg.GetData(), msg.GetSize() );
	}
}

/*
==================
idAsyncClient::ListServers
==================
*/
void idAsyncClient::ListServers( void ) {
	int i;

	for ( i = 0; i < serverList.Num(); i++ ) {
		common->Printf( "%3d: %s %dms (%s)\n", i, serverList[i].serverInfo.GetString( "si_name" ), serverList[ i ].ping, Sys_NetAdrToString( serverList[i].adr ) );
	}
}

/*
==================
idAsyncClient::ClearServers
==================
*/
void idAsyncClient::ClearServers( void ) {
	serverList.Clear();
}

/*
==================
idAsyncClient::RemoteConsole
==================
*/
void idAsyncClient::RemoteConsole( const char *command ) {
	netadr_t	adr;
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	if ( !InitPort() ) {
		return;
	}

	if ( active ) {
		adr = serverAddress;
	} else {
		Sys_StringToNetAdr( idAsyncNetwork::clientRemoteConsoleAddress.GetString(), &adr, true );
	}

	if ( !adr.port ) {
		adr.port = PORT_SERVER;
	}

	lastRconAddress = adr;
	lastRconTime = realTime;

	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
	msg.WriteString( "rcon" );
	msg.WriteString( idAsyncNetwork::clientRemoteConsolePassword.GetString() );
	msg.WriteString( command );

	clientPort.SendPacket( adr, msg.GetData(), msg.GetSize() );
}

/*
==================
idAsyncClient::GetPrediction
==================
*/
int idAsyncClient::GetPrediction( void ) const {
	if ( clientState < CS_CONNECTED ) {
		return -1;
	} else {
		return clientPrediction;
	}
}

/*
==================
idAsyncClient::GetTimeSinceLastPacket
==================
*/
int idAsyncClient::GetTimeSinceLastPacket( void ) const {
	if ( clientState < CS_CONNECTED ) {
		return -1;
	} else {
		return clientTime - lastPacketTime;
	}
}

/*
==================
idAsyncClient::GetOutgoingRate
==================
*/
int idAsyncClient::GetOutgoingRate( void ) const {
	if ( clientState < CS_CONNECTED ) {
		return -1;
	} else {
		return channel.GetOutgoingRate();
	}
}

/*
==================
idAsyncClient::GetIncomingRate
==================
*/
int idAsyncClient::GetIncomingRate( void ) const {
	if ( clientState < CS_CONNECTED ) {
		return -1;
	} else {
		return channel.GetIncomingRate();
	}
}

/*
==================
idAsyncClient::GetOutgoingCompression
==================
*/
float idAsyncClient::GetOutgoingCompression( void ) const {
	if ( clientState < CS_CONNECTED ) {
		return 0.0f;
	} else {
		return channel.GetOutgoingCompression();
	}
}

/*
==================
idAsyncClient::GetIncomingCompression
==================
*/
float idAsyncClient::GetIncomingCompression( void ) const {
	if ( clientState < CS_CONNECTED ) {
		return 0.0f;
	} else {
		return channel.GetIncomingCompression();
	}
}

/*
==================
idAsyncClient::GetIncomingPacketLoss
==================
*/
float idAsyncClient::GetIncomingPacketLoss( void ) const {
	if ( clientState < CS_CONNECTED ) {
		return 0.0f;
	} else {
		return channel.GetIncomingPacketLoss();
	}
}

/*
==================
idAsyncClient::DuplicateUsercmds
==================
*/
void idAsyncClient::DuplicateUsercmds( int frame, int time ) {
	int i, previousIndex, currentIndex;

	previousIndex = ( frame - 1 ) & ( MAX_USERCMD_BACKUP - 1 );
	currentIndex = frame & ( MAX_USERCMD_BACKUP - 1 );

	// duplicate previous user commands if no new commands are available for a client
	for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		idAsyncNetwork::DuplicateUsercmd( userCmds[previousIndex][i], userCmds[currentIndex][i], frame, time );
	}
}

/*
==================
idAsyncClient::SendUserInfoToServer
==================
*/
void idAsyncClient::SendUserInfoToServer( void ) {
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];
	idDict		info;

	if ( clientState < CS_CONNECTED ) {
		return;
	}

	info = *cvarSystem->MoveCVarsToDict( CVAR_USERINFO );

	// send reliable client info to server
	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteByte( CLIENT_RELIABLE_MESSAGE_CLIENTINFO );
	msg.WriteDeltaDict( info, &sessLocal.mapSpawnData.userInfo[ clientNum ] );

	if ( !channel.SendReliableMessage( msg ) ) {
		common->Error( "client->server reliable messages overflow\n" );
	}

	sessLocal.mapSpawnData.userInfo[clientNum] = info;
}

/*
==================
idAsyncClient::SendEmptyToServer
==================
*/
void idAsyncClient::SendEmptyToServer( bool force, bool mapLoad ) {
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	if ( lastEmptyTime > realTime ) {
		lastEmptyTime = realTime;
	}

	if ( !force && ( realTime - lastEmptyTime < EMPTY_RESEND_TIME ) ) {
		return;
	}

	if ( idAsyncNetwork::verbose.GetInteger() ) {
		common->Printf( "sending empty to server, gameInitId = %d\n", mapLoad ? GAME_INIT_ID_MAP_LOAD : gameInitId );
	}

	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteInt( serverMessageSequence );
	msg.WriteInt( mapLoad ? GAME_INIT_ID_MAP_LOAD : gameInitId );
	msg.WriteInt( snapshotSequence );
	msg.WriteByte( CLIENT_UNRELIABLE_MESSAGE_EMPTY );

	channel.SendMessage( clientPort, clientTime, msg );

	while( channel.UnsentFragmentsLeft() ) {
		channel.SendNextFragment( clientPort, clientTime );
	}

	lastEmptyTime = realTime;
}

/*
==================
idAsyncClient::SendPingResponseToServer
==================
*/
void idAsyncClient::SendPingResponseToServer( int time ) {
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	if ( idAsyncNetwork::verbose.GetInteger() == 2 ) {
		common->Printf( "sending ping response to server, gameInitId = %d\n", gameInitId );
	}

	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteInt( serverMessageSequence );
	msg.WriteInt( gameInitId );
	msg.WriteInt( snapshotSequence );
	msg.WriteByte( CLIENT_UNRELIABLE_MESSAGE_PINGRESPONSE );
	msg.WriteInt( time );

	channel.SendMessage( clientPort, clientTime, msg );
	while( channel.UnsentFragmentsLeft() ) {
		channel.SendNextFragment( clientPort, clientTime );
	}
}

/*
==================
idAsyncClient::SendUsercmdsToServer
==================
*/
void idAsyncClient::SendUsercmdsToServer( void ) {
	int			i, numUsercmds, index;
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];
	usercmd_t *	last;

	if ( idAsyncNetwork::verbose.GetInteger() == 2 ) {
		common->Printf( "sending usercmd to server: gameInitId = %d, gameFrame = %d, gameTime = %d\n", gameInitId, gameFrame, gameTime );
	}

	// generate user command for this client
	index = gameFrame & ( MAX_USERCMD_BACKUP - 1 );
	userCmds[index][clientNum] = usercmdGen->GetDirectUsercmd();
	userCmds[index][clientNum].gameFrame = gameFrame;
	userCmds[index][clientNum].gameTime = gameTime;

	// send the user commands to the server
	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteInt( serverMessageSequence );
	msg.WriteInt( gameInitId );
	msg.WriteInt( snapshotSequence );
	msg.WriteByte( CLIENT_UNRELIABLE_MESSAGE_USERCMD );
	msg.WriteShort( clientPrediction );

	numUsercmds = idMath::ClampInt( 0, 10, idAsyncNetwork::clientUsercmdBackup.GetInteger() ) + 1;

	// write the user commands
	msg.WriteInt( gameFrame );
	msg.WriteByte( numUsercmds );
	for ( last = NULL, i = gameFrame - numUsercmds + 1; i <= gameFrame; i++ ) {
		index = i & ( MAX_USERCMD_BACKUP - 1 );
		idAsyncNetwork::WriteUserCmdDelta( msg, userCmds[index][clientNum], last );
		last = &userCmds[index][clientNum];
	}

	channel.SendMessage( clientPort, clientTime, msg );
	while( channel.UnsentFragmentsLeft() ) {
		channel.SendNextFragment( clientPort, clientTime );
	}
}

/*
==================
idAsyncClient::InitGame
==================
*/
void idAsyncClient::InitGame( int serverGameInitId, int serverGameFrame, int serverGameTime, const idDict &serverSI ) {
	gameInitId = serverGameInitId;
	gameFrame = snapshotGameFrame = serverGameFrame;
	gameTime = snapshotGameTime = serverGameTime;
	gameTimeResidual = 0;
	memset( userCmds, 0, sizeof( userCmds ) );

	for ( int i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		sessLocal.mapSpawnData.userInfo[ i ].Clear();
	}

	sessLocal.mapSpawnData.serverInfo = serverSI;
}

/*
==================
idAsyncClient::ProcessUnreliableServerMessage
==================
*/
void idAsyncClient::ProcessUnreliableServerMessage( const idBitMsg &msg ) {
	int i, j, index, id, numDuplicatedUsercmds, aheadOfServer, numUsercmds, delta;
	int serverGameInitId, serverGameFrame, serverGameTime;
	idDict serverSI;
	usercmd_t *last;
	bool pureWait;

	serverGameInitId = msg.ReadInt();

	id = msg.ReadByte();
	switch( id ) {
		case SERVER_UNRELIABLE_MESSAGE_EMPTY: {
			if ( idAsyncNetwork::verbose.GetInteger() ) {
				common->Printf( "received empty message from server\n" );
			}
			break;
		}
		case SERVER_UNRELIABLE_MESSAGE_PING: {
			if ( idAsyncNetwork::verbose.GetInteger() == 2 ) {
				common->Printf( "received ping message from server\n" );
			}
			SendPingResponseToServer( msg.ReadInt() );
			break;
		}
		case SERVER_UNRELIABLE_MESSAGE_GAMEINIT: {
			serverGameFrame = msg.ReadInt();
			serverGameTime = msg.ReadInt();
			msg.ReadDeltaDict( serverSI, NULL );
			pureWait = serverSI.GetBool( "si_pure" );

			InitGame( serverGameInitId, serverGameFrame, serverGameTime, serverSI );

			channel.ResetRate();

			if ( idAsyncNetwork::verbose.GetInteger() ) {
				common->Printf( "received gameinit, gameInitId = %d, gameFrame = %d, gameTime = %d\n", gameInitId, gameFrame, gameTime );
			}

			// mute sound
			soundSystem->SetMute( true );

			// ensure chat icon goes away when the GUI is changed...
			//cvarSystem->SetCVarBool( "ui_chat", false );

			if ( pureWait ) {
				guiNetMenu = uiManager->FindGui( "guis/netmenu.gui", true, false, true );
				session->SetGUI( guiNetMenu, HandleGuiCommand );
				session->MessageBox( MSG_ABORT, common->GetLanguageDict()->GetString ( "#str_04317" ), common->GetLanguageDict()->GetString ( "#str_04318" ), false, "pure_abort" );
			} else {
				// load map
				session->SetGUI( NULL, NULL );
				sessLocal.ExecuteMapChange();
			}

			break;
		}
		case SERVER_UNRELIABLE_MESSAGE_SNAPSHOT: {
			// if the snapshot is from a different game
			if ( serverGameInitId != gameInitId ) {
				if ( idAsyncNetwork::verbose.GetInteger() ) {
					common->Printf( "ignoring snapshot with != gameInitId\n" );
				}
				break;
			}

			snapshotSequence = msg.ReadInt();
			snapshotGameFrame = msg.ReadInt();
			snapshotGameTime = msg.ReadInt();
			numDuplicatedUsercmds = msg.ReadByte();
			aheadOfServer = msg.ReadShort();

			// read the game snapshot
			game->ClientReadSnapshot( clientNum, snapshotSequence, snapshotGameFrame, snapshotGameTime, numDuplicatedUsercmds, aheadOfServer, msg );

			// read user commands of other clients from the snapshot
			for ( last = NULL, i = msg.ReadByte(); i < MAX_ASYNC_CLIENTS; i = msg.ReadByte() ) {
				numUsercmds = msg.ReadByte();
				if ( numUsercmds > MAX_USERCMD_RELAY ) {
					common->Error( "snapshot %d contains too many user commands for client %d", snapshotSequence, i );
					break;
				}
				for ( j = 0; j < numUsercmds; j++ ) {
					index = ( snapshotGameFrame + j ) & ( MAX_USERCMD_BACKUP - 1 );
					idAsyncNetwork::ReadUserCmdDelta( msg, userCmds[index][i], last );
					userCmds[index][i].gameFrame = snapshotGameFrame + j;
					userCmds[index][i].duplicateCount = 0;
					last = &userCmds[index][i];
				}
				// clear all user commands after the ones just read from the snapshot
				for ( j = numUsercmds; j < MAX_USERCMD_BACKUP; j++ ) {
					index = ( snapshotGameFrame + j ) & ( MAX_USERCMD_BACKUP - 1 );
					userCmds[index][i].gameFrame = 0;
					userCmds[index][i].gameTime = 0;
				}
			}

			// if this is the first snapshot after a game init was received
			if ( clientState == CS_CONNECTED ) {
				gameTimeResidual = 0;
				clientState = CS_INGAME;
				assert( !sessLocal.GetActiveMenu( ) );
				if ( idAsyncNetwork::verbose.GetInteger() ) {
					common->Printf( "received first snapshot, gameInitId = %d, gameFrame %d gameTime %d\n", gameInitId, snapshotGameFrame, snapshotGameTime );
				}
			}

			// if the snapshot is newer than the clients current game time
			if ( gameTime < snapshotGameTime || gameTime > snapshotGameTime + idAsyncNetwork::clientMaxPrediction.GetInteger() ) {
				gameFrame = snapshotGameFrame;
				gameTime = snapshotGameTime;
				gameTimeResidual = idMath::ClampInt( -idAsyncNetwork::clientMaxPrediction.GetInteger(), idAsyncNetwork::clientMaxPrediction.GetInteger(), gameTimeResidual );
				clientPredictTime = idMath::ClampInt( -idAsyncNetwork::clientMaxPrediction.GetInteger(), idAsyncNetwork::clientMaxPrediction.GetInteger(), clientPredictTime );
			}

			// adjust the client prediction time based on the snapshot time
			clientPrediction -= ( 1 - ( INTSIGNBITSET( aheadOfServer - idAsyncNetwork::clientPrediction.GetInteger() ) << 1 ) );
			clientPrediction = idMath::ClampInt( idAsyncNetwork::clientPrediction.GetInteger(), idAsyncNetwork::clientMaxPrediction.GetInteger(), clientPrediction );
			delta = gameTime - ( snapshotGameTime + clientPrediction );
			clientPredictTime -= ( delta / PREDICTION_FAST_ADJUST ) + ( 1 - ( INTSIGNBITSET( delta ) << 1 ) );

			lastSnapshotTime = clientTime;

			if ( idAsyncNetwork::verbose.GetInteger() == 2 ) {
				common->Printf( "received snapshot, gameInitId = %d, gameFrame = %d, gameTime = %d\n", gameInitId, gameFrame, gameTime );
			}

			if ( numDuplicatedUsercmds && ( idAsyncNetwork::verbose.GetInteger() == 2 ) ) {
				common->Printf( "server duplicated %d user commands before snapshot %d\n", numDuplicatedUsercmds, snapshotGameFrame );
			}
			break;
		}
		default: {
			common->Printf( "unknown unreliable server message %d\n", id );
			break;
		}
	}
}

/*
==================
idAsyncClient::ProcessReliableMessagePure
==================
*/
void idAsyncClient::ProcessReliableMessagePure( const idBitMsg &msg ) {
	idBitMsg	outMsg;
	byte		msgBuf[ MAX_MESSAGE_SIZE ];
	int			inChecksums[ MAX_PURE_PAKS ];
	int			i;
	int			serverGameInitId;

	session->SetGUI( NULL, NULL );

	serverGameInitId = msg.ReadInt();

	if ( serverGameInitId != gameInitId ) {
		common->DPrintf( "ignoring pure server checksum from an outdated gameInitId (%d)\n", serverGameInitId );
		return;
	}

	if ( !ValidatePureServerChecksums( serverAddress, msg ) ) {

		return;
	}

	if ( idAsyncNetwork::verbose.GetInteger() ) {
		common->Printf( "received new pure server info. ExecuteMapChange and report back\n" );
	}

	// it is now ok to load the next map with updated pure checksums
	sessLocal.ExecuteMapChange( true );

	// upon receiving our pure list, the server will send us SCS_INGAME and we'll start getting snapshots
	fileSystem->GetPureServerChecksums( inChecksums );
	outMsg.Init( msgBuf, sizeof( msgBuf ) );
	outMsg.WriteByte( CLIENT_RELIABLE_MESSAGE_PURE );

	outMsg.WriteInt( gameInitId );

	i = 0;
	while ( inChecksums[ i ] ) {
		outMsg.WriteInt( inChecksums[ i++ ] );
	}
	outMsg.WriteInt( 0 );

	if ( !channel.SendReliableMessage( outMsg ) ) {
		common->Error( "client->server reliable messages overflow\n" );
	}
}

/*
===============
idAsyncClient::ReadLocalizedServerString
===============
*/
void idAsyncClient::ReadLocalizedServerString( const idBitMsg &msg, char *out, int maxLen ) {
	msg.ReadString( out, maxLen );
	// look up localized string. if the message is not an #str_ format, we'll just get it back unchanged
	idStr::snPrintf( out, maxLen - 1, "%s", common->GetLanguageDict()->GetString( out ) );
}

/*
==================
idAsyncClient::ProcessReliableServerMessages
==================
*/
void idAsyncClient::ProcessReliableServerMessages( void ) {
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];
	byte		id;

	msg.Init( msgBuf, sizeof( msgBuf ) );

	while ( channel.GetReliableMessage( msg ) ) {
		id = msg.ReadByte();
		switch( id ) {
			case SERVER_RELIABLE_MESSAGE_CLIENTINFO: {
				int clientNum;
				clientNum = msg.ReadByte();

				idDict &info = sessLocal.mapSpawnData.userInfo[ clientNum ];
				bool haveBase = ( msg.ReadBits( 1 ) != 0 );

#if ID_CLIENTINFO_TAGS
				int checksum = info.Checksum();
				int srv_checksum = msg.ReadInt();
				if ( checksum != srv_checksum ) {
					common->DPrintf( "SERVER_RELIABLE_MESSAGE_CLIENTINFO %d (haveBase: %s): != checksums srv: 0x%x local: 0x%x\n", clientNum, haveBase ? "true" : "false", checksum, srv_checksum );
					info.Print();
				} else {
					common->DPrintf( "SERVER_RELIABLE_MESSAGE_CLIENTINFO %d (haveBase: %s): checksums ok 0x%x\n", clientNum, haveBase ? "true" : "false", checksum );
				}
#endif

				if ( haveBase ) {
					msg.ReadDeltaDict( info, &info );
				} else {
					msg.ReadDeltaDict( info, NULL );
				}

				// server forces us to a different userinfo
				if ( clientNum == idAsyncClient::clientNum ) {
					common->DPrintf( "local user info modified by server\n" );
					cvarSystem->SetCVarsFromDict( info );
					cvarSystem->ClearModifiedFlags( CVAR_USERINFO ); // don't emit back
				}
				game->SetUserInfo( clientNum, info, true, false );
				break;
			}
			case SERVER_RELIABLE_MESSAGE_SYNCEDCVARS: {
				idDict &info = sessLocal.mapSpawnData.syncedCVars;
				msg.ReadDeltaDict( info, &info );
				cvarSystem->SetCVarsFromDict( info );
				if ( !idAsyncNetwork::allowCheats.GetBool() ) {
					cvarSystem->ResetFlaggedVariables( CVAR_CHEAT );
				}
				break;
			}
			case SERVER_RELIABLE_MESSAGE_PRINT: {
				char string[MAX_STRING_CHARS];
				msg.ReadString( string, MAX_STRING_CHARS );
				common->Printf( "%s\n", string );
				break;
			}
			case SERVER_RELIABLE_MESSAGE_DISCONNECT: {
				int clientNum;
				char string[MAX_STRING_CHARS];
				clientNum = msg.ReadInt( );
				ReadLocalizedServerString( msg, string, MAX_STRING_CHARS );
				if ( clientNum == idAsyncClient::clientNum ) {
					session->Stop();
					session->MessageBox( MSG_OK, string, common->GetLanguageDict()->GetString ( "#str_04319" ), true );
					session->StartMenu();
				} else {
					common->Printf( "client %d %s\n", clientNum, string );
					cmdSystem->BufferCommandText( CMD_EXEC_NOW, va( "addChatLine \"%s^0 %s\"", sessLocal.mapSpawnData.userInfo[ clientNum ].GetString( "ui_name" ), string ) );
					sessLocal.mapSpawnData.userInfo[ clientNum ].Clear();
				}
				break;
			}
			case SERVER_RELIABLE_MESSAGE_APPLYSNAPSHOT: {
				int sequence;
				sequence = msg.ReadInt();
				if ( !game->ClientApplySnapshot( clientNum, sequence ) ) {
					session->Stop();
					common->Error( "couldn't apply snapshot %d", sequence );
				}
				break;
			}
			case SERVER_RELIABLE_MESSAGE_PURE: {
				ProcessReliableMessagePure( msg );
				break;
			}
			case SERVER_RELIABLE_MESSAGE_RELOAD: {
				if ( idAsyncNetwork::verbose.GetBool() ) {
					common->Printf( "got MESSAGE_RELOAD from server\n" );
				}
				// simply reconnect, so that if the server restarts in pure mode we can get the right list and avoid spurious reloads
				cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "reconnect\n" );
				break;
			}
			case SERVER_RELIABLE_MESSAGE_ENTERGAME: {
				SendUserInfoToServer();
				game->SetUserInfo( clientNum, sessLocal.mapSpawnData.userInfo[ clientNum ], true, false );
				cvarSystem->ClearModifiedFlags( CVAR_USERINFO );
				break;
			}
			default: {
				// pass reliable message on to game code
				game->ClientProcessReliableMessage( clientNum, msg );
				break;
			}
		}
	}
}

/*
==================
idAsyncClient::ProcessChallengeResponseMessage
==================
*/
void idAsyncClient::ProcessChallengeResponseMessage( const netadr_t from, const idBitMsg &msg ) {
	char serverGame[ MAX_STRING_CHARS ], serverGameBase[ MAX_STRING_CHARS ];

	if ( clientState != CS_CHALLENGING ) {
		common->Printf( "Unwanted challenge response received.\n" );
		return;
	}

	serverChallenge = msg.ReadInt();
	serverId = msg.ReadShort();
	msg.ReadString( serverGameBase, MAX_STRING_CHARS );
	msg.ReadString( serverGame, MAX_STRING_CHARS );

	// the server is running a different game... we need to reload in the correct fs_game
	// even pure pak checks would fail if we didn't, as there are files we may not even see atm
	// NOTE: we could read the pure list from the server at the same time and set it up for the restart
	// ( if the client can restart directly with the right pak order, then we avoid an extra reloadEngine later.. )
	if ( idStr::Icmp( cvarSystem->GetCVarString( "fs_game_base" ), serverGameBase ) ||
		idStr::Icmp( cvarSystem->GetCVarString( "fs_game" ), serverGame ) ) {
		// bug #189 - if the server is running ROE and ROE is not locally installed, refuse to connect or we might crash
		if ( !fileSystem->HasD3XP() && ( !idStr::Icmp( serverGameBase, "d3xp" ) || !idStr::Icmp( serverGame, "d3xp" ) ) ) {
			common->Printf( "The server is running Doom3: Resurrection of Evil expansion pack. RoE is not installed on this client. Aborting the connection..\n" );
			cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "disconnect\n" );
			return;
		}
		common->Printf( "The server is running a different mod (%s-%s). Restarting..\n", serverGameBase, serverGame );
		cvarSystem->SetCVarString( "fs_game_base", serverGameBase );
		cvarSystem->SetCVarString( "fs_game", serverGame );
		cmdSystem->BufferCommandText( CMD_EXEC_NOW, "reloadEngine" );
		cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "reconnect\n" );
		return;
	}

	common->Printf( "received challenge response 0x%x from %s\n", serverChallenge, Sys_NetAdrToString( from ) );

	// start sending connect packets instead of challenge request packets
	clientState = CS_CONNECTING;
	lastConnectTime = -9999;

	// take this address as the new server address.  This allows
	// a server proxy to hand off connections to multiple servers
	serverAddress = from;
}

/*
==================
idAsyncClient::ProcessConnectResponseMessage
==================
*/
void idAsyncClient::ProcessConnectResponseMessage( const netadr_t from, const idBitMsg &msg ) {
	int serverGameInitId, serverGameFrame, serverGameTime;
	idDict serverSI;

	if ( clientState >= CS_CONNECTED ) {
		common->Printf( "Duplicate connect received.\n" );
		return;
	}
	if ( clientState != CS_CONNECTING ) {
		common->Printf( "Connect response packet while not connecting.\n" );
		return;
	}
	if ( !Sys_CompareNetAdrBase( from, serverAddress ) ) {
		common->Printf( "Connect response from a different server.\n" );
		common->Printf( "%s should have been %s\n", Sys_NetAdrToString( from ), Sys_NetAdrToString( serverAddress ) );
		return;
	}

	common->Printf( "received connect response from %s\n", Sys_NetAdrToString( from ) );

	channel.Init( from, clientId );
	clientNum = msg.ReadInt();
	clientState = CS_CONNECTED;
	lastPacketTime = -9999;

	serverGameInitId = msg.ReadInt();
	serverGameFrame = msg.ReadInt();
	serverGameTime = msg.ReadInt();
	msg.ReadDeltaDict( serverSI, NULL );

	InitGame( serverGameInitId, serverGameFrame, serverGameTime, serverSI );

	// load map
	session->SetGUI( NULL, NULL );
	sessLocal.ExecuteMapChange();

	clientPredictTime = clientPrediction = idMath::ClampInt( 0, idAsyncNetwork::clientMaxPrediction.GetInteger(), clientTime - lastConnectTime );
}

/*
==================
idAsyncClient::ProcessDisconnectMessage
==================
*/
void idAsyncClient::ProcessDisconnectMessage( const netadr_t from, const idBitMsg &msg ) {
	if ( clientState == CS_DISCONNECTED ) {
		common->Printf( "Disconnect packet while not connected.\n" );
		return;
	}
	if ( !Sys_CompareNetAdrBase( from, serverAddress ) ) {
		common->Printf( "Disconnect packet from unknown server.\n" );
		return;
	}
	session->Stop();
	session->MessageBox( MSG_OK, common->GetLanguageDict()->GetString ( "#str_04320" ), NULL, true );
	session->StartMenu();
}

/*
==================
idAsyncClient::ProcessInfoResponseMessage
==================
*/
void idAsyncClient::ProcessInfoResponseMessage( const netadr_t from, const idBitMsg &msg ) {
	int i, protocol, index;
	networkServer_t serverInfo;
	bool verbose = false;

	if ( from.type == NA_LOOPBACK || cvarSystem->GetCVarBool( "developer" ) ) {
		verbose = true;
	}

	serverInfo.clients = 0;
	serverInfo.adr = from;
	serverInfo.challenge = msg.ReadInt();			// challenge
	protocol = msg.ReadInt();
	if ( protocol != ASYNC_PROTOCOL_VERSION ) {
		common->Printf( "server %s ignored - protocol %d.%d, expected %d.%d\n", Sys_NetAdrToString( serverInfo.adr ), protocol >> 16, protocol & 0xffff, ASYNC_PROTOCOL_MAJOR, ASYNC_PROTOCOL_MINOR );
		return;
	}
	msg.ReadDeltaDict( serverInfo.serverInfo, NULL );

	if ( verbose ) {
		common->Printf( "server IP = %s\n", Sys_NetAdrToString( serverInfo.adr ) );
		serverInfo.serverInfo.Print();
	}
	for ( i = msg.ReadByte(); i < MAX_ASYNC_CLIENTS; i = msg.ReadByte() ) {
		serverInfo.pings[ serverInfo.clients ] = msg.ReadShort();
		serverInfo.rate[ serverInfo.clients ] = msg.ReadInt();
		msg.ReadString( serverInfo.nickname[ serverInfo.clients ], MAX_NICKLEN );
		if ( verbose ) {
			common->Printf( "client %2d: %s, ping = %d, rate = %d\n", i, serverInfo.nickname[ serverInfo.clients ], serverInfo.pings[ serverInfo.clients ], serverInfo.rate[ serverInfo.clients ] );
		}
		serverInfo.clients++;
	}
	index = serverList.InfoResponse( serverInfo );

	common->Printf( "%d: server %s - protocol %d.%d - %s\n", index, Sys_NetAdrToString( serverInfo.adr ), protocol >> 16, protocol & 0xffff, serverInfo.serverInfo.GetString( "si_name" ) );
}

/*
==================
idAsyncClient::ProcessPrintMessage
==================
*/
void idAsyncClient::ProcessPrintMessage( const netadr_t from, const idBitMsg &msg ) {
	char		string[ MAX_STRING_CHARS ];
	int			opcode;
	int			game_opcode = ALLOW_YES;
	const char	*retpass;

	opcode = msg.ReadInt();
	if ( opcode == SERVER_PRINT_GAMEDENY ) {
		game_opcode = msg.ReadInt();
	}
	ReadLocalizedServerString( msg, string, MAX_STRING_CHARS );
	common->Printf( "%s\n", string );
	guiNetMenu->SetStateString( "status", string );
	if ( opcode == SERVER_PRINT_GAMEDENY ) {
		if ( game_opcode == ALLOW_BADPASS ) {
			retpass = session->MessageBox( MSG_PROMPT, common->GetLanguageDict()->GetString ( "#str_04321" ), string, true, "passprompt_ok" );
			ClearPendingPackets();
			guiNetMenu->SetStateString( "status",  common->GetLanguageDict()->GetString ( "#str_04322" ));
			if ( retpass ) {
				// #790
				cvarSystem->SetCVarString( "password", "" );
				cvarSystem->SetCVarString( "password", retpass );
			} else {
				cmdSystem->BufferCommandText( CMD_EXEC_NOW, "disconnect" );
			}
		} else if ( game_opcode == ALLOW_NO ) {
			session->MessageBox( MSG_OK, string, common->GetLanguageDict()->GetString ( "#str_04323" ), true );
			ClearPendingPackets();
			cmdSystem->BufferCommandText( CMD_EXEC_NOW, "disconnect" );
		}
		// ALLOW_NOTYET just keeps running as usual. The GUI has an abort button
	} else if ( opcode == SERVER_PRINT_BADCHALLENGE && clientState >= CS_CONNECTING ) {
		cmdSystem->BufferCommandText( CMD_EXEC_NOW, "reconnect" );
	}
}

/*
==================
idAsyncClient::ProcessServersListMessage
==================
*/
void idAsyncClient::ProcessServersListMessage( const netadr_t from, const idBitMsg &msg ) {
	if ( !Sys_CompareNetAdrBase( idAsyncNetwork::GetMasterAddress(), from ) ) {
		common->DPrintf( "received a server list from %s - not a valid master\n", Sys_NetAdrToString( from ) );
		return;
	}
	while ( msg.GetRemaingData() ) {
		int a,b,c,d;
		a = msg.ReadByte(); b = msg.ReadByte(); c = msg.ReadByte(); d = msg.ReadByte();
		serverList.AddServer( serverList.Num(), va( "%i.%i.%i.%i:%i", a, b, c, d, msg.ReadShort() ) );
	}
}

/*
==================
idAsyncClient::ProcessAuthKeyMessage
==================
*/
void idAsyncClient::ProcessAuthKeyMessage( const netadr_t from, const idBitMsg &msg ) {
	authKeyMsg_t		authMsg;
	char				read_string[ MAX_STRING_CHARS ];
	const char			*retkey;
	authBadKeyStatus_t	authBadStatus;
	int					key_index;
	bool				valid[ 2 ];
	idStr				auth_msg;

	if ( clientState != CS_CONNECTING && !session->WaitingForGameAuth() ) {
		common->Printf( "clientState != CS_CONNECTING, not waiting for game auth, authKey ignored\n" );
		return;
	}

	authMsg = (authKeyMsg_t)msg.ReadByte();
	if ( authMsg == AUTHKEY_BADKEY ) {
		valid[ 0 ] = valid[ 1 ] = true;
		key_index = 0;
		authBadStatus = (authBadKeyStatus_t)msg.ReadByte();
		switch ( authBadStatus ) {
		case AUTHKEY_BAD_INVALID:
			valid[ 0 ] = ( msg.ReadByte() == 1 );
			valid[ 1 ] = ( msg.ReadByte() == 1 );
			idAsyncNetwork::BuildInvalidKeyMsg( auth_msg, valid );
			break;
		case AUTHKEY_BAD_BANNED:
			key_index = msg.ReadByte();
			auth_msg = common->GetLanguageDict()->GetString( va( "#str_0719%1d", 6 + key_index ) );
			auth_msg += "\n";
			auth_msg += common->GetLanguageDict()->GetString( "#str_04304" );
			valid[ key_index ] = false;
			break;
		case AUTHKEY_BAD_INUSE:
			key_index = msg.ReadByte();
			auth_msg = common->GetLanguageDict()->GetString( va( "#str_0719%1d", 8 + key_index ) );
			auth_msg += "\n";
			auth_msg += common->GetLanguageDict()->GetString( "#str_04304" );
			valid[ key_index ] = false;
			break;
		case AUTHKEY_BAD_MSG:
			// a general message explaining why this key is denied
			// no specific use for this atm. let's not clear the keys either
			msg.ReadString( read_string, MAX_STRING_CHARS );
			auth_msg = read_string;
			break;
		}
		common->DPrintf( "auth deny: %s\n", auth_msg.c_str() );

		// keys to be cleared. applies to both net connect and game auth
		session->ClearCDKey( valid );

		// get rid of the bad key - at least that's gonna annoy people who stole a fake key
		if ( clientState == CS_CONNECTING ) {
			while ( 1 ) {
				// here we use the auth status message
				retkey = session->MessageBox( MSG_CDKEY, auth_msg, common->GetLanguageDict()->GetString( "#str_04325" ), true );
				if ( retkey ) {
					if ( session->CheckKey( retkey, true, valid ) ) {
						cmdSystem->BufferCommandText( CMD_EXEC_NOW, "reconnect" );
					} else {
						// build a more precise message about the offline check failure
						idAsyncNetwork::BuildInvalidKeyMsg( auth_msg, valid );
						session->MessageBox( MSG_OK, auth_msg.c_str(), common->GetLanguageDict()->GetString( "#str_04327" ), true );
						continue;
					}
				} else {
					cmdSystem->BufferCommandText( CMD_EXEC_NOW, "disconnect" );
				}
				break;
			}
		} else {
			// forward the auth status information to the session code
			session->CDKeysAuthReply( false, auth_msg );
		}
	} else {
		msg.ReadString( read_string, MAX_STRING_CHARS );
		cvarSystem->SetCVarString( "com_guid", read_string );
		common->Printf( "guid set to %s\n", read_string );
		session->CDKeysAuthReply( true, NULL );
	}
}

/*
==================
idAsyncClient::ProcessVersionMessage
==================
*/
void idAsyncClient::ProcessVersionMessage( const netadr_t from, const idBitMsg &msg ) {
	char string[ MAX_STRING_CHARS ];

	if ( updateState != UPDATE_SENT ) {
		common->Printf( "ProcessVersionMessage: version reply, != UPDATE_SENT\n" );
		return;
	}

	common->Printf( "A new version is available\n" );
	msg.ReadString( string, MAX_STRING_CHARS );
	updateMSG = string;
	updateDirectDownload = ( msg.ReadByte() != 0 );
	msg.ReadString( string, MAX_STRING_CHARS );
	updateURL = string;
	updateMime = (dlMime_t)msg.ReadByte();
	msg.ReadString( string, MAX_STRING_CHARS );
	updateFallback = string;
	updateState = UPDATE_READY;
}

/*
==================
idAsyncClient::ValidatePureServerChecksums
==================
*/
bool idAsyncClient::ValidatePureServerChecksums( const netadr_t from, const idBitMsg &msg ) {
	int			i, numChecksums, numMissingChecksums;
	int			inChecksums[ MAX_PURE_PAKS ];
	int			missingChecksums[ MAX_PURE_PAKS ];
	idBitMsg	dlmsg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	// read checksums
	// pak checksums, in a 0-terminated list
	numChecksums = 0;
	do {
		i = msg.ReadInt( );
		inChecksums[ numChecksums++ ] = i;
		// just to make sure a broken message doesn't crash us
		if ( numChecksums >= MAX_PURE_PAKS ) {
			common->Warning( "MAX_PURE_PAKS ( %d ) exceeded in idAsyncClient::ProcessPureMessage\n", MAX_PURE_PAKS );
			return false;
		}
	} while ( i );
	inChecksums[ numChecksums ] = 0;

	fsPureReply_t reply = fileSystem->SetPureServerChecksums( inChecksums, missingChecksums );
	switch ( reply ) {
		case PURE_RESTART:
			// need to restart the filesystem with a different pure configuration
			cmdSystem->BufferCommandText( CMD_EXEC_NOW, "disconnect" );
			// restart with the right FS configuration and get back to the server
			clientState = CS_PURERESTART;
			fileSystem->SetRestartChecksums( inChecksums );
			cmdSystem->BufferCommandText( CMD_EXEC_NOW, "reloadEngine" );
			return false;
		case PURE_MISSING: {

			idStr checksums;

			i = 0;
			while ( missingChecksums[ i ] ) {
				checksums += va( "0x%x ", missingChecksums[ i++ ] );
			}
			numMissingChecksums = i;

			if ( idAsyncNetwork::clientDownload.GetInteger() == 0 ) {
				// never any downloads
				idStr message = va( common->GetLanguageDict()->GetString( "#str_07210" ), Sys_NetAdrToString( from ) );

				if ( numMissingChecksums > 0 ) {
					message += va( common->GetLanguageDict()->GetString( "#str_06751" ), numMissingChecksums, checksums.c_str() );
				}

				common->Printf( message );
				cmdSystem->BufferCommandText( CMD_EXEC_NOW, "disconnect" );
				session->MessageBox( MSG_OK, message, common->GetLanguageDict()->GetString( "#str_06735" ), true );
			} else {
				if ( clientState >= CS_CONNECTED ) {
					// we are already connected, reconnect to negociate the paks in connectionless mode
					cmdSystem->BufferCommandText( CMD_EXEC_NOW, "reconnect" );
					return false;
				}
				// ask the server to send back download info
				common->DPrintf( "missing %d paks: %s\n", numMissingChecksums, checksums.c_str() );
				// store the requested downloads
				GetDownloadRequest( missingChecksums, numMissingChecksums );
				// build the download request message
				// NOTE: in a specific function?
				dlmsg.Init( msgBuf, sizeof( msgBuf ) );
				dlmsg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
				dlmsg.WriteString( "downloadRequest" );
				dlmsg.WriteInt( serverChallenge );
				dlmsg.WriteShort( clientId );
				// used to make sure the server replies to the same download request
				dlmsg.WriteInt( dlRequest );
				// special case the code pak - if we have a 0 checksum then we don't need to download it
				// 0-terminated list of missing paks
				i = 0;
				while ( missingChecksums[ i ] ) {
					dlmsg.WriteInt( missingChecksums[ i++ ] );
				}
				dlmsg.WriteInt( 0 );
				clientPort.SendPacket( from, dlmsg.GetData(), dlmsg.GetSize() );
			}

			return false;
		}
		default:
			break;
	}

	return true;
}

/*
==================
idAsyncClient::ProcessPureMessage
==================
*/
void idAsyncClient::ProcessPureMessage( const netadr_t from, const idBitMsg &msg ) {
	idBitMsg	outMsg;
	byte		msgBuf[ MAX_MESSAGE_SIZE ];
	int			i;
	int			inChecksums[ MAX_PURE_PAKS ];

	if ( clientState != CS_CONNECTING ) {
		common->Printf( "clientState != CS_CONNECTING, pure msg ignored\n" );
		return;
	}

	if ( !ValidatePureServerChecksums( from, msg ) ) {
		return;
	}

	fileSystem->GetPureServerChecksums( inChecksums );
	outMsg.Init( msgBuf, sizeof( msgBuf ) );
	outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
	outMsg.WriteString( "pureClient" );
	outMsg.WriteInt( serverChallenge );
	outMsg.WriteShort( clientId );
	i = 0;
	while ( inChecksums[ i ] ) {
		outMsg.WriteInt( inChecksums[ i++ ] );
	}
	outMsg.WriteInt( 0 );

	clientPort.SendPacket( from, outMsg.GetData(), outMsg.GetSize() );
}

/*
==================
idAsyncClient::ConnectionlessMessage
==================
*/
void idAsyncClient::ConnectionlessMessage( const netadr_t from, const idBitMsg &msg ) {
	char string[MAX_STRING_CHARS*2];  // M. Quinn - Even Balance - PB packets can go beyond 1024

	msg.ReadString( string, sizeof( string ) );

	// info response from a server, are accepted from any source
	if ( idStr::Icmp( string, "infoResponse" ) == 0 ) {
		ProcessInfoResponseMessage( from, msg );
		return;
	}

	// from master server:
	if ( Sys_CompareNetAdrBase( from, idAsyncNetwork::GetMasterAddress( ) ) ) {
		// server list
		if ( idStr::Icmp( string, "servers" ) == 0 ) {
			ProcessServersListMessage( from, msg );
			return;
		}

		if ( idStr::Icmp( string, "authKey" ) == 0 ) {
			ProcessAuthKeyMessage( from, msg );
			return;
		}

		if ( idStr::Icmp( string, "newVersion" ) == 0 ) {
			ProcessVersionMessage( from, msg );
			return;
		}
	}

	// ignore if not from the current/last server
	if ( !Sys_CompareNetAdrBase( from, serverAddress ) && ( lastRconTime + 10000 < realTime || !Sys_CompareNetAdrBase( from, lastRconAddress ) ) ) {
		common->DPrintf( "got message '%s' from bad source: %s\n", string, Sys_NetAdrToString( from ) );
		return;
	}

	// challenge response from the server we are connecting to
	if ( idStr::Icmp( string, "challengeResponse" ) == 0 ) {
		ProcessChallengeResponseMessage( from, msg );
		return;
	}

	// connect response from the server we are connecting to
	if ( idStr::Icmp( string, "connectResponse" ) == 0 ) {
		ProcessConnectResponseMessage( from, msg );
		return;
	}

	// a disconnect message from the server, which will happen if the server
	// dropped the connection but is still getting packets from this client
	if ( idStr::Icmp( string, "disconnect" ) == 0 ) {
		ProcessDisconnectMessage( from, msg );
		return;
	}

	// print request from server
	if ( idStr::Icmp( string, "print" ) == 0 ) {
		ProcessPrintMessage( from, msg );
		return;
	}

	// server pure list
	if ( idStr::Icmp( string, "pureServer" ) == 0 ) {
		ProcessPureMessage( from, msg );
		return;
	}

	if ( idStr::Icmp( string, "downloadInfo" ) == 0 ) {
		ProcessDownloadInfoMessage( from, msg );
	}

	if ( idStr::Icmp( string, "authrequired" ) == 0 ) {
		// server telling us that he's expecting an auth mode connect, just in case we're trying to connect in LAN mode
		if ( idAsyncNetwork::LANServer.GetBool() ) {
			common->Warning( "server %s requests master authorization for this client. Turning off LAN mode\n", Sys_NetAdrToString( from ) );
			idAsyncNetwork::LANServer.SetBool( false );
		}
	}

	common->DPrintf( "ignored message from %s: %s\n", Sys_NetAdrToString( from ), string );
}

/*
=================
idAsyncClient::ProcessMessage
=================
*/
void idAsyncClient::ProcessMessage( const netadr_t from, idBitMsg &msg ) {
	int id;

	id = msg.ReadShort();

	// check for a connectionless packet
	if ( id == CONNECTIONLESS_MESSAGE_ID ) {
		ConnectionlessMessage( from, msg );
		return;
	}

	if ( clientState < CS_CONNECTED ) {
		return;		// can't be a valid sequenced packet
	}

	if ( msg.GetRemaingData() < 4 ) {
		common->DPrintf( "%s: tiny packet\n", Sys_NetAdrToString( from ) );
		return;
	}

	// is this a packet from the server
	if ( !Sys_CompareNetAdrBase( from, channel.GetRemoteAddress() ) || id != serverId ) {
		common->DPrintf( "%s: sequenced server packet without connection\n", Sys_NetAdrToString( from ) );
		return;
	}

	if ( !channel.Process( from, clientTime, msg, serverMessageSequence ) ) {
		return;		// out of order, duplicated, fragment, etc.
	}

	lastPacketTime = clientTime;
	ProcessReliableServerMessages();
	ProcessUnreliableServerMessage( msg );
}

/*
==================
idAsyncClient::SetupConnection
==================
*/
void idAsyncClient::SetupConnection( void ) {
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	if ( clientTime - lastConnectTime < SETUP_CONNECTION_RESEND_TIME ) {
		return;
	}

	if ( clientState == CS_CHALLENGING ) {
		common->Printf( "sending challenge to %s\n", Sys_NetAdrToString( serverAddress ) );
		msg.Init( msgBuf, sizeof( msgBuf ) );
		msg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
		msg.WriteString( "challenge" );
		msg.WriteInt( clientId );
		clientPort.SendPacket( serverAddress, msg.GetData(), msg.GetSize() );
	} else if ( clientState == CS_CONNECTING ) {
		common->Printf( "sending connect to %s with challenge 0x%x\n", Sys_NetAdrToString( serverAddress ), serverChallenge );
		msg.Init( msgBuf, sizeof( msgBuf ) );
		msg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
		msg.WriteString( "connect" );
		msg.WriteInt( ASYNC_PROTOCOL_VERSION );
		msg.WriteInt( clientDataChecksum );
		msg.WriteInt( serverChallenge );
		msg.WriteShort( clientId );
		msg.WriteInt( cvarSystem->GetCVarInteger( "net_clientMaxRate" ) );
		msg.WriteString( cvarSystem->GetCVarString( "com_guid" ) );
		msg.WriteString( cvarSystem->GetCVarString( "password" ), -1, false );
		// do not make the protocol depend on PB
		msg.WriteShort( 0 );
		clientPort.SendPacket( serverAddress, msg.GetData(), msg.GetSize() );
#if ID_ENFORCE_KEY_CLIENT
		if ( idAsyncNetwork::LANServer.GetBool() ) {
			common->Printf( "net_LANServer is set, connecting in LAN mode\n" );
		} else {
			// emit a cd key authorization request
			// modified at protocol 1.37 for XP key addition
			msg.BeginWriting();
			msg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
			msg.WriteString( "clAuth" );
			msg.WriteInt( ASYNC_PROTOCOL_VERSION );
			msg.WriteNetadr( serverAddress );
			// if we don't have a com_guid, this will request a direct reply from auth with it
			msg.WriteByte( cvarSystem->GetCVarString( "com_guid" )[0] ? 1 : 0 );
			// send the main key, and flag an extra byte to add XP key
			msg.WriteString( session->GetCDKey( false ) );
			const char *xpkey = session->GetCDKey( true );
			msg.WriteByte( xpkey ? 1 : 0 );
			if ( xpkey ) {
				msg.WriteString( xpkey );
			}
			clientPort.SendPacket( idAsyncNetwork::GetMasterAddress(), msg.GetData(), msg.GetSize() );
		}
#else
		if (! Sys_IsLANAddress( serverAddress ) ) {
			common->Printf( "Build Does not have CD Key Enforcement enabled. The Server ( %s ) is not within the lan addresses. Attemting to connect.\n", Sys_NetAdrToString( serverAddress ) );
		}
		common->Printf( "Not Testing key.\n" );
#endif
	} else {
		return;
	}

	lastConnectTime = clientTime;
}

/*
==================
idAsyncClient::SendReliableGameMessage
==================
*/
void idAsyncClient::SendReliableGameMessage( const idBitMsg &msg ) {
	idBitMsg	outMsg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	if ( clientState < CS_INGAME ) {
		return;
	}

	outMsg.Init( msgBuf, sizeof( msgBuf ) );
	outMsg.WriteByte( CLIENT_RELIABLE_MESSAGE_GAME );
	outMsg.WriteData( msg.GetData(), msg.GetSize() );
	if ( !channel.SendReliableMessage( outMsg ) ) {
		common->Error( "client->server reliable messages overflow\n" );
	}
}

/*
==================
idAsyncClient::Idle
==================
*/
void idAsyncClient::Idle( void ) {
	// also need to read mouse for the connecting guis
	usercmdGen->GetDirectUsercmd();

	SendEmptyToServer();
}

/*
==================
idAsyncClient::UpdateTime
==================
*/
int idAsyncClient::UpdateTime( int clamp ) {
	int time, msec;

	time = Sys_Milliseconds();
	msec = idMath::ClampInt( 0, clamp, time - realTime );
	realTime = time;
	clientTime += msec;
	return msec;
}

/*
==================
idAsyncClient::RunFrame
==================
*/
void idAsyncClient::RunFrame( void ) {
	int			msec, size;
	bool		newPacket;
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];
	netadr_t	from;

	msec = UpdateTime( 100 );

	if ( !clientPort.GetPort() ) {
		return;
	}

	// handle ongoing pk4 downloads and patch downloads
	HandleDownloads();

	gameTimeResidual += msec;

	// spin in place processing incoming packets until enough time lapsed to run a new game frame
	do {

		do {

			// blocking read with game time residual timeout
			newPacket = clientPort.GetPacketBlocking( from, msgBuf, size, sizeof( msgBuf ), USERCMD_MSEC - ( gameTimeResidual + clientPredictTime ) - 1 );
			if ( newPacket ) {
				msg.Init( msgBuf, sizeof( msgBuf ) );
				msg.SetSize( size );
				msg.BeginReading();
				ProcessMessage( from, msg );
			}

			msec = UpdateTime( 100 );
			gameTimeResidual += msec;

		} while( newPacket );

	} while( gameTimeResidual + clientPredictTime < USERCMD_MSEC );

	// update server list
	serverList.RunFrame();

	if ( clientState == CS_DISCONNECTED ) {
		usercmdGen->GetDirectUsercmd();
		gameTimeResidual = USERCMD_MSEC - 1;
		clientPredictTime = 0;
		return;
	}

	if ( clientState == CS_PURERESTART ) {
		clientState = CS_DISCONNECTED;
		Reconnect();
		gameTimeResidual = USERCMD_MSEC - 1;
		clientPredictTime = 0;
		return;
	}

	// if not connected setup a connection
	if ( clientState < CS_CONNECTED ) {
		// also need to read mouse for the connecting guis
		usercmdGen->GetDirectUsercmd();
		SetupConnection();
		gameTimeResidual = USERCMD_MSEC - 1;
		clientPredictTime = 0;
		return;
	}

	if ( CheckTimeout() ) {
		return;
	}

	// if not yet in the game send empty messages to keep data flowing through the channel
	if ( clientState < CS_INGAME ) {
		Idle();
		gameTimeResidual = 0;
		return;
	}

	// check for user info changes
	if ( cvarSystem->GetModifiedFlags() & CVAR_USERINFO ) {
		game->ThrottleUserInfo( );
		SendUserInfoToServer( );
		game->SetUserInfo( clientNum, sessLocal.mapSpawnData.userInfo[ clientNum ], true, false );
		cvarSystem->ClearModifiedFlags( CVAR_USERINFO );
	}

	if ( gameTimeResidual + clientPredictTime >= USERCMD_MSEC ) {
		lastFrameDelta = 0;
	}

	// generate user commands for the predicted time
	while ( gameTimeResidual + clientPredictTime >= USERCMD_MSEC ) {

		// send the user commands of this client to the server
		SendUsercmdsToServer();

		// update time
		gameFrame++;
		gameTime += USERCMD_MSEC;
		gameTimeResidual -= USERCMD_MSEC;

		// run from the snapshot up to the local game frame
		while ( snapshotGameFrame < gameFrame ) {

			lastFrameDelta++;

			// duplicate usercmds for clients if no new ones are available
			DuplicateUsercmds( snapshotGameFrame, snapshotGameTime );

			// indicate the last prediction frame before a render
			bool lastPredictFrame = ( snapshotGameFrame + 1 >= gameFrame && gameTimeResidual + clientPredictTime < USERCMD_MSEC );

			// run client prediction
			gameReturn_t ret = game->ClientPrediction( clientNum, userCmds[ snapshotGameFrame & ( MAX_USERCMD_BACKUP - 1 ) ], lastPredictFrame );

			idAsyncNetwork::ExecuteSessionCommand( ret.sessionCommand );

			snapshotGameFrame++;
			snapshotGameTime += USERCMD_MSEC;
		}
	}
}

/*
==================
idAsyncClient::PacifierUpdate
==================
*/
void idAsyncClient::PacifierUpdate( void ) {
	if ( !IsActive() ) {
		return;
	}
	realTime = Sys_Milliseconds();
	SendEmptyToServer( false, true );
}

/*
==================
idAsyncClient::SendVersionCheck
==================
*/
void idAsyncClient::SendVersionCheck( bool fromMenu ) {
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	if ( updateState != UPDATE_NONE && !fromMenu ) {
		common->DPrintf( "up-to-date check was already performed\n" );
		return;
	}

	InitPort();
	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
	msg.WriteString( "versionCheck" );
	msg.WriteInt( ASYNC_PROTOCOL_VERSION );
	msg.WriteString( cvarSystem->GetCVarString( "si_version" ) );
	msg.WriteString( cvarSystem->GetCVarString( "com_guid" ) );
	clientPort.SendPacket( idAsyncNetwork::GetMasterAddress(), msg.GetData(), msg.GetSize() );

	common->DPrintf( "sent a version check request\n" );

	updateState = UPDATE_SENT;
	updateSentTime = clientTime;
	showUpdateMessage = fromMenu;
}

/*
==================
idAsyncClient::SendVersionDLUpdate

sending those packets is not strictly necessary. just a way to tell the update server
about what is going on. allows the update server to have a more precise view of the overall
network load for the updates
==================
*/
void idAsyncClient::SendVersionDLUpdate( int state ) {
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
	msg.WriteString( "versionDL" );
	msg.WriteInt( ASYNC_PROTOCOL_VERSION );
	msg.WriteShort( state );
	clientPort.SendPacket( idAsyncNetwork::GetMasterAddress(), msg.GetData(), msg.GetSize() );
}

/*
==================
idAsyncClient::HandleDownloads
==================
*/
void idAsyncClient::HandleDownloads( void ) {

	if ( updateState == UPDATE_SENT && clientTime > updateSentTime + 2000 ) {
		// timing out on no reply
		updateState = UPDATE_DONE;
		if ( showUpdateMessage ) {
			session->MessageBox( MSG_OK, common->GetLanguageDict()->GetString ( "#str_04839" ), common->GetLanguageDict()->GetString ( "#str_04837" ), true );
			showUpdateMessage = false;
		}
		common->DPrintf( "No update available\n" );
	} else if ( backgroundDownload.completed ) {
		// only enter these if the download slot is free
		if ( updateState == UPDATE_READY ) {
			//
			if ( session->MessageBox( MSG_YESNO, updateMSG, common->GetLanguageDict()->GetString ( "#str_04330" ), true, "yes" )[0] ) {
				if ( !updateDirectDownload ) {
					sys->OpenURL( updateURL, true );
					updateState = UPDATE_DONE;
				} else {

					// we're just creating the file at toplevel inside fs_savepath
					updateURL.ExtractFileName( updateFile );
					idFile_Permanent *f = static_cast< idFile_Permanent *>( fileSystem->OpenFileWrite( updateFile ) );
					dltotal = 0;
					dlnow = 0;

					backgroundDownload.completed = false;
					backgroundDownload.opcode = DLTYPE_URL;
					backgroundDownload.f = f;
					backgroundDownload.url.status = DL_WAIT;
					backgroundDownload.url.dlnow = 0;
					backgroundDownload.url.dltotal = 0;
					backgroundDownload.url.url = updateURL;
					fileSystem->BackgroundDownload( &backgroundDownload );

					updateState = UPDATE_DLING;
					SendVersionDLUpdate( 0 );
					session->DownloadProgressBox( &backgroundDownload, va( "Downloading %s\n", updateFile.c_str() ) );
					updateState = UPDATE_DONE;
					if ( backgroundDownload.url.status == DL_DONE ) {
						SendVersionDLUpdate( 1 );
						idStr fullPath = f->GetFullPath();
						fileSystem->CloseFile( f );
						if ( session->MessageBox( MSG_YESNO, common->GetLanguageDict()->GetString ( "#str_04331" ), common->GetLanguageDict()->GetString ( "#str_04332" ), true, "yes" )[0] ) {
							if ( updateMime == DL_FILE_EXEC ) {
								sys->StartProcess( fullPath, true );
							} else {
								sys->OpenURL( va( "file://%s", fullPath.c_str() ), true );
							}
						} else {
							session->MessageBox( MSG_OK, va( common->GetLanguageDict()->GetString ( "#str_04333" ), fullPath.c_str() ), common->GetLanguageDict()->GetString ( "#str_04334" ), true );
						}
					} else {
						if ( backgroundDownload.url.dlerror[ 0 ] ) {
							common->Warning( "update download failed. curl error: %s", backgroundDownload.url.dlerror );
						}
						SendVersionDLUpdate( 2 );
						idStr name = f->GetName();
						fileSystem->CloseFile( f );
						fileSystem->RemoveFile( name );
						session->MessageBox( MSG_OK, common->GetLanguageDict()->GetString ( "#str_04335" ), common->GetLanguageDict()->GetString ( "#str_04336" ), true );
						if ( updateFallback.Length() ) {
							sys->OpenURL( updateFallback.c_str(), true );
						} else {
							common->Printf( "no fallback URL\n" );
						}
					}
				}
			} else {
				updateState = UPDATE_DONE;
			}
		} else if ( dlList.Num() ) {

			int numPaks = dlList.Num();
			int pakCount = 1;
			int progress_start, progress_end;
			currentDlSize = 0;

			do {

				if ( dlList[ 0 ].url[ 0 ] == '\0' ) {
					// ignore empty files
					dlList.RemoveIndex( 0 );
					continue;
				}
				common->Printf( "start download for %s\n", dlList[ 0 ].url.c_str() );

				idFile_Permanent *f = static_cast< idFile_Permanent *>( fileSystem->MakeTemporaryFile( ) );
				if ( !f ) {
					common->Warning( "could not create temporary file" );
					dlList.Clear();
					return;
				}

				backgroundDownload.completed = false;
				backgroundDownload.opcode = DLTYPE_URL;
				backgroundDownload.f = f;
				backgroundDownload.url.status = DL_WAIT;
				backgroundDownload.url.dlnow = 0;
				backgroundDownload.url.dltotal = dlList[ 0 ].size;
				backgroundDownload.url.url = dlList[ 0 ].url;
				fileSystem->BackgroundDownload( &backgroundDownload );
				idStr dltitle;
				// "Downloading %s"
				sprintf( dltitle, common->GetLanguageDict()->GetString( "#str_07213" ), dlList[ 0 ].filename.c_str() );
				if ( numPaks > 1 ) {
					dltitle += va( " (%d/%d)", pakCount, numPaks );
				}
				if ( totalDlSize ) {
					progress_start = (int)( (float)currentDlSize * 100.0f / (float)totalDlSize );
					progress_end = (int)( (float)( currentDlSize + dlList[ 0 ].size ) * 100.0f / (float)totalDlSize );
				} else {
					progress_start = 0;
					progress_end = 100;
				}
				session->DownloadProgressBox( &backgroundDownload, dltitle, progress_start, progress_end );
				if ( backgroundDownload.url.status == DL_DONE ) {
					idFile		*saveas;
					const int	CHUNK_SIZE = 1024 * 1024;
					byte		*buf;
					int			remainlen;
					int			readlen;
					int			retlen;
					int			checksum;

					common->Printf( "file downloaded\n" );
					idStr finalPath = cvarSystem->GetCVarString( "fs_savepath" );
					finalPath.AppendPath( dlList[ 0 ].filename );
					fileSystem->CreateOSPath( finalPath );
					// do the final copy ourselves so we do by small chunks in case the file is big
					saveas = fileSystem->OpenExplicitFileWrite( finalPath );
					buf = (byte*)Mem_Alloc( CHUNK_SIZE );
					f->Seek( 0, FS_SEEK_END );
					remainlen = f->Tell();
					f->Seek( 0, FS_SEEK_SET );
					while ( remainlen ) {
						readlen = Min( remainlen, CHUNK_SIZE );
						retlen = f->Read( buf, readlen );
						if ( retlen != readlen ) {
							common->FatalError( "short read %d of %d in idFileSystem::HandleDownload", retlen, readlen );
						}
						retlen = saveas->Write( buf, readlen );
						if ( retlen != readlen ) {
							common->FatalError( "short write %d of %d in idFileSystem::HandleDownload", retlen, readlen );
						}
						remainlen -= readlen;
					}
					fileSystem->CloseFile( f );
					fileSystem->CloseFile( saveas );
					common->Printf( "saved as %s\n", finalPath.c_str() );
					Mem_Free( buf );

					// add that file to our paks list
					checksum = fileSystem->AddZipFile( dlList[ 0 ].filename );

					// verify the checksum to be what the server says
					if ( !checksum || checksum != dlList[ 0 ].checksum ) {
						// "pak is corrupted ( checksum 0x%x, expected 0x%x )"
						session->MessageBox( MSG_OK, va( common->GetLanguageDict()->GetString( "#str_07214" ) , checksum, dlList[0].checksum ), "Download failed", true );
						fileSystem->RemoveFile( dlList[ 0 ].filename );
						dlList.Clear();
						return;
					}

					currentDlSize += dlList[ 0 ].size;

				} else {
					common->Warning( "download failed: %s", dlList[ 0 ].url.c_str() );
					if ( backgroundDownload.url.dlerror[ 0 ] ) {
						common->Warning( "curl error: %s", backgroundDownload.url.dlerror );
					}
					// "The download failed or was cancelled"
					// "Download failed"
					session->MessageBox( MSG_OK, common->GetLanguageDict()->GetString( "#str_07215" ), common->GetLanguageDict()->GetString( "#str_07216" ), true );
					dlList.Clear();
					return;
				}

				pakCount++;
				dlList.RemoveIndex( 0 );
			} while ( dlList.Num() );

			// all downloads successful - do the dew
			cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "reconnect\n" );
		}
	}
}

/*
===============
idAsyncClient::SendAuthCheck
===============
*/
bool idAsyncClient::SendAuthCheck( const char *cdkey, const char *xpkey ) {
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
	msg.WriteString( "gameAuth" );
	msg.WriteInt( ASYNC_PROTOCOL_VERSION );
	msg.WriteByte( cdkey ? 1 : 0 );
	msg.WriteString( cdkey ? cdkey : "" );
	msg.WriteByte( xpkey ? 1 : 0 );
	msg.WriteString( xpkey ? xpkey : "" );
	InitPort();
	clientPort.SendPacket( idAsyncNetwork::GetMasterAddress(), msg.GetData(), msg.GetSize() );
	return true;
}

/*
===============
idAsyncClient::CheckTimeout
===============
*/
bool idAsyncClient::CheckTimeout( void ) {
	if ( lastPacketTime > 0 && ( lastPacketTime + idAsyncNetwork::clientServerTimeout.GetInteger()*1000 < clientTime ) ) {
		session->StopBox();
		session->MessageBox( MSG_OK, common->GetLanguageDict()->GetString ( "#str_04328" ), common->GetLanguageDict()->GetString ( "#str_04329" ), true );
		cmdSystem->BufferCommandText( CMD_EXEC_NOW, "disconnect" );
		return true;
	}
	return false;
}

/*
===============
idAsyncClient::ProcessDownloadInfoMessage
===============
*/
void idAsyncClient::ProcessDownloadInfoMessage( const netadr_t from, const idBitMsg &msg ) {
	char			buf[ MAX_STRING_CHARS ];
	int				srvDlRequest = msg.ReadInt();
	int				infoType = msg.ReadByte();
	int				pakDl;
	int				pakIndex;

	pakDlEntry_t	entry;
	bool			gotAllFiles = true;
	idStr			sizeStr;
	bool			gotGame = false;

	if ( dlRequest == -1 || srvDlRequest != dlRequest ) {
		common->Warning( "bad download id from server, ignored" );
		return;
	}
	// mark the dlRequest as dead now whatever how we process it
	dlRequest = -1;

	if ( infoType == SERVER_DL_REDIRECT ) {
		msg.ReadString( buf, MAX_STRING_CHARS );
		cmdSystem->BufferCommandText( CMD_EXEC_NOW, "disconnect" );
		// "You are missing required pak files to connect to this server.\nThe server gave a web page though:\n%s\nDo you want to go there now?"
		// "Missing required files"
		if ( session->MessageBox( MSG_YESNO, va( common->GetLanguageDict()->GetString( "#str_07217" ), buf ),
								  common->GetLanguageDict()->GetString( "#str_07218" ), true, "yes" )[ 0 ] ) {
			sys->OpenURL( buf, true );
		}
	} else if ( infoType == SERVER_DL_LIST ) {
		cmdSystem->BufferCommandText( CMD_EXEC_NOW, "disconnect" );
		if ( dlList.Num() ) {
			common->Warning( "tried to process a download list while already busy downloading things" );
			return;
		}
		// read the URLs, check against what we requested, prompt for download
		pakIndex = -1;
		totalDlSize = 0;
		do {
			pakIndex++;
			pakDl = msg.ReadByte();
			if ( pakDl == SERVER_PAK_YES ) {
				if ( pakIndex == 0 ) {
					gotGame = true;
				}
				msg.ReadString( buf, MAX_STRING_CHARS );
				entry.filename = buf;
				msg.ReadString( buf, MAX_STRING_CHARS );
				entry.url = buf;
				entry.size = msg.ReadInt();
				// checksums are not transmitted, we read them from the dl request we sent
				entry.checksum = dlChecksums[ pakIndex ];
				totalDlSize += entry.size;
				dlList.Append( entry );
				common->Printf( "download %s from %s ( 0x%x )\n", entry.filename.c_str(), entry.url.c_str(), entry.checksum );
			} else if ( pakDl == SERVER_PAK_NO ) {
				msg.ReadString( buf, MAX_STRING_CHARS );
				entry.filename = buf;
				entry.url = "";
				entry.size = 0;
				entry.checksum = 0;
				dlList.Append( entry );
				// first pak is game pak, only fail it if we actually requested it
				if ( pakIndex != 0 || dlChecksums[ 0 ] != 0 ) {
					common->Printf( "no download offered for %s ( 0x%x )\n", entry.filename.c_str(), dlChecksums[ pakIndex ] );
					gotAllFiles = false;
				}
			} else {
				assert( pakDl == SERVER_PAK_END );
			}
		} while ( pakDl != SERVER_PAK_END );
		if ( dlList.Num() < dlCount ) {
			common->Printf( "%d files were ignored by the server\n", dlCount - dlList.Num() );
			gotAllFiles = false;
		}
		sizeStr.BestUnit( "%.2f", totalDlSize, MEASURE_SIZE );
		cmdSystem->BufferCommandText( CMD_EXEC_NOW, "disconnect" );
		if ( totalDlSize == 0 ) {
			// was no downloadable stuff for us
			// "Can't connect to the pure server: no downloads offered"
			// "Missing required files"
			dlList.Clear();
			session->MessageBox( MSG_OK, common->GetLanguageDict()->GetString( "#str_07219" ), common->GetLanguageDict()->GetString( "#str_07218" ), true );
			return;
		}
		bool asked = false;
		if ( gotGame ) {
			asked = true;
			// "You need to download game code to connect to this server. Are you sure? You should only answer yes if you trust the server administrators."
			// "Missing game binaries"
			if ( !session->MessageBox( MSG_YESNO, common->GetLanguageDict()->GetString( "#str_07220" ), common->GetLanguageDict()->GetString( "#str_07221" ), true, "yes" )[ 0 ] ) {
				dlList.Clear();
				return;
			}
		}
		if ( !gotAllFiles ) {
			asked = true;
			// "The server only offers to download some of the files required to connect ( %s ). Download anyway?"
			// "Missing required files"
			if ( !session->MessageBox( MSG_YESNO, va( common->GetLanguageDict()->GetString( "#str_07222" ), sizeStr.c_str() ),
									   common->GetLanguageDict()->GetString( "#str_07218" ), true, "yes" )[ 0 ] ) {
				dlList.Clear();
				return;
			}
		}
		if ( !asked && idAsyncNetwork::clientDownload.GetInteger() == 1 ) {
			// "You need to download some files to connect to this server ( %s ), proceed?"
			// "Missing required files"
			if ( !session->MessageBox( MSG_YESNO, va( common->GetLanguageDict()->GetString( "#str_07224" ), sizeStr.c_str() ),
									   common->GetLanguageDict()->GetString( "#str_07218" ), true, "yes" )[ 0 ] ) {
				dlList.Clear();
				return;
			}
		}
	} else {
		cmdSystem->BufferCommandText( CMD_EXEC_NOW, "disconnect" );
		// "You are missing some files to connect to this server, and the server doesn't provide downloads."
		// "Missing required files"
		session->MessageBox( MSG_OK, common->GetLanguageDict()->GetString( "#str_07223" ), common->GetLanguageDict()->GetString( "#str_07218" ), true );
	}
}

/*
===============
idAsyncClient::GetDownloadRequest
===============
*/
int idAsyncClient::GetDownloadRequest( const int checksums[ MAX_PURE_PAKS ], int count ) {
	assert( !checksums[ count ] ); // 0-terminated
	if ( memcmp( dlChecksums, checksums, sizeof( int ) * count ) ) {
		idRandom newreq;

		memcpy( dlChecksums, checksums, sizeof( int ) * MAX_PURE_PAKS );

		newreq.SetSeed( Sys_Milliseconds() );
		dlRequest = newreq.RandomInt();
		dlCount = count;
		return dlRequest;
	}
	// this is the same dlRequest, we haven't heard from the server. keep the same id
	return dlRequest;
}
