From 8e3930dd6ce711211188ec1783293033d434fca7 Mon Sep 17 00:00:00 2001 From: Lyberta Date: Fri, 9 Mar 2018 13:14:03 +0300 Subject: [PATCH] New team balance API. --- .../gamemodes/gamemode/nexball/nexball.qc | 2 +- .../gamemode/onslaught/sv_onslaught.qc | 7 +- qcsrc/common/t_items.qc | 12 +- qcsrc/common/teams.qh | 10 + qcsrc/server/bot/default/bot.qc | 10 +- qcsrc/server/client.qc | 13 +- qcsrc/server/command/cmd.qc | 9 +- qcsrc/server/command/sv_cmd.qc | 68 +- qcsrc/server/g_world.qc | 6 +- qcsrc/server/mutators/events.qh | 24 +- .../mutators/mutator/gamemode_assault.qc | 2 +- qcsrc/server/mutators/mutator/gamemode_ca.qc | 3 +- qcsrc/server/mutators/mutator/gamemode_ctf.qc | 6 +- .../mutators/mutator/gamemode_domination.qc | 7 +- .../mutators/mutator/gamemode_freezetag.qc | 3 +- .../mutators/mutator/gamemode_invasion.qc | 5 +- .../mutators/mutator/gamemode_keyhunt.qc | 3 +- .../server/mutators/mutator/gamemode_race.qc | 3 +- qcsrc/server/mutators/mutator/gamemode_tdm.qc | 3 +- qcsrc/server/scores_rules.qc | 12 +- qcsrc/server/teamplay.qc | 1012 +++++++++-------- qcsrc/server/teamplay.qh | 234 ++-- 22 files changed, 835 insertions(+), 619 deletions(-) diff --git a/qcsrc/common/gamemodes/gamemode/nexball/nexball.qc b/qcsrc/common/gamemodes/gamemode/nexball/nexball.qc index fa718eb16..e4b755fc4 100644 --- a/qcsrc/common/gamemodes/gamemode/nexball/nexball.qc +++ b/qcsrc/common/gamemodes/gamemode/nexball/nexball.qc @@ -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; diff --git a/qcsrc/common/gamemodes/gamemode/onslaught/sv_onslaught.qc b/qcsrc/common/gamemodes/gamemode/onslaught/sv_onslaught.qc index 04425c24b..8fc4d89c4 100644 --- a/qcsrc/common/gamemodes/gamemode/onslaught/sv_onslaught.qc +++ b/qcsrc/common/gamemodes/gamemode/onslaught/sv_onslaught.qc @@ -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); diff --git a/qcsrc/common/t_items.qc b/qcsrc/common/t_items.qc index 183c38d3b..4ce8c8104 100644 --- a/qcsrc/common/t_items.qc +++ b/qcsrc/common/t_items.qc @@ -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); diff --git a/qcsrc/common/teams.qh b/qcsrc/common/teams.qh index 032aa38d6..1b03540dd 100644 --- a/qcsrc/common/teams.qh +++ b/qcsrc/common/teams.qh @@ -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) diff --git a/qcsrc/server/bot/default/bot.qc b/qcsrc/server/bot/default/bot.qc index ec6ee35d3..3ce03694c 100644 --- a/qcsrc/server/bot/default/bot.qc +++ b/qcsrc/server/bot/default/bot.qc @@ -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; diff --git a/qcsrc/server/client.qc b/qcsrc/server/client.qc index 4109818ab..410245460 100644 --- a/qcsrc/server/client.qc +++ b/qcsrc/server/client.qc @@ -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; diff --git a/qcsrc/server/command/cmd.qc b/qcsrc/server/command/cmd.qc index 2314f7103..af228f729 100644 --- a/qcsrc/server/command/cmd.qc +++ b/qcsrc/server/command/cmd.qc @@ -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)) diff --git a/qcsrc/server/command/sv_cmd.qc b/qcsrc/server/command/sv_cmd.qc index d7ede6af4..9d8c901db 100644 --- a/qcsrc/server/command/sv_cmd.qc +++ b/qcsrc/server/command/sv_cmd.qc @@ -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, { diff --git a/qcsrc/server/g_world.qc b/qcsrc/server/g_world.qc index 5c1724ce3..4777524f0 100644 --- a/qcsrc/server/g_world.qc +++ b/qcsrc/server/g_world.qc @@ -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; } diff --git a/qcsrc/server/mutators/events.qh b/qcsrc/server/mutators/events.qh index 6853c04a1..35f69fa0b 100644 --- a/qcsrc/server/mutators/events.qh +++ b/qcsrc/server/mutators/events.qh @@ -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) \ diff --git a/qcsrc/server/mutators/mutator/gamemode_assault.qc b/qcsrc/server/mutators/mutator/gamemode_assault.qc index aa7137a26..f3f104197 100644 --- a/qcsrc/server/mutators/mutator/gamemode_assault.qc +++ b/qcsrc/server/mutators/mutator/gamemode_assault.qc @@ -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); diff --git a/qcsrc/server/mutators/mutator/gamemode_ca.qc b/qcsrc/server/mutators/mutator/gamemode_ca.qc index 43ed39aea..077e277a6 100644 --- a/qcsrc/server/mutators/mutator/gamemode_ca.qc +++ b/qcsrc/server/mutators/mutator/gamemode_ca.qc @@ -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) diff --git a/qcsrc/server/mutators/mutator/gamemode_ctf.qc b/qcsrc/server/mutators/mutator/gamemode_ctf.qc index b34e3f59f..e5ac9eb77 100644 --- a/qcsrc/server/mutators/mutator/gamemode_ctf.qc +++ b/qcsrc/server/mutators/mutator/gamemode_ctf.qc @@ -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); diff --git a/qcsrc/server/mutators/mutator/gamemode_domination.qc b/qcsrc/server/mutators/mutator/gamemode_domination.qc index c72d5fbfd..dec8ac5a9 100644 --- a/qcsrc/server/mutators/mutator/gamemode_domination.qc +++ b/qcsrc/server/mutators/mutator/gamemode_domination.qc @@ -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; diff --git a/qcsrc/server/mutators/mutator/gamemode_freezetag.qc b/qcsrc/server/mutators/mutator/gamemode_freezetag.qc index 36546c43a..0ba3d04f1 100644 --- a/qcsrc/server/mutators/mutator/gamemode_freezetag.qc +++ b/qcsrc/server/mutators/mutator/gamemode_freezetag.qc @@ -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) diff --git a/qcsrc/server/mutators/mutator/gamemode_invasion.qc b/qcsrc/server/mutators/mutator/gamemode_invasion.qc index 1b8b77ae0..575fa308d 100644 --- a/qcsrc/server/mutators/mutator/gamemode_invasion.qc +++ b/qcsrc/server/mutators/mutator/gamemode_invasion.qc @@ -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) { diff --git a/qcsrc/server/mutators/mutator/gamemode_keyhunt.qc b/qcsrc/server/mutators/mutator/gamemode_keyhunt.qc index 04576486b..6e0fc0ffa 100644 --- a/qcsrc/server/mutators/mutator/gamemode_keyhunt.qc +++ b/qcsrc/server/mutators/mutator/gamemode_keyhunt.qc @@ -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) diff --git a/qcsrc/server/mutators/mutator/gamemode_race.qc b/qcsrc/server/mutators/mutator/gamemode_race.qc index aa6d12a83..82d0ca79e 100644 --- a/qcsrc/server/mutators/mutator/gamemode_race.qc +++ b/qcsrc/server/mutators/mutator/gamemode_race.qc @@ -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) diff --git a/qcsrc/server/mutators/mutator/gamemode_tdm.qc b/qcsrc/server/mutators/mutator/gamemode_tdm.qc index aad319328..86ff94f53 100644 --- a/qcsrc/server/mutators/mutator/gamemode_tdm.qc +++ b/qcsrc/server/mutators/mutator/gamemode_tdm.qc @@ -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) diff --git a/qcsrc/server/scores_rules.qc b/qcsrc/server/scores_rules.qc index 17714e4c2..39dbd49a3 100644 --- a/qcsrc/server/scores_rules.qc +++ b/qcsrc/server/scores_rules.qc @@ -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, {}); } diff --git a/qcsrc/server/teamplay.qc b/qcsrc/server/teamplay.qc index aa2a44b29..3efab4cf5 100644 --- a/qcsrc/server/teamplay.qc +++ b/qcsrc/server/teamplay.qc @@ -15,9 +15,22 @@ #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); } diff --git a/qcsrc/server/teamplay.qh b/qcsrc/server/teamplay.qh index 71b73b240..e589a176f 100644 --- a/qcsrc/server/teamplay.qh +++ b/qcsrc/server/teamplay.qh @@ -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); -- 2.39.2