Automatic per-map min & max player limits, many warmup and player count and Welcome message things
See merge request xonotic/xonotic-data.pk3dir!1022
seta hud_panel_notify_fadetime "3"
seta hud_panel_notify_icon_aspect "1"
-seta hud_panel_timer_pos "0.455000 0"
-seta hud_panel_timer_size "0.090000 0.050000"
+seta hud_panel_timer_pos "0.450000 0"
+seta hud_panel_timer_size "0.100000 0.050000"
seta hud_panel_timer_bg "border_plain_north"
seta hud_panel_timer_bg_color ""
seta hud_panel_timer_bg_color_team ""
seta hud_panel_notify_icon_aspect "2"
seta hud_panel_timer_pos "0.790000 0.040000"
-seta hud_panel_timer_size "0.090000 0.050000"
+seta hud_panel_timer_size "0.100000 0.050000"
seta hud_panel_timer_bg "border_small_timer"
seta hud_panel_timer_bg_color ""
seta hud_panel_timer_bg_color_team ""
seta notification_ANNCE_VOTE_CALL "2" "0 = disabled, 1 = enabled if gentle mode is off, 2 = always enabled"
seta notification_ANNCE_VOTE_FAIL "2" "0 = disabled, 1 = enabled if gentle mode is off, 2 = always enabled"
-// MSG_INFO notifications (count = 335):
+// MSG_INFO notifications (count = 337):
seta notification_INFO_CA_JOIN_LATE "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_INFO_CA_LEAVE "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_INFO_CHAT_NOSPECTATORS "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
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_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_INFO_WEAPON_VAPORIZER_MURDER "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_INFO_WEAPON_VORTEX_MURDER "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
-// MSG_CENTER notifications (count = 242):
+// MSG_CENTER notifications (count = 243):
seta notification_CENTER_ALONE "1" "0 = off, 1 = centerprint"
seta notification_CENTER_ASSAULT_ATTACKING "1" "0 = off, 1 = centerprint"
seta notification_CENTER_ASSAULT_DEFENDING "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_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"
seta notification_CENTER_VEHICLE_STEAL_SELF "1" "0 = off, 1 = centerprint"
seta notification_CENTER_WEAPON_MINELAYER_LIMIT "1" "0 = off, 1 = centerprint"
-// MSG_MULTI notifications (count = 157):
+// MSG_MULTI notifications (count = 158):
+seta notification_COUNTDOWN_BEGIN "1" "Enable this multiple notification"
+seta notification_COUNTDOWN_STOP "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"
seta notification_ITEM_WEAPON_PRIMORSEC "1" "Enable this multiple notification"
seta notification_ITEM_WEAPON_UNAVAILABLE "1" "Enable this multiple notification"
seta notification_MULTI_COINTOSS "1" "Enable this multiple notification"
-seta notification_MULTI_COUNTDOWN_BEGIN "1" "Enable this multiple notification"
seta notification_MULTI_INSTAGIB_FINDAMMO "1" "Enable this multiple notification"
seta notification_WEAPON_ACCORDEON_MURDER "1" "Enable this multiple notification"
seta notification_WEAPON_ACCORDEON_SUICIDE "1" "Enable this multiple notification"
seta notification_show_sprees_info_newline "1" "Show attacker spree information for MSG_INFO messages on a separate line than the death notification itself"
seta notification_show_sprees_info_specialonly "1" "Don't show attacker spree information in MSG_INFO messages if it isn't an achievement"
-// Notification counts (total = 842): MSG_ANNCE = 80, MSG_INFO = 335, MSG_CENTER = 242, MSG_MULTI = 157, MSG_CHOICE = 28
+// Notification counts (total = 846): MSG_ANNCE = 80, MSG_INFO = 337, MSG_CENTER = 243, MSG_MULTI = 158, MSG_CHOICE = 28
if(countdown <= 0) // countdown has finished, starttime is now
{
Local_Notification(MSG_CENTER, CENTER_COUNTDOWN_BEGIN);
- Local_Notification(MSG_MULTI, MULTI_COUNTDOWN_BEGIN);
+ Local_Notification(MSG_MULTI, COUNTDOWN_BEGIN);
delete(this);
announcer_countdown = NULL;
Announcer_ClearTitle();
float roundstarttime = STAT(ROUNDSTARTTIME);
if(roundstarttime > startTime)
startTime = roundstarttime;
- if(intermission)
+ if(intermission || warmup_stage)
{
Announcer_ClearTitle();
if(announcer_countdown)
else
blinkcolor = "^3";
- if(ready_waiting && !spectatee_status)
+ if(warmup_stage && STAT(WARMUP_TIMELIMIT) <= 0 && srv_minplayers)
+ {
+ Scoreboard_UpdatePlayerTeams(); // ensure numplayers is current
+ if(srv_minplayers - numplayers == 1)
+ s = _("^31^2 more player is needed for the match to start.");
+ else
+ s = sprintf(_("^3%d^2 more players are needed for the match to start."), srv_minplayers - numplayers);
+ InfoMessage(s);
+ }
+ else if(ready_waiting && !spectatee_status)
{
if(ready_waiting_for_me)
{
#pragma once
#include "../panel.qh"
+#include "scoreboard.qh" // for Scoreboard_UpdatePlayerTeams()
bool autocvar_hud_panel_infomessages;
bool autocvar_hud_panel_infomessages_dynamichud = false;
update_time = time;
entity pl, tmp;
+ numplayers = 0;
//int num = 0;
for(pl = players.sort_next; pl; pl = pl.sort_next)
{
+ numplayers += pl.team != NUM_SPECTATOR;
//num += 1;
int Team = entcs_GetScoreTeam(pl.sv_entnum);
if(SetTeam(pl, Team))
}
}
drawcolorcodedstring(pos + '1 0 0' * (panel_size.x - stringwidth(str, true, sb_gameinfo_detail_fontsize)), str, sb_gameinfo_detail_fontsize, panel_fg_alpha, DRAWFLAG_NORMAL); // align right
- // map name
- str = sprintf(_("^7Map: ^2%s"), shortmapname);
+ // map name and player count
+ str = sprintf(_("^5%d^7/^5%d ^7players"), numplayers, srv_maxplayers ? srv_maxplayers : maxclients);
+ str = strcat("^7", _("Map:"), " ^2", mi_shortname, " ", str); // reusing "Map:" translatable string
drawcolorcodedstring(pos, str, sb_gameinfo_detail_fontsize, panel_fg_alpha, DRAWFLAG_NORMAL); // align left
}
// End of Game Info Section
float scoreboard_left;
float scoreboard_right;
+int numplayers;
+
void Cmd_Scoreboard_SetFields(int argc);
void Scoreboard_Draw();
void Scoreboard_InitScores();
#include "timer.qh"
+#include "scoreboard.qh"
#include <client/draw.qh>
#include <client/view.qh>
// Use real or frozen time and get the time limit
curtime = (intermission_time ? intermission_time : time);
if(warmup_stage)
- {
timelimit = STAT(WARMUP_TIMELIMIT);
- if(timelimit == 0)
- timelimit = STAT(TIMELIMIT) * 60;
- }
else
- {
timelimit = STAT(TIMELIMIT) * 60;
- }
// Calculate time left
timeleft = HUD_Timer_TimeLeft(curtime, STAT(GAMESTARTTIME), timelimit);
int overtimes = STAT(OVERTIMES);
if(warmup_stage || autocvar__hud_configure)
- subtext = _("Warmup");
+ {
+ if (STAT(WARMUP_TIMELIMIT) > 0)
+ subtext = _("Warmup");
+ else
+ subtext = srv_minplayers ? _("Warmup: too few players") : _("Warmup: no time limit");
+ }
else if(STAT(TIMEOUT_STATUS) == 2)
subtext = _("Timeout");
else if (overtimes == -1)
{
get_mi_min_max_texcoords(1); // try the CLEVER way first
minimapname = strcat("gfx/", mi_shortname, "_radar");
- shortmapname = mi_shortname;
if (precache_pic(minimapname) == "")
{
}
strcpy(hostname, ReadString());
-
string hostversion = ReadString();
bool version_mismatch = ReadByte();
bool version_check = ReadByte();
- string ver = GetVersionMessage(hostversion, version_mismatch, version_check);
-
+ srv_minplayers = ReadByte();
+ srv_maxplayers = ReadByte();
string modifications = translate_modifications(ReadString());
string weaponarena_list = translate_weaponarena(ReadString());
string cache_mutatormsg = ReadString();
string motd = ReadString();
- string msg = "";
- msg = strcat(msg, ver);
- msg = strcat(msg, "^8\n\n", strcat(_("Gametype:"), " ^1", MapInfo_Type_ToText(gametype)), "^8\n");
+ string msg = GetVersionMessage(hostversion, version_mismatch, version_check);
+
+ msg = strcat(msg, "\n\n", _("Gametype:"), " ^1", MapInfo_Type_ToText(gametype), "\n");
+
+ msg = strcat(msg, "\n", _("Map:"), " ^2");
+ if (world.message == "")
+ msg = strcat(msg, mi_shortname, "\n");
+ else
+ {
+ int i = strstrofs(world.message, " by ", 0); // matches _MapInfo_Generate()
+ string longname = i >= 0 ? substring(world.message, 0, i) : world.message;
+ msg = strcat(msg, (strcasecmp(longname, mi_shortname) ? strcat(mi_shortname, " ^7// ^2") : ""), longname, "\n");
+ }
+
+ if (srv_minplayers || srv_maxplayers)
+ {
+ msg = strcat(msg, "\n", _("This match supports"), " ^5");
+ if (srv_minplayers == srv_maxplayers)
+ msg = strcat(msg, sprintf(_("%d players"), srv_maxplayers), "\n");
+ else if (srv_minplayers && srv_maxplayers)
+ msg = strcat(msg, sprintf(_("%d to %d players"), srv_minplayers, srv_maxplayers), "\n");
+ else if (srv_maxplayers)
+ msg = strcat(msg, sprintf(_("%d players maximum"), srv_maxplayers), "\n");
+ else
+ msg = strcat(msg, sprintf(_("%d players minimum"), srv_minplayers), "\n");
+ }
modifications = cons_mid(modifications, ", ", weaponarena_list);
if(modifications != "")
- msg = strcat(msg, "^8\n", _("Active modifications:"), " ^3", modifications, "^8\n");
+ msg = strcat(msg, "\n", _("Active modifications:"), " ^3", modifications, "\n");
if (cache_mutatormsg != "")
- msg = strcat(msg, "\n\n^8", _("Special gameplay tips:"), " ^7", cache_mutatormsg);
+ msg = strcat(msg, "\n", _("Special gameplay tips:"), " ^7", cache_mutatormsg, "\n");
string mutator_msg = "";
MUTATOR_CALLHOOK(BuildGameplayTipsString, mutator_msg);
mutator_msg = M_ARGV(0, string);
msg = strcat(msg, mutator_msg); // trust that the mutator will do proper formatting
if (motd != "")
- msg = strcat(msg, "\n\n^8", _("MOTD:"), " ^7", motd);
+ msg = strcat(msg, "\n^9↓ ", _("Server's message"), " ↓\n", motd);
strcpy(welcome_msg, msg);
welcome_msg_menu_check_maxtime = time + 1; // wait for menu to load before showing the welcome dialog
string hostname;
string welcome_msg;
+int srv_minplayers;
+int srv_maxplayers;
float welcome_msg_menu_check_maxtime;
void Welcome_Message_Show_Try();
#define player_currententnum (spectatee_status > 0 ? spectatee_status : player_localnum + 1)
-// short mapname
-string shortmapname;
-
// database for misc stuff
int tempdb;
int ClientProgsDB;
rr = CTS_RECORD;
else
rr = RACE_RECORD;
- float t = stof(db_get(ClientProgsDB, strcat(shortmapname, rr, "time")));
+ float t = stof(db_get(ClientProgsDB, strcat(mi_shortname, rr, "time")));
if(score && (score < t || !t)) {
- db_put(ClientProgsDB, strcat(shortmapname, rr, "time"), ftos(score));
+ db_put(ClientProgsDB, strcat(mi_shortname, rr, "time"), ftos(score));
if(autocvar_cl_autodemo_delete_keeprecords)
{
float f = autocvar_cl_autodemo_delete;
{
if (value) {
serverflags |= SERVERFLAG_TEAMPLAY;
- teamplay = 1;
+ teamplay = 1; // aka AVAILABLE_TEAMS, updated by ScoreRules_basics() after team ents spawn
cvar_set("teamplay", "2"); // DP needs this for sending proper getstatus replies.
Team_InitTeams();
GameRules_spawning_teams(true);
return r;
}
+bool MapReadSizes(string map)
+{
+ // TODO: implement xonotic#28 / xonvote 172 (sizes in mapinfo)
+ string readsize_msg = strcat("MapReadSizes ", map);
+ float fh = fopen(strcat("maps/", map, ".sizes"), FILE_READ);
+ if(fh >= 0)
+ {
+ map_minplayers = stoi(fgets(fh));
+ map_maxplayers = stoi(fgets(fh));
+ fclose(fh);
+ LOG_TRACEF(readsize_msg, ": ok, min %d max %d", map_minplayers, map_maxplayers);
+ return true;
+ }
+ LOG_TRACE(readsize_msg, ": not found");
+ return false;
+}
+
float MapInfo_FindName(string s)
{
// if there is exactly one map of prefix s, return it
// load info about a map by name into the MapInfo_Map_* globals
int MapInfo_Get_ByName(string s, float allowGenerate, Gametype gametypeToSet); // 1 on success, 0 on failure, 2 if it autogenerated a mapinfo file
+// load map-specific player limits
+int map_minplayers;
+int map_maxplayers;
+bool MapReadSizes(string map);
+
// look for a map by a prefix, returns the actual map name on success, string_null on failure or ambigous match
string MapInfo_FindName_match; // the name of the map that was found
float MapInfo_FindName_firstResult; // -1 if none were found, index of first one if not unique but found (FindName then returns -1)
{
string key = getcommandkey(_("off-hand hook"), "+hook");
M_ARGV(0, string) = strcat(M_ARGV(0, string),
- "\n\n", sprintf(_("^3grappling hook^8 is enabled, press ^3%s^8 to use it"), key), "\n");
+ "\n", sprintf(_("^3grappling hook^8 is enabled, press ^3%s^8 to use it"), key), "\n");
}
}
{
string key = getcommandkey(_("drop weapon / throw nade"), "dropweapon");
M_ARGV(0, string) = strcat(M_ARGV(0, string),
- "\n\n", sprintf(_("^3nades^8 are enabled, press ^3%s^8 to use them"), key), "\n");
+ "\n", sprintf(_("^3nades^8 are enabled, press ^3%s^8 to use them"), key), "\n");
}
}
{
string key = getcommandkey(_("off-hand hook"), "+hook");
M_ARGV(0, string) = strcat(M_ARGV(0, string),
- "\n\n", sprintf(_("^3offhand blaster^8 is enabled, press ^3%s^8 to use it"), key), "\n");
+ "\n", sprintf(_("^3offhand blaster^8 is enabled, press ^3%s^8 to use it"), key), "\n");
}
}
MSG_INFO_NOTIF(CA_JOIN_LATE, N_CONSOLE, 0, 0, "", "", "", _("^F1Round already started, you will join the game in the next round"), "")
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(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_INFO_NOTIF(DEATH_MURDER_DROWN, N_CONSOLE, 3, 2, "spree_inf s1 s2 s3loc spree_end", "s2 s1", "notify_water", _("^BG%s%s^K1 was drowned 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_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_CENTER_NOTIF(ITEM_WEAPON_UNAVAILABLE, N_ENABLE, 0, 1, "item_wepname", CPID_ITEM, "item_centime 0", _("^F1%s^BG is ^F4not available^BG on this map"), "")
MSG_CENTER_NOTIF(JOIN_NOSPAWNS, N_ENABLE, 0, 0, "", CPID_PREVENT_JOIN, "0 0", _("^K1No spawnpoints available!\nHope your team can fix it..."), "")
- MSG_CENTER_NOTIF(JOIN_PREVENT, N_ENABLE, 0, 0, "", CPID_PREVENT_JOIN, "0 0", _("^K1You may not join the game at this time.\nThe player limit reached maximum capacity."), "")
+ MSG_CENTER_NOTIF(JOIN_PREVENT, N_ENABLE, 0, 1, "f1", CPID_PREVENT_JOIN, "0 0", _("^K1You may not join the game at this time.\nThis match is limited to ^F2%s^BG players."), "")
MSG_CENTER_NOTIF(KEEPAWAY_DROPPED, N_ENABLE, 1, 0, "s1", CPID_KEEPAWAY, "0 0", _("^BG%s^BG has dropped the ball!"), "")
MSG_CENTER_NOTIF(KEEPAWAY_PICKUP, N_ENABLE, 1, 0, "s1", CPID_KEEPAWAY, "0 0", _("^BG%s^BG has picked up the ball!"), "")
MSG_MULTI_NOTIF(prefix##_PINK, defaultvalue, anncepre##PINK, infopre##PINK, centerpre##PINK)
// 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(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)
MSG_MULTI_NOTIF(DEATH_MURDER_DROWN, N_ENABLE, NULL, INFO_DEATH_MURDER_DROWN, NULL)
MSG_MULTI_NOTIF(ITEM_WEAPON_UNAVAILABLE, N_ENABLE, NULL, INFO_ITEM_WEAPON_UNAVAILABLE, CENTER_ITEM_WEAPON_UNAVAILABLE)
MSG_MULTI_NOTIF(MULTI_COINTOSS, N_ENABLE, NULL, INFO_COINTOSS, CENTER_COINTOSS)
- MSG_MULTI_NOTIF(MULTI_COUNTDOWN_BEGIN, N_ENABLE, ANNCE_BEGIN, NULL, CENTER_COUNTDOWN_BEGIN)
MSG_MULTI_NOTIF(MULTI_INSTAGIB_FINDAMMO, N_ENABLE, ANNCE_NUM_10, NULL, CENTER_INSTAGIB_FINDAMMO_FIRST)
MSG_MULTI_NOTIF(WEAPON_ACCORDEON_MURDER, N_ENABLE, NULL, INFO_WEAPON_ACCORDEON_MURDER, NULL)
int smallest_count = -1;
if (teamplay)
{
- for (int i = 1; i <= AvailableTeams(); ++i)
+ for (int i = 1; i <= AVAILABLE_TEAMS; ++i)
{
// NOTE if (autocvar_g_campaign && autocvar_g_campaign_forceteam == i)
// TeamBalance_GetNumberOfPlayers(balance, i); returns the number of players + 1
});
if (!conflict)
prio += 1;
- if (teamplay && !(autocvar_bot_vs_human && AvailableTeams() == 2))
+ if (teamplay && !(autocvar_bot_vs_human && AVAILABLE_TEAMS == 2))
{
int forced_team = stof(argv(5));
if (!Team_IsValidIndex(forced_team))
if(argv(4) != "" && stof(argv(4)) >= 0) bot_pants = argv(4);
else bot_pants = ftos(floor(random() * 15));
- if (teamplay && !(autocvar_bot_vs_human && AvailableTeams() == 2))
+ if (teamplay && !(autocvar_bot_vs_human && AVAILABLE_TEAMS == 2))
{
this.bot_forced_team = stof(argv(5));
if (!Team_IsValidIndex(this.bot_forced_team))
// But don't remove bots immediately on level change, as the real players
// usually haven't rejoined yet
bots_would_leave = false;
- if (teamplay && autocvar_bot_vs_human && AvailableTeams() == 2)
+ if (autocvar_bot_vs_human && AVAILABLE_TEAMS == 2)
bots = min(ceil(fabs(autocvar_bot_vs_human) * activerealplayers), maxclients - realplayers);
else if ((realplayers || autocvar_bot_join_empty || (currentbots > 0 && time < 5)))
{
int minplayers = max(0, floor(autocvar_minplayers));
if (teamplay)
- minplayers = max(0, floor(autocvar_minplayers_per_team) * AvailableTeams());
+ minplayers = max(0, floor(autocvar_minplayers_per_team) * AVAILABLE_TEAMS);
int minbots = max(0, floor(autocvar_bot_number));
// add bots to reach minplayers if needed
{
if (vote_called) { VoteCount(false); }
this.ready = false;
- recount_ready = true;
+ if (warmup_stage || game_starttime > time) recount_ready = true;
}
entcs_update_players(this);
}
TRANSMUTE(Observer, this);
- if(recount_ready) ReadyCount();
+ if(recount_ready) ReadyCount(); // FIXME: please add comment about why this is delayed
WaypointSprite_PlayerDead(this);
accuracy_resend(this);
this.alivetime = time;
antilag_clear(this, CS(this));
+
+ if (warmup_stage == -1)
+ ReadyCount();
}
/** Called when a client spawns in the server */
WriteString(msg_type, autocvar_g_xonoticversion);
WriteByte(msg_type, CS(this).version_mismatch);
WriteByte(msg_type, (CS(this).version < autocvar_gameversion));
+ WriteByte(msg_type, map_minplayers);
+ WriteByte(msg_type, GetPlayerLimit());
MUTATOR_CALLHOOK(BuildMutatorsPrettyString, "");
string modifications = M_ARGV(0, string);
if (!autocvar_sv_foginterval && world.fog != "")
stuffcmd(this, strcat("\nfog ", world.fog, "\nr_fog_exp2 0\nr_drawfog 1\n"));
- if (autocvar_sv_teamnagger && !(autocvar_bot_vs_human && AvailableTeams() == 2))
+ if (autocvar_sv_teamnagger && !(autocvar_bot_vs_human && AVAILABLE_TEAMS == 2))
if(!MUTATOR_CALLHOOK(HideTeamNagger, this))
send_CSQC_teamnagger();
if (this.personal) delete(this.personal);
this.playerid = 0;
- ReadyCount();
+ if (warmup_stage || game_starttime > time) ReadyCount();
if (vote_called && IS_REAL_CLIENT(this)) VoteCount(false);
player_powerups_remove_all(this); // stop powerup sound
{
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;
}
/**
static float msg_time = 0;
if(this && !INGAME(this) && ignore && !free_slots && time > msg_time)
{
- Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_JOIN_PREVENT);
+ Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_JOIN_PREVENT, player_limit);
msg_time = time + 0.5;
}
}
caller.last_ready = time;
-
- // cannot reset the game while a timeout is active!
- if (!timeout_status) ReadyCount();
+ ReadyCount();
}
}
return; // never fall through to usage
timeout_leadtime = 0;
delete(this);
+
+ // ReadyCount() does nothing when a timeout is active or pending
+ // so check readiness now to support g_warmup_allow_timeout
+ if (warmup_stage) ReadyCount();
}
void timeout_handler_think(entity this)
if (time <= game_starttime && game_stopped)
return;
if (!is_fake_round_start)
- bprint("^1Match is restarting...\n");
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_COUNTDOWN_RESTART);
VoteReset();
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
+ if (timeout_status) return;
+
float ready_needed_factor, ready_needed_count;
- float t_ready = 0, t_players = 0;
+ float t_players = 0;
+ readycount = 0;
FOREACH_CLIENT(IS_REAL_CLIENT(it) && (IS_PLAYER(it) || INGAME_JOINED(it)), {
++t_players;
- if (it.ready) ++t_ready;
+ if (it.ready) ++readycount;
});
- readycount = t_ready;
-
Nagger_ReadyCounted();
- ready_needed_factor = bound(0.5, cvar("g_warmup_majority_factor"), 0.999);
- ready_needed_count = floor(t_players * ready_needed_factor) + 1;
+ if (t_players < map_minplayers) // map_minplayers will only be set if g_warmup -1 at worldspawn
+ {
+ if (game_starttime > time) // someone bailed during countdown, back to warmup
+ {
+ warmup_stage = -1; // CAN change it AFTER calling Nagger_ReadyCounted() this frame
+ game_starttime = time;
+ Send_Notification(NOTIF_ALL, NULL, MSG_MULTI, COUNTDOWN_STOP, map_minplayers);
+ }
+ 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"), 1);
+ ready_needed_count = ceil(t_players * ready_needed_factor);
if (readycount >= ready_needed_count) ReadyRestart(true);
}
// warmup and nagger stuff
const float RESTART_COUNTDOWN = 10;
entity nagger;
-float readycount; // amount of players who are ready
+int readycount; // amount of players who are ready
.float ready; // flag for if a player is ready
.float last_ready; // last ready time for anti-spam
.int team_saved; // team number to restore upon map reset
-.void(entity this) reset; // if set, an entity is reset using this
+.void(entity this) reset; // if set, an entity is reset using this
.void(entity this) reset2; // if set, an entity is reset using this (after calling ALL the reset functions for other entities)
void reset_map(float dorespawn, bool is_fake_round_start);
void ReadyCount();
{
int minplayers = max(0, floor(autocvar_minplayers));
if (teamplay)
- minplayers = max(0, floor(autocvar_minplayers_per_team) * AvailableTeams());
+ minplayers = max(0, floor(autocvar_minplayers_per_team) * AVAILABLE_TEAMS);
if (autocvar_g_maplist_check_waypoints
&& (currentbots || autocvar_bot_number || player_count < minplayers))
{
return true;
// open map size restriction file
- string opensize_msg = strcat("opensize ", map);
- float fh = fopen(strcat("maps/", map, ".sizes"), FILE_READ);
+ if(!MapReadSizes(map))
+ return true; // map has no size restrictions
+
+ string checksize_msg = strcat("MapHasRightSize ", map);
int player_limit = ((autocvar_g_maplist_sizes_count_maxplayers) ? GetPlayerLimit() : 0);
int pcount = ((player_limit > 0) ? min(player_count, player_limit) : player_count); // bind it to the player limit so that forced spectators don't influence the limits
+
if(!autocvar_g_maplist_sizes_count_bots)
pcount -= currentbots;
- if(fh >= 0)
+ pcount -= rint(cvar("g_maplist_sizes_specparty") * pcount);
+
+ // ensure small maps can be selected when pcount is low
+ if(map_minplayers <= (_MapInfo_GetTeamPlayBool(MapInfo_CurrentGametype()) ? 4 : 2))
+ map_minplayers = 0;
+
+ if(pcount < map_minplayers)
{
- opensize_msg = strcat(opensize_msg, ": ok, ");
- int mapmin = stoi(fgets(fh));
- int mapmax = stoi(fgets(fh));
- fclose(fh);
- if(pcount < mapmin)
- {
- LOG_TRACE(opensize_msg, "not enough");
- return false;
- }
- if(mapmax && pcount > mapmax)
- {
- LOG_TRACE(opensize_msg, "too many");
- return false;
- }
- LOG_TRACE(opensize_msg, "right size");
- return true;
+ LOG_TRACE(checksize_msg, ": not enough");
+ return false;
+ }
+ if(map_maxplayers && pcount > map_maxplayers)
+ {
+ LOG_TRACE(checksize_msg, ": too many");
+ return false;
}
- LOG_TRACE(opensize_msg, ": not found");
+ LOG_TRACE(checksize_msg, ": right size");
return true;
}
return boolean(teams & BIT(0)) + boolean(teams & BIT(1)) + boolean(teams & BIT(2)) + boolean(teams & BIT(3));
}
-int AvailableTeams()
-{
- return NumTeams(ScoreRules_teams);
-}
-
// NOTE: ST_constants may not be >= MAX_TEAMSCORE
// scores that should be in all modes:
void ScoreRules_basics(int teams, float sprio, float stprio, float score_enabled)
ScoreInfo_SetLabel_TeamScore(i, "", 0);
ScoreRules_teams = teams;
+ AVAILABLE_TEAMS = NumTeams(teams);
if(score_enabled)
ScoreInfo_SetLabel_TeamScore(ST_SCORE, "score", stprio);
bool IsTeamAvailable(int team_num);
int NumTeams(int teams);
-int AvailableTeams();
void ScoreRules_basics(int teams, float sprio, float stprio, float score_enabled);
void ScoreRules_basics_end();
void ScoreRules_generic();
+
+#define AVAILABLE_TEAMS teamplay
}
// TODO: Balance quantity of bots across > 2 teams when bot_vs_human is set (and remove next line)
- if (autocvar_bot_vs_human && AvailableTeams() == 2 && for_whom)
+ if (autocvar_bot_vs_human && AVAILABLE_TEAMS == 2 && for_whom)
{
if (autocvar_bot_vs_human > 0)
{
BADCVAR("g_start_delay");
BADCVAR("g_superspectate");
BADCVAR("g_tdm_teams_override");
- BADCVAR("g_warmup");
BADCVAR("g_weapon_stay"); BADPRESUFFIX("g_", "_weapon_stay");
BADCVAR("hostname");
BADCVAR("log_file");
BADVALUE("sys_ticrate", "0.0333333");
BADCVAR("teamplay_mode");
BADCVAR("timelimit_override");
- BADPREFIX("g_warmup_");
+ BADPREFIX("g_warmup");
BADPREFIX("sv_info_");
BADPREFIX("sv_ready_restart_");
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()
GameRules_limit_fallbacks();
if(warmup_limit == 0)
- warmup_limit = (autocvar_timelimit > 0) ? autocvar_timelimit * 60 : autocvar_timelimit;
+ warmup_limit = autocvar_timelimit * 60;
player_count = 0;
bot_waypoints_for_items = autocvar_g_waypoints_for_items;
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;
//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"
-set g_warmup_majority_factor 0.8 "minimum percentage of players ready needed for warmup to end"
+set g_warmup_majority_factor 0.8 "fraction of joined players sufficient to end warmup before g_warmup_limit by readying up"
alias sv_hook_warmupend
set g_maplist_ignore_sizes 0 "when 1, all maps are shown in the map list regardless of player count"
set g_maplist_sizes_count_maxplayers 1 "check the player limit when getting the player count so forced spectators don't affect the size restrictions"
set g_maplist_sizes_count_bots 1 "include the number of bots currently in the server when counting the number of players for size restrictions"
+set g_maplist_sizes_specparty 0 "this fraction of people are expected to only spectate, reduces player count used to select voting GUI maps"
set g_items_mindist 4000 "starting distance for the fading of items"
set g_items_maxdist 4500 "maximum distance at which an item can be viewed, after which it will be invisible"