]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Create queue system to prevent team imbalance in teamplay
authorz411 <z411@omaera.org>
Sat, 25 May 2024 11:09:54 +0000 (11:09 +0000)
committerbones_was_here <bones_was_here@xonotic.au>
Sat, 25 May 2024 11:09:54 +0000 (11:09 +0000)
17 files changed:
notifications.cfg
qcsrc/client/hud/panel/infomessages.qc
qcsrc/client/hud/panel/scoreboard.qc
qcsrc/common/ent_cs.qc
qcsrc/common/ent_cs.qh
qcsrc/common/notifications/all.inc
qcsrc/common/notifications/all.qh
qcsrc/common/teams.qh
qcsrc/server/client.qc
qcsrc/server/client.qh
qcsrc/server/clientkill.qc
qcsrc/server/command/cmd.qc
qcsrc/server/main.qc
qcsrc/server/teamplay.qc
qcsrc/server/teamplay.qh
qcsrc/server/world.qc
xonotic-server.cfg

index 22a4e70a31403caeaef852e66b96d1c74ae9f011..92f41697162f0b48e6b74e8f00e634ba033280c9 100644 (file)
@@ -234,6 +234,8 @@ seta notification_INFO_JETPACK_NOFUEL "1" "0 = off, 1 = print to console, 2 = pr
 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)"
@@ -246,6 +248,8 @@ seta notification_INFO_LMS_NOLIVES "1" "0 = off, 1 = print to console, 2 = print
 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)"
@@ -262,6 +266,7 @@ seta notification_INFO_QUIT_KICK_IDLING "2" "0 = off, 1 = print to console, 2 =
 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)"
@@ -483,6 +488,9 @@ seta notification_CENTER_JOIN_NOSPAWNS "1" "0 = off, 1 = centerprint"
 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"
@@ -499,6 +507,7 @@ seta notification_CENTER_LMS_VISIBLE_OTHER "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"
index 94bfa47511a9d7ece3909eb565b66e86a3758852..ff5bd8d5d8e30e407a34b780a452713a3ca27def 100644 (file)
@@ -127,7 +127,13 @@ void HUD_InfoMessages()
 
                        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);
                        }
                }
index 1258a5e5da63f055fab6e318d685d9c16f57012b..9e1fd9181c92006335c518e01fe75e62c56bac0f 100644 (file)
@@ -1497,6 +1497,15 @@ vector Scoreboard_DrawOthers(vector item_pos, vector rgb, int this_team, entity
                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)
                {
index 85119de08825c52ab9c6692d87d4d9a3eb9fd83b..11c5e7bb019da168b83b30481683c7d6b537e274 100644 (file)
@@ -152,6 +152,10 @@ ENTCS_PROP(FRAGS, true, frags, frags, ENTCS_SET_NORMAL,
        { 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); },
index aa689e59d707684eda169334528faf49cce8eefe..3c96661e60b678c5cd2db85ef57d7d200d11bbe1 100644 (file)
@@ -71,6 +71,7 @@ REGISTER_NET_TEMP(CLIENT_ENTCS)
      * @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))
