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

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/Session_local.h"
#include "framework/Game.h"

#include "framework/async/AsyncNetwork.h"

const int MIN_RECONNECT_TIME			= 2000;
const int EMPTY_RESEND_TIME				= 500;
const int PING_RESEND_TIME				= 500;
const int NOINPUT_IDLE_TIME				= 30000;

const int HEARTBEAT_MSEC				= 5*60*1000;

// must be kept in sync with authReplyMsg_t
const char* authReplyMsg[] = {
	//	"Waiting for authorization",
	"#str_07204",
	//	"Client unknown to auth",
	"#str_07205",
	//	"Access denied - CD Key in use",
	"#str_07206",
	//	"Auth custom message", // placeholder - we propagate a message from the master
	"#str_07207",
	//	"Authorize Server - Waiting for client"
	"#str_07208"
};

const char* authReplyStr[] = {
	"AUTH_NONE",
	"AUTH_OK",
	"AUTH_WAIT",
	"AUTH_DENY"
};

/*
==================
idAsyncServer::idAsyncServer
==================
*/
idAsyncServer::idAsyncServer( void ) {
	int i;

	active = false;
	realTime = 0;
	serverTime = 0;
	serverId = 0;
	serverDataChecksum = 0;
	localClientNum = -1;
	gameInitId = 0;
	gameFrame = 0;
	gameTime = 0;
	gameTimeResidual = 0;
	memset( challenges, 0, sizeof( challenges ) );
	memset( userCmds, 0, sizeof( userCmds ) );
	for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		ClearClient( i );
	}
	serverReloadingEngine = false;
	nextHeartbeatTime = 0;
	nextAsyncStatsTime = 0;
	noRconOutput = true;
	lastAuthTime = 0;

	memset( stats_outrate, 0, sizeof( stats_outrate ) );
	stats_current = 0;
	stats_average_sum = 0;
	stats_max = 0;
	stats_max_index = 0;
}

/*
==================
idAsyncServer::InitPort
==================
*/
bool idAsyncServer::InitPort( void ) {
	int lastPort;

	// if this is the first time we have spawned a server, open the UDP port
	if ( !serverPort.GetPort() ) {
		if ( cvarSystem->GetCVarInteger( "net_port" ) != 0 ) {
			if ( !serverPort.InitForPort( cvarSystem->GetCVarInteger( "net_port" ) ) ) {
				common->Printf( "Unable to open server on port %d (net_port)\n", cvarSystem->GetCVarInteger( "net_port" ) );
				return false;
			}
		} else {
			// scan for multiple ports, in case other servers are running on this IP already
			for ( lastPort = 0; lastPort < NUM_SERVER_PORTS; lastPort++ ) {
				if ( serverPort.InitForPort( PORT_SERVER + lastPort ) ) {
					break;
				}
			}
			if ( lastPort >= NUM_SERVER_PORTS ) {
				common->Printf( "Unable to open server network port.\n" );
				return false;
			}
		}
	}

	return true;
}

/*
==================
idAsyncServer::ClosePort
==================
*/
void idAsyncServer::ClosePort( void ) {
	int i;

	serverPort.Close();
	for ( i = 0; i < MAX_CHALLENGES; i++ ) {
		challenges[ i ].authReplyPrint.Clear();
	}
}

/*
==================
idAsyncServer::Spawn
==================
*/
void idAsyncServer::Spawn( void ) {
	int			i, size;
	byte		msgBuf[MAX_MESSAGE_SIZE];
	netadr_t	from;

	// shutdown any current game
	session->Stop();

	if ( active ) {
		return;
	}

	if ( !InitPort() ) {
		return;
	}

	// trash any currently pending packets
	while( serverPort.GetPacket( from, msgBuf, size, sizeof( msgBuf ) ) ) {
	}

	// reset cheats cvars
	if ( !idAsyncNetwork::allowCheats.GetBool() ) {
		cvarSystem->ResetFlaggedVariables( CVAR_CHEAT );
	}

	memset( challenges, 0, sizeof( challenges ) );
	memset( userCmds, 0, sizeof( userCmds ) );
	for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		ClearClient( i );
	}

	common->Printf( "Server spawned on port %i.\n", serverPort.GetPort() );

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

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

	active = true;

	nextHeartbeatTime = 0;
	nextAsyncStatsTime = 0;

	ExecuteMapChange();
}

/*
==================
idAsyncServer::Kill
==================
*/
void idAsyncServer::Kill( void ) {
	int i, j;

	if ( !active ) {
		return;
	}

	// drop all clients
	for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		DropClient( i, "#str_07135" );
	}

	// send some empty messages to the zombie clients to make sure they disconnect
	for ( j = 0; j < 4; j++ ) {
		for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
			if ( clients[i].clientState == SCS_ZOMBIE ) {
				if ( clients[i].channel.UnsentFragmentsLeft() ) {
					clients[i].channel.SendNextFragment( serverPort, serverTime );
				} else {
					SendEmptyToClient( i, true );
				}
			}
		}
		Sys_Sleep( 10 );
	}

	// reset any pureness
	fileSystem->ClearPureChecksums();

	active = false;

	// shutdown any current game
	session->Stop();
}

/*
==================
idAsyncServer::ExecuteMapChange
==================
*/
void idAsyncServer::ExecuteMapChange( void ) {
	int			i;
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];
	idStr		mapName;
	findFile_t	ff;
	bool		addonReload = false;
	char		bestGameType[ MAX_STRING_CHARS ];

	assert( active );

	// reset any pureness
	fileSystem->ClearPureChecksums();

	// make sure the map/gametype combo is good
	game->GetBestGameType( cvarSystem->GetCVarString("si_map"), cvarSystem->GetCVarString("si_gametype"), bestGameType );
	cvarSystem->SetCVarString("si_gametype", bestGameType );

	// initialize map settings
	cmdSystem->BufferCommandText( CMD_EXEC_NOW, "rescanSI" );

	sprintf( mapName, "maps/%s", sessLocal.mapSpawnData.serverInfo.GetString( "si_map" ) );
	mapName.SetFileExtension( ".map" );
	ff = fileSystem->FindFile( mapName, !serverReloadingEngine );
	switch( ff ) {
	case FIND_NO:
		common->Printf( "Can't find map %s\n", mapName.c_str() );
		cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "disconnect\n" );
		return;
	case FIND_ADDON:
		// NOTE: we have no problem with addon dependencies here because if the map is in
		// an addon pack that's already on search list, then all it's deps are assumed to be on search as well
		common->Printf( "map %s is in an addon pak - reloading\n", mapName.c_str() );
		addonReload = true;
		break;
	default:
		break;
	}

	// if we are asked to do a full reload, the strategy is completely different
	if ( !serverReloadingEngine && ( addonReload || idAsyncNetwork::serverReloadEngine.GetInteger() != 0 ) ) {
		if ( idAsyncNetwork::serverReloadEngine.GetInteger() != 0 ) {
			common->Printf( "net_serverReloadEngine enabled - doing a full reload\n" );
		}
		// tell the clients to reconnect
		// FIXME: shouldn't they wait for the new pure list, then reload?
		// in a lot of cases this is going to trigger two reloadEngines for the clients
		// one to restart, the other one to set paks right ( with addon for instance )
		// can fix by reconnecting without reloading and waiting for the server to tell..
		for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
			if ( clients[ i ].clientState >= SCS_PUREWAIT && i != localClientNum ) {
				msg.Init( msgBuf, sizeof( msgBuf ) );
				msg.WriteByte( SERVER_RELIABLE_MESSAGE_RELOAD );
				SendReliableMessage( i, msg );
				clients[ i ].clientState = SCS_ZOMBIE; // so we don't bother sending a disconnect
			}
		}
		cmdSystem->BufferCommandText( CMD_EXEC_NOW, "reloadEngine" );
		serverReloadingEngine = true; // don't get caught in endless loop
		cmdSystem->BufferCommandText( CMD_EXEC_APPEND, "spawnServer\n" );
		// decrease feature
		if ( idAsyncNetwork::serverReloadEngine.GetInteger() > 0 ) {
			idAsyncNetwork::serverReloadEngine.SetInteger( idAsyncNetwork::serverReloadEngine.GetInteger() - 1 );
		}
		return;
	}
	serverReloadingEngine = false;

	serverTime = 0;

	// initialize game id and time
	gameInitId ^= Sys_Milliseconds();	// NOTE: make sure the gameInitId is always a positive number because negative numbers have special meaning
	gameFrame = 0;
	gameTime = 0;
	gameTimeResidual = 0;
	memset( userCmds, 0, sizeof( userCmds ) );

	if ( idAsyncNetwork::serverDedicated.GetInteger() == 0 ) {
		InitLocalClient( 0 );
	} else {
		localClientNum = -1;
	}

	// re-initialize all connected clients for the new map
	for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		if ( clients[i].clientState >= SCS_PUREWAIT && i != localClientNum ) {

			InitClient( i, clients[i].clientId, clients[i].clientRate );

			SendGameInitToClient( i );

			if ( sessLocal.mapSpawnData.serverInfo.GetBool( "si_pure" ) ) {
				clients[ i ].clientState = SCS_PUREWAIT;
			}
		}
	}

	// load map
	sessLocal.ExecuteMapChange();

	if ( localClientNum >= 0 ) {
		BeginLocalClient();
	} else {
		game->SetLocalClient( -1 );
	}

	if ( sessLocal.mapSpawnData.serverInfo.GetInt( "si_pure" ) ) {
		// lock down the pak list
		fileSystem->UpdatePureServerChecksums( );
		// tell the clients so they can work out their pure lists
		for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
			if ( clients[ i ].clientState == SCS_PUREWAIT ) {
				if ( !SendReliablePureToClient( i ) ) {
					clients[ i ].clientState = SCS_CONNECTED;
				}
			}
		}
	}

	// serverTime gets reset, force a heartbeat so timings restart
	MasterHeartbeat( true );
}

/*
==================
idAsyncServer::GetPort
==================
*/
int idAsyncServer::GetPort( void ) const {
	return serverPort.GetPort();
}

/*
===============
idAsyncServer::GetBoundAdr
===============
*/
netadr_t idAsyncServer::GetBoundAdr( void ) const {
	return serverPort.GetAdr();
}

/*
==================
idAsyncServer::GetOutgoingRate
==================
*/
int idAsyncServer::GetOutgoingRate( void ) const {
	int i, rate;

	rate = 0;
	for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		const serverClient_t &client = clients[i];

		if ( client.clientState >= SCS_CONNECTED ) {
			rate += client.channel.GetOutgoingRate();
		}
	}
	return rate;
}

/*
==================
idAsyncServer::GetIncomingRate
==================
*/
int idAsyncServer::GetIncomingRate( void ) const {
	int i, rate;

	rate = 0;
	for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		const serverClient_t &client = clients[i];

		if ( client.clientState >= SCS_CONNECTED ) {
			rate += client.channel.GetIncomingRate();
		}
	}
	return rate;
}

