]> git.xonotic.org Git - xonotic/darkplaces.git/blobdiff - sv_main.c
Convert \ to / when loading texture from Q3 shader
[xonotic/darkplaces.git] / sv_main.c
index 8ac96a112211392dbb7e15bd9f780dc529e2c0cb..f89c307314260af5083f2abd27a6bc5104d16c1d 100644 (file)
--- a/sv_main.c
+++ b/sv_main.c
@@ -124,6 +124,7 @@ cvar_t sv_gameplayfix_q1bsptracelinereportstexture = {CVAR_SERVER, "sv_gameplayf
 cvar_t sv_gameplayfix_unstickplayers = {CVAR_SERVER, "sv_gameplayfix_unstickplayers", "1", "big hack to try and fix the rare case of MOVETYPE_WALK entities getting stuck in the world clipping hull."};
 cvar_t sv_gameplayfix_unstickentities = {CVAR_SERVER, "sv_gameplayfix_unstickentities", "1", "hack to check if entities are crossing world collision hull and try to move them to the right position"};
 cvar_t sv_gameplayfix_fixedcheckwatertransition = {CVAR_SERVER, "sv_gameplayfix_fixedcheckwatertransition", "1", "fix two very stupid bugs in SV_CheckWaterTransition when watertype is CONTENTS_EMPTY (the bugs causes waterlevel to be 1 on first frame, -1 on second frame - the fix makes it 0 on both frames)"};
+cvar_t sv_gameplayfix_customstats = {CVAR_SERVER, "sv_gameplayfix_customstats", "0", "Disable stats higher than 220, for use by certain games such as Xonotic"};
 cvar_t sv_gravity = {CVAR_SERVER | CVAR_NOTIFY, "sv_gravity","800", "how fast you fall (512 = roughly earth gravity)"};
 cvar_t sv_init_frame_count = {CVAR_SERVER, "sv_init_frame_count", "2", "number of frames to run to allow everything to settle before letting clients connect"};
 cvar_t sv_idealpitchscale = {CVAR_SERVER, "sv_idealpitchscale","0.8", "how much to look up/down slopes and stairs when not using freelook"};
@@ -413,13 +414,10 @@ prvm_required_field_t sv_reqglobals[] =
 #undef PRVM_DECLARE_function
 };
 
-static void Host_Timescale_c(char *string)
+static void Host_Timescale_c(cvar_t *var)
 {
-       double value;
-       value = atof(string);
-
-       if(value < 0.00001 && value != 0)
-               string[0] = '0', string[1] = 0;
+       if(var->value < 0.00001 && var->value != 0)
+               Cvar_SetValueQuick(var, 0);
 }
 
 //============================================================================
@@ -453,14 +451,10 @@ void SV_Init (void)
        Cvar_RegisterVariable (&csqc_progsize);
        Cvar_RegisterVariable (&csqc_usedemoprogs);
 
-       Cmd_AddCommand(&cmd_server, "sv_saveentfile", SV_SaveEntFile_f, "save map entities to .ent file (to allow external editing)");
-       Cmd_AddCommand(&cmd_client, "sv_saveentfile", SV_SaveEntFile_f, "save map entities to .ent file (to allow external editing)");
-       Cmd_AddCommand(&cmd_server, "sv_areastats", SV_AreaStats_f, "prints statistics on entity culling during collision traces");
-       Cmd_AddCommand(&cmd_client, "sv_areastats", SV_AreaStats_f, "prints statistics on entity culling during collision traces");
-       Cmd_AddCommand(&cmd_serverfromclient, "sv_startdownload", SV_StartDownload_f, "begins sending a file to the client (network protocol use only)");
-       Cmd_AddCommand(&cmd_serverfromclient, "download", SV_Download_f, "downloads a specified file from the server");
-       Cmd_AddCommand(&cmd_client, "sv_startdownload", Cmd_ForwardToServer_f, "begins sending a file to the client (network protocol use only)");
-       Cmd_AddCommand(&cmd_client, "download", Cmd_ForwardToServer_f, "downloads a specified file from the server");
+       Cmd_AddCommand(CMD_SHARED, "sv_saveentfile", SV_SaveEntFile_f, "save map entities to .ent file (to allow external editing)");
+       Cmd_AddCommand(CMD_SHARED, "sv_areastats", SV_AreaStats_f, "prints statistics on entity culling during collision traces");
+       Cmd_AddCommand(CMD_CLIENT | CMD_SERVER_FROM_CLIENT, "sv_startdownload", SV_StartDownload_f, "begins sending a file to the client (network protocol use only)");
+       Cmd_AddCommand(CMD_CLIENT | CMD_SERVER_FROM_CLIENT, "download", SV_Download_f, "downloads a specified file from the server");
 
        Cvar_RegisterVariable (&sv_disablenotify);
        Cvar_RegisterVariable (&coop);
