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)"
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"
// 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"
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)
{
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)
}
}
- 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);
}
}
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)
float scoreboard_right;
int numplayers;
+int ts_min, ts_max; ///< team size
void Cmd_Scoreboard_SetFields(int argc);
void Scoreboard_Draw();
{
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");
}
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"), "")
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")), "")
// 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)
{
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);
}
TRANSMUTE(Observer, this);
- if(recount_ready) ReadyCount(); // FIXME: please add comment about why this is delayed
-
WaypointSprite_PlayerDead(this);
accuracy_resend(this);
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)
antilag_clear(this, CS(this));
- if (warmup_stage < 0 || warmup_stage > 1)
+ if (warmup_stage)
ReadyCount();
}
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
// 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;
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
}
}
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);
/// \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,
BADPREFIX("g_warmup");
BADPREFIX("sv_info_");
BADPREFIX("sv_ready_restart_");
+ BADCVAR("sv_teamnagger");
BADPRESUFFIXVALUE("g_", "_weaponarena", "most");
BADPRESUFFIXVALUE("g_", "_weaponarena", "most_available");
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"