From a5cb1c7ba35ccf8173d35c23bbc33ec201da1b60 Mon Sep 17 00:00:00 2001 From: bones_was_here Date: Fri, 14 Jun 2024 18:36:49 +1000 Subject: [PATCH] join queue: UI improvements and refactoring Fixes duplicate and redundant chatcon notifications. Displays relevant centreprint each time the team selection GUI or +jump are used to try to join (even if the player is already queued) for better noob friendliness. Always notifies the player when their team selection conflicts with that of a queued player. Notifies queued players to get their attention when they join as it may take some time before another player triggers the join. Includes the team the player actually got assigned to in the relevant centreprint (for the cases where they had no preference, or someone else chose their preferred team first). Prevents SetPlayerTeam() and queuePlayer() when the player definitely can't play (version mismatch, locked teams), fixing misleading notifications. Allows players to join the queue even when g_maxplayers blocks them from joining the match, so they can tag in if someone leaves. Fixes bug with > 2 teams where if a player with a lower entity number queues for any available team, a player with a higher entity number and a specific team preference could be assigned to the same team (by adding a second pass in Join() ). Fixes lack of "now playing" notification when a bot joins a gametype without teams. This required some refactoring: Calls joinAllowed() from ClientCommand_selectteam(), passing the team_index through to queuePlayer(). This means calling queuePlayer from SetPlayerTeam() is no longer needed. Uses a different logic order in joinAllowed() when the queue is enabled. Moves the ForbidSpawn MUTATOR hook from joinAllowed() to PutPlayerInServer(). It seemed to be in the wrong place anyway: the player is only blocked from spawning, not from joining the match. No longer sets the .team field when adding a player to the queue (they're not actually on the team yet), instead calls SetPlayerTeam() from Join() when actually spawning the queued player. Uses the .team_selected field to pass selection conflicts to Join(), it can't be done with a parameter because `join` and `selectteam` are separate commands. Moves the queue conflict detection from Player_SetTeamIndexChecked() to queuePlayer(). Reduces TeamBalance_JoinBestTeam() callsites. Fixes some uses of NOTIF_ONE where it should have been NOTIF_ONE_ONLY. --- notifications.cfg | 4 +- qcsrc/common/notifications/all.inc | 4 +- qcsrc/server/bot/default/bot.qc | 3 + qcsrc/server/client.qc | 152 ++++++++++++++++++++--------- qcsrc/server/client.qh | 6 +- qcsrc/server/clientkill.qc | 3 +- qcsrc/server/command/cmd.qc | 13 +-- qcsrc/server/teamplay.qc | 21 +--- 8 files changed, 130 insertions(+), 76 deletions(-) diff --git a/notifications.cfg b/notifications.cfg index 0f5b959d8..b1c203976 100644 --- a/notifications.cfg +++ b/notifications.cfg @@ -488,10 +488,12 @@ seta notification_CENTER_ITEM_WEAPON_PRIMORSEC "1" "0 = off, 1 = centerprint" seta notification_CENTER_ITEM_WEAPON_UNAVAILABLE "1" "0 = off, 1 = centerprint" seta notification_CENTER_JOIN_NOSPAWNS "1" "0 = off, 1 = centerprint" seta notification_CENTER_JOIN_PLAYBAN "1" "0 = off, 1 = centerprint" +seta notification_CENTER_JOIN_PLAY_TEAM_QUEUECONFLICT "1" "0 = off, 1 = centerprint" +seta notification_CENTER_JOIN_PLAY_TEAM "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_CONFLICT "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" diff --git a/qcsrc/common/notifications/all.inc b/qcsrc/common/notifications/all.inc index 0b4c0402d..8e9a1ced6 100644 --- a/qcsrc/common/notifications/all.inc +++ b/qcsrc/common/notifications/all.inc @@ -725,7 +725,9 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input != 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!"), "") diff --git a/qcsrc/server/bot/default/bot.qc b/qcsrc/server/bot/default/bot.qc index 07587befa..6965b0795 100644 --- a/qcsrc/server/bot/default/bot.qc +++ b/qcsrc/server/bot/default/bot.qc @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +53,8 @@ entity bot_spawn() ClientConnect(bot); bot_setclientfields(bot); PutClientInServer(bot); + if (!teamplay) + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_PLAY, bot.netname); } return bot; } diff --git a/qcsrc/server/client.qc b/qcsrc/server/client.qc index 749bbf8c4..dbb62150a 100644 --- a/qcsrc/server/client.qc +++ b/qcsrc/server/client.qc @@ -552,16 +552,23 @@ void GiveWarmupResources(entity this) 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) { @@ -2034,46 +2041,63 @@ bool ShowTeamSelection(entity this) 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; } @@ -2147,41 +2171,81 @@ int nJoinAllowed(entity this, entity ignore) 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; } @@ -2393,7 +2457,7 @@ void ObserverOrSpectatorThink(entity this) } 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))) @@ -2446,8 +2510,8 @@ void ObserverOrSpectatorThink(entity this) 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; @@ -2551,8 +2615,8 @@ void PlayerPreThink (entity this) || (!(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; } } @@ -2903,7 +2967,7 @@ void PlayerFrame (entity this) // 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) diff --git a/qcsrc/server/client.qh b/qcsrc/server/client.qh index 2bff39359..890a223a0 100644 --- a/qcsrc/server/client.qh +++ b/qcsrc/server/client.qh @@ -72,7 +72,8 @@ float autocvar_sv_player_scale; .int spectatee_status; .bool zoomstate; -.bool team_selected; +/// > 0 is a team index, if wants_join == -1 the player can't have the team they selected (conflict) +.int team_selected; .bool just_joined; /// > 0 is a team index, -1 means team selection is deferred until Join() .int wants_join; @@ -404,8 +405,7 @@ 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); +bool joinAllowed(entity this, int team_index); void PlayerFrame (entity this); diff --git a/qcsrc/server/clientkill.qc b/qcsrc/server/clientkill.qc index 21df2319c..d692d8723 100644 --- a/qcsrc/server/clientkill.qc +++ b/qcsrc/server/clientkill.qc @@ -37,12 +37,13 @@ void ClientKill_Now_TeamChange(entity this) if (this.wants_join) { + Kill_Notification(NOTIF_ONE_ONLY, this, MSG_CENTER, CPID_PREVENT_JOIN); Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_QUEUE, this.netname); SetPlayerTeam(this, -1, TEAM_CHANGE_SPECTATOR); // 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; } else { diff --git a/qcsrc/server/command/cmd.qc b/qcsrc/server/command/cmd.qc index 850efa1f3..41e75d5a4 100644 --- a/qcsrc/server/command/cmd.qc +++ b/qcsrc/server/command/cmd.qc @@ -359,8 +359,8 @@ void ClientCommand_join(entity caller, int request) { if (!game_stopped && IS_CLIENT(caller) && !IS_PLAYER(caller)) { - if (joinAllowed(caller)) - Join(caller, true); + if (joinAllowed(caller, caller.wants_join)) + Join(caller, teamplay); else if(time < CS(caller).jointime + MIN_SPEC_TIME) CS(caller).autojoin_checked = -1; } @@ -524,7 +524,6 @@ void ClientCommand_say_team(entity caller, int request, int argc, string command } } -.bool team_selected; void ClientCommand_selectteam(entity caller, int request, int argc) { switch (request) @@ -581,11 +580,9 @@ void ClientCommand_selectteam(entity caller, int request, int argc) } TeamBalance_Destroy(balance); } - if (team_num) - ClientKill_TeamChange(caller, team_num); - else // auto - if (!queuePlayer(caller, 0)) // the queue uses deferred autoselect - ClientKill_TeamChange(caller, -1); + + if (joinAllowed(caller, team_num ? Team_TeamToIndex(team_num) : -1)) + ClientKill_TeamChange(caller, team_num ? team_num : -1); return; } diff --git a/qcsrc/server/teamplay.qc b/qcsrc/server/teamplay.qc index f2c35665c..4558eb723 100644 --- a/qcsrc/server/teamplay.qc +++ b/qcsrc/server/teamplay.qc @@ -274,11 +274,8 @@ bool SetPlayerTeam(entity player, int team_index, int type) if (team_index != -1) { - 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() - } + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(player.team, INFO_JOIN_PLAY_TEAM), player.netname); + player.team_selected = team_index; // no autoselect in Join() if (warmup_stage) ReadyCount(); // teams might be balanced now @@ -351,18 +348,6 @@ void Player_SetTeamIndexChecked(entity player, int team_index) } 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) && QueueNeeded(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); } @@ -941,7 +926,7 @@ void TeamBalance_GetTeamCounts(entity balance, entity ignore) } if (it.wants_join) { - continue; // Queued players aren't actually in the game. + continue; // Queued players aren't actually in the game (and shouldn't have .team set). } int team_num; // TODO: Reconsider when the player is truly on the team. -- 2.39.2