@@ -552,6 +546,7 @@ void SV_Init (void)
        Cvar_RegisterVariable (&sv_gameplayfix_unstickplayers);
        Cvar_RegisterVariable (&sv_gameplayfix_unstickentities);
        Cvar_RegisterVariable (&sv_gameplayfix_fixedcheckwatertransition);
+       Cvar_RegisterVariable (&sv_gameplayfix_customstats);
        Cvar_RegisterVariable (&sv_gravity);
        Cvar_RegisterVariable (&sv_init_frame_count);
        Cvar_RegisterVariable (&sv_idealpitchscale);
@@ -628,6 +623,8 @@ void SV_Init (void)
        Cvar_RegisterVariable (&sv_mapformat_is_quake2);
        Cvar_RegisterVariable (&sv_mapformat_is_quake3);
 
+       SV_InitOperatorCommands();
+
        sv_mempool = Mem_AllocPool("server", 0, NULL);
 }
 
@@ -1069,7 +1066,7 @@ void SV_SendServerinfo (client_t *client)
        client->ping = 0;
 
        // allow the client some time to send his keepalives, even if map loading took ages
-       client->netconnection->timeout = realtime + net_connecttimeout.value;
+       client->netconnection->timeout = host.realtime + net_connecttimeout.value;
 }
 
 /*
@@ -1130,7 +1127,7 @@ void SV_ConnectClient (int clientnum, netconn_t *netconnection)
        client->unreliablemsg.maxsize = sizeof(client->unreliablemsg_data);
        // updated by receiving "rate" command from client, this is also the default if not using a DP client
        client->rate = 1000000000;
-       client->connecttime = realtime;
+       client->connecttime = host.realtime;
 
        if (!sv.loadgame)
        {
@@ -1154,6 +1151,126 @@ void SV_ConnectClient (int clientnum, netconn_t *netconnection)
                client->prespawned = client->spawned = client->begun = true;
 }
 
+/*
+=====================
+SV_DropClient
+
+Called when the player is getting totally kicked off the host
+if (crash = true), don't bother sending signofs
+=====================
+*/
+void SV_DropClient(qboolean crash)
+{
+       prvm_prog_t *prog = SVVM_prog;
+       int i;
+       Con_Printf("Client \"%s\" dropped\n", host_client->name);
+
+       SV_StopDemoRecording(host_client);
+
+       // make sure edict is not corrupt (from a level change for example)
+       host_client->edict = PRVM_EDICT_NUM(host_client - svs.clients + 1);
+
+       if (host_client->netconnection)
+       {
+               // tell the client to be gone
+               if (!crash)
+               {
+                       // LadyHavoc: no opportunity for resending, so use unreliable 3 times
+                       unsigned char bufdata[8];
+                       sizebuf_t buf;
+                       memset(&buf, 0, sizeof(buf));
+                       buf.data = bufdata;
+                       buf.maxsize = sizeof(bufdata);
+                       MSG_WriteByte(&buf, svc_disconnect);
+                       NetConn_SendUnreliableMessage(host_client->netconnection, &buf, sv.protocol, 10000, 0, false);
+                       NetConn_SendUnreliableMessage(host_client->netconnection, &buf, sv.protocol, 10000, 0, false);
+                       NetConn_SendUnreliableMessage(host_client->netconnection, &buf, sv.protocol, 10000, 0, false);
+               }
+       }
+
+       // call qc ClientDisconnect function
+       // LadyHavoc: don't call QC if server is dead (avoids recursive
+       // Host_Error in some mods when they run out of edicts)
+       if (host_client->clientconnectcalled && sv.active && host_client->edict)
+       {
+               // call the prog function for removing a client
+               // this will set the body to a dead frame, among other things
+               int saveSelf = PRVM_serverglobaledict(self);
+               host_client->clientconnectcalled = false;
+               PRVM_serverglobalfloat(time) = sv.time;
+               PRVM_serverglobaledict(self) = PRVM_EDICT_TO_PROG(host_client->edict);
+               prog->ExecuteProgram(prog, PRVM_serverfunction(ClientDisconnect), "QC function ClientDisconnect is missing");
+               PRVM_serverglobaledict(self) = saveSelf;
+       }
+
+       if (host_client->netconnection)
+       {
+               // break the net connection
+               NetConn_Close(host_client->netconnection);
+               host_client->netconnection = NULL;
+       }
+
+       // if a download is active, close it
+       if (host_client->download_file)
+       {
+               Con_DPrintf("Download of %s aborted when %s dropped\n", host_client->download_name, host_client->name);
+               FS_Close(host_client->download_file);
+               host_client->download_file = NULL;
+               host_client->download_name[0] = 0;
+               host_client->download_expectedposition = 0;
+               host_client->download_started = false;
+       }
+
+       // remove leaving player from scoreboard
+       host_client->name[0] = 0;
+       host_client->colors = 0;
+       host_client->frags = 0;
+       // send notification to all clients
+       // get number of client manually just to make sure we get it right...
+       i = host_client - svs.clients;
+       MSG_WriteByte (&sv.reliable_datagram, svc_updatename);
+       MSG_WriteByte (&sv.reliable_datagram, i);
+       MSG_WriteString (&sv.reliable_datagram, host_client->name);
+       MSG_WriteByte (&sv.reliable_datagram, svc_updatecolors);
+       MSG_WriteByte (&sv.reliable_datagram, i);
+       MSG_WriteByte (&sv.reliable_datagram, host_client->colors);
+       MSG_WriteByte (&sv.reliable_datagram, svc_updatefrags);
+       MSG_WriteByte (&sv.reliable_datagram, i);
+       MSG_WriteShort (&sv.reliable_datagram, host_client->frags);
+
+       // free the client now
+       if (host_client->entitydatabase)
+               EntityFrame_FreeDatabase(host_client->entitydatabase);
+       if (host_client->entitydatabase4)
+               EntityFrame4_FreeDatabase(host_client->entitydatabase4);
+       if (host_client->entitydatabase5)
+               EntityFrame5_FreeDatabase(host_client->entitydatabase5);
+
+       if (sv.active)
+       {
+               // clear a fields that matter to DP_SV_CLIENTNAME and DP_SV_CLIENTCOLORS, and also frags
+               PRVM_ED_ClearEdict(prog, host_client->edict);
+       }
+
+       // clear the client struct (this sets active to false)
+       memset(host_client, 0, sizeof(*host_client));
+
+       // update server listing on the master because player count changed
+       // (which the master uses for filtering empty/full servers)
+       NetConn_Heartbeat(1);
+
+       if (sv.loadgame)
+       {
+               for (i = 0;i < svs.maxclients;i++)
+                       if (svs.clients[i].active && !svs.clients[i].spawned)
+                               break;
+               if (i == svs.maxclients)
+               {
+                       Con_Printf("Loaded game, everyone rejoined - unpausing\n");
+                       sv.paused = sv.loadgame = false; // we're basically done with loading now
+               }
+       }
+}
 
 /*
 ===============================================================================
@@ -1749,12 +1866,12 @@ static void SV_MarkWriteEntityStateToClient(entity_state_t *s)
                                                        break;
                                        if(eyeindex < sv.writeentitiestoclient_numeyes)
                                                svs.clients[sv.writeentitiestoclient_clientnumber].visibletime[s->number] =
-                                                       realtime + (
+                                                       host.realtime + (
                                                                s->number <= svs.maxclients
                                                                        ? sv_cullentities_trace_delay_players.value
                                                                        : sv_cullentities_trace_delay.value
                                                        );
-                                       else if (realtime > svs.clients[sv.writeentitiestoclient_clientnumber].visibletime[s->number])
+                                       else if (host.realtime > svs.clients[sv.writeentitiestoclient_clientnumber].visibletime[s->number])
                                        {
                                                sv.writeentitiestoclient_stats_culled_trace++;
                                                return;
@@ -2119,47 +2236,50 @@ void SV_WriteClientdataToMessage (client_t *client, prvm_edict_t *ent, sizebuf_t
        //stats[STAT_SECRETS] = PRVM_serverglobalfloat(found_secrets);
        //stats[STAT_MONSTERS] = PRVM_serverglobalfloat(killed_monsters);
 
-       // movement settings for prediction
-       // note: these are not sent in protocols with lower MAX_CL_STATS limits
-       stats[STAT_MOVEFLAGS] = MOVEFLAG_VALID
-               | (sv_gameplayfix_q2airaccelerate.integer ? MOVEFLAG_Q2AIRACCELERATE : 0)
-               | (sv_gameplayfix_nogravityonground.integer ? MOVEFLAG_NOGRAVITYONGROUND : 0)
-               | (sv_gameplayfix_gravityunaffectedbyticrate.integer ? MOVEFLAG_GRAVITYUNAFFECTEDBYTICRATE : 0)
-       ;
-       statsf[STAT_MOVEVARS_TICRATE] = sys_ticrate.value;
-       statsf[STAT_MOVEVARS_TIMESCALE] = host_timescale.value;
-       statsf[STAT_MOVEVARS_GRAVITY] = sv_gravity.value;
-       statsf[STAT_MOVEVARS_STOPSPEED] = sv_stopspeed.value;
-       statsf[STAT_MOVEVARS_MAXSPEED] = sv_maxspeed.value;
-       statsf[STAT_MOVEVARS_SPECTATORMAXSPEED] = sv_maxspeed.value; // FIXME: QW has a separate cvar for this
-       statsf[STAT_MOVEVARS_ACCELERATE] = sv_accelerate.value;
-       statsf[STAT_MOVEVARS_AIRACCELERATE] = sv_airaccelerate.value >= 0 ? sv_airaccelerate.value : sv_accelerate.value;
-       statsf[STAT_MOVEVARS_WATERACCELERATE] = sv_wateraccelerate.value >= 0 ? sv_wateraccelerate.value : sv_accelerate.value;
-       statsf[STAT_MOVEVARS_ENTGRAVITY] = gravity;
-       statsf[STAT_MOVEVARS_JUMPVELOCITY] = sv_jumpvelocity.value;
-       statsf[STAT_MOVEVARS_EDGEFRICTION] = sv_edgefriction.value;
-       statsf[STAT_MOVEVARS_MAXAIRSPEED] = sv_maxairspeed.value;
-       statsf[STAT_MOVEVARS_STEPHEIGHT] = sv_stepheight.value;
-       statsf[STAT_MOVEVARS_AIRACCEL_QW] = sv_airaccel_qw.value;
-       statsf[STAT_MOVEVARS_AIRACCEL_QW_STRETCHFACTOR] = sv_airaccel_qw_stretchfactor.value;
-       statsf[STAT_MOVEVARS_AIRACCEL_SIDEWAYS_FRICTION] = sv_airaccel_sideways_friction.value;
-       statsf[STAT_MOVEVARS_FRICTION] = sv_friction.value;
-       statsf[STAT_MOVEVARS_WATERFRICTION] = sv_waterfriction.value >= 0 ? sv_waterfriction.value : sv_friction.value;
-       statsf[STAT_MOVEVARS_AIRSTOPACCELERATE] = sv_airstopaccelerate.value;
-       statsf[STAT_MOVEVARS_AIRSTRAFEACCELERATE] = sv_airstrafeaccelerate.value;
-       statsf[STAT_MOVEVARS_MAXAIRSTRAFESPEED] = sv_maxairstrafespeed.value;
-       statsf[STAT_MOVEVARS_AIRSTRAFEACCEL_QW] = sv_airstrafeaccel_qw.value;
-       statsf[STAT_MOVEVARS_AIRCONTROL] = sv_aircontrol.value;
-       statsf[STAT_MOVEVARS_AIRCONTROL_POWER] = sv_aircontrol_power.value;
-       statsf[STAT_MOVEVARS_AIRCONTROL_PENALTY] = sv_aircontrol_penalty.value;
-       statsf[STAT_MOVEVARS_WARSOWBUNNY_AIRFORWARDACCEL] = sv_warsowbunny_airforwardaccel.value;
-       statsf[STAT_MOVEVARS_WARSOWBUNNY_ACCEL] = sv_warsowbunny_accel.value;
-       statsf[STAT_MOVEVARS_WARSOWBUNNY_TOPSPEED] = sv_warsowbunny_topspeed.value;
-       statsf[STAT_MOVEVARS_WARSOWBUNNY_TURNACCEL] = sv_warsowbunny_turnaccel.value;
-       statsf[STAT_MOVEVARS_WARSOWBUNNY_BACKTOSIDERATIO] = sv_warsowbunny_backtosideratio.value;
-       statsf[STAT_MOVEVARS_AIRSPEEDLIMIT_NONQW] = sv_airspeedlimit_nonqw.value;
-       statsf[STAT_FRAGLIMIT] = fraglimit.value;
-       statsf[STAT_TIMELIMIT] = timelimit.value;
+       if(!sv_gameplayfix_customstats.integer)
+       {
+               statsf[STAT_MOVEVARS_AIRACCEL_QW_STRETCHFACTOR] = sv_airaccel_qw_stretchfactor.value;
+               statsf[STAT_MOVEVARS_AIRCONTROL_PENALTY] = sv_aircontrol_penalty.value;
+               statsf[STAT_MOVEVARS_AIRSPEEDLIMIT_NONQW] = sv_airspeedlimit_nonqw.value;               
+               statsf[STAT_MOVEVARS_AIRSTRAFEACCEL_QW] = sv_airstrafeaccel_qw.value;
+               statsf[STAT_MOVEVARS_AIRCONTROL_POWER] = sv_aircontrol_power.value;
+               // movement settings for prediction
+               // note: these are not sent in protocols with lower MAX_CL_STATS limits
+               stats[STAT_MOVEFLAGS] = MOVEFLAG_VALID
+                       | (sv_gameplayfix_q2airaccelerate.integer ? MOVEFLAG_Q2AIRACCELERATE : 0)
+                       | (sv_gameplayfix_nogravityonground.integer ? MOVEFLAG_NOGRAVITYONGROUND : 0)
+                       | (sv_gameplayfix_gravityunaffectedbyticrate.integer ? MOVEFLAG_GRAVITYUNAFFECTEDBYTICRATE : 0)
+               ;
+               statsf[STAT_MOVEVARS_WARSOWBUNNY_AIRFORWARDACCEL] = sv_warsowbunny_airforwardaccel.value;
+               statsf[STAT_MOVEVARS_WARSOWBUNNY_ACCEL] = sv_warsowbunny_accel.value;
+               statsf[STAT_MOVEVARS_WARSOWBUNNY_TOPSPEED] = sv_warsowbunny_topspeed.value;
+               statsf[STAT_MOVEVARS_WARSOWBUNNY_TURNACCEL] = sv_warsowbunny_turnaccel.value;
+               statsf[STAT_MOVEVARS_WARSOWBUNNY_BACKTOSIDERATIO] = sv_warsowbunny_backtosideratio.value;
+               statsf[STAT_MOVEVARS_AIRSTOPACCELERATE] = sv_airstopaccelerate.value;
+               statsf[STAT_MOVEVARS_AIRSTRAFEACCELERATE] = sv_airstrafeaccelerate.value;
+               statsf[STAT_MOVEVARS_MAXAIRSTRAFESPEED] = sv_maxairstrafespeed.value;
+               statsf[STAT_MOVEVARS_AIRCONTROL] = sv_aircontrol.value;
+               statsf[STAT_FRAGLIMIT] = fraglimit.value;
+               statsf[STAT_TIMELIMIT] = timelimit.value;
+               statsf[STAT_MOVEVARS_FRICTION] = sv_friction.value;     
+               statsf[STAT_MOVEVARS_WATERFRICTION] = sv_waterfriction.value >= 0 ? sv_waterfriction.value : sv_friction.value;
+               statsf[STAT_MOVEVARS_TICRATE] = sys_ticrate.value;
+               statsf[STAT_MOVEVARS_TIMESCALE] = host_timescale.value;
+               statsf[STAT_MOVEVARS_GRAVITY] = sv_gravity.value;
+               statsf[STAT_MOVEVARS_STOPSPEED] = sv_stopspeed.value;
+               statsf[STAT_MOVEVARS_MAXSPEED] = sv_maxspeed.value;
+               statsf[STAT_MOVEVARS_SPECTATORMAXSPEED] = sv_maxspeed.value; // FIXME: QW has a separate cvar for this
+               statsf[STAT_MOVEVARS_ACCELERATE] = sv_accelerate.value;
+               statsf[STAT_MOVEVARS_AIRACCELERATE] = sv_airaccelerate.value >= 0 ? sv_airaccelerate.value : sv_accelerate.value;
+               statsf[STAT_MOVEVARS_WATERACCELERATE] = sv_wateraccelerate.value >= 0 ? sv_wateraccelerate.value : sv_accelerate.value;
+               statsf[STAT_MOVEVARS_ENTGRAVITY] = gravity;
+               statsf[STAT_MOVEVARS_JUMPVELOCITY] = sv_jumpvelocity.value;
+               statsf[STAT_MOVEVARS_EDGEFRICTION] = sv_edgefriction.value;
+               statsf[STAT_MOVEVARS_MAXAIRSPEED] = sv_maxairspeed.value;
+               statsf[STAT_MOVEVARS_STEPHEIGHT] = sv_stepheight.value;
+               statsf[STAT_MOVEVARS_AIRACCEL_QW] = sv_airaccel_qw.value;
+               statsf[STAT_MOVEVARS_AIRACCEL_SIDEWAYS_FRICTION] = sv_airaccel_sideways_friction.value;
+       }
 
        if (sv.protocol == PROTOCOL_QUAKE || sv.protocol == PROTOCOL_QUAKEDP || sv.protocol == PROTOCOL_NEHAHRAMOVIE || sv.protocol == PROTOCOL_NEHAHRABJP || sv.protocol == PROTOCOL_NEHAHRABJP2 || sv.protocol == PROTOCOL_NEHAHRABJP3 || sv.protocol == PROTOCOL_DARKPLACES1 || sv.protocol == PROTOCOL_DARKPLACES2 || sv.protocol == PROTOCOL_DARKPLACES3 || sv.protocol == PROTOCOL_DARKPLACES4 || sv.protocol == PROTOCOL_DARKPLACES5)
        {
@@ -2393,7 +2513,7 @@ static void SV_SendClientDatagram (client_t *client)
                timedelta *= 1 - net_burstreserve.value;
 
                // only try to use excess time
-               timedelta = bound(0, realtime - host_client->netconnection->cleartime, timedelta);
+               timedelta = bound(0, host.realtime - host_client->netconnection->cleartime, timedelta);
 
                // but we know next packet will be in sys_ticrate, so we can use up THAT bandwidth
                timedelta += sys_ticrate.value;
@@ -2453,12 +2573,12 @@ static void SV_SendClientDatagram (client_t *client)
                // now write as many entities as we can fit, and also sends stats
                SV_WriteEntitiesToClient (client, client->edict, &msg, maxsize);
        }
-       else if (realtime > client->keepalivetime)
+       else if (host.realtime > client->keepalivetime)
        {
                // the player isn't totally in the game yet
                // send small keepalive messages if too much time has passed
                // (may also be sending downloads)
-               client->keepalivetime = realtime + 5;
+               client->keepalivetime = host.realtime + 5;
                MSG_WriteChar (&msg, svc_nop);
        }
 
@@ -2724,7 +2844,7 @@ static void SV_Download_f(cmd_state_t *cmd)
        {
                // at this point we'll assume the previous download should be aborted
                Con_DPrintf("Download of %s aborted by %s starting a new download\n", host_client->download_name, host_client->name);
-               Host_ClientCommands("\nstopdownload\n");
+               SV_ClientCommands("\nstopdownload\n");
 
                // close the file and reset variables
                FS_Close(host_client->download_file);
@@ -2739,7 +2859,7 @@ static void SV_Download_f(cmd_state_t *cmd)
        if (!sv_allowdownloads.integer && !is_csqc)
        {
                SV_ClientPrintf("Downloads are disabled on this server\n");
-               Host_ClientCommands("\nstopdownload\n");
+               SV_ClientCommands("\nstopdownload\n");
                return;
        }
 
@@ -2768,7 +2888,7 @@ static void SV_Download_f(cmd_state_t *cmd)
                        host_client->download_file = FS_FileFromData(svs.csqc_progdata, sv.csqc_progsize, true);
                
                // no, no space is needed between %s and %s :P
-               Host_ClientCommands("\ncl_downloadbegin %i %s%s\n", (int)FS_FileSize(host_client->download_file), host_client->download_name, extensions);
+               SV_ClientCommands("\ncl_downloadbegin %i %s%s\n", (int)FS_FileSize(host_client->download_file), host_client->download_name, extensions);
 
                host_client->download_expectedposition = 0;
                host_client->download_started = false;
@@ -2779,7 +2899,7 @@ static void SV_Download_f(cmd_state_t *cmd)
        if (!FS_FileExists(host_client->download_name))
        {
                SV_ClientPrintf("Download rejected: server does not have the file \"%s\"\nYou may need to separately download or purchase the data archives for this game/mod to get this file\n", host_client->download_name);
-               Host_ClientCommands("\nstopdownload\n");
+               SV_ClientCommands("\nstopdownload\n");
                return;
        }
 
@@ -2789,7 +2909,7 @@ static void SV_Download_f(cmd_state_t *cmd)
        if ((whichpack && whichpack2 && !strcasecmp(whichpack, whichpack2)) || FS_IsRegisteredQuakePack(host_client->download_name))
        {
                SV_ClientPrintf("Download rejected: file \"%s\" is part of registered Quake(r)\nYou must purchase Quake(r) from id Software or a retailer to get this file\nPlease go to http://www.idsoftware.com/games/quake/quake/index.php?game_section=buy\n", host_client->download_name);
-               Host_ClientCommands("\nstopdownload\n");
+               SV_ClientCommands("\nstopdownload\n");
                return;
        }
 
@@ -2800,7 +2920,7 @@ static void SV_Download_f(cmd_state_t *cmd)
                if (whichpack)
                {
                        SV_ClientPrintf("Download rejected: file \"%s\" is in an archive (\"%s\")\nYou must separately download or purchase the data archives for this game/mod to get this file\n", host_client->download_name, whichpack);
-                       Host_ClientCommands("\nstopdownload\n");
+                       SV_ClientCommands("\nstopdownload\n");
                        return;
                }
        }
@@ -2810,7 +2930,7 @@ static void SV_Download_f(cmd_state_t *cmd)
                if (!strcasecmp(extension, "cfg"))
                {
                        SV_ClientPrintf("Download rejected: file \"%s\" is a .cfg file which is forbidden for security reasons\nYou must separately download or purchase the data archives for this game/mod to get this file\n", host_client->download_name);
-                       Host_ClientCommands("\nstopdownload\n");
+                       SV_ClientCommands("\nstopdownload\n");
                        return;
                }
        }
@@ -2820,7 +2940,7 @@ static void SV_Download_f(cmd_state_t *cmd)
                if (!strncasecmp(host_client->download_name, "dlcache/", 8))
                {
                        SV_ClientPrintf("Download rejected: file \"%s\" is in the dlcache/ directory which is forbidden for security reasons\nYou must separately download or purchase the data archives for this game/mod to get this file\n", host_client->download_name);
-                       Host_ClientCommands("\nstopdownload\n");
+                       SV_ClientCommands("\nstopdownload\n");
                        return;
                }
        }
@@ -2830,7 +2950,7 @@ static void SV_Download_f(cmd_state_t *cmd)
                if (!strcasecmp(extension, "pak") || !strcasecmp(extension, "pk3"))
                {
                        SV_ClientPrintf("Download rejected: file \"%s\" is an archive\nYou must separately download or purchase the data archives for this game/mod to get this file\n", host_client->download_name);
-                       Host_ClientCommands("\nstopdownload\n");
+                       SV_ClientCommands("\nstopdownload\n");
                        return;
                }
        }
@@ -2839,14 +2959,14 @@ static void SV_Download_f(cmd_state_t *cmd)
        if (!host_client->download_file)
        {
                SV_ClientPrintf("Download rejected: server could not open the file \"%s\"\n", host_client->download_name);
-               Host_ClientCommands("\nstopdownload\n");
+               SV_ClientCommands("\nstopdownload\n");
                return;
        }
 
        if (FS_FileSize(host_client->download_file) > 1<<30)
        {
                SV_ClientPrintf("Download rejected: file \"%s\" is very large\n", host_client->download_name);
-               Host_ClientCommands("\nstopdownload\n");
+               SV_ClientCommands("\nstopdownload\n");
                FS_Close(host_client->download_file);
                host_client->download_file = NULL;
                return;
@@ -2855,7 +2975,7 @@ static void SV_Download_f(cmd_state_t *cmd)
        if (FS_FileSize(host_client->download_file) < 0)
        {
                SV_ClientPrintf("Download rejected: file \"%s\" is not a regular file\n", host_client->download_name);
-               Host_ClientCommands("\nstopdownload\n");
+               SV_ClientCommands("\nstopdownload\n");
                FS_Close(host_client->download_file);
                host_client->download_file = NULL;
                return;
@@ -2874,10 +2994,10 @@ static void SV_Download_f(cmd_state_t *cmd)
                        strlcat(extensions, " deflate", sizeof(extensions));
 
                // no, no space is needed between %s and %s :P
-               Host_ClientCommands("\ncl_downloadbegin %i %s%s\n", (int)FS_FileSize(host_client->download_file), host_client->download_name, extensions);
+               SV_ClientCommands("\ncl_downloadbegin %i %s%s\n", (int)FS_FileSize(host_client->download_file), host_client->download_name, extensions);
        }
        */
