]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
New team balance API.
authorLyberta <lyberta@lyberta.net>
Fri, 9 Mar 2018 10:14:03 +0000 (13:14 +0300)
committerLyberta <lyberta@lyberta.net>
Fri, 9 Mar 2018 10:14:03 +0000 (13:14 +0300)
22 files changed:
qcsrc/common/gamemodes/gamemode/nexball/nexball.qc
qcsrc/common/gamemodes/gamemode/onslaught/sv_onslaught.qc
qcsrc/common/t_items.qc
qcsrc/common/teams.qh
qcsrc/server/bot/default/bot.qc
qcsrc/server/client.qc
qcsrc/server/command/cmd.qc
qcsrc/server/command/sv_cmd.qc
qcsrc/server/g_world.qc
qcsrc/server/mutators/events.qh
qcsrc/server/mutators/mutator/gamemode_assault.qc
qcsrc/server/mutators/mutator/gamemode_ca.qc
qcsrc/server/mutators/mutator/gamemode_ctf.qc
qcsrc/server/mutators/mutator/gamemode_domination.qc
qcsrc/server/mutators/mutator/gamemode_freezetag.qc
qcsrc/server/mutators/mutator/gamemode_invasion.qc
qcsrc/server/mutators/mutator/gamemode_keyhunt.qc
qcsrc/server/mutators/mutator/gamemode_race.qc
qcsrc/server/mutators/mutator/gamemode_tdm.qc
qcsrc/server/scores_rules.qc
qcsrc/server/teamplay.qc
qcsrc/server/teamplay.qh

index fa718eb16fe9884a74512688212372359fd026b6..e4b755fc42111e168c351e44340acee6e05008c2 100644 (file)
@@ -920,7 +920,7 @@ MUTATOR_HOOKFUNCTION(nb, ItemTouch)
        return MUT_ITEMTOUCH_CONTINUE;
 }
 
-MUTATOR_HOOKFUNCTION(nb, CheckAllowedTeams)
+MUTATOR_HOOKFUNCTION(nb, TeamBalance_CheckAllowedTeams)
 {
        M_ARGV(1, string) = "nexball_team";
        return true;
index 04425c24b39f0969a026c0cc555826e3a1c246d7..8fc4d89c4afbfbf74df86f69606c2f660fe73746 100644 (file)
@@ -1922,7 +1922,7 @@ MUTATOR_HOOKFUNCTION(ons, HavocBot_ChooseRole)
        return true;
 }
 
-MUTATOR_HOOKFUNCTION(ons, CheckAllowedTeams)
+MUTATOR_HOOKFUNCTION(ons, TeamBalance_CheckAllowedTeams)
 {
        // onslaught is special
        for(entity tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext)
@@ -2159,8 +2159,9 @@ spawnfunc(onslaught_generator)
 // scoreboard setup
 void ons_ScoreRules()
 {
-       CheckAllowedTeams(NULL);
-       int teams = GetAllowedTeams();
+       entity balance = TeamBalance_CheckAllowedTeams(NULL);
+       int teams = TeamBalance_GetAllowedTeams(balance);
+       TeamBalance_Destroy(balance);
        GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
            field_team(ST_ONS_CAPS, "destroyed", SFL_SORT_PRIO_PRIMARY);
            field(SP_ONS_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
index 183c38d3b1ea5e7ed29bc1e1d2b61992bb2ef744..4ce8c8104bb11656586fad6332c26c3f6e00261d 100644 (file)
@@ -628,17 +628,17 @@ float adjust_respawntime(float normal_respawntime) {
                return normal_respawntime;
        }
 
-       CheckAllowedTeams(NULL);
-       GetTeamCounts(NULL);
+       entity balance = TeamBalance_CheckAllowedTeams(NULL);
+       TeamBalance_GetTeamCounts(balance, NULL);
        int players = 0;
-       for (int i = 1; i < 5; ++i)
+       for (int i = 1; i <= NUM_TEAMS; ++i)
        {
-               entity team_ = Team_GetTeamFromIndex(i);
-               if (Team_IsAllowed(team_))
+               if (TeamBalance_IsTeamAllowed(balance, i))
                {
-                       players += Team_GetNumberOfPlayers(team_);
+                       players += TeamBalance_GetNumberOfPlayers(balance, i);
                }
        }
+       TeamBalance_Destroy(balance);
        
        if (players >= 2) {
                return normal_respawntime * (r / (players + o) + l);
index 032aa38d605bdb7dd89a16e31a9d1e815e5ea7d5..1b03540dd7fd008dfe45dda3d461a34ac63cdc02 100644 (file)
@@ -1,5 +1,7 @@
 #pragma once
 
+const int NUM_TEAMS = 4; ///< Number of teams in the game.
+
 #ifdef TEAMNUMBERS_THAT_ARENT_STUPID
 const int NUM_TEAM_1 = 1;  // red
 const int NUM_TEAM_2 = 2; // blue
@@ -187,6 +189,14 @@ float Team_TeamToNumber(float teamid)
        return -1;
 }
 
+/// \brief Converts team index into bit value that is used in team bitmasks.
+/// \param[in] index Team index to convert.
+/// \return Team bit.
+int Team_IndexToBit(int index)
+{
+       return BIT(index - 1);
+}
+
 
 // legacy aliases for shitty code
 #define TeamByColor(teamid) (Team_TeamToNumber(teamid) - 1)
index ec6ee35d339f35fef8c41c1ef705ac54146e5d84..3ce03694cd4ca9b8c223bc47f4560726509d8949 100644 (file)
@@ -438,15 +438,15 @@ void bot_clientconnect(entity this)
        else if(this.bot_forced_team==4)
                this.team = NUM_TEAM_4;
        else
-               JoinBestTeamForBalance(this, true);
+               TeamBalance_JoinBestTeam(this, true);
 
        havocbot_setupbot(this);
 }
 
 void bot_removefromlargestteam()
 {
-       CheckAllowedTeams(NULL);
-       GetTeamCounts(NULL);
+       entity balance = TeamBalance_CheckAllowedTeams(NULL);
+       TeamBalance_GetTeamCounts(balance, NULL);
 
        entity best = NULL;
        float besttime = 0;
@@ -467,7 +467,8 @@ void bot_removefromlargestteam()
 
                if (Team_IsValidTeam(it.team))
                {
-                       thiscount = Team_GetNumberOfPlayers(Team_GetTeam(it.team));
+                       thiscount = TeamBalance_GetNumberOfPlayers(balance,
+                               Team_TeamToNumber(it.team));
                }
 
                if(thiscount > bestcount)
@@ -482,6 +483,7 @@ void bot_removefromlargestteam()
                        best = it;
                }
        });
+       TeamBalance_Destroy(balance);
        if(!bcount)
                return; // no bots to remove
        currentbots = currentbots - 1;
index 4109818ab5baa09ea868152029bbad30681f48cb..410245460b8c17e8ea57369a2859d774c1cde2b9 100644 (file)
@@ -516,7 +516,7 @@ void PutPlayerInServer(entity this)
        accuracy_resend(this);
 
        if (this.team < 0)
