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're queued to join any available team."), "")
MULTITEAM_CENTER(JOIN_PREVENT_QUEUE_TEAM, N_ENABLE, 0, 0, "", CPID_PREVENT_JOIN, "0 0", _("^BGYou're queued to join the ^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^K2 first."), "", NAME)
+ MULTITEAM_CENTER(JOIN_PREVENT_QUEUE_TEAM_CONFLICT, N_ENABLE, 1, 0, "s1", CPID_PREVENT_JOIN, "0 0", _("^K2You're queued to join any available team.\n%s^K2 chose ^TC^TT^K2 first."), "", NAME)
+ MULTITEAM_CENTER(JOIN_PLAY_TEAM_QUEUECONFLICT, N_ENABLE, 1, 0, "s1", CPID_Null, "0 0", _("^K2You're now playing on ^TC^TT^K2 team!\n%s^K2 chose your preferred team first."), "", NAME)
+ MULTITEAM_CENTER(JOIN_PLAY_TEAM, N_ENABLE, 0, 0, "", CPID_Null, "0 0", _("^BGYou're now playing on ^TC^TT^BG team!"), "", 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!"), "")
void PutPlayerInServer(entity this)
{
+ if (MUTATOR_CALLHOOK(ForbidSpawn, this))
+ return;
+
if (this.vehicle) vehicles_exit(this.vehicle, VHEF_RELEASE);
PlayerState_attach(this);
accuracy_resend(this);
- if (teamplay && this.bot_forced_team)
- SetPlayerTeam(this, this.bot_forced_team, TEAM_CHANGE_MANUAL);
-
- if (this.team < 0)
- TeamBalance_JoinBestTeam(this);
+ if (teamplay)
+ {
+ if (this.bot_forced_team)
+ SetPlayerTeam(this, this.bot_forced_team, TEAM_CHANGE_MANUAL);
+ else if (this.wants_join > 0)
+ SetPlayerTeam(this, this.wants_join, TEAM_CHANGE_MANUAL);
+ else if (this.team <= 0 || this.wants_join < 0 || autocvar_g_campaign)
+ TeamBalance_JoinBestTeam(this);
+ }
entity spot = SelectSpawnPoint(this, false);
if (!spot) {
return true;
}
+/// it's assumed this isn't called for bots (campaign_bots_may_start, centreprints)
void Join(entity this, bool queued_join)
{
- bool teamautoselect = autocvar_g_campaign || autocvar_g_balance_teams || this.wants_join < 0;
+ entity player_with_dibs = NULL;
if (autocvar_g_campaign && !campaign_bots_may_start && !game_stopped && time >= game_starttime)
ReadyRestart(true);
- TRANSMUTE(Player, this);
-
if(queued_join
&& TeamBalance_AreEqual(this, true)) // if a player couldn't tag in for balance, don't join them here as it would cause a stack
{
- // 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,
+ // First we must join player(s) queued for specific team(s) (they chose first)
+ // so TeamBalance_JoinBestTeam() (if necessary) won't select the same team(s).
+ // Relies on `this` skipping the queue (this.team already set, this.wants_join not set) or using autoselect.
+ FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != this && it.wants_join > 0,
{
+ // detect any conflict between `this` and a queued player (queuePlayer() handles other conflicts)
+ if (this.team < 0 && this.team_selected > 0 // `this` can't have their preference
+ && it.wants_join == this.team_selected) // `it` is the player who already chose the team `this` wanted
+ player_with_dibs = it;
+
Join(it, false);
- // ensure TeamBalance_JoinBestTeam will run if necessary for `this`
- teamautoselect = true;
});
- }
- if(!this.team_selected && teamautoselect)
- TeamBalance_JoinBestTeam(this);
+ // Second pass: queued players whose team will be autoselected
+ FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != this && it.wants_join < 0,
+ {
+ Join(it, false);
+ });
+ }
if(autocvar_g_campaign)
campaign_bots_may_start = true;
Kill_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CPID_PREVENT_JOIN);
+ TRANSMUTE(Player, this);
PutClientInServer(this);
- if(IS_PLAYER(this))
- if(teamplay && this.team != -1)
+ if(IS_PLAYER(this)) // could be false due to PutClientInServer() mutator hook
{
- if(this.wants_join)
- Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_JOIN_PLAY_TEAM), this.netname);
+ if (!teamplay)
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_PLAY, this.netname);
+ else if (player_with_dibs)
+ // limitation: notifications support only 1 translated team name
+ // so the team `this` preferred can't be mentioned, only the team they got assigned to.
+ Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, APP_TEAM_NUM(this.team, CENTER_JOIN_PLAY_TEAM_QUEUECONFLICT), player_with_dibs.netname);
+ else if (this.wants_join)
+ {
+ // Get queued player's attention
+ if (game_starttime <= time) // No countdown in progress
+ Send_Notification(NOTIF_ONE_ONLY, this, MSG_ANNCE, ANNCE_BEGIN);
+ Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, APP_TEAM_NUM(this.team, CENTER_JOIN_PLAY_TEAM));
+ }
}
- else
- Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_PLAY, this.netname);
- this.team_selected = false;
+
+ this.team_selected = 0;
this.wants_join = 0;
}
return free_slots;
}
+// Callsites other than ClientCommand_selectteam() should pass this.wants_join as team_index
+// so the player won't accidentally reset a specific preference by pressing +jump
+// and will see the centreprint with their current preference each time they press +jump.
bool queuePlayer(entity this, int team_index)
{
- if(IS_BOT_CLIENT(this) || !QueueNeeded(this) || QueuedPlayersReady(this, false))
+ if (IS_BOT_CLIENT(this) || !QueueNeeded(this))
return false;
- if(team_index <= 0)
+ // check if a queued player already chose the selected team
+ if (team_index > 0)
{
- // defer team selection until Join()
+ FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != this && it.wants_join == team_index,
+ {
+ if (QueuedPlayersReady(this, false))
+ {
+ // Join() will handle the notification so it can mention the team `player` will actually get
+ this.team = -1; // force autoselect in Join() (last player skips queue)
+ this.team_selected = team_index; // tell it which team to check for to find the conflict
+ }
+ else // > 2 teams
+ {
+ Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, APP_TEAM_NUM(Team_IndexToTeam(team_index), CENTER_JOIN_PREVENT_QUEUE_TEAM_CONFLICT), it.netname);
+ this.wants_join = -1; // force autoselect in Join()
+ this.team_selected = -1; // prevents clobbering by CENTER_JOIN_PREVENT_QUEUE
+ }
+ return true;
+ });
+ }
+
+ if (QueuedPlayersReady(this, false))
+ return false;
+
+ if (team_index <= 0) // team auto select deferred until Join()
+ {
+ if (team_index != this.wants_join || !this.wants_join) // prevents chatcon spam
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_WANTS, this.netname);
+ if (this.team_selected >= 0) // prevents CENTER_JOIN_PREVENT_QUEUE_TEAM_CONFLICT getting clobbered
+ Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CENTER_JOIN_PREVENT_QUEUE);
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);
+ this.team_selected = 0;
}
else
{
+ int team_num = Team_IndexToTeam(team_index);
+ if (team_index != this.wants_join) // prevents chatcon spam
+ Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(team_num, INFO_JOIN_WANTS_TEAM), this.netname);
+ Send_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, APP_TEAM_NUM(team_num, CENTER_JOIN_PREVENT_QUEUE_TEAM));
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));
+ this.team_selected = team_index;
}
return true;
}
-bool joinAllowed(entity this)
+bool joinAllowed(entity this, int team_index)
{
if (CS(this).version_mismatch) return false;
if (time < CS(this).jointime + MIN_SPEC_TIME) return false;
- if (!nJoinAllowed(this, this)) 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;
+
+ if (QueueNeeded(this))
+ {
+ if (team_index == 0) // so ClientCommand_selectteam() can check joinAllowed() before calling SetPlayerTeam() without chicken/egg problem
+ if (ShowTeamSelection(this)) return false; // only needed by callsites other than selectteam
+ // queuePlayer called here so that only conditions above block queuing (g_maxplayers shouldn't)
+ if (queuePlayer(this, team_index)) return false;
+ if (!nJoinAllowed(this, this)) return false;
+ }
+ else
+ {
+ if (!nJoinAllowed(this, this)) return false;
+ if (team_index == 0) // so ClientCommand_selectteam() can check joinAllowed() before calling SetPlayerTeam() without chicken/egg problem
+ if (ShowTeamSelection(this)) return false; // only needed by callsites other than selectteam
+ }
+
return true;
}
}
if (this.flags & FL_JUMPRELEASED) {
- if (PHYS_INPUT_BUTTON_JUMP(this) && (joinAllowed(this) || time < CS(this).jointime + MIN_SPEC_TIME)) {
+ if (PHYS_INPUT_BUTTON_JUMP(this) && (joinAllowed(this, this.wants_join) || time < CS(this).jointime + MIN_SPEC_TIME)) {
this.flags &= ~FL_JUMPRELEASED;
this.flags |= FL_SPAWNING;
} else if((is_spec && (PHYS_INPUT_BUTTON_ATCK(this) || CS(this).impulse == 10 || CS(this).impulse == 15 || CS(this).impulse == 18 || (CS(this).impulse >= 200 && CS(this).impulse <= 209)))
if(this.flags & FL_SPAWNING)
{
this.flags &= ~FL_SPAWNING;
- if(joinAllowed(this))
- Join(this, true);
+ if(joinAllowed(this, this.wants_join))
+ Join(this, teamplay);
else if(time < CS(this).jointime + MIN_SPEC_TIME)
CS(this).autojoin_checked = -1;
return;
|| (!(autocvar_sv_spectate || autocvar_g_campaign || (Player_GetForcedTeamIndex(this) == TEAM_FORCE_SPECTATOR))
&& (!teamplay || autocvar_g_balance_teams)))
{
- if(joinAllowed(this))
- Join(this, true);
+ if(joinAllowed(this, this.wants_join))
+ Join(this, teamplay);
return;
}
}
// Can't do this in PutObserverInServer() or SetPlayerTeam() cos it causes
// mouse2 (change spectate mode) to kick the player off the join queue.
this.wants_join = 0;
- this.team_selected = false;
+ this.team_selected = 0;
// when the player is kicked off the server, these are called in ClientDisconnect()
if (!TeamBalance_QueuedPlayersTagIn(this))
if (autocvar_g_balance_teams_remove)