-       Host_ClientCommands("\ncl_downloadbegin %i %s\n", (int)FS_FileSize(host_client->download_file), host_client->download_name);
+       SV_ClientCommands("\ncl_downloadbegin %i %s\n", (int)FS_FileSize(host_client->download_file), host_client->download_name);
 
        host_client->download_expectedposition = 0;
        host_client->download_started = false;
@@ -3320,6 +3440,15 @@ void SV_SpawnServer (const char *server)
 
        if(sv.active)
        {
+               client_t *client;
+               for (i = 0, client = svs.clients;i < svs.maxclients;i++, client++)
+               {
+                       if (client->netconnection)
+                       {
+                               MSG_WriteByte(&client->netconnection->message, svc_stufftext);
+                               MSG_WriteString(&client->netconnection->message, "reconnect\n");
+                       }
+               }
                World_End(&sv.world);
                if(PRVM_serverfunction(SV_Shutdown))
                {
@@ -3362,23 +3491,8 @@ void SV_SpawnServer (const char *server)
 //
 // tell all connected clients that we are going to a new level
 //
-       if (sv.active)
-       {
-               client_t *client;
-               for (i = 0, client = svs.clients;i < svs.maxclients;i++, client++)
-               {
-                       if (client->netconnection)
-                       {
-                               MSG_WriteByte(&client->netconnection->message, svc_stufftext);
-                               MSG_WriteString(&client->netconnection->message, "reconnect\n");
-                       }
-               }
-       }
-       else
-       {
-               // open server port
+       if (!sv.active)
                NetConn_OpenServerPorts(true);
-       }
 
 //
 // make cvars consistant
@@ -3543,7 +3657,7 @@ void SV_SpawnServer (const char *server)
        }
 
        // Once all init frames have been run, we consider svqc code fully initialized.
-       prog->inittime = realtime;
+       prog->inittime = host.realtime;
 
        if (cls.state == ca_dedicated)
                Mod_PurgeUnused();
@@ -3595,6 +3709,54 @@ void SV_SpawnServer (const char *server)
 //     SV_UnlockThreadMutex();
 }
 