/*
==================
idAsyncServer::IsClientInGame
==================
*/
bool idAsyncServer::IsClientInGame( int clientNum ) const {
	return ( clients[clientNum].clientState >= SCS_INGAME );
}

/*
==================
idAsyncServer::GetClientPing
==================
*/
int idAsyncServer::GetClientPing( int clientNum ) const {
	const serverClient_t &client = clients[clientNum];

	if ( client.clientState < SCS_CONNECTED ) {
		return 99999;
	} else {
		return client.clientPing;
	}
}

/*
==================
idAsyncServer::GetClientPrediction
==================
*/
int idAsyncServer::GetClientPrediction( int clientNum ) const {
	const serverClient_t &client = clients[clientNum];

	if ( client.clientState < SCS_CONNECTED ) {
		return 99999;
	} else {
		return client.clientPrediction;
	}
}

/*
==================
idAsyncServer::GetClientTimeSinceLastPacket
==================
*/
int idAsyncServer::GetClientTimeSinceLastPacket( int clientNum ) const {
	const serverClient_t &client = clients[clientNum];

	if ( client.clientState < SCS_CONNECTED ) {
		return 99999;
	} else {
		return serverTime - client.lastPacketTime;
	}
}

/*
==================
idAsyncServer::GetClientTimeSinceLastInput
==================
*/
int idAsyncServer::GetClientTimeSinceLastInput( int clientNum ) const {
	const serverClient_t &client = clients[clientNum];

	if ( client.clientState < SCS_CONNECTED ) {
		return 99999;
	} else {
		return serverTime - client.lastInputTime;
	}
}

/*
==================
idAsyncServer::GetClientOutgoingRate
==================
*/
int idAsyncServer::GetClientOutgoingRate( int clientNum ) const {
	const serverClient_t &client = clients[clientNum];

	if ( client.clientState < SCS_CONNECTED ) {
		return -1;
	} else {
		return client.channel.GetOutgoingRate();
	}
}

/*
==================
idAsyncServer::GetClientIncomingRate
==================
*/
int idAsyncServer::GetClientIncomingRate( int clientNum ) const {
	const serverClient_t &client = clients[clientNum];

	if ( client.clientState < SCS_CONNECTED ) {
		return -1;
	} else {
		return client.channel.GetIncomingRate();
	}
}

/*
==================
idAsyncServer::GetClientOutgoingCompression
==================
*/
float idAsyncServer::GetClientOutgoingCompression( int clientNum ) const {
	const serverClient_t &client = clients[clientNum];

	if ( client.clientState < SCS_CONNECTED ) {
		return 0.0f;
	} else {
		return client.channel.GetOutgoingCompression();
	}
}

/*
==================
idAsyncServer::GetClientIncomingCompression
==================
*/
float idAsyncServer::GetClientIncomingCompression( int clientNum ) const {
	const serverClient_t &client = clients[clientNum];

	if ( client.clientState < SCS_CONNECTED ) {
		return 0.0f;
	} else {
		return client.channel.GetIncomingCompression();
	}
}

/*
==================
idAsyncServer::GetClientIncomingPacketLoss
==================
*/
float idAsyncServer::GetClientIncomingPacketLoss( int clientNum ) const {
	const serverClient_t &client = clients[clientNum];

	if ( client.clientState < SCS_CONNECTED ) {
		return 0.0f;
	} else {
		return client.channel.GetIncomingPacketLoss();
	}
}

/*
==================
idAsyncServer::GetNumClients
==================
*/
int idAsyncServer::GetNumClients( void ) const {
	int ret = 0;
	for ( int i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		if ( clients[ i ].clientState >= SCS_CONNECTED ) {
			ret++;
		}
	}
	return ret;
}

/*
==================
idAsyncServer::GetNumIdleClients
==================
*/
int idAsyncServer::GetNumIdleClients( void ) const {
	int ret = 0;
	for ( int i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		if ( clients[ i ].clientState >= SCS_CONNECTED ) {
			if ( serverTime - clients[ i ].lastInputTime > NOINPUT_IDLE_TIME ) {
				ret++;
			}
		}
	}
	return ret;
}

/*
==================
idAsyncServer::DuplicateUsercmds
==================
*/
void idAsyncServer::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++ ) {
		if ( clients[i].clientState == SCS_FREE ) {
			continue;
		}

		if ( idAsyncNetwork::DuplicateUsercmd( userCmds[previousIndex][i], userCmds[currentIndex][i], frame, time ) ) {
			clients[i].numDuplicatedUsercmds++;
		}
	}
}

/*
==================
idAsyncServer::ClearClient
==================
*/
void idAsyncServer::ClearClient( int clientNum ) {
	serverClient_t &client = clients[clientNum];
	client.clientId = 0;
	client.clientState = SCS_FREE;
	client.clientPrediction = 0;
	client.clientAheadTime = 0;
	client.clientRate = 0;
	client.clientPing = 0;
	client.gameInitSequence = 0;
	client.gameFrame = 0;
	client.gameTime = 0;
	client.channel.Shutdown();
	client.lastConnectTime = 0;
	client.lastEmptyTime = 0;
	client.lastPingTime = 0;
	client.lastSnapshotTime = 0;
	client.lastPacketTime = 0;
	client.lastInputTime = 0;
	client.snapshotSequence = 0;
	client.acknowledgeSnapshotSequence = 0;
	client.numDuplicatedUsercmds = 0;
}

/*
==================
idAsyncServer::InitClient
==================
*/
void idAsyncServer::InitClient( int clientNum, int clientId, int clientRate ) {
	int i;

	// clear the user info
	sessLocal.mapSpawnData.userInfo[ clientNum ].Clear();	// always start with a clean base

	// clear the server client
	serverClient_t &client = clients[clientNum];
	client.clientId = clientId;
	client.clientState = SCS_CONNECTED;
	client.clientPrediction = 0;
	client.clientAheadTime = 0;
	client.gameInitSequence = -1;
	client.gameFrame = 0;
	client.gameTime = 0;
	client.channel.ResetRate();
	client.clientRate = clientRate ? clientRate : idAsyncNetwork::serverMaxClientRate.GetInteger();
	client.channel.SetMaxOutgoingRate( Min( idAsyncNetwork::serverMaxClientRate.GetInteger(), client.clientRate ) );
	client.clientPing = 0;
	client.lastConnectTime = serverTime;
	client.lastEmptyTime = serverTime;
	client.lastPingTime = serverTime;
	client.lastSnapshotTime = serverTime;
	client.lastPacketTime = serverTime;
	client.lastInputTime = serverTime;
	client.acknowledgeSnapshotSequence = 0;
	client.numDuplicatedUsercmds = 0;

	// clear the user commands
	for ( i = 0; i < MAX_USERCMD_BACKUP; i++ ) {
		memset( &userCmds[i][clientNum], 0, sizeof( userCmds[i][clientNum] ) );
	}

	// let the game know a player connected
	game->ServerClientConnect( clientNum, client.guid );
}

/*
==================
idAsyncServer::InitLocalClient
==================
*/
void idAsyncServer::InitLocalClient( int clientNum ) {
	netadr_t badAddress;

	localClientNum = clientNum;
	InitClient( clientNum, 0, 0 );
	memset( &badAddress, 0, sizeof( badAddress ) );
	badAddress.type = NA_BAD;
	clients[clientNum].channel.Init( badAddress, serverId );
	clients[clientNum].clientState = SCS_INGAME;
	sessLocal.mapSpawnData.userInfo[clientNum] = *cvarSystem->MoveCVarsToDict( CVAR_USERINFO );
}

/*
==================
idAsyncServer::BeginLocalClient
==================
*/
void idAsyncServer::BeginLocalClient( void ) {
	game->SetLocalClient( localClientNum );
	game->SetUserInfo( localClientNum, sessLocal.mapSpawnData.userInfo[localClientNum], false, false );
	game->ServerClientBegin( localClientNum );
}

/*
==================
idAsyncServer::LocalClientInput
==================
*/
void idAsyncServer::LocalClientInput( void ) {
	int index;

	if ( localClientNum < 0 ) {
		return;
	}

	index = gameFrame & ( MAX_USERCMD_BACKUP - 1 );
	userCmds[index][localClientNum] = usercmdGen->GetDirectUsercmd();
	userCmds[index][localClientNum].gameFrame = gameFrame;
	userCmds[index][localClientNum].gameTime = gameTime;
	if ( idAsyncNetwork::UsercmdInputChanged( userCmds[( gameFrame - 1 ) & ( MAX_USERCMD_BACKUP - 1 )][localClientNum], userCmds[index][localClientNum] ) ) {
		clients[localClientNum].lastInputTime = serverTime;
	}
	clients[localClientNum].gameFrame = gameFrame;
	clients[localClientNum].gameTime = gameTime;
	clients[localClientNum].lastPacketTime = serverTime;
}

/*
==================
idAsyncServer::DropClient
==================
*/
void idAsyncServer::DropClient( int clientNum, const char *reason ) {
	int			i;
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	serverClient_t &client = clients[clientNum];

	if ( client.clientState <= SCS_ZOMBIE ) {
		return;
	}

	if ( client.clientState >= SCS_PUREWAIT && clientNum != localClientNum ) {
		msg.Init( msgBuf, sizeof( msgBuf ) );
		msg.WriteByte( SERVER_RELIABLE_MESSAGE_DISCONNECT );
		msg.WriteInt( clientNum );
		msg.WriteString( reason );
		for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
			// clientNum so SCS_PUREWAIT client gets it's own disconnect msg
			if ( i == clientNum || clients[i].clientState >= SCS_CONNECTED ) {
				SendReliableMessage( i, msg );
			}
		}
	}

	reason = common->GetLanguageDict()->GetString( reason );
	common->Printf( "client %d %s\n", clientNum, reason );
	cmdSystem->BufferCommandText( CMD_EXEC_NOW, va( "addChatLine \"%s^0 %s\"", sessLocal.mapSpawnData.userInfo[ clientNum ].GetString( "ui_name" ), reason ) );

	// remove the player from the game
	game->ServerClientDisconnect( clientNum );

	client.clientState = SCS_ZOMBIE;
}

/*
==================
idAsyncServer::SendReliableMessage
==================
*/
void idAsyncServer::SendReliableMessage( int clientNum, const idBitMsg &msg ) {
	if ( clientNum == localClientNum ) {
		return;
	}
	if ( !clients[ clientNum ].channel.SendReliableMessage( msg ) ) {
		clients[ clientNum ].channel.ClearReliableMessages();
		DropClient( clientNum, "#str_07136" );
	}
}

