예제 #1
0
/*
=====================
CL_ConfigstringModified
=====================
*/
void CL_ConfigstringModified( Cmd::Args& csCmd )
{
	if (csCmd.Argc() < 3) {
		Com_Error( errorParm_t::ERR_DROP, "CL_ConfigstringModified: wrong command received" );
	}

	int index = atoi( csCmd.Argv(1).c_str() );

	if ( index < 0 || index >= MAX_CONFIGSTRINGS )
	{
		Com_Error( errorParm_t::ERR_DROP, "CL_ConfigstringModified: bad index %i", index );
	}

	if ( cl.gameState[index] == csCmd.Argv(2) )
	{
		return;
	}

	cl.gameState[index] = csCmd.Argv(2);

	if ( index == CS_SYSTEMINFO )
	{
		// parse serverId and other cvars
		CL_SystemInfoChanged();
	}
}
예제 #2
0
/*
================
SVC_Status

Responds with all the info that qplug or qspy can see about the server
and all connected players.  Used for getting detailed information after
the simple info query.
================
*/
void SVC_Status( netadr_t from, const Cmd::Args& args )
{
	char          player[ 1024 ];
	char          status[ MAX_MSGLEN ];
	int           i;
	client_t      *cl;
	playerState_t *ps;
	int           statusLength;
	int           playerLength;
	char          infostring[ MAX_INFO_STRING ];

	//bani - bugtraq 12534
	if ( args.Argc() > 1 && !SV_VerifyChallenge( args.Argv(1).c_str() ) )
	{
		return;
	}

	Q_strncpyz( infostring, Cvar_InfoString( CVAR_SERVERINFO, false ), MAX_INFO_STRING );

	if ( args.Argc() > 1 )
	{
		// echo back the parameter to status. so master servers can use it as a challenge
		// to prevent timed spoofed reply packets that add ghost servers
		Info_SetValueForKey( infostring, "challenge", args.Argv(1).c_str(), false );
	}

	status[ 0 ] = 0;
	statusLength = 0;

	for ( i = 0; i < sv_maxclients->integer; i++ )
	{
		cl = &svs.clients[ i ];

		if ( cl->state >= CS_CONNECTED )
		{
			ps = SV_GameClientNum( i );
			Com_sprintf( player, sizeof( player ), "%i %i \"%s\"\n", ps->persistant[ PERS_SCORE ], cl->ping, cl->name );
			playerLength = strlen( player );

			if ( statusLength + playerLength >= (int) sizeof( status ) )
			{
				break; // can't hold any more
			}

			strcpy( status + statusLength, player );
			statusLength += playerLength;
		}
	}

	NET_OutOfBandPrint( NS_SERVER, from, "statusResponse\n%s\n%s", infostring, status );
}
예제 #3
0
/*
==================
SV_NextDownload_f

The argument will be the last acknowledged block from the client, it should be
the same as cl->downloadClientBlock
==================
*/
void SV_NextDownload_f( client_t *cl, const Cmd::Args& args )
{
	int block;
	if (args.Argc() < 2 or not Str::ParseInt(block, args.Argv(1))) {
		return;
	}

	if ( block == cl->downloadClientBlock )
	{
		Log::Debug( "clientDownload: %d: client acknowledge of block %d", ( int )( cl - svs.clients ), block );

		// Find out if we are done.  A zero-length block indicates EOF
		if ( cl->downloadBlockSize[ cl->downloadClientBlock % MAX_DOWNLOAD_WINDOW ] == 0 )
		{
			Log::Notice( "clientDownload: %d : file \"%s\" completed\n", ( int )( cl - svs.clients ), cl->downloadName );
			SV_CloseDownload( cl );
			return;
		}

		cl->downloadSendTime = svs.time;
		cl->downloadClientBlock++;
		return;
	}

	// We aren't getting an acknowledge for the correct block, drop the client
	// FIXME: this is bad... the client will never parse the disconnect message
	//          because the cgame isn't loaded yet
	SV_DropClient( cl, "broken download" );
}
예제 #4
0
            void StartTest(const Cmd::Args& args) const {
                const std::string& name = args.Argv(1);

                if (name == "none") {
                    testingReverb = true;
                    for (int i = 0; i < N_REVERB_SLOTS; i++) {
                        reverbSlots[i].effect->SetGain(0.0f);
                    }
                    return;
                }

                auto preset = AL::GetPresetByName(name);

                if (not preset) {
                    Print("Reverb preset '%s' doesn't exist.", name);
                    return;
                }

                AL::Effect effectParams;
                effectParams.ApplyReverbPreset(*preset);

                testingReverb = true;
                for (int i = 1; i < N_REVERB_SLOTS; i++) {
                    reverbSlots[i].effect->SetGain(0.0f);
                }

                reverbSlots[0].ratio = 0.5f;
                reverbSlots[0].effect->SetEffect(effectParams);
            }
