From 5779b2cc46d995d92677f0e5946992b307c12511 Mon Sep 17 00:00:00 2001 From: bones_was_here Date: Wed, 5 Jun 2024 03:53:18 +1000 Subject: [PATCH] Don't end warmup when teams are unbalanced (WRT sv_teamnagger) Allows spectators to see the unbalanced teams warning, because they could fix it by joining the smaller team. Aborts countdown if teams become unbalanced, for the case of someone joining this can be avoided by enabling g_balance_teams_queue. Updates the chatcon notification for countdown abort when the player count drops below the minimum. --- notifications.cfg | 9 ++++-- qcsrc/client/hud/panel/infomessages.qc | 40 +++++++++++--------------- qcsrc/client/hud/panel/scoreboard.qc | 19 ++++++++++++ qcsrc/client/hud/panel/scoreboard.qh | 1 + qcsrc/client/hud/panel/timer.qc | 4 ++- qcsrc/common/notifications/all.inc | 9 ++++-- qcsrc/server/client.qc | 11 +++---- qcsrc/server/command/vote.qc | 24 +++++++++++----- qcsrc/server/teamplay.qc | 26 +++++++++++++++++ qcsrc/server/teamplay.qh | 3 ++ qcsrc/server/world.qc | 1 + xonotic-server.cfg | 2 +- 12 files changed, 105 insertions(+), 44 deletions(-) diff --git a/notifications.cfg b/notifications.cfg index 92f416971..0f5b959d8 100644 --- a/notifications.cfg +++ b/notifications.cfg @@ -104,7 +104,8 @@ seta notification_INFO_CHAT_TEAM_DISABLED "2" "0 = off, 1 = print to console, 2 seta notification_INFO_COINTOSS "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" seta notification_INFO_CONNECTING "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" seta notification_INFO_COUNTDOWN_RESTART "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" -seta notification_INFO_COUNTDOWN_STOP "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" +seta notification_INFO_COUNTDOWN_STOP_BADTEAMS "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" +seta notification_INFO_COUNTDOWN_STOP_MINPLAYERS "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" seta notification_INFO_CTF_CAPTURE_BROKEN "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" seta notification_INFO_CTF_CAPTURE_NEUTRAL "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" seta notification_INFO_CTF_CAPTURE "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)" @@ -367,7 +368,8 @@ seta notification_CENTER_COUNTDOWN_BEGIN "1" "0 = off, 1 = centerprint" seta notification_CENTER_COUNTDOWN_GAMESTART "1" "0 = off, 1 = centerprint" seta notification_CENTER_COUNTDOWN_ROUNDSTART "1" "0 = off, 1 = centerprint" seta notification_CENTER_COUNTDOWN_ROUNDSTOP "1" "0 = off, 1 = centerprint" -seta notification_CENTER_COUNTDOWN_STOP "1" "0 = off, 1 = centerprint" +seta notification_CENTER_COUNTDOWN_STOP_BADTEAMS "1" "0 = off, 1 = centerprint" +seta notification_CENTER_COUNTDOWN_STOP_MINPLAYERS "1" "0 = off, 1 = centerprint" seta notification_CENTER_CTF_CAPTURESHIELD_FREE "1" "0 = off, 1 = centerprint" seta notification_CENTER_CTF_CAPTURESHIELD_INACTIVE "1" "0 = off, 1 = centerprint" seta notification_CENTER_CTF_CAPTURESHIELD_SHIELDED "1" "0 = off, 1 = centerprint" @@ -570,7 +572,8 @@ seta notification_CENTER_WEAPON_MINELAYER_LIMIT "1" "0 = off, 1 = centerprint" // MSG_MULTI notifications: seta notification_COUNTDOWN_BEGIN "1" "Enable this multiple notification" -seta notification_COUNTDOWN_STOP "1" "Enable this multiple notification" +seta notification_COUNTDOWN_STOP_BADTEAMS "1" "Enable this multiple notification" +seta notification_COUNTDOWN_STOP_MINPLAYERS "1" "Enable this multiple notification" seta notification_DEATH_MURDER_BUFF "1" "Enable this multiple notification" seta notification_DEATH_MURDER_CHEAT "1" "Enable this multiple notification" seta notification_DEATH_MURDER_DROWN "1" "Enable this multiple notification" diff --git a/qcsrc/client/hud/panel/infomessages.qc b/qcsrc/client/hud/panel/infomessages.qc index 8d11b6fbd..43ed2440e 100644 --- a/qcsrc/client/hud/panel/infomessages.qc +++ b/qcsrc/client/hud/panel/infomessages.qc @@ -157,11 +157,9 @@ void HUD_InfoMessages() InfoMessage(_("^2Currently in ^1warmup^2 stage!")); int players_needed = 0; + Scoreboard_UpdatePlayerTeams(); // ensure numplayers, ts_min, ts_max are current if(STAT(WARMUP_TIMELIMIT) <= 0 && srv_minplayers) - { - Scoreboard_UpdatePlayerTeams(); // ensure numplayers is current players_needed = srv_minplayers - numplayers; - } if(players_needed > 0) { @@ -171,6 +169,11 @@ void HUD_InfoMessages() s = sprintf(_("^3%d^2 more players are needed for the match to start."), players_needed); InfoMessage(s); } + else if(teamnagger && (ts_max - ts_min) >= teamnagger) + { + // ready won't end warmup so don't display that message + // see below for unbalanced teams warning + } else if(!spectatee_status) { if(ready_waiting) @@ -186,29 +189,18 @@ void HUD_InfoMessages() } } - if(teamplay && !spectatee_status && teamnagger) + if (teamplay && numplayers > 1 && teamnagger) { - float ts_min = 0, ts_max = 0; - entity tm = teams.sort_next; - if (tm) + if (!warmup_stage) // in warmup this was done above + Scoreboard_UpdatePlayerTeams(); + + if ((ts_max - ts_min) >= teamnagger) { - for (; tm.sort_next; tm = tm.sort_next) - { - if(!tm.team_size || tm.team == NUM_SPECTATOR) - continue; - if(!ts_min) ts_min = tm.team_size; - else ts_min = min(ts_min, tm.team_size); - if(!ts_max) ts_max = tm.team_size; - else ts_max = max(ts_max, tm.team_size); - } - if ((ts_max - ts_min) >= teamnagger) - { - s = strcat(blinkcolor, _("Teamnumbers are unbalanced!")); - tm = GetTeam(myteam, false); - if (tm && tm.team != NUM_SPECTATOR && tm.team_size == ts_max) - s = strcat(s, sprintf(_(" Press ^3%s%s to adjust"), getcommandkey(_("team selection"), "team_selection_show"), blinkcolor)); - InfoMessage(s); - } + s = strcat(blinkcolor, _("Teams are unbalanced!")); + entity tm = GetTeam(myteam, false); + if (tm && tm.team != NUM_SPECTATOR && tm.team_size == ts_max) + s = strcat(s, sprintf(_(" Press ^3%s%s to adjust"), getcommandkey(_("team selection"), "team_selection_show"), blinkcolor)); + InfoMessage(s); } } diff --git a/qcsrc/client/hud/panel/scoreboard.qc b/qcsrc/client/hud/panel/scoreboard.qc index 2710427e0..d59ed97fe 100644 --- a/qcsrc/client/hud/panel/scoreboard.qc +++ b/qcsrc/client/hud/panel/scoreboard.qc @@ -592,6 +592,25 @@ void Scoreboard_UpdatePlayerTeams() print(strcat("PNUM: ", ftos(num), "\n")); lastpnum = num; */ + + // update the smallest and largest team sizes + if (!teamplay || !teams.sort_next) + ts_min = ts_max = 0; + else + { + ts_min = 255, ts_max = 0; + + for (entity tm = teams.sort_next; tm; tm = tm.sort_next) + { + if (tm.team == NUM_SPECTATOR) + continue; + + if (ts_min > tm.team_size) + ts_min = tm.team_size; + if (ts_max < tm.team_size) + ts_max = tm.team_size; + } + } } int Scoreboard_CompareScore(int vl, int vr, int f) diff --git a/qcsrc/client/hud/panel/scoreboard.qh b/qcsrc/client/hud/panel/scoreboard.qh index b6cf1dab0..a7cb2b553 100644 --- a/qcsrc/client/hud/panel/scoreboard.qh +++ b/qcsrc/client/hud/panel/scoreboard.qh @@ -18,6 +18,7 @@ float scoreboard_left; float scoreboard_right; int numplayers; +int ts_min, ts_max; ///< team size void Cmd_Scoreboard_SetFields(int argc); void Scoreboard_Draw(); diff --git a/qcsrc/client/hud/panel/timer.qc b/qcsrc/client/hud/panel/timer.qc index 9b10f63e3..09557afe9 100644 --- a/qcsrc/client/hud/panel/timer.qc +++ b/qcsrc/client/hud/panel/timer.qc @@ -128,7 +128,9 @@ void HUD_Timer() { Scoreboard_UpdatePlayerTeams(); // ensure numplayers is current if (srv_minplayers - numplayers > 0) - subtext = _("Warmup: too few players"); + subtext = _("Warmup: too few players!"); + else if (teamnagger && (ts_max - ts_min) >= teamnagger) + subtext = _("Warmup: teams unbalanced!"); else subtext = _("Warmup: no time limit"); } diff --git a/qcsrc/common/notifications/all.inc b/qcsrc/common/notifications/all.inc index 365f6cb60..8db1de4f6 100644 --- a/qcsrc/common/notifications/all.inc +++ b/qcsrc/common/notifications/all.inc @@ -271,7 +271,8 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input != MSG_INFO_NOTIF(CA_LEAVE, N_CONSOLE, 0, 0, "", "", "", _("^F2You will spectate in the next round"), "") MSG_INFO_NOTIF(COUNTDOWN_RESTART, N_CHATCON, 0, 0, "", "", "", _("^F2Match is restarting..."), "") - MSG_INFO_NOTIF(COUNTDOWN_STOP, N_CHATCON, 0, 0, "", "", "", _("^F4Countdown stopped!"), "") + MSG_INFO_NOTIF(COUNTDOWN_STOP_MINPLAYERS, N_CHATCON, 0, 1, "f1", "", "", _("^F4Countdown stopped! ^BG%s players are needed for this match."), "") + MSG_INFO_NOTIF(COUNTDOWN_STOP_BADTEAMS, N_CHATCON, 0, 0, "", "", "", _("^F4Countdown stopped! ^BGTeams are too unbalanced."), "") MSG_INFO_NOTIF(DEATH_MURDER_BUFF, N_CONSOLE, 3, 3, "spree_inf s1 s2 f3buffname s3loc spree_end", "s2 s1", "notify_death", _("^BG%s%s^K1 was killed by ^BG%s^K1's ^BG%s^K1 buff ^K1%s%s"), _("^BG%s%s^K1 was scored against by ^BG%s^K1's ^BG%s^K1 buff ^K1%s%s")) MSG_INFO_NOTIF(DEATH_MURDER_CHEAT, N_CONSOLE, 3, 2, "spree_inf s1 s2 s3loc spree_end", "s2 s1", "notify_death", _("^BG%s%s^K1 was unfairly eliminated by ^BG%s^K1%s%s"), "") @@ -550,7 +551,8 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input != MSG_CENTER_NOTIF(ASSAULT_DEFENDING, N_ENABLE, 0, 0, "", CPID_ASSAULT_ROLE, "0 0", _("^BGYou are defending!"), "") MSG_CENTER_NOTIF(ASSAULT_OBJ_DESTROYED, N_ENABLE, 0, 1, "f1time", CPID_ASSAULT_ROLE, "0 0", _("^BGObjective destroyed in ^F4%s^BG!"), "") - MSG_CENTER_NOTIF(COUNTDOWN_STOP, N_ENABLE, 0, 1, "f1", CPID_MISSING_PLAYERS, "4 0", strcat(BOLD(_("^F4Countdown stopped!")), "\n^BG", _("%s players are needed for this match.")), "") + MSG_CENTER_NOTIF(COUNTDOWN_STOP_MINPLAYERS, N_ENABLE, 0, 1, "f1", CPID_MISSING_PLAYERS, "4 0", strcat(BOLD(_("^F4Countdown stopped!")), "\n^BG", _("%s players are needed for this match.")), "") + MSG_CENTER_NOTIF(COUNTDOWN_STOP_BADTEAMS, N_ENABLE, 0, 0, "", CPID_MISSING_PLAYERS, "0 0", strcat(BOLD(_("^F4Countdown stopped!")), "\n^BG", _("Teams are too unbalanced.")), "") MSG_CENTER_NOTIF(COUNTDOWN_BEGIN, N_ENABLE, 0, 0, "", CPID_ROUND, "2 0", BOLD(_("^BGBegin!")), "") MSG_CENTER_NOTIF(COUNTDOWN_GAMESTART, N_ENABLE, 0, 1, "", CPID_ROUND, "1 f1", strcat(_("^BGGame starts in"), "\n", BOLD("^COUNT")), "") MSG_CENTER_NOTIF(COUNTDOWN_ROUNDSTART, N_ENABLE, 0, 2, "f1", CPID_ROUND, "1 f2", strcat(_("^BGRound %s starts in"), "\n", BOLD("^COUNT")), "") @@ -835,7 +837,8 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input != // MSG_MULTI_NOTIFICATIONS MSG_MULTI_NOTIF(COUNTDOWN_BEGIN, N_ENABLE, ANNCE_BEGIN, NULL, CENTER_COUNTDOWN_BEGIN) - MSG_MULTI_NOTIF(COUNTDOWN_STOP, N_ENABLE, NULL, INFO_COUNTDOWN_STOP, CENTER_COUNTDOWN_STOP) + MSG_MULTI_NOTIF(COUNTDOWN_STOP_MINPLAYERS, N_ENABLE, NULL, INFO_COUNTDOWN_STOP_MINPLAYERS, CENTER_COUNTDOWN_STOP_MINPLAYERS) + MSG_MULTI_NOTIF(COUNTDOWN_STOP_BADTEAMS, N_ENABLE, NULL, INFO_COUNTDOWN_STOP_BADTEAMS, CENTER_COUNTDOWN_STOP_BADTEAMS) MSG_MULTI_NOTIF(DEATH_MURDER_BUFF, N_ENABLE, NULL, INFO_DEATH_MURDER_BUFF, NULL) MSG_MULTI_NOTIF(DEATH_MURDER_CHEAT, N_ENABLE, NULL, INFO_DEATH_MURDER_CHEAT, NULL) diff --git a/qcsrc/server/client.qc b/qcsrc/server/client.qc index 9c371ae00..3016275b9 100644 --- a/qcsrc/server/client.qc +++ b/qcsrc/server/client.qc @@ -259,7 +259,7 @@ void PutObserverInServer(entity this, bool is_forced, bool use_spawnpoint) { if (vote_called) { VoteCount(false); } this.ready = false; - if (warmup_stage || game_starttime > time) recount_ready = true; + if (warmup_stage || game_starttime > time) /* warmup OR countdown */ recount_ready = true; } entcs_update_players(this); } @@ -310,8 +310,6 @@ void PutObserverInServer(entity this, bool is_forced, bool use_spawnpoint) TRANSMUTE(Observer, this); - if(recount_ready) ReadyCount(); // FIXME: please add comment about why this is delayed - WaypointSprite_PlayerDead(this); accuracy_resend(this); @@ -413,6 +411,9 @@ void PutObserverInServer(entity this, bool is_forced, bool use_spawnpoint) if (CS(this).just_joined) CS(this).just_joined = false; + + if (recount_ready) + ReadyCount(); // must be called after SetPlayerTeam() and TRANSMUTE(Observer } int player_getspecies(entity this) @@ -821,7 +822,7 @@ void PutPlayerInServer(entity this) antilag_clear(this, CS(this)); - if (warmup_stage < 0 || warmup_stage > 1) + if (warmup_stage) ReadyCount(); } @@ -1297,7 +1298,7 @@ void ClientDisconnect(entity this) if (this.personal) delete(this.personal); this.playerid = 0; - if (warmup_stage || game_starttime > time) ReadyCount(); + if (warmup_stage || game_starttime > time) /* warmup OR countdown */ ReadyCount(); if (vote_called && IS_REAL_CLIENT(this)) VoteCount(false); player_powerups_remove_all(this); // stop powerup sound diff --git a/qcsrc/server/command/vote.qc b/qcsrc/server/command/vote.qc index e8c6812c2..57e1f8990 100644 --- a/qcsrc/server/command/vote.qc +++ b/qcsrc/server/command/vote.qc @@ -549,23 +549,33 @@ void ReadyCount() // map_minplayers can only be > 0 if g_warmup was -1 at worldspawn int minplayers = autocvar_g_warmup > 1 ? autocvar_g_warmup : map_minplayers; - if (total_players < minplayers) + // This allows warmup to end with zero players to prevent complaints + // of server never changing map with legacy config (sv_autopause 0). + bool badteams = (teamplay && total_players && autocvar_sv_teamnagger) + ? !MUTATOR_CALLHOOK(HideTeamNagger, NULL) && TeamBalance_SizeDifference(NULL) >= autocvar_sv_teamnagger + : false; + + if (total_players < minplayers || badteams) { if (game_starttime > time) // someone bailed during countdown, back to warmup { warmup_stage = autocvar_g_warmup; // CAN change it AFTER calling Nagger_ReadyCounted() this frame game_starttime = time; - Send_Notification(NOTIF_ALL, NULL, MSG_MULTI, COUNTDOWN_STOP, minplayers); + if (total_players < minplayers) + Send_Notification(NOTIF_ALL, NULL, MSG_MULTI, COUNTDOWN_STOP_MINPLAYERS, minplayers); + else + Send_Notification(NOTIF_ALL, NULL, MSG_MULTI, COUNTDOWN_STOP_BADTEAMS); if (!sv_ready_restart_after_countdown) // if we ran reset_map() at start of countdown FOREACH_CLIENT(IS_PLAYER(it), { GiveWarmupResources(it); }); } - if (warmup_limit > 0) - warmup_limit = -1; - return; // don't ReadyRestart if players are ready but too few + warmup_limit = -1; + return; // don't ReadyRestart if players are ready but too few or teams are bad } - else if (minplayers && warmup_limit <= 0) + else if (warmup_limit <= 0 + && game_starttime <= time) // No countdown in progress, check prevents early countdown end if only player leaves { - // there's enough players now but we're still in infinite warmup + // there's enough players now and teams are ok + // but we're still in infinite warmup and may need to switch to timed warmup warmup_limit = cvar("g_warmup_limit"); if (warmup_limit == 0) warmup_limit = autocvar_timelimit * 60; diff --git a/qcsrc/server/teamplay.qc b/qcsrc/server/teamplay.qc index 6e6a3e1e2..feee31c9b 100644 --- a/qcsrc/server/teamplay.qc +++ b/qcsrc/server/teamplay.qc @@ -279,6 +279,9 @@ bool SetPlayerTeam(entity player, int team_index, int type) Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(player.team, INFO_JOIN_PLAY_TEAM), player.netname); player.team_selected = true; // no autoselect in Join() } + + if (warmup_stage) + ReadyCount(); // teams might be balanced now } } @@ -697,6 +700,29 @@ int TeamBalance_GetAllowedTeams(entity balance) return result; } +int TeamBalance_SizeDifference(entity ignore) +{ + if (!teamplay) + return 0; + + entity balance = TeamBalance_CheckAllowedTeams(ignore); + TeamBalance_GetTeamCounts(balance, ignore); + + int ts_min = 255, ts_max = 0; + for (int i = 1; i <= AVAILABLE_TEAMS; ++i) + { + int ts = TeamBalance_GetTeamFromIndex(balance, i).m_num_players; + if (ts_min > ts) + ts_min = ts; + if (ts_max < ts) + ts_max = ts; + } + + TeamBalance_Destroy(balance); + + return ts_max - ts_min; +} + bool TeamBalance_AreEqual(entity ignore, bool would_leave) { entity balance = TeamBalance_CheckAllowedTeams(ignore); diff --git a/qcsrc/server/teamplay.qh b/qcsrc/server/teamplay.qh index 61d086c78..9a75a03d9 100644 --- a/qcsrc/server/teamplay.qh +++ b/qcsrc/server/teamplay.qh @@ -192,6 +192,9 @@ void TeamBalance_Destroy(entity balance); /// \return Bitmask of allowed teams. int TeamBalance_GetAllowedTeams(entity balance); +/// Returns the size difference between the largest and smallest team (bots included). +int TeamBalance_SizeDifference(entity ignore); + bool TeamBalance_AreEqual(entity ignore, bool would_leave); void TeamBalance_RemoveExcessPlayers(entity ignore); /** Joins queued player(s) to team(s) with a shortage, diff --git a/qcsrc/server/world.qc b/qcsrc/server/world.qc index edfc6e1a8..effc4d51d 100644 --- a/qcsrc/server/world.qc +++ b/qcsrc/server/world.qc @@ -536,6 +536,7 @@ void cvar_changes_init() BADPREFIX("g_warmup"); BADPREFIX("sv_info_"); BADPREFIX("sv_ready_restart_"); + BADCVAR("sv_teamnagger"); BADPRESUFFIXVALUE("g_", "_weaponarena", "most"); BADPRESUFFIXVALUE("g_", "_weaponarena", "most_available"); diff --git a/xonotic-server.cfg b/xonotic-server.cfg index 19a82c977..5e4ec73c2 100644 --- a/xonotic-server.cfg +++ b/xonotic-server.cfg @@ -286,7 +286,7 @@ set g_balance_teams_remove 0 "remove excess players from teams to maintain balan set g_balance_teams_remove_wait 10 "seconds to warn everyone before removing an excess player (0 = immediately)" set g_changeteam_banned 0 "not allowed to change team" -set sv_teamnagger 2 "enable a nag message when the teams are unbalanced, value sets team size difference threshold, 1 is recommended when g_balance_teams_queue is enabled" +set sv_teamnagger 2 "enable a nag message when the teams are unbalanced, value sets team size difference threshold, 1 is recommended when g_balance_teams_queue is enabled, g_warmup won't end while the message is visible" set g_bloodloss 0 "amount of health below which blood loss occurs" -- 2.39.2