/*
==================
idAsyncServer::CheckClientTimeouts
==================
*/
void idAsyncServer::CheckClientTimeouts( void ) {
	int i, zombieTimeout, clientTimeout;

	zombieTimeout = serverTime - idAsyncNetwork::serverZombieTimeout.GetInteger() * 1000;
	clientTimeout = serverTime - idAsyncNetwork::serverClientTimeout.GetInteger() * 1000;

	for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		serverClient_t &client = clients[i];

		if ( i == localClientNum ) {
			continue;
		}

		if ( client.lastPacketTime > serverTime ) {
			client.lastPacketTime = serverTime;
			continue;
		}

		if ( client.clientState == SCS_ZOMBIE && client.lastPacketTime < zombieTimeout ) {
			client.channel.Shutdown();
			client.clientState = SCS_FREE;
			continue;
		}

		if ( client.clientState >= SCS_PUREWAIT && client.lastPacketTime < clientTimeout ) {
			DropClient( i, "#str_07137" );
			continue;
		}
	}
}

/*
==================
idAsyncServer::SendPrintBroadcast
==================
*/
void idAsyncServer::SendPrintBroadcast( const char *string ) {
	int			i;
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteByte( SERVER_RELIABLE_MESSAGE_PRINT );
	msg.WriteString( string );

	for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		if ( clients[i].clientState >= SCS_CONNECTED ) {
			SendReliableMessage( i, msg );
		}
	}
}

/*
==================
idAsyncServer::SendPrintToClient
==================
*/
void idAsyncServer::SendPrintToClient( int clientNum, const char *string ) {
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	serverClient_t &client = clients[clientNum];

	if ( client.clientState < SCS_CONNECTED ) {
		return;
	}

	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteByte( SERVER_RELIABLE_MESSAGE_PRINT );
	msg.WriteString( string );

	SendReliableMessage( clientNum, msg );
}

/*
==================
idAsyncServer::SendUserInfoBroadcast
==================
*/
void idAsyncServer::SendUserInfoBroadcast( int userInfoNum, const idDict &info, bool sendToAll ) {
	idBitMsg		msg;
	byte			msgBuf[MAX_MESSAGE_SIZE];
	const idDict	*gameInfo;
	bool			gameModifiedInfo;

	gameInfo = game->SetUserInfo( userInfoNum, info, false, true );
	if ( gameInfo ) {
		gameModifiedInfo = true;
	} else {
		gameModifiedInfo = false;
		gameInfo = &info;
	}

	if ( userInfoNum == localClientNum ) {
		common->DPrintf( "local user info modified by server\n" );
		cvarSystem->SetCVarsFromDict( *gameInfo );
		cvarSystem->ClearModifiedFlags( CVAR_USERINFO ); // don't emit back
	}

	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteByte( SERVER_RELIABLE_MESSAGE_CLIENTINFO );
	msg.WriteByte( userInfoNum );
	if ( gameModifiedInfo || sendToAll ) {
		msg.WriteBits( 0, 1 );
	} else {
		msg.WriteBits( 1, 1 );
	}

#if ID_CLIENTINFO_TAGS
	msg.WriteInt( sessLocal.mapSpawnData.userInfo[userInfoNum].Checksum() );
	common->DPrintf( "broadcast for client %d: 0x%x\n", userInfoNum, sessLocal.mapSpawnData.userInfo[userInfoNum].Checksum() );
	sessLocal.mapSpawnData.userInfo[userInfoNum].Print();
#endif

	if ( gameModifiedInfo || sendToAll ) {
		msg.WriteDeltaDict( *gameInfo, NULL );
	} else {
		msg.WriteDeltaDict( *gameInfo, &sessLocal.mapSpawnData.userInfo[userInfoNum] );
	}

	for ( int i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		if ( clients[i].clientState >= SCS_CONNECTED && ( sendToAll || i != userInfoNum || gameModifiedInfo ) ) {
			SendReliableMessage( i, msg );
		}
	}

	sessLocal.mapSpawnData.userInfo[userInfoNum] = *gameInfo;
}

/*
==================
idAsyncServer::UpdateUI
if the game modifies userInfo, it will call this through command system
we then need to get the info from the game, and broadcast to clients
( using DeltaDict and our current mapSpawnData as a base )
==================
*/
void idAsyncServer::UpdateUI( int clientNum ) {
	const idDict	*info = game->GetUserInfo( clientNum );

	if ( !info ) {
		common->Warning( "idAsyncServer::UpdateUI: no info from game\n" );
		return;
	}

	SendUserInfoBroadcast( clientNum, *info, true );
}

/*
==================
idAsyncServer::SendUserInfoToClient
==================
*/
void idAsyncServer::SendUserInfoToClient( int clientNum, int userInfoNum, const idDict &info ) {
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	if ( clients[clientNum].clientState < SCS_CONNECTED ) {
		return;
	}

	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteByte( SERVER_RELIABLE_MESSAGE_CLIENTINFO );
	msg.WriteByte( userInfoNum );
	msg.WriteBits( 0, 1 );

#if ID_CLIENTINFO_TAGS
	msg.WriteInt( 0 );
	common->DPrintf( "user info %d to client %d: NULL base\n", userInfoNum, clientNum );
#endif

	msg.WriteDeltaDict( info, NULL );

	SendReliableMessage( clientNum, msg );
}

/*
==================
idAsyncServer::SendSyncedCvarsBroadcast
==================
*/
void idAsyncServer::SendSyncedCvarsBroadcast( const idDict &cvars ) {
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];
	int			i;

	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteByte( SERVER_RELIABLE_MESSAGE_SYNCEDCVARS );
	msg.WriteDeltaDict( cvars, &sessLocal.mapSpawnData.syncedCVars );

	for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		if ( clients[i].clientState >= SCS_CONNECTED ) {
			SendReliableMessage( i, msg );
		}
	}

	sessLocal.mapSpawnData.syncedCVars = cvars;
}

/*
==================
idAsyncServer::SendSyncedCvarsToClient
==================
*/
void idAsyncServer::SendSyncedCvarsToClient( int clientNum, const idDict &cvars ) {
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	if ( clients[clientNum].clientState < SCS_CONNECTED ) {
		return;
	}

	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteByte( SERVER_RELIABLE_MESSAGE_SYNCEDCVARS );
	msg.WriteDeltaDict( cvars, NULL );

	SendReliableMessage( clientNum, msg );
}

/*
==================
idAsyncServer::SendApplySnapshotToClient
==================
*/
void idAsyncServer::SendApplySnapshotToClient( int clientNum, int sequence ) {
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteByte( SERVER_RELIABLE_MESSAGE_APPLYSNAPSHOT );
	msg.WriteInt( sequence );

	SendReliableMessage( clientNum, msg );
}

/*
==================
idAsyncServer::SendEmptyToClient
==================
*/
bool idAsyncServer::SendEmptyToClient( int clientNum, bool force ) {
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	serverClient_t &client = clients[clientNum];

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

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

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

	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteInt( gameInitId );
	msg.WriteByte( SERVER_UNRELIABLE_MESSAGE_EMPTY );

	client.channel.SendMessage( serverPort, serverTime, msg );

	client.lastEmptyTime = realTime;

	return true;
}

/*
==================
idAsyncServer::SendPingToClient
==================
*/
bool idAsyncServer::SendPingToClient( int clientNum ) {
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	serverClient_t &client = clients[clientNum];

	if ( client.lastPingTime > realTime ) {
		client.lastPingTime = realTime;
	}

	if ( realTime - client.lastPingTime < PING_RESEND_TIME ) {
		return false;
	}

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

	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteInt( gameInitId );
	msg.WriteByte( SERVER_UNRELIABLE_MESSAGE_PING );
	msg.WriteInt( realTime );

	client.channel.SendMessage( serverPort, serverTime, msg );

	client.lastPingTime = realTime;

	return true;
}

/*
==================
idAsyncServer::SendGameInitToClient
==================
*/
void idAsyncServer::SendGameInitToClient( int clientNum ) {
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

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

	serverClient_t &client = clients[clientNum];

	// clear the unsent fragments. might flood winsock but that's ok
	while( client.channel.UnsentFragmentsLeft() ) {
		client.channel.SendNextFragment( serverPort, serverTime );
	}

	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteInt( gameInitId );
	msg.WriteByte( SERVER_UNRELIABLE_MESSAGE_GAMEINIT );
	msg.WriteInt( gameFrame );
	msg.WriteInt( gameTime );
	msg.WriteDeltaDict( sessLocal.mapSpawnData.serverInfo, NULL );
	client.gameInitSequence = client.channel.SendMessage( serverPort, serverTime, msg );
}

/*
==================
idAsyncServer::SendSnapshotToClient
==================
*/
bool idAsyncServer::SendSnapshotToClient( int clientNum ) {
	int			i, j, index, numUsercmds;
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];
	usercmd_t *	last;
	byte		clientInPVS[MAX_ASYNC_CLIENTS >> 3];

	serverClient_t &client = clients[clientNum];

	if ( serverTime - client.lastSnapshotTime < idAsyncNetwork::serverSnapshotDelay.GetInteger() ) {
		return false;
	}

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

	// how far is the client ahead of the server minus the packet delay
	client.clientAheadTime = client.gameTime - ( gameTime + gameTimeResidual );

	// write the snapshot
	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteInt( gameInitId );
	msg.WriteByte( SERVER_UNRELIABLE_MESSAGE_SNAPSHOT );
	msg.WriteInt( client.snapshotSequence );
	msg.WriteInt( gameFrame );
	msg.WriteInt( gameTime );
	msg.WriteByte( idMath::ClampChar( client.numDuplicatedUsercmds ) );
	msg.WriteShort( idMath::ClampShort( client.clientAheadTime ) );

	// write the game snapshot
	game->ServerWriteSnapshot( clientNum, client.snapshotSequence, msg, clientInPVS, MAX_ASYNC_CLIENTS );

	// write the latest user commands from the other clients in the PVS to the snapshot
	for ( last = NULL, i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		serverClient_t &client = clients[i];

		if ( client.clientState == SCS_FREE || i == clientNum ) {
			continue;
		}

		// if the client is not in the PVS
		if ( !( clientInPVS[i >> 3] & ( 1 << ( i & 7 ) ) ) ) {
			continue;
		}

		int maxRelay = idMath::ClampInt( 1, MAX_USERCMD_RELAY, idAsyncNetwork::serverMaxUsercmdRelay.GetInteger() );

		// Max( 1, to always send at least one cmd, which we know we have because we call DuplicateUsercmds in RunFrame
		numUsercmds = Max( 1, Min( client.gameFrame, gameFrame + maxRelay ) - gameFrame );
		msg.WriteByte( i );
		msg.WriteByte( numUsercmds );
		for ( j = 0; j < numUsercmds; j++ ) {
			index = ( gameFrame + j ) & ( MAX_USERCMD_BACKUP - 1 );
			idAsyncNetwork::WriteUserCmdDelta( msg, userCmds[index][i], last );
			last = &userCmds[index][i];
		}
	}
	msg.WriteByte( MAX_ASYNC_CLIENTS );

	client.channel.SendMessage( serverPort, serverTime, msg );

	client.lastSnapshotTime = serverTime;
	client.snapshotSequence++;
	client.numDuplicatedUsercmds = 0;

	return true;
}

