mirror of
https://github.com/blendogames/thirtyflightsofloving.git
synced 2024-11-15 00:41:21 +00:00
8a2739c555
Tweaked client handling of "Server is version x.xx" server rejection messages. Client now sends info requests to servers with both KMQ2 and stock Q2 protocol versions. Client now ignores server "wrong version" status messages. Added server sending protocol version in challenge message.
1298 lines
34 KiB
C
1298 lines
34 KiB
C
/*
|
|
===========================================================================
|
|
Copyright (C) 1997-2001 Id Software, Inc.
|
|
|
|
This file is part of Quake 2 source code.
|
|
|
|
Quake 2 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 2 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 2 source code; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
===========================================================================
|
|
*/
|
|
|
|
// sv_main.c -- server main program
|
|
|
|
#include "server.h"
|
|
|
|
netadr_t master_adr[MAX_MASTERS]; // address of group servers
|
|
|
|
client_t *sv_client; // current client
|
|
|
|
cvar_t *sv_paused;
|
|
cvar_t *sv_timedemo;
|
|
|
|
cvar_t *sv_enforcetime;
|
|
|
|
cvar_t *timeout; // seconds without any message
|
|
cvar_t *zombietime; // seconds to sink messages after disconnect
|
|
|
|
cvar_t *rcon_password; // password for remote server commands
|
|
|
|
cvar_t *allow_download;
|
|
cvar_t *allow_download_players;
|
|
cvar_t *allow_download_models;
|
|
cvar_t *allow_download_sounds;
|
|
cvar_t *allow_download_maps;
|
|
cvar_t *allow_download_pics;
|
|
cvar_t *allow_download_textures;
|
|
// Knightmare- whether to allow downloading 24-bit textures
|
|
cvar_t *allow_download_textures_24bit;
|
|
|
|
cvar_t *sv_downloadserver; // from R1Q2
|
|
|
|
cvar_t *sv_baselines_maxlen; // Knightmare- max packet size for connect messages
|
|
cvar_t *sv_limit_msglen; // Knightmare- whether to use MAX_MSGLEN_MP for multiplayer games
|
|
|
|
cvar_t *sv_airaccelerate;
|
|
|
|
cvar_t *sv_noreload; // don't reload level state when reentering
|
|
|
|
cvar_t *maxclients; // FIXME: rename sv_maxclients
|
|
cvar_t *sv_showclamp;
|
|
|
|
cvar_t *hostname;
|
|
cvar_t *public_server; // should heartbeats be sent
|
|
|
|
cvar_t *sv_iplimit; // r1ch: max connections from a single IP (prevent DoS)
|
|
|
|
cvar_t *sv_reconnect_limit; // minimum seconds between connect messages
|
|
|
|
cvar_t *sv_entfile; // whether to use .ent file
|
|
|
|
void Master_Shutdown (void);
|
|
|
|
|
|
//============================================================================
|
|
|
|
|
|
/*
|
|
=====================
|
|
SV_DropClient
|
|
|
|
Called when the player is totally leaving the server, either willingly
|
|
or unwillingly. This is NOT called if the entire server is quiting
|
|
or crashing.
|
|
=====================
|
|
*/
|
|
void SV_DropClient (client_t *drop)
|
|
{
|
|
// add the disconnect
|
|
MSG_WriteByte (&drop->netchan.message, svc_disconnect);
|
|
|
|
if (drop->state == cs_spawned)
|
|
{
|
|
// call the prog function for removing a client
|
|
// this will remove the body, among other things
|
|
ge->ClientDisconnect (drop->edict);
|
|
}
|
|
|
|
if (drop->download)
|
|
{
|
|
FS_FreeFile (drop->download);
|
|
drop->download = NULL;
|
|
}
|
|
|
|
// r1ch: fix for mods that don't clean score
|
|
if (drop->edict && drop->edict->client)
|
|
drop->edict->client->ps.stats[STAT_FRAGS] = 0;
|
|
|
|
drop->state = cs_zombie; // become free in a few seconds
|
|
drop->name[0] = 0;
|
|
}
|
|
|
|
|
|
//Knightmare added
|
|
/*
|
|
=====================
|
|
GetClientFromAdr
|
|
|
|
Given an netadr_t, returns the matching client.
|
|
=====================
|
|
*/
|
|
client_t *GetClientFromAdr (netadr_t address)
|
|
{
|
|
client_t *cl;
|
|
int i;
|
|
qboolean found = false;
|
|
|
|
// for (i = 0; i < maxclients->value; i++)
|
|
for (i = 0; i < maxclients->integer; i++)
|
|
{
|
|
cl = &svs.clients[i];
|
|
if (NET_CompareBaseAdr(cl->netchan.remote_address, address)) {
|
|
found = true; break; }
|
|
}
|
|
if (found)
|
|
return cl;
|
|
else // don't return non-matching client
|
|
return NULL;
|
|
}
|
|
|
|
|
|
/*
|
|
=====================
|
|
SV_DropClientFromAdr
|
|
|
|
Calls SV_DropClient, takes netadr_t instead of client pointer.
|
|
=====================
|
|
*/
|
|
void SV_DropClientFromAdr (netadr_t address)
|
|
{ // adapted Pat Aftermoon's simplified version of this
|
|
client_t *drop = GetClientFromAdr(address);
|
|
|
|
if (!drop) return; // make sure we have a client to drop
|
|
|
|
SV_BroadcastPrintf (PRINT_HIGH, "dropping client %s\n", drop->name);
|
|
|
|
SV_DropClient (drop);
|
|
|
|
drop->state = cs_free; // don't bother with zombie state
|
|
}
|
|
// end Knightmare
|
|
|
|
/*
|
|
=====================
|
|
SV_KickClient
|
|
|
|
From R1Q2
|
|
=====================
|
|
*/
|
|
void SV_KickClient (client_t *cl, const char *reason, const char *cprintf)
|
|
{
|
|
if (reason && cl->state == cs_spawned && cl->name[0])
|
|
SV_BroadcastPrintf (PRINT_HIGH, "%s was dropped: %s\n", cl->name, reason);
|
|
if (cprintf)
|
|
SV_ClientPrintf (cl, PRINT_HIGH, "%s", cprintf);
|
|
Com_Printf ("Dropping %s, %s.\n", cl->name, reason ? reason : "SV_KickClient");
|
|
SV_DropClient (cl);
|
|
}
|
|
|
|
|
|
/*
|
|
=====================
|
|
SV_CleanClient
|
|
|
|
From R1Q2
|
|
r1ch: this does the final cleaning up of a client after zombie state.
|
|
=====================
|
|
*/
|
|
void SV_CleanClient (client_t *drop)
|
|
{
|
|
if (drop->download)
|
|
{
|
|
Z_Free (drop->download);
|
|
drop->download = NULL;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==============================================================================
|
|
|
|
CONNECTIONLESS COMMANDS
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
/*
|
|
===============
|
|
SV_StatusString
|
|
|
|
Builds the string that is sent as heartbeats and status replies
|
|
===============
|
|
*/
|
|
char *SV_StatusString (void)
|
|
{
|
|
char player[1024];
|
|
static char status[MAX_MSGLEN - 16];
|
|
int i;
|
|
client_t *cl;
|
|
int statusLength;
|
|
int playerLength;
|
|
|
|
// strncpy (status, Cvar_Serverinfo());
|
|
// strncat (status, "\n");
|
|
Q_strncpyz (status, sizeof(status), Cvar_Serverinfo());
|
|
Q_strncatz (status, sizeof(status), "\n");
|
|
statusLength = (int)strlen(status);
|
|
|
|
// for (i=0 ; i<maxclients->value ; i++)
|
|
for (i=0 ; i<maxclients->integer ; i++)
|
|
{
|
|
cl = &svs.clients[i];
|
|
if (cl->state == cs_connected || cl->state == cs_spawned )
|
|
{
|
|
Com_sprintf (player, sizeof(player), "%i %i \"%s\"\n",
|
|
cl->edict->client->ps.stats[STAT_FRAGS], cl->ping, cl->name);
|
|
playerLength = (int)strlen(player);
|
|
if (statusLength + playerLength >= sizeof(status) )
|
|
break; // can't hold any more
|
|
// strncpy (status + statusLength, player);
|
|
Q_strncpyz (status+statusLength, sizeof(status)-statusLength, player);
|
|
statusLength += playerLength;
|
|
}
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
/*
|
|
================
|
|
SVC_Status
|
|
|
|
Responds with all the info that qplug or qspy can see
|
|
================
|
|
*/
|
|
void SVC_Status (void)
|
|
{
|
|
Netchan_OutOfBandPrint (NS_SERVER, net_from, "print\n%s", SV_StatusString());
|
|
#if 0
|
|
Com_BeginRedirect (RD_PACKET, sv_outputbuf, SV_OUTPUTBUF_LENGTH, SV_FlushRedirect);
|
|
Com_Printf (SV_StatusString());
|
|
Com_EndRedirect ();
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
================
|
|
SVC_Ack
|
|
|
|
================
|
|
*/
|
|
void SVC_Ack (void)
|
|
{
|
|
Com_Printf ("Ping acknowledge from %s\n", NET_AdrToString(net_from));
|
|
}
|
|
|
|
/*
|
|
================
|
|
SVC_Info
|
|
|
|
Responds with short info for broadcast scans
|
|
The second parameter should be the current protocol version number.
|
|
================
|
|
*/
|
|
void SVC_Info (void)
|
|
{
|
|
char string[64];
|
|
int i, count;
|
|
int version;
|
|
|
|
// if (maxclients->value == 1)
|
|
if (maxclients->integer == 1)
|
|
return; // ignore in single player
|
|
|
|
version = atoi (Cmd_Argv(1));
|
|
|
|
if (version != PROTOCOL_VERSION)
|
|
{ // According to r1ch, this can be used to make servers endlessly ping each other
|
|
// Com_sprintf (string, sizeof(string), "%s: wrong version\n", hostname->string, sizeof(string));
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
count = 0;
|
|
// for (i=0 ; i<maxclients->value ; i++)
|
|
for (i=0 ; i<maxclients->integer ; i++)
|
|
if (svs.clients[i].state >= cs_connected)
|
|
count++;
|
|
|
|
// Com_sprintf (string, sizeof(string), "%16s %8s %2i/%2i\n", hostname->string, sv.name, count, (int)maxclients->value);
|
|
Com_sprintf (string, sizeof(string), "%16s %8s %2i/%2i\n", hostname->string, sv.name, count, maxclients->integer);
|
|
}
|
|
|
|
Netchan_OutOfBandPrint (NS_SERVER, net_from, "info\n%s", string);
|
|
}
|
|
|
|
/*
|
|
================
|
|
SVC_Ping
|
|
|
|
Just responds with an acknowledgement
|
|
================
|
|
*/
|
|
void SVC_Ping (void)
|
|
{
|
|
Netchan_OutOfBandPrint (NS_SERVER, net_from, "ack");
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
SVC_GetChallenge
|
|
|
|
Returns a challenge number that can be used
|
|
in a subsequent client_connect command.
|
|
We do this to prevent denial of service attacks that
|
|
flood the server with invalid connection IPs. With a
|
|
challenge, they must give a valid IP address.
|
|
=================
|
|
*/
|
|
void SVC_GetChallenge (void)
|
|
{
|
|
int i;
|
|
int oldest;
|
|
int oldestTime;
|
|
|
|
oldest = 0;
|
|
oldestTime = 0x7fffffff;
|
|
|
|
// see if we already have a challenge for this ip
|
|
for (i = 0 ; i < MAX_CHALLENGES ; i++)
|
|
{
|
|
if (NET_CompareBaseAdr (net_from, svs.challenges[i].adr))
|
|
break;
|
|
if (svs.challenges[i].time < oldestTime)
|
|
{
|
|
oldestTime = svs.challenges[i].time;
|
|
oldest = i;
|
|
}
|
|
}
|
|
|
|
if (i == MAX_CHALLENGES)
|
|
{
|
|
// overwrite the oldest
|
|
svs.challenges[oldest].challenge = rand() & 0x7fff;
|
|
svs.challenges[oldest].adr = net_from;
|
|
svs.challenges[oldest].time = curtime;
|
|
i = oldest;
|
|
}
|
|
|
|
// send it back
|
|
// Netchan_OutOfBandPrint (NS_SERVER, net_from, "challenge %i", svs.challenges[i].challenge);
|
|
Netchan_OutOfBandPrint (NS_SERVER, net_from, "challenge %i p=%i", svs.challenges[i].challenge, PROTOCOL_VERSION);
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SVC_DirectConnect
|
|
|
|
A connection request that did not come from the master
|
|
==================
|
|
*/
|
|
void SVC_DirectConnect (void)
|
|
{
|
|
char userinfo[MAX_INFO_STRING];
|
|
netadr_t adr;
|
|
int i;
|
|
client_t *cl, *newcl;
|
|
client_t temp;
|
|
edict_t *ent;
|
|
int edictnum;
|
|
int version;
|
|
int qport;
|
|
int challenge;
|
|
int previousclients; // rich: connection limit per IP
|
|
|
|
adr = net_from;
|
|
|
|
Com_DPrintf ("SVC_DirectConnect ()\n");
|
|
|
|
version = atoi(Cmd_Argv(1));
|
|
if (version != PROTOCOL_VERSION)
|
|
{
|
|
// Netchan_OutOfBandPrint (NS_SERVER, adr, "print\nServer is version %4.2f.\n", VERSION);
|
|
Netchan_OutOfBandPrint (NS_SERVER, adr, "print\nYou need KMQuake2 version %4.2f to play on this server.\n", VERSION);
|
|
Com_DPrintf (" rejected connect from version %i\n", version);
|
|
return;
|
|
}
|
|
|
|
qport = atoi(Cmd_Argv(2));
|
|
|
|
challenge = atoi(Cmd_Argv(3));
|
|
|
|
// r1ch: limit connections from a single IP
|
|
previousclients = 0;
|
|
// for (i=0,cl=svs.clients; i<(int)maxclients->value; i++,cl++)
|
|
for (i=0,cl=svs.clients; i<maxclients->integer; i++,cl++)
|
|
{
|
|
if (cl->state == cs_free)
|
|
continue;
|
|
if (NET_CompareBaseAdr (adr, cl->netchan.remote_address))
|
|
{
|
|
// r1ch: zombies are less dangerous
|
|
if (cl->state == cs_zombie)
|
|
previousclients++;
|
|
else
|
|
previousclients += 2;
|
|
}
|
|
}
|
|
if (previousclients >= (int)sv_iplimit->value * 2)
|
|
{
|
|
Netchan_OutOfBandPrint (NS_SERVER, adr, "print\nToo many connections from your host.\n");
|
|
Com_DPrintf (" too many connections\n");
|
|
return;
|
|
}
|
|
// end r1ch fix
|
|
|
|
strncpy (userinfo, Cmd_Argv(4), sizeof(userinfo)-1);
|
|
userinfo[sizeof(userinfo) - 1] = 0;
|
|
|
|
// force the IP key/value pair so the game can filter based on ip
|
|
Info_SetValueForKey (userinfo, "ip", NET_AdrToString(net_from));
|
|
|
|
// attractloop servers are ONLY for local clients
|
|
if (sv.attractloop)
|
|
{
|
|
if (!NET_IsLocalAddress (adr))
|
|
{
|
|
Com_Printf ("Remote connect in attract loop. Ignored.\n");
|
|
Netchan_OutOfBandPrint (NS_SERVER, adr, "print\nConnection refused.\n");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// see if the challenge is valid
|
|
if (!NET_IsLocalAddress (adr))
|
|
{
|
|
for (i=0 ; i<MAX_CHALLENGES ; i++)
|
|
{
|
|
if (NET_CompareBaseAdr (net_from, svs.challenges[i].adr))
|
|
{
|
|
if (challenge == svs.challenges[i].challenge)
|
|
break; // good
|
|
Netchan_OutOfBandPrint (NS_SERVER, adr, "print\nBad challenge.\n");
|
|
return;
|
|
}
|
|
}
|
|
if (i == MAX_CHALLENGES)
|
|
{
|
|
Netchan_OutOfBandPrint (NS_SERVER, adr, "print\nNo challenge for address.\n");
|
|
return;
|
|
}
|
|
}
|
|
|
|
newcl = &temp;
|
|
memset (newcl, 0, sizeof(client_t));
|
|
|
|
// if there is already a slot for this ip, reuse it
|
|
// for (i=0,cl=svs.clients ; i<maxclients->value ; i++,cl++)
|
|
for (i=0,cl=svs.clients; i<maxclients->integer; i++,cl++)
|
|
{
|
|
if (cl->state == cs_free)
|
|
continue;
|
|
if (NET_CompareBaseAdr (adr, cl->netchan.remote_address)
|
|
&& ( cl->netchan.qport == qport
|
|
|| adr.port == cl->netchan.remote_address.port ) )
|
|
{
|
|
// if (!NET_IsLocalAddress (adr) && (svs.realtime - cl->lastconnect) < ((int)sv_reconnect_limit->value * 1000))
|
|
if (!NET_IsLocalAddress (adr) && (svs.realtime - cl->lastconnect) < (sv_reconnect_limit->integer * 1000))
|
|
{
|
|
Com_DPrintf ("%s:reconnect rejected : too soon\n", NET_AdrToString (adr));
|
|
return;
|
|
}
|
|
// r1ch: !! fix nasty bug where non-disconnected clients (from dropped disconnect
|
|
// packets) could be overwritten!
|
|
if (cl->state != cs_zombie)
|
|
{
|
|
Com_DPrintf (" client already found\n");
|
|
// If we legitly get here, spoofed udp isn't possible (passed challenge) and client addr/port combo
|
|
// is exactly the same, so we can assume its really a dropped/crashed client. i hope...
|
|
Com_Printf ("Dropping %s, ghost reconnect\n", cl->name);
|
|
SV_DropClient (cl);
|
|
}
|
|
// end r1ch fix
|
|
|
|
Com_Printf ("%s:reconnect\n", NET_AdrToString (adr));
|
|
|
|
SV_CleanClient (cl); // r1ch: clean up last client data
|
|
|
|
newcl = cl;
|
|
goto gotnewcl;
|
|
}
|
|
}
|
|
|
|
// find a client slot
|
|
newcl = NULL;
|
|
// for (i=0,cl=svs.clients ; i<maxclients->value ; i++,cl++)
|
|
for (i=0,cl=svs.clients ; i<maxclients->integer ; i++,cl++)
|
|
{
|
|
if (cl->state == cs_free)
|
|
{
|
|
newcl = cl;
|
|
break;
|
|
}
|
|
}
|
|
if (!newcl)
|
|
{
|
|
Netchan_OutOfBandPrint (NS_SERVER, adr, "print\nServer is full.\n");
|
|
Com_DPrintf ("Rejected a connection.\n");
|
|
return;
|
|
}
|
|
|
|
gotnewcl:
|
|
// build a new connection
|
|
// accept the new client
|
|
// this is the only place a client_t is ever initialized
|
|
*newcl = temp;
|
|
sv_client = newcl;
|
|
edictnum = (newcl-svs.clients)+1;
|
|
ent = EDICT_NUM(edictnum);
|
|
newcl->edict = ent;
|
|
newcl->challenge = challenge; // save challenge for checksumming
|
|
|
|
// get the game a chance to reject this connection or modify the userinfo
|
|
if (!(ge->ClientConnect (ent, userinfo)))
|
|
{
|
|
if (*Info_ValueForKey (userinfo, "rejmsg"))
|
|
Netchan_OutOfBandPrint (NS_SERVER, adr, "print\n%s\nConnection refused.\n",
|
|
Info_ValueForKey (userinfo, "rejmsg"));
|
|
else
|
|
Netchan_OutOfBandPrint (NS_SERVER, adr, "print\nConnection refused.\n" );
|
|
Com_DPrintf ("Game rejected a connection.\n");
|
|
return;
|
|
}
|
|
|
|
// parse some info from the info strings
|
|
strncpy (newcl->userinfo, userinfo, sizeof(newcl->userinfo)-1);
|
|
SV_UserinfoChanged (newcl);
|
|
|
|
// send the connect packet to the client
|
|
// r1ch: note we could ideally send this twice but it prints unsightly message on original client.
|
|
if (sv_downloadserver->string[0])
|
|
Netchan_OutOfBandPrint (NS_SERVER, adr, "client_connect dlserver=%s", sv_downloadserver->string);
|
|
else
|
|
Netchan_OutOfBandPrint (NS_SERVER, adr, "client_connect");
|
|
|
|
Netchan_Setup (NS_SERVER, &newcl->netchan , adr, qport);
|
|
|
|
newcl->state = cs_connected;
|
|
|
|
SZ_Init (&newcl->datagram, newcl->datagram_buf, sizeof(newcl->datagram_buf) );
|
|
newcl->datagram.allowoverflow = true;
|
|
newcl->lastmessage = svs.realtime; // don't timeout
|
|
newcl->lastconnect = svs.realtime;
|
|
}
|
|
|
|
int Rcon_Validate (void)
|
|
{
|
|
if (!strlen (rcon_password->string))
|
|
return 0;
|
|
|
|
if (strcmp (Cmd_Argv(1), rcon_password->string) )
|
|
return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
/*
|
|
===============
|
|
SVC_RemoteCommand
|
|
|
|
A client issued an rcon command.
|
|
Shift down the remaining args
|
|
Redirect all printfs
|
|
===============
|
|
*/
|
|
void SVC_RemoteCommand (void)
|
|
{
|
|
int i;
|
|
char remaining[1024];
|
|
|
|
i = Rcon_Validate ();
|
|
|
|
if (i == 0)
|
|
Com_Printf ("Bad rcon from %s:\n%s\n", NET_AdrToString (net_from), net_message.data+4);
|
|
else
|
|
Com_Printf ("Rcon from %s:\n%s\n", NET_AdrToString (net_from), net_message.data+4);
|
|
|
|
Com_BeginRedirect (RD_PACKET, sv_outputbuf, SV_OUTPUTBUF_LENGTH, SV_FlushRedirect);
|
|
|
|
if (!Rcon_Validate ())
|
|
{
|
|
Com_Printf ("Bad rcon_password.\n");
|
|
}
|
|
else
|
|
{
|
|
remaining[0] = 0;
|
|
|
|
for (i=2 ; i<Cmd_Argc() ; i++)
|
|
{
|
|
// strncat (remaining, Cmd_Argv(i) );
|
|
// strncat (remaining, " ");
|
|
Q_strncatz (remaining, sizeof(remaining), Cmd_Argv(i));
|
|
Q_strncatz (remaining, sizeof(remaining), " ");
|
|
}
|
|
|
|
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.
|
|
=================
|
|
*/
|
|
void SV_ConnectionlessPacket (void)
|
|
{
|
|
char *s;
|
|
char *c;
|
|
|
|
// r1ch fix: make sure we never talk to ourselves
|
|
// if (NET_IsLocalAddress (net_from.ip[0] == 127) && !NET_IsLocalHost(net_from) && ShortSwap(net_from.port) == server_port)
|
|
/* if ( (net_from.ip[0] == 127) && (net_from.type != NA_LOOPBACK) && (ShortSwap(net_from.port) == server_port) )
|
|
{
|
|
Com_DPrintf ("dropped %d byte connectionless packet from self! (spoofing attack?)\n", net_message.cursize);
|
|
return;
|
|
}*/
|
|
|
|
MSG_BeginReading (&net_message);
|
|
MSG_ReadLong (&net_message); // skip the -1 marker
|
|
|
|
s = MSG_ReadStringLine (&net_message);
|
|
|
|
Cmd_TokenizeString (s, false);
|
|
|
|
c = Cmd_Argv(0);
|
|
Com_DPrintf ("Packet %s : %s\n", NET_AdrToString(net_from), c);
|
|
|
|
if (!strcmp(c, "ping"))
|
|
SVC_Ping ();
|
|
else if (!strcmp(c, "ack"))
|
|
SVC_Ack ();
|
|
else if (!strcmp(c,"status"))
|
|
SVC_Status ();
|
|
else if (!strcmp(c,"info"))
|
|
SVC_Info ();
|
|
else if (!strcmp(c,"getchallenge"))
|
|
SVC_GetChallenge ();
|
|
else if (!strcmp(c,"connect"))
|
|
SVC_DirectConnect ();
|
|
else if (!strcmp(c, "rcon"))
|
|
SVC_RemoteCommand ();
|
|
else
|
|
Com_Printf ("bad connectionless packet from %s:\n%s\n"
|
|
, NET_AdrToString (net_from), s);
|
|
}
|
|
|
|
|
|
//============================================================================
|
|
|
|
/*
|
|
===================
|
|
SV_CalcPings
|
|
|
|
Updates the cl->ping variables
|
|
===================
|
|
*/
|
|
void SV_CalcPings (void)
|
|
{
|
|
int i, j;
|
|
client_t *cl;
|
|
int total, count;
|
|
|
|
// for (i=0 ; i<maxclients->value ; i++)
|
|
for (i=0 ; i<maxclients->integer ; i++)
|
|
{
|
|
cl = &svs.clients[i];
|
|
if (cl->state != cs_spawned )
|
|
continue;
|
|
|
|
#if 0
|
|
if (cl->lastframe > 0)
|
|
cl->frame_latency[sv.framenum&(LATENCY_COUNTS-1)] = sv.framenum - cl->lastframe + 1;
|
|
else
|
|
cl->frame_latency[sv.framenum&(LATENCY_COUNTS-1)] = 0;
|
|
#endif
|
|
|
|
total = 0;
|
|
count = 0;
|
|
for (j=0 ; j<LATENCY_COUNTS ; j++)
|
|
{
|
|
if (cl->frame_latency[j] > 0)
|
|
{
|
|
count++;
|
|
total += cl->frame_latency[j];
|
|
}
|
|
}
|
|
if (!count)
|
|
cl->ping = 0;
|
|
else
|
|
#if 0
|
|
cl->ping = total*100/count - 100;
|
|
#else
|
|
cl->ping = total / count;
|
|
#endif
|
|
|
|
// let the game dll know about the ping
|
|
cl->edict->client->ping = cl->ping;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
===================
|
|
SV_GiveMsec
|
|
|
|
Every few frames, gives all clients an allotment of milliseconds
|
|
for their command moves. If they exceed it, assume cheating.
|
|
===================
|
|
*/
|
|
void SV_GiveMsec (void)
|
|
{
|
|
int i;
|
|
client_t *cl;
|
|
|
|
if (sv.framenum & 15)
|
|
return;
|
|
|
|
// for (i=0 ; i<maxclients->value ; i++)
|
|
for (i=0 ; i<maxclients->integer ; i++)
|
|
{
|
|
cl = &svs.clients[i];
|
|
if (cl->state == cs_free )
|
|
continue;
|
|
|
|
cl->commandMsec = 1800; // 1600 + some slop
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
SV_ReadPackets
|
|
=================
|
|
*/
|
|
void SV_ReadPackets (void)
|
|
{
|
|
int i;
|
|
client_t *cl;
|
|
int qport;
|
|
|
|
while (NET_GetPacket (NS_SERVER, &net_from, &net_message))
|
|
{
|
|
// check for connectionless packet (0xffffffff) first
|
|
if (*(int *)net_message.data == -1)
|
|
{
|
|
SV_ConnectionlessPacket ();
|
|
continue;
|
|
}
|
|
|
|
// read the qport out of the message so we can fix up
|
|
// stupid address translating routers
|
|
MSG_BeginReading (&net_message);
|
|
MSG_ReadLong (&net_message); // sequence number
|
|
MSG_ReadLong (&net_message); // sequence number
|
|
qport = MSG_ReadShort (&net_message) & 0xffff;
|
|
|
|
// check for packets from connected clients
|
|
// for (i=0, cl=svs.clients ; i<maxclients->value ; i++,cl++)
|
|
for (i=0, cl=svs.clients ; i<maxclients->integer ; i++,cl++)
|
|
{
|
|
if (cl->state == cs_free)
|
|
continue;
|
|
if (!NET_CompareBaseAdr (net_from, cl->netchan.remote_address))
|
|
continue;
|
|
if (cl->netchan.qport != qport)
|
|
continue;
|
|
if (cl->netchan.remote_address.port != net_from.port)
|
|
{
|
|
Com_Printf ("SV_ReadPackets: fixing up a translated port\n");
|
|
cl->netchan.remote_address.port = net_from.port;
|
|
}
|
|
|
|
if (Netchan_Process(&cl->netchan, &net_message))
|
|
{ // this is a valid, sequenced packet, so process it
|
|
if (cl->state != cs_zombie)
|
|
{
|
|
cl->lastmessage = svs.realtime; // don't timeout
|
|
SV_ExecuteClientMessage (cl);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
// if (i != maxclients->value)
|
|
if (i != maxclients->integer)
|
|
continue;
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_CheckTimeouts
|
|
|
|
If a packet has not been received from a client for timeout->value
|
|
seconds, drop the conneciton. Server frames are 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 = svs.realtime - 1000*timeout->value;
|
|
zombiepoint = svs.realtime - 1000*zombietime->value;
|
|
|
|
// for (i=0,cl=svs.clients ; i<maxclients->value ; i++,cl++)
|
|
for (i=0,cl=svs.clients ; i<maxclients->integer ; i++,cl++)
|
|
{
|
|
// message times may be wrong across a changelevel
|
|
if (cl->lastmessage > svs.realtime)
|
|
cl->lastmessage = svs.realtime;
|
|
|
|
if (cl->state == cs_zombie
|
|
&& cl->lastmessage < zombiepoint)
|
|
{
|
|
SV_CleanClient (cl); // r1ch fix: make sure client is cleaned up
|
|
cl->state = cs_free; // can now be reused
|
|
continue;
|
|
}
|
|
if ( (cl->state == cs_connected || cl->state == cs_spawned)
|
|
&& cl->lastmessage < droppoint)
|
|
{
|
|
// r1ch fix: only message if they spawned (less spam plz)
|
|
if (cl->state == cs_spawned && cl->name[0])
|
|
SV_BroadcastPrintf (PRINT_HIGH, "%s timed out\n", cl->name);
|
|
SV_DropClient (cl);
|
|
cl->state = cs_free; // don't bother with zombie state
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
================
|
|
SV_PrepWorldFrame
|
|
|
|
This has to be done before the world logic, because
|
|
player processing happens outside RunWorldFrame
|
|
================
|
|
*/
|
|
void SV_PrepWorldFrame (void)
|
|
{
|
|
edict_t *ent;
|
|
int i;
|
|
|
|
for (i=0 ; i<ge->num_edicts ; i++, ent++)
|
|
{
|
|
ent = EDICT_NUM(i);
|
|
// events only last for a single message
|
|
ent->s.event = 0;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
SV_RunGameFrame
|
|
=================
|
|
*/
|
|
void SV_RunGameFrame (void)
|
|
{
|
|
// if (host_speeds->value)
|
|
if (host_speeds->integer)
|
|
time_before_game = Sys_Milliseconds ();
|
|
|
|
// we always need to bump framenum, even if we
|
|
// don't run the world, otherwise the delta
|
|
// compression can get confused when a client
|
|
// has the "current" frame
|
|
sv.framenum++;
|
|
sv.time = sv.framenum*100;
|
|
|
|
// don't run if paused
|
|
// if (!sv_paused->value || maxclients->value > 1)
|
|
if (!sv_paused->integer || maxclients->integer > 1)
|
|
{
|
|
ge->RunFrame ();
|
|
|
|
// never get more than one tic behind
|
|
if (sv.time < svs.realtime)
|
|
{
|
|
// if (sv_showclamp->value)
|
|
if (sv_showclamp->integer)
|
|
Com_Printf ("sv highclamp\n");
|
|
svs.realtime = sv.time;
|
|
}
|
|
}
|
|
|
|
// if (host_speeds->value)
|
|
if (host_speeds->integer)
|
|
time_after_game = Sys_Milliseconds ();
|
|
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_Frame
|
|
|
|
==================
|
|
*/
|
|
void SV_Frame (int msec)
|
|
{
|
|
time_before_game = time_after_game = 0;
|
|
|
|
// if server is not active, do nothing
|
|
if (!svs.initialized)
|
|
return;
|
|
|
|
svs.realtime += msec;
|
|
|
|
// keep the random time dependent
|
|
rand ();
|
|
|
|
// check timeouts
|
|
SV_CheckTimeouts ();
|
|
|
|
// get packets from clients
|
|
SV_ReadPackets ();
|
|
|
|
// move autonomous things around if enough time has passed
|
|
// if (!sv_timedemo->value && svs.realtime < sv.time)
|
|
if (!sv_timedemo->integer && svs.realtime < sv.time)
|
|
{
|
|
// never let the time get too far off
|
|
if (sv.time - svs.realtime > 100)
|
|
{
|
|
// if (sv_showclamp->value)
|
|
if (sv_showclamp->integer)
|
|
Com_Printf ("sv lowclamp\n");
|
|
svs.realtime = sv.time - 100;
|
|
}
|
|
NET_Sleep(sv.time - svs.realtime);
|
|
return;
|
|
}
|
|
|
|
// update ping based on the last known frame from all clients
|
|
SV_CalcPings ();
|
|
|
|
// give the clients some timeslices
|
|
SV_GiveMsec ();
|
|
|
|
// let everything in the world think and move
|
|
SV_RunGameFrame ();
|
|
|
|
// send messages back to the clients that had packets read this frame
|
|
SV_SendClientMessages ();
|
|
|
|
// save the entire world state if recording a serverdemo
|
|
SV_RecordDemoMessage ();
|
|
|
|
// send a heartbeat to the master if needed
|
|
Master_Heartbeat ();
|
|
|
|
// clear teleport flags, etc for next frame
|
|
SV_PrepWorldFrame ();
|
|
|
|
}
|
|
|
|
//============================================================================
|
|
|
|
/*
|
|
================
|
|
Master_Heartbeat
|
|
|
|
Send a message to the master every few minutes to
|
|
let it know we are alive, and log information
|
|
================
|
|
*/
|
|
#define HEARTBEAT_SECONDS 300
|
|
void Master_Heartbeat (void)
|
|
{
|
|
char *string;
|
|
int i;
|
|
|
|
// pgm post 3.19 change, cvar pointer not validated before dereferencing
|
|
// if (!dedicated || !dedicated->value)
|
|
if (!dedicated || !dedicated->integer)
|
|
return; // only dedicated servers send heartbeats
|
|
|
|
// pgm post 3.19 change, cvar pointer not validated before dereferencing
|
|
// if (!public_server || !public_server->value)
|
|
if (!public_server || !public_server->integer)
|
|
return; // a private dedicated game
|
|
|
|
// check for time wraparound
|
|
if (svs.last_heartbeat > svs.realtime)
|
|
svs.last_heartbeat = svs.realtime;
|
|
|
|
if (svs.realtime - svs.last_heartbeat < HEARTBEAT_SECONDS*1000)
|
|
return; // not time to send yet
|
|
|
|
svs.last_heartbeat = svs.realtime;
|
|
|
|
// send the same string that we would give for a status OOB command
|
|
string = SV_StatusString();
|
|
|
|
// send to group master
|
|
for (i=0 ; i<MAX_MASTERS ; i++)
|
|
if (master_adr[i].port)
|
|
{
|
|
Com_Printf ("Sending heartbeat to %s\n", NET_AdrToString (master_adr[i]));
|
|
Netchan_OutOfBandPrint (NS_SERVER, master_adr[i], "heartbeat\n%s", string);
|
|
}
|
|
}
|
|
|
|
/*
|
|
=================
|
|
Master_Shutdown
|
|
|
|
Informs all masters that this server is going down
|
|
=================
|
|
*/
|
|
void Master_Shutdown (void)
|
|
{
|
|
int i;
|
|
|
|
// pgm post3.19 change, cvar pointer not validated before dereferencing
|
|
// if (!dedicated || !dedicated->value)
|
|
if (!dedicated || !dedicated->integer)
|
|
return; // only dedicated servers send heartbeats
|
|
|
|
// pgm post3.19 change, cvar pointer not validated before dereferencing
|
|
// if (!public_server || !public_server->value)
|
|
if (!public_server || !public_server->integer)
|
|
return; // a private dedicated game
|
|
|
|
// send to group master
|
|
for (i=0 ; i<MAX_MASTERS ; i++)
|
|
if (master_adr[i].port)
|
|
{
|
|
if (i > 0)
|
|
Com_Printf ("Sending heartbeat to %s\n", NET_AdrToString (master_adr[i]));
|
|
Netchan_OutOfBandPrint (NS_SERVER, master_adr[i], "shutdown");
|
|
}
|
|
}
|
|
|
|
//============================================================================
|
|
|
|
|
|
/*
|
|
=================
|
|
SV_UserinfoChanged
|
|
|
|
Pull specific info from a newly changed userinfo string
|
|
into a more C freindly form.
|
|
=================
|
|
*/
|
|
void SV_UserinfoChanged (client_t *cl)
|
|
{
|
|
char *val;
|
|
int i;
|
|
|
|
// call prog code to allow overrides
|
|
ge->ClientUserinfoChanged (cl->edict, cl->userinfo);
|
|
|
|
// name for C code
|
|
strncpy (cl->name, Info_ValueForKey (cl->userinfo, "name"), sizeof(cl->name)-1);
|
|
// mask off high bit
|
|
for (i=0 ; i<sizeof(cl->name) ; i++)
|
|
cl->name[i] &= 127;
|
|
|
|
// rate command
|
|
val = Info_ValueForKey (cl->userinfo, "rate");
|
|
if (strlen(val))
|
|
{
|
|
i = atoi(val);
|
|
cl->rate = i;
|
|
if (cl->rate < 100)
|
|
cl->rate = 100;
|
|
if (cl->rate > 15000)
|
|
cl->rate = 15000;
|
|
}
|
|
else
|
|
cl->rate = 5000;
|
|
|
|
// msg command
|
|
val = Info_ValueForKey (cl->userinfo, "msg");
|
|
if (strlen(val))
|
|
{
|
|
cl->messagelevel = atoi(val);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
//============================================================================
|
|
|
|
/*
|
|
===============
|
|
SV_Init
|
|
|
|
Only called at quake2.exe startup, not for each game
|
|
===============
|
|
*/
|
|
void SV_Init (void)
|
|
{
|
|
SV_InitOperatorCommands ();
|
|
|
|
rcon_password = Cvar_Get ("rcon_password", "", 0);
|
|
Cvar_SetDescription ("rcon_password", "Sets password for rcon commands.");
|
|
Cvar_Get ("skill", "1", 0);
|
|
Cvar_SetDescription ("skill", "Sets skill level. 0 = Easy, 1 = Medium, 2 = Hard, 3 = Nightmare.");
|
|
Cvar_Get ("deathmatch", "0", CVAR_LATCH);
|
|
Cvar_SetDescription ("deathmatch", "Enables non-coop multiplayer. This is enabled for both Deathmatch and CTF.");
|
|
Cvar_Get ("coop", "0", CVAR_LATCH);
|
|
Cvar_SetDescription ("coop", "Enables cooperative multiplayer.");
|
|
Cvar_Get ("dmflags", va("%i", DF_INSTANT_ITEMS), CVAR_SERVERINFO);
|
|
Cvar_SetDescription ("dmflags", "Bitfield for deathmatch options. Options and values vary by mod.");
|
|
Cvar_Get ("fraglimit", "0", CVAR_SERVERINFO);
|
|
Cvar_SetDescription ("fraglimit", "Number of frags reached in Deathmatch that triggers a map change.");
|
|
Cvar_Get ("timelimit", "0", CVAR_SERVERINFO);
|
|
Cvar_SetDescription ("timelimit", "Time in minutes between Deathmatch map changes.");
|
|
Cvar_Get ("cheats", "0", CVAR_SERVERINFO|CVAR_LATCH);
|
|
Cvar_SetDescription ("cheats", "Enables cheat codes in multiplayer games.");
|
|
Cvar_Get ("protocol", va("%i", PROTOCOL_VERSION), CVAR_SERVERINFO|CVAR_NOSET);;
|
|
Cvar_SetDescription ("protocol", "Protocol version for this server.");
|
|
maxclients = Cvar_Get ("maxclients", "1", CVAR_SERVERINFO | CVAR_LATCH);
|
|
Cvar_SetDescription ("maxclients", "Maximum number of players allowed on this server.");
|
|
hostname = Cvar_Get ("hostname", "noname", CVAR_SERVERINFO | CVAR_ARCHIVE);
|
|
Cvar_SetDescription ("hostname", "Name of this server visible to clients.");
|
|
timeout = Cvar_Get ("timeout", "125", 0);
|
|
Cvar_SetDescription ("timeout", "Time in seconds after which non-responsive clients are dropped.");
|
|
zombietime = Cvar_Get ("zombietime", "2", 0);
|
|
Cvar_SetDescription ("zombietime", "Time in seconds which client slots are kept in zombie state.");
|
|
sv_showclamp = Cvar_Get ("showclamp", "0", 0);
|
|
Cvar_SetDescription ("showclamp", "Enables output of high and low frame clamps.");
|
|
sv_paused = Cvar_Get ("paused", "0", 0);
|
|
Cvar_SetDescription ("paused", "Value that determines if the game is paused. Considered a client cheat in multiplayer.");
|
|
sv_timedemo = Cvar_Get ("timedemo", "0", 0);
|
|
Cvar_SetDescription ("timedemo", "Set to 1 for timing playback of demos. Useful for old-school bencmarking.");
|
|
sv_enforcetime = Cvar_Get ("sv_enforcetime", "0", 0);
|
|
Cvar_SetDescription ("sv_enforcetime", "Enables check for underflow of client command time.");
|
|
allow_download = Cvar_Get ("allow_download", "1", CVAR_ARCHIVE);
|
|
Cvar_SetDescription ("allow_download", "Enables autodownload of game assets in multiplayer.");
|
|
allow_download_players = Cvar_Get ("allow_download_players", "0", CVAR_ARCHIVE);
|
|
Cvar_SetDescription ("allow_download_players", "Enables download of player models.");
|
|
allow_download_models = Cvar_Get ("allow_download_models", "1", CVAR_ARCHIVE);
|
|
Cvar_SetDescription ("allow_download_models", "Enables download of models.");
|
|
allow_download_sounds = Cvar_Get ("allow_download_sounds", "1", CVAR_ARCHIVE);
|
|
Cvar_SetDescription ("allow_download_sounds", "Enables download of sounds.");
|
|
allow_download_maps = Cvar_Get ("allow_download_maps", "1", CVAR_ARCHIVE);
|
|
Cvar_SetDescription ("allow_download_maps", "Enables download of maps.");
|
|
allow_download_pics = Cvar_Get ("allow_download_pics", "1", CVAR_ARCHIVE);
|
|
Cvar_SetDescription ("allow_download_pics", "Enables download of .pcx images.");
|
|
allow_download_textures = Cvar_Get ("allow_download_textures", "1", CVAR_ARCHIVE);
|
|
Cvar_SetDescription ("allow_download_textures", "Enables download of .wal textures.");
|
|
// Knightmare- whether to allow downloading 24-bit textures
|
|
allow_download_textures_24bit = Cvar_Get ("allow_download_textures_24bit", "0", CVAR_ARCHIVE);
|
|
Cvar_SetDescription ("allow_download_textures_24bit", "Enables download of TGA/JPG/PNG textures.");
|
|
|
|
sv_downloadserver = Cvar_Get ("sv_downloadserver", "", 0); // r1ch: http dl server
|
|
Cvar_SetDescription ("sv_downloadserver", "Sets URL of HTTP autodownload server where clients can download game content over HTTP. Default empty. Path leads to game dir name, e.g. quake2.com/baseq2/maps.");
|
|
|
|
sv_baselines_maxlen = Cvar_Get ("sv_baselines_maxlen", "1200", 0); // Knightmare- max packet size for connect messages
|
|
Cvar_SetDescription ("sv_baselines_maxlen", "Sets max packet size for non-local network connect messages.");
|
|
sv_limit_msglen = Cvar_Get ("sv_limit_msglen", "1", 0); // Knightmare- whether to use MAX_MSGLEN_MP for multiplayer games
|
|
Cvar_SetDescription ("sv_limit_msglen", "Enables limited packet size for non-local clients.");
|
|
|
|
sv_noreload = Cvar_Get ("sv_noreload", "0", 0);
|
|
Cvar_SetDescription ("sv_noreload", "Disables loading of savegames on map change when set to 1.");
|
|
|
|
sv_airaccelerate = Cvar_Get("sv_airaccelerate", "0", CVAR_LATCH);
|
|
Cvar_SetDescription ("sv_airaccelerate", "Sets client air acceleration for non-coop multiplayer games.");
|
|
|
|
public_server = Cvar_Get ("public", "0", 0);
|
|
Cvar_SetDescription ("public", "Sets whether this is a public server.");
|
|
|
|
sv_iplimit = Cvar_Get ("sv_iplimit", "3", 0); // r1ch: limit connections per ip address (stop zombie dos/flood)
|
|
Cvar_SetDescription("sv_iplimit", "Sets connection limit per IP address. Stops zombie DoS/Flood.");
|
|
|
|
sv_reconnect_limit = Cvar_Get ("sv_reconnect_limit", "3", CVAR_ARCHIVE);
|
|
Cvar_SetDescription ("sv_reconnect_limit", "Sets time in seconds before a client can reconnect.");
|
|
|
|
sv_entfile = Cvar_Get ("sv_entfile", "1", CVAR_ARCHIVE); // whether to use .ent file
|
|
Cvar_SetDescription ("sv_entfile", "Enables automatic loading of .ent files to replace BSP entity lump.");
|
|
|
|
SZ_Init (&net_message, net_message_buffer, sizeof(net_message_buffer));
|
|
}
|
|
|
|
/*
|
|
==================
|
|
SV_FinalMessage
|
|
|
|
Used by SV_Shutdown to send a final message to all
|
|
connected clients before the server goes down. The messages are sent immediately,
|
|
not just stuck on the outgoing message list, because the server is going
|
|
to totally exit after returning from this function.
|
|
==================
|
|
*/
|
|
void SV_FinalMessage (char *message, qboolean reconnect)
|
|
{
|
|
int i;
|
|
client_t *cl;
|
|
|
|
SZ_Clear (&net_message);
|
|
MSG_WriteByte (&net_message, svc_print);
|
|
MSG_WriteByte (&net_message, PRINT_HIGH);
|
|
MSG_WriteString (&net_message, message);
|
|
|
|
if (reconnect)
|
|
MSG_WriteByte (&net_message, svc_reconnect);
|
|
else
|
|
MSG_WriteByte (&net_message, svc_disconnect);
|
|
|
|
// send it twice
|
|
// stagger the packets to crutch operating system limited buffers
|
|
|
|
// for (i=0, cl = svs.clients ; i<maxclients->value ; i++, cl++)
|
|
for (i=0, cl = svs.clients ; i<maxclients->integer ; i++, cl++)
|
|
if (cl->state >= cs_connected)
|
|
Netchan_Transmit (&cl->netchan, net_message.cursize
|
|
, net_message.data);
|
|
|
|
// for (i=0, cl = svs.clients ; i<maxclients->value ; i++, cl++)
|
|
for (i=0, cl = svs.clients ; i<maxclients->integer ; i++, cl++)
|
|
if (cl->state >= cs_connected)
|
|
Netchan_Transmit (&cl->netchan, net_message.cursize
|
|
, net_message.data);
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
================
|
|
SV_Shutdown
|
|
|
|
Called when each game quits,
|
|
before Sys_Quit or Sys_Error
|
|
================
|
|
*/
|
|
void SV_Shutdown (char *finalmsg, qboolean reconnect)
|
|
{
|
|
if (svs.clients)
|
|
SV_FinalMessage (finalmsg, reconnect);
|
|
|
|
Master_Shutdown ();
|
|
SV_ShutdownGameProgs ();
|
|
|
|
// free current level
|
|
if (sv.demofile)
|
|
FS_FCloseFile (sv.demofile);
|
|
memset (&sv, 0, sizeof(sv));
|
|
Com_SetServerState (sv.state);
|
|
|
|
// free server static data
|
|
if (svs.clients)
|
|
Z_Free (svs.clients);
|
|
if (svs.client_entities)
|
|
Z_Free (svs.client_entities);
|
|
if (svs.demofile)
|
|
fclose (svs.demofile);
|
|
memset (&svs, 0, sizeof(svs));
|
|
}
|
|
|