]> git.rm.cloudns.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Don't end warmup when teams are unbalanced (WRT sv_teamnagger)
authorbones_was_here <bones_was_here@xonotic.au>
Tue, 4 Jun 2024 17:53:18 +0000 (03:53 +1000)
committerbones_was_here <bones_was_here@xonotic.au>
Mon, 5 Aug 2024 16:26:24 +0000 (02:26 +1000)
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.

12 files changed:
notifications.cfg
qcsrc/client/hud/panel/infomessages.qc
qcsrc/client/hud/panel/scoreboard.qc
qcsrc/client/hud/panel/scoreboard.qh
qcsrc/client/hud/panel/timer.qc
qcsrc/common/notifications/all.inc
qcsrc/server/client.qc
qcsrc/server/command/vote.qc
qcsrc/server/teamplay.qc
qcsrc/server/teamplay.qh
qcsrc/server/world.qc
xonotic-server.cfg

index 92f41697162f0b48e6b74e8f00e634ba033280c9..0f5b959d8ba6ff26d9c608b64878f839bd8c3e89 100644 (file)
@@ -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"
index 8d11b6fbda0e3e1cbc0607f0defac25641c8376f..43ed2440e7771f3b65437e5057e8b5bc804f8bff 100644 (file)
@@ -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);
                        }
                }
 
index 2710427e08c9ecfd0819006d0bf7ab3666eeb744..d59ed97fe38b31fdb511b8801227cc1a887eecea 100644 (file)
@@ -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)
index b6cf1dab0bca97923de9237a678df0094feddb79..a7cb2b553927a2896b8c317a782ca15f9753e4ac 100644 (file)
@@ -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();
index 9b10f63e308c19d79dd246d0b7f5db88b8008a80..09557afe91f622a84bd0f64f80e1e87dea4c3503 100644 (file)
@@ -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");
                }
index 365f6cb60e7e823afd715a26dcdeb62950b3da00..8db1de4f6a6ab82881e635c075111c86709d4015 100644 (file)
@@ -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)
index 9c371ae0052bc815e76fac4dfb0eadbb13992bfe..3016275b9833f1876251bad1a5844ba048502253 100644 (file)
@@ -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
index e8c6812c2c4faaf38c380314de00c43a8095cfdd..57e1f89909726f75705f43ea27b98be93effc6fd 100644 (file)
@@ -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;
index 6e6a3e1e260001ec5df430dd6eedfd6443d3961d..feee31c9b950dc8bfe62bc67fd4708ccd6517200 100644 (file)
@@ -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);
index 61d086c7855d9c0630824a2d3b9a6838be34962b..9a75a03d950f2c64c010e5badeb3125f9595e059 100644 (file)
@@ -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,
index edfc6e1a8da3bf628b7f2543cb88daee4b0e5001..effc4d51d407b28b783cae9f0dcbb8e3cbc30a36 100644 (file)
@@ -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");
index 19a82c977104c7135b326304a8e6f71513b1d8cd..5e4ec73c261284bcf98aaf76e060ae6842c3cf4c 100644 (file)
@@ -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"