]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blobdiff - qcsrc/server/world.qc
Use correct droptofloor distance for each Quake map format
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / world.qc
index 5801fae88a3eb2fbd06d091e81bed09b779c38d4..cfaec58e38cd5dc17413cfa28b1d5d497d5c2d34 100644 (file)
@@ -22,6 +22,7 @@
 #include <common/util.qh>
 #include <common/vehicles/all.qh>
 #include <common/weapons/_all.qh>
+#include <lib/warpzone/common.qh>
 #include <server/anticheat.qh>
 #include <server/antilag.qh>
 #include <server/bot/api.qh>
@@ -35,7 +36,6 @@
 #include <server/damage.qh>
 #include <server/gamelog.qh>
 #include <server/hook.qh>
-#include <server/intermission.qh>
 #include <server/ipban.qh>
 #include <server/items/items.qh>
 #include <server/main.qh>
@@ -46,7 +46,6 @@
 #include <server/scores_rules.qh>
 #include <server/spawnpoints.qh>
 #include <server/teamplay.qh>
-#include <server/weapons/common.qh>
 #include <server/weapons/weaponstats.qh>
 
 const float LATENCY_THINKRATE = 10;
@@ -279,7 +278,6 @@ void cvar_changes_init()
                BADCVAR("g_duel_not_dm_maps");
                BADCVAR("g_freezetag");
                BADCVAR("g_freezetag_teams");
-               BADCVAR("g_invasion_teams");
                BADCVAR("g_invasion_type");
                BADCVAR("g_jailbreak");
                BADCVAR("g_jailbreak_teams");
@@ -306,6 +304,7 @@ void cvar_changes_init()
                BADCVAR("nextmap");
                BADCVAR("teamplay");
                BADCVAR("timelimit");
+               BADCVAR("g_mapinfo_q3compat");
                BADCVAR("g_mapinfo_settemp_acl");
                BADCVAR("g_mapinfo_ignore_warnings");
                BADCVAR("g_maplist_ignore_sizes");
@@ -316,6 +315,7 @@ void cvar_changes_init()
                BADCVAR("g_maplist");
                BADCVAR("g_maplist_mostrecent");
                BADCVAR("sv_motd");
+               BADCVAR("sv_termsofservice_url");
 
                v = cvar_string(k);
                d = cvar_defstring(k);
@@ -337,7 +337,6 @@ void cvar_changes_init()
                // does nothing gameplay relevant
                BADCVAR("captureleadlimit_override");
                BADCVAR("condump_stripcolors");
-               BADCVAR("gameversion");
                BADCVAR("fs_gamedir");
                BADCVAR("g_allow_oldvortexbeam");
                BADCVAR("g_balance_kill_delay");
@@ -397,7 +396,7 @@ void cvar_changes_init()
                BADCVAR("w_prop_interval");
                BADPREFIX("chat_");
                BADPREFIX("crypto_");
-               BADPREFIX("gameversion_");
+               BADPREFIX("gameversion");
                BADPREFIX("g_chat_");
                BADPREFIX("g_ctf_captimerecord_");
                BADPREFIX("g_hats_");
@@ -416,6 +415,7 @@ void cvar_changes_init()
                BADPREFIX("sv_timeout_");
                BADPREFIX("sv_vote_");
                BADPREFIX("timelimit_");
+               BADPRESUFFIX("g_", "_round_timelimit");
 
                // allowed changes to server admins (please sync this to server.cfg)
                // vi commands:
@@ -469,7 +469,6 @@ void cvar_changes_init()
                BADCVAR("g_start_delay");
                BADCVAR("g_superspectate");
                BADCVAR("g_tdm_teams_override");
-               BADCVAR("g_warmup");
                BADCVAR("g_weapon_stay"); BADPRESUFFIX("g_", "_weapon_stay");
                BADCVAR("hostname");
                BADCVAR("log_file");
@@ -495,6 +494,7 @@ void cvar_changes_init()
                BADCVAR("sv_motd");
                BADCVAR("sv_public");
                BADCVAR("sv_showfps");
+               BADCVAR("sv_showspectators");
                BADCVAR("sv_status_privacy");
                BADCVAR("sv_taunt");
                BADCVAR("sv_vote_call");
@@ -508,7 +508,7 @@ void cvar_changes_init()
                BADVALUE("sys_ticrate", "0.0333333");
                BADCVAR("teamplay_mode");
                BADCVAR("timelimit_override");
