mirror of
https://github.com/Q3Rally-Team/q3rally.git
synced 2024-11-26 22:01:50 +00:00
3b4f4cdfa9
Some revision messages: Cache servers for each master server in q3_ui, otherwise servers from last updated master for shown for all Internet# sources. Play correct team sounds when in spectator mode and following a player. Check last listener number instead of clc.clientNum in S_AL_HearingThroughEntity so sound work correctly when spectate following a client. (Related to bug 5741.) When in third person, don't play player's sounds as full volume in Base sound system. OpenAL already does this. (Related to bug 5741.) really fix the confusion with game entity and refentity numbers to further reduce confusion, rename constants like MAX_ENTITIES to MAX_REFENTITIES Added Rend2, an alternate renderer. (Bug #4358) Fix restoring fs_game when default.cfg is missing. Fix restoring old fs_game upon leaving a server. Patch by Ensiform. Change more operator commands to require sv_running to be usable. Patch by Ensiform. Fix some "> MAX_*" to be ">= MAX_*". Fix follow command to find clients whose name begins with a number. Fix up "gc" command, make it more like "tell". Based on patch by Ensiform. Add usage messages for gc, tell, vtell, and votell commands. Check player names in gc, tell, vtell, and votell commands. #5799 - Change messagemode text box to display colors like in console input box. Improve "play" command, based on a patch from Ensiform. Check for invalid filename in OpenAL's RegisterSound function. Changed Base sound system to warn not error when sound filename is empty or too long. Remove references to non-existent functions CM_MarkFragments and CM_LerpTag.
1305 lines
34 KiB
C
1305 lines
34 KiB
C
/*
|
|
===========================================================================
|
|
Copyright (C) 1999-2005 Id Software, Inc.
|
|
|
|
This file is part of Quake III Arena source code.
|
|
|
|
Quake III Arena 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 2 of the License,
|
|
or (at your option) any later version.
|
|
|
|
Quake III Arena 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 Quake III Arena source code; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
===========================================================================
|
|
*/
|
|
|
|
#include "server.h"
|
|
|
|
#ifdef USE_VOIP
|
|
cvar_t *sv_voip;
|
|
#endif
|
|
|
|
serverStatic_t svs; // persistant server info
|
|
server_t sv; // local server
|
|
vm_t *gvm = NULL; // game virtual machine
|
|
|
|
cvar_t *sv_fps = NULL; // time rate for running non-clients
|
|
cvar_t *sv_timeout; // seconds without any message
|
|
cvar_t *sv_zombietime; // seconds to sink messages after disconnect
|
|
cvar_t *sv_rconPassword; // password for remote server commands
|
|
cvar_t *sv_privatePassword; // password for the privateClient slots
|
|
cvar_t *sv_allowDownload;
|
|
cvar_t *sv_maxclients;
|
|
|
|
cvar_t *sv_privateClients; // number of clients reserved for password
|
|
cvar_t *sv_hostname;
|
|
cvar_t *sv_master[MAX_MASTER_SERVERS]; // master server ip address
|
|
cvar_t *sv_reconnectlimit; // minimum seconds between connect messages
|
|
cvar_t *sv_showloss; // report when usercmds are lost
|
|
cvar_t *sv_padPackets; // add nop bytes to messages
|
|
cvar_t *sv_killserver; // menu system can set to 1 to shut server down
|
|
cvar_t *sv_mapname;
|
|
cvar_t *sv_mapChecksum;
|
|
cvar_t *sv_serverid;
|
|
cvar_t *sv_minRate;
|
|
cvar_t *sv_maxRate;
|
|
cvar_t *sv_dlRate;
|
|
cvar_t *sv_minPing;
|
|
cvar_t *sv_maxPing;
|
|
cvar_t *sv_gametype;
|
|
cvar_t *sv_pure;
|
|
cvar_t *sv_floodProtect;
|
|
cvar_t *sv_lanForceRate; // dedicated 1 (LAN) server forces local client rates to 99999 (bug #491)
|
|
#ifndef STANDALONE
|
|
cvar_t *sv_strictAuth;
|
|
#endif
|
|
cvar_t *sv_banFile;
|
|
|
|
serverBan_t serverBans[SERVER_MAXBANS];
|
|
int serverBansCount = 0;
|
|
|
|
/*
|
|
=============================================================================
|
|
|
|
EVENT MESSAGES
|
|
|
|
=============================================================================
|
|
*/
|
|
|
|
/*
|
|
===============
|
|
SV_ExpandNewlines
|
|
|
|
Converts newlines to "\n" so a line prints nicer
|
|
===============
|
|
*/
|
|
static char *SV_ExpandNewlines( char *in ) {
|
|
static char string[1024];
|
|
int l;
|
|
|
|
l = 0;
|
|
while ( *in && l < sizeof(string) - 3 ) {
|
|
if ( *in == '\n' ) {
|
|
string[l++] = '\\';
|
|
string[l++] = 'n';
|
|
} else {
|
|
string[l++] = *in;
|
|
}
|
|
in++;
|
|
}
|
|
string[l] = 0;
|
|
|
|
return string;
|
|
}
|
|
|
|
/*
|
|
======================
|
|
SV_ReplacePendingServerCommands
|
|
|
|
FIXME: This is ugly
|
|
======================
|
|
*/
|
|
#if 0 // unused
|
|
static int SV_ReplacePendingServerCommands( client_t *client, const char *cmd ) {
|
|
int i, index, csnum1, csnum2;
|
|
|
|
for ( i = client->reliableSent+1; i <= client->reliableSequence; i++ ) {
|
|
index = i & ( MAX_RELIABLE_COMMANDS - 1 );
|
|
//
|
|
if ( !Q_strncmp(cmd, client->reliableCommands[ index ], strlen("cs")) ) {
|
|
sscanf(cmd, "cs %i", &csnum1);
|
|
sscanf(client->reliableCommands[ index ], "cs %i", &csnum2);
|
|
if ( csnum1 == csnum2 ) {
|
|
Q_strncpyz( client->reliableCommands[ index ], cmd, sizeof( client->reliableCommands[ index ] ) );
|
|
/*
|
|
if ( client->netchan.remoteAddress.type != NA_BOT ) {
|
|
Com_Printf( "WARNING: client %i removed double pending config string %i: %s\n", client-svs.clients, csnum1, cmd );
|
|
}
|
|
*/
|
|
return qtrue;
|
|
}
|
|
}
|
|
}
|
|
return qfalse;
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
======================
|
|
SV_AddServerCommand
|
|
|
|
The given command will be transmitted to the client, and is guaranteed to
|
|
not have future snapshot_t executed before it is executed
|
|
======================
|
|
*/
|
|
void SV_AddServerCommand( client_t *client, const char *cmd ) {
|
|
int index, i;
|
|
|
|
// this is very ugly but it's also a waste to for instance send multiple config string updates
|
|
// for the same config string index in one snapshot
|
|
// if ( SV_ReplacePendingServerCommands( client, cmd ) ) {
|
|
// return;
|
|
// }
|
|
|
|
// do not send commands until the gamestate has been sent
|
|
if( client->state < CS_PRIMED )
|
|
return;
|
|
|
|
client->reliableSequence++;
|
|
// if we would be losing an old command that hasn't been acknowledged,
|
|
// we must drop the connection
|
|
// we check == instead of >= so a broadcast print added by SV_DropClient()
|
|
// doesn't cause a recursive drop client
|
|
if ( client->reliableSequence - client->reliableAcknowledge == MAX_RELIABLE_COMMANDS + 1 ) {
|
|
Com_Printf( "===== pending server commands =====\n" );
|
|
for ( i = client->reliableAcknowledge + 1 ; i <= client->reliableSequence ; i++ ) {
|
|
Com_Printf( "cmd %5d: %s\n", i, client->reliableCommands[ i & (MAX_RELIABLE_COMMANDS-1) ] );
|
|
}
|
|
Com_Printf( "cmd %5d: %s\n", i, cmd );
|
|
SV_DropClient( client, "Server command overflow" );
|
|
return;
|
|
}
|
|
index = client->reliableSequence & ( MAX_RELIABLE_COMMANDS - 1 );
|
|
Q_strncpyz( client->reliableCommands[ index ], cmd, sizeof( client->reliableCommands[ index ] ) );
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
SV_SendServerCommand
|
|
|
|
Sends a reliable command string to be interpreted by
|
|
the client game module: "cp", "print", "chat", etc
|
|
A NULL client will broadcast to all clients
|
|
=================
|
|
*/
|
|
void QDECL SV_SendServerCommand(client_t *cl, const char *fmt, ...) {
|
|
va_list argptr;
|
|
byte message[MAX_MSGLEN];
|
|
client_t *client;
|
|
int j;
|
|
|
|
va_start (argptr,fmt);
|
|
Q_vsnprintf ((char *)message, sizeof(message), fmt,argptr);
|
|
va_end (argptr);
|
|
|
|
// Fix to http://aluigi.altervista.org/adv/q3msgboom-adv.txt
|
|
// The actual cause of the bug is probably further downstream
|
|
// and should maybe be addressed later, but this certainly
|
|
// fixes the problem for now
|
|
if ( strlen ((char *)message) > 1022 ) {
|
|
return;
|
|
}
|
|
|
|
if ( cl != NULL ) {
|
|
SV_AddServerCommand( cl, (char *)message );
|
|
return;
|
|
}
|
|
|
|
// hack to echo broadcast prints to console
|
|
if ( com_dedicated->integer && !strncmp( (char *)message, "print", 5) ) {
|
|
Com_Printf ("broadcast: %s\n", SV_ExpandNewlines((char *)message) );
|
|
}
|
|
|
|
// send the data to all relevent clients
|
|
for (j = 0, client = svs.clients; j < sv_maxclients->integer ; j++, client++) {
|
|
SV_AddServerCommand( client, (char *)message );
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
==============================================================================
|
|
|
|
MASTER SERVER FUNCTIONS
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
/*
|
|
================
|
|
SV_MasterHeartbeat
|
|
|
|
Send a message to the masters every few minutes to
|
|
let it know we are alive, and log information.
|
|
We will also have a heartbeat sent when a server
|
|
changes from empty to non-empty, and full to non-full,
|
|
but not on every player enter or exit.
|
|
================
|
|
*/
|
|
#define HEARTBEAT_MSEC 300*1000
|
|
void SV_MasterHeartbeat(const char *message)
|
|
{
|
|
static netadr_t adr[MAX_MASTER_SERVERS][2]; // [2] for v4 and v6 address for the same address string.
|
|
int i;
|
|
int res;
|
|
int netenabled;
|
|
|
|
netenabled = Cvar_VariableIntegerValue("net_enabled");
|
|
|
|
// "dedicated 1" is for lan play, "dedicated 2" is for inet public play
|
|
if (!com_dedicated || com_dedicated->integer != 2 || !(netenabled & (NET_ENABLEV4 | NET_ENABLEV6)))
|
|
return; // only dedicated servers send heartbeats
|
|
|
|
// if not time yet, don't send anything
|
|
if ( svs.time < svs.nextHeartbeatTime )
|
|
return;
|
|
|
|
svs.nextHeartbeatTime = svs.time + HEARTBEAT_MSEC;
|
|
|
|
// send to group masters
|
|
for (i = 0; i < MAX_MASTER_SERVERS; i++)
|
|
{
|
|
if(!sv_master[i]->string[0])
|
|
continue;
|
|
|
|
// see if we haven't already resolved the name
|
|
// resolving usually causes hitches on win95, so only
|
|
// do it when needed
|
|
if(sv_master[i]->modified || (adr[i][0].type == NA_BAD && adr[i][1].type == NA_BAD))
|
|
{
|
|
sv_master[i]->modified = qfalse;
|
|
|
|
if(netenabled & NET_ENABLEV4)
|
|
{
|
|
Com_Printf("Resolving %s (IPv4)\n", sv_master[i]->string);
|
|
res = NET_StringToAdr(sv_master[i]->string, &adr[i][0], NA_IP);
|
|
|
|
if(res == 2)
|
|
{
|
|
// if no port was specified, use the default master port
|
|
adr[i][0].port = BigShort(PORT_MASTER);
|
|
}
|
|
|
|
if(res)
|
|
Com_Printf( "%s resolved to %s\n", sv_master[i]->string, NET_AdrToStringwPort(adr[i][0]));
|
|
else
|
|
Com_Printf( "%s has no IPv4 address.\n", sv_master[i]->string);
|
|
}
|
|
|
|
if(netenabled & NET_ENABLEV6)
|
|
{
|
|
Com_Printf("Resolving %s (IPv6)\n", sv_master[i]->string);
|
|
res = NET_StringToAdr(sv_master[i]->string, &adr[i][1], NA_IP6);
|
|
|
|
if(res == 2)
|
|
{
|
|
// if no port was specified, use the default master port
|
|
adr[i][1].port = BigShort(PORT_MASTER);
|
|
}
|
|
|
|
if(res)
|
|
Com_Printf( "%s resolved to %s\n", sv_master[i]->string, NET_AdrToStringwPort(adr[i][1]));
|
|
else
|
|
Com_Printf( "%s has no IPv6 address.\n", sv_master[i]->string);
|
|
}
|
|
|
|
if(adr[i][0].type == NA_BAD && adr[i][1].type == NA_BAD)
|
|
{
|
|
// if the address failed to resolve, clear it
|
|
// so we don't take repeated dns hits
|
|
Com_Printf("Couldn't resolve address: %s\n", sv_master[i]->string);
|
|
Cvar_Set(sv_master[i]->name, "");
|
|
sv_master[i]->modified = qfalse;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
|
|
Com_Printf ("Sending heartbeat to %s\n", sv_master[i]->string );
|
|
|
|
// this command should be changed if the server info / status format
|
|
// ever incompatably changes
|
|
|
|
if(adr[i][0].type != NA_BAD)
|
|
NET_OutOfBandPrint( NS_SERVER, adr[i][0], "heartbeat %s\n", message);
|
|
if(adr[i][1].type != NA_BAD)
|
|
NET_OutOfBandPrint( NS_SERVER, adr[i][1], "heartbeat %s\n", message);
|
|
}
|
|
}
|
|
|
|
/*
|
|
=================
|
|
SV_MasterShutdown
|
|
|
|
Informs all masters that this server is going down
|
|
=================
|
|
*/
|
|
void SV_MasterShutdown( void ) {
|
|
// send a heartbeat right now
|
|
svs.nextHeartbeatTime = -9999;
|
|
SV_MasterHeartbeat(HEARTBEAT_FOR_MASTER);
|
|
|
|
// send it again to minimize chance of drops
|
|
svs.nextHeartbeatTime = -9999;
|
|
SV_MasterHeartbeat(HEARTBEAT_FOR_MASTER);
|
|
|
|
// when the master tries to poll the server, it won't respond, so
|
|
// it will be removed from the list
|
|
}
|
|
|
|
|
|
/*
|
|
==============================================================================
|
|
|
|
CONNECTIONLESS COMMANDS
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
typedef struct leakyBucket_s leakyBucket_t;
|
|
struct leakyBucket_s {
|
|
netadrtype_t type;
|
|
|
|
union {
|
|
byte _4[4];
|
|
byte _6[16];
|
|
} ipv;
|
|
|
|
int lastTime;
|
|
signed char burst;
|
|
|
|
long hash;
|
|
|
|
leakyBucket_t *prev, *next;
|
|
};
|
|
|
|
// This is deliberately quite large to make it more of an effort to DoS
|
|
#define MAX_BUCKETS 16384
|
|
#define MAX_HASHES 1024
|
|
|
|
static leakyBucket_t buckets[ MAX_BUCKETS ];
|
|
static leakyBucket_t *bucketHashes[ MAX_HASHES ];
|
|
static leakyBucket_t outboundLeakyBucket;
|
|
|
|
/*
|
|
================
|
|
SVC_HashForAddress
|
|
================
|
|
*/
|
|
static long SVC_HashForAddress( netadr_t address ) {
|
|
byte *ip = NULL;
|
|
size_t size = 0;
|
|
int i;
|
|
long hash = 0;
|
|
|
|
switch ( address.type ) {
|
|
case NA_IP: ip = address.ip; size = 4; break;
|
|
case NA_IP6: ip = address.ip6; size = 16; break;
|
|
default: break;
|
|
}
|
|
|
|
for ( i = 0; i < size; i++ ) {
|
|
hash += (long)( ip[ i ] ) * ( i + 119 );
|
|
}
|
|
|
|
hash = ( hash ^ ( hash >> 10 ) ^ ( hash >> 20 ) );
|
|
hash &= ( MAX_HASHES - 1 );
|
|
|
|
return hash;
|
|
}
|
|
|
|
/*
|
|
================
|
|
SVC_BucketForAddress
|
|
|
|
Find or allocate a bucket for an address
|
|
================
|
|
*/
|
|
static leakyBucket_t *SVC_BucketForAddress( netadr_t address, int burst, int period ) {
|
|
leakyBucket_t *bucket = NULL;
|
|
int i;
|
|
long hash = SVC_HashForAddress( address );
|
|
int now = Sys_Milliseconds();
|
|
|
|
for ( bucket = bucketHashes[ hash ]; bucket; bucket = bucket->next ) {
|
|
switch ( bucket->type ) {
|
|
case NA_IP:
|
|
if ( memcmp( bucket->ipv._4, address.ip, 4 ) == 0 ) {
|
|
return bucket;
|
|
}
|
|
break;
|
|
|
|
case NA_IP6:
|
|
if ( memcmp( bucket->ipv._6, address.ip6, 16 ) == 0 ) {
|
|
return bucket;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
for ( i = 0; i < MAX_BUCKETS; i++ ) {
|
|
int interval;
|
|
|
|
bucket = &buckets[ i ];
|
|
interval = now - bucket->lastTime;
|
|
|
|
// Reclaim expired buckets
|
|
if ( bucket->lastTime > 0 && ( interval > ( burst * period ) ||
|
|
interval < 0 ) ) {
|
|
if ( bucket->prev != NULL ) {
|
|
bucket->prev->next = bucket->next;
|
|
} else {
|
|
bucketHashes[ bucket->hash ] = bucket->next;
|
|
}
|
|
|
|
if ( bucket->next != NULL ) {
|
|
bucket->next->prev = bucket->prev;
|
|
}
|
|
|
|
Com_Memset( bucket, 0, sizeof( leakyBucket_t ) );
|
|
}
|
|
|
|
if ( bucket->type == NA_BAD ) {
|
|
bucket->type = address.type;
|
|
switch ( address.type ) {
|
|
case NA_IP: Com_Memcpy( bucket->ipv._4, address.ip, 4 ); break;
|
|
case NA_IP6: Com_Memcpy( bucket->ipv._6, address.ip6, 16 ); break;
|
|
default: break;
|
|
}
|
|
|
|
bucket->lastTime = now;
|
|
bucket->burst = 0;
|
|
bucket->hash = hash;
|
|
|
|
// Add to the head of the relevant hash chain
|
|
bucket->next = bucketHashes[ hash ];
|
|
if ( bucketHashes[ hash ] != NULL ) {
|
|
bucketHashes[ hash ]->prev = bucket;
|
|
}
|
|
|
|
bucket->prev = NULL;
|
|
bucketHashes[ hash ] = bucket;
|
|
|
|
return bucket;
|
|
}
|
|
}
|
|
|
|
// Couldn't allocate a bucket for this address
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
================
|
|
SVC_RateLimit
|
|
================
|
|
*/
|
|
static qboolean SVC_RateLimit( leakyBucket_t *bucket, int burst, int period ) {
|
|
if ( bucket != NULL ) {
|
|
int now = Sys_Milliseconds();
|
|
int interval = now - bucket->lastTime;
|
|
int expired = interval / period;
|
|
int expiredRemainder = interval % period;
|
|
|
|
if ( expired > bucket->burst ) {
|
|
bucket->burst = 0;
|
|
bucket->lastTime = now;
|
|
} else {
|
|
bucket->burst -= expired;
|
|
bucket->lastTime = now - expiredRemainder;
|
|
}
|
|
|
|
if ( bucket->burst < burst ) {
|
|
bucket->burst++;
|
|
|
|
return qfalse;
|
|
}
|
|
}
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
================
|
|
SVC_RateLimitAddress
|
|
|
|
Rate limit for a particular address
|
|
================
|
|
*/
|
|
static qboolean SVC_RateLimitAddress( netadr_t from, int burst, int period ) {
|
|
leakyBucket_t *bucket = SVC_BucketForAddress( from, burst, period );
|
|
|
|
return SVC_RateLimit( bucket, burst, period );
|
|
}
|
|
|
|
/*
|
|
================
|
|
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.
|
|
================
|
|
*/
|
|
static void SVC_Status( netadr_t from ) {
|
|
char player[1024];
|
|
char status[MAX_MSGLEN];
|
|
int i;
|
|
client_t *cl;
|
|
playerState_t *ps;
|
|
int statusLength;
|
|
int playerLength;
|
|
char infostring[MAX_INFO_STRING];
|
|
|
|
// ignore if we are in single player
|
|
if ( Cvar_VariableValue( "g_gametype" ) == GT_SINGLE_PLAYER || Cvar_VariableValue("ui_singlePlayerActive")) {
|
|
return;
|
|
}
|
|
|
|
// Prevent using getstatus as an amplifier
|
|
if ( SVC_RateLimitAddress( from, 10, 1000 ) ) {
|
|
Com_DPrintf( "SVC_Status: rate limit from %s exceeded, dropping request\n",
|
|
NET_AdrToString( from ) );
|
|
return;
|
|
}
|
|
|
|
// Allow getstatus to be DoSed relatively easily, but prevent
|
|
// excess outbound bandwidth usage when being flooded inbound
|
|
if ( SVC_RateLimit( &outboundLeakyBucket, 10, 100 ) ) {
|
|
Com_DPrintf( "SVC_Status: rate limit exceeded, dropping request\n" );
|
|
return;
|
|
}
|
|
|
|
// A maximum challenge length of 128 should be more than plenty.
|
|
if(strlen(Cmd_Argv(1)) > 128)
|
|
return;
|
|
|
|
strcpy( infostring, Cvar_InfoString( CVAR_SERVERINFO ) );
|
|
|
|
// 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", Cmd_Argv(1) );
|
|
|
|
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 >= 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 );
|
|
}
|
|
|
|
/*
|
|
================
|
|
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 ) {
|
|
int i, count, humans;
|
|
char *gamedir;
|
|
char infostring[MAX_INFO_STRING];
|
|
|
|
// ignore if we are in single player
|
|
if ( Cvar_VariableValue( "g_gametype" ) == GT_SINGLE_PLAYER || Cvar_VariableValue("ui_singlePlayerActive")) {
|
|
return;
|
|
}
|
|
|
|
// Prevent using getinfo as an amplifier
|
|
if ( SVC_RateLimitAddress( from, 10, 1000 ) ) {
|
|
Com_DPrintf( "SVC_Info: rate limit from %s exceeded, dropping request\n",
|
|
NET_AdrToString( from ) );
|
|
return;
|
|
}
|
|
|
|
// Allow getinfo to be DoSed relatively easily, but prevent
|
|
// excess outbound bandwidth usage when being flooded inbound
|
|
if ( SVC_RateLimit( &outboundLeakyBucket, 10, 100 ) ) {
|
|
Com_DPrintf( "SVC_Info: rate limit exceeded, dropping request\n" );
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* 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(Cmd_Argv(1)) > 128)
|
|
return;
|
|
|
|
// don't count privateclients
|
|
count = humans = 0;
|
|
for ( i = sv_privateClients->integer ; i < sv_maxclients->integer ; i++ ) {
|
|
if ( svs.clients[i].state >= CS_CONNECTED ) {
|
|
count++;
|
|
if (svs.clients[i].netchan.remoteAddress.type != NA_BOT) {
|
|
humans++;
|
|
}
|
|
}
|
|
}
|
|
|
|
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", Cmd_Argv(1) );
|
|
|
|
Info_SetValueForKey( infostring, "gamename", com_gamename->string );
|
|
|
|
#ifdef LEGACY_PROTOCOL
|
|
if(com_legacyprotocol->integer > 0)
|
|
Info_SetValueForKey(infostring, "protocol", va("%i", com_legacyprotocol->integer));
|
|
else
|
|
#endif
|
|
Info_SetValueForKey(infostring, "protocol", va("%i", com_protocol->integer));
|
|
|
|
Info_SetValueForKey( infostring, "hostname", sv_hostname->string );
|
|
Info_SetValueForKey( infostring, "mapname", sv_mapname->string );
|
|
Info_SetValueForKey( infostring, "clients", va("%i", count) );
|
|
Info_SetValueForKey(infostring, "g_humanplayers", va("%i", humans));
|
|
Info_SetValueForKey( infostring, "sv_maxclients",
|
|
va("%i", sv_maxclients->integer - sv_privateClients->integer ) );
|
|
Info_SetValueForKey( infostring, "gametype", va("%i", sv_gametype->integer ) );
|
|
Info_SetValueForKey( infostring, "pure", va("%i", sv_pure->integer ) );
|
|
Info_SetValueForKey(infostring, "g_needpass", va("%d", Cvar_VariableIntegerValue("g_needpass")));
|
|
|
|
#ifdef USE_VOIP
|
|
if (sv_voip->integer) {
|
|
Info_SetValueForKey( infostring, "voip", va("%i", sv_voip->integer ) );
|
|
}
|
|
#endif
|
|
|
|
if( sv_minPing->integer ) {
|
|
Info_SetValueForKey( infostring, "minPing", va("%i", sv_minPing->integer) );
|
|
}
|
|
if( sv_maxPing->integer ) {
|
|
Info_SetValueForKey( infostring, "maxPing", va("%i", sv_maxPing->integer) );
|
|
}
|
|
gamedir = Cvar_VariableString( "fs_game" );
|
|
if( *gamedir ) {
|
|
Info_SetValueForKey( infostring, "game", gamedir );
|
|
}
|
|
|
|
NET_OutOfBandPrint( NS_SERVER, from, "infoResponse\n%s", infostring );
|
|
}
|
|
|
|
/*
|
|
================
|
|
SVC_FlushRedirect
|
|
|
|
================
|
|
*/
|
|
static void SV_FlushRedirect( char *outputbuf ) {
|
|
NET_OutOfBandPrint( NS_SERVER, svs.redirectAddress, "print\n%s", outputbuf );
|
|
}
|
|
|
|
/*
|
|
===============
|
|
SVC_RemoteCommand
|
|
|
|
An rcon packet arrived from the network.
|
|
Shift down the remaining args
|
|
Redirect all printfs
|
|
===============
|
|
*/
|
|
static void SVC_RemoteCommand( netadr_t from, msg_t *msg ) {
|
|
qboolean valid;
|
|
char remaining[1024];
|
|
// TTimo - scaled down to accumulate, but not overflow anything network wise, print wise etc.
|
|
// (OOB messages are the bottleneck here)
|
|
#define SV_OUTPUTBUF_LENGTH (1024 - 16)
|
|
char sv_outputbuf[SV_OUTPUTBUF_LENGTH];
|
|
char *cmd_aux;
|
|
|
|
// Prevent using rcon as an amplifier and make dictionary attacks impractical
|
|
if ( SVC_RateLimitAddress( from, 10, 1000 ) ) {
|
|
Com_DPrintf( "SVC_RemoteCommand: rate limit from %s exceeded, dropping request\n",
|
|
NET_AdrToString( from ) );
|
|
return;
|
|
}
|
|
|
|
if ( !strlen( sv_rconPassword->string ) ||
|
|
strcmp (Cmd_Argv(1), sv_rconPassword->string) ) {
|
|
static leakyBucket_t bucket;
|
|
|
|
// Make DoS via rcon impractical
|
|
if ( SVC_RateLimit( &bucket, 10, 1000 ) ) {
|
|
Com_DPrintf( "SVC_RemoteCommand: rate limit exceeded, dropping request\n" );
|
|
return;
|
|
}
|
|
|
|
valid = qfalse;
|
|
Com_Printf ("Bad rcon from %s: %s\n", NET_AdrToString (from), Cmd_ArgsFrom(2) );
|
|
} else {
|
|
valid = qtrue;
|
|
Com_Printf ("Rcon from %s: %s\n", NET_AdrToString (from), Cmd_ArgsFrom(2) );
|
|
}
|
|
|
|
// start redirecting all print outputs to the packet
|
|
svs.redirectAddress = from;
|
|
Com_BeginRedirect (sv_outputbuf, SV_OUTPUTBUF_LENGTH, SV_FlushRedirect);
|
|
|
|
if ( !strlen( sv_rconPassword->string ) ) {
|
|
Com_Printf ("No rconpassword set on the server.\n");
|
|
} else if ( !valid ) {
|
|
Com_Printf ("Bad rconpassword.\n");
|
|
} else {
|
|
remaining[0] = 0;
|
|
|
|
// https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=543
|
|
// get the command directly, "rcon <pass> <command>" to avoid quoting issues
|
|
// extract the command by walking
|
|
// since the cmd formatting can fuckup (amount of spaces), using a dumb step by step parsing
|
|
cmd_aux = Cmd_Cmd();
|
|
cmd_aux+=4;
|
|
while(cmd_aux[0]==' ')
|
|
cmd_aux++;
|
|
while(cmd_aux[0] && cmd_aux[0]!=' ') // password
|
|
cmd_aux++;
|
|
while(cmd_aux[0]==' ')
|
|
cmd_aux++;
|
|
|
|
Q_strcat( remaining, sizeof(remaining), cmd_aux);
|
|
|
|
Cmd_ExecuteString (remaining);
|
|
|
|
}
|
|
|
|
Com_EndRedirect ();
|
|
}
|
|
|
|
/*
|
|
=================
|
|
SV_ConnectionlessPacket
|
|
|
|
A connectionless packet has four leading 0xff
|
|
characters to distinguish it from a game channel.
|
|
Clients that are in the game can still send
|
|
connectionless packets.
|
|
=================
|
|
*/
|
|
static void SV_ConnectionlessPacket( netadr_t from, msg_t *msg ) {
|
|
char *s;
|
|
char *c;
|
|
|
|
MSG_BeginReadingOOB( msg );
|
|
MSG_ReadLong( msg ); // skip the -1 marker
|
|
|
|
if (!Q_strncmp("connect", (char *) &msg->data[4], 7)) {
|
|
Huff_Decompress(msg, 12);
|
|
}
|
|
|
|
s = MSG_ReadStringLine( msg );
|
|
Cmd_TokenizeString( s );
|
|
|
|
c = Cmd_Argv(0);
|
|
Com_DPrintf ("SV packet %s : %s\n", NET_AdrToString(from), c);
|
|
|
|
if (!Q_stricmp(c, "getstatus")) {
|
|
SVC_Status( from );
|
|
} else if (!Q_stricmp(c, "getinfo")) {
|
|
SVC_Info( from );
|
|
} else if (!Q_stricmp(c, "getchallenge")) {
|
|
SV_GetChallenge(from);
|
|
} else if (!Q_stricmp(c, "connect")) {
|
|
SV_DirectConnect( from );
|
|
#ifndef STANDALONE
|
|
} else if (!Q_stricmp(c, "ipAuthorize")) {
|
|
SV_AuthorizeIpPacket( from );
|
|
#endif
|
|
} else if (!Q_stricmp(c, "rcon")) {
|
|
SVC_RemoteCommand( from, msg );
|
|
} else if (!Q_stricmp(c, "disconnect")) {
|
|
// if a client starts up a local server, we may see some spurious
|
|
// server disconnect messages when their new server sees our final
|
|
// sequenced messages to the old client
|
|
} else {
|
|
Com_DPrintf ("bad connectionless packet from %s:\n%s\n",
|
|
NET_AdrToString (from), s);
|
|
}
|
|
}
|
|
|
|
//============================================================================
|
|
|
|
/*
|
|
=================
|
|
SV_PacketEvent
|
|
=================
|
|
*/
|
|
void SV_PacketEvent( netadr_t from, msg_t *msg ) {
|
|
int i;
|
|
client_t *cl;
|
|
int qport;
|
|
|
|
// check for connectionless packet (0xffffffff) first
|
|
if ( msg->cursize >= 4 && *(int *)msg->data == -1) {
|
|
SV_ConnectionlessPacket( from, msg );
|
|
return;
|
|
}
|
|
|
|
// read the qport out of the message so we can fix up
|
|
// stupid address translating routers
|
|
MSG_BeginReadingOOB( msg );
|
|
MSG_ReadLong( msg ); // sequence number
|
|
qport = MSG_ReadShort( msg ) & 0xffff;
|
|
|
|
// find which client the message is from
|
|
for (i=0, cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++) {
|
|
if (cl->state == CS_FREE) {
|
|
continue;
|
|
}
|
|
if ( !NET_CompareBaseAdr( from, cl->netchan.remoteAddress ) ) {
|
|
continue;
|
|
}
|
|
// it is possible to have multiple clients from a single IP
|
|
// address, so they are differentiated by the qport variable
|
|
if (cl->netchan.qport != qport) {
|
|
continue;
|
|
}
|
|
|
|
// the IP port can't be used to differentiate them, because
|
|
// some address translating routers periodically change UDP
|
|
// port assignments
|
|
if (cl->netchan.remoteAddress.port != from.port) {
|
|
Com_Printf( "SV_PacketEvent: fixing up a translated port\n" );
|
|
cl->netchan.remoteAddress.port = from.port;
|
|
}
|
|
|
|
// make sure it is a valid, in sequence packet
|
|
if (SV_Netchan_Process(cl, msg)) {
|
|
// zombie clients still need to do the Netchan_Process
|
|
// to make sure they don't need to retransmit the final
|
|
// reliable message, but they don't do any other processing
|
|
if (cl->state != CS_ZOMBIE) {
|
|
cl->lastPacketTime = svs.time; // don't timeout
|
|
SV_ExecuteClientMessage( cl, msg );
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
===================
|
|
SV_CalcPings
|
|
|
|
Updates the cl->ping variables
|
|
===================
|
|
*/
|
|
static void SV_CalcPings( void ) {
|
|
int i, j;
|
|
client_t *cl;
|
|
int total, count;
|
|
int delta;
|
|
playerState_t *ps;
|
|
|
|
for (i=0 ; i < sv_maxclients->integer ; i++) {
|
|
cl = &svs.clients[i];
|
|
if ( cl->state != CS_ACTIVE ) {
|
|
cl->ping = 999;
|
|
continue;
|
|
}
|
|
if ( !cl->gentity ) {
|
|
cl->ping = 999;
|
|
continue;
|
|
}
|
|
if ( cl->gentity->r.svFlags & SVF_BOT ) {
|
|
cl->ping = 0;
|
|
continue;
|
|
}
|
|
|
|
total = 0;
|
|
count = 0;
|
|
for ( j = 0 ; j < PACKET_BACKUP ; j++ ) {
|
|
if ( cl->frames[j].messageAcked <= 0 ) {
|
|
continue;
|
|
}
|
|
delta = cl->frames[j].messageAcked - cl->frames[j].messageSent;
|
|
count++;
|
|
total += delta;
|
|
}
|
|
if (!count) {
|
|
cl->ping = 999;
|
|
} else {
|
|
cl->ping = total/count;
|
|
if ( cl->ping > 999 ) {
|
|
cl->ping = 999;
|
|
}
|
|
}
|
|
|
|
// let the game dll know about the ping
|
|
ps = SV_GameClientNum( i );
|
|
ps->ping = cl->ping;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_CheckTimeouts
|
|
|
|
If a packet has not been received from a client for timeout->integer
|
|
seconds, drop the conneciton. Server time is used instead of
|
|
realtime to avoid dropping the local client while debugging.
|
|
|
|
When a client is normally dropped, the client_t goes into a zombie state
|
|
for a few seconds to make sure any final reliable message gets resent
|
|
if necessary
|
|
==================
|
|
*/
|
|
static void SV_CheckTimeouts( void ) {
|
|
int i;
|
|
client_t *cl;
|
|
int droppoint;
|
|
int zombiepoint;
|
|
|
|
droppoint = svs.time - 1000 * sv_timeout->integer;
|
|
zombiepoint = svs.time - 1000 * sv_zombietime->integer;
|
|
|
|
for (i=0,cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++) {
|
|
// message times may be wrong across a changelevel
|
|
if (cl->lastPacketTime > svs.time) {
|
|
cl->lastPacketTime = svs.time;
|
|
}
|
|
|
|
if (cl->state == CS_ZOMBIE
|
|
&& cl->lastPacketTime < zombiepoint) {
|
|
// using the client id cause the cl->name is empty at this point
|
|
Com_DPrintf( "Going from CS_ZOMBIE to CS_FREE for client %d\n", i );
|
|
cl->state = CS_FREE; // can now be reused
|
|
continue;
|
|
}
|
|
if ( cl->state >= CS_CONNECTED && cl->lastPacketTime < droppoint) {
|
|
// wait several frames so a debugger session doesn't
|
|
// cause a timeout
|
|
if ( ++cl->timeoutCount > 5 ) {
|
|
SV_DropClient (cl, "timed out");
|
|
cl->state = CS_FREE; // don't bother with zombie state
|
|
}
|
|
} else {
|
|
cl->timeoutCount = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
==================
|
|
SV_CheckPaused
|
|
==================
|
|
*/
|
|
static qboolean SV_CheckPaused( void ) {
|
|
int count;
|
|
client_t *cl;
|
|
int i;
|
|
|
|
if ( !cl_paused->integer ) {
|
|
return qfalse;
|
|
}
|
|
|
|
// only pause if there is just a single client connected
|
|
count = 0;
|
|
for (i=0,cl=svs.clients ; i < sv_maxclients->integer ; i++,cl++) {
|
|
if ( cl->state >= CS_CONNECTED && cl->netchan.remoteAddress.type != NA_BOT ) {
|
|
count++;
|
|
}
|
|
}
|
|
|
|
if ( count > 1 ) {
|
|
// don't pause
|
|
if (sv_paused->integer)
|
|
Cvar_Set("sv_paused", "0");
|
|
return qfalse;
|
|
}
|
|
|
|
if (!sv_paused->integer)
|
|
Cvar_Set("sv_paused", "1");
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_FrameMsec
|
|
Return time in millseconds until processing of the next server frame.
|
|
==================
|
|
*/
|
|
int SV_FrameMsec()
|
|
{
|
|
if(sv_fps)
|
|
{
|
|
int frameMsec;
|
|
|
|
frameMsec = 1000.0f / sv_fps->value;
|
|
|
|
if(frameMsec < sv.timeResidual)
|
|
return 0;
|
|
else
|
|
return frameMsec - sv.timeResidual;
|
|
}
|
|
else
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_Frame
|
|
|
|
Player movement occurs as a result of packet events, which
|
|
happen before SV_Frame is called
|
|
==================
|
|
*/
|
|
void SV_Frame( int msec ) {
|
|
int frameMsec;
|
|
int startTime;
|
|
|
|
// the menu kills the server with this cvar
|
|
if ( sv_killserver->integer ) {
|
|
SV_Shutdown ("Server was killed");
|
|
Cvar_Set( "sv_killserver", "0" );
|
|
return;
|
|
}
|
|
|
|
if (!com_sv_running->integer)
|
|
{
|
|
// Running as a server, but no map loaded
|
|
#ifdef DEDICATED
|
|
// Block until something interesting happens
|
|
Sys_Sleep(-1);
|
|
#endif
|
|
|
|
return;
|
|
}
|
|
|
|
// allow pause if only the local client is connected
|
|
if ( SV_CheckPaused() ) {
|
|
return;
|
|
}
|
|
|
|
// if it isn't time for the next frame, do nothing
|
|
if ( sv_fps->integer < 1 ) {
|
|
Cvar_Set( "sv_fps", "10" );
|
|
}
|
|
|
|
frameMsec = 1000 / sv_fps->integer * com_timescale->value;
|
|
// don't let it scale below 1ms
|
|
if(frameMsec < 1)
|
|
{
|
|
Cvar_Set("timescale", va("%f", sv_fps->integer / 1000.0f));
|
|
frameMsec = 1;
|
|
}
|
|
|
|
sv.timeResidual += msec;
|
|
|
|
if (!com_dedicated->integer) SV_BotFrame (sv.time + sv.timeResidual);
|
|
|
|
// if time is about to hit the 32nd bit, kick all clients
|
|
// and clear sv.time, rather
|
|
// than checking for negative time wraparound everywhere.
|
|
// 2giga-milliseconds = 23 days, so it won't be too often
|
|
if ( svs.time > 0x70000000 ) {
|
|
SV_Shutdown( "Restarting server due to time wrapping" );
|
|
Cbuf_AddText( va( "map %s\n", Cvar_VariableString( "mapname" ) ) );
|
|
return;
|
|
}
|
|
// this can happen considerably earlier when lots of clients play and the map doesn't change
|
|
if ( svs.nextSnapshotEntities >= 0x7FFFFFFE - svs.numSnapshotEntities ) {
|
|
SV_Shutdown( "Restarting server due to numSnapshotEntities wrapping" );
|
|
Cbuf_AddText( va( "map %s\n", Cvar_VariableString( "mapname" ) ) );
|
|
return;
|
|
}
|
|
|
|
if( sv.restartTime && sv.time >= sv.restartTime ) {
|
|
sv.restartTime = 0;
|
|
Cbuf_AddText( "map_restart 0\n" );
|
|
return;
|
|
}
|
|
|
|
// update infostrings if anything has been changed
|
|
if ( cvar_modifiedFlags & CVAR_SERVERINFO ) {
|
|
SV_SetConfigstring( CS_SERVERINFO, Cvar_InfoString( CVAR_SERVERINFO ) );
|
|
cvar_modifiedFlags &= ~CVAR_SERVERINFO;
|
|
}
|
|
if ( cvar_modifiedFlags & CVAR_SYSTEMINFO ) {
|
|
SV_SetConfigstring( CS_SYSTEMINFO, Cvar_InfoString_Big( CVAR_SYSTEMINFO ) );
|
|
cvar_modifiedFlags &= ~CVAR_SYSTEMINFO;
|
|
}
|
|
|
|
if ( com_speeds->integer ) {
|
|
startTime = Sys_Milliseconds ();
|
|
} else {
|
|
startTime = 0; // quite a compiler warning
|
|
}
|
|
|
|
// update ping based on the all received frames
|
|
SV_CalcPings();
|
|
|
|
if (com_dedicated->integer) SV_BotFrame (sv.time);
|
|
|
|
// run the game simulation in chunks
|
|
while ( sv.timeResidual >= frameMsec ) {
|
|
sv.timeResidual -= frameMsec;
|
|
svs.time += frameMsec;
|
|
sv.time += frameMsec;
|
|
|
|
// let everything in the world think and move
|
|
VM_Call (gvm, GAME_RUN_FRAME, sv.time);
|
|
}
|
|
|
|
if ( com_speeds->integer ) {
|
|
time_game = Sys_Milliseconds () - startTime;
|
|
}
|
|
|
|
// check timeouts
|
|
SV_CheckTimeouts();
|
|
|
|
// send messages back to the clients
|
|
SV_SendClientMessages();
|
|
|
|
// send a heartbeat to the master if needed
|
|
SV_MasterHeartbeat(HEARTBEAT_FOR_MASTER);
|
|
}
|
|
|
|
/*
|
|
====================
|
|
SV_RateMsec
|
|
|
|
Return the number of msec until another message can be sent to
|
|
a client based on its rate settings
|
|
====================
|
|
*/
|
|
|
|
#define UDPIP_HEADER_SIZE 28
|
|
#define UDPIP6_HEADER_SIZE 48
|
|
|
|
int SV_RateMsec(client_t *client)
|
|
{
|
|
int rate, rateMsec;
|
|
int messageSize;
|
|
|
|
messageSize = client->netchan.lastSentSize;
|
|
rate = client->rate;
|
|
|
|
if(sv_maxRate->integer)
|
|
{
|
|
if(sv_maxRate->integer < 1000)
|
|
Cvar_Set( "sv_MaxRate", "1000" );
|
|
if(sv_maxRate->integer < rate)
|
|
rate = sv_maxRate->integer;
|
|
}
|
|
|
|
if(sv_minRate->integer)
|
|
{
|
|
if(sv_minRate->integer < 1000)
|
|
Cvar_Set("sv_minRate", "1000");
|
|
if(sv_minRate->integer > rate)
|
|
rate = sv_minRate->integer;
|
|
}
|
|
|
|
if(client->netchan.remoteAddress.type == NA_IP6)
|
|
messageSize += UDPIP6_HEADER_SIZE;
|
|
else
|
|
messageSize += UDPIP_HEADER_SIZE;
|
|
|
|
rateMsec = messageSize * 1000 / ((int) (rate * com_timescale->value));
|
|
rate = Sys_Milliseconds() - client->netchan.lastSentTime;
|
|
|
|
if(rate > rateMsec)
|
|
return 0;
|
|
else
|
|
return rateMsec - rate;
|
|
}
|
|
|
|
/*
|
|
====================
|
|
SV_SendQueuedPackets
|
|
|
|
Send download messages and queued packets in the time that we're idle, i.e.
|
|
not computing a server frame or sending client snapshots.
|
|
Return the time in msec until we expect to be called next
|
|
====================
|
|
*/
|
|
|
|
int SV_SendQueuedPackets()
|
|
{
|
|
int numBlocks;
|
|
int dlStart, deltaT, delayT;
|
|
static int dlNextRound = 0;
|
|
int timeVal = INT_MAX;
|
|
|
|
// Send out fragmented packets now that we're idle
|
|
delayT = SV_SendQueuedMessages();
|
|
if(delayT >= 0)
|
|
timeVal = delayT;
|
|
|
|
if(sv_dlRate->integer)
|
|
{
|
|
// Rate limiting. This is very imprecise for high
|
|
// download rates due to millisecond timedelta resolution
|
|
dlStart = Sys_Milliseconds();
|
|
deltaT = dlNextRound - dlStart;
|
|
|
|
if(deltaT > 0)
|
|
{
|
|
if(deltaT < timeVal)
|
|
timeVal = deltaT + 1;
|
|
}
|
|
else
|
|
{
|
|
numBlocks = SV_SendDownloadMessages();
|
|
|
|
if(numBlocks)
|
|
{
|
|
// There are active downloads
|
|
deltaT = Sys_Milliseconds() - dlStart;
|
|
|
|
delayT = 1000 * numBlocks * MAX_DOWNLOAD_BLKSIZE;
|
|
delayT /= sv_dlRate->integer * 1024;
|
|
|
|
if(delayT <= deltaT + 1)
|
|
{
|
|
// Sending the last round of download messages
|
|
// took too long for given rate, don't wait for
|
|
// next round, but always enforce a 1ms delay
|
|
// between DL message rounds so we don't hog
|
|
// all of the bandwidth. This will result in an
|
|
// effective maximum rate of 1MB/s per user, but the
|
|
// low download window size limits this anyways.
|
|
if(timeVal > 2)
|
|
timeVal = 2;
|
|
|
|
dlNextRound = dlStart + deltaT + 1;
|
|
}
|
|
else
|
|
{
|
|
dlNextRound = dlStart + delayT;
|
|
delayT -= deltaT;
|
|
|
|
if(delayT < timeVal)
|
|
timeVal = delayT;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(SV_SendDownloadMessages())
|
|
timeVal = 0;
|
|
}
|
|
|
|
return timeVal;
|
|
}
|