-               JoinBestTeamForBalance(this, true);
+               TeamBalance_JoinBestTeam(this, true);
 
        entity spot = SelectSpawnPoint(this, false);
        if (!spot) {
@@ -903,7 +903,7 @@ void ClientKill_Now_TeamChange(entity this)
 {
        if(this.killindicator_teamchange == -1)
        {
-               JoinBestTeamForBalance(this, true);
+               TeamBalance_JoinBestTeam(this, true);
        }
        else if(this.killindicator_teamchange == -2)
        {
@@ -1216,7 +1216,7 @@ void ClientConnect(entity this)
 
        int playerid_save = this.playerid;
        this.playerid = 0; // silent
-       JoinBestTeamForBalance(this, false); // if the team number is valid, keep it
+       TeamBalance_JoinBestTeam(this, false); // if the team number is valid, keep it
        this.playerid = playerid_save;
 
        if (autocvar_sv_spectate || autocvar_g_campaign || this.team_forced < 0) {
@@ -1261,8 +1261,9 @@ void ClientConnect(entity this)
        // notify about available teams
        if (teamplay)
        {
-               CheckAllowedTeams(this);
-               int t = GetAllowedTeams();
+               entity balance = TeamBalance_CheckAllowedTeams(this);
+               int t = TeamBalance_GetAllowedTeams(balance);
+               TeamBalance_Destroy(balance);
                stuffcmd(this, sprintf("set _teams_available %d\n", t));
        }
        else
@@ -2006,7 +2007,7 @@ void Join(entity this)
 
        if(!this.team_selected)
        if(autocvar_g_campaign || autocvar_g_balance_teams)
-               JoinBestTeamForBalance(this, true);
+               TeamBalance_JoinBestTeam(this, true);
 
        if(autocvar_g_campaign)
                campaign_bots_may_start = true;
index 2314f71033c85bc664366607011b0714761eeb6a..af228f729333bccaac7823ec8fb113fb219d0ab0 100644 (file)
@@ -394,13 +394,16 @@ void ClientCommand_selectteam(entity caller, float request, float argc)
                        if ((selection != -1) && autocvar_g_balance_teams &&
                                autocvar_g_balance_teams_prevent_imbalance)
                        {
-                               CheckAllowedTeams(caller);
-                               GetTeamCounts(caller);
-                               if ((BIT(Team_TeamToNumber(selection) - 1) & FindBestTeamsForBalance(caller, false)) == 0)
+                               entity balance = TeamBalance_CheckAllowedTeams(caller);
+                               TeamBalance_GetTeamCounts(balance, caller);
+                               if ((Team_IndexToBit(Team_TeamToNumber(selection)) &
+                                       TeamBalance_FindBestTeams(balance, caller, false)) == 0)
                                {
                                        Send_Notification(NOTIF_ONE, caller, MSG_INFO, INFO_TEAMCHANGE_LARGERTEAM);
+                                       TeamBalance_Destroy(balance);
                                        return;
                                }
+                               TeamBalance_Destroy(balance);
                        }
                        ClientKill_TeamChange(caller, selection);
                        if (!IS_PLAYER(caller))
index d7ede6af4b9e7b8bdb99d0175c8e05053a3f0d4f..9d8c901dbbde5634925c72ef4242ae14458e9563 100644 (file)
@@ -1064,6 +1064,7 @@ void GameCommand_moveplayer(float request, float argc)
 
                                                                // find the team to move the player to
                                                                team_id = Team_ColorToTeam(destination);
+                                                               entity balance;
                                                                if (team_id == client.team)  // already on the destination team
                                                                {
                                                                        // keep the forcing undone
@@ -1072,25 +1073,67 @@ void GameCommand_moveplayer(float request, float argc)
                                                                }
                                                                else if (team_id == 0)  // auto team
                                                                {
-                                                                       CheckAllowedTeams(client);
-                                                                       team_id = Team_NumberToTeam(FindBestTeamForBalance(client, false));
+                                                                       balance = TeamBalance_CheckAllowedTeams(client);
+                                                                       team_id = Team_NumberToTeam(TeamBalance_FindBestTeam(balance, client, false));
                                                                }
                                                                else
                                                                {
-                                                                       CheckAllowedTeams(client);
+                                                                       balance = TeamBalance_CheckAllowedTeams(client);
                                                                }
                                                                client.team_forced = save;
 
                                                                // Check to see if the destination team is even available
                                                                switch (team_id)
                                                                {
-                                                                       case NUM_TEAM_1: if (Team_IsAllowed(Team_GetTeamFromIndex(1))) { LOG_INFO("Sorry, can't move player to red team if it doesn't exist."); return; } break;
-                                                                       case NUM_TEAM_2: if (Team_IsAllowed(Team_GetTeamFromIndex(2))) { LOG_INFO("Sorry, can't move player to blue team if it doesn't exist."); return; } break;
-                                                                       case NUM_TEAM_3: if (Team_IsAllowed(Team_GetTeamFromIndex(3))) { LOG_INFO("Sorry, can't move player to yellow team if it doesn't exist."); return; } break;
-                                                                       case NUM_TEAM_4: if (Team_IsAllowed(Team_GetTeamFromIndex(4))) { LOG_INFO("Sorry, can't move player to pink team if it doesn't exist."); return; } break;
-
-                                                                       default: LOG_INFO("Sorry, can't move player here if team ", destination, " doesn't exist.");
+                                                                       case NUM_TEAM_1:
+                                                                       {
+                                                                               if (!TeamBalance_IsTeamAllowed(balance, 1))
+                                                                               {
+                                                                                       LOG_INFO("Sorry, can't move player to red team if it doesn't exist.");
+                                                                                       TeamBalance_Destroy(balance);
+                                                                                       return;
+                                                                               }
+                                                                               TeamBalance_Destroy(balance);
+                                                                               break;
+                                                                       }
+                                                                       case NUM_TEAM_2:
+                                                                       {
+                                                                               if (!TeamBalance_IsTeamAllowed(balance, 2))
+                                                                               {
+                                                                                       LOG_INFO("Sorry, can't move player to blue team if it doesn't exist.");
+                                                                                       TeamBalance_Destroy(balance);
+                                                                                       return;
+                                                                               }
+                                                                               TeamBalance_Destroy(balance);
+                                                                               break;
+                                                                       }
+                                                                       case NUM_TEAM_3:
+                                                                       {
+                                                                               if (!TeamBalance_IsTeamAllowed(balance, 3))
+                                                                               {
+                                                                                       LOG_INFO("Sorry, can't move player to yellow team if it doesn't exist.");
+                                                                                       TeamBalance_Destroy(balance);
+                                                                                       return;
+                                                                               }
+                                                                               TeamBalance_Destroy(balance);
+                                                                               break;
+                                                                       }
+                                                                       case NUM_TEAM_4:
+                                                                       {
+                                                                               if (!TeamBalance_IsTeamAllowed(balance, 4))
+                                                                               {
+                                                                                       LOG_INFO("Sorry, can't move player to pink team if it doesn't exist.");
+                                                                                       TeamBalance_Destroy(balance);
+                                                                                       return;
+                                                                               }
+                                                                               TeamBalance_Destroy(balance);
+                                                                               break;
+                                                                       }
+                                                                       default:
+                                                                       {
+                                                                               LOG_INFO("Sorry, can't move player here if team ", destination, " doesn't exist.");
                                                                                return;
+                                                                       }
                                                                }
 
                                                                // If so, lets continue and finally move the player
@@ -1366,14 +1409,15 @@ void GameCommand_shuffleteams(float request)
                        });
 
                        int number_of_teams = 0;
-                       CheckAllowedTeams(NULL);
-                       for (int i = 1; i < 5; ++i)
+                       entity balance = TeamBalance_CheckAllowedTeams(NULL);
+                       for (int i = 1; i <= NUM_TEAMS; ++i)
                        {
-                               if (Team_IsAllowed(Team_GetTeamFromIndex(i)))
+                               if (TeamBalance_IsTeamAllowed(balance, i))
                                {
                                        number_of_teams = max(i, number_of_teams);
                                }
                        }
+                       TeamBalance_Destroy(balance);
 
                        int team_index = 0;
                        FOREACH_CLIENT_RANDOM(IS_PLAYER(it) || it.caplayer, {
index 5c1724ce353af85c660a0004a7d7225b844e32d5..4777524f0bd82e174b6f76c2cf6696d78425d762 100644 (file)
@@ -1806,16 +1806,16 @@ float WinningCondition_RanOutOfSpawns()
                        t = 3;
                else // if(team4_score)
                        t = 4;
-               CheckAllowedTeams(NULL);
+               entity balance = TeamBalance_CheckAllowedTeams(NULL);
                for(i = 0; i < MAX_TEAMSCORE; ++i)
                {
-                       for (int j = 1; j < 5; ++j)
+                       for (int j = 1; j <= NUM_TEAMS; ++j)
                        {
                                if (t == j)
                                {
                                        continue;
                                }
-                               if (!Team_IsAllowed(Team_GetTeamFromIndex(j)))
+                               if (!TeamBalance_IsTeamAllowed(balance, j))
                                {
                                        continue;
                                }
index 6853c04a15641dc69bd6e52f7da39615a41610a4..35f69fa0bd7b047eddafd3cbc72c5b248c63a2e8 100644 (file)
@@ -126,21 +126,25 @@ MUTATOR_HOOKABLE(GiveFragsForKill, EV_GiveFragsForKill);
 /** called when the match ends */
 MUTATOR_HOOKABLE(MatchEnd, EV_NO_ARGS);
 
-/** allows adjusting allowed teams */
-#define EV_CheckAllowedTeams(i, o) \
+/** Allows adjusting allowed teams. Return true to use the bitmask value and set
+ * non-empty string to use team entity name. Both behaviors can be active at the
+ * same time and will stack allowed teams.
+ */
+#define EV_TeamBalance_CheckAllowedTeams(i, o) \
     /** mask of teams      */ i(float, MUTATOR_ARGV_0_float) \
     /**/                      o(float, MUTATOR_ARGV_0_float) \
     /** team entity name   */ i(string, MUTATOR_ARGV_1_string) \
     /**/                      o(string, MUTATOR_ARGV_1_string) \
     /** player checked     */ i(entity, MUTATOR_ARGV_2_entity) \
     /**/
-MUTATOR_HOOKABLE(CheckAllowedTeams, EV_CheckAllowedTeams);
+MUTATOR_HOOKABLE(TeamBalance_CheckAllowedTeams,
+       EV_TeamBalance_CheckAllowedTeams);
 
 /** return true to manually override team counts */
-MUTATOR_HOOKABLE(GetTeamCounts, EV_NO_ARGS);
+MUTATOR_HOOKABLE(TeamBalance_GetTeamCounts, EV_NO_ARGS);
 
 /** allow overriding of team counts */
-#define EV_GetTeamCount(i, o) \
+#define EV_TeamBalance_GetTeamCount(i, o) \
     /** team to count                   */ i(float, MUTATOR_ARGV_0_float) \
     /** player to ignore                */ i(entity, MUTATOR_ARGV_1_entity) \
     /** number of players in a team     */ i(float, MUTATOR_ARGV_2_float) \
@@ -152,14 +156,16 @@ MUTATOR_HOOKABLE(GetTeamCounts, EV_NO_ARGS);
     /** lowest scoring bot in a team    */ i(entity, MUTATOR_ARGV_5_entity) \
     /**/                                   o(entity, MUTATOR_ARGV_5_entity) \
     /**/
-MUTATOR_HOOKABLE(GetTeamCount, EV_GetTeamCount);
+MUTATOR_HOOKABLE(TeamBalance_GetTeamCount, EV_TeamBalance_GetTeamCount);
 
-/** allows overriding best teams */
-#define EV_FindBestTeams(i, o) \
+/** allows overriding the teams that will make the game most balanced if the
+ *  player joins any of them.
+ */
+#define EV_TeamBalance_FindBestTeams(i, o) \
     /** player checked   */ i(entity, MUTATOR_ARGV_0_entity) \
     /** bitmask of teams */ o(float, MUTATOR_ARGV_1_float) \
     /**/
-MUTATOR_HOOKABLE(FindBestTeams, EV_FindBestTeams);
+MUTATOR_HOOKABLE(TeamBalance_FindBestTeams, EV_TeamBalance_FindBestTeams);
 
 /** copies variables for spectating "spectatee" to "this" */
 #define EV_SpectateCopy(i, o) \
index aa7137a265b097f4c08d0b35c108e3bd3d29a728..f3f104197efd744193dfc7a16d7335907784bd90 100644 (file)
@@ -576,7 +576,7 @@ MUTATOR_HOOKFUNCTION(as, PlayHitsound)
        return (frag_victim.classname == "func_assault_destructible");
 }
 
-MUTATOR_HOOKFUNCTION(as, CheckAllowedTeams)
+MUTATOR_HOOKFUNCTION(as, TeamBalance_CheckAllowedTeams)
 {
        // assault always has 2 teams
        M_ARGV(0, float) = BIT(0) | BIT(1);
index 43ed39aea4924cee57ed841d68e0fe883f4e8429..077e277a6babe5a9d2a0caf5088b5f2f1af23a02 100644 (file)
@@ -230,9 +230,10 @@ MUTATOR_HOOKFUNCTION(ca, reset_map_global)
        return true;
 }
 
-MUTATOR_HOOKFUNCTION(ca, CheckAllowedTeams, CBC_ORDER_EXCLUSIVE)
+MUTATOR_HOOKFUNCTION(ca, TeamBalance_CheckAllowedTeams, CBC_ORDER_EXCLUSIVE)
 {
        M_ARGV(0, float) = ca_teams;
+       return true;
 }
 
 entity ca_LastPlayerForTeam(entity this)
index b34e3f59f51fe58ac3f5cccb5828b68fae45f242..e5ac9eb773aac16e894f212b01d852d3059116df 100644 (file)
@@ -2463,11 +2463,9 @@ MUTATOR_HOOKFUNCTION(ctf, HavocBot_ChooseRole)
        return true;
 }
 
-MUTATOR_HOOKFUNCTION(ctf, CheckAllowedTeams)
+MUTATOR_HOOKFUNCTION(ctf, TeamBalance_CheckAllowedTeams)
 {
-       //M_ARGV(0, float) = ctf_teams;
        M_ARGV(1, string) = "ctf_team";
-       return true;
 }
 
 MUTATOR_HOOKFUNCTION(ctf, SpectateCopy)
@@ -2693,7 +2691,7 @@ spawnfunc(team_CTL_bluelolly)  { spawnfunc_item_flag_team2(this);    }
 // scoreboard setup
 void ctf_ScoreRules(int teams)
 {
-       CheckAllowedTeams(NULL);
+       //CheckAllowedTeams(NULL); // Bug? Need to get allowed teams?
        GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, 0, {
         field_team(ST_CTF_CAPS, "caps", SFL_SORT_PRIO_PRIMARY);
         field(SP_CTF_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
index c72d5fbfdf308f2a853f9f48946882df5ad1174e..dec8ac5a91301a05e7ae43bcd5e17cc266520f1b 100644 (file)
@@ -415,7 +415,7 @@ void havocbot_role_dom(entity this)
        }
 }
 
-MUTATOR_HOOKFUNCTION(dom, CheckAllowedTeams)
+MUTATOR_HOOKFUNCTION(dom, TeamBalance_CheckAllowedTeams)
 {
        // fallback?
        M_ARGV(0, float) = domination_teams;
@@ -639,8 +639,9 @@ void dom_DelayedInit(entity this) // Do this check with a delay so we can wait f
                dom_spawnteams(domination_teams);
        }
 
-       CheckAllowedTeams(NULL);
-       int teams = GetAllowedTeams();
+       entity balance = TeamBalance_CheckAllowedTeams(NULL);
+       int teams = TeamBalance_GetAllowedTeams(balance);
+       TeamBalance_Destroy(balance);
        domination_teams = teams;
 
        domination_roundbased = autocvar_g_domination_roundbased;
index 36546c43a03bebc34c5875416e7b26473a5a364d..0ba3d04f12f0354c5e5d518831f913ef5ccf2ff6 100644 (file)
@@ -538,9 +538,10 @@ MUTATOR_HOOKFUNCTION(ft, HavocBot_ChooseRole)
        return true;
 }
 
-MUTATOR_HOOKFUNCTION(ft, CheckAllowedTeams, CBC_ORDER_EXCLUSIVE)
+MUTATOR_HOOKFUNCTION(ft, TeamBalance_CheckAllowedTeams, CBC_ORDER_EXCLUSIVE)
 {
        M_ARGV(0, float) = freezetag_teams;
+       return true;
 }
 
 MUTATOR_HOOKFUNCTION(ft, SetWeaponArena)
index 1b8b77ae078158e566fb34a87c754701436175d3..575fa308de182e3078e777187c9060fbf96fcb79 100644 (file)
@@ -546,9 +546,10 @@ MUTATOR_HOOKFUNCTION(inv, CheckRules_World)
        return true;
 }
 
-MUTATOR_HOOKFUNCTION(inv, CheckAllowedTeams, CBC_ORDER_EXCLUSIVE)
+MUTATOR_HOOKFUNCTION(inv, TeamBalance_CheckAllowedTeams, CBC_ORDER_EXCLUSIVE)
 {
        M_ARGV(0, float) = invasion_teams;
+       return true;
 }
 
 MUTATOR_HOOKFUNCTION(inv, AllowMobButcher)
@@ -559,7 +560,7 @@ MUTATOR_HOOKFUNCTION(inv, AllowMobButcher)
 
 void invasion_ScoreRules(int inv_teams)
 {
-       if(inv_teams) { CheckAllowedTeams(NULL); }
+       //if(inv_teams) { CheckAllowedTeams(NULL); } // Another bug?
        GameRules_score_enabled(false);
        GameRules_scoring(inv_teams, 0, 0, {
            if (inv_teams) {
index 04576486b71bacd743304e7a5de42b709d3b95d7..6e0fc0ffafb178808ab8f3a290ac63559f8ad3b5 100644 (file)
@@ -1262,9 +1262,10 @@ MUTATOR_HOOKFUNCTION(kh, MatchEnd)
        kh_finalize();
 }
 
-MUTATOR_HOOKFUNCTION(kh, CheckAllowedTeams, CBC_ORDER_EXCLUSIVE)
+MUTATOR_HOOKFUNCTION(kh, TeamBalance_CheckAllowedTeams, CBC_ORDER_EXCLUSIVE)
 {
        M_ARGV(0, float) = kh_teams;
+       return true;
 }
 
 MUTATOR_HOOKFUNCTION(kh, SpectateCopy)
index aa6d12a83e063166da288364d0314e521d3c9970..82d0ca79eaf0f74414242eeee6559436f539d14a 100644 (file)
@@ -363,9 +363,10 @@ MUTATOR_HOOKFUNCTION(rc, ForbidPlayerScore_Clear)
                return true; // in qualifying, you don't lose score by observing
 }
 
-MUTATOR_HOOKFUNCTION(rc, CheckAllowedTeams, CBC_ORDER_EXCLUSIVE)
+MUTATOR_HOOKFUNCTION(rc, TeamBalance_CheckAllowedTeams, CBC_ORDER_EXCLUSIVE)
 {
        M_ARGV(0, float) = race_teams;
+       return true;
 }
 
 MUTATOR_HOOKFUNCTION(rc, Scores_CountFragsRemaining)
index aad31932884556dff7b4a7b6f1a186dcbf15e44c..86ff94f53b88bc1f83bbafdd62809e778981d0e8 100644 (file)
@@ -50,10 +50,9 @@ void tdm_DelayedInit(entity this)
        }
 }
 
-MUTATOR_HOOKFUNCTION(tdm, CheckAllowedTeams, CBC_ORDER_EXCLUSIVE)
+MUTATOR_HOOKFUNCTION(tdm, TeamBalance_CheckAllowedTeams, CBC_ORDER_EXCLUSIVE)
 {
        M_ARGV(1, string) = "tdm_team";
-       return true;
 }
 
 MUTATOR_HOOKFUNCTION(tdm, Scores_CountFragsRemaining)
index 17714e4c29bb6bf8ec9fe7888533d5923587b6ba..39dbd49a35ddc630c85c849624330e1785d30d01 100644 (file)
@@ -9,8 +9,6 @@
 
 int ScoreRules_teams;
 
-void CheckAllowedTeams (entity for_whom);
-
 int NumTeams(int teams)
 {
        return boolean(teams & BIT(0)) + boolean(teams & BIT(1)) + boolean(teams & BIT(2)) + boolean(teams & BIT(3));
@@ -19,8 +17,6 @@ int NumTeams(int teams)
 int AvailableTeams()
 {
        return NumTeams(ScoreRules_teams);
-       // NOTE: this method is unreliable, as forced teams set the c* globals to weird values
-       //return boolean(c1 >= 0) + boolean(c2 >= 0) + boolean(c3 >= 0) + boolean(c4 >= 0);
 }
 
 // NOTE: ST_constants may not be >= MAX_TEAMSCORE
@@ -66,9 +62,11 @@ void ScoreRules_basics_end()
 void ScoreRules_generic()
 {
     int teams = 0;
-       if (teamplay) {
-               CheckAllowedTeams(NULL);
-               teams = GetAllowedTeams();
+       if (teamplay)
+       {
+               entity balance = TeamBalance_CheckAllowedTeams(NULL);
+               teams = TeamBalance_GetAllowedTeams(balance);
+               TeamBalance_Destroy(balance);
        }
        GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, SFL_SORT_PRIO_PRIMARY, {});
 }
index aa2a44b2920e41165fad693e2a676294cf3fa0e9..3efab4cf5d37a20827e99dd9129c0886e78b47d2 100644 (file)
 #include "../common/gamemodes/_mod.qh"
 #include "../common/teams.qh"
 
+/// \brief Describes a state of team balance entity.
+enum
+{
+       TEAM_BALANCE_UNINITIALIZED, ///< The team balance has not been initialized.
+       /// \brief TeamBalance_CheckAllowedTeams has been called.
+       TEAM_BALANCE_TEAMS_CHECKED,
+       /// \brief TeamBalance_GetTeamCounts has been called.
+       TEAM_BALANCE_TEAM_COUNTS_FILLED
+};
+
 /// \brief Indicates that the player is not allowed to join a team.
 const int TEAM_NOT_ALLOWED = -1;
 
+.int m_team_balance_state; ///< Holds the state of the team balance entity.
+.entity m_team_balance_team[NUM_TEAMS]; ///< ???
+
 .float m_team_score; ///< The score of the team.
 .int m_num_players; ///< Number of players (both humans and bots) in a team.
 .int m_num_bots; ///< Number of bots in a team.
@@ -62,320 +75,6 @@ void Team_SetTeamScore(entity team_, float score)
        team_.m_team_score = score;
 }
 
-void CheckAllowedTeams(entity for_whom)
-{
-       for (int i = 0; i < 4; ++i)
-       {
-               g_team_entities[i].m_num_players = TEAM_NOT_ALLOWED;
-               g_team_entities[i].m_num_bots = 0;
-               g_team_entities[i].m_lowest_human = NULL;
-               g_team_entities[i].m_lowest_bot = NULL;
-       }
-       
-       int teams_mask = 0;     
-       string teament_name = string_null;
-       bool mutator_returnvalue = MUTATOR_CALLHOOK(CheckAllowedTeams, teams_mask,
-               teament_name, for_whom);
-       teams_mask = M_ARGV(0, float);
-       teament_name = M_ARGV(1, string);
-       if (mutator_returnvalue)
-       {
-               for (int i = 0; i < 4; ++i)
-               {
-                       if (teams_mask & BIT(i))
-                       {
-                               g_team_entities[i].m_num_players = 0;
-                       }
-               }
-       }
-
-       // find out what teams are allowed if necessary
-       if (teament_name)
-       {
-               entity head = find(NULL, classname, teament_name);
-               while (head)
-               {
-                       if (Team_IsValidTeam(head.team))
-                       {
-                               Team_GetTeam(head.team).m_num_players = 0;
-                       }
-                       head = find(head, classname, teament_name);
-               }
-       }
-
-       // TODO: Balance quantity of bots across > 2 teams when bot_vs_human is set (and remove next line)
-       if (AvailableTeams() == 2)
-       if (autocvar_bot_vs_human && for_whom)
-       {
-               if (autocvar_bot_vs_human > 0)
-               {
-                       // find last team available
-                       if (IS_BOT_CLIENT(for_whom))
-                       {
-                               if (Team_IsAllowed(g_team_entities[3]))
-                               {
-                                       g_team_entities[2].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[1].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[0].m_num_players = TEAM_NOT_ALLOWED;
-                               }
-                               else if (Team_IsAllowed(g_team_entities[2]))
-                               {
-                                       g_team_entities[3].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[1].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[0].m_num_players = TEAM_NOT_ALLOWED;
-                               }
-                               else
-                               {
-                                       g_team_entities[3].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[2].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[0].m_num_players = TEAM_NOT_ALLOWED;
-                               }
-                               // no further cases, we know at least 2 teams exist
-                       }
-                       else
-                       {
-                               if (Team_IsAllowed(g_team_entities[0]))
-                               {
-                                       g_team_entities[1].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[2].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[3].m_num_players = TEAM_NOT_ALLOWED;
-                               }
-                               else if (Team_IsAllowed(g_team_entities[1]))
-                               {
-                                       g_team_entities[0].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[2].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[3].m_num_players = TEAM_NOT_ALLOWED;
-                               }
-                               else
-                               {
-                                       g_team_entities[0].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[1].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[3].m_num_players = TEAM_NOT_ALLOWED;
-                               }
-                               // no further cases, bots have one of the teams
-                       }
-               }
-               else
-               {
-                       // find first team available
-                       if (IS_BOT_CLIENT(for_whom))
-                       {
-                               if (Team_IsAllowed(g_team_entities[0]))
-                               {
-                                       g_team_entities[1].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[2].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[3].m_num_players = TEAM_NOT_ALLOWED;
-                               }
-                               else if (Team_IsAllowed(g_team_entities[1]))
-                               {
-                                       g_team_entities[0].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[2].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[3].m_num_players = TEAM_NOT_ALLOWED;
-                               }
-                               else
-                               {
-                                       g_team_entities[0].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[1].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[3].m_num_players = TEAM_NOT_ALLOWED;
-                               }
-                               // no further cases, we know at least 2 teams exist
-                       }
-                       else
-                       {
-                               if (Team_IsAllowed(g_team_entities[3]))
-                               {
-                                       g_team_entities[2].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[1].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[0].m_num_players = TEAM_NOT_ALLOWED;
-                               }
-                               else if (Team_IsAllowed(g_team_entities[2]))
-                               {
-                                       g_team_entities[3].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[1].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[0].m_num_players = TEAM_NOT_ALLOWED;
-                               }
-                               else
-                               {
-                                       g_team_entities[3].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[2].m_num_players = TEAM_NOT_ALLOWED;
-                                       g_team_entities[0].m_num_players = TEAM_NOT_ALLOWED;
-                               }
-                               // no further cases, bots have one of the teams
-                       }
-               }
-       }
-
-       if (!for_whom)
-       {
-               return;
-       }
-
-       // if player has a forced team, ONLY allow that one
-       if (for_whom.team_forced == NUM_TEAM_1 && Team_IsAllowed(
-               g_team_entities[0]))
-       {
-               g_team_entities[1].m_num_players = TEAM_NOT_ALLOWED;
-               g_team_entities[2].m_num_players = TEAM_NOT_ALLOWED;
-               g_team_entities[3].m_num_players = TEAM_NOT_ALLOWED;
-       }
-       else if (for_whom.team_forced == NUM_TEAM_2 && Team_IsAllowed(
-               g_team_entities[1]))
-       {
-               g_team_entities[0].m_num_players = TEAM_NOT_ALLOWED;
-               g_team_entities[2].m_num_players = TEAM_NOT_ALLOWED;
-               g_team_entities[3].m_num_players = TEAM_NOT_ALLOWED;
-       }
-       else if (for_whom.team_forced == NUM_TEAM_3 && Team_IsAllowed(
-               g_team_entities[2]))
-       {
-               g_team_entities[0].m_num_players = TEAM_NOT_ALLOWED;
-               g_team_entities[1].m_num_players = TEAM_NOT_ALLOWED;
-               g_team_entities[3].m_num_players = TEAM_NOT_ALLOWED;
-       }
-       else if (for_whom.team_forced == NUM_TEAM_4 && Team_IsAllowed(
-               g_team_entities[3]))
-       {
-               g_team_entities[0].m_num_players = TEAM_NOT_ALLOWED;
-               g_team_entities[1].m_num_players = TEAM_NOT_ALLOWED;
-               g_team_entities[2].m_num_players = TEAM_NOT_ALLOWED;
-       }
-}
-
-int GetAllowedTeams()
-{
-       int result = 0;
-       for (int i = 0; i < 4; ++i)
-       {
-               if (Team_IsAllowed(g_team_entities[i]))
-               {
-                       result |= BIT(i);
-               }
-       }
-       return result;
-}
-
-bool Team_IsAllowed(entity team_)
-{
-       return team_.m_num_players != TEAM_NOT_ALLOWED;
-}
-
-void GetTeamCounts(entity ignore)
-{
-       if (MUTATOR_CALLHOOK(GetTeamCounts) == true)
-       {
-               // Mutator has overriden the configuration.
-               for (int i = 0; i < 4; ++i)
-               {
-                       entity team_ = g_team_entities[i];
-                       if (Team_IsAllowed(team_))
-                       {
-                               MUTATOR_CALLHOOK(GetTeamCount, Team_NumberToTeam(i + 1), ignore,
-                                       team_.m_num_players, team_.m_num_bots, team_.m_lowest_human,
-                                       team_.m_lowest_bot);
-                               team_.m_num_players = M_ARGV(2, float);
-                               team_.m_num_bots = M_ARGV(3, float);
-                               team_.m_lowest_human = M_ARGV(4, entity);
-                               team_.m_lowest_bot = M_ARGV(5, entity);
-                       }
-               }
-       }
-       else
-       {
-               // Manually count all players.
-               FOREACH_CLIENT(true,
-               {
-                       if (it == ignore)
-                       {
-                               continue;
-                       }
-                       int team_num;
-                       if (IS_PLAYER(it) || it.caplayer)
-                       {
-                               team_num = it.team;
-                       }
-                       else if (it.team_forced > 0)
-                       {
-                               team_num = it.team_forced; // reserve the spot
-                       }
-                       else
-                       {
-                               continue;
-                       }
-                       if (!Team_IsValidTeam(team_num))
-                       {
-                               continue;
-                       }
-                       entity team_ = Team_GetTeam(team_num);
-                       if (!Team_IsAllowed(team_))
-                       {
-                               continue;
-                       }
-                       ++team_.m_num_players;
-                       if (IS_BOT_CLIENT(it))
-                       {
-                               ++team_.m_num_bots;
-                       }
-                       float temp_score = PlayerScore_Get(it, SP_SCORE);
-                       if (!IS_BOT_CLIENT(it))
-                       {
-                               if (team_.m_lowest_human == NULL)
-                               {
-                                       team_.m_lowest_human = it;
-                                       continue;
-                               }
-                               if (temp_score < PlayerScore_Get(team_.m_lowest_human,
-                                       SP_SCORE))
-                               {
-                                       team_.m_lowest_human = it;
-                               }
-                               continue;
-                       }
-                       if (team_.m_lowest_bot == NULL)
-                       {
-                               team_.m_lowest_bot = it;
-                               continue;
-                       }
-                       if (temp_score < PlayerScore_Get(team_.m_lowest_bot, SP_SCORE))
-                       {
-                               team_.m_lowest_bot = it;
-                       }
-               });
-       }
-
-       // if the player who has a forced team has not joined yet, reserve the spot
-       if (autocvar_g_campaign)
-       {
-               if (Team_IsValidIndex(autocvar_g_campaign_forceteam))
-               {
-                       entity team_ = Team_GetTeamFromIndex(autocvar_g_campaign_forceteam);
-                       if (team_.m_num_players == team_.m_num_bots)
-                       {
-                               ++team_.m_num_players;
-                       }
-               }
-       }
-}
-
-int Team_GetNumberOfPlayers(entity team_)
-{
-       return team_.m_num_players;
-}
-
-int Team_GetNumberOfBots(entity team_)
-{
-       return team_.m_num_bots;
-}
-
-entity Team_GetLowestHuman(entity team_)
-{
-       return team_.m_lowest_human;
-}
-
-entity Team_GetLowestBot(entity team_)
-{
-       return team_.m_lowest_bot;
-}
-
 void TeamchangeFrags(entity e)
 {
        PlayerScore_Clear(e);
@@ -595,111 +294,414 @@ bool SetPlayerTeam(entity player, int destination_team_index,
        return true;
 }
 
-int FindBestTeamsForBalance(entity player, bool use_score)
+entity TeamBalance_CheckAllowedTeams(entity for_whom)
 {
-       if (MUTATOR_CALLHOOK(FindBestTeams, player) == true)
-       {
-               return M_ARGV(1, float);
-       }
-       int team_bits = 0;
-       int previous_team = 0;
-       if (Team_IsAllowed(g_team_entities[0]))
+       entity balance = spawn();
+       for (int i = 0; i < NUM_TEAMS; ++i)
        {
-               team_bits = BIT(0);
-               previous_team = 1;
+               balance.(m_team_balance_team[i]) = spawn();
+               entity team_ = balance.(m_team_balance_team[i]);
+               team_.m_team_score = g_team_entities[i].m_team_score;
+               team_.m_num_players = TEAM_NOT_ALLOWED;
+               team_.m_num_bots = 0;
+               team_.m_lowest_human = NULL;
+               team_.m_lowest_bot = NULL;
        }
-       if (Team_IsAllowed(g_team_entities[1]))
+       
+       int teams_mask = 0;     
+       string teament_name = string_null;
+       bool mutator_returnvalue = MUTATOR_CALLHOOK(TeamBalance_CheckAllowedTeams,
+               teams_mask, teament_name, for_whom);
+       teams_mask = M_ARGV(0, float);
+       teament_name = M_ARGV(1, string);
+       if (mutator_returnvalue)
        {
-               if (previous_team == 0)
-               {
-                       team_bits = BIT(1);
-                       previous_team = 2;
-               }
-               else if (IsTeamSmallerThanTeam(2, previous_team, player, use_score))
-               {
-                       team_bits = BIT(1);
-                       previous_team = 2;
-               }
-               else if (IsTeamEqualToTeam(2, previous_team, player, use_score))
+               for (int i = 0; i < NUM_TEAMS; ++i)
                {
-                       team_bits |= BIT(1);
-                       previous_team = 2;
+                       if (teams_mask & BIT(i))
+                       {
+                               balance.(m_team_balance_team[i]).m_num_players = 0;
+                       }
                }
        }
-       if (Team_IsAllowed(g_team_entities[2]))
+
+       if (teament_name)
        {
-               if (previous_team == 0)
-               {
-                       team_bits = BIT(2);
-                       previous_team = 3;
-               }
-               else if (IsTeamSmallerThanTeam(3, previous_team, player, use_score))
-               {
-                       team_bits = BIT(2);
-                       previous_team = 3;
-               }
-               else if (IsTeamEqualToTeam(3, previous_team, player, use_score))
+               entity head = find(NULL, classname, teament_name);
+               while (head)
                {
-                       team_bits |= BIT(2);
-                       previous_team = 3;
+                       if (Team_IsValidTeam(head.team))
+                       {
+                               TeamBalance_GetTeam(balance, head.team).m_num_players = 0;
+                       }
+                       head = find(head, classname, teament_name);
                }
        }
-       if (Team_IsAllowed(g_team_entities[3]))
-       {
-               if (previous_team == 0)
-               {
-                       team_bits = BIT(3);
+
+       // TODO: Balance quantity of bots across > 2 teams when bot_vs_human is set (and remove next line)
+       if (AvailableTeams() == 2)
+       if (autocvar_bot_vs_human && for_whom)
+       {
+               if (autocvar_bot_vs_human > 0)
+               {
+                       // find last team available
+                       if (IS_BOT_CLIENT(for_whom))
+                       {
+                               if (TeamBalance_IsTeamAllowedInternal(balance, 4))
+                               {
+                                       TeamBalance_BanTeamsExcept(balance, 4);
+                               }
+                               else if (TeamBalance_IsTeamAllowedInternal(balance, 3))
+                               {
+                                       TeamBalance_BanTeamsExcept(balance, 3);
+                               }
+                               else
+                               {
+                                       TeamBalance_BanTeamsExcept(balance, 2);
+                               }
+                               // no further cases, we know at least 2 teams exist
+                       }
+                       else
+                       {
+                               if (TeamBalance_IsTeamAllowedInternal(balance, 1))
+                               {
+                                       TeamBalance_BanTeamsExcept(balance, 1);
+                               }
+                               else if (TeamBalance_IsTeamAllowedInternal(balance, 2))
+                               {
+                                       TeamBalance_BanTeamsExcept(balance, 2);
+                               }
+                               else
+                               {
+                                       TeamBalance_BanTeamsExcept(balance, 3);
+                               }
+                               // no further cases, bots have one of the teams
+                       }
+               }
+               else
+               {
+                       // find first team available
+                       if (IS_BOT_CLIENT(for_whom))
+                       {
+                               if (TeamBalance_IsTeamAllowedInternal(balance, 1))
+                               {
+                                       TeamBalance_BanTeamsExcept(balance, 1);
+                               }
+                               else if (TeamBalance_IsTeamAllowedInternal(balance, 2))
+                               {
+                                       TeamBalance_BanTeamsExcept(balance, 2);
+                               }
+                               else
+                               {
+                                       TeamBalance_BanTeamsExcept(balance, 3);
+                               }
+                               // no further cases, we know at least 2 teams exist
+                       }
+                       else
+                       {
+                               if (TeamBalance_IsTeamAllowedInternal(balance, 4))
+                               {
+                                       TeamBalance_BanTeamsExcept(balance, 4);
+                               }
+                               else if (TeamBalance_IsTeamAllowedInternal(balance, 3))
+                               {
+                                       TeamBalance_BanTeamsExcept(balance, 3);
+                               }
+                               else
+                               {
+                                       TeamBalance_BanTeamsExcept(balance, 2);
+                               }
+                               // no further cases, bots have one of the teams
+                       }
                }
-               else if (IsTeamSmallerThanTeam(4, previous_team, player, use_score))
+       }
+
+       if (!for_whom)
+       {
+               balance.m_team_balance_state = TEAM_BALANCE_TEAMS_CHECKED;
+               return balance;
+       }
+
+       // if player has a forced team, ONLY allow that one
+       for (int i = 1; i <= NUM_TEAMS; ++i)
+       {
+               if (for_whom.team_forced == Team_NumberToTeam(i) &&
+                       TeamBalance_IsTeamAllowedInternal(balance, i))
                {
-                       team_bits = BIT(3);
+                       TeamBalance_BanTeamsExcept(balance, i);
                }
-               else if (IsTeamEqualToTeam(4, previous_team, player, use_score))
+               break;
+       }
+       balance.m_team_balance_state = TEAM_BALANCE_TEAMS_CHECKED;
+       return balance;
+}
+
+void TeamBalance_Destroy(entity balance)
+{
+       if (balance == NULL)
+       {
+               return;
+       }
+       for (int i = 0; i < NUM_TEAMS; ++i)
+       {
+               remove(balance.(m_team_balance_team[i]));
+       }
+       remove(balance);
+}
+
+int TeamBalance_GetAllowedTeams(entity balance)
+{
+       if (balance == NULL)
+       {
+               LOG_FATAL("TeamBalance_GetAllowedTeams: Team balance entity is NULL.");
+       }
+       if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
+       {
+               LOG_FATAL("TeamBalance_GetAllowedTeams: "
+                       "Team balance entity is not initialized.");
+       }
+       int result = 0;
+       for (int i = 1; i <= NUM_TEAMS; ++i)
+       {
+               if (TeamBalance_IsTeamAllowedInternal(balance, i))
                {
-                       team_bits |= BIT(3);
+                       result |= Team_IndexToBit(i);
                }
        }
-       return team_bits;
+       return result;
+}
+
+bool TeamBalance_IsTeamAllowed(entity balance, int index)
+{
+       if (balance == NULL)
+       {
+               LOG_FATAL("TeamBalance_IsTeamAllowed: Team balance entity is NULL.");
+       }
+       if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
+       {
+               LOG_FATAL("TeamBalance_IsTeamAllowed: "
+                       "Team balance entity is not initialized.");
+       }
+       if (!Team_IsValidIndex(index))
+       {
+               LOG_FATALF("TeamBalance_IsTeamAllowed: Team index is invalid: %f",
+                       index);
+       }
+       return TeamBalance_IsTeamAllowedInternal(balance, index);
 }
 
-int FindBestTeamForBalance(entity player, float ignore_player)
+void TeamBalance_GetTeamCounts(entity balance, entity ignore)
 {
+       if (balance == NULL)
+       {
+               LOG_FATAL("TeamBalance_GetTeamCounts: Team balance entity is NULL.");
+       }
+       if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
+       {
+               LOG_FATAL("TeamBalance_GetTeamCounts: "
+                       "Team balance entity is not initialized.");
+       }
+       if (MUTATOR_CALLHOOK(TeamBalance_GetTeamCounts) == true)
+       {
+               // Mutator has overriden the configuration.
+               for (int i = 1; i <= NUM_TEAMS; ++i)
+               {
+                       entity team_ = TeamBalance_GetTeamFromIndex(balance, i);
+                       if (TeamBalanceTeam_IsAllowed(team_))
+                       {
+                               MUTATOR_CALLHOOK(TeamBalance_GetTeamCount, Team_NumberToTeam(i),
+                                       ignore, team_.m_num_players, team_.m_num_bots,
+                                       team_.m_lowest_human, team_.m_lowest_bot);
+                               team_.m_num_players = M_ARGV(2, float);
+                               team_.m_num_bots = M_ARGV(3, float);
+                               team_.m_lowest_human = M_ARGV(4, entity);
+                               team_.m_lowest_bot = M_ARGV(5, entity);
+                       }
+               }
+       }
+       else
+       {
+               // Manually count all players.
+               FOREACH_CLIENT(true,
+               {
+                       if (it == ignore)
+                       {
+                               continue;
+                       }
+                       int team_num;
+                       if (IS_PLAYER(it) || it.caplayer)
+                       {
+                               team_num = it.team;
+                       }
+                       else if (it.team_forced > 0)
+                       {
+                               team_num = it.team_forced; // reserve the spot
+                       }
+                       else
+                       {
+                               continue;
+                       }
+                       if (!Team_IsValidTeam(team_num))
+                       {
+                               continue;
+                       }
+                       entity team_ = TeamBalance_GetTeam(balance, team_num);
+                       if (!TeamBalanceTeam_IsAllowed(team_))
+                       {
+                               continue;
+                       }
+                       ++team_.m_num_players;
+                       if (IS_BOT_CLIENT(it))
+                       {
+                               ++team_.m_num_bots;
+                       }
+                       float temp_score = PlayerScore_Get(it, SP_SCORE);
+                       if (!IS_BOT_CLIENT(it))
+                       {
+                               if (team_.m_lowest_human == NULL)
+                               {
+                                       team_.m_lowest_human = it;
+                                       continue;
+                               }
+                               if (temp_score < PlayerScore_Get(team_.m_lowest_human,
+                                       SP_SCORE))
+                               {
+                                       team_.m_lowest_human = it;
+                               }
+                               continue;
+                       }
+                       if (team_.m_lowest_bot == NULL)
+                       {
+                               team_.m_lowest_bot = it;
+                               continue;
+                       }
+                       if (temp_score < PlayerScore_Get(team_.m_lowest_bot, SP_SCORE))
+                       {
+                               team_.m_lowest_bot = it;
+                       }
+               });
+       }
+
+       // if the player who has a forced team has not joined yet, reserve the spot
+       if (autocvar_g_campaign)
+       {
+               if (Team_IsValidIndex(autocvar_g_campaign_forceteam))
+               {
+                       entity team_ = TeamBalance_GetTeamFromIndex(balance,
+                               autocvar_g_campaign_forceteam);
+                       if (team_.m_num_players == team_.m_num_bots)
+                       {
+                               ++team_.m_num_players;
+                       }
+               }
+       }
+       balance.m_team_balance_state = TEAM_BALANCE_TEAM_COUNTS_FILLED;
+}
+
+int TeamBalance_GetNumberOfPlayers(entity balance, int index)
+{
+       if (balance == NULL)
+       {
+               LOG_FATAL("TeamBalance_GetNumberOfPlayers: "
+                       "Team balance entity is NULL.");
+       }
+       if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
+       {
+               LOG_FATAL("TeamBalance_GetNumberOfPlayers: "
+                       "TeamBalance_GetTeamCounts has not been called.");
+       }
+       if (!Team_IsValidIndex(index))
+       {
+               LOG_FATALF("TeamBalance_GetNumberOfPlayers: Team index is invalid: %f",
+                       index);
+       }
+       return balance.(m_team_balance_team[index - 1]).m_num_players;
+}
+
+int TeamBalance_FindBestTeam(entity balance, entity player, bool ignore_player)
+{
+       if (balance == NULL)
+       {
+               LOG_FATAL("TeamBalance_FindBestTeam: Team balance entity is NULL.");
+       }
+       if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
+       {
+               LOG_FATAL("TeamBalance_FindBestTeam: "
+                       "Team balance entity is not initialized.");
+       }
        // count how many players are in each team
        if (ignore_player)
        {
-               GetTeamCounts(player);
+               TeamBalance_GetTeamCounts(balance, player);
        }
        else
        {
-               GetTeamCounts(NULL);
+               TeamBalance_GetTeamCounts(balance, NULL);
        }
-       int team_bits = FindBestTeamsForBalance(player, true);
+       int team_bits = TeamBalance_FindBestTeams(balance, player, true);
        if (team_bits == 0)
        {
-               LOG_FATALF("FindBestTeam: No teams available for %s\n",
+               LOG_FATALF("TeamBalance_FindBestTeam: No teams available for %s\n",
                        MapInfo_Type_ToString(MapInfo_CurrentGametype()));
        }
        RandomSelection_Init();
-       if ((team_bits & BIT(0)) != 0)
+       for (int i = 1; i <= NUM_TEAMS; ++i)
        {
-               RandomSelection_AddFloat(1, 1, 1);
+               if (team_bits & Team_IndexToBit(i))
+               {
+                       RandomSelection_AddFloat(i, 1, 1);
+               }
        }
-       if ((team_bits & BIT(1)) != 0)
+       return RandomSelection_chosen_float;
+}
+
+int TeamBalance_FindBestTeams(entity balance, entity player, bool use_score)
+{
+       if (balance == NULL)
        {
-               RandomSelection_AddFloat(2, 1, 1);
+               LOG_FATAL("TeamBalance_FindBestTeams: Team balance entity is NULL.");
        }
-       if ((team_bits & BIT(2)) != 0)
+       if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
        {
-               RandomSelection_AddFloat(3, 1, 1);
+               LOG_FATAL("TeamBalance_FindBestTeams: "
+                       "TeamBalance_GetTeamCounts has not been called.");
        }
-       if ((team_bits & BIT(3)) != 0)
+       if (MUTATOR_CALLHOOK(TeamBalance_FindBestTeams, player) == true)
        {
-               RandomSelection_AddFloat(4, 1, 1);
+               return M_ARGV(1, float);
        }
-       return RandomSelection_chosen_float;
+       int team_bits = 0;
+       int previous_team = 0;
+       for (int i = 1; i <= NUM_TEAMS; ++i)
+       {
+               if (!TeamBalance_IsTeamAllowedInternal(balance, i))
+               {
+                       continue;
+               }
+               if (previous_team == 0)
+               {
+                       team_bits = Team_IndexToBit(i);
+                       previous_team = i;
+                       continue;
+               }
+               int compare = TeamBalance_CompareTeams(balance, i, previous_team,
+                       player, use_score);
+               if (compare == TEAMS_COMPARE_LESS)
+               {
+                       team_bits = Team_IndexToBit(i);
+                       previous_team = i;
+                       continue;
+               }
+               if (compare == TEAMS_COMPARE_EQUAL)
+               {
+                       team_bits |= Team_IndexToBit(i);
+                       previous_team = i;
+               }
+       }
+       return team_bits;
 }
 
-void JoinBestTeamForBalance(entity this, bool force_best_team)
+void TeamBalance_JoinBestTeam(entity this, bool force_best_team)
 {
        // don't join a team if we're not playing a team game
        if (!teamplay)
@@ -708,16 +710,16 @@ void JoinBestTeamForBalance(entity this, bool force_best_team)
        }
 
        // find out what teams are available
-       CheckAllowedTeams(this);
+       entity balance = TeamBalance_CheckAllowedTeams(this);
 
        // if we don't care what team they end up on, put them on whatever team they entered as.
        // if they're not on a valid team, then let other code put them on the smallest team
        if (!force_best_team)
        {
                int selected_team_num = -1;
-               for (int i = 0; i < 4; ++i)
+               for (int i = 1; i <= NUM_TEAMS; ++i)
                {
-                       if (Team_IsAllowed(g_team_entities[i]) && (this.team ==
+                       if (TeamBalance_IsTeamAllowedInternal(balance, i) && (this.team ==
                                Team_NumberToTeam(i)))
                        {
                                selected_team_num = this.team;
@@ -729,15 +731,17 @@ void JoinBestTeamForBalance(entity this, bool force_best_team)
                {
                        SetPlayerTeamSimple(this, selected_team_num);
                        LogTeamchange(this.playerid, this.team, 99);
+                       TeamBalance_Destroy(balance);
                        return;
                }
        }
        // otherwise end up on the smallest team (handled below)
        if (this.bot_forced_team)
        {
+               TeamBalance_Destroy(balance);
                return;
        }
-       int best_team_index = FindBestTeamForBalance(this, true);
+       int best_team_index = TeamBalance_FindBestTeam(balance, this, true);
        int best_team_num = Team_NumberToTeam(best_team_index);
        int old_team_index = Team_TeamToNumber(this.team);
        TeamchangeFrags(this);
@@ -745,78 +749,158 @@ void JoinBestTeamForBalance(entity this, bool force_best_team)
        LogTeamchange(this.playerid, this.team, 2); // log auto join
        if ((old_team_index != -1) && !IS_BOT_CLIENT(this))
        {
-               AutoBalanceBots(old_team_index, best_team_index);
+               TeamBalance_AutoBalanceBots(balance, old_team_index, best_team_index);
        }
        KillPlayerForTeamChange(this);
+       TeamBalance_Destroy(balance);
 }
 
-bool IsTeamSmallerThanTeam(int team_index_a, int team_index_b, entity player,
-       bool use_score)
+int TeamBalance_CompareTeams(entity balance, int team_index_a, int team_index_b,
+       entity player, bool use_score)
 {
+       if (balance == NULL)
+       {
+               LOG_FATAL("TeamBalance_CompareTeams: Team balance entity is NULL.");
+       }
+       if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
+       {
+               LOG_FATAL("TeamBalance_CompareTeams: "
+                       "TeamBalance_GetTeamCounts has not been called.");
+       }
        if (!Team_IsValidIndex(team_index_a))
        {
-               LOG_FATALF("IsTeamSmallerThanTeam: team_index_a is invalid: %f",
+               LOG_FATALF("TeamBalance_CompareTeams: team_index_a is invalid: %f",
                        team_index_a);
        }
        if (!Team_IsValidIndex(team_index_b))
        {
-               LOG_FATALF("IsTeamSmallerThanTeam: team_index_b is invalid: %f",
+               LOG_FATALF("TeamBalance_CompareTeams: team_index_b is invalid: %f",
                        team_index_b);
        }
        if (team_index_a == team_index_b)
        {
-               return false;
+               return TEAMS_COMPARE_EQUAL;
        }
-       entity team_a = Team_GetTeamFromIndex(team_index_a);
-       entity team_b = Team_GetTeamFromIndex(team_index_b);
-       if (!Team_IsAllowed(team_a) || !Team_IsAllowed(team_b))
+       entity team_a = TeamBalance_GetTeamFromIndex(balance, team_index_a);
+       entity team_b = TeamBalance_GetTeamFromIndex(balance, team_index_b);
+       return TeamBalance_CompareTeamsInternal(team_a, team_b, player, use_score);
+}
+
+void TeamBalance_AutoBalanceBots(entity balance, int source_team_index,
+       int destination_team_index)
+{
+       if (balance == NULL)
        {
-               return false;
+               LOG_FATAL("TeamBalance_AutoBalanceBots: Team balance entity is NULL.");
        }
-       int num_players_team_a = team_a.m_num_players;
-       int num_players_team_b = team_b.m_num_players;
-       if (IS_REAL_CLIENT(player) && bots_would_leave)
+       if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
        {
-               num_players_team_a -= team_a.m_num_bots;
-               num_players_team_b -= team_b.m_num_bots;
+               LOG_FATAL("TeamBalance_AutoBalanceBots: "
+                       "TeamBalance_GetTeamCounts has not been called.");
        }
-       if (!use_score)
+       if (!Team_IsValidIndex(source_team_index))
        {
-               return num_players_team_a < num_players_team_b;
+               LOG_WARNF("AutoBalanceBots: Source team index is invalid: %f",
+                       source_team_index);
+               return;
        }
-       if (num_players_team_a < num_players_team_b)
+       if (!Team_IsValidIndex(destination_team_index))
        {
-               return true;
+               LOG_WARNF("AutoBalanceBots: Destination team index is invalid: %f",
+                       destination_team_index);
+               return;
        }
-       if (num_players_team_a > num_players_team_b)
+       if (!autocvar_g_balance_teams ||
+               !autocvar_g_balance_teams_prevent_imbalance)
        {
-               return false;
+               return;
+       }
+       entity source_team = TeamBalance_GetTeamFromIndex(balance,
+               source_team_index);
+       if (!TeamBalanceTeam_IsAllowed(source_team))
+       {
+               return;
        }
-       return team_a.m_team_score < team_b.m_team_score;
+       entity destination_team = TeamBalance_GetTeamFromIndex(balance,
+               destination_team_index);
+       if ((destination_team.m_num_players <= source_team.m_num_players) ||
+               (destination_team.m_lowest_bot == NULL))
+       {
+               return;
+       }
+       SetPlayerTeamSimple(destination_team.m_lowest_bot,
+               Team_NumberToTeam(source_team_index));
+       KillPlayerForTeamChange(destination_team.m_lowest_bot);
 }
 
-bool IsTeamEqualToTeam(int team_index_a, int team_index_b, entity player,
-       bool use_score)
+bool TeamBalance_IsTeamAllowedInternal(entity balance, int index)
 {
-       if (!Team_IsValidIndex(team_index_a))
+       return balance.(m_team_balance_team[index - 1]).m_num_players !=
+               TEAM_NOT_ALLOWED;
+}
+
+void TeamBalance_BanTeamsExcept(entity balance, int index)
+{
+       for (int i = 1; i <= NUM_TEAMS; ++i)
        {
-               LOG_FATALF("IsTeamEqualToTeam: team_index_a is invalid: %f",
-                       team_index_a);
+               if (i != index)
+               {
+                       balance.(m_team_balance_team[i - 1]).m_num_players =
+                               TEAM_NOT_ALLOWED;
+               }
        }
-       if (!Team_IsValidIndex(team_index_b))
+}
+
+entity TeamBalance_GetTeamFromIndex(entity balance, int index)
+{
+       if (!Team_IsValidIndex(index))
        {
-               LOG_FATALF("IsTeamEqualToTeam: team_index_b is invalid: %f",
-                       team_index_b);
+               LOG_FATALF("TeamBalance_GetTeamFromIndex: Index is invalid: %f", index);
        }
-       if (team_index_a == team_index_b)
+       return balance.m_team_balance_team[index - 1];
+}
+
+entity TeamBalance_GetTeam(entity balance, int team_num)
+{
+       return TeamBalance_GetTeamFromIndex(balance, Team_TeamToNumber(team_num));
+}
+
+bool TeamBalanceTeam_IsAllowed(entity team_)
+{
+       return team_.m_num_players != TEAM_NOT_ALLOWED;
+}
+
+int TeamBalanceTeam_GetNumberOfPlayers(entity team_)
+{
+       return team_.m_num_players;
+}
+
+int TeamBalanceTeam_GetNumberOfBots(entity team_)
+{
+       return team_.m_num_bots;
+}
+
+entity TeamBalanceTeam_GetLowestHuman(entity team_)
+{
+       return team_.m_lowest_human;
+}
+
+entity TeamBalanceTeam_GetLowestBot(entity team_)
+{
+       return team_.m_lowest_bot;
+}
+
+int TeamBalance_CompareTeamsInternal(entity team_a, entity team_b,
+       entity player, bool use_score)
+{
+       if (team_a == team_b)
        {
-               return true;
+               return TEAMS_COMPARE_EQUAL;
        }
-       entity team_a = Team_GetTeamFromIndex(team_index_a);
-       entity team_b = Team_GetTeamFromIndex(team_index_b);
-       if (!Team_IsAllowed(team_a) || !Team_IsAllowed(team_b))
+       if (!TeamBalanceTeam_IsAllowed(team_a) ||
+               !TeamBalanceTeam_IsAllowed(team_b))
        {
-               return false;
+               return TEAMS_COMPARE_INVALID;
        }
        int num_players_team_a = team_a.m_num_players;
        int num_players_team_b = team_b.m_num_players;
@@ -825,15 +909,27 @@ bool IsTeamEqualToTeam(int team_index_a, int team_index_b, entity player,
                num_players_team_a -= team_a.m_num_bots;
                num_players_team_b -= team_b.m_num_bots;
        }
+       if (num_players_team_a < num_players_team_b)
+       {
+               return TEAMS_COMPARE_LESS;
+       }
+       if (num_players_team_a > num_players_team_b)
+       {
+               return TEAMS_COMPARE_GREATER;
+       }
        if (!use_score)
        {
-               return num_players_team_a == num_players_team_b;
+               return TEAMS_COMPARE_EQUAL;
        }
-       if (num_players_team_a != num_players_team_b)
+       if (team_a.m_team_score < team_b.m_team_score)
        {
-               return false;
+               return TEAMS_COMPARE_LESS;
+       }
+       if (team_a.m_team_score > team_b.m_team_score)
+       {
+               return TEAMS_COMPARE_GREATER;
        }
-       return team_a.m_team_score == team_b.m_team_score;
+       return TEAMS_COMPARE_EQUAL;
 }
 
 void SV_ChangeTeam(entity this, float _color)
@@ -866,17 +962,34 @@ void SV_ChangeTeam(entity this, float _color)
                return;
        }
 
-       CheckAllowedTeams(this);
+       entity balance = TeamBalance_CheckAllowedTeams(this);
 
-       if (destination_team_index == 1 && !Team_IsAllowed(g_team_entities[0])) destination_team_index = 4;
-       if (destination_team_index == 4 && !Team_IsAllowed(g_team_entities[3])) destination_team_index = 3;
-       if (destination_team_index == 3 && !Team_IsAllowed(g_team_entities[2])) destination_team_index = 2;
-       if (destination_team_index == 2 && !Team_IsAllowed(g_team_entities[1])) destination_team_index = 1;
+       if (destination_team_index == 1 && !TeamBalance_IsTeamAllowedInternal(
+               balance, 1))
+       {
+               destination_team_index = 4;
+       }
+       if (destination_team_index == 4 && !TeamBalance_IsTeamAllowedInternal(
+               balance, 4))
+       {
+               destination_team_index = 3;
+       }
+       if (destination_team_index == 3 && !TeamBalance_IsTeamAllowedInternal(
+               balance, 3))
+       {
+               destination_team_index = 2;
+       }
+       if (destination_team_index == 2 && !TeamBalance_IsTeamAllowedInternal(
+               balance, 2))
+       {
+               destination_team_index = 1;
+       }
 
        // not changing teams
        if (source_color == destination_color)
        {
                SetPlayerTeam(this, destination_team_index, source_team_index, true);
+               TeamBalance_Destroy(balance);
                return;
        }
 
@@ -888,10 +1001,12 @@ void SV_ChangeTeam(entity this, float _color)
        // autocvar_g_balance_teams_prevent_imbalance only makes sense if autocvar_g_balance_teams is on, as it makes the team selection dialog pointless
        if (autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance)
        {
-               GetTeamCounts(this);
-               if ((BIT(destination_team_index - 1) & FindBestTeamsForBalance(this, false)) == 0)
+               TeamBalance_GetTeamCounts(balance, this);
+               if ((Team_IndexToBit(destination_team_index) &
+                       TeamBalance_FindBestTeams(balance, this, false)) == 0)
                {
                        Send_Notification(NOTIF_ONE, this, MSG_INFO, INFO_TEAMCHANGE_LARGERTEAM);
+                       TeamBalance_Destroy(balance);
                        return;
                }
        }
@@ -903,47 +1018,16 @@ void SV_ChangeTeam(entity this, float _color)
        if (!SetPlayerTeam(this, destination_team_index, source_team_index,
                !IS_CLIENT(this)))
        {
+               TeamBalance_Destroy(balance);
                return;
        }
-       AutoBalanceBots(source_team_index, destination_team_index);
+       TeamBalance_AutoBalanceBots(balance, source_team_index,
+               destination_team_index);
        if (!IS_PLAYER(this) || (source_team_index == destination_team_index))
        {
+               TeamBalance_Destroy(balance);
                return;
        }
        KillPlayerForTeamChange(this);
-}
-
-void AutoBalanceBots(int source_team_index, int destination_team_index)
-{
-       if (!Team_IsValidIndex(source_team_index))
-       {
-               LOG_WARNF("AutoBalanceBots: Source team index is invalid: %f",
-                       source_team_index);
-               return;
-       }
-       if (!Team_IsValidIndex(destination_team_index))
-       {
-               LOG_WARNF("AutoBalanceBots: Destination team index is invalid: %f",
-                       destination_team_index);
-               return;
-       }
-       if (!autocvar_g_balance_teams ||
-               !autocvar_g_balance_teams_prevent_imbalance)
-       {
-               return;
-       }
-       entity source_team = Team_GetTeamFromIndex(source_team_index);
-       if (!Team_IsAllowed(source_team))
-       {
-               return;
-       }
-       entity destination_team = Team_GetTeamFromIndex(destination_team_index);
-       if ((destination_team.m_num_players <= source_team.m_num_players) ||
-               (destination_team.m_lowest_bot == NULL))
-       {
-               return;
-       }
-       SetPlayerTeamSimple(destination_team.m_lowest_bot,
-               Team_NumberToTeam(source_team_index));
-       KillPlayerForTeamChange(destination_team.m_lowest_bot);
+       TeamBalance_Destroy(balance);
 }
index 71b73b240fde3785c4b03eed1aca6b9ca6c0fa7c..e589a176f139890b69610505eaa87641b97d03e9 100644 (file)
@@ -24,63 +24,6 @@ float Team_GetTeamScore(entity team_);
 /// \param[in] score Score to set.
 void Team_SetTeamScore(entity team_, float score);
 
-/// \brief Checks whether the player can join teams according to global
-/// configuration and mutator settings.
-/// \param[in] for_whom Player to check for. Pass NULL for global rules.
-/// \note This function sets various internal variables and is required to be
-/// called before several other functions.
-void CheckAllowedTeams(entity for_whom);
-
-/// \brief Returns the bitmask of allowed teams.
-/// \return Bitmask of allowed teams.
-/// \note You need to call CheckAllowedTeams before calling this function.
-int GetAllowedTeams();
-
-/// \brief Returns whether the team is allowed.
-/// \param[in] team_ Team entity.
-/// \return True if team is allowed, false otherwise.
-/// \note You need to call CheckAllowedTeams before calling this function.
-bool Team_IsAllowed(entity team_);
-
-/// \brief Counts the number of players and various other information about
-/// each team.
-/// \param[in] ignore Player to ignore. This is useful if you plan to switch the
-/// player's team. Pass NULL for global information.
-/// \note You need to call CheckAllowedTeams before calling this function.
-/// \note This function sets many internal variables and is required to be
-/// called before several other functions.
-void GetTeamCounts(entity ignore);
-
-/// \brief Returns the number of players (both humans and bots) in a team.
-/// \param[in] team_ Team entity.
-/// \return Number of player (both humans and bots) in a team.
-/// \note You need to call CheckAllowedTeams and GetTeamCounts before calling
-/// this function.
-int Team_GetNumberOfPlayers(entity team_);
-
-/// \brief Returns the number of bots in a team.
-/// \param[in] team_ Team entity.
-/// \return Number of bots in a team.
-/// \note You need to call CheckAllowedTeams and GetTeamCounts before calling
-/// this function.
-int Team_GetNumberOfBots(entity team_);
-
-/// \brief Returns the human with the lowest score in a team or NULL if there is
-/// none.
-/// \param[in] team_ Team entity.
-/// \return Human with the lowest score in a team or NULL if there is none.
-/// \note You need to call CheckAllowedTeams and GetTeamCounts before calling
-/// this function.
-entity Team_GetLowestHuman(entity team_);
-
-/// \brief Returns the bot with the lowest score in a team or NULL if there is
-/// none.
-/// \param[in] team_ Team entity.
-/// \return Bot with the lowest score in a team or NULL if there is none.
-/// \note You need to call CheckAllowedTeams and GetTeamCounts before calling
-/// this function.
-entity Team_GetLowestBot(entity team_);
-
 int redowned, blueowned, yellowowned, pinkowned;
 
 void TeamchangeFrags(entity e);
@@ -118,52 +61,173 @@ bool SetPlayerTeamSimple(entity player, int team_num);
 bool SetPlayerTeam(entity player, int destination_team_index,
        int source_team_index, bool no_print);
 
-/// \brief Returns the bitmask of the teams that will make the game most
-/// balanced if the player joins any of them.
-/// \param[in] player Player to check.
-/// \param[in] use_score Whether to take into account team scores.
-/// \return Bitmask of the teams that will make the game most balanced if the
-/// player joins any of them.
-/// \note You need to call CheckAllowedTeams and GetTeamCounts before calling
-/// this function.
-int FindBestTeamsForBalance(entity player, bool use_score);
+// ========================= Team balance API =================================
+
+/// \brief Checks whether the player can join teams according to global
+/// configuration and mutator settings.
+/// \param[in] for_whom Player to check for. Pass NULL for global rules.
+/// \return Team balance entity that holds information about teams. This entity
+/// must be manually destroyed by calling TeamBalance_Destroy.
+entity TeamBalance_CheckAllowedTeams(entity for_whom);
+
+/// \brief Destroy the team balance entity.
+/// \param[in,out] balance Team balance entity to destroy.
+/// \note Team balance entity is allowed to be NULL.
+void TeamBalance_Destroy(entity balance);
+
+/// \brief Returns the bitmask of allowed teams.
+/// \param[in] balance Team balance entity.
+/// \return Bitmask of allowed teams.
+int TeamBalance_GetAllowedTeams(entity balance);
+
+/// \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.
+/// \return True if team change to the specified team is allowed, false
+/// otherwise.
+bool TeamBalance_IsTeamAllowed(entity balance, int index);
+
+/// \brief Counts the number of players and various other information about
+/// each team.
+/// \param[in,out] balance Team balance entity.
+/// \param[in] ignore Player to ignore. This is useful if you plan to switch the
+/// player's team. Pass NULL for global information.
+/// \note This function updates the internal state of the team balance entity.
+void TeamBalance_GetTeamCounts(entity balance, entity ignore);
+
+/// \brief Returns the number of players (both humans and bots) in a team.
+/// \param[in] balance Team balance entity.
+/// \param[in] index Index of the team.
+/// \return Number of player (both humans and bots) in a team.
+/// \note You need to call TeamBalance_GetTeamCounts before calling this
+/// function.
+int TeamBalance_GetNumberOfPlayers(entity balance, int index);
 
 /// \brief Finds the team that will make the game most balanced if the player
 /// joins it.
+/// \param[in] balance Team balance entity.
 /// \param[in] player Player to check.
 /// \param[in] ignore_player ???
 /// \return Index of the team that will make the game most balanced if the
 /// player joins it. If there are several equally good teams available, the
 /// function will pick a random one.
-int FindBestTeamForBalance(entity player, float ignore_player);
-
-void JoinBestTeamForBalance(entity this, bool force_best_team);
+int TeamBalance_FindBestTeam(entity balance, entity player, bool ignore_player);
 
-/// \brief Returns whether one team is smaller than the other.
-/// \param[in] team_index_a Index of the first team.
-/// \param[in] team_index_b Index of the second team.
+/// \brief Returns the bitmask of the teams that will make the game most
+/// balanced if the player joins any of them.
+/// \param[in] balance Team balance entity.
 /// \param[in] player Player to check.
 /// \param[in] use_score Whether to take into account team scores.
-/// \return True if first team is smaller than the second one, false otherwise.
-/// \note You need to call CheckAllowedTeams and GetTeamCounts before calling
-/// this function.
-bool IsTeamSmallerThanTeam(int team_index_a, int team_index_b, entity player,
-       bool use_score);
-
-/// \brief Returns whether one team is equal to the other.
+/// \return Bitmask of the teams that will make the game most balanced if the
+/// player joins any of them.
+/// \note You need to call TeamBalance_GetTeamCounts before calling this
+/// function.
+int TeamBalance_FindBestTeams(entity balance, entity player, bool use_score);
+
+void TeamBalance_JoinBestTeam(entity this, bool force_best_team);
+
+/// \brief Describes the result of comparing teams.
+enum
+{
+       TEAMS_COMPARE_INVALID, ///< One or both teams are invalid.
+       TEAMS_COMPARE_LESS, ///< First team is less than the second one.
+       TEAMS_COMPARE_EQUAL, ///< Both teams are equal.
+       TEAMS_COMPARE_GREATER ///< First team the greater than the second one.
+};
+
+/// \brief Compares two teams for the purposes of game balance.
+/// \param[in] balance Team balance entity.
 /// \param[in] team_index_a Index of the first team.
 /// \param[in] team_index_b Index of the second team.
 /// \param[in] player Player to check.
 /// \param[in] use_score Whether to take into account team scores.
-/// \return True if first team is equal to the second one, false otherwise.
-/// \note You need to call CheckAllowedTeams and GetTeamCounts before calling
-/// this function.
-bool IsTeamEqualToTeam(int team_index_a, int team_index_b, entity player,
-       bool use_score);
+/// \return TEAMS_COMPARE value. See above.
+/// \note You need to call TeamBalance_GetTeamCounts before calling this
+/// function.
+int TeamBalance_CompareTeams(entity balance, int team_index_a, int team_index_b,
+       entity player, bool use_score);
 
 /// \brief Auto balances bots in teams after the player has changed team.
+/// \param[in] balance Team balance entity.
 /// \param[in] source_team_index Previous index of the team of the player.
 /// \param[in] destination_team_index Current index of the team of the player.
 /// \note You need to call CheckAllowedTeams and GetTeamCounts before calling
 /// this function.
-void AutoBalanceBots(int source_team_index, int destination_team_index);
+void TeamBalance_AutoBalanceBots(entity balance, int source_team_index,
+       int destination_team_index);
+
+// ============================ Internal API ==================================
+
+/// \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.
+/// \return True if team change to the specified team is allowed, false
+/// otherwise.
+/// \note This function bypasses all the sanity checks.
+bool TeamBalance_IsTeamAllowedInternal(entity balance, int index);
+
+/// \brief Bans team change to all teams except the given one.
+/// \param[in,out] balance Team balance entity.
+/// \param[in] index Index of the team.
+void TeamBalance_BanTeamsExcept(entity balance, int index);
+
+/// \brief Returns the team entity of the team balance entity at the given
+/// index.
+/// \param[in] balance Team balance entity.
+/// \param[in] index Index of the team.
+/// \return Team entity of the team balance entity at the given index.
+entity TeamBalance_GetTeamFromIndex(entity balance, int index);
+
+/// \brief Returns the team entity of the team balance entity that corresponds
+/// to the given TEAM_NUM value.
+/// \param[in] balance Team balance entity.
+/// \param[in] team_num Team value. See TEAM_NUM constants.
+/// \return Team entity of the team balance entity that corresponds to the given
+/// TEAM_NUM value.
+entity TeamBalance_GetTeam(entity balance, int team_num);
+
+/// \brief Returns whether the team is allowed.
+/// \param[in] team_ Team entity.
+/// \return True if team is allowed, false otherwise.
+bool TeamBalanceTeam_IsAllowed(entity team_);
+
+/// \brief Returns the number of players (both humans and bots) in a team.
+/// \param[in] team_ Team entity.
+/// \return Number of player (both humans and bots) in a team.
+/// \note You need to call TeamBalance_GetTeamCounts before calling this
+/// function.
+int TeamBalanceTeam_GetNumberOfPlayers(entity team_);
+
+/// \brief Returns the number of bots in a team.
+/// \param[in] team_ Team entity.
+/// \return Number of bots in a team.
+/// \note You need to call TeamBalance_GetTeamCounts before calling this
+/// function.
+int TeamBalanceTeam_GetNumberOfBots(entity team_);
+
+/// \brief Returns the human with the lowest score in a team or NULL if there is
+/// none.
+/// \param[in] team_ Team entity.
+/// \return Human with the lowest score in a team or NULL if there is none.
+/// \note You need to call TeamBalance_GetTeamCounts before calling this
+/// function.
+entity TeamBalanceTeam_GetLowestHuman(entity team_);
+
+/// \brief Returns the bot with the lowest score in a team or NULL if there is
+/// none.
+/// \param[in] team_ Team entity.
+/// \return Bot with the lowest score in a team or NULL if there is none.
+/// \note You need to call TeamBalance_GetTeamCounts before calling this
+/// function.
+entity TeamBalanceTeam_GetLowestBot(entity team_);
+
+/// \brief Compares two teams for the purposes of game balance.
+/// \param[in] team_a First team.
+/// \param[in] team_b Second team.
+/// \param[in] player Player to check.
+/// \param[in] use_score Whether to take into account team scores.
+/// \return TEAMS_COMPARE value. See above.
+/// \note You need to call TeamBalance_GetTeamCounts before calling this
+/// function.
+int TeamBalance_CompareTeamsInternal(entity team_a, entity team_index_b,
+       entity player, bool use_score);