/* * Copyright (C) 1997-2001 Id Software, Inc. * * This program 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. * * This program 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 * this program; if not, write to the Free Software Foundation, Inc., 59 * Temple Place - Suite 330, Boston, MA 02111-1307, USA. * */ #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 *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_reconnect_limit; /* minimum seconds between connect messages */ cvar_t *sv_killserver; 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; } drop->state = cs_zombie;/* become free in a few seconds */ drop->name[0] = 0; } /* * * ============================================================================ * == * * 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; Q_strncpyz(status, Cvar_Serverinfo(), sizeof(status)); strncat(status, "\n", sizeof(status) - strlen(status) - 1); statusLength = strlen(status); for (i = 0; i < maxclients->value; 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 = strlen(player); if (statusLength + playerLength >= sizeof(status)) break; /* can't hold any more */ Q_strncpyz(status + statusLength, player, sizeof(status) - statusLength); 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()); } /* * ================ 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) return; /* ignore in single player */ version = atoi(Cmd_Argv(1)); if (version != PROTOCOL_VERSION) Com_sprintf(string, sizeof(string), "%s: wrong version\n", hostname->string, sizeof(string)); else { count = 0; for (i = 0; i < maxclients->value; 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); } 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); } /* * ================== 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; 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); Com_DPrintf(" rejected connect from version %i\n", version); return; } qport = atoi(Cmd_Argv(2)); challenge = atoi(Cmd_Argv(3)); Q_strncpyz(userinfo, Cmd_Argv(4), sizeof(userinfo)); 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++) { 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)) { /* * sku - avoid reusing slot of the client already * connected */ if (cl->state != cs_zombie) { Netchan_OutOfBandPrint(NS_SERVER, adr, "print\nConnected client from this IP is already present.\n"); return; } if (!NET_IsLocalAddress(adr) && (svs.realtime - cl->lastconnect) < ((int)sv_reconnect_limit->value * 1000)) { Com_DPrintf("%s:reconnect rejected : too soon\n", NET_AdrToString(adr)); return; } Com_Printf("%s:reconnect\n", NET_AdrToString(adr)); newcl = cl; goto gotnewcl; } } /* find a client slot */ newcl = NULL; for (i = 0, cl = svs.clients; i < maxclients->value; 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 */ Q_strncpyz(newcl->userinfo, userinfo, sizeof(newcl->userinfo)); SV_UserinfoChanged(newcl); /* send the connect packet to the client */ 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++) { strcat(remaining, Cmd_Argv(i)); strcat(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; 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++) { cl = &svs.clients[i]; if (cl->state != cs_spawned) continue; 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 cl->ping = total / count; /* 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++) { 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++) { 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) 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++) { /* 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) { cl->state = cs_free; /* can now be reused */ continue; } if ((cl->state == cs_connected || cl->state == cs_spawned) && cl->lastmessage < droppoint) { 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) 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) { ge->RunFrame(); /* never get more than one tic behind */ if (sv.time < svs.realtime) { if (sv_showclamp->value) Com_Printf("sv highclamp\n"); svs.realtime = sv.time; } } if (host_speeds->value) 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) { /* never let the time get too far off */ if (sv.time - svs.realtime > 100) { if (sv_showclamp->value) 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 post3.19 change, cvar pointer not validated before * dereferencing */ if (!dedicated || !dedicated->value) return; /* only dedicated servers send heartbeats */ /* * pgm post3.19 change, cvar pointer not validated before * dereferencing */ if (!public_server || !public_server->value) 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) return; /* only dedicated servers send heartbeats */ /* * pgm post3.19 change, cvar pointer not validated before * dereferencing */ if (!public_server || !public_server->value) 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 */ Q_strncpyz(cl->name, Info_ValueForKey(cl->userinfo, "name"), sizeof(cl->name)); /* 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_Get("skill", "1", 0); Cvar_Get("deathmatch", "0", CVAR_LATCH); Cvar_Get("coop", "0", CVAR_LATCH); Cvar_Get("dmflags", va("%i", DF_INSTANT_ITEMS), CVAR_SERVERINFO); Cvar_Get("fraglimit", "0", CVAR_SERVERINFO); Cvar_Get("timelimit", "0", CVAR_SERVERINFO); Cvar_Get("cheats", "0", CVAR_SERVERINFO | CVAR_LATCH); Cvar_Get("protocol", va("%i", PROTOCOL_VERSION), CVAR_SERVERINFO | CVAR_NOSET);; maxclients = Cvar_Get("maxclients", "1", CVAR_SERVERINFO | CVAR_LATCH); hostname = Cvar_Get("hostname", "noname", CVAR_SERVERINFO | CVAR_ARCHIVE); timeout = Cvar_Get("timeout", "125", 0); zombietime = Cvar_Get("zombietime", "2", 0); sv_showclamp = Cvar_Get("showclamp", "0", 0); sv_paused = Cvar_Get("paused", "0", 0); sv_timedemo = Cvar_Get("timedemo", "0", 0); sv_enforcetime = Cvar_Get("sv_enforcetime", "0", 0); allow_download = Cvar_Get("allow_download", "1", CVAR_ARCHIVE); allow_download_players = Cvar_Get("allow_download_players", "0", CVAR_ARCHIVE); allow_download_models = Cvar_Get("allow_download_models", "1", CVAR_ARCHIVE); allow_download_sounds = Cvar_Get("allow_download_sounds", "1", CVAR_ARCHIVE); allow_download_maps = Cvar_Get("allow_download_maps", "1", CVAR_ARCHIVE); sv_noreload = Cvar_Get("sv_noreload", "0", 0); sv_airaccelerate = Cvar_Get("sv_airaccelerate", "0", CVAR_LATCH); public_server = Cvar_Get("public", "0", 0); sv_reconnect_limit = Cvar_Get("sv_reconnect_limit", "3", CVAR_ARCHIVE); sv_killserver = Cvar_Get("sv_killserver", "0", 0); 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++) 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++) 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)); }