@@ -90,6 +91,15 @@ REGISTER_NET_TEMP(CLIENT_ENTCS)
 
        /**
      * @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)
        {
index dc118fac66617e6333ad52212fa16db7fe2557d4..2d79f73def22fa2804d3b51af8617902cfadcaea 100644 (file)
@@ -392,6 +392,8 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input !=
     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!"), "")
@@ -424,10 +426,13 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input !=
     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"), "")
@@ -677,6 +682,7 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input !=
 
     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!"), "")
@@ -715,6 +721,9 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input !=
     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!"), "")
index f4c25968cc2bf3154b7bd2643c4403ad946b0055..2ac682f96789c2f49739c6b58d11eb5311e68dca 100644 (file)
@@ -52,6 +52,7 @@ ENUMCLASS(CPID)
        CASE(CPID, STALEMATE)
        CASE(CPID, NADES)
        CASE(CPID, IDLING)
+       CASE(CPID, REMOVE)
        CASE(CPID, ITEM)
        CASE(CPID, PREVENT_JOIN)
        CASE(CPID, KEEPAWAY)
index 62c3e7b162b2aa11e27962da6e49b218889cbc91..dfedb2a274e346c161af01fc739e687bcd573e5f 100644 (file)
@@ -83,7 +83,7 @@ vector Team_ColorRGB(int teamid)
                case NUM_TEAM_4: return '1 0.0625 1'; // 0xFF0FFF
        }
 
-       return '0 0 0';
+       return '1 1 1';
 }
 
 string Team_ColorName(int teamid)
index 1d62feffa2858ea3bbc689fa8da8b1059f3a215a..ead29f4ceb47f09b636689fcea2550fb1fbdb6d2 100644 (file)
@@ -1134,6 +1134,7 @@ void ClientConnect(entity this)
                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?
@@ -1305,6 +1306,10 @@ void ClientDisconnect(entity 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)
@@ -2000,24 +2005,38 @@ void ShowRespawnCountdown(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)
@@ -2030,10 +2049,13 @@ void Join(entity this)
        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()
@@ -2106,6 +2128,31 @@ int nJoinAllowed(entity this, entity ignore)
        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;
@@ -2114,6 +2161,8 @@ bool joinAllowed(entity this)
        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;
 }
 
@@ -2379,7 +2428,7 @@ void ObserverOrSpectatorThink(entity this)
                        {
                                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;
@@ -2484,7 +2533,7 @@ void PlayerPreThink (entity this)
                                && (!teamplay || autocvar_g_balance_teams)))
                {
                        if(joinAllowed(this))
-                               Join(this);
+                               Join(this, true);
                        return;
                }
        }
@@ -2749,9 +2798,9 @@ void PlayerFrame (entity this)
 
 
 // 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;
@@ -2768,7 +2817,7 @@ void PlayerFrame (entity this)
                                        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),
                        {
@@ -2792,22 +2841,35 @@ void PlayerFrame (entity this)
                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
                                {
@@ -2816,7 +2878,9 @@ void PlayerFrame (entity this)
                                }
                                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;
index 9611df4e87980f2fb639198e7f94fd1cae7f70ca..e8fcd5ffe5a62c153dab6e655043fa9b03d26a0a 100644 (file)
@@ -72,7 +72,9 @@ float autocvar_sv_player_scale;
 .int spectatee_status;
 .bool zoomstate;
 
+.bool team_selected;
 .bool just_joined;
+.bool wants_join;
 
 .int pressedkeys;
 
@@ -158,6 +160,7 @@ CLASS(Client, Object)
     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);
@@ -361,8 +364,6 @@ bool PlayerInIPList(entity p, string iplist);
 
 void ClientData_Touch(entity e);
 
-int nJoinAllowed(entity this, entity ignore);
-
 void PlayerUseKey(entity this);
 
 void FixClientCvars(entity e);
@@ -399,8 +400,10 @@ void ClientInit_misc(entity this);
 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);
 
index 296a95bfe5f384c9d8428626f0a1cbd0282fff2f..d61481ab0b07633d250ff1a565669106224c5555 100644 (file)
@@ -25,7 +25,21 @@ void ClientKill_Now_TeamChange(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
        {
index f93880a682d45d6b50c7983c18778b9968ebc1f4..0cc6086e85d09e53d82e4f2cb2485ecb64fa9a2e 100644 (file)
@@ -359,7 +359,7 @@ void ClientCommand_join(entity caller, int request)
                        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;
                        }
@@ -583,12 +583,9 @@ void ClientCommand_selectteam(entity caller, int request, int argc)
                        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:
@@ -694,7 +691,7 @@ void ClientCommand_spectate(entity caller, int request)
 
                                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
                        }
index 72e3dac3dd039a9131d7d7c76dd100f5a226988c..1948081779223775f9ea498bfb10f508402de86e 100644 (file)
@@ -59,6 +59,11 @@ bool dropclient_schedule(entity this)
        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;
 }
 
index 1cfdbf4a0538da331630210cdfbc92a4ef4333ec..8ce35494a4e75a4a016d122af136b95b2e63cefd 100644 (file)
@@ -234,6 +234,27 @@ bool Player_SetTeamIndex(entity player, int index)
        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);
@@ -252,7 +273,13 @@ bool SetPlayerTeam(entity player, int team_index, int type)
                        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)
@@ -320,6 +347,19 @@ 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) && 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);
 }
 
@@ -657,6 +697,171 @@ int TeamBalance_GetAllowedTeams(entity balance)
        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)
@@ -708,6 +913,10 @@ void TeamBalance_GetTeamCounts(entity balance, entity ignore)
                        {
                                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))
index d96b7df4b25e9559172db0f4b741cad32886a9ed..61d086c7855d9c0630824a2d3b9a6838be34962b 100644 (file)
@@ -7,6 +7,9 @@ bool autocvar_teamplay_lockonrestart;
 
 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;
 
@@ -14,6 +17,10 @@ bool lockteams;
 
 .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();
@@ -113,6 +120,8 @@ enum
        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.
@@ -183,6 +192,16 @@ void TeamBalance_Destroy(entity balance);
 /// \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.
index 4ce29a707f26338b3dd9f818362a04cf7b216318..7779879d8069660e27c1efad0ab0b0bd4326844f 100644 (file)
@@ -450,6 +450,9 @@ void cvar_changes_init()
                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");
index e638577222b1cc279385114895fd6e4e8777e022..461c2e524f05a59305835c9ff572469e00221add 100644 (file)
@@ -281,6 +281,9 @@ set g_teamdamage_resetspeed 20      "for teamplay_mode 4: how fast player's team
 
 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"