-               BADPREFIX("g_warmup_");
+               BADPREFIX("g_warmup");
                BADPREFIX("sv_info_");
                BADPREFIX("sv_ready_restart_");
 
@@ -519,13 +519,6 @@ void cvar_changes_init()
                BADCVAR("g_grappling_hook");
                BADCVAR("g_jetpack");
 
-               // temporary for testing
-               // TODO remove before 0.8.3 release
-               BADCVAR("g_ca_weaponarena");
-               BADCVAR("g_freezetag_weaponarena");
-               BADCVAR("g_lms_weaponarena");
-               BADCVAR("g_ctf_stalemate_time");
-
 #undef BADPRESUFFIX
 #undef BADPREFIX
 #undef BADCVAR
@@ -598,15 +591,17 @@ spawnfunc(__init_dedicated_server)
 
        e = new(info_player_deathmatch);  // safeguard against player joining
 
-    // assign reflectively to avoid "assignment to world" warning
-    for (int i = 0, n = numentityfields(); i < n; ++i) {
-        string k = entityfieldname(i);
-        if (k == "classname") {
-            // safeguard against various stuff ;)
-            putentityfieldstring(i, this, "worldspawn");
-            break;
-        }
-    }
+       // assign reflectively to avoid "assignment to world" warning
+       for (int i = 0, n = numentityfields(); i < n; ++i)
+       {
+               string k = entityfieldname(i);
+               if (k == "classname")
+               {
+                       // safeguard against various stuff ;)
+                       putentityfieldstring(i, this, "worldspawn");
+                       break;
+               }
+       }
 
        // needs to be done so early because of the constants they create
        static_init();
@@ -633,8 +628,42 @@ STATIC_INIT_EARLY(maxclients)
 
 void GameplayMode_DelayedInit(entity this)
 {
+       // at this stage team entities are spawned, teamplay contains the number of them
+
        if(!scores_initialized)
                ScoreRules_generic();
+
+       if (warmup_stage >= 0 && autocvar_g_maxplayers >= 0)
+               return;
+       if (!g_duel)
+               MapReadSizes(mapname);
+
+       if (autocvar_g_maxplayers < 0 && teamplay)
+       {
+               // automatic maxplayers should be a multiple of team count
+               if (map_maxplayers == 0 || map_maxplayers > maxclients)
+                       map_maxplayers = maxclients; // unlimited, but may need rounding
+               int d = map_maxplayers % AVAILABLE_TEAMS;
+               int u = AVAILABLE_TEAMS - d;
+               map_maxplayers += (u <= d && u + map_maxplayers <= maxclients) ? u : -d;
+       }
+
+       if (warmup_stage < 0)
+       {
+               int m = GetPlayerLimit();
+               if (m <= 0) m = maxclients;
+               map_minplayers = bound(max(2, AVAILABLE_TEAMS * 2), map_minplayers, m);
+               if (teamplay)
+               {
+                       // automatic minplayers should be a multiple of team count
+                       int d = map_minplayers % AVAILABLE_TEAMS;
+                       int u = AVAILABLE_TEAMS - d;
+                       map_minplayers += (u < d && u + map_minplayers <= m) ? u : -d;
+               }
+               warmup_limit = -1;
+       }
+       else
+               map_minplayers = 0; // don't display a minimum if it's not used
 }
 
 void InitGameplayMode()
@@ -644,12 +673,15 @@ void InitGameplayMode()
        // find out good world mins/maxs bounds, either the static bounds found by looking for solid, or the mapinfo specified bounds
        get_mi_min_max(1);
        // assign reflectively to avoid "assignment to world" warning