/*
==================
idAsyncServer::ProcessUnreliableClientMessage
==================
*/
void idAsyncServer::ProcessUnreliableClientMessage( int clientNum, const idBitMsg &msg ) {
	int i, id, acknowledgeSequence, clientGameInitId, clientGameFrame, numUsercmds, index;
	usercmd_t *last;

	serverClient_t &client = clients[clientNum];

	if ( client.clientState == SCS_ZOMBIE ) {
		return;
	}

	acknowledgeSequence = msg.ReadInt();
	clientGameInitId = msg.ReadInt();

	// while loading a map the client may send empty messages to keep the connection alive
	if ( clientGameInitId == GAME_INIT_ID_MAP_LOAD ) {
		if ( idAsyncNetwork::verbose.GetInteger() ) {
			common->Printf( "ignore unreliable msg from client %d, gameInitId == ID_MAP_LOAD\n", clientNum );
		}
		return;
	}

	// check if the client is in the right game
	if ( clientGameInitId != gameInitId ) {
		if ( acknowledgeSequence > client.gameInitSequence ) {
			// the client is connected but not in the right game
			client.clientState = SCS_CONNECTED;

			// send game init to client
			SendGameInitToClient( clientNum );

			if ( sessLocal.mapSpawnData.serverInfo.GetBool( "si_pure" ) ) {
				client.clientState = SCS_PUREWAIT;
				if ( !SendReliablePureToClient( clientNum ) ) {
					client.clientState = SCS_CONNECTED;
				}
			}
		} else if ( idAsyncNetwork::verbose.GetInteger() ) {
			common->Printf( "ignore unreliable msg from client %d, wrong gameInit, old sequence\n", clientNum );
		}
		return;
	}

	client.acknowledgeSnapshotSequence = msg.ReadInt();

	if ( client.clientState == SCS_CONNECTED ) {

		// the client is in the right game
		client.clientState = SCS_INGAME;

		// send the user info of other clients
		for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
			if ( clients[i].clientState >= SCS_CONNECTED && i != clientNum ) {
				SendUserInfoToClient( clientNum, i, sessLocal.mapSpawnData.userInfo[i] );
			}
		}

		// send synchronized cvars to client
		SendSyncedCvarsToClient( clientNum, sessLocal.mapSpawnData.syncedCVars );

		SendEnterGameToClient( clientNum );

		// get the client running in the game
		game->ServerClientBegin( clientNum );

		// write any reliable messages to initialize the client game state
		game->ServerWriteInitialReliableMessages( clientNum );
	} else if ( client.clientState == SCS_INGAME ) {

		// apply the last snapshot the client received
		if ( game->ServerApplySnapshot( clientNum, client.acknowledgeSnapshotSequence ) ) {
			SendApplySnapshotToClient( clientNum, client.acknowledgeSnapshotSequence );
		}
	}

	// process the unreliable message
	id = msg.ReadByte();
	switch( id ) {
		case CLIENT_UNRELIABLE_MESSAGE_EMPTY: {
			if ( idAsyncNetwork::verbose.GetInteger() ) {
				common->Printf( "received empty message for client %d\n", clientNum );
			}
			break;
		}
		case CLIENT_UNRELIABLE_MESSAGE_PINGRESPONSE: {
			client.clientPing = realTime - msg.ReadInt();
			break;
		}
		case CLIENT_UNRELIABLE_MESSAGE_USERCMD: {

			client.clientPrediction = msg.ReadShort();

			// read user commands
			clientGameFrame = msg.ReadInt();
			numUsercmds = msg.ReadByte();
			for ( last = NULL, i = clientGameFrame - numUsercmds + 1; i <= clientGameFrame; i++ ) {
				index = i & ( MAX_USERCMD_BACKUP - 1 );
				idAsyncNetwork::ReadUserCmdDelta( msg, userCmds[index][clientNum], last );
				userCmds[index][clientNum].gameFrame = i;
				userCmds[index][clientNum].duplicateCount = 0;
				if ( idAsyncNetwork::UsercmdInputChanged( userCmds[( i - 1 ) & ( MAX_USERCMD_BACKUP - 1 )][clientNum], userCmds[index][clientNum] ) ) {
					client.lastInputTime = serverTime;
				}
				last = &userCmds[index][clientNum];
			}

			if ( last ) {
				client.gameFrame = last->gameFrame;
				client.gameTime = last->gameTime;
			}

			if ( idAsyncNetwork::verbose.GetInteger() == 2 ) {
				common->Printf( "received user command for client %d, gameInitId = %d, gameFrame, %d gameTime %d\n", clientNum, clientGameInitId, client.gameFrame, client.gameTime );
			}
			break;
		}
		default: {
			common->Printf( "unknown unreliable message %d from client %d\n", id, clientNum );
			break;
		}
	}
}

/*
==================
idAsyncServer::ProcessReliableClientMessages
==================
*/
void idAsyncServer::ProcessReliableClientMessages( int clientNum ) {
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];
	byte		id;

	serverClient_t &client = clients[clientNum];

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

	while ( client.channel.GetReliableMessage( msg ) ) {
		id = msg.ReadByte();
		switch( id ) {
			case CLIENT_RELIABLE_MESSAGE_CLIENTINFO: {
				idDict info;
				msg.ReadDeltaDict( info, &sessLocal.mapSpawnData.userInfo[clientNum] );
				SendUserInfoBroadcast( clientNum, info );
				break;
			}
			case CLIENT_RELIABLE_MESSAGE_PRINT: {
				char string[MAX_STRING_CHARS];
				msg.ReadString( string, sizeof( string ) );
				common->Printf( "%s\n", string );
				break;
			}
			case CLIENT_RELIABLE_MESSAGE_DISCONNECT: {
				DropClient( clientNum, "#str_07138" );
				break;
			}
			case CLIENT_RELIABLE_MESSAGE_PURE: {
				// we get this message once the client has successfully updated it's pure list
				ProcessReliablePure( clientNum, msg );
				break;
			}
			default: {
				// pass reliable message on to game code
				game->ServerProcessReliableMessage( clientNum, msg );
				break;
			}
		}
	}
}

/*
==================
idAsyncServer::ProcessAuthMessage
==================
*/
void idAsyncServer::ProcessAuthMessage( const idBitMsg &msg ) {
	netadr_t		client_from;
	char			client_guid[ 12 ], string[ MAX_STRING_CHARS ];
	int				i, clientId;
	authReply_t		reply;
	authReplyMsg_t	replyMsg = AUTH_REPLY_WAITING;
	idStr			replyPrintMsg;

	reply = (authReply_t)msg.ReadByte();
	if ( reply <= 0 || reply >= AUTH_MAXSTATES ) {
		common->DPrintf( "auth: invalid reply %d\n", reply );
		return;
	}
	clientId = msg.ReadShort( );
	msg.ReadNetadr( &client_from );
	msg.ReadString( client_guid, sizeof( client_guid ) );
	if ( reply != AUTH_OK ) {
		replyMsg = (authReplyMsg_t)msg.ReadByte();
		if ( replyMsg <= 0 || replyMsg >= AUTH_REPLY_MAXSTATES ) {
			common->DPrintf( "auth: invalid reply msg %d\n", replyMsg );
			return;
		}
		if ( replyMsg == AUTH_REPLY_PRINT ) {
			msg.ReadString( string, MAX_STRING_CHARS );
			replyPrintMsg = string;
		}
	}

	lastAuthTime = serverTime;

	// no message parsing below

	for ( i = 0; i < MAX_CHALLENGES; i++ ) {
		if ( !challenges[i].connected && challenges[ i ].clientId == clientId ) {
			// return if something is wrong
			// break if we have found a valid auth
			if ( !strlen( challenges[ i ].guid ) ) {
				common->DPrintf( "auth: client %s has no guid yet\n", Sys_NetAdrToString( challenges[ i ].address ) );
				return;
			}
			if ( idStr::Cmp( challenges[ i ].guid, client_guid ) ) {
				common->DPrintf( "auth: client %s %s not matched, auth server says guid %s\n", Sys_NetAdrToString( challenges[ i ].address ), challenges[i].guid, client_guid );
				return;
			}
			if ( !Sys_CompareNetAdrBase( client_from, challenges[i].address ) ) {
				// let auth work when server and master don't see the same IP
				common->DPrintf( "auth: matched guid '%s' for != IPs %s and %s\n", client_guid, Sys_NetAdrToString( client_from ), Sys_NetAdrToString( challenges[i].address ) );
			}
			break;
		}
	}
	if ( i >= MAX_CHALLENGES ) {
		common->DPrintf( "auth: failed client lookup %s %s\n", Sys_NetAdrToString( client_from ), client_guid );
		return;
	}

	if ( challenges[ i ].authState != CDK_WAIT ) {
		common->DWarning( "auth: challenge 0x%x %s authState %d != CDK_WAIT", challenges[ i ].challenge, Sys_NetAdrToString( challenges[ i ].address ), challenges[ i ].authState );
		return;
	}

	idStr::snPrintf( challenges[ i ].guid, 12, client_guid );
	if ( reply == AUTH_OK ) {
		challenges[ i ].authState = CDK_OK;
		common->Printf( "client %s %s is authed\n", Sys_NetAdrToString( client_from ), client_guid );
	} else {
		const char *msg;
		if ( replyMsg != AUTH_REPLY_PRINT ) {
			msg = authReplyMsg[ replyMsg ];
		} else {
			msg = replyPrintMsg.c_str();
		}
		// maybe localize it
		const char *l_msg = common->GetLanguageDict()->GetString( msg );
		common->DPrintf( "auth: client %s %s - %s %s\n", Sys_NetAdrToString( client_from ), client_guid, authReplyStr[ reply ], l_msg );
		challenges[ i ].authReply = reply;
		challenges[ i ].authReplyMsg = replyMsg;
		challenges[ i ].authReplyPrint = replyPrintMsg;
	}
}