예제 #5
0
/*
==================
SV_BeginDownload_f
==================
*/
void SV_BeginDownload_f( client_t *cl, const Cmd::Args& args )
{
	// Kill any existing download
	SV_CloseDownload( cl );

	if (args.Argc() < 2) {
		return;
	}

	//bani - stop us from printing dupe messages
	if (args.Argv(1) != cl->downloadName)
	{
		cl->downloadnotify = DLNOTIFY_ALL;
	}

	// cl->downloadName is non-zero now, SV_WriteDownloadToClient will see this and open
	// the file itself
	Q_strncpyz( cl->downloadName, args.Argv(1).c_str(), sizeof( cl->downloadName ) );
}
예제 #6
0
/*
==================
SV_UpdateUserinfo_f
==================
*/
static void SV_UpdateUserinfo_f( client_t *cl, const Cmd::Args& args )
{
	if (args.Argc() < 2) {
		return;
	}

	Q_strncpyz(cl->userinfo, args.Argv(1).c_str(), sizeof(cl->userinfo)); // FIXME QUOTING INFO

	SV_UserinfoChanged( cl );
	// call prog code to allow overrides
	gvm.GameClientUserInfoChanged( cl - svs.clients );
}
예제 #7
0
/*
==================
SV_DirectConnect

A "connect" OOB command has been received
==================
*/
void SV_DirectConnect( netadr_t from, const Cmd::Args& args )
{
	if ( args.Argc() < 2 )
	{
		return;
	}

	Log::Debug( "SVC_DirectConnect ()" );

	InfoMap userinfo = InfoStringToMap(args.Argv(1));

	// DHM - Nerve :: Update Server allows any protocol to connect
	// NOTE TTimo: but we might need to store the protocol around for potential non http/ftp clients
	int version = atoi( userinfo["protocol"].c_str() );

	if ( version != PROTOCOL_VERSION )
	{
		Net::OutOfBandPrint( netsrc_t::NS_SERVER, from, "print\nServer uses protocol version %i (yours is %i).", PROTOCOL_VERSION, version );
		Log::Debug( "    rejected connect from version %i", version );
		return;
	}

	int qport = atoi( userinfo["qport"].c_str() );

	auto clients_begin = svs.clients;
	auto clients_end = clients_begin + sv_maxclients->integer;

	client_t* reconnecting = std::find_if(clients_begin, clients_end,
		[&from, qport](const client_t& client)
		{
			return NET_CompareBaseAdr( from, client.netchan.remoteAddress )
		     && ( client.netchan.qport == qport || from.port == client.netchan.remoteAddress.port );
		}
	);

	if ( reconnecting != clients_end &&
		svs.time - reconnecting->lastConnectTime < sv_reconnectlimit->integer * 1000 )
	{
		Log::Debug( "%s: reconnect rejected: too soon", NET_AdrToString( from ) );
		return;
	}


	if ( NET_IsLocalAddress( from ) )
	{
		userinfo["ip"] = "loopback";
	}
	else
	{
		// see if the challenge is valid (local clients don't need to challenge)
		Challenge::Duration ping_duration;
		if ( !ChallengeManager::MatchString( from, userinfo["challenge"], &ping_duration ) )
		{
			Net::OutOfBandPrint( netsrc_t::NS_SERVER, from, "print\n[err_dialog]No or bad challenge for address." );
			return;
		}

		userinfo["ip"] = NET_AdrToString( from );
	}

	client_t *new_client = nullptr;

	// if there is already a slot for this IP address, reuse it
	if ( reconnecting != clients_end )
	{
		Log::Notice( "%s:reconnect\n", NET_AdrToString( from ) );
		new_client = reconnecting;
	}
	else
	{
		// find a client slot
		// if "sv_privateClients" is set > 0, then that number
		// of client slots will be reserved for connections that
		// have "password" set to the value of "sv_privatePassword"
		// Info requests will report the maxclients as if the private
		// slots didn't exist, to prevent people from trying to connect
		// to a full server.
		// This is to allow us to reserve a couple slots here on our
		// servers so we can play without having to kick people.
		// check for privateClient password

		int startIndex = 0;
		if ( userinfo["password"] != sv_privatePassword->string )
		{
			// skip past the reserved slots
			startIndex = sv_privateClients->integer;
		}

		new_client = std::find_if(clients_begin, clients_end,
			[](const client_t& client) {
				return client.state == clientState_t::CS_FREE;
		});

		if ( new_client == clients_end )
		{
			if ( NET_IsLocalAddress( from ) )
			{
				int count = std::count_if(clients_begin+startIndex, clients_end,
					[](const client_t& client) { return SV_IsBot(&client); }
				);

				// if they're all bots
				if ( count >= sv_maxclients->integer - startIndex )
				{
					SV_DropClient( &svs.clients[ sv_maxclients->integer - 1 ], "only bots on server" );
					new_client = &svs.clients[ sv_maxclients->integer - 1 ];
				}
				else
				{
					Com_Error( errorParm_t::ERR_FATAL, "server is full on local connect" );
				}
			}
			else
			{
				Net::OutOfBandPrint( netsrc_t::NS_SERVER, from, "print\n%s", sv_fullmsg->string );
				Log::Debug( "Rejected a connection." );
				return;
			}
		}
	}

	// build a new connection
	// accept the new client
	// this is the only place a client_t is ever initialized
	memset( new_client, 0, sizeof( client_t ) );
	int clientNum = new_client - svs.clients;


#ifdef HAVE_GEOIP
		const char * country = NET_GeoIP_Country( &from );

		if ( country )
		{
			Log::Notice( "Client %i connecting from %s\n", clientNum, country );
			userinfo["geoip"] = country;
		}
		else
		{
			Log::Notice( "Client %i connecting from somewhere unknown\n", clientNum );
		}
#else
		Log::Notice( "Client %i connecting\n", clientNum );
#endif

	new_client->gentity = SV_GentityNum( clientNum );
	new_client->gentity->r.svFlags = 0;

	// save the address
	Netchan_Setup( netsrc_t::NS_SERVER, &new_client->netchan, from, qport );
	// init the netchan queue

	// Save the pubkey
	Q_strncpyz( new_client->pubkey, userinfo["pubkey"].c_str(), sizeof( new_client->pubkey ) );
	userinfo.erase("pubkey");
	// save the userinfo
	Q_strncpyz( new_client->userinfo, InfoMapToString(userinfo).c_str(), sizeof( new_client->userinfo ) );

	// get the game a chance to reject this connection or modify the userinfo
	char reason[ MAX_STRING_CHARS ];
	if ( gvm.GameClientConnect( reason, sizeof( reason ), clientNum, true, false ) )
	{
		Net::OutOfBandPrint( netsrc_t::NS_SERVER, from, "print\n[err_dialog]%s", reason );
		Log::Debug( "Game rejected a connection: %s.", reason );
		return;
	}

	SV_UserinfoChanged( new_client );

	// send the connect packet to the client
	Net::OutOfBandPrint( netsrc_t::NS_SERVER, from, "connectResponse" );

	Log::Debug( "Going from CS_FREE to CS_CONNECTED for %s", new_client->name );

	new_client->state = clientState_t::CS_CONNECTED;
	new_client->nextSnapshotTime = svs.time;
	new_client->lastPacketTime = svs.time;
	new_client->lastConnectTime = svs.time;

	// when we receive the first packet from the client, we will
	// notice that it is from a different serverid and that the
	// gamestate message was not just sent, forcing a retransmit
	new_client->gamestateMessageNum = -1;

	// if this was the first client on the server, or the last client
	// the server can hold, send a heartbeat to the master.
	int count = std::count_if(clients_begin, clients_end,
		[](const client_t& client) {
			return client.state >= clientState_t::CS_CONNECTED;
	});

	if ( count == 1 || count == sv_maxclients->integer )
	{
		SV_Heartbeat_f();
	}
}
예제 #8
0
/*
==================
SV_WWWDownload_f
==================
*/
void SV_WWWDownload_f( client_t *cl, const Cmd::Args& args )
{
	if (args.Argc() < 2) {
		return;
	}

	const char *subcmd = args.Argv(1).c_str();

	// only accept wwwdl commands for clients which we first flagged as wwwdl ourselves
	if ( !cl->bWWWDl )
	{
		Log::Notice( "SV_WWWDownload: unexpected wwwdl '%s' for client '%s'\n", subcmd, cl->name );
		SV_DropClient( cl, va( "SV_WWWDownload: unexpected wwwdl %s", subcmd ) );
		return;
	}

	if ( !Q_stricmp( subcmd, "ack" ) )
	{
		if ( cl->bWWWing )
		{
			Log::Warn("dupe wwwdl ack from client '%s'", cl->name );
		}

		cl->bWWWing = true;
		return;
	}
	else if ( !Q_stricmp( subcmd, "bbl8r" ) )
	{
		SV_DropClient( cl, "acking disconnected download mode" );
		return;
	}

	// below for messages that only happen during/after download
	if ( !cl->bWWWing )
	{
		Log::Notice( "SV_WWWDownload: unexpected wwwdl '%s' for client '%s'\n", subcmd, cl->name );
		SV_DropClient( cl, va( "SV_WWWDownload: unexpected wwwdl %s", subcmd ) );
		return;
	}

	if ( !Q_stricmp( subcmd, "done" ) )
	{
		*cl->downloadName = 0;
		cl->bWWWing = false;
		return;
	}
	else if ( !Q_stricmp( subcmd, "fail" ) )
	{
		*cl->downloadName = 0;
		cl->bWWWing = false;
		cl->bFallback = true;
		// send a reconnect
		SV_SendClientGameState( cl );
		return;
	}
	else if ( !Q_stricmp( subcmd, "chkfail" ) )
	{
		Log::Warn("client '%s' reports that the redirect download for '%s' had wrong checksum.\n\tYou should check your download redirect configuration.",
				 cl->name, cl->downloadName );
		*cl->downloadName = 0;
		cl->bWWWing = false;
		cl->bFallback = true;
		// send a reconnect
		SV_SendClientGameState( cl );
		return;
	}

	Log::Notice("SV_WWWDownload: unknown wwwdl subcommand '%s' for client '%s'\n", subcmd, cl->name );
	SV_DropClient( cl, va( "SV_WWWDownload: unknown wwwdl subcommand '%s'", subcmd ) );
}
예제 #9
0
/*
================
SVC_Info

Responds with a short info message that should be enough to determine
if a user is interested in a server to do a full status
================
*/
void SVC_Info( netadr_t from, const Cmd::Args& args )
{
	int  i, count, botCount;
	char infostring[ MAX_INFO_STRING ];

	if ( args.Argc() < 2 )
	{
		return;
	}

	const char *challenge = args.Argv(1).c_str();

	/*
	 * Check whether Cmd_Argv(1) has a sane length. This was not done in the original Quake3 version which led
	 * to the Infostring bug discovered by Luigi Auriemma. See http://aluigi.altervista.org/ for the advisory.
	*/
	// A maximum challenge length of 128 should be more than plenty.
	if ( strlen( challenge ) > MAX_CHALLENGE_LEN  )
	{
		return;
	}

	//bani - bugtraq 12534
	if ( !SV_VerifyChallenge( challenge ) )
	{
		return;
	}

	SV_ResolveMasterServers();

	// don't count privateclients
	botCount = count = 0;

	for ( i = sv_privateClients->integer; i < sv_maxclients->integer; i++ )
	{
		if ( svs.clients[ i ].state >= CS_CONNECTED )
		{
			if ( SV_IsBot(&svs.clients[ i ]) )
			{
				++botCount;
			}
			else
			{
				++count;
			}
		}
	}

	infostring[ 0 ] = 0;

	// echo back the parameter to status. so servers can use it as a challenge
	// to prevent timed spoofed reply packets that add ghost servers
	Info_SetValueForKey( infostring, "challenge", challenge, false );

	// If the master server listens on IPv4 and IPv6, we want to send the
	// most recent challenge received from it over the OTHER protocol
	for ( i = 0; i < MAX_MASTER_SERVERS; i++ )
	{
		// First, see if the challenge was sent by this master server
		if ( !NET_CompareBaseAdr( from, masterServerAddr[ i ].ipv4 ) && !NET_CompareBaseAdr( from, masterServerAddr[ i ].ipv6 ) )
		{
			continue;
		}

		// It was - if the saved challenge is for the other protocol, send it and record the current one
		if ( challenges[ i ].type == NA_IP || challenges[ i ].type == NA_IP6 )
		{
			if ( challenges[ i ].type != from.type )
			{
				Info_SetValueForKey( infostring, "challenge2", challenges[ i ].text, false );
				challenges[ i ].type = from.type;
				strcpy( challenges[ i ].text, challenge );
				break;
			}
		}

		// Otherwise record the current one regardless and check the next server
		challenges[ i ].type = from.type;
		strcpy( challenges[ i ].text, challenge );
	}

	Info_SetValueForKey( infostring, "protocol", va( "%i", PROTOCOL_VERSION ), false );
	Info_SetValueForKey( infostring, "hostname", sv_hostname->string, false );
	Info_SetValueForKey( infostring, "serverload", va( "%i", svs.serverLoad ), false );
	Info_SetValueForKey( infostring, "mapname", sv_mapname->string, false );
	Info_SetValueForKey( infostring, "clients", va( "%i", count ), false );
	Info_SetValueForKey( infostring, "bots", va( "%i", botCount ), false );
	Info_SetValueForKey( infostring, "sv_maxclients", va( "%i", sv_maxclients->integer - sv_privateClients->integer ), false );
	Info_SetValueForKey( infostring, "pure", va( "%i", sv_pure->integer ), false );

	if ( sv_statsURL->string[0] )
	{
		Info_SetValueForKey( infostring, "stats", sv_statsURL->string, false );
	}

#ifdef USE_VOIP

	if ( sv_voip->integer )
	{
		Info_SetValueForKey( infostring, "voip", va( "%i", sv_voip->integer ), false );
	}

#endif

	if ( sv_minPing->integer )
	{
		Info_SetValueForKey( infostring, "minPing", va( "%i", sv_minPing->integer ), false );
	}

	if ( sv_maxPing->integer )
	{
		Info_SetValueForKey( infostring, "maxPing", va( "%i", sv_maxPing->integer ), false );
	}

	Info_SetValueForKey( infostring, "gamename", GAMENAME_STRING, false );  // Arnout: to be able to filter out Quake servers

	NET_OutOfBandPrint( NS_SERVER, from, "infoResponse\n%s", infostring );
}
예제 #10
0
/*
==================
SV_DirectConnect

A "connect" OOB command has been received
==================
*/
void SV_DirectConnect( netadr_t from, const Cmd::Args& args )
{
	char                userinfo[ MAX_INFO_STRING ];
	int                 i;
	client_t            *cl, *newcl;
	client_t            temp;
	sharedEntity_t      *ent;
	int                 clientNum;
	int                 version;
	int                 qport;
	int                 challenge;
	const char                *password;
	int                 startIndex;
	bool            denied;
	char                reason[ MAX_STRING_CHARS ];
	int                 count;
	const char          *ip;
#ifdef HAVE_GEOIP
	const char          *country = nullptr;
#endif

	if ( args.Argc() < 2 )
	{
		return;
	}

	Log::Debug( "SVC_DirectConnect ()" );

	Q_strncpyz( userinfo, args.Argv(1).c_str(), sizeof( userinfo ) );

	// DHM - Nerve :: Update Server allows any protocol to connect
	// NOTE TTimo: but we might need to store the protocol around for potential non http/ftp clients
	version = atoi( Info_ValueForKey( userinfo, "protocol" ) );

	if ( version != PROTOCOL_VERSION )
	{
		NET_OutOfBandPrint( netsrc_t::NS_SERVER, from, "print\nServer uses protocol version %i (yours is %i).", PROTOCOL_VERSION, version );
		Log::Debug( "    rejected connect from version %i", version );
		return;
	}

	challenge = atoi( Info_ValueForKey( userinfo, "challenge" ) );
	qport = atoi( Info_ValueForKey( userinfo, "qport" ) );

	// quick reject
	for ( i = 0, cl = svs.clients; i < sv_maxclients->integer; i++, cl++ )
	{
		// DHM - Nerve :: This check was allowing clients to reconnect after zombietime(2 secs)
		//if ( cl->state == CS_FREE ) {
		//continue;
		//}
		if ( NET_CompareBaseAdr( from, cl->netchan.remoteAddress )
		     && ( cl->netchan.qport == qport
		          || from.port == cl->netchan.remoteAddress.port ) )
		{
			if ( ( svs.time - cl->lastConnectTime )
			     < ( sv_reconnectlimit->integer * 1000 ) )
			{
				Log::Debug( "%s: reconnect rejected: too soon", NET_AdrToString( from ) );
				return;
			}

			break;
		}
	}

	if ( NET_IsLocalAddress( from ) )
	{
		ip = "localhost";
	}
	else
	{
		ip = NET_AdrToString( from );
	}

	if ( ( strlen( ip ) + strlen( userinfo ) + 4 ) >= MAX_INFO_STRING )
	{
		NET_OutOfBandPrint( netsrc_t::NS_SERVER, from,
		                    "print\nUserinfo string length exceeded.  "
		                    "Try removing setu cvars from your config." );
		return;
	}

	Info_SetValueForKey( userinfo, "ip", ip, false );

	// see if the challenge is valid (local clients don't need to challenge)
	if ( !NET_IsLocalAddress( from ) )
	{
		int ping;

		for ( i = 0; i < MAX_CHALLENGES; i++ )
		{
			if ( NET_CompareAdr( from, svs.challenges[ i ].adr ) )
			{
				if ( challenge == svs.challenges[ i ].challenge )
				{
					break; // good
				}
			}
		}

		if ( i == MAX_CHALLENGES )
		{
			NET_OutOfBandPrint( netsrc_t::NS_SERVER, from, "print\n[err_dialog]No or bad challenge for address." );
			return;
		}

		// force the IP address key/value pair, so the game can filter based on it
		Info_SetValueForKey( userinfo, "ip", NET_AdrToString( from ), false );

		if ( svs.challenges[ i ].firstPing == 0 )
		{
			ping = svs.time - svs.challenges[ i ].pingTime;
			svs.challenges[ i ].firstPing = ping;
		}
		else
		{
			ping = svs.challenges[ i ].firstPing;
		}

#ifdef HAVE_GEOIP
		country = NET_GeoIP_Country( &from );

		if ( country )
		{
			Log::Notice( "Client %i connecting from %s with %i challenge ping\n", i, country, ping );
		}
		else
		{
			Log::Notice( "Client %i connecting from somewhere unknown with %i challenge ping\n", i, ping );
		}
#else
		Log::Notice( "Client %i connecting with %i challenge ping\n", i, ping );
#endif

		svs.challenges[ i ].connected = true;

		// never reject a LAN client based on ping
		if ( !Sys_IsLANAddress( from ) )
		{
			if ( sv_minPing->value && ping < sv_minPing->value )
			{
				NET_OutOfBandPrint( netsrc_t::NS_SERVER, from, "print\n[err_dialog]Server is for high pings only" );
				Log::Debug( "Client %i rejected on a too low ping", i );
				return;
			}

			if ( sv_maxPing->value && ping > sv_maxPing->value )
			{
				NET_OutOfBandPrint( netsrc_t::NS_SERVER, from, "print\n[err_dialog]Server is for low pings only" );
				Log::Debug( "Client %i rejected on a too high ping: %i", i, ping );
				return;
			}
		}
	}
	else
	{
		// force the "ip" info key to "localhost"
		Info_SetValueForKey( userinfo, "ip", "localhost", false );
	}

	newcl = &temp;
	memset( newcl, 0, sizeof( client_t ) );

	// if there is already a slot for this IP address, reuse it
	for ( i = 0, cl = svs.clients; i < sv_maxclients->integer; i++, cl++ )
	{
		if ( cl->state == clientState_t::CS_FREE )
		{
			continue;
		}

		if ( NET_CompareBaseAdr( from, cl->netchan.remoteAddress )
		     && ( cl->netchan.qport == qport
		          || from.port == cl->netchan.remoteAddress.port ) )
		{
			Log::Notice( "%s:reconnect\n", NET_AdrToString( from ) );
			newcl = cl;

			// this doesn't work because it nukes the players userinfo

//			// disconnect the client from the game first so any flags the
//			// player might have are dropped
//			VM_Call( gvm, GAME_CLIENT_DISCONNECT, newcl - svs.clients );
			//
			goto gotnewcl;
		}
	}

	// find a client slot
	// if "sv_privateClients" is set > 0, then that number
	// of client slots will be reserved for connections that
	// have "password" set to the value of "sv_privatePassword"
	// Info requests will report the maxclients as if the private
	// slots didn't exist, to prevent people from trying to connect
	// to a full server.
	// This is to allow us to reserve a couple slots here on our
	// servers so we can play without having to kick people.

	// check for privateClient password
	password = Info_ValueForKey( userinfo, "password" );

	if ( !strcmp( password, sv_privatePassword->string ) )
	{
		startIndex = 0;
	}
	else
	{
		// skip past the reserved slots
		startIndex = sv_privateClients->integer;
	}

	newcl = nullptr;

	for ( i = startIndex; i < sv_maxclients->integer; i++ )
	{
		cl = &svs.clients[ i ];

		if ( cl->state == clientState_t::CS_FREE )
		{
			newcl = cl;
			break;
		}
	}

	if ( !newcl )
	{
		if ( NET_IsLocalAddress( from ) )
		{
			count = 0;

			for ( i = startIndex; i < sv_maxclients->integer; i++ )
			{
				cl = &svs.clients[ i ];

				if ( SV_IsBot(cl) )
				{
					count++;
				}
			}

			// if they're all bots
			if ( count >= sv_maxclients->integer - startIndex )
			{
				SV_DropClient( &svs.clients[ sv_maxclients->integer - 1 ], "only bots on server" );
				newcl = &svs.clients[ sv_maxclients->integer - 1 ];
			}
			else
			{
				Com_Error( errorParm_t::ERR_FATAL, "server is full on local connect" );
			}
		}
		else
		{
			NET_OutOfBandPrint( netsrc_t::NS_SERVER, from, "print\n%s", sv_fullmsg->string );
			Log::Debug( "Rejected a connection." );
			return;
		}
	}

	// we got a newcl, so reset the reliableSequence and reliableAcknowledge
	cl->reliableAcknowledge = 0;
	cl->reliableSequence = 0;

gotnewcl:
	// build a new connection
	// accept the new client
	// this is the only place a client_t is ever initialized
	*newcl = std::move(temp);
	clientNum = newcl - svs.clients;
	ent = SV_GentityNum( clientNum );
	newcl->gentity = ent;
	ent->r.svFlags = 0;

#ifdef HAVE_GEOIP

	if ( country )
	{
		Info_SetValueForKey( userinfo, "geoip", country, false );
	}
#endif

	// save the challenge
	newcl->challenge = challenge;

	// save the address
	Netchan_Setup( netsrc_t::NS_SERVER, &newcl->netchan, from, qport );
	// init the netchan queue

	// Save the pubkey
	Q_strncpyz( newcl->pubkey, Info_ValueForKey( userinfo, "pubkey" ), sizeof( newcl->pubkey ) );
	Info_RemoveKey( userinfo, "pubkey", false );
	// save the userinfo
	Q_strncpyz( newcl->userinfo, userinfo, sizeof( newcl->userinfo ) );

	// get the game a chance to reject this connection or modify the userinfo
	denied = gvm.GameClientConnect( reason, sizeof( reason ), clientNum, true, false );  // firstTime = true

	if ( denied )
	{
		NET_OutOfBandPrint( netsrc_t::NS_SERVER, from, "print\n[err_dialog]%s", reason );
		Log::Debug( "Game rejected a connection: %s.", reason );
		return;
	}

	SV_UserinfoChanged( newcl );

	// DHM - Nerve :: Clear out firstPing now that client is connected
	svs.challenges[ i ].firstPing = 0;

	// send the connect packet to the client
	NET_OutOfBandPrint( netsrc_t::NS_SERVER, from, "connectResponse" );

	Log::Debug( "Going from CS_FREE to CS_CONNECTED for %s", newcl->name );

	newcl->state = clientState_t::CS_CONNECTED;
	newcl->nextSnapshotTime = svs.time;
	newcl->lastPacketTime = svs.time;
	newcl->lastConnectTime = svs.time;

	// when we receive the first packet from the client, we will
	// notice that it is from a different serverid and that the
	// gamestate message was not just sent, forcing a retransmit
	newcl->gamestateMessageNum = -1;

	// if this was the first client on the server, or the last client
	// the server can hold, send a heartbeat to the master.
	count = 0;

	for ( i = 0, cl = svs.clients; i < sv_maxclients->integer; i++, cl++ )
	{
		if ( svs.clients[ i ].state >= clientState_t::CS_CONNECTED )
		{
			count++;
		}
	}

	if ( count == 1 || count == sv_maxclients->integer )
	{
		SV_Heartbeat_f();
	}
}