seta notification_INFO_JOIN_CONNECT "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_INFO_JOIN_PLAY "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_INFO_JOIN_PLAY_TEAM "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
+seta notification_INFO_JOIN_WANTS "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
+seta notification_INFO_JOIN_WANTS_TEAM "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_INFO_KEEPAWAY_DROPPED "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_INFO_KEEPAWAY_PICKUP "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_INFO_KEYHUNT_CAPTURE "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_INFO_MINIGAME_INVITE "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_INFO_MONSTERS_DISABLED "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_INFO_MOVETOSPEC_IDLING "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
+seta notification_INFO_MOVETOSPEC_IDLING_QUEUE "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
+seta notification_INFO_MOVETOSPEC_REMOVE "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_INFO_NEXBALL_RETURN_HELD "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_INFO_ONSLAUGHT_CAPTURE "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_INFO_ONSLAUGHT_CAPTURE_NONAME "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_INFO_QUIT_KICK_SPECTATING "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_INFO_QUIT_KICK_TEAMKILL "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_INFO_QUIT_PLAYBAN_TEAMKILL "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
+seta notification_INFO_QUIT_QUEUE "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_INFO_QUIT_SPECTATE "2" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_INFO_RACE_ABANDONED "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_INFO_RACE_FAIL_RANKED "1" "0 = off, 1 = print to console, 2 = print to console and chatbox (if notification_allow_chatboxprint is enabled)"
seta notification_CENTER_JOIN_PLAYBAN "1" "0 = off, 1 = centerprint"
seta notification_CENTER_JOIN_PREVENT "1" "0 = off, 1 = centerprint"
seta notification_CENTER_JOIN_PREVENT_MINIGAME "1" "0 = off, 1 = centerprint"
+seta notification_CENTER_JOIN_PREVENT_QUEUE "1" "0 = off, 1 = centerprint"
+seta notification_CENTER_JOIN_PREVENT_QUEUE_TEAM_FAIL "1" "0 = off, 1 = centerprint"
+seta notification_CENTER_JOIN_PREVENT_QUEUE_TEAM "1" "0 = off, 1 = centerprint"
seta notification_CENTER_KEEPAWAY_DROPPED "1" "0 = off, 1 = centerprint"
seta notification_CENTER_KEEPAWAY_PICKUP "1" "0 = off, 1 = centerprint"
seta notification_CENTER_KEEPAWAY_PICKUP_SELF "1" "0 = off, 1 = centerprint"
seta notification_CENTER_MISSING_PLAYERS "1" "0 = off, 1 = centerprint"
seta notification_CENTER_MISSING_TEAMS "1" "0 = off, 1 = centerprint"
seta notification_CENTER_MOVETOSPEC_IDLING "1" "0 = off, 1 = centerprint"
+seta notification_CENTER_MOVETOSPEC_REMOVE "1" "0 = off, 1 = centerprint"
seta notification_CENTER_NADE_BONUS "1" "0 = off, 1 = centerprint"
seta notification_CENTER_NADE_THROW "1" "0 = off, 1 = centerprint"
seta notification_CENTER_NIX_COUNTDOWN "1" "0 = off, 1 = centerprint"
if(!mutator_returnvalue)
{
- s = sprintf(_("^1Press ^3%s^1 to join"), getcommandkey(_("jump"), "+jump"));
+ if(entcs_GetWantsJoin(current_player))
+ {
+ int tm = Team_IndexToTeam(entcs_GetWantsJoin(current_player));
+ s = sprintf(_("^2You're queued to join the %s%s^2 team"), Team_ColorCode(tm), Team_ColorName(tm));
+ }
+ else
+ s = sprintf(_("^1Press ^3%s^1 to join"), getcommandkey(_("jump"), "+jump"));
InfoMessage(s);
}
}
if(pl == ignored_pl)
continue;
+ if(entcs_GetWantsJoin(pl.sv_entnum))
+ {
+ vector tmcolor = Team_ColorRGB(Team_IndexToTeam(entcs_GetWantsJoin(pl.sv_entnum)));
+ tmcolor -= tmcolor * sin(2*M_PI*time);
+
+ drawstring(pos, "(Q)", hud_fontsize, tmcolor, sbt_fg_alpha, DRAWFLAG_NORMAL);
+ pos.x += stringwidth("(Q) ", true, hud_fontsize);
+ }
+
field = "";
if(this_team == NUM_SPECTATOR)
{
{ WriteShort(chan, ent.frags); },
{ ent.frags = ReadShort(); })
+ENTCS_PROP(WANTSJOIN, true, wants_join, wants_join, ENTCS_SET_NORMAL,
+ { WriteByte(chan, ent.wants_join); },
+ { ent.wants_join = ReadByte(); })
+
// use sv_solid to avoid changing solidity state of entcs entities
ENTCS_PROP(SOLID, true, sv_solid, solid, ENTCS_SET_NORMAL,
{ WriteByte(chan, ent.sv_solid); },
* @param i zero indexed player
*/
.int frags;
+ .int wants_join;
const int ENTCS_SPEC_PURE = 1; // real spectator
const int ENTCS_SPEC_IN_SCOREBOARD = 2; // spectator but still in game (can be in a team)
#define entcs_IsSpectating(i) boolean(entcs_GetSpecState(i))
/**
* @param i zero indexed player
+ */
+ int entcs_GetWantsJoin(int i)
+ {
+ entity e = entcs_receiver(i);
+ return e.wants_join;
+ }
+
+ /**
+ * @param i zero indexed player
*/
int entcs_GetClientColors(int i)
{
MSG_INFO_NOTIF(JOIN_CONNECT, N_CHATCON, 1, 0, "s1", "", "", _("^BG%s^F3 connected"), "")
MSG_INFO_NOTIF(JOIN_PLAY, N_CHATCON, 1, 0, "s1", "", "", _("^BG%s^F3 is now playing"), "")
MULTITEAM_INFO(JOIN_PLAY_TEAM, N_CHATCON, 1, 0, "s1", "", "", _("^BG%s^F3 is now playing on the ^TC^TT team"), "", NAME)
+ MULTITEAM_INFO(JOIN_WANTS_TEAM, N_CHATCON, 1, 0, "s1", "", "", _("^BG%s^F3 wants to play on the ^TC^TT team"), "", NAME)
+ MSG_INFO_NOTIF(JOIN_WANTS, N_CHATCON, 1, 0, "s1", "", "", _("^BG%s^F3 wants to play"), "")
MSG_INFO_NOTIF(KEEPAWAY_DROPPED, N_CONSOLE, 1, 0, "s1", "s1", "notify_balldropped", _("^BG%s^BG has dropped the ball!"), "")
MSG_INFO_NOTIF(KEEPAWAY_PICKUP, N_CONSOLE, 1, 0, "s1", "s1", "notify_ballpickedup", _("^BG%s^BG has picked up the ball!"), "")
MSG_INFO_NOTIF(QUIT_DISCONNECT, N_CHATCON, 1, 0, "s1", "", "", _("^BG%s^F3 disconnected"), "")
MSG_INFO_NOTIF(QUIT_KICK_IDLING, N_CHATCON, 1, 1, "s1 f1", "", "", _("^BG%s^F3 was kicked after idling for %s seconds"), "")
MSG_INFO_NOTIF(MOVETOSPEC_IDLING, N_CHATCON, 1, 1, "s1 f1", "", "", _("^BG%s^F3 was moved to^BG spectators^F3 after idling for %s seconds"), "")
+ MSG_INFO_NOTIF(MOVETOSPEC_IDLING_QUEUE, N_CHATCON, 1, 1, "s1 f1", "", "", _("^BG%s^F3 has left the queue after idling for %s seconds"), "")
+ MSG_INFO_NOTIF(MOVETOSPEC_REMOVE, N_CHATCON, 1, 0, "s1", "", "", _("^BG%s^F3 was moved to^BG spectators^F3 for balance reasons"), "")
MSG_INFO_NOTIF(QUIT_KICK_SPECTATING, N_CONSOLE, 0, 0, "", "", "", _("^F2You were kicked from the server because you are a spectator and spectators aren't allowed at the moment."), "")
MSG_INFO_NOTIF(QUIT_KICK_TEAMKILL, N_CHATCON, 1, 0, "s1", "", "", _("^BG%s^F3 was kicked for excessive teamkilling"), "")
MSG_INFO_NOTIF(QUIT_PLAYBAN_TEAMKILL, N_CHATCON, 1, 0, "s1", "", "", _("^BG%s^F3 was forced to spectate for excessive teamkilling"), "")
MSG_INFO_NOTIF(QUIT_SPECTATE, N_CHATCON, 1, 0, "s1", "", "", _("^BG%s^F3 is now^BG spectating"), "")
+ MSG_INFO_NOTIF(QUIT_QUEUE, N_CHATCON, 1, 0, "s1", "", "", _("^BG%s^F3 has left the queue"), "")
MSG_INFO_NOTIF(RACE_ABANDONED, N_CONSOLE, 1, 0, "s1", "", "", _("^BG%s^BG has abandoned the race"), "")
MSG_INFO_NOTIF(RACE_FAIL_RANKED, N_CONSOLE, 1, 3, "s1 race_col f1ord race_col f3race_time race_diff", "s1 f3race_time", "race_newfail", _("^BG%s^BG couldn't break their %s%s^BG place record of %s%s %s"), "")
MSG_CENTER_NOTIF(DISCONNECT_IDLING, N_ENABLE, 0, 1, "", CPID_IDLING, "1 f1", BOLD(_("^K1Stop idling!\n^BGDisconnecting in ^COUNT...")), "")
MSG_CENTER_NOTIF(MOVETOSPEC_IDLING, N_ENABLE, 0, 1, "", CPID_IDLING, "1 f1", BOLD(_("^K1Stop idling!\n^BGMoving to spectators in ^COUNT...")), "")
+ MSG_CENTER_NOTIF(MOVETOSPEC_REMOVE, N_ENABLE, 1, 1, "s1", CPID_REMOVE, "1 f1", BOLD(_("^K1Teams unbalanced!\n^BGMoving %s^BG to spectators in ^COUNT...")), "")
MSG_CENTER_NOTIF(DOOR_LOCKED_NEED, N_ENABLE, 1, 0, "s1", CPID_Null, "0 0", _("^BGYou need %s^BG!"), "")
MSG_CENTER_NOTIF(DOOR_LOCKED_ALSONEED, N_ENABLE, 1, 0, "s1", CPID_Null, "0 0", _("^BGYou also need %s^BG!"), "")
MSG_CENTER_NOTIF(JOIN_PLAYBAN, N_ENABLE, 0, 0, "", CPID_PREVENT_JOIN, "0 0", BOLD(_("^K1You aren't allowed to play because you are banned in this server")), "")
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(JOIN_PREVENT_MINIGAME, N_ENABLE, 0, 0, "", CPID_Null, "0 0", _("^K1Cannot join given minigame session!"), "" )
+ MSG_CENTER_NOTIF(JOIN_PREVENT_QUEUE, N_ENABLE, 0, 0, "", CPID_PREVENT_JOIN, "0 0", _("^BGYou are now queued to join the game."), "")
+ MULTITEAM_CENTER(JOIN_PREVENT_QUEUE_TEAM, N_ENABLE, 0, 0, "", CPID_PREVENT_JOIN, "0 0", _("^BGYou are now queued to join on ^TC^TT^BG team."), "", NAME)
+ MULTITEAM_CENTER(JOIN_PREVENT_QUEUE_TEAM_FAIL, N_ENABLE, 1, 0, "s1", CPID_PREVENT_JOIN, "0 0", _("^K2Please choose a different team! %s^K2 chose ^TC^TT^BG first."), "", NAME)
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!"), "")
CASE(CPID, STALEMATE)
CASE(CPID, NADES)
CASE(CPID, IDLING)
+ CASE(CPID, REMOVE)
CASE(CPID, ITEM)
CASE(CPID, PREVENT_JOIN)
CASE(CPID, KEEPAWAY)
case NUM_TEAM_4: return '1 0.0625 1'; // 0xFF0FFF
}
- return '0 0 0';
+ return '1 1 1';
}
string Team_ColorName(int teamid)
GameLogEcho(strcat(":join:", ftos(this.playerid), ":", ftos(etof(this)), ":", ((IS_REAL_CLIENT(this)) ? GameLog_ProcessIP(this.netaddress) : "bot"), ":", playername(this.netname, this.team, false)));
CS(this).just_joined = true; // stop spamming the eventlog with additional lines when the client connects
+ this.wants_join = 0;
stuffcmd(this, clientstuff, "\n");
stuffcmd(this, "cl_particles_reloadeffects\n"); // TODO do we still need this?
if (player_count == 0)
localcmd("\nsv_hook_lastleave\n");
+
+ if (!TeamBalance_QueuedPlayersTagIn(this))
+ if (autocvar_g_balance_teams_remove)
+ TeamBalance_RemoveExcessPlayers(NULL);
}
void ChatBubbleThink(entity this)
}
}
-.bool team_selected;
bool ShowTeamSelection(entity this)
{
if (!teamplay || autocvar_g_campaign || autocvar_g_balance_teams || this.team_selected || (CS(this).wasplayer && autocvar_g_changeteam_banned) || Player_HasRealForcedTeam(this))
return false;
+ if (QueuedPlayersReady(this, true))
+ return false;
if (frametime) // once per frame is more than enough
stuffcmd(this, "_scoreboard_team_selection 1\n");
return true;
}
-void Join(entity this)
+
+void Join(entity this, bool queued_join)
{
+ bool teamautoselect = autocvar_g_campaign || autocvar_g_balance_teams || this.wants_join < 0;
+
if (autocvar_g_campaign && !campaign_bots_may_start && !game_stopped && time >= game_starttime)
ReadyRestart(true);
TRANSMUTE(Player, this);
- if(!this.team_selected)
- if(autocvar_g_campaign || autocvar_g_balance_teams)
+ if(queued_join)
+ {
+ // First we must put queued player(s) in their team(s) (they chose first).
+ FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != this && it.wants_join,
+ {
+ Join(it, false);
+ // ensure TeamBalance_JoinBestTeam will run if necessary for `this`
+ teamautoselect = true;
+ });
+ }
+
+ if(!this.team_selected && teamautoselect)
TeamBalance_JoinBestTeam(this);
if(autocvar_g_campaign)
if(IS_PLAYER(this))
if(teamplay && this.team != -1)
{
+ if(this.wants_join)
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_JOIN_PLAY_TEAM), this.netname);
}
else
Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_PLAY, this.netname);
this.team_selected = false;
+ this.wants_join = 0;
}
int GetPlayerLimit()
return free_slots;
}
+bool queuePlayer(entity this, int team_index)
+{
+ if(IS_BOT_CLIENT(this) || !IS_QUEUE_NEEDED(this) || QueuedPlayersReady(this, false))
+ return false;
+
+ if(team_index <= 0)
+ {
+ // defer team selection until Join()
+ this.wants_join = -1;
+ this.team_selected = false;
+ this.team = -1;
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_WANTS, this.netname);
+ Send_Notification(NOTIF_ONE, this, MSG_CENTER, CENTER_JOIN_PREVENT_QUEUE);
+ }
+ else
+ {
+ this.wants_join = team_index; // Player queued to join
+ this.team_selected = true; // no autoselect in Join()
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_JOIN_WANTS_TEAM), this.netname);
+ Send_Notification(NOTIF_ONE, this, MSG_CENTER, APP_TEAM_NUM(this.team, CENTER_JOIN_PREVENT_QUEUE_TEAM));
+ }
+
+ return true;
+}
+
bool joinAllowed(entity this)
{
if (CS(this).version_mismatch) return false;
if (teamplay && lockteams) return false;
if (MUTATOR_CALLHOOK(ForbidSpawn, this)) return false;
if (ShowTeamSelection(this)) return false;
+ if (this.wants_join) return false;
+ if (queuePlayer(this, 0)) return false;
return true;
}
{
this.flags &= ~FL_SPAWNING;
if(joinAllowed(this))
- Join(this);
+ Join(this, true);
else if(time < CS(this).jointime + MIN_SPEC_TIME)
CS(this).autojoin_checked = -1;
return;
&& (!teamplay || autocvar_g_balance_teams)))
{
if(joinAllowed(this))
- Join(this);
+ Join(this, true);
return;
}
}
// formerly PostThink code
- if (autocvar_sv_maxidle > 0 || (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0))
+ if (autocvar_sv_maxidle > 0 || ((IS_PLAYER(this) || this.wants_join) && autocvar_sv_maxidle_playertospectator > 0))
if (IS_REAL_CLIENT(this))
- if (IS_PLAYER(this) || autocvar_sv_maxidle_alsokickspectators)
+ if (IS_PLAYER(this) || this.wants_join || autocvar_sv_maxidle_alsokickspectators)
if (!intermission_running) // NextLevel() kills all centerprints after setting this true
{
int totalClients = 0;
totalClients = 0;
}
}
- else if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
+ else if ((IS_PLAYER(this) || this.wants_join) && autocvar_sv_maxidle_playertospectator > 0)
{
FOREACH_CLIENT(IS_REAL_CLIENT(it),
{
else
{
float maxidle_time = autocvar_sv_maxidle;
- if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
+ if ((IS_PLAYER(this) || this.wants_join)
+ && autocvar_sv_maxidle_playertospectator > 0)
maxidle_time = autocvar_sv_maxidle_playertospectator;
float timeleft = ceil(maxidle_time - (time - CS(this).parm_idlesince));
float countdown_time = max(min(10, maxidle_time - 1), ceil(maxidle_time * 0.33)); // - 1 to support maxidle_time <= 10
if (timeleft == countdown_time && !CS(this).idlekick_lasttimeleft)
{
- if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
- Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_MOVETOSPEC_IDLING, timeleft);
+ if ((IS_PLAYER(this) || this.wants_join) && autocvar_sv_maxidle_playertospectator > 0)
+ {
+ if (!this.wants_join) // no countdown centreprint when getting kicked off the join queue
+ Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_MOVETOSPEC_IDLING, timeleft);
+ }
else
Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_DISCONNECT_IDLING, timeleft);
}
- if (timeleft <= 0) {
- if (IS_PLAYER(this) && autocvar_sv_maxidle_playertospectator > 0)
+ if (timeleft <= 0)
+ {
+ if ((IS_PLAYER(this) || this.wants_join)
+ && autocvar_sv_maxidle_playertospectator > 0)
{
- Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MOVETOSPEC_IDLING, this.netname, maxidle_time);
+ if (this.wants_join)
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MOVETOSPEC_IDLING_QUEUE, this.netname, maxidle_time);
+ else
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MOVETOSPEC_IDLING, this.netname, maxidle_time);
PutObserverInServer(this, true, true);
+ // when the player is kicked off the server, these are called in ClientDisconnect()
+ if (!TeamBalance_QueuedPlayersTagIn(this))
+ if (autocvar_g_balance_teams_remove)
+ TeamBalance_RemoveExcessPlayers(this);
}
else
{
}
return;
}
- else if (timeleft <= countdown_time) {
+ else if (timeleft <= countdown_time
+ && !this.wants_join) // no countdown bangs when getting kicked off the join queue
+ {
if (timeleft != CS(this).idlekick_lasttimeleft)
play2(this, SND(TALK2));
CS(this).idlekick_lasttimeleft = timeleft;
.int spectatee_status;
.bool zoomstate;
+.bool team_selected;
.bool just_joined;
+.bool wants_join;
.int pressedkeys;
ATTRIB(Client, teamkill_soundsource, entity, this.teamkill_soundsource);
ATTRIB(Client, usekeypressed, bool, this.usekeypressed);
ATTRIB(Client, jointime, float, this.jointime);
+ ATTRIB(Client, wants_join, bool, this.wants_join);
ATTRIB(Client, spectatortime, float, this.spectatortime);
ATTRIB(Client, startplaytime, float, this.startplaytime);
ATTRIB(Client, version_nagtime, float, this.version_nagtime);
void ClientData_Touch(entity e);
-int nJoinAllowed(entity this, entity ignore);
-
void PlayerUseKey(entity this);
void FixClientCvars(entity e);
int GetPlayerLimit();
const int MIN_SPEC_TIME = 1;
+void Join(entity this, bool queued_join);
+int nJoinAllowed(entity this, entity ignore);
+bool queuePlayer(entity this, int team_index);
bool joinAllowed(entity this);
-void Join(entity this);
void PlayerFrame (entity this);
{
if (blockSpectators)
Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_SPECTATE_WARNING, autocvar_g_maxplayers_spectator_blocktime);
- PutObserverInServer(this, false, true);
+
+ if (this.wants_join)
+ {
+ this.wants_join = 0;
+ this.team_selected = false;
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_QUEUE, this.netname);
+ SetPlayerTeam(this, -1, TEAM_CHANGE_SPECTATOR);
+ }
+ else
+ {
+ PutObserverInServer(this, false, true);
+ if (!TeamBalance_QueuedPlayersTagIn(this))
+ if (autocvar_g_balance_teams_remove)
+ TeamBalance_RemoveExcessPlayers(this);
+ }
}
else
{
if (!game_stopped && IS_CLIENT(caller) && !IS_PLAYER(caller))
{
if (joinAllowed(caller))
- Join(caller);
+ Join(caller, true);
else if(time < CS(caller).jointime + MIN_SPEC_TIME)
CS(caller).autojoin_checked = -1;
}
if (team_num)
ClientKill_TeamChange(caller, team_num);
else // auto
- ClientKill_TeamChange(caller, -1);
+ if (!queuePlayer(caller, 0)) // the queue uses deferred autoselect
+ ClientKill_TeamChange(caller, -1);
- if (!IS_PLAYER(caller))
- {
- caller.team_selected = true; // avoids asking again for team selection on join
- }
return;
}
default:
if (mutator_returnvalue == MUT_SPECCMD_RETURN) return;
- if ((IS_PLAYER(caller) || mutator_returnvalue == MUT_SPECCMD_FORCE))
+ if ((IS_PLAYER(caller) || mutator_returnvalue == MUT_SPECCMD_FORCE || caller.wants_join))
if (autocvar_sv_spectate == 1)
ClientKill_TeamChange(caller, -2); // observe
}
setthink(e, dropclient_do);
e.owner = this;
e.nextthink = time + 0.1;
+
+ // ignore this player for team balancing and queuing
+ this.team = -1;
+ this.wants_join = 0;
+ this.classname = STR_OBSERVER;
return true;
}
return true;
}
+/** Returns true when enough players are queued that the next will join directly
+ * to the only available team (also triggering the joins of the queued players).
+ * Optionally only counts players who selected a specific team when joining the queue.
+ */
+bool QueuedPlayersReady(entity this, bool checkspecificteam)
+{
+ int numplayersqueued = 0;
+
+ FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != this
+ && (checkspecificteam ? it.wants_join > 0 : it.wants_join),
+ {
+ LOG_DEBUGF("Player %s is waiting to join team %d", it.netname, it.wants_join);
+ ++numplayersqueued;
+ if (numplayersqueued >= AVAILABLE_TEAMS - 1)
+ return true;
+ });
+
+ LOG_DEBUG("No players waiting to join.");
+ return false;
+}
+
bool SetPlayerTeam(entity player, int team_index, int type)
{
int old_team_index = Entity_GetTeamIndex(player);
TeamBalance_AutoBalanceBots();
if (team_index != -1)
- Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(player.team, INFO_JOIN_PLAY_TEAM), player.netname);
+ {
+ if (!queuePlayer(player, team_index))
+ {
+ 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 (team_index == -1)
}
}
TeamBalance_Destroy(balance);
+
+ // g_balance_teams_queue: before joining the queue,
+ // check if a queued player already chose the selected team
+ if (!IS_BOT_CLIENT(player) && IS_QUEUE_NEEDED(player))
+ {
+ FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != player && it.wants_join == team_index,
+ {
+ Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(Team_IndexToTeam(team_index), CENTER_JOIN_PREVENT_QUEUE_TEAM_FAIL), it.netname);
+ player.team_selected = false;
+ return;
+ });
+ }
+
SetPlayerTeam(player, team_index, TEAM_CHANGE_MANUAL);
}
return result;
}
+bool TeamBalance_AreEqual(entity ignore, bool would_leave)
+{
+ entity balance = TeamBalance_CheckAllowedTeams(ignore);
+ TeamBalance_GetTeamCounts(balance, ignore);
+
+ bool equality = true;
+ int total;
+ int prev_total = 0;
+ int bots = 0;
+
+ for(int i = 1; i <= AVAILABLE_TEAMS; ++i)
+ {
+ total = TeamBalance_GetTeamFromIndex(balance, i).m_num_players;
+ bots += TeamBalance_GetTeamFromIndex(balance, i).m_num_bots;
+ if(i > 1 && total != prev_total)
+ {
+ equality = false;
+ break;
+ }
+ prev_total = total;
+ }
+ TeamBalance_Destroy(balance);
+
+ // Ignore if there are "ghost" bots that would leave if anyone joined
+ if (would_leave && bots > autocvar_bot_number)
+ return false;
+
+ return equality;
+}
+
+entity remove_countdown;
+void Remove_Countdown(entity this)
+{
+ if(this.lifetime <= 0 || TeamBalance_AreEqual(NULL, false))
+ {
+ if(this.lifetime <= 0)
+ {
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MOVETOSPEC_REMOVE, playername(remove_countdown.enemy.netname, remove_countdown.enemy.team, true));
+ PutObserverInServer(remove_countdown.enemy, true, true);
+ }
+
+ Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_REMOVE);
+
+ delete(this);
+ remove_countdown = NULL;
+
+ TeamBalance_RemoveExcessPlayers(NULL); // Check again for excess players in case someone also left while in countdown
+ return;
+ }
+
+ --this.lifetime;
+ this.nextthink = time + 1;
+}
+
+// FIXME: support more than 2 teams, the notification will be... awkward
+// FIXME: also don't kick the fc/bc/kc lol
+void TeamBalance_RemoveExcessPlayers(entity ignore)
+{
+ if(AVAILABLE_TEAMS != 2 || autocvar_g_campaign) return;
+
+ entity balance = TeamBalance_CheckAllowedTeams(ignore);
+ TeamBalance_GetTeamCounts(balance, ignore);
+
+ int min = 0;
+
+ for(int i = 1; i <= AVAILABLE_TEAMS; ++i)
+ {
+ int cur = TeamBalance_GetTeamFromIndex(balance, i).m_num_players;
+ if(i == 1 || cur < min)
+ min = cur;
+ }
+
+ for(int tmi = 1; tmi <= AVAILABLE_TEAMS; ++tmi)
+ {
+ int cur = TeamBalance_GetTeamFromIndex(balance, tmi).m_num_players;
+ if(cur > 0 && cur > min) // If this team has excess players
+ {
+ // Get newest player
+ int latest_join = 0;
+ entity latest_join_pl = NULL;
+
+ FOREACH_CLIENT(IS_REAL_CLIENT(it) || INGAME(it), {
+ if(it.team == Team_IndexToTeam(tmi) && CS(it).startplaytime > latest_join)
+ {
+ latest_join = CS(it).startplaytime;
+ latest_join_pl = it;
+ }
+ });
+
+ // Force player to spectate
+ if(latest_join_pl)
+ {
+ // Send player to spectate
+ if(autocvar_g_balance_teams_remove_wait)
+ {
+ // Give a warning before moving to spect
+ if (!remove_countdown)
+ {
+ remove_countdown = new_pure(remove_countdown);
+ setthink(remove_countdown, Remove_Countdown);
+ remove_countdown.nextthink = time;
+ Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_MOVETOSPEC_REMOVE, playername(latest_join_pl.netname, latest_join_pl.team, true), autocvar_g_balance_teams_remove_wait);
+ }
+ remove_countdown.enemy = latest_join_pl;
+ remove_countdown.lifetime = autocvar_g_balance_teams_remove_wait;
+ }
+ else
+ {
+ // Move to spects immediately
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MOVETOSPEC_REMOVE, latest_join_pl.netname);
+ PutObserverInServer(latest_join_pl, true, true);
+ }
+ }
+ }
+ }
+
+ TeamBalance_Destroy(balance);
+}
+
+bool TeamBalance_QueuedPlayersTagIn(entity ignore)
+{
+ if (!teamplay)
+ return true;
+
+ bool balanced = true;
+ int j, teamplayers, teamplayers_max = 0;
+ entity balance = TeamBalance_CheckAllowedTeams(ignore);
+
+ TeamBalance_GetTeamCounts(balance, ignore);
+
+ // find the target player count
+ for (j = 1; j <= AVAILABLE_TEAMS; ++j)
+ {
+ teamplayers = TeamBalance_GetTeamFromIndex(balance, j).m_num_players;
+ if(teamplayers > teamplayers_max)
+ teamplayers_max = teamplayers;
+ }
+
+ for (j = 1; j <= AVAILABLE_TEAMS; ++j)
+ {
+ teamplayers = TeamBalance_GetTeamFromIndex(balance, j).m_num_players;
+ // first pass: find player(s) who want to play on this specific team
+ FOREACH_CLIENT(it != ignore && it.wants_join == j,
+ {
+ if (teamplayers >= teamplayers_max)
+ break;
+ Join(it, false);
+ ++teamplayers;
+ });
+ // second pass: find player(s) who want to play on any team
+ FOREACH_CLIENT(it != ignore && it.wants_join < 0,
+ {
+ if (teamplayers >= teamplayers_max)
+ break;
+ Join(it, false);
+ ++teamplayers;
+ });
+ if (teamplayers < teamplayers_max)
+ balanced = false;
+ }
+
+ TeamBalance_Destroy(balance);
+ return balanced;
+}
+
bool TeamBalance_IsTeamAllowed(entity balance, int index)
{
if (balance == NULL)
{
continue;
}
+ if (it.wants_join)
+ {
+ continue; // Queued players aren't actually in the game.
+ }
int team_num;
// TODO: Reconsider when the player is truly on the team.
if (IS_CLIENT(it) || INGAME(it))
bool autocvar_g_balance_teams;
bool autocvar_g_balance_teams_prevent_imbalance;
+bool autocvar_g_balance_teams_queue;
+bool autocvar_g_balance_teams_remove;
+int autocvar_g_balance_teams_remove_wait;
string autocvar_g_forced_team_otherwise;
.int team_forced; // can be a team number to force a team, or 0 for default action, or -1 for forced spectator
+#define IS_QUEUE_NEEDED(ignore) \
+ (teamplay && !warmup_stage && autocvar_g_balance_teams_queue && !autocvar_g_campaign \
+ && TeamBalance_AreEqual(ignore, true))
+
// ========================== Global teams API ================================
void Team_InitTeams();
TEAM_CHANGE_SPECTATOR = 4 ///< Player is joining spectators. //TODO: Remove?
};
+bool QueuedPlayersReady(entity this, bool checkspecificteam);
+
/// \brief Sets the team of the player.
/// \param[in,out] player Player to adjust.
/// \param[in] team_index Index of the team to set.
/// \return Bitmask of allowed teams.
int TeamBalance_GetAllowedTeams(entity balance);
+bool TeamBalance_AreEqual(entity ignore, bool would_leave);
+void TeamBalance_RemoveExcessPlayers(entity ignore);
+/** Joins queued player(s) to team(s) with a shortage,
+ * this should be more robust than only replacing the player that left.
+ * Chooses players with a specific team preference first
+ * to increase chances of everyone getting what they want.
+ * Returns true if the teams are now balanced.
+ */
+bool TeamBalance_QueuedPlayersTagIn(entity ignore);
+
/// \brief Returns whether the team change to the specified team is allowed.
/// \param[in] balance Team balance entity.
/// \param[in] index Index of the team.
BADCVAR("gametype");
BADCVAR("g_antilag");
BADCVAR("g_balance_teams");
+ BADCVAR("g_balance_teams_queue");
+ BADCVAR("g_balance_teams_remove");
+ BADCVAR("g_balance_teams_remove_wait");
BADCVAR("g_balance_teams_prevent_imbalance");
BADCVAR("g_balance_teams_scorefactor");
BADCVAR("g_ban_sync_trusted_servers");
set g_balance_teams 1 "automatically balance out players entering instead of asking them for their preferred team"
set g_balance_teams_prevent_imbalance 1 "prevent players from changing to larger teams"
+set g_balance_teams_queue 0 "queue players to maintain balance when they join during the match"
+set g_balance_teams_remove 0 "remove excess players from teams to maintain balance when someone leaves (currently does nothing in matches with more than 2 teams)"
+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 1 "enable a nag message when the teams are unbalanced"