/*
==================
idAsyncServer::ProcessChallengeMessage
==================
*/
void idAsyncServer::ProcessChallengeMessage( const netadr_t from, const idBitMsg &msg ) {
	int			i, clientId, oldest, oldestTime;
	idBitMsg	outMsg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	clientId = msg.ReadInt();

	oldest = 0;
	oldestTime = 0x7fffffff;

	// see if we already have a challenge for this ip
	for ( i = 0; i < MAX_CHALLENGES; i++ ) {
		if ( !challenges[i].connected && Sys_CompareNetAdrBase( from, challenges[i].address ) && clientId == challenges[i].clientId ) {
			break;
		}
		if ( challenges[i].time < oldestTime ) {
			oldestTime = challenges[i].time;
			oldest = i;
		}
	}

	if ( i >= MAX_CHALLENGES ) {
		// this is the first time this client has asked for a challenge
		i = oldest;
		challenges[i].address = from;
		challenges[i].clientId = clientId;
		challenges[i].challenge = ( (rand() << 16) ^ rand() ) ^ serverTime;
		challenges[i].time = serverTime;
		challenges[i].connected = false;
		challenges[i].authState = CDK_WAIT;
		challenges[i].authReply = AUTH_NONE;
		challenges[i].authReplyMsg = AUTH_REPLY_WAITING;
		challenges[i].authReplyPrint = "";
		challenges[i].guid[0] = '\0';
	}
	challenges[i].pingTime = serverTime;

	common->Printf( "sending challenge 0x%x to %s\n", challenges[i].challenge, Sys_NetAdrToString( from ) );

	outMsg.Init( msgBuf, sizeof( msgBuf ) );
	outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
	outMsg.WriteString( "challengeResponse" );
	outMsg.WriteInt( challenges[i].challenge );
	outMsg.WriteShort( serverId );
	outMsg.WriteString( cvarSystem->GetCVarString( "fs_game_base" ) );
	outMsg.WriteString( cvarSystem->GetCVarString( "fs_game" ) );

	serverPort.SendPacket( from, outMsg.GetData(), outMsg.GetSize() );

#if ID_ENFORCE_KEY_CLIENT
	if ( Sys_IsLANAddress( from ) ) {
		// no CD Key check for LAN clients
		challenges[i].authState = CDK_OK;
	} else {
		if ( idAsyncNetwork::LANServer.GetBool() ) {
			common->Printf( "net_LANServer is enabled. Client %s is not a LAN address, will be rejected\n", Sys_NetAdrToString( from ) );
			challenges[ i ].authState = CDK_ONLYLAN;
		} else {
			// emit a cd key confirmation request
			outMsg.BeginWriting();
			outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
			outMsg.WriteString( "srvAuth" );
			outMsg.WriteInt( ASYNC_PROTOCOL_VERSION );
			outMsg.WriteNetadr( from );
			outMsg.WriteInt( -1 ); // this identifies "challenge" auth vs "connect" auth
			// protocol 1.37 addition
			outMsg.WriteByte( fileSystem->RunningD3XP() );
			serverPort.SendPacket( idAsyncNetwork::GetMasterAddress(), outMsg.GetData(), outMsg.GetSize() );
		}
	}
#else
	if (! Sys_IsLANAddress( from ) ) {
		common->Printf( "Build Does not have CD Key Enforcement enabled. Client %s is not a LAN address, but will be accepted\n", Sys_NetAdrToString( from ) );
	}
	challenges[i].authState = CDK_OK;
#endif
}

/*
==================
idAsyncServer::SendPureServerMessage
==================
*/
bool idAsyncServer::SendPureServerMessage( const netadr_t to ) {
	idBitMsg	outMsg;
	byte		msgBuf[ MAX_MESSAGE_SIZE ];
	int			serverChecksums[ MAX_PURE_PAKS ];
	int			i;

	fileSystem->GetPureServerChecksums( serverChecksums );
	if ( !serverChecksums[ 0 ] ) {
		// happens if you run fully expanded assets with si_pure 1
		common->Warning( "pure server has no pak files referenced" );
		return false;
	}
	common->DPrintf( "client %s: sending pure pak list\n", Sys_NetAdrToString( to ) );

	// send our list of required paks
	outMsg.Init( msgBuf, sizeof( msgBuf ) );
	outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
	outMsg.WriteString( "pureServer" );

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

	serverPort.SendPacket( to, outMsg.GetData(), outMsg.GetSize() );
	return true;
}

/*
==================
idAsyncServer::SendReliablePureToClient
==================
*/
bool idAsyncServer::SendReliablePureToClient( int clientNum ) {
	idBitMsg	msg;
	byte		msgBuf[ MAX_MESSAGE_SIZE ];
	int			serverChecksums[ MAX_PURE_PAKS ];
	int			i;

	fileSystem->GetPureServerChecksums( serverChecksums );
	if ( !serverChecksums[ 0 ] ) {
		// happens if you run fully expanded assets with si_pure 1
		common->Warning( "pure server has no pak files referenced" );
		return false;
	}

	common->DPrintf( "client %d: sending pure pak list (reliable channel) @ gameInitId %d\n", clientNum, gameInitId );

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

	msg.WriteInt( gameInitId );

	i = 0;
	while ( serverChecksums[ i ] ) {
		msg.WriteInt( serverChecksums[ i++ ] );
	}
	msg.WriteInt( 0 );

	SendReliableMessage( clientNum, msg );

	return true;
}

/*
==================
idAsyncServer::ValidateChallenge
==================
*/
int idAsyncServer::ValidateChallenge( const netadr_t from, int challenge, int clientId ) {
	int i;
	for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		const serverClient_t &client = clients[i];

		if ( client.clientState == SCS_FREE ) {
			continue;
		}
		if ( Sys_CompareNetAdrBase( from, client.channel.GetRemoteAddress() ) &&
					( clientId == client.clientId || from.port == client.channel.GetRemoteAddress().port ) ) {
			if ( serverTime - client.lastConnectTime < MIN_RECONNECT_TIME ) {
				common->Printf( "%s: reconnect rejected : too soon\n", Sys_NetAdrToString( from ) );
				return -1;
			}
			break;
		}
	}

	for ( i = 0; i < MAX_CHALLENGES; i++ ) {
		if ( Sys_CompareNetAdrBase( from, challenges[i].address ) && from.port == challenges[i].address.port ) {
			if ( challenge == challenges[i].challenge ) {
				break;
			}
		}
	}
	if ( i == MAX_CHALLENGES ) {
		PrintOOB( from, SERVER_PRINT_BADCHALLENGE, "#str_04840" );
		return -1;
	}
	return i;
}

/*
==================
idAsyncServer::ProcessConnectMessage
==================
*/
void idAsyncServer::ProcessConnectMessage( const netadr_t from, const idBitMsg &msg ) {
	int			clientNum, protocol, clientDataChecksum, challenge, clientId, ping, clientRate;
	idBitMsg	outMsg;
	byte		msgBuf[ MAX_MESSAGE_SIZE ];
	char		guid[ 12 ];
	char		password[ 17 ];
	int			i, ichallenge, islot, numClients;

	protocol = msg.ReadInt();

	// check the protocol version
	if ( protocol != ASYNC_PROTOCOL_VERSION ) {
		// that's a msg back to a client, we don't know about it's localization, so send english
		PrintOOB( from, SERVER_PRINT_BADPROTOCOL, va( "server uses protocol %d.%d\n", ASYNC_PROTOCOL_MAJOR, ASYNC_PROTOCOL_MINOR ) );
		return;
	}

	clientDataChecksum = msg.ReadInt();
	challenge = msg.ReadInt();
	clientId = msg.ReadShort();
	clientRate = msg.ReadInt();

	// check the client data - only for non pure servers
	if ( !sessLocal.mapSpawnData.serverInfo.GetInt( "si_pure" ) && clientDataChecksum != serverDataChecksum ) {
		PrintOOB( from, SERVER_PRINT_MISC, "#str_04842" );
		return;
	}

	if ( ( ichallenge = ValidateChallenge( from, challenge, clientId ) ) == -1 ) {
		return;
	}

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

	switch ( challenges[ ichallenge ].authState ) {
		case CDK_PUREWAIT:
			SendPureServerMessage( from );
			return;
		case CDK_ONLYLAN:
			common->DPrintf( "%s: not a lan client\n", Sys_NetAdrToString( from ) );
			PrintOOB( from, SERVER_PRINT_MISC, "#str_04843" );
			return;
		case CDK_WAIT:
			if ( challenges[ ichallenge ].authReply == AUTH_NONE && Min( serverTime - lastAuthTime, serverTime - challenges[ ichallenge ].time ) > AUTHORIZE_TIMEOUT ) {
				common->DPrintf( "%s: Authorize server timed out\n", Sys_NetAdrToString( from ) );
				break; // will continue with the connecting process
			}
			const char *msg, *l_msg;
			if ( challenges[ ichallenge ].authReplyMsg != AUTH_REPLY_PRINT ) {
				msg = authReplyMsg[ challenges[ ichallenge ].authReplyMsg ];
			} else {
				msg = challenges[ ichallenge ].authReplyPrint.c_str();
			}
			l_msg = common->GetLanguageDict()->GetString( msg );

			common->DPrintf( "%s: %s\n", Sys_NetAdrToString( from ), l_msg );

			if ( challenges[ ichallenge ].authReplyMsg == AUTH_REPLY_UNKNOWN || challenges[ ichallenge ].authReplyMsg == AUTH_REPLY_WAITING ) {
				// the client may be trying to connect to us in LAN mode, and the server disagrees
				// let the client know so it would switch to authed connection
				idBitMsg outMsg;
				byte msgBuf[ MAX_MESSAGE_SIZE ];
				outMsg.Init( msgBuf, sizeof( msgBuf ) );
				outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
				outMsg.WriteString( "authrequired" );
				serverPort.SendPacket( from, outMsg.GetData(), outMsg.GetSize() );
			}

			PrintOOB( from, SERVER_PRINT_MISC, msg );

			// update the guid in the challenges
			idStr::snPrintf( challenges[ ichallenge ].guid, sizeof( challenges[ ichallenge ].guid ), guid );

			// once auth replied denied, stop sending further requests
			if ( challenges[ ichallenge ].authReply != AUTH_DENY ) {
				// emit a cd key confirmation request
				outMsg.Init( msgBuf, sizeof( msgBuf ) );
				outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
				outMsg.WriteString( "srvAuth" );
				outMsg.WriteInt( ASYNC_PROTOCOL_VERSION );
				outMsg.WriteNetadr( from );
				outMsg.WriteInt( clientId );
				outMsg.WriteString( guid );
				// protocol 1.37 addition
				outMsg.WriteByte( fileSystem->RunningD3XP() );
				serverPort.SendPacket( idAsyncNetwork::GetMasterAddress(), outMsg.GetData(), outMsg.GetSize() );
			}
			return;
		default:
			assert( challenges[ ichallenge ].authState == CDK_OK || challenges[ ichallenge ].authState == CDK_PUREOK );
	}

	numClients = 0;
	for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		serverClient_t &client = clients[ i ];
		if ( client.clientState >= SCS_PUREWAIT ) {
			numClients++;
		}
	}

	// game may be passworded, client banned by IP or GUID
	// if authState == CDK_PUREOK, the check was already performed once before entering pure checks
	// but meanwhile, the max players may have been reached
	msg.ReadString( password, sizeof( password ) );
	char reason[MAX_STRING_CHARS];
	allowReply_t reply = game->ServerAllowClient( numClients, Sys_NetAdrToString( from ), guid, password, reason );
	if ( reply != ALLOW_YES ) {
		common->DPrintf( "game denied connection for %s\n", Sys_NetAdrToString( from ) );

		// SERVER_PRINT_GAMEDENY passes the game opcode through. Don't use PrintOOB
		outMsg.Init( msgBuf, sizeof( msgBuf ) );
		outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
		outMsg.WriteString( "print" );
		outMsg.WriteInt( SERVER_PRINT_GAMEDENY );
		outMsg.WriteInt( reply );
		outMsg.WriteString( reason );
		serverPort.SendPacket( from, outMsg.GetData(), outMsg.GetSize() );

		return;
	}

	// enter pure checks if necessary
	if ( sessLocal.mapSpawnData.serverInfo.GetInt( "si_pure" ) && challenges[ ichallenge ].authState != CDK_PUREOK ) {
		if ( SendPureServerMessage( from ) ) {
			challenges[ ichallenge ].authState = CDK_PUREWAIT;
			return;
		}
	}

	// push back decl checksum here when running pure. just an additional safe check
	if ( sessLocal.mapSpawnData.serverInfo.GetInt( "si_pure" ) && clientDataChecksum != serverDataChecksum ) {
		PrintOOB( from, SERVER_PRINT_MISC, "#str_04844" );
		return;
	}

	ping = serverTime - challenges[ ichallenge ].pingTime;
	common->Printf( "challenge from %s connecting with %d ping\n", Sys_NetAdrToString( from ), ping );
	challenges[ ichallenge ].connected = true;

	// find a slot for the client
	for ( islot = 0; islot < 3; islot++ ) {
		for ( clientNum = 0; clientNum < MAX_ASYNC_CLIENTS; clientNum++ ) {
			serverClient_t &client = clients[ clientNum ];

			if ( islot == 0 ) {
				// if this slot uses the same IP and port
				if ( Sys_CompareNetAdrBase( from, client.channel.GetRemoteAddress() ) &&
						( clientId == client.clientId || from.port == client.channel.GetRemoteAddress().port ) ) {
					break;
				}
			} else if ( islot == 1 ) {
				// if this client is not connected and the slot uses the same IP
				if ( client.clientState >= SCS_PUREWAIT ) {
					continue;
				}
				if ( Sys_CompareNetAdrBase( from, client.channel.GetRemoteAddress() ) ) {
					break;
				}
			} else if ( islot == 2 ) {
				// if this slot is free
				if ( client.clientState == SCS_FREE ) {
					break;
				}
			}
		}

		if ( clientNum < MAX_ASYNC_CLIENTS ) {
			// initialize
			clients[ clientNum ].channel.Init( from, serverId );
			strncpy( clients[ clientNum ].guid, guid, 12 );
			clients[ clientNum ].guid[11] = 0;
			break;
		}
	}

	// if no free spots available
	if ( clientNum >= MAX_ASYNC_CLIENTS ) {
		PrintOOB( from, SERVER_PRINT_MISC, "#str_04845" );
		return;
	}

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

	// send connect response message
	outMsg.Init( msgBuf, sizeof( msgBuf ) );
	outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
	outMsg.WriteString( "connectResponse" );
	outMsg.WriteInt( clientNum );
	outMsg.WriteInt( gameInitId );
	outMsg.WriteInt( gameFrame );
	outMsg.WriteInt( gameTime );
	outMsg.WriteDeltaDict( sessLocal.mapSpawnData.serverInfo, NULL );

	serverPort.SendPacket( from, outMsg.GetData(), outMsg.GetSize() );

	InitClient( clientNum, clientId, clientRate );

	clients[clientNum].gameInitSequence = 1;
	clients[clientNum].snapshotSequence = 1;

	// clear the challenge struct so a reconnect from this client IP starts clean
	memset( &challenges[ ichallenge ], 0, sizeof( challenge_t ) );
}