-       int done = 0; for (int i = 0, n = numentityfields(); i < n; ++i) {
-           string k = entityfieldname(i); vector v = (k == "mins") ? mi_min : (k == "maxs") ? mi_max : '0 0 0';
-           if (v) {
-            putentityfieldstring(i, world, sprintf("%v", v));
-            if (++done == 2) break;
-        }
+       for (int i = 0, done = 0, n = numentityfields(); i < n; ++i)
+       {
+               string k = entityfieldname(i);
+               vector v = (k == "mins") ? mi_min : (k == "maxs") ? mi_max : '0 0 0';
+               if (v)
+               {
+                       putentityfieldstring(i, world, sprintf("%v", v));
+                       if (++done == 2) break;
+               }
        }
        // currently, NetRadiant's limit is 131072 qu for each side
        // distance from one corner of a 131072qu cube to the opposite corner is approx. 227023 qu
@@ -687,6 +719,7 @@ void InitGameplayMode()
 bool world_already_spawned;
 spawnfunc(worldspawn)
 {
+       cvar_set("_endmatch", "0");
        server_is_dedicated = boolean(stof(cvar_defstring("is_dedicated")));
 
        if (autocvar_sv_termsofservice_url && autocvar_sv_termsofservice_url != "")
@@ -820,11 +853,11 @@ spawnfunc(worldspawn)
 
        if(autocvar_g_campaign)
                CampaignPreInit();
+       else
+               PlayerStats_GameReport_Init(); // we need this to be initiated before InitGameplayMode
 
        Map_MarkAsRecent(mapname);
 
-       PlayerStats_GameReport_Init(); // we need this to be initiated before InitGameplayMode
-
        InitGameplayMode();
        static_init_late();
        static_init_precache();
@@ -833,7 +866,7 @@ spawnfunc(worldspawn)
        GameRules_limit_fallbacks();
 
        if(warmup_limit == 0)
-               warmup_limit = (autocvar_timelimit > 0) ? autocvar_timelimit * 60 : autocvar_timelimit;
+               warmup_limit = autocvar_timelimit * 60;
 
        player_count = 0;
        bot_waypoints_for_items = autocvar_g_waypoints_for_items;
@@ -843,46 +876,15 @@ spawnfunc(worldspawn)
 
        WaypointSprite_Init();
 
-       GameLogInit(); // prepare everything
        // NOTE for matchid:
        // changing the logic generating it is okay. But:
        // it HAS to stay <= 64 chars
        // character set: ASCII 33-126 without the following characters: : ; ' " \ $
-       if(autocvar_sv_eventlog)
-       {
-               string num = strftime_s(); // strftime(false, "%s") isn't reliable, see strftime_s description
-               string s = sprintf("%s.%s.%06d", itos(autocvar_sv_eventlog_files_counter), num, floor(random() * 1000000));
-               matchid = strzone(s);
-
-               GameLogEcho(strcat(":gamestart:", GetGametype(), "_", GetMapname(), ":", s));
-               s = ":gameinfo:mutators:LIST";
-
-               MUTATOR_CALLHOOK(BuildMutatorsString, s);
-               s = M_ARGV(0, string);
-
-               // initialiation stuff, not good in the mutator system
-               if(!autocvar_g_use_ammunition)
-                       s = strcat(s, ":no_use_ammunition");
-
-               // initialiation stuff, not good in the mutator system
-               if(autocvar_g_pickup_items == 0)
-                       s = strcat(s, ":no_pickup_items");
-               if(autocvar_g_pickup_items > 0)
-                       s = strcat(s, ":pickup_items");
-
-               // initialiation stuff, not good in the mutator system
-               if(autocvar_g_weaponarena != "0")
-                       s = strcat(s, ":", autocvar_g_weaponarena, " arena");
+       // strftime(false, "%s") isn't reliable, see strftime_s description
+       matchid = strzone(sprintf("%d.%s.%06d", autocvar_sv_eventlog_files_counter, strftime_s(), random() * 1000000));
 
-               // TODO to mutator system
-               if(autocvar_g_norecoil)
-                       s = strcat(s, ":norecoil");
-
-               GameLogEcho(s);
-               GameLogEcho(":gameinfo:end");
-       }
-       else
-               matchid = strzone(ftos(random()));
+       if(autocvar_sv_eventlog)
+               GameLogInit(); // requires matchid to be set
 
        cvar_set("nextmap", "");
 
@@ -896,8 +898,22 @@ spawnfunc(worldspawn)
        MapInfo_Enumerate();
        MapInfo_FilterGametype(MapInfo_CurrentGametype(), MapInfo_CurrentFeatures(), MapInfo_RequiredFlags(), MapInfo_ForbiddenFlags(), 1);
 
-       q3compat = BITSET(q3compat, Q3COMPAT_ARENA, fexists(strcat("scripts/", mapname, ".arena")));
-       q3compat = BITSET(q3compat, Q3COMPAT_DEFI, fexists(strcat("scripts/", mapname, ".defi")));
+       q3compat = BITSET(q3compat, Q3COMPAT_ARENA, _MapInfo_FindArenaFile(mapname, ".arena") != "");
+       q3compat = BITSET(q3compat, Q3COMPAT_DEFI, _MapInfo_FindArenaFile(mapname, ".defi") != "");
+
+       // quake 3 music support
+       if(world.music || world.noise)
+       {
+               // prefer .music over .noise
+               string chosen_music;
+               if(world.music)
+                       chosen_music = world.music;
+               else
+                       chosen_music = world.noise;
+
+               string newstuff = strcat(clientstuff, "cd loop \"", chosen_music, "\"\n");
+               strcpy(clientstuff, newstuff);
+       }
 
        if(whichpack(strcat("maps/", mapname, ".cfg")) != "")
        {
@@ -1032,72 +1048,80 @@ spawnfunc(light)
        delete(this);
 }
 
-bool MoveToRandomLocationWithinBounds(entity e, vector boundmin, vector boundmax, float goodcontents, float badcontents, float badsurfaceflags, int attempts, float maxaboveground, float minviewdistance)
+bool MoveToRandomLocationWithinBounds(entity e, vector boundmin, vector boundmax, float goodcontents, float badcontents, float badsurfaceflags, int attempts, float maxaboveground, float minviewdistance, bool frompos)
 {
-    float m = e.dphitcontentsmask;
-    e.dphitcontentsmask = goodcontents | badcontents;
-
-    vector org = boundmin;
-    vector delta = boundmax - boundmin;
-
-    vector start, end;
-    start = end = org;
-    int j; // used after the loop
-    for(j = 0; j < attempts; ++j)
-    {
-        start.x = org.x + random() * delta.x;
-        start.y = org.y + random() * delta.y;
-        start.z = org.z + random() * delta.z;
-
-        // rule 1: start inside world bounds, and outside
-        // solid, and don't start from somewhere where you can
-        // fall down to evil
-        tracebox(start, e.mins, e.maxs, start - '0 0 1' * delta.z, MOVE_NORMAL, e);
-        if (trace_fraction >= 1)
-            continue;
-        if (trace_startsolid)
-            continue;
-        if (trace_dphitcontents & badcontents)
-            continue;
-        if (trace_dphitq3surfaceflags & badsurfaceflags)
-            continue;
-
-        // rule 2: if we are too high, lower the point
-        if (trace_fraction * delta.z > maxaboveground)
-            start = trace_endpos + '0 0 1' * maxaboveground;
-        vector enddown = trace_endpos;
-
-        // rule 3: make sure we aren't outside the map. This only works
-        // for somewhat well formed maps. A good rule of thumb is that
-        // the map should have a convex outside hull.
-        // these can be traceLINES as we already verified the starting box
-        vector mstart = start + 0.5 * (e.mins + e.maxs);
-        traceline(mstart, mstart + '1 0 0' * delta.x, MOVE_NORMAL, e);
-        if (trace_fraction >= 1 || trace_dphittexturename == "common/caulk")
-            continue;
-        traceline(mstart, mstart - '1 0 0' * delta.x, MOVE_NORMAL, e);
-        if (trace_fraction >= 1 || trace_dphittexturename == "common/caulk")
-            continue;
-        traceline(mstart, mstart + '0 1 0' * delta.y, MOVE_NORMAL, e);
-        if (trace_fraction >= 1 || trace_dphittexturename == "common/caulk")
-            continue;
-        traceline(mstart, mstart - '0 1 0' * delta.y, MOVE_NORMAL, e);
-        if (trace_fraction >= 1 || trace_dphittexturename == "common/caulk")
-            continue;
-        traceline(mstart, mstart + '0 0 1' * delta.z, MOVE_NORMAL, e);
-        if (trace_fraction >= 1 || trace_dphittexturename == "common/caulk")
-            continue;
+       float m = e.dphitcontentsmask;
+       e.dphitcontentsmask = goodcontents | badcontents;
+
+       vector org = boundmin;
+       vector delta = boundmax - boundmin;
+
+       vector start, end;
+       start = end = org;
+       int j; // used after the loop
+       for(j = 0; j < attempts; ++j)
+       {
+               start.x = org.x + random() * delta.x;
+               start.y = org.y + random() * delta.y;
+               start.z = org.z + random() * delta.z;
+
+               // rule 1: start inside world bounds, and outside
+               // solid, and don't start from somewhere where you can
+               // fall down to evil
+               tracebox(start, e.mins, e.maxs, start - '0 0 1' * delta.z, MOVE_NORMAL, e);
+               if (trace_fraction >= 1)
+                       continue;
+               if (trace_startsolid)
+                       continue;
+               if (trace_dphitcontents & badcontents)
+                       continue;
+               if (trace_dphitq3surfaceflags & badsurfaceflags)
+                       continue;
+
+               // rule 2: if we are too high, lower the point
+               if (trace_fraction * delta.z > maxaboveground)
+                       start = trace_endpos + '0 0 1' * maxaboveground;
+               vector enddown = trace_endpos;
+
+               // rule 3: make sure we aren't outside the map. This only works
+               // for somewhat well formed maps. A good rule of thumb is that
+               // the map should have a convex outside hull.
+               // these can be traceLINES as we already verified the starting box
+               vector mstart = start + 0.5 * (e.mins + e.maxs);
+               traceline(mstart, mstart + '1 0 0' * delta.x, MOVE_NORMAL, e);
+               if (trace_fraction >= 1 || trace_dphittexturename == "common/caulk")
+                       continue;
+               traceline(mstart, mstart - '1 0 0' * delta.x, MOVE_NORMAL, e);
+               if (trace_fraction >= 1 || trace_dphittexturename == "common/caulk")
+                       continue;
+               traceline(mstart, mstart + '0 1 0' * delta.y, MOVE_NORMAL, e);
+               if (trace_fraction >= 1 || trace_dphittexturename == "common/caulk")
+                       continue;
+               traceline(mstart, mstart - '0 1 0' * delta.y, MOVE_NORMAL, e);
+               if (trace_fraction >= 1 || trace_dphittexturename == "common/caulk")
+                       continue;
+               traceline(mstart, mstart + '0 0 1' * delta.z, MOVE_NORMAL, e);
+               if (trace_fraction >= 1 || trace_dphittexturename == "common/caulk")
+                       continue;
 
                // rule 4: we must "see" some spawnpoint or item
-           entity sp = NULL;
-           IL_EACH(g_spawnpoints, checkpvs(mstart, it),
-           {
-               if((traceline(mstart, it.origin, MOVE_NORMAL, e), trace_fraction) >= 1)
-               {
-                       sp = it;
-                       break;
-               }
-           });
+               entity sp = NULL;
+               if(frompos)
+               {
+                       if((traceline(mstart, e.origin, MOVE_NORMAL, e), trace_fraction) >= 1)
+                               sp = e;
+               }
+               if(!sp)
+               {
+                       IL_EACH(g_spawnpoints, checkpvs(mstart, it),
+                       {
+                               if((traceline(mstart, it.origin, MOVE_NORMAL, e), trace_fraction) >= 1)
+                               {
+                                       sp = it;
+                                       break;
+                               }
+                       });
+               }
                if(!sp)
                {
                        int items_checked = 0;
@@ -1118,45 +1142,45 @@ bool MoveToRandomLocationWithinBounds(entity e, vector boundmin, vector boundmax
                                continue;
                }
 
-        // find a random vector to "look at"
-        end.x = org.x + random() * delta.x;
-        end.y = org.y + random() * delta.y;
-        end.z = org.z + random() * delta.z;
-        end = start + normalize(end - start) * vlen(delta);
-
-        // rule 4: start TO end must not be too short
-        tracebox(start, e.mins, e.maxs, end, MOVE_NORMAL, e);
-        if(trace_startsolid)
-            continue;
-        if(trace_fraction < minviewdistance / vlen(delta))
-            continue;
-
-        // rule 5: don't want to look at sky
-        if(trace_dphitq3surfaceflags & Q3SURFACEFLAG_SKY)
-            continue;
-
-        // rule 6: we must not end up in trigger_hurt
-        if(tracebox_hits_trigger_hurt(start, e.mins, e.maxs, enddown))
-            continue;
-
-        break;
-    }
-
-    e.dphitcontentsmask = m;
-
-    if(j < attempts)
-    {
-        setorigin(e, start);
-        e.angles = vectoangles(end - start);
-        LOG_DEBUG("Needed ", ftos(j + 1), " attempts");
-        return true;
-    }
-    return false;
+               // find a random vector to "look at"
+               end.x = org.x + random() * delta.x;
+               end.y = org.y + random() * delta.y;
+               end.z = org.z + random() * delta.z;
+               end = start + normalize(end - start) * vlen(delta);
+
+               // rule 4: start TO end must not be too short
+               tracebox(start, e.mins, e.maxs, end, MOVE_NORMAL, e);
+               if(trace_startsolid)
+                       continue;
+               if(trace_fraction < minviewdistance / vlen(delta))
+                       continue;
+
+               // rule 5: don't want to look at sky
+               if(trace_dphitq3surfaceflags & Q3SURFACEFLAG_SKY)
+                       continue;
+
+               // rule 6: we must not end up in trigger_hurt
+               if(tracebox_hits_trigger_hurt(start, e.mins, e.maxs, enddown))
+                       continue;
+
+               break;
+       }
+
+       e.dphitcontentsmask = m;
+
+       if(j < attempts)
+       {
+               setorigin(e, start);
+               e.angles = vectoangles(end - start);
+               LOG_DEBUG("Needed ", ftos(j + 1), " attempts");
+               return true;
+       }
+       return false;
 }
 
 float MoveToRandomMapLocation(entity e, float goodcontents, float badcontents, float badsurfaceflags, float attempts, float maxaboveground, float minviewdistance)
 {
-       return MoveToRandomLocationWithinBounds(e, world.mins, world.maxs, goodcontents, badcontents, badsurfaceflags, attempts, maxaboveground, minviewdistance);
+       return MoveToRandomLocationWithinBounds(e, world.mins, world.maxs, goodcontents, badcontents, badsurfaceflags, attempts, maxaboveground, minviewdistance, false);
 }
 
 /*
@@ -1884,25 +1908,25 @@ void readplayerstartcvars()
        else if (s == "all" || s == "1")
        {
                g_weaponarena = 1;
-               g_weaponarena_list = "All Weapons";
+               g_weaponarena_list = "All Weapons Arena";
                g_weaponarena_weapons = weapons_all();
        }
        else if (s == "devall")
        {
                g_weaponarena = 1;
-               g_weaponarena_list = "Dev All Weapons";
+               g_weaponarena_list = "Dev All Weapons Arena";
                g_weaponarena_weapons = weapons_devall();
        }
        else if (s == "most")
        {
                g_weaponarena = 1;
-               g_weaponarena_list = "Most Weapons";
+               g_weaponarena_list = "Most Weapons Arena";
                g_weaponarena_weapons = weapons_most();
        }
        else if (s == "all_available")
        {
                g_weaponarena = 1;
-               g_weaponarena_list = "All Available Weapons";
+               g_weaponarena_list = "All Available Weapons Arena";
 
                // this needs to run after weaponsInMapAll is initialized
                InitializeEntity(NULL, weaponarena_available_all_update, INITPRIO_FINDTARGET);
@@ -1910,7 +1934,7 @@ void readplayerstartcvars()
        else if (s == "devall_available")
        {
                g_weaponarena = 1;
-               g_weaponarena_list = "Dev All Available Weapons";
+               g_weaponarena_list = "Dev All Available Weapons Arena";
 
                // this needs to run after weaponsInMapAll is initialized
                InitializeEntity(NULL, weaponarena_available_devall_update, INITPRIO_FINDTARGET);
@@ -1918,7 +1942,7 @@ void readplayerstartcvars()
        else if (s == "most_available")
        {
                g_weaponarena = 1;
-               g_weaponarena_list = "Most Available Weapons";
+               g_weaponarena_list = "Most Available Weapons Arena";
 
                // this needs to run after weaponsInMapAll is initialized
                InitializeEntity(NULL, weaponarena_available_most_update, INITPRIO_FINDTARGET);
@@ -1926,7 +1950,7 @@ void readplayerstartcvars()
        else if (s == "none")
        {
                g_weaponarena = 1;
-               g_weaponarena_list = "No Weapons";
+               g_weaponarena_list = "No Weapons Arena";
        }
        else
        {
@@ -1940,10 +1964,13 @@ void readplayerstartcvars()
                        if(wep != WEP_Null)
                        {
                                g_weaponarena_weapons |= (wep.m_wepset);
-                               g_weaponarena_list = strcat(g_weaponarena_list, wep.m_name, " & ");
+                               g_weaponarena_list = strcat(g_weaponarena_list, wep.netname, " & ");
                        }
                }
-               g_weaponarena_list = strzone(substring(g_weaponarena_list, 0, strlen(g_weaponarena_list) - 3));
+               if (g_weaponarena_list != "") // remove trailing " & "
+                       g_weaponarena_list = substring(g_weaponarena_list, 0, strlen(g_weaponarena_list) - 3);
+               else // no valid weapon found
+                       g_weaponarena_list = "No Weapons Arena";
        }
 
        if (g_weaponarena)
@@ -1951,6 +1978,7 @@ void readplayerstartcvars()
                g_weapon_stay = 0; // incompatible
                start_weapons = g_weaponarena_weapons;
                start_items |= IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS;
+               g_weaponarena_list = strzone(g_weaponarena_list);
        }
        else
        {
@@ -2068,9 +2096,14 @@ void readplayerstartcvars()
 
 void readlevelcvars()
 {
+       serverflags &= ~SERVERFLAG_ALLOW_FULLBRIGHT;
        if(cvar("sv_allow_fullbright"))
                serverflags |= SERVERFLAG_ALLOW_FULLBRIGHT;
 
+       serverflags &= ~SERVERFLAG_FORBID_PICKUPTIMER;
+       if(cvar("sv_forbid_pickuptimer"))
+               serverflags |= SERVERFLAG_FORBID_PICKUPTIMER;
+
        sv_ready_restart_after_countdown = cvar("sv_ready_restart_after_countdown");
 
        warmup_stage = cvar("g_warmup");
@@ -2135,13 +2168,13 @@ void readlevelcvars()
        g_pickup_ammo_anyway = cvar("g_pickup_ammo_anyway");
        g_pickup_weapons_anyway = cvar("g_pickup_weapons_anyway");
 
-    g_weapon_stay = cvar(strcat("g_", GetGametype(), "_weapon_stay"));
-    if(!g_weapon_stay)
-        g_weapon_stay = cvar("g_weapon_stay");
+       g_weapon_stay = cvar(strcat("g_", GetGametype(), "_weapon_stay"));
+       if(!g_weapon_stay)
+               g_weapon_stay = cvar("g_weapon_stay");
 
-    MUTATOR_CALLHOOK(ReadLevelCvars);
+       MUTATOR_CALLHOOK(ReadLevelCvars);
 
-       if (!warmup_stage)
+       if (!warmup_stage && !autocvar_g_campaign)
                game_starttime = time + cvar("g_start_delay");
 
        FOREACH(Weapons, it != WEP_Null, { it.wr_init(it); });
@@ -2151,79 +2184,149 @@ void readlevelcvars()
 
 void InitializeEntity(entity e, void(entity this) func, int order)
 {
-    entity prev, cur;
-
-    if (!e || e.initialize_entity)
-    {
-        // make a proxy initializer entity
-        entity e_old = e;
-        e = new(initialize_entity);
-        e.enemy = e_old;
-    }
-
-    e.initialize_entity = func;
-    e.initialize_entity_order = order;
-
-    cur = initialize_entity_first;
-    prev = NULL;
-    for (;;)
-    {
-        if (!cur || cur.initialize_entity_order > order)
-        {
-            // insert between prev and cur
-            if (prev)
-                prev.initialize_entity_next = e;
-            else
-                initialize_entity_first = e;
-            e.initialize_entity_next = cur;
-            return;
-        }
-        prev = cur;
-        cur = cur.initialize_entity_next;
-    }
+       entity prev, cur;
+
+       if (!e || e.initialize_entity)
+       {
+               // make a proxy initializer entity
+               entity e_old = e;
+               e = new(initialize_entity);
+               e.enemy = e_old;
+       }
+
+       e.initialize_entity = func;
+       e.initialize_entity_order = order;
+
+       cur = initialize_entity_first;
+       prev = NULL;
+       for (;;)
+       {
+               if (!cur || cur.initialize_entity_order > order)
+               {
+                       // insert between prev and cur
+                       if (prev)
+                               prev.initialize_entity_next = e;
+                       else
+                               initialize_entity_first = e;
+                       e.initialize_entity_next = cur;
+                       return;
+               }
+               prev = cur;
+               cur = cur.initialize_entity_next;
+       }
 }
 void InitializeEntitiesRun()
 {
-    entity startoflist = initialize_entity_first;
-    initialize_entity_first = NULL;
-    delete_fn = remove_except_protected;
-    for (entity e = startoflist; e; e = e.initialize_entity_next)
-    {
+       entity startoflist = initialize_entity_first;
+       initialize_entity_first = NULL;
+       delete_fn = remove_except_protected;
+       for (entity e = startoflist; e; e = e.initialize_entity_next)
+       {
                e.remove_except_protected_forbidden = 1;
-    }
-    for (entity e = startoflist; e; )
-    {
+       }
+       for (entity e = startoflist; e; )
+       {
                e.remove_except_protected_forbidden = 0;
-        e.initialize_entity_order = 0;
-       entity next = e.initialize_entity_next;
-        e.initialize_entity_next = NULL;
-        var void(entity this) func = e.initialize_entity;
-        e.initialize_entity = func_null;
-        if (e.classname == "initialize_entity")
-        {
-            entity wrappee = e.enemy;
-            builtin_remove(e);
-            e = wrappee;
-        }
-        //dprint("Delayed initialization: ", e.classname, "\n");
-        if (func)
-        {
-               func(e);
-        }
-        else
-        {
-            eprint(e);
-            backtrace(strcat("Null function in: ", e.classname, "\n"));
-        }
-        e = next;
-    }
-    delete_fn = remove_unsafely;
+               e.initialize_entity_order = 0;
+               entity next = e.initialize_entity_next;
+               e.initialize_entity_next = NULL;
+               var void(entity this) func = e.initialize_entity;
+               e.initialize_entity = func_null;
+               if (e.classname == "initialize_entity")
+               {
+                       entity wrappee = e.enemy;
+                       builtin_remove(e);
+                       e = wrappee;
+               }
+               //dprint("Delayed initialization: ", e.classname, "\n");
+               if (func)
+               {
+                       func(e);
+               }
+               else
+               {
+                       eprint(e);
+                       backtrace(strcat("Null function in: ", e.classname, "\n"));
+               }
+               e = next;
+       }
+       delete_fn = remove_unsafely;
 }
 
 // deferred dropping
+// ported from VM_SV_droptofloor TODO: make a common function for the client-side?
 void DropToFloor_Handler(entity this)
 {
-       WITHSELF(this, builtin_droptofloor());
+       if(!this || wasfreed(this))
+       {
+               // no modifying free entities
+               return;
+       }
+
+       vector end = this.origin;
+       if (autocvar_sv_mapformat_is_quake3)
+               end.z -= 4096;
+       else if (autocvar_sv_mapformat_is_quake2)
+               end.z -= 128;
+       else
+               end.z -= 256; // Quake, QuakeWorld
+
+       // NOTE: SV_NudgeOutOfSolid is used in the engine here
+       if(autocvar_sv_gameplayfix_droptofloorstartsolid_nudgetocorrect)
+       {
+               _Movetype_UnstickEntity(this);
+               move_out_of_solid(this);
+       }
+
+       tracebox(this.origin, this.mins, this.maxs, end, MOVE_NORMAL, this);
+
+       if(trace_startsolid && autocvar_sv_gameplayfix_droptofloorstartsolid)
+       {
+               vector offset, org;
+               offset = 0.5 * (this.mins + this.maxs);
+               offset.z = this.mins.z;
+               org = this.origin + offset;
+               traceline(org, end, MOVE_NORMAL, this);
+               trace_endpos = trace_endpos - offset;
+               if(trace_startsolid)
+               {
+                       LOG_DEBUGF("DropToFloor_Handler: %v could not fix badly placed entity", this.origin);
+                       _Movetype_LinkEdict(this, false);
+                       SET_ONGROUND(this);
+                       this.groundentity = NULL;
+               }
+               else if(trace_fraction < 1)
+               {
+                       LOG_DEBUGF("DropToFloor_Handler: %v fixed badly placed entity", this.origin);
+                       setorigin(this, trace_endpos);
+                       if(autocvar_sv_gameplayfix_droptofloorstartsolid_nudgetocorrect)
+                       {
+                               _Movetype_UnstickEntity(this);
+                               move_out_of_solid(this);
+                       }
+                       SET_ONGROUND(this);
+                       this.groundentity = trace_ent;
+                       // if support is destroyed, keep suspended (gross hack for floating items in various maps)
+                       this.move_suspendedinair = true;
+               }
+       }
+       else
+       {
+               if(!trace_allsolid && trace_fraction < 1)
+               {
+                       setorigin(this, trace_endpos);
+                       SET_ONGROUND(this);
+                       this.groundentity = trace_ent;
+                       // if support is destroyed, keep suspended (gross hack for floating items in various maps)
+                       this.move_suspendedinair = true;
+               }
+               else
+               {
+                       // if we can't get the entity out of solid, mark it as on ground so physics doesn't attempt to drop it
+                       // hacky workaround for #2774
+                       SET_ONGROUND(this);
+               }
+       }
        this.dropped_origin = this.origin;
 }