2013-04-22 05:25:59 +00:00
|
|
|
// leave this as first line for PCH reasons...
|
|
|
|
//
|
|
|
|
#include "../server/exe_headers.h"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#include "server.h"
|
|
|
|
/*
|
|
|
|
Ghoul2 Insert Start
|
|
|
|
*/
|
|
|
|
#if !defined (MINIHEAP_H_INC)
|
|
|
|
#include "../qcommon/miniheap.h"
|
|
|
|
#endif
|
|
|
|
/*
|
|
|
|
Ghoul2 Insert End
|
|
|
|
*/
|
|
|
|
|
|
|
|
serverStatic_t svs; // persistant server info
|
|
|
|
server_t sv; // local server
|
|
|
|
game_export_t *ge;
|
|
|
|
|
|
|
|
cvar_t *sv_fps; // 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_reconnectlimit; // minimum seconds between connect messages
|
|
|
|
cvar_t *sv_showloss; // report when usercmds are lost
|
|
|
|
cvar_t *sv_killserver; // menu system can set to 1 to shut server down
|
|
|
|
cvar_t *sv_mapname;
|
|
|
|
cvar_t *sv_spawntarget;
|
|
|
|
cvar_t *sv_mapChecksum;
|
|
|
|
cvar_t *sv_serverid;
|
|
|
|
cvar_t *sv_testsave; // Run the savegame enumeration every game frame
|
|
|
|
cvar_t *sv_compress_saved_games; // compress the saved games on the way out (only affect saver, loader can read both)
|
|
|
|
|
|
|
|
/*
|
|
|
|
=============================================================================
|
|
|
|
|
|
|
|
EVENT MESSAGES
|
|
|
|
|
|
|
|
=============================================================================
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
|
|
|
===============
|
|
|
|
SV_ExpandNewlines
|
|
|
|
|
|
|
|
Converts newlines to "\n" so a line prints nicer
|
|
|
|
===============
|
|
|
|
*/
|
|
|
|
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_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;
|
|
|
|
|
|
|
|
// if we would be losing an old command that hasn't been acknowledged,
|
|
|
|
// we must drop the connection
|
|
|
|
if ( client->reliableSequence - client->reliableAcknowledge > MAX_RELIABLE_COMMANDS ) {
|
|
|
|
SV_DropClient( client, "Server command overflow" );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
client->reliableSequence++;
|
|
|
|
index = client->reliableSequence & ( MAX_RELIABLE_COMMANDS - 1 );
|
|
|
|
if ( client->reliableCommands[ index ] ) {
|
|
|
|
Z_Free( client->reliableCommands[ index ] );
|
|
|
|
}
|
|
|
|
client->reliableCommands[ index ] = CopyString( cmd );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
=================
|
|
|
|
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 SV_SendServerCommand(client_t *cl, const char *fmt, ...) {
|
|
|
|
va_list argptr;
|
|
|
|
byte message[MAX_MSGLEN];
|
|
|
|
int len;
|
|
|
|
client_t *client;
|
|
|
|
int j;
|
|
|
|
|
|
|
|
message[0] = svc_serverCommand;
|
|
|
|
|
|
|
|
va_start (argptr,fmt);
|
|
|
|
vsprintf ((char *)message+1, fmt,argptr);
|
|
|
|
va_end (argptr);
|
|
|
|
len = strlen( (char *)message ) + 1;
|
|
|
|
|
|
|
|
if ( cl != NULL ) {
|
|
|
|
SV_AddServerCommand( cl, (char *)message );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// send the data to all relevent clients
|
|
|
|
for (j = 0, client = svs.clients; j < 1 ; j++, client++) {
|
|
|
|
if ( client->state < CS_PRIMED ) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
SV_AddServerCommand( client, (char *)message );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
==============================================================================
|
|
|
|
|
|
|
|
CONNECTIONLESS COMMANDS
|
|
|
|
|
|
|
|
==============================================================================
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
|
|
|
================
|
|
|
|
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 ) {
|
|
|
|
char player[1024];
|
|
|
|
char status[MAX_MSGLEN];
|
|
|
|
int i;
|
|
|
|
client_t *cl;
|
|
|
|
int statusLength;
|
|
|
|
int playerLength;
|
|
|
|
int score;
|
|
|
|
char infostring[MAX_INFO_STRING];
|
|
|
|
|
|
|
|
strcpy( infostring, Cvar_InfoString( CVAR_SERVERINFO ) );
|
|
|
|
|
|
|
|
// 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) );
|
|
|
|
|
|
|
|
status[0] = 0;
|
|
|
|
statusLength = 0;
|
|
|
|
|
|
|
|
for (i=0 ; i < 1 ; i++) {
|
|
|
|
cl = &svs.clients[i];
|
|
|
|
if ( cl->state >= CS_CONNECTED ) {
|
|
|
|
if ( cl->gentity && cl->gentity->client ) {
|
|
|
|
score = cl->gentity->client->persistant[PERS_SCORE];
|
|
|
|
} else {
|
|
|
|
score = 0;
|
|
|
|
}
|
|
|
|
Com_sprintf (player, sizeof(player), "%i %i \"%s\"\n",
|
|
|
|
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
|
|
|
|
================
|
|
|
|
*/
|
|
|
|
static void SVC_Info( netadr_t from ) {
|
|
|
|
int i, count;
|
|
|
|
char infostring[MAX_INFO_STRING];
|
|
|
|
|
|
|
|
count = 0;
|
|
|
|
for ( i = 0 ; i < 1 ; i++ ) {
|
|
|
|
if ( svs.clients[i].state >= CS_CONNECTED ) {
|
|
|
|
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", Cmd_Argv(1) );
|
|
|
|
|
|
|
|
Info_SetValueForKey( infostring, "protocol", va("%i", PROTOCOL_VERSION) );
|
|
|
|
//Info_SetValueForKey( infostring, "hostname", sv_hostname->string );
|
|
|
|
Info_SetValueForKey( infostring, "mapname", sv_mapname->string );
|
|
|
|
Info_SetValueForKey( infostring, "clients", va("%i", count) );
|
|
|
|
Info_SetValueForKey( infostring, "sv_maxclients", va("%i", 1) );
|
|
|
|
|
|
|
|
NET_OutOfBandPrint( NS_SERVER, from, "infoResponse\n%s", infostring );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
=================
|
|
|
|
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_BeginReading( msg );
|
|
|
|
MSG_ReadLong( msg ); // skip the -1 marker
|
|
|
|
|
|
|
|
s = MSG_ReadStringLine( msg );
|
|
|
|
|
|
|
|
Cmd_TokenizeString( s );
|
|
|
|
|
|
|
|
c = Cmd_Argv(0);
|
|
|
|
Com_DPrintf ("SV packet %s : %s\n", NET_AdrToString(from), c);
|
|
|
|
|
|
|
|
if (!strcmp(c,"getstatus")) {
|
|
|
|
SVC_Status( from );
|
|
|
|
} else if (!strcmp(c,"getinfo")) {
|
|
|
|
SVC_Info( from );
|
|
|
|
} else if (!strcmp(c,"connect")) {
|
|
|
|
SV_DirectConnect( from );
|
|
|
|
} else if (!strcmp(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_ReadPackets
|
|
|
|
=================
|
|
|
|
*/
|
|
|
|
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_BeginReading( msg );
|
|
|
|
MSG_ReadLong( msg ); // sequence number
|
|
|
|
MSG_ReadLong( msg ); // sequence number
|
|
|
|
qport = MSG_ReadShort( msg ) & 0xffff;
|
|
|
|
|
|
|
|
// find which client the message is from
|
|
|
|
for (i=0, cl=svs.clients ; i < 1 ; 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_ReadPackets: fixing up a translated port\n" );
|
|
|
|
cl->netchan.remoteAddress.port = from.port;
|
|
|
|
}
|
|
|
|
|
|
|
|
// make sure it is a valid, in sequence packet
|
|
|
|
if (Netchan_Process(&cl->netchan, msg)) {
|
|
|
|
// zombie clients stil neet 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 = sv.time; // don't timeout
|
|
|
|
cl->frames[ cl->netchan.incomingAcknowledged & PACKET_MASK ]
|
|
|
|
.messageAcked = sv.time;
|
|
|
|
SV_ExecuteClientMessage( cl, msg );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// if we received a sequenced packet from an address we don't reckognize,
|
|
|
|
// send an out of band disconnect packet to it
|
|
|
|
NET_OutOfBandPrint( NS_SERVER, from, "disconnect" );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
===================
|
|
|
|
SV_CalcPings
|
|
|
|
|
|
|
|
Updates the cl->ping variables
|
|
|
|
===================
|
|
|
|
*/
|
|
|
|
void SV_CalcPings (void) {
|
|
|
|
int i, j;
|
|
|
|
client_t *cl;
|
|
|
|
int total, count;
|
|
|
|
int delta;
|
|
|
|
|
|
|
|
for (i=0 ; i < 1 ; i++) {
|
|
|
|
cl = &svs.clients[i];
|
|
|
|
if ( cl->state != CS_ACTIVE ) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if ( cl->gentity->svFlags & SVF_BOT ) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
total = 0;
|
|
|
|
count = 0;
|
|
|
|
for ( j = 0 ; j < PACKET_BACKUP ; j++ ) {
|
|
|
|
delta = cl->frames[j].messageAcked - cl->frames[j].messageSent;
|
|
|
|
if ( delta >= 0 ) {
|
|
|
|
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
|
|
|
|
cl->gentity->client->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
|
|
|
|
==================
|
|
|
|
*/
|
|
|
|
void SV_CheckTimeouts( void ) {
|
|
|
|
int i;
|
|
|
|
client_t *cl;
|
|
|
|
int droppoint;
|
|
|
|
int zombiepoint;
|
|
|
|
|
|
|
|
droppoint = sv.time - 1000 * sv_timeout->integer;
|
|
|
|
zombiepoint = sv.time - 1000 * sv_zombietime->integer;
|
|
|
|
|
|
|
|
for (i=0,cl=svs.clients ; i < 1 ; i++,cl++) {
|
|
|
|
// message times may be wrong across a changelevel
|
|
|
|
if (cl->lastPacketTime > sv.time) {
|
|
|
|
cl->lastPacketTime = sv.time;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cl->state == CS_ZOMBIE
|
|
|
|
&& cl->lastPacketTime < zombiepoint) {
|
|
|
|
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
|
|
|
|
==================
|
|
|
|
*/
|
|
|
|
qboolean SV_CheckPaused( void ) {
|
|
|
|
if ( !cl_paused->integer ) {
|
|
|
|
return qfalse;
|
|
|
|
}
|
|
|
|
|
|
|
|
sv_paused->integer = 1;
|
|
|
|
return qtrue;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
==================
|
|
|
|
SV_Frame
|
|
|
|
|
|
|
|
Player movement occurs as a result of packet events, which
|
|
|
|
happen before SV_Frame is called
|
|
|
|
==================
|
|
|
|
*/
|
|
|
|
extern cvar_t *cl_newClock;
|
|
|
|
void SV_Frame( int msec,float fractionMsec ) {
|
|
|
|
int frameMsec;
|
|
|
|
int startTime=0;
|
|
|
|
|
|
|
|
// the menu kills the server with this cvar
|
|
|
|
if ( sv_killserver->integer ) {
|
|
|
|
SV_Shutdown ("Server was killed.\n");
|
|
|
|
Cvar_Set( "sv_killserver", "0" );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !com_sv_running->integer ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
extern void SP_CheckForLanguageUpdates(void);
|
|
|
|
SP_CheckForLanguageUpdates(); // will fast-return else load different language if menu changed it
|
|
|
|
|
|
|
|
// allow pause if only the local client is connected
|
|
|
|
if ( SV_CheckPaused() ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// go ahead and let time slip if the server really hitched badly
|
|
|
|
if ( msec > 1000 ) {
|
|
|
|
Com_DPrintf( "SV_Frame: Truncating msec of %i to 1000\n", msec );
|
|
|
|
msec = 1000;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 ;
|
|
|
|
|
|
|
|
sv.timeResidual += msec;
|
|
|
|
sv.timeResidualFraction+=fractionMsec;
|
|
|
|
if (sv.timeResidualFraction>=1.0f)
|
|
|
|
{
|
|
|
|
sv.timeResidualFraction-=1.0f;
|
|
|
|
if (cl_newClock&&cl_newClock->integer)
|
|
|
|
{
|
|
|
|
sv.timeResidual++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ( sv.timeResidual < frameMsec ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// if time is about to hit the 32nd bit, restart the
|
|
|
|
// level, which will force the time back to zero, rather
|
|
|
|
// than checking for negative time wraparound everywhere.
|
|
|
|
// 2giga-milliseconds = 23 days, so it won't be too often
|
|
|
|
if ( sv.time > 0x70000000 ) {
|
|
|
|
SV_Shutdown( "Restarting server due to time wrapping" );
|
|
|
|
Com_Printf("You win. if you can play this long and not die, you deserve to win.\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( CVAR_SYSTEMINFO ) );
|
|
|
|
cvar_modifiedFlags &= ~CVAR_SYSTEMINFO;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( com_speeds->integer ) {
|
|
|
|
startTime = Sys_Milliseconds ();
|
|
|
|
}
|
|
|
|
|
|
|
|
// SV_BotFrame( sv.time );
|
|
|
|
|
|
|
|
// run the game simulation in chunks
|
|
|
|
while ( sv.timeResidual >= frameMsec ) {
|
|
|
|
sv.timeResidual -= frameMsec;
|
|
|
|
sv.time += frameMsec;
|
|
|
|
G2API_SetTime(sv.time,G2T_SV_TIME);
|
|
|
|
|
|
|
|
// let everything in the world think and move
|
|
|
|
ge->RunFrame( sv.time );
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( com_speeds->integer ) {
|
|
|
|
time_game = Sys_Milliseconds () - startTime;
|
|
|
|
}
|
|
|
|
|
|
|
|
SG_TestSave(); // returns immediately if not active, used for fake-save-every-cycle to test (mainly) Icarus disk code
|
|
|
|
|
|
|
|
// check timeouts
|
|
|
|
SV_CheckTimeouts ();
|
|
|
|
|
|
|
|
// update ping based on the last known frame from all clients
|
|
|
|
SV_CalcPings ();
|
|
|
|
|
|
|
|
// send messages back to the clients
|
|
|
|
SV_SendClientMessages ();
|
|
|
|
}
|
|
|
|
|
|
|
|
//============================================================================
|
|
|
|
|