/*
==================
idAsyncServer::VerifyChecksumMessage
==================
*/
bool idAsyncServer::VerifyChecksumMessage( int clientNum, const netadr_t *from, const idBitMsg &msg, idStr &reply ) {
	int		i, numChecksums;
	int		checksums[ MAX_PURE_PAKS ];
	int		serverChecksums[ MAX_PURE_PAKS ];

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

	fileSystem->GetPureServerChecksums( serverChecksums );
	assert( serverChecksums[ 0 ] );

	for ( i = 0; serverChecksums[ i ] != 0; i++ ) {
		if ( checksums[ i ] != serverChecksums[ i ] ) {
			common->DPrintf( "client %s: pak missing ( 0x%x )\n", from ? Sys_NetAdrToString( *from ) : va( "%d", clientNum ), serverChecksums[ i ] );
			sprintf( reply, "pak missing ( 0x%x )\n", serverChecksums[ i ] );
			return false;
		}
	}
	if ( checksums[ i ] != 0 ) {
		common->DPrintf( "client %s: extra pak file referenced ( 0x%x )\n", from ? Sys_NetAdrToString( *from ) : va( "%d", clientNum ), checksums[ i ] );
		sprintf( reply, "extra pak file referenced ( 0x%x )\n", checksums[ i ] );
		return false;
	}
	return true;
}

/*
==================
idAsyncServer::ProcessPureMessage
==================
*/
void idAsyncServer::ProcessPureMessage( const netadr_t from, const idBitMsg &msg ) {
	int		iclient, challenge, clientId;
	idStr	reply;

	challenge = msg.ReadInt();
	clientId = msg.ReadShort();

	if ( ( iclient = ValidateChallenge( from, challenge, clientId ) ) == -1 ) {
		return;
	}

	if ( challenges[ iclient ].authState != CDK_PUREWAIT ) {
		common->DPrintf( "client %s: got pure message, not in CDK_PUREWAIT\n", Sys_NetAdrToString( from ) );
		return;
	}

	if ( !VerifyChecksumMessage( iclient, &from, msg, reply ) ) {
		PrintOOB( from, SERVER_PRINT_MISC, reply );
		return;
	}

	common->DPrintf( "client %s: passed pure checks\n", Sys_NetAdrToString( from ) );
	challenges[ iclient ].authState = CDK_PUREOK; // next connect message will get the client through completely
}

/*
==================
idAsyncServer::ProcessReliablePure
==================
*/
void idAsyncServer::ProcessReliablePure( int clientNum, const idBitMsg &msg ) {
	idStr		reply;
	idBitMsg	outMsg;
	byte		msgBuf[MAX_MESSAGE_SIZE];
	int			clientGameInitId;

	clientGameInitId = msg.ReadInt();
	if ( clientGameInitId != gameInitId ) {
		common->DPrintf( "client %d: ignoring reliable pure from an old gameInit (%d)\n", clientNum, clientGameInitId );
		return;
	}

	if ( clients[ clientNum ].clientState != SCS_PUREWAIT ) {
		// should not happen unless something is very wrong. still, don't let this crash us, just get rid of the client
		common->DPrintf( "client %d: got reliable pure while != SCS_PUREWAIT, sending a reload\n", clientNum );
		outMsg.Init( msgBuf, sizeof( msgBuf ) );
		outMsg.WriteByte( SERVER_RELIABLE_MESSAGE_RELOAD );
		SendReliableMessage( clientNum, msg );
		// go back to SCS_CONNECTED to sleep on the client until it goes away for a reconnect
		clients[ clientNum ].clientState = SCS_CONNECTED;
		return;
	}

	if ( !VerifyChecksumMessage( clientNum, NULL, msg, reply ) ) {
		DropClient( clientNum, reply );
		return;
	}
	common->DPrintf( "client %d: passed pure checks (reliable channel)\n", clientNum );
	clients[ clientNum ].clientState = SCS_CONNECTED;
}

/*
==================
idAsyncServer::RemoteConsoleOutput
==================
*/
void idAsyncServer::RemoteConsoleOutput( const char *string ) {
	noRconOutput = false;
	PrintOOB( rconAddress, SERVER_PRINT_RCON, string );
}

/*
==================
RConRedirect
==================
*/
void RConRedirect( const char *string ) {
	idAsyncNetwork::server.RemoteConsoleOutput( string );
}

/*
==================
idAsyncServer::ProcessRemoteConsoleMessage
==================
*/
void idAsyncServer::ProcessRemoteConsoleMessage( const netadr_t from, const idBitMsg &msg ) {
	idBitMsg	outMsg;
	byte		msgBuf[952];
	char		string[MAX_STRING_CHARS];

	if ( idAsyncNetwork::serverRemoteConsolePassword.GetString()[0] == '\0' ) {
		PrintOOB( from, SERVER_PRINT_MISC, "#str_04846" );
		return;
	}

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

	if ( idStr::Icmp( string, idAsyncNetwork::serverRemoteConsolePassword.GetString() ) != 0 ) {
		PrintOOB( from, SERVER_PRINT_MISC, "#str_04847" );
		return;
	}

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

	common->Printf( "rcon from %s: %s\n", Sys_NetAdrToString( from ), string );

	rconAddress = from;
	noRconOutput = true;
	common->BeginRedirect( (char *)msgBuf, sizeof( msgBuf ), RConRedirect );

	cmdSystem->BufferCommandText( CMD_EXEC_NOW, string );

	common->EndRedirect();

	if ( noRconOutput ) {
		PrintOOB( rconAddress, SERVER_PRINT_RCON, "#str_04848" );
	}
}

/*
==================
idAsyncServer::ProcessGetInfoMessage
==================
*/
void idAsyncServer::ProcessGetInfoMessage( const netadr_t from, const idBitMsg &msg ) {
	int			i, challenge;
	idBitMsg	outMsg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	if ( !IsActive() ) {
		return;
	}

	common->DPrintf( "Sending info response to %s\n", Sys_NetAdrToString( from ) );

	challenge = msg.ReadInt();

	outMsg.Init( msgBuf, sizeof( msgBuf ) );
	outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
	outMsg.WriteString( "infoResponse" );
	outMsg.WriteInt( challenge );
	outMsg.WriteInt( ASYNC_PROTOCOL_VERSION );
	outMsg.WriteDeltaDict( sessLocal.mapSpawnData.serverInfo, NULL );

	for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		serverClient_t &client = clients[i];

		if ( client.clientState < SCS_CONNECTED ) {
			continue;
		}

		outMsg.WriteByte( i );
		outMsg.WriteShort( client.clientPing );
		outMsg.WriteInt( client.channel.GetMaxOutgoingRate() );
		outMsg.WriteString( sessLocal.mapSpawnData.userInfo[i].GetString( "ui_name", "Player" ) );
	}
	outMsg.WriteByte( MAX_ASYNC_CLIENTS );
	// Stradex: Originally Doom3 did outMsg.WriteLong( fileSystem->GetOSMask() ); here
	//          dhewm3 eliminated GetOSMask() and WriteLong() became WriteInt() as it's supposed to write an int32
	//          Sending -1 (instead of nothing at all) restores compatibility with id's masterserver.
	outMsg.WriteInt( -1 );

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

