From 613663cc624d93b575bbb2e1402ab673d94d02c7 Mon Sep 17 00:00:00 2001 From: bones_was_here Date: Mon, 26 Sep 2022 15:21:58 +1000 Subject: [PATCH] Implement automatic per-map min & max player limits 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 | 8 ++++++-- qcsrc/server/command/vote.qc | 25 ++++++++++++++++++++++++- qcsrc/server/world.qc | 34 ++++++++++++++++++++++++++++++++++ qcsrc/server/world.qh | 2 +- xonotic-server.cfg | 4 ++-- 5 files changed, 67 insertions(+), 6 deletions(-) diff --git a/qcsrc/server/client.qc b/qcsrc/server/client.qc index 8c974b28a..f3ef104a9 100644 --- a/qcsrc/server/client.qc +++ b/qcsrc/server/client.qc @@ -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; } /** diff --git a/qcsrc/server/command/vote.qc b/qcsrc/server/command/vote.qc index 8c70ae6f9..3367ef310 100644 --- a/qcsrc/server/command/vote.qc +++ b/qcsrc/server/command/vote.qc @@ -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; diff --git a/qcsrc/server/world.qc b/qcsrc/server/world.qc index 50a6347a0..7f651fa77 100644 --- a/qcsrc/server/world.qc +++ b/qcsrc/server/world.qc @@ -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() diff --git a/qcsrc/server/world.qh b/qcsrc/server/world.qh index e74ab2dc7..d82469c11 100644 --- a/qcsrc/server/world.qh +++ b/qcsrc/server/world.qh @@ -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; diff --git a/xonotic-server.cfg b/xonotic-server.cfg index f0937a16d..572d89d7e 100644 --- a/xonotic-server.cfg +++ b/xonotic-server.cfg @@ -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" -- 2.39.2