+/*
+==================
+SV_Shutdown
+
+This only happens at the end of a game, not between levels
+==================
+*/
+void SV_Shutdown(void)
+{
+       prvm_prog_t *prog = SVVM_prog;
+       int i;
+
+       Con_DPrintf("SV_Shutdown\n");
+
+       if (!sv.active)
+               return;
+
+       NetConn_Heartbeat(2);
+       NetConn_Heartbeat(2);
+
+// make sure all the clients know we're disconnecting
+       World_End(&sv.world);
+       if(prog->loaded)
+       {
+               if(PRVM_serverfunction(SV_Shutdown))
+               {
+                       func_t s = PRVM_serverfunction(SV_Shutdown);
+                       PRVM_serverglobalfloat(time) = sv.time;
+                       PRVM_serverfunction(SV_Shutdown) = 0; // prevent it from getting called again
+                       prog->ExecuteProgram(prog, s,"SV_Shutdown() required");
+               }
+       }
+       for (i = 0, host_client = svs.clients;i < svs.maxclients;i++, host_client++)
+               if (host_client->active)
+                       SV_DropClient(false); // server shutdown
+
+       NetConn_CloseServerPorts();
+
+       sv.active = false;
+//
+// clear structures
+//
+       memset(&sv, 0, sizeof(sv));
+       memset(svs.clients, 0, svs.maxclients*sizeof(client_t));
+
+       cl.islocalgame = false;
+}
+
 /////////////////////////////////////////////////////
 // SV VM stuff