/*
===============
idAsyncServer::PrintLocalServerInfo
see (client) "getInfo" -> (server) "infoResponse" -> (client)ProcessGetInfoMessage
===============
*/
void idAsyncServer::PrintLocalServerInfo( void ) {
	int i;

	common->Printf( "server '%s' IP = %s\nprotocol %d.%d\n",
					sessLocal.mapSpawnData.serverInfo.GetString( "si_name" ),
					Sys_NetAdrToString( serverPort.GetAdr() ),
					ASYNC_PROTOCOL_MAJOR,
					ASYNC_PROTOCOL_MINOR );
	sessLocal.mapSpawnData.serverInfo.Print();
	for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		serverClient_t &client = clients[i];
		if ( client.clientState < SCS_CONNECTED ) {
			continue;
		}
		common->Printf( "client %2d: %s, ping = %d, rate = %d\n", i,
						sessLocal.mapSpawnData.userInfo[i].GetString( "ui_name", "Player" ),
						client.clientPing, client.channel.GetMaxOutgoingRate() );
	}
}

/*
==================
idAsyncServer::ConnectionlessMessage
==================
*/
bool idAsyncServer::ConnectionlessMessage( const netadr_t from, const idBitMsg &msg ) {
	char		string[MAX_STRING_CHARS*2];  // M. Quinn - Even Balance - PB Packets need more than 1024

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

	// info request
	if ( idStr::Icmp( string, "getInfo" ) == 0 ) {
		ProcessGetInfoMessage( from, msg );
		return false;
	}

	// remote console
	if ( idStr::Icmp( string, "rcon" ) == 0 ) {
		ProcessRemoteConsoleMessage( from, msg );
		return true;
	}

	if ( !active ) {
		PrintOOB( from, SERVER_PRINT_MISC, "#str_04849" );
		return false;
	}

	// challenge from a client
	if ( idStr::Icmp( string, "challenge" ) == 0 ) {
		ProcessChallengeMessage( from, msg );
		return false;
	}

	// connect from a client
	if ( idStr::Icmp( string, "connect" ) == 0 ) {
		ProcessConnectMessage( from, msg );
		return false;
	}

	// pure mesasge from a client
	if ( idStr::Icmp( string, "pureClient" ) == 0 ) {
		ProcessPureMessage( from, msg );
		return false;
	}

	// download request
	if ( idStr::Icmp( string, "downloadRequest" ) == 0 ) {
		ProcessDownloadRequestMessage( from, msg );
	}

	// auth server
	if ( idStr::Icmp( string, "auth" ) == 0 ) {
		if ( !Sys_CompareNetAdrBase( from, idAsyncNetwork::GetMasterAddress() ) ) {
			common->Printf( "auth: bad source %s\n", Sys_NetAdrToString( from ) );
			return false;
		}
		if ( idAsyncNetwork::LANServer.GetBool() ) {
			common->Printf( "auth message from master. net_LANServer is enabled, ignored.\n" );
		}
		ProcessAuthMessage( msg );
		return false;
	}

	return false;
}

/*
==================
idAsyncServer::ProcessMessage
==================
*/
bool idAsyncServer::ProcessMessage( const netadr_t from, idBitMsg &msg ) {
	int			i, id, sequence;
	idBitMsg	outMsg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	id = msg.ReadShort();

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

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

	// find out which client the message is from
	for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		serverClient_t &client = clients[i];

		if ( client.clientState == SCS_FREE ) {
			continue;
		}

		// This does not compare the UDP port, because some address translating
		// routers will change that at arbitrary times.
		if ( !Sys_CompareNetAdrBase( from, client.channel.GetRemoteAddress() ) || id != client.clientId ) {
			continue;
		}

		// make sure it is a valid, in sequence packet
		if ( !client.channel.Process( from, serverTime, msg, sequence ) ) {
			return false;		// out of order, duplicated, fragment, etc.
		}

		// zombie clients still need to do the channel processing to make sure they don't
		// need to retransmit the final reliable message, but they don't do any other processing
		if ( client.clientState == SCS_ZOMBIE ) {
			return false;
		}

		client.lastPacketTime = serverTime;

		ProcessReliableClientMessages( i );
		ProcessUnreliableClientMessage( i, msg );

		return false;
	}

	// if we received a sequenced packet from an address we don't recognize,
	// send an out of band disconnect packet to it
	outMsg.Init( msgBuf, sizeof( msgBuf ) );
	outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
	outMsg.WriteString( "disconnect" );
	serverPort.SendPacket( from, outMsg.GetData(), outMsg.GetSize() );

	return false;
}

/*
==================
idAsyncServer::SendReliableGameMessage
==================
*/
void idAsyncServer::SendReliableGameMessage( int clientNum, const idBitMsg &msg ) {
	int			i;
	idBitMsg	outMsg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	outMsg.Init( msgBuf, sizeof( msgBuf ) );
	outMsg.WriteByte( SERVER_RELIABLE_MESSAGE_GAME );
	outMsg.WriteData( msg.GetData(), msg.GetSize() );

	if ( clientNum >= 0 && clientNum < MAX_ASYNC_CLIENTS ) {
		if ( clients[clientNum].clientState == SCS_INGAME ) {
			SendReliableMessage( clientNum, outMsg );
		}
		return;
	}

	for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		if ( clients[i].clientState != SCS_INGAME ) {
			continue;
		}
		SendReliableMessage( i, outMsg );
	}
}

/*
==================
idAsyncServer::LocalClientSendReliableMessageExcluding
==================
*/
void idAsyncServer::SendReliableGameMessageExcluding( int clientNum, const idBitMsg &msg ) {
	int			i;
	idBitMsg	outMsg;
	byte		msgBuf[MAX_MESSAGE_SIZE];

	assert( clientNum >= 0 && clientNum < MAX_ASYNC_CLIENTS );

	outMsg.Init( msgBuf, sizeof( msgBuf ) );
	outMsg.WriteByte( SERVER_RELIABLE_MESSAGE_GAME );
	outMsg.WriteData( msg.GetData(), msg.GetSize() );

	for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		if ( i == clientNum ) {
			continue;
		}
		if ( clients[i].clientState != SCS_INGAME ) {
			continue;
		}
		SendReliableMessage( i, outMsg );
	}
}

/*
==================
idAsyncServer::LocalClientSendReliableMessage
==================
*/
void idAsyncServer::LocalClientSendReliableMessage( const idBitMsg &msg ) {
	if ( localClientNum < 0 ) {
		common->Printf( "LocalClientSendReliableMessage: no local client\n" );
		return;
	}
	game->ServerProcessReliableMessage( localClientNum, msg );
}

/*
==================
idAsyncServer::ProcessConnectionLessMessages
==================
*/
void idAsyncServer::ProcessConnectionLessMessages( void ) {
	int			size, id;
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];
	netadr_t	from;

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

	while( serverPort.GetPacket( from, msgBuf, size, sizeof( msgBuf ) ) ) {
		msg.Init( msgBuf, sizeof( msgBuf ) );
		msg.SetSize( size );
		msg.BeginReading();
		id = msg.ReadShort();
		if ( id == CONNECTIONLESS_MESSAGE_ID ) {
			ConnectionlessMessage( from, msg );
		}
	}
}

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

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

/*
==================
idAsyncServer::RunFrame
==================
*/
void idAsyncServer::RunFrame( void ) {
	int			i, msec, size;
	bool		newPacket;
	idBitMsg	msg;
	byte		msgBuf[MAX_MESSAGE_SIZE];
	netadr_t	from;
	int			outgoingRate, incomingRate;
	float		outgoingCompression, incomingCompression;

	msec = UpdateTime( 100 );

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

	if ( !active ) {
		ProcessConnectionLessMessages();
		return;
	}

	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 = serverPort.GetPacketBlocking( from, msgBuf, size, sizeof( msgBuf ), USERCMD_MSEC - gameTimeResidual - 1 );
			if ( newPacket ) {
				msg.Init( msgBuf, sizeof( msgBuf ) );
				msg.SetSize( size );
				msg.BeginReading();
				if ( ProcessMessage( from, msg ) ) {
					return;	// return because rcon was used
				}
			}

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

		} while( newPacket );

	} while( gameTimeResidual < USERCMD_MSEC );

	// send heart beat to master servers
	MasterHeartbeat();

	// check for clients that timed out
	CheckClientTimeouts();

	if ( idAsyncNetwork::idleServer.GetBool() == ( !GetNumClients() || GetNumIdleClients() != GetNumClients()  ) ) {
		idAsyncNetwork::idleServer.SetBool( !idAsyncNetwork::idleServer.GetBool() );
		// the need to propagate right away, only this
		sessLocal.mapSpawnData.serverInfo.Set( "si_idleServer", idAsyncNetwork::idleServer.GetString() );
		game->SetServerInfo( sessLocal.mapSpawnData.serverInfo );
	}

	// make sure the time doesn't wrap
	if ( serverTime > 0x70000000 ) {
		ExecuteMapChange();
		return;
	}

	// check for synchronized cvar changes
	if ( cvarSystem->GetModifiedFlags() & CVAR_NETWORKSYNC ) {
		idDict newCvars;
		newCvars = *cvarSystem->MoveCVarsToDict( CVAR_NETWORKSYNC );
		SendSyncedCvarsBroadcast( newCvars );
		cvarSystem->ClearModifiedFlags( CVAR_NETWORKSYNC );
	}

	// check for user info changes of the local client
	if ( cvarSystem->GetModifiedFlags() & CVAR_USERINFO ) {
		if ( localClientNum >= 0 ) {
			idDict newInfo;
			game->ThrottleUserInfo( );
			newInfo = *cvarSystem->MoveCVarsToDict( CVAR_USERINFO );
			SendUserInfoBroadcast( localClientNum, newInfo );
		}
		cvarSystem->ClearModifiedFlags( CVAR_USERINFO );
	}

	// advance the server game
	while( gameTimeResidual >= USERCMD_MSEC ) {

		// sample input for the local client
		LocalClientInput();

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

		// advance game
		gameReturn_t ret = game->RunFrame( userCmds[gameFrame & ( MAX_USERCMD_BACKUP - 1 ) ] );

		idAsyncNetwork::ExecuteSessionCommand( ret.sessionCommand );

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

	// duplicate usercmds so there is always at least one available to send with snapshots
	DuplicateUsercmds( gameFrame, gameTime );

	// send snapshots to connected clients
	for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		serverClient_t &client = clients[i];

		if ( client.clientState == SCS_FREE || i == localClientNum ) {
			continue;
		}

		// modify maximum rate if necesary
		if ( idAsyncNetwork::serverMaxClientRate.IsModified() ) {
			client.channel.SetMaxOutgoingRate( Min( client.clientRate, idAsyncNetwork::serverMaxClientRate.GetInteger() ) );
		}

		// if the channel is not yet ready to send new data
		if ( !client.channel.ReadyToSend( serverTime ) ) {
			continue;
		}

		// send additional message fragments if the last message was too large to send at once
		if ( client.channel.UnsentFragmentsLeft() ) {
			client.channel.SendNextFragment( serverPort, serverTime );
			continue;
		}

		if ( client.clientState == SCS_INGAME ) {
			if ( !SendSnapshotToClient( i ) ) {
				SendPingToClient( i );
			}
		} else {
			SendEmptyToClient( i );
		}
	}

	if ( com_showAsyncStats.GetBool() ) {

		UpdateAsyncStatsAvg();

		// dedicated will verbose to console
		if ( idAsyncNetwork::serverDedicated.GetBool() && serverTime >= nextAsyncStatsTime ) {
			common->Printf( "delay = %d msec, total outgoing rate = %d KB/s, total incoming rate = %d KB/s\n", GetDelay(),
							GetOutgoingRate() >> 10, GetIncomingRate() >> 10 );

			for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {

				outgoingRate = GetClientOutgoingRate( i );
				incomingRate = GetClientIncomingRate( i );
				outgoingCompression = GetClientOutgoingCompression( i );
				incomingCompression = GetClientIncomingCompression( i );

				if ( outgoingRate != -1 && incomingRate != -1 ) {
					common->Printf( "client %d: out rate = %d B/s (% -2.1f%%), in rate = %d B/s (% -2.1f%%)\n",
									i, outgoingRate, outgoingCompression, incomingRate, incomingCompression );
				}
			}

			idStr msg;
			GetAsyncStatsAvgMsg( msg );
			common->Printf( va( "%s\n", msg.c_str() ) );

			nextAsyncStatsTime = serverTime + 1000;
		}
	}

	idAsyncNetwork::serverMaxClientRate.ClearModified();
}

