]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Implement automatic per-map min & max player limits
authorbones_was_here <bones_was_here@xonotic.au>
Mon, 26 Sep 2022 05:21:58 +0000 (15:21 +1000)
committerbones_was_here <bones_was_here@xonotic.au>
Mon, 26 Sep 2022 05:21:58 +0000 (15:21 +1000)
Enabled by g_warmup -1 and g_maxplayers -1 respectively.

Map settings are loaded from .sizes files
and are rounded to a multiple of the number of teams.
At the midpoint, min players is rounded down and max players is rounded up.
Neither can exceed engine maxplayers which is also rounded down if necessary.

g_warmup -1 means stay in warmup until enough players have joined,
then switch to g_warmup_limit and wait for ready players.
"Enough" has a lower limit of 2 or 2 * number of teams,
so this can be useful on maps with no set minimum.

qcsrc/server/client.qc
qcsrc/server/command/vote.qc
qcsrc/server/world.qc
qcsrc/server/world.qh
xonotic-server.cfg

index 8c974b28afa84d841bc5f4c6afcab5990da1df0e..f3ef104a9f54106b650e6fc29d37552541a47794 100644 (file)
@@ -809,6 +809,9 @@ void PutPlayerInServer(entity this)
                this.alivetime = time;
 
        antilag_clear(this, CS(this));
+
+       if (warmup_stage == -1)
+               ReadyCount();
 }
 
 /** Called when a client spawns in the server */
@@ -1980,10 +1983,11 @@ int GetPlayerLimit()
 {
        if(g_duel)
                return 2; // TODO: this workaround is needed since the mutator hook from duel can't be activated before the gametype is loaded (e.g. switching modes via gametype vote screen)
-       int player_limit = autocvar_g_maxplayers;
+       // don't return map_maxplayers during intermission as it would interfere with MapHasRightSize()
+       int player_limit = (autocvar_g_maxplayers >= 0 || intermission_running) ? autocvar_g_maxplayers : map_maxplayers;
        MUTATOR_CALLHOOK(GetPlayerLimit, player_limit);
        player_limit = M_ARGV(0, int);
-       return player_limit;
+       return player_limit < maxclients ? player_limit : 0;
 }
 
 /**
index 8c70ae6f99d328b13fdea79b026f062b6a49bb90..3367ef310c2e1eb5d97852b34964f1aa5188ab02 100644 (file)
@@ -503,7 +503,11 @@ void ReadyRestart(bool forceWarmupEnd)
        ReadyRestart_force(false);
 }
 
-// Count the players who are ready and determine whether or not to restart the match
+/* Count the players who are ready and determine whether or not to restart the match when:
+ * a player presses F4                      server/command/cmd.qc  ClientCommand_ready()
+ * a player switches from players to specs  server/client.qc       PutObserverInServer()
+ * a player joins (from specs or directly)  server/client.qc       PutPlayerInServer()
+ * a player disconnects                     server/client.qc       ClientDisconnect()  */
 void ReadyCount()
 {
        // cannot reset the game while a timeout is active or pending
@@ -520,6 +524,25 @@ void ReadyCount()
 
        Nagger_ReadyCounted();
 
+       if (t_players < map_minplayers) // map_minplayers will only be set if g_warmup -1 at worldspawn
+       {
+               // TODO: handle player spectating/disconnecting during countdown
+               if (warmup_limit > 0)
+                       warmup_limit = -1;
+               return; // don't ReadyRestart if players are ready but too few
+       }
+       else if (map_minplayers && warmup_limit <= 0)
+       {
+               // there's enough players now but we're still in infinite warmup
+               warmup_limit = cvar("g_warmup_limit");
+               if (warmup_limit == 0)
+                       warmup_limit = autocvar_timelimit * 60;
+               if (warmup_limit > 0)
+                       game_starttime = time;
+               // implicit else: g_warmup -1 && g_warmup_limit -1 means
+               // warmup continues until enough players AND enough RUPs (no time limit)
+       }
+
        ready_needed_factor = bound(0.5, cvar("g_warmup_majority_factor"), 0.999);
        ready_needed_count = floor(t_players * ready_needed_factor) + 1;
 
index 50a6347a04ff301b6c9dd16896553176769d28a5..7f651fa7748291ff5e7090192793cf39c206afa9 100644 (file)
@@ -624,8 +624,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()
index e74ab2dc7589733b4ca9e4034ab587c49daa78d1..d82469c11ef1ca7573752897113a614c673e01f3 100644 (file)
@@ -114,7 +114,7 @@ float g_weapon_stay;
 float want_weapon(entity weaponinfo, float allguns); // WEAPONTODO: what still needs done?
 
 float g_grappling_hook;
-float warmup_stage;
+int warmup_stage;
 
 bool sv_ready_restart_after_countdown;
 
index f0937a16d1a23b4750ee0df279cde36d56364abe..572d89d7e1f360f95fe4cc92e532658abb68b97a 100644 (file)
@@ -26,11 +26,11 @@ alias sv_hook_readyrestart
 //nifreks lockonrestart feature, used in team-based game modes, if set to 1 and all players readied up no other player can then join the game anymore, useful to block spectators from joining
 set teamplay_lockonrestart 0 "lock teams once all players readied up and the game restarted (no new players can join after restart unless using the server-command unlockteams)"
 
-set g_maxplayers 0 "maximum number of players allowed to play at the same time, set to 0 to allow all players to join the game"
+set g_maxplayers 0 "maximum number of players allowed to play at the same time, 0 means unlimited, -1 uses the map setting or unlimited if not set (rounded to multiple of team number)"
 set g_maxplayers_spectator_blocktime 5 "if the players voted for the \"nospectators\" command, this setting defines the number of seconds a observer/spectator has time to join the game before he gets kicked"
 
 // tournament mod
-set g_warmup 0 "split the game into a warmup- and match-stage"
+set g_warmup 0 "split the game into a warmup- and match-stage, -1 means stay in warmup until enough (set by map, lower bound of 2 or 2 per team) players join, then g_warmup_limit and readiness apply"
 set g_warmup_limit 180 "limit warmup-stage to this time (in seconds); if set to -1 the warmup-stage is not affected by any timelimit, if set to 0 the usual timelimit also affects warmup-stage"
 set g_warmup_allow_timeout 0 "allow calling timeouts in the warmup-stage (if sv_timeout is set to 1)"
 set g_warmup_allguns 1 "provide more weapons on start while in warmup: 0 = normal start weapons, 1 = all guns available on the map, 2 = all normal weapons"