/*
==================
idAsyncServer::PacifierUpdate
==================
*/
void idAsyncServer::PacifierUpdate( void ) {
	int i;

	if ( !IsActive() ) {
		return;
	}
	realTime = Sys_Milliseconds();
	ProcessConnectionLessMessages();
	for ( i = 0; i < MAX_ASYNC_CLIENTS; i++ ) {
		if ( clients[i].clientState >= SCS_PUREWAIT ) {
			if ( clients[i].channel.UnsentFragmentsLeft() ) {
				clients[i].channel.SendNextFragment( serverPort, serverTime );
			} else {
				SendEmptyToClient( i );
			}
		}
	}
}

/*
==================
idAsyncServer::PrintOOB
==================
*/
void idAsyncServer::PrintOOB( const netadr_t to, int opcode, const char *string ) {
	idBitMsg	outMsg;
	byte		msgBuf[ MAX_MESSAGE_SIZE ];

	outMsg.Init( msgBuf, sizeof( msgBuf ) );
	outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
	outMsg.WriteString( "print" );
	outMsg.WriteInt( opcode );
	outMsg.WriteString( string );
	serverPort.SendPacket( to, outMsg.GetData(), outMsg.GetSize() );
}

/*
==================
idAsyncServer::MasterHeartbeat
==================
*/
void idAsyncServer::MasterHeartbeat( bool force ) {
	if ( idAsyncNetwork::LANServer.GetBool() ) {
		if ( force ) {
			common->Printf( "net_LANServer is enabled. Not sending heartbeats\n" );
		}
		return;
	}
	if ( force ) {
		nextHeartbeatTime = 0;
	}
	// not yet
	if ( serverTime < nextHeartbeatTime ) {
		return;
	}
	nextHeartbeatTime = serverTime + HEARTBEAT_MSEC;
	for ( int i = 0 ; i < MAX_MASTER_SERVERS ; i++ ) {
		netadr_t adr;
		if ( idAsyncNetwork::GetMasterAddress( i, adr ) ) {
			common->Printf( "Sending heartbeat to %s\n", Sys_NetAdrToString( adr ) );
			idBitMsg outMsg;
			byte msgBuf[ MAX_MESSAGE_SIZE ];
			outMsg.Init( msgBuf, sizeof( msgBuf ) );
			outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
			outMsg.WriteString( "heartbeat" );
			serverPort.SendPacket( adr, outMsg.GetData(), outMsg.GetSize() );
		}
	}
}

/*
===============
idAsyncServer::SendEnterGameToClient
===============
*/
void idAsyncServer::SendEnterGameToClient( int clientNum ) {
	idBitMsg	msg;
	byte		msgBuf[ MAX_MESSAGE_SIZE ];

	msg.Init( msgBuf, sizeof( msgBuf ) );
	msg.WriteByte( SERVER_RELIABLE_MESSAGE_ENTERGAME );
	SendReliableMessage( clientNum, msg );
}

/*
===============
idAsyncServer::UpdateAsyncStatsAvg
===============
*/
void idAsyncServer::UpdateAsyncStatsAvg( void ) {
	stats_average_sum -= stats_outrate[ stats_current ];
	stats_outrate[ stats_current ] = idAsyncNetwork::server.GetOutgoingRate();
	if ( stats_outrate[ stats_current ] > stats_max ) {
		stats_max = stats_outrate[ stats_current ];
		stats_max_index = stats_current;
	} else if ( stats_current == stats_max_index ) {
		// find the new max
		int i;
		stats_max = 0;
		for ( i = 0; i < stats_numsamples ; i++ ) {
			if ( stats_outrate[ i ] > stats_max ) {
				stats_max = stats_outrate[ i ];
				stats_max_index = i;
			}
		}
	}
	stats_average_sum += stats_outrate[ stats_current ];
	stats_current++; stats_current %= stats_numsamples;
}

/*
===============
idAsyncServer::GetAsyncStatsAvgMsg
===============
*/
void idAsyncServer::GetAsyncStatsAvgMsg( idStr &msg ) {
	sprintf( msg, "avrg out: %d B/s - max %d B/s ( over %d ms )", stats_average_sum / stats_numsamples, stats_max, idAsyncNetwork::serverSnapshotDelay.GetInteger() * stats_numsamples );
}

/*
===============
idAsyncServer::ProcessDownloadRequestMessage
===============
*/
void idAsyncServer::ProcessDownloadRequestMessage( const netadr_t from, const idBitMsg &msg ) {
	int			challenge, clientId, iclient, numPaks, i;
	int			dlPakChecksum;
	int			dlSize[ MAX_PURE_PAKS ];	// sizes
	idStrList	pakNames;					// relative path
	idStrList	pakURLs;					// game URLs
	char		pakbuf[ MAX_STRING_CHARS ];
	idStr		paklist;
	byte		msgBuf[ MAX_MESSAGE_SIZE ];
	byte		tmpBuf[ MAX_MESSAGE_SIZE ];
	idBitMsg	outMsg, tmpMsg;
	int			dlRequest;
	int			voidSlots = 0;				// to count and verbose the right number of paks requested for downloads

	challenge = msg.ReadInt();
	clientId = msg.ReadShort();
	dlRequest = msg.ReadInt();

	if ( ( iclient = ValidateChallenge( from, challenge, clientId ) ) == -1 ) {
		return;
	}

	if ( challenges[ iclient ].authState != CDK_PUREWAIT ) {
		common->DPrintf( "client %s: got download request message, not in CDK_PUREWAIT\n", Sys_NetAdrToString( from ) );
		return;
	}

	pakNames.Append( pakbuf );
	numPaks = 1;

	// read the checksums, build path names and pass that to the game code
	dlPakChecksum = msg.ReadInt();
	while ( dlPakChecksum ) {
		if ( !( dlSize[ numPaks ] = fileSystem->ValidateDownloadPakForChecksum( dlPakChecksum, pakbuf ) ) ) {
			// we pass an empty token to the game so our list doesn't get offset
			common->Warning( "client requested an unknown pak 0x%x", dlPakChecksum );
			pakbuf[ 0 ] = '\0';
			voidSlots++;
		}
		pakNames.Append( pakbuf );
		numPaks++;
		dlPakChecksum = msg.ReadInt();
	}

	for ( i = 0; i < pakNames.Num(); i++ ) {
		if ( i > 0 ) {
			paklist += ";";
		}
		paklist += pakNames[ i ].c_str();
	}

	// read the message and pass it to the game code
	common->DPrintf( "got download request for %d paks - %s\n", numPaks - voidSlots, paklist.c_str() );

	outMsg.Init( msgBuf, sizeof( msgBuf ) );
	outMsg.WriteShort( CONNECTIONLESS_MESSAGE_ID );
	outMsg.WriteString( "downloadInfo" );
	outMsg.WriteInt( dlRequest );
	if ( !game->DownloadRequest( Sys_NetAdrToString( from ), challenges[ iclient ].guid, paklist.c_str(), pakbuf ) ) {
		common->DPrintf( "game: no downloads\n" );
		outMsg.WriteByte( SERVER_DL_NONE );
		serverPort.SendPacket( from, outMsg.GetData(), outMsg.GetSize() );
		return;
	}

	char *token, *next;
	int type = 0;

	token = pakbuf;
	next = strchr( token, ';' );
	while ( token ) {
		if ( next ) {
			*next = '\0';
		}

		if ( type == 0 ) {
			type = atoi( token );
		} else if ( type == SERVER_DL_REDIRECT ) {
			common->DPrintf( "download request: redirect to URL %s\n", token );
			outMsg.WriteByte( SERVER_DL_REDIRECT );
			outMsg.WriteString( token );
			serverPort.SendPacket( from, outMsg.GetData(), outMsg.GetSize() );
			return;
		} else if ( type == SERVER_DL_LIST ) {
			pakURLs.Append( token );
		} else {
			common->DPrintf( "wrong op type %d\n", type );
			next = token = NULL;
		}

		if ( next ) {
			token = next + 1;
			next = strchr( token, ';' );
		} else {
			token = NULL;
		}
	}

	if ( type == SERVER_DL_LIST ) {
		int totalDlSize = 0;
		int numActualPaks = 0;

		// put the answer packet together
		outMsg.WriteByte( SERVER_DL_LIST );

		tmpMsg.Init( tmpBuf, MAX_MESSAGE_SIZE );

		for ( i = 0; i < pakURLs.Num(); i++ ) {
			tmpMsg.BeginWriting();
			if ( !dlSize[ i ] || !pakURLs[ i ].Length() ) {
				// still send the relative path so the client knows what it missed
				tmpMsg.WriteByte( SERVER_PAK_NO );
				tmpMsg.WriteString( pakNames[ i ] );
			} else {
				totalDlSize += dlSize[ i ];
				numActualPaks++;
				tmpMsg.WriteByte( SERVER_PAK_YES );
				tmpMsg.WriteString( pakNames[ i ] );
				tmpMsg.WriteString( pakURLs[ i ] );
				tmpMsg.WriteInt( dlSize[ i ] );
			}

			// keep last 5 bytes for an 'end of message' - SERVER_PAK_END and the totalDlSize long
			if ( outMsg.GetRemainingSpace() - tmpMsg.GetSize() > 5 ) {
				outMsg.WriteData( tmpMsg.GetData(), tmpMsg.GetSize() );
			} else {
				outMsg.WriteByte( SERVER_PAK_END );
				break;
			}
		}
		if ( i == pakURLs.Num() ) {
			// put a closure even if size not exceeded
			outMsg.WriteByte( SERVER_PAK_END );
		}
		common->DPrintf( "download request: download %d paks, %d bytes\n", numActualPaks, totalDlSize );

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