]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Merge branch 'master' into Lyberta/TeamplayOverhaul
authorLyberta <lyberta@lyberta.net>
Mon, 11 Jun 2018 10:15:27 +0000 (13:15 +0300)
committerLyberta <lyberta@lyberta.net>
Mon, 11 Jun 2018 10:15:27 +0000 (13:15 +0300)
1  2 
qcsrc/common/t_items.qc
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/g_world.qh
qcsrc/server/player.qc
qcsrc/server/player.qh
qcsrc/server/teamplay.qc

diff --combined qcsrc/common/t_items.qc
index 497235d2aed555235695aea5f75198f18208b79f,f4ed4f1bf423e83e62be4463a76e0ab9a72aaac6..320895b77059c39469f6dcd76086948f57e77a5c
@@@ -613,18 -613,14 +613,18 @@@ float adjust_respawntime(float normal_r
                return normal_respawntime;
        }
  
 -      CheckAllowedTeams(NULL);
 -      GetTeamCounts(NULL);
 +      entity balance = TeamBalance_CheckAllowedTeams(NULL);
 +      TeamBalance_GetTeamCounts(balance, NULL);
        int players = 0;
 -      if (c1 != -1) players += c1;
 -      if (c2 != -1) players += c2;
 -      if (c3 != -1) players += c3;
 -      if (c4 != -1) players += c4;
 -
 +      for (int i = 1; i <= NUM_TEAMS; ++i)
 +      {
 +              if (TeamBalance_IsTeamAllowed(balance, i))
 +              {
 +                      players += TeamBalance_GetNumberOfPlayers(balance, i);
 +              }
 +      }
 +      TeamBalance_Destroy(balance);
 +      
        if (players >= 2) {
                return normal_respawntime * (r / (players + o) + l);
        } else {
@@@ -1107,23 -1103,23 +1107,23 @@@ float ammo_pickupevalfunc(entity player
  
        float noammorating = 0.5;
  
-       if ((need_shells) && (item.ammo_shells) && (player.ammo_shells < g_pickup_shells_max))
-               c = item.ammo_shells / max(noammorating, player.ammo_shells);
+       if ((need_shells) && (item.ammo_shells) && (GetResourceAmount(player, RESOURCE_SHELLS) < g_pickup_shells_max))
+               c = item.ammo_shells / max(noammorating, GetResourceAmount(player, RESOURCE_SHELLS));
  
-       if ((need_nails) && (item.ammo_nails) && (player.ammo_nails < g_pickup_nails_max))
-               c = item.ammo_nails / max(noammorating, player.ammo_nails);
+       if ((need_nails) && (item.ammo_nails) && (GetResourceAmount(player, RESOURCE_BULLETS) < g_pickup_nails_max))
+               c = item.ammo_nails / max(noammorating, GetResourceAmount(player, RESOURCE_BULLETS));
  
-       if ((need_rockets) && (item.ammo_rockets) && (player.ammo_rockets < g_pickup_rockets_max))
-               c = item.ammo_rockets / max(noammorating, player.ammo_rockets);
+       if ((need_rockets) && (item.ammo_rockets) && (GetResourceAmount(player, RESOURCE_ROCKETS) < g_pickup_rockets_max))
+               c = item.ammo_rockets / max(noammorating, GetResourceAmount(player, RESOURCE_ROCKETS));
  
-       if ((need_cells) && (item.ammo_cells) && (player.ammo_cells < g_pickup_cells_max))
-               c = item.ammo_cells / max(noammorating, player.ammo_cells);
+       if ((need_cells) && (item.ammo_cells) && (GetResourceAmount(player, RESOURCE_CELLS) < g_pickup_cells_max))
+               c = item.ammo_cells / max(noammorating, GetResourceAmount(player, RESOURCE_CELLS));
  
-       if ((need_plasma) && (item.ammo_plasma) && (player.ammo_plasma < g_pickup_plasma_max))
-               c = item.ammo_plasma / max(noammorating, player.ammo_plasma);
+       if ((need_plasma) && (item.ammo_plasma) && (GetResourceAmount(player, RESOURCE_PLASMA) < g_pickup_plasma_max))
+               c = item.ammo_plasma / max(noammorating, GetResourceAmount(player, RESOURCE_PLASMA));
  
-       if ((need_fuel) && (item.ammo_fuel) && (player.ammo_fuel < g_pickup_fuel_max))
-               c = item.ammo_fuel / max(noammorating, player.ammo_fuel);
+       if ((need_fuel) && (item.ammo_fuel) && (GetResourceAmount(player, RESOURCE_FUEL) < g_pickup_fuel_max))
+               c = item.ammo_fuel / max(noammorating, GetResourceAmount(player, RESOURCE_FUEL));
  
        rating *= min(c, 2);
        if(wpn)
@@@ -1182,7 -1178,7 +1182,7 @@@ void _StartItem(entity this, entity def
      this.item_pickupsound_ent = pickupsound;
  
      if(def.m_iteminit)
-       def.m_iteminit(this);
+       def.m_iteminit(def, this);
  
        if(!this.respawntime) // both need to be set
        {
index b191e3103b4122cf74fd9bde788f0a85153926af,976d67ec996d3e28c67088d95d19798c584d037b..48543821097abeeb3371c125f395f630c03d28c7
@@@ -21,7 -21,7 +21,7 @@@
  #include "../../race.qh"
  #include <common/t_items.qh>
  
- #include "../../mutators/_mod.qh"
+ #include <server/mutators/_mod.qh>
  
  #include "../../weapons/accuracy.qh"
  
@@@ -430,15 -430,15 +430,15 @@@ void bot_clientconnect(entity this
        else if(this.bot_forced_team==4)
                this.team = NUM_TEAM_4;
        else
 -              JoinBestTeam(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;
  
                int thiscount = 0;
  
 -              switch(it.team)
 +              if (Team_IsValidTeam(it.team))
                {
 -                      case NUM_TEAM_1: thiscount = c1; break;
 -                      case NUM_TEAM_2: thiscount = c2; break;
 -                      case NUM_TEAM_3: thiscount = c3; break;
 -                      case NUM_TEAM_4: thiscount = c4; break;
 +                      thiscount = TeamBalance_GetNumberOfPlayers(balance,
 +                              Team_TeamToIndex(it.team));
                }
  
                if(thiscount > bestcount)
                        best = it;
                }
        });
 +      TeamBalance_Destroy(balance);
        if(!bcount)
                return; // no bots to remove
        currentbots = currentbots - 1;
diff --combined qcsrc/server/client.qc
index e4f4e7789ee97258740148a86bfc80beb3f4b2ff,003a29abae0696a392acd8a3626757a2624db246..b30a69f149156011312259e297f5477a82274abf
@@@ -291,8 -291,10 +291,8 @@@ void PutObserverInServer(entity this
        if (mutator_returnvalue) {
            // mutator prevents resetting teams+score
        } else {
 -              int oldteam = this.team;
 -              this.team = -1;  // move this as it is needed to log the player spectating in eventlog
 -              MUTATOR_CALLHOOK(Player_ChangedTeam, this, oldteam, this.team);
 -        this.frags = FRAGS_SPECTATOR;
 +              Player_SetTeamIndex(this, -1);
 +              this.frags = FRAGS_SPECTATOR;
          PlayerScore_Clear(this);  // clear scores when needed
      }
  
                        Send_Notification(NOTIF_ONE_ONLY, this, MSG_INFO, INFO_CHAT_NOSPECTATORS);
  
                if(!CS(this).just_joined)
 -                      LogTeamchange(this.playerid, -1, 4);
 +                      LogTeamchange(this.playerid, -1, TEAM_CHANGE_SPECTATOR);
                else
                        CS(this).just_joined = false;
        }
@@@ -523,7 -525,7 +523,7 @@@ void PutPlayerInServer(entity this
        accuracy_resend(this);
  
        if (this.team < 0)
 -              JoinBestTeam(this, true);
 +              TeamBalance_JoinBestTeam(this, true);
  
        entity spot = SelectSpawnPoint(this, false);
        if (!spot) {
        this.effects = EF_TELEPORT_BIT | EF_RESTARTANIM_BIT;
  
        if (warmup_stage) {
-               this.ammo_shells = warmup_start_ammo_shells;
-               this.ammo_nails = warmup_start_ammo_nails;
-               this.ammo_rockets = warmup_start_ammo_rockets;
-               this.ammo_cells = warmup_start_ammo_cells;
-               this.ammo_plasma = warmup_start_ammo_plasma;
-               this.ammo_fuel = warmup_start_ammo_fuel;
+               SetResourceAmount(this, RESOURCE_SHELLS, warmup_start_ammo_shells);
+               SetResourceAmount(this, RESOURCE_BULLETS, warmup_start_ammo_nails);
+               SetResourceAmount(this, RESOURCE_ROCKETS, warmup_start_ammo_rockets);
+               SetResourceAmount(this, RESOURCE_CELLS, warmup_start_ammo_cells);
+               SetResourceAmount(this, RESOURCE_PLASMA, warmup_start_ammo_plasma);
+               SetResourceAmount(this, RESOURCE_FUEL, warmup_start_ammo_fuel);
                this.health = warmup_start_health;
                this.armorvalue = warmup_start_armorvalue;
                STAT(WEAPONS, this) = WARMUP_START_WEAPONS;
        } else {
-               this.ammo_shells = start_ammo_shells;
-               this.ammo_nails = start_ammo_nails;
-               this.ammo_rockets = start_ammo_rockets;
-               this.ammo_cells = start_ammo_cells;
-               this.ammo_plasma = start_ammo_plasma;
-               this.ammo_fuel = start_ammo_fuel;
+               SetResourceAmount(this, RESOURCE_SHELLS, start_ammo_shells);
+               SetResourceAmount(this, RESOURCE_BULLETS, start_ammo_nails);
+               SetResourceAmount(this, RESOURCE_ROCKETS, start_ammo_rockets);
+               SetResourceAmount(this, RESOURCE_CELLS, start_ammo_cells);
+               SetResourceAmount(this, RESOURCE_PLASMA, start_ammo_plasma);
+               SetResourceAmount(this, RESOURCE_FUEL, start_ammo_fuel);
                this.health = start_health;
                this.armorvalue = start_armorvalue;
                STAT(WEAPONS, this) = start_weapons;
@@@ -910,7 -912,7 +910,7 @@@ void ClientKill_Now_TeamChange(entity t
  {
        if(this.killindicator_teamchange == -1)
        {
 -              JoinBestTeam( this, true );
 +              TeamBalance_JoinBestTeam(this, true);
        }
        else if(this.killindicator_teamchange == -2)
        {
@@@ -1168,76 -1170,6 +1168,76 @@@ void ClientPreConnect(entity this
  }
  #endif
  
 +string GetClientVersionMessage(entity this)
 +{
 +      if (CS(this).version_mismatch) {
 +              if(CS(this).version < autocvar_gameversion) {
 +                      return strcat("This is Xonotic ", autocvar_g_xonoticversion,
 +                              "\n^3Your client version is outdated.\n\n\n### YOU WON'T BE ABLE TO PLAY ON THIS SERVER ###\n\n\nPlease update!!!^8");
 +              } else {
 +                      return strcat("This is Xonotic ", autocvar_g_xonoticversion,
 +                              "\n^3This server is using an outdated Xonotic version.\n\n\n ### THIS SERVER IS INCOMPATIBLE AND THUS YOU CANNOT JOIN ###.^8");
 +              }
 +      } else {
 +              return strcat("Welcome to Xonotic ", autocvar_g_xonoticversion);
 +      }
 +}
 +
 +string getwelcomemessage(entity this)
 +{
 +      MUTATOR_CALLHOOK(BuildMutatorsPrettyString, "");
 +      string modifications = M_ARGV(0, string);
 +
 +      if(g_weaponarena)
 +      {
 +              if(g_weaponarena_random)
 +                      modifications = strcat(modifications, ", ", ftos(g_weaponarena_random), " of ", g_weaponarena_list, " Arena");
 +              else
 +                      modifications = strcat(modifications, ", ", g_weaponarena_list, " Arena");
 +      }
 +      else if(cvar("g_balance_blaster_weaponstartoverride") == 0)
 +              modifications = strcat(modifications, ", No start weapons");
 +      if(cvar("sv_gravity") < stof(cvar_defstring("sv_gravity")))
 +              modifications = strcat(modifications, ", Low gravity");
 +      if(g_weapon_stay && !g_cts)
 +              modifications = strcat(modifications, ", Weapons stay");
 +      if(g_jetpack)
 +              modifications = strcat(modifications, ", Jet pack");
 +      if(autocvar_g_powerups == 0)
 +              modifications = strcat(modifications, ", No powerups");
 +      if(autocvar_g_powerups > 0)
 +              modifications = strcat(modifications, ", Powerups");
 +      modifications = substring(modifications, 2, strlen(modifications) - 2);
 +
 +      string versionmessage = GetClientVersionMessage(this);
 +      string s = strcat(versionmessage, "^8\n^8\nmatch type is ^1", gamemode_name, "^8\n");
 +
 +      if(modifications != "")
 +              s = strcat(s, "^8\nactive modifications: ^3", modifications, "^8\n");
 +
 +      if(cache_lastmutatormsg != autocvar_g_mutatormsg)
 +      {
 +              strcpy(cache_lastmutatormsg, autocvar_g_mutatormsg);
 +              strcpy(cache_mutatormsg, cache_lastmutatormsg);
 +      }
 +
 +      if (cache_mutatormsg != "") {
 +              s = strcat(s, "\n\n^8special gameplay tips: ^7", cache_mutatormsg);
 +      }
 +
 +      string mutator_msg = "";
 +      MUTATOR_CALLHOOK(BuildGameplayTipsString, mutator_msg);
 +      mutator_msg = M_ARGV(0, string);
 +
 +      s = strcat(s, mutator_msg); // trust that the mutator will do proper formatting
 +
 +      string motd = autocvar_sv_motd;
 +      if (motd != "") {
 +              s = strcat(s, "\n\n^8MOTD: ^7", strreplace("\\n", "\n", motd));
 +      }
 +      return s;
 +}
 +
  /**
  =============
  ClientConnect
@@@ -1293,7 -1225,7 +1293,7 @@@ void ClientConnect(entity this
  
        int playerid_save = this.playerid;
        this.playerid = 0; // silent
 -      JoinBestTeam(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) {
        if (autocvar_sv_eventlog)
                GameLogEcho(strcat(":join:", ftos(this.playerid), ":", ftos(etof(this)), ":", ((IS_REAL_CLIENT(this)) ? this.netaddress : "bot"), ":", playername(this, false)));
  
 -      LogTeamchange(this.playerid, this.team, 1);
 +      LogTeamchange(this.playerid, this.team, TEAM_CHANGE_CONNECT);
  
        CS(this).just_joined = true;  // stop spamming the eventlog with additional lines when the client connects
  
        // notify about available teams
        if (teamplay)
        {
 -              CheckAllowedTeams(this);
 -              int t = 0;
 -              if (c1 >= 0) t |= BIT(0);
 -              if (c2 >= 0) t |= BIT(1);
 -              if (c3 >= 0) t |= BIT(2);
 -              if (c4 >= 0) t |= BIT(3);
 +              entity balance = TeamBalance_CheckAllowedTeams(this);
 +              int t = TeamBalance_GetAllowedTeams(balance);
 +              TeamBalance_Destroy(balance);
                stuffcmd(this, sprintf("set _teams_available %d\n", t));
        }
        else
@@@ -1543,6 -1478,54 +1543,54 @@@ void respawn(entity this
        PutClientInServer(this);
  }
  
+ void PrintToChat(entity client, string text)
+ {
+       text = strcat("\{1}^7", text, "\n");
+       sprint(client, text);
+ }
+ void DebugPrintToChat(entity client, string text)
+ {
+       if (autocvar_developer)
+       {
+               PrintToChat(client, text);
+       }
+ }
+ void PrintToChatAll(string text)
+ {
+       text = strcat("\{1}^7", text, "\n");
+       bprint(text);
+ }
+ void DebugPrintToChatAll(string text)
+ {
+       if (autocvar_developer)
+       {
+               PrintToChatAll(text);
+       }
+ }
+ void PrintToChatTeam(int team_num, string text)
+ {
+       text = strcat("\{1}^7", text, "\n");
+       FOREACH_CLIENT(IS_REAL_CLIENT(it),
+       {
+               if (it.team == team_num)
+               {
+                       sprint(it, text);
+               }
+       });
+ }
+ void DebugPrintToChatTeam(int team_num, string text)
+ {
+       if (autocvar_developer)
+       {
+               PrintToChatTeam(team_num, text);
+       }
+ }
  void play_countdown(entity this, float finished, Sound samp)
  {
      TC(Sound, samp);
@@@ -1837,7 -1820,7 +1885,7 @@@ void SpectateCopy(entity this, entity s
        PS(this) = PS(spectatee);
        this.armortype = spectatee.armortype;
        this.armorvalue = spectatee.armorvalue;
-       this.ammo_cells = spectatee.ammo_cells;
+       this.ammo_cells = spectatee.ammo_cells; // TODO: these will be a part of inventory, so no need to worry about setting them later!
        this.ammo_plasma = spectatee.ammo_plasma;
        this.ammo_shells = spectatee.ammo_shells;
        this.ammo_nails = spectatee.ammo_nails;
@@@ -2084,7 -2067,7 +2132,7 @@@ void Join(entity this
  
        if(!this.team_selected)
        if(autocvar_g_campaign || autocvar_g_balance_teams)
 -              JoinBestTeam(this, true);
 +              TeamBalance_JoinBestTeam(this, true);
  
        if(autocvar_g_campaign)
                campaign_bots_may_start = true;
  
        if(IS_PLAYER(this))
        if(teamplay && this.team != -1)
 -              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_JOIN_PLAY_TEAM), this.netname);
 +      {
 +              //Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_JOIN_PLAY_TEAM), this.netname);
 +      }
        else
                Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_JOIN_PLAY, this.netname);
        this.team_selected = false;
index ca48f906dc9e1b5f6ef6a8853482ad6c4b8e25df,8111a64ef89214cd56cf5a6780255eed2c6aa9d5..68273dddd16714df19b70ff6f64bc60ccac6689d
@@@ -16,7 -16,7 +16,7 @@@
  #include "../scores.qh"
  #include "../teamplay.qh"
  
- #include "../mutators/_mod.qh"
+ #include <server/mutators/_mod.qh>
  #include <common/gamemodes/_mod.qh>
  
  #ifdef SVQC
@@@ -397,16 -397,13 +397,16 @@@ void ClientCommand_selectteam(entity ca
                        if ((selection != -1) && autocvar_g_balance_teams &&
                                autocvar_g_balance_teams_prevent_imbalance)
                        {
 -                              CheckAllowedTeams(caller);
 -                              GetTeamCounts(caller);
 -                              if ((BIT(Team_TeamToNumber(selection) - 1) & FindBestTeams(caller, false)) == 0)
 +                              entity balance = TeamBalance_CheckAllowedTeams(caller);
 +                              TeamBalance_GetTeamCounts(balance, caller);
 +                              if ((Team_IndexToBit(Team_TeamToIndex(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 de289137165d48dc495e3a9fb8a6f61f778afd56,0471cff0721aa4f61d6ad165d497d152d24e3419..103ca4e34f558ffdd81898da0a66bc037e69cd8b
@@@ -20,7 -20,7 +20,7 @@@
  
  #include "../bot/api.qh"
  
- #include "../mutators/_mod.qh"
+ #include <server/mutators/_mod.qh>
  #include <common/gamemodes/_mod.qh>
  
  #include <common/constants.qh>
@@@ -1065,7 -1065,6 +1065,7 @@@ void GameCommand_moveplayer(float reque
  
                                                                // 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
                                                                }
                                                                else if (team_id == 0)  // auto team
                                                                {
 -                                                                      CheckAllowedTeams(client);
 -                                                                      team_id = Team_NumberToTeam(FindSmallestTeam(client, false));
 +                                                                      balance = TeamBalance_CheckAllowedTeams(client);
 +                                                                      team_id = Team_IndexToTeam(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 (c1 == -1) { LOG_INFO("Sorry, can't move player to red team if it doesn't exist."); return; } break;
 -                                                                      case NUM_TEAM_2: if (c2 == -1) { LOG_INFO("Sorry, can't move player to blue team if it doesn't exist."); return; } break;
 -                                                                      case NUM_TEAM_3: if (c3 == -1) { LOG_INFO("Sorry, can't move player to yellow team if it doesn't exist."); return; } break;
 -                                                                      case NUM_TEAM_4: if (c4 == -1) { 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
                                                                client.team_forced = 0;
 -                                                              if (MoveToTeam(client, team_id, 6))
 +                                                              if (MoveToTeam(client, Team_TeamToIndex(team_id), 6))
                                                                {
                                                                        successful = strcat(successful, (successful ? ", " : ""), playername(client, false));
                                                                        LOG_INFO("Player ", ftos(GetFilteredNumber(t)), " (", playername(client, false), ") has been moved to the ", Team_ColoredFullName(team_id), "^7.");
@@@ -1410,23 -1367,16 +1410,23 @@@ void GameCommand_shuffleteams(float req
                        });
  
                        int number_of_teams = 0;
 -                      CheckAllowedTeams(NULL);
 -                      if (c1 >= 0) number_of_teams = max(1, number_of_teams);
 -                      if (c2 >= 0) number_of_teams = max(2, number_of_teams);
 -                      if (c3 >= 0) number_of_teams = max(3, number_of_teams);
 -                      if (c4 >= 0) number_of_teams = max(4, number_of_teams);
 +                      entity balance = TeamBalance_CheckAllowedTeams(NULL);
 +                      for (int i = 1; i <= NUM_TEAMS; ++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, {
 -                              int target_team_number = Team_NumberToTeam(team_index + 1);
 -                              if (it.team != target_team_number) MoveToTeam(it, target_team_number, 6);
 +                              int target_team_index = team_index + 1;
 +                              if (Entity_GetTeamIndex(it) != target_team_index)
 +                              {
 +                                      MoveToTeam(it, target_team_index, 6);
 +                              }
                                team_index = (team_index + 1) % number_of_teams;
                        });
  
diff --combined qcsrc/server/g_world.qc
index b063981ee282d7840ed7e2d246abd97d4a4e50a9,842f5b53c71c4887bb0a50d82cbdc568c2cb2876..df0c1d2cb94539ef3ec61309c3a869c83f2b7aea
@@@ -13,7 -13,7 +13,7 @@@
  #include "g_hook.qh"
  #include "ipban.qh"
  #include "mapvoting.qh"
- #include "mutators/_mod.qh"
+ #include <server/mutators/_mod.qh>
  #include "race.qh"
  #include "scores.qh"
  #include "teamplay.qh"
@@@ -21,6 -21,7 +21,7 @@@
  #include "../common/constants.qh"
  #include <common/net_linked.qh>
  #include "../common/deathtypes/all.qh"
+ #include "../common/gamemodes/sv_rules.qh"
  #include "../common/mapinfo.qh"
  #include "../common/monsters/_mod.qh"
  #include "../common/monsters/sv_monsters.qh"
@@@ -91,7 -92,6 +92,6 @@@ const float SPAWNFLAG_NO_WAYPOINTS_FOR_
  string redirection_target;
  float world_initialized;
  
- string GetGametype();
  void ShuffleMaplist();
  
  void SetDefaultAlpha()
@@@ -578,57 -578,6 +578,57 @@@ STATIC_INIT_EARLY(maxclients
        }
  }
  
 +void default_delayedinit(entity this)
 +{
 +      if(!scores_initialized)
 +              ScoreRules_generic();
 +}
 +
 +void InitGameplayMode()
 +{
 +      VoteReset();
 +
 +      // find out good world mins/maxs bounds, either the static bounds found by looking for solid, or the mapinfo specified bounds
 +      get_mi_min_max(1);
 +      // assign reflectively to avoid "assignment to world" warning
 +      int done = 0; for (int i = 0, n = numentityfields(); i < n; ++i) {
 +          string k = entityfieldname(i); vector v = (k == "mins") ? mi_min : (k == "maxs") ? mi_max : '0 0 0';
 +          if (v) {
 +            putentityfieldstring(i, world, sprintf("%v", v));
 +            if (++done == 2) break;
 +        }
 +      }
 +      // currently, NetRadiant's limit is 131072 qu for each side
 +      // distance from one corner of a 131072qu cube to the opposite corner is approx. 227023 qu
 +      // set the distance according to map size but don't go over the limit to avoid issues with float precision
 +      // in case somebody makes extremely large maps
 +      max_shot_distance = min(230000, vlen(world.maxs - world.mins));
 +
 +      MapInfo_LoadMapSettings(mapname);
 +      GameRules_teams(false);
 +
 +      if (!cvar_value_issafe(world.fog))
 +      {
 +              LOG_INFO("The current map contains a potentially harmful fog setting, ignored");
 +              world.fog = string_null;
 +      }
 +      if(MapInfo_Map_fog != "")
 +              if(MapInfo_Map_fog == "none")
 +                      world.fog = string_null;
 +              else
 +                      world.fog = strzone(MapInfo_Map_fog);
 +      clientstuff = strzone(MapInfo_Map_clientstuff);
 +
 +      MapInfo_ClearTemps();
 +
 +      gamemode_name = MapInfo_Type_ToText(MapInfo_LoadedGametype);
 +
 +      cache_mutatormsg = strzone("");
 +      cache_lastmutatormsg = strzone("");
 +
 +      InitializeEntity(NULL, default_delayedinit, INITPRIO_GAMETYPE_FALLBACK);
 +}
 +
  void Map_MarkAsRecent(string m);
  float world_already_spawned;
  void Nagger_Init();
@@@ -1702,11 -1651,10 +1702,11 @@@ float WinningCondition_Scores(float lim
  
        if(teamplay)
        {
 -              team1_score = TeamScore_GetCompareValue(NUM_TEAM_1);
 -              team2_score = TeamScore_GetCompareValue(NUM_TEAM_2);
 -              team3_score = TeamScore_GetCompareValue(NUM_TEAM_3);
 -              team4_score = TeamScore_GetCompareValue(NUM_TEAM_4);
 +              for (int i = 1; i < 5; ++i)
 +              {
 +                      Team_SetTeamScore(Team_GetTeamFromIndex(i),
 +                              TeamScore_GetCompareValue(Team_IndexToTeam(i)));
 +              }
        }
  
        ClearWinners();
@@@ -1776,32 -1724,30 +1776,32 @@@ float WinningCondition_RanOutOfSpawns(
        if(!some_spawn_has_been_used)
                return WINNING_NO;
  
 -      team1_score = team2_score = team3_score = team4_score = 0;
 -
 -      FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it), {
 -              switch(it.team)
 +      for (int i = 1; i < 5; ++i)
 +      {
 +              Team_SetTeamScore(Team_GetTeamFromIndex(i), 0);
 +      }
 +      
 +      FOREACH_CLIENT(IS_PLAYER(it) && !IS_DEAD(it),
 +      {
 +              if (Team_IsValidTeam(it.team))
                {
 -                      case NUM_TEAM_1: team1_score = 1; break;
 -                      case NUM_TEAM_2: team2_score = 1; break;
 -                      case NUM_TEAM_3: team3_score = 1; break;
 -                      case NUM_TEAM_4: team4_score = 1; break;
 +                      Team_SetTeamScore(Team_GetTeam(it.team), 1);
                }
        });
  
        IL_EACH(g_spawnpoints, true,
        {
 -              switch(it.team)
 +              if (Team_IsValidTeam(it.team))
                {
 -                      case NUM_TEAM_1: team1_score = 1; break;
 -                      case NUM_TEAM_2: team2_score = 1; break;
 -                      case NUM_TEAM_3: team3_score = 1; break;
 -                      case NUM_TEAM_4: team4_score = 1; break;
 +                      Team_SetTeamScore(Team_GetTeam(it.team), 1);
                }
        });
  
        ClearWinners();
 +      float team1_score = Team_GetTeamScore(Team_GetTeamFromIndex(1));
 +      float team2_score = Team_GetTeamScore(Team_GetTeamFromIndex(2));
 +      float team3_score = Team_GetTeamScore(Team_GetTeamFromIndex(3));
 +      float team4_score = Team_GetTeamScore(Team_GetTeamFromIndex(4));
        if(team1_score + team2_score + team3_score + team4_score == 0)
        {
                checkrules_equality = true;
        {
                float t, i;
                if(team1_score)
 -                      t = NUM_TEAM_1;
 +                      t = 1;
                else if(team2_score)
 -                      t = NUM_TEAM_2;
 +                      t = 2;
                else if(team3_score)
 -                      t = NUM_TEAM_3;
 +                      t = 3;
                else // if(team4_score)
 -                      t = NUM_TEAM_4;
 -              CheckAllowedTeams(NULL);
 +                      t = 4;
 +              entity balance = TeamBalance_CheckAllowedTeams(NULL);
                for(i = 0; i < MAX_TEAMSCORE; ++i)
                {
 -                      if(t != NUM_TEAM_1) if(c1 >= 0) TeamScore_AddToTeam(NUM_TEAM_1, i, -1000);
 -                      if(t != NUM_TEAM_2) if(c2 >= 0) TeamScore_AddToTeam(NUM_TEAM_2, i, -1000);
 -                      if(t != NUM_TEAM_3) if(c3 >= 0) TeamScore_AddToTeam(NUM_TEAM_3, i, -1000);
 -                      if(t != NUM_TEAM_4) if(c4 >= 0) TeamScore_AddToTeam(NUM_TEAM_4, i, -1000);
 +                      for (int j = 1; j <= NUM_TEAMS; ++j)
 +                      {
 +                              if (t == j)
 +                              {
 +                                      continue;
 +                              }
 +                              if (!TeamBalance_IsTeamAllowed(balance, j))
 +                              {
 +                                      continue;
 +                              }
 +                              TeamScore_AddToTeam(Team_IndexToTeam(j), i, -1000);
 +                      }
                }
  
                AddWinners(team, t);
diff --combined qcsrc/server/g_world.qh
index 1633d419a1d5f12bc578fe47cba0c7fb48bfe3ea,35ea5fe7da17cb73a4af79baddc1a4580d73b196..531987997362741908c94a674d79926dab00c304
@@@ -5,9 -5,6 +5,9 @@@ float checkrules_suddendeathwarning
  float checkrules_suddendeathend;
  float checkrules_overtimesadded; //how many overtimes have been already added
  
 +string cache_mutatormsg;
 +string cache_lastmutatormsg;
 +
  const int WINNING_NO = 0; // no winner, but time limits may terminate the game
  const int WINNING_YES = 1; // winner found
  const int WINNING_NEVER = 2; // no winner, enter overtime if time limit is reached
@@@ -19,6 -16,8 +19,8 @@@ void IntermissionThink(entity this)
  void GotoNextMap(float reinit);
  void ReadyRestart();
  
+ string GetGametype();
  void DumpStats(float final);
  float Map_IsRecent(string m);
  string GetNextMap();
diff --combined qcsrc/server/player.qc
index 693a9d29501508282412729f0abd74e7f4824cf9,4d59943b682dfb89a81d68e274100e2b1f1d0a86..03e0bfb64f6d6a80d2977ae34c31bf0bae6624af
@@@ -14,6 -14,7 +14,7 @@@
  #include "../common/anim.qh"
  #include "../common/animdecide.qh"
  #include "../common/csqcmodel_settings.qh"
+ #include "../common/gamemodes/sv_rules.qh"
  #include "../common/deathtypes/all.qh"
  #include "../common/mapobjects/subs.qh"
  #include "../common/playerstats.qh"
@@@ -663,60 -664,21 +664,6 @@@ void PlayerDamage(entity this, entity i
        }
  }
  
- /** print(), but only print if the server is not local */
- void dedicated_print(string input)
- {
-       if (server_is_dedicated) print(input);
- }
- void PrintToChat(entity player, string text)
- {
-       text = strcat("\{1}^7", text, "\n");
-       sprint(player, text);
- }
- void DebugPrintToChat(entity player, string text)
- {
-       if (autocvar_developer)
-       {
-               PrintToChat(player, text);
-       }
- }
- void PrintToChatAll(string text)
- {
-       text = strcat("\{1}^7", text, "\n");
-       bprint(text);
- }
- void DebugPrintToChatAll(string text)
- {
-       if (autocvar_developer)
-       {
-               PrintToChatAll(text);
-       }
- }
- void PrintToChatTeam(int teamnum, string text)
- {
-       text = strcat("\{1}^7", text, "\n");
-       FOREACH_CLIENT(IS_REAL_CLIENT(it),
-       {
-               if (it.team == teamnum)
-               {
-                       sprint(it, text);
-               }
-       });
- }
- void DebugPrintToChatTeam(int teamnum, string text)
 -bool MoveToTeam(entity client, int team_colour, int type)
--{
-       if (autocvar_developer)
 -      int lockteams_backup = lockteams;  // backup any team lock
 -      lockteams = 0;  // disable locked teams
 -      TeamchangeFrags(client);  // move the players frags
 -      if (!SetPlayerTeamSimple(client, team_colour))
--      {
-               PrintToChatTeam(teamnum, text);
 -              return false;
--      }
 -      Damage(client, client, client, 100000, DEATH_AUTOTEAMCHANGE.m_id, DMG_NOWEP, client.origin, '0 0 0');  // kill the player
 -      lockteams = lockteams_backup;  // restore the team lock
 -      LogTeamchange(client.playerid, client.team, type);
 -      return true;
--}
--
  /**
   * message "": do not say, just test flood control
   * return value:
diff --combined qcsrc/server/player.qh
index f462a1a52febb157ccb50aff5209f2d56fe10c02,5e6642e0471a78220a1af579a705e7b50d74b76b..8d6bc51ae1b3d07d4490a62945c67bdf4424440f
@@@ -9,45 -9,6 +9,6 @@@
  void CopyBody_Think(entity this);
  void CopyBody(entity this, float keepvelocity);
  
- void dedicated_print(string input);
- /// \brief Print the string to player's chat.
- /// \param[in] player Player to print to.
- /// \param[in] text Text to print.
- /// \return No return.
- void PrintToChat(entity player, string text);
- /// \brief Print the string to player's chat if the server cvar "developer" is
- /// not 0.
- /// \param[in] player Player to print to.
- /// \param[in] text Text to print.
- /// \return No return.
- void DebugPrintToChat(entity player, string text);
- /// \brief Prints the string to all players' chat.
- /// \param[in] text Text to print.
- /// \return No return.
- void PrintToChatAll(string text);
- /// \brief Prints the string to all players' chat if the server cvar "developer"
- /// is not 0.
- /// \param[in] text Text to print.
- /// \return No return.
- void DebugPrintToChatAll(string text);
- /// \brief Print the string to chat of all players of the specified team.
- /// \param[in] teamnum Team to print to. See NUM_TEAM constants.
- /// \param[in] text Text to print.
- /// \return No return.
- void PrintToChatTeam(int teamnum, string text);
- /// \brief Print the string to chat of all players of the specified team if the
- /// server cvar "developer" is not 0.
- /// \param[in] teamnum Team to print to. See NUM_TEAM constants.
- /// \param[in] text Text to print.
- /// \return No return.
- void DebugPrintToChatTeam(int teamnum, string text);
  void player_setupanimsformodel(entity this);
  
  void player_anim(entity this);
@@@ -69,6 -30,13 +30,6 @@@ void calculate_player_respawn_time(enti
  
  void ClientKill_Now_TeamChange(entity this);
  
 -/// \brief Moves player to the specified team.
 -/// \param[in,out] client Client to move.
 -/// \param[in] team_colour Color of the team.
 -/// \param[in] type ???
 -/// \return True on success, false otherwise.
 -bool MoveToTeam(entity client, float team_colour, float type);
 -
  void PlayerDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force);
  
  int Say(entity source, float teamsay, entity privatesay, string msgin, float floodcontrol);
diff --combined qcsrc/server/teamplay.qc
index 22ea235463b949c92ce7cb109abc2afd437bd2d2,d9bab5d7f894cd1346f60c507becb6d4009586eb..3f125131043a8c341e59a0280ebd494b661e2be8
  
  #include "command/vote.qh"
  
- #include "mutators/_mod.qh"
+ #include <server/mutators/_mod.qh>
  
  #include "../common/deathtypes/all.qh"
  #include <common/gamemodes/_mod.qh>
  #include "../common/teams.qh"
  
 -void TeamchangeFrags(entity e)
 +/// \brief Describes a state of team balance entity.
 +enum
  {
 -      PlayerScore_Clear(e);
 -}
 +      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
 +};
  
 -void LogTeamchange(float player_id, float team_number, float type)
 -{
 -      if(!autocvar_sv_eventlog)
 -              return;
 +/// \brief Indicates that the player is not allowed to join a team.
 +const int TEAM_NOT_ALLOWED = -1;
  
 -      if(player_id < 1)
 -              return;
 +.int m_team_balance_state; ///< Holds the state of the team balance entity.
 +.entity m_team_balance_team[NUM_TEAMS]; ///< ???
  
 -      GameLogEcho(strcat(":team:", ftos(player_id), ":", ftos(team_number), ":", ftos(type)));
 -}
 +.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.
 +.int m_num_players_alive; ///< Number of alive players in a team.
 +.int m_num_control_points; ///< Number of control points owned by a team.
 +
 +entity g_team_entities[NUM_TEAMS]; ///< Holds global team entities.
  
 -void default_delayedinit(entity this)
 +STATIC_INIT(g_team_entities)
  {
 -      if(!scores_initialized)
 -              ScoreRules_generic();
 +      for (int i = 0; i < NUM_TEAMS; ++i)
 +      {
 +              g_team_entities[i] = spawn();
 +      }
  }
  
 -void InitGameplayMode()
 +entity Team_GetTeamFromIndex(int index)
  {
 -      VoteReset();
 -
 -      // find out good world mins/maxs bounds, either the static bounds found by looking for solid, or the mapinfo specified bounds
 -      get_mi_min_max(1);
 -      // assign reflectively to avoid "assignment to world" warning
 -      int done = 0; for (int i = 0, n = numentityfields(); i < n; ++i) {
 -          string k = entityfieldname(i); vector v = (k == "mins") ? mi_min : (k == "maxs") ? mi_max : '0 0 0';
 -          if (v) {
 -            putentityfieldstring(i, world, sprintf("%v", v));
 -            if (++done == 2) break;
 -        }
 -      }
 -      // currently, NetRadiant's limit is 131072 qu for each side
 -      // distance from one corner of a 131072qu cube to the opposite corner is approx. 227023 qu
 -      // set the distance according to map size but don't go over the limit to avoid issues with float precision
 -      // in case somebody makes extremely large maps
 -      max_shot_distance = min(230000, vlen(world.maxs - world.mins));
 -
 -      MapInfo_LoadMapSettings(mapname);
 -      GameRules_teams(false);
 -
 -      if (!cvar_value_issafe(world.fog))
 -      {
 -              LOG_INFO("The current map contains a potentially harmful fog setting, ignored");
 -              world.fog = string_null;
 -      }
 -      if(MapInfo_Map_fog != "")
 -              if(MapInfo_Map_fog == "none")
 -                      world.fog = string_null;
 -              else
 -                      world.fog = strzone(MapInfo_Map_fog);
 -      clientstuff = strzone(MapInfo_Map_clientstuff);
 -
 -      MapInfo_ClearTemps();
 -
 -      gamemode_name = MapInfo_Type_ToText(MapInfo_LoadedGametype);
 -
 -      cache_mutatormsg = strzone("");
 -      cache_lastmutatormsg = strzone("");
 -
 -      InitializeEntity(NULL, default_delayedinit, INITPRIO_GAMETYPE_FALLBACK);
 +      if (!Team_IsValidIndex(index))
 +      {
 +              LOG_FATALF("Team_GetTeamFromIndex: Index is invalid: %f", index);
 +      }
 +      return g_team_entities[index - 1];
  }
  
 -string GetClientVersionMessage(entity this)
 +entity Team_GetTeam(int team_num)
  {
 -      if (CS(this).version_mismatch) {
 -              if(CS(this).version < autocvar_gameversion) {
 -                      return strcat("This is Xonotic ", autocvar_g_xonoticversion,
 -                              "\n^3Your client version is outdated.\n\n\n### YOU WON'T BE ABLE TO PLAY ON THIS SERVER ###\n\n\nPlease update!!!^8");
 -              } else {
 -                      return strcat("This is Xonotic ", autocvar_g_xonoticversion,
 -                              "\n^3This server is using an outdated Xonotic version.\n\n\n ### THIS SERVER IS INCOMPATIBLE AND THUS YOU CANNOT JOIN ###.^8");
 -              }
 -      } else {
 -              return strcat("Welcome to Xonotic ", autocvar_g_xonoticversion);
 +      if (!Team_IsValidTeam(team_num))
 +      {
 +              LOG_FATALF("Team_GetTeam: Value is invalid: %f", team_num);
        }
 +      return g_team_entities[Team_TeamToIndex(team_num) - 1];
  }
  
 -string getwelcomemessage(entity this)
 +float Team_GetTeamScore(entity team_ent)
  {
 -      MUTATOR_CALLHOOK(BuildMutatorsPrettyString, "");
 -      string modifications = M_ARGV(0, string);
 +      return team_ent.m_team_score;
 +}
  
 -      if(g_weaponarena)
 -      {
 -              if(g_weaponarena_random)
 -                      modifications = strcat(modifications, ", ", ftos(g_weaponarena_random), " of ", g_weaponarena_list, " Arena"); // TODO: somehow get this into the mutator
 -              else
 -                      modifications = strcat(modifications, ", ", g_weaponarena_list, " Arena");
 -      }
 -      else if(cvar("g_balance_blaster_weaponstartoverride") == 0)
 -              modifications = strcat(modifications, ", No start weapons");
 -      if(cvar("sv_gravity") < stof(cvar_defstring("sv_gravity")))
 -              modifications = strcat(modifications, ", Low gravity");
 -      if(g_weapon_stay && !g_cts)
 -              modifications = strcat(modifications, ", Weapons stay");
 -      if(g_jetpack)
 -              modifications = strcat(modifications, ", Jet pack");
 -      if(autocvar_g_powerups == 0)
 -              modifications = strcat(modifications, ", No powerups");
 -      if(autocvar_g_powerups > 0)
 -              modifications = strcat(modifications, ", Powerups");
 -      modifications = substring(modifications, 2, strlen(modifications) - 2);
 +void Team_SetTeamScore(entity team_ent, float score)
 +{
 +      team_ent.m_team_score = score;
 +}
  
 -      string versionmessage = GetClientVersionMessage(this);
 -      string s = strcat(versionmessage, "^8\n^8\nmatch type is ^1", gamemode_name, "^8\n");
 +int Team_GetNumberOfAlivePlayers(entity team_ent)
 +{
 +      return team_ent.m_num_players_alive;
 +}
  
 -      if(modifications != "")
 -              s = strcat(s, "^8\nactive modifications: ^3", modifications, "^8\n");
 +void Team_SetNumberOfAlivePlayers(entity team_ent, int number)
 +{
 +      team_ent.m_num_players_alive = number;
 +}
  
 -      if(cache_lastmutatormsg != autocvar_g_mutatormsg)
 +int Team_GetNumberOfAliveTeams()
 +{
 +      int result = 0;
 +      for (int i = 0; i < NUM_TEAMS; ++i)
        {
 -              strcpy(cache_lastmutatormsg, autocvar_g_mutatormsg);
 -              strcpy(cache_mutatormsg, cache_lastmutatormsg);
 -      }
 -
 -      if (cache_mutatormsg != "") {
 -              s = strcat(s, "\n\n^8special gameplay tips: ^7", cache_mutatormsg);
 +              if (g_team_entities[i].m_num_players_alive > 0)
 +              {
 +                      ++result;
 +              }
        }
 +      return result;
 +}
  
 -      string mutator_msg = "";
 -      MUTATOR_CALLHOOK(BuildGameplayTipsString, mutator_msg);
 -      mutator_msg = M_ARGV(0, string);
 +int Team_GetNumberOfControlPoints(entity team_ent)
 +{
 +      return team_ent.m_num_control_points;
 +}
  
 -      s = strcat(s, mutator_msg); // trust that the mutator will do proper formatting
 +void Team_SetNumberOfControlPoints(entity team_ent, int number)
 +{
 +      team_ent.m_num_control_points = number;
 +}
  
 -      string motd = autocvar_sv_motd;
 -      if (motd != "") {
 -              s = strcat(s, "\n\n^8MOTD: ^7", strreplace("\\n", "\n", motd));
 +int Team_GetNumberOfTeamsWithControlPoints()
 +{
 +      int result = 0;
 +      for (int i = 0; i < NUM_TEAMS; ++i)
 +      {
 +              if (g_team_entities[i].m_num_control_points > 0)
 +              {
 +                      ++result;
 +              }
        }
 -      return s;
 +      return result;
  }
  
  void setcolor(entity this, int clr)
  #endif
  }
  
 +bool Entity_HasValidTeam(entity this)
 +{
 +      return Team_IsValidTeam(this.team);
 +}
 +
 +int Entity_GetTeamIndex(entity this)
 +{
 +      return Team_TeamToIndex(this.team);
 +}
 +
 +entity Entity_GetTeam(entity this)
 +{
 +      int index = Entity_GetTeamIndex(this);
 +      if (!Team_IsValidIndex(index))
 +      {
 +              return NULL;
 +      }
 +      return Team_GetTeamFromIndex(index);
 +}
 +
  void SetPlayerColors(entity player, float _color)
  {
        float pants = _color & 0x0F;
        }
  }
  
 -void KillPlayerForTeamChange(entity player)
 +bool Player_SetTeamIndex(entity player, int index)
  {
 -      if (IS_DEAD(player))
 +      int new_team = Team_IndexToTeam(index);
 +      if (player.team == new_team)
        {
 -              return;
 +              if (new_team != -1)
 +              {
 +                      // This is important when players join the game and one of their
 +                      // color matches the team color while other doesn't. For example
 +                      // [BOT]Lion.
 +                      SetPlayerColors(player, new_team - 1);
 +              }
 +              return true;
        }
 -      if (MUTATOR_CALLHOOK(Player_ChangeTeamKill, player) == true)
 +      int old_index = Team_TeamToIndex(player.team);
 +      if (MUTATOR_CALLHOOK(Player_ChangeTeam, player, old_index, index) == true)
        {
 -              return;
 +              // Mutator has blocked team change.
 +              return false;
        }
 -      Damage(player, player, player, 100000, DEATH_TEAMCHANGE.m_id, DMG_NOWEP, player.origin,
 -              '0 0 0');
 +      if (new_team != -1)
 +      {
 +              SetPlayerColors(player, new_team - 1);
 +      }
 +      MUTATOR_CALLHOOK(Player_ChangedTeam, player, old_index, index);
 +      return true;
  }
  
 -bool SetPlayerTeamSimple(entity player, int team_num)
 +bool SetPlayerTeam(entity player, int team_index, int type)
  {
 -      if (player.team == team_num)
 +      int old_team_index = Entity_GetTeamIndex(player);
 +      if (!Player_SetTeamIndex(player, team_index))
        {
 -              // This is important when players join the game and one of their color
 -              // matches the team color while other doesn't. For example [BOT]Lion.
 -              SetPlayerColors(player, team_num - 1);
 -              return true;
 +              return false;
        }
 -      if (MUTATOR_CALLHOOK(Player_ChangeTeam, player, Team_TeamToNumber(
 -              player.team), Team_TeamToNumber(team_num)) == true)
 +      LogTeamchange(player.playerid, player.team, type);
 +      if (team_index != old_team_index)
        {
 -              // Mutator has blocked team change.
 -              return false;
 +              Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(player.team,
 +                      INFO_JOIN_PLAY_TEAM), player.netname);
 +              KillPlayerForTeamChange(player);
        }
 -      int old_team = player.team;
 -      SetPlayerColors(player, team_num - 1);
 -      MUTATOR_CALLHOOK(Player_ChangedTeam, player, old_team, player.team);
        return true;
  }
  
 -bool SetPlayerTeam(entity player, int destination_team, int source_team,
 -      bool no_print)
 +bool MoveToTeam(entity client, int team_index, int type)
  {
 -      int team_num = Team_NumberToTeam(destination_team);
 -      if (!SetPlayerTeamSimple(player, team_num))
 +      //PrintToChatAll(sprintf("MoveToTeam: %s, %f", client.netname, team_index));
 +      int lockteams_backup = lockteams;  // backup any team lock
 +      lockteams = 0;  // disable locked teams
 +      PlayerScore_Clear(client);
 +      if (!SetPlayerTeam(client, team_index, type))
        {
 +              lockteams = lockteams_backup;  // restore the team lock
                return false;
        }
 -      LogTeamchange(player.playerid, player.team, 3);  // log manual team join
 -      if (no_print)
 +      lockteams = lockteams_backup;  // restore the team lock
 +      return true;
 +}
 +
 +void KillPlayerForTeamChange(entity player)
 +{
 +      if (IS_DEAD(player))
        {
 -              return true;
 +              return;
        }
 -      bprint(playername(player, false), "^7 has changed from ", Team_NumberToColoredFullName(source_team), "^7 to ", Team_NumberToColoredFullName(destination_team), "\n");
 -      return true;
 +      if (MUTATOR_CALLHOOK(Player_ChangeTeamKill, player) == true)
 +      {
 +              return;
 +      }
 +      Damage(player, player, player, 100000, DEATH_TEAMCHANGE.m_id, DMG_NOWEP,
 +              player.origin, '0 0 0');
  }
  
 -// set c1...c4 to show what teams are allowed
 -void CheckAllowedTeams(entity for_whom)
 +void LogTeamchange(float player_id, float team_number, int type)
  {
 -      int teams_mask = 0;
 +      if(!autocvar_sv_eventlog)
 +              return;
  
 -      c1 = c2 = c3 = c4 = -1;
 -      num_bots_team1 = num_bots_team2 = num_bots_team3 = num_bots_team4 = 0;
 +      if(player_id < 1)
 +              return;
  
 -      string teament_name = string_null;
 +      GameLogEcho(strcat(":team:", ftos(player_id), ":", ftos(team_number), ":", ftos(type)));
 +}
  
 -      bool mutator_returnvalue = MUTATOR_CALLHOOK(CheckAllowedTeams, teams_mask, teament_name, for_whom);
 +entity TeamBalance_CheckAllowedTeams(entity for_whom)
 +{
 +      entity balance = spawn();
 +      for (int i = 0; i < NUM_TEAMS; ++i)
 +      {
 +              entity team_ent = balance.m_team_balance_team[i] = spawn();
 +              team_ent.m_team_score = g_team_entities[i].m_team_score;
 +              team_ent.m_num_players = TEAM_NOT_ALLOWED;
 +              team_ent.m_num_bots = 0;
 +      }
 +      
 +      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 (mutator_returnvalue)
        {
 -              if(teams_mask & BIT(0)) c1 = 0;
 -              if(teams_mask & BIT(1)) c2 = 0;
 -              if(teams_mask & BIT(2)) c3 = 0;
 -              if(teams_mask & BIT(3)) c4 = 0;
 +              for (int i = 0; i < NUM_TEAMS; ++i)
 +              {
 +                      if (teams_mask & BIT(i))
 +                      {
 +                              balance.m_team_balance_team[i].m_num_players = 0;
 +                      }
 +              }
        }
  
 -      // find out what teams are allowed if necessary
 -      if(teament_name)
 +      if (teament_name)
        {
                entity head = find(NULL, classname, teament_name);
 -              while(head)
 +              while (head)
                {
 -                      switch(head.team)
 +                      if (Team_IsValidTeam(head.team))
                        {
 -                              case NUM_TEAM_1: c1 = 0; break;
 -                              case NUM_TEAM_2: c2 = 0; break;
 -                              case NUM_TEAM_3: c3 = 0; break;
 -                              case NUM_TEAM_4: c4 = 0; break;
 +                              TeamBalance_GetTeam(balance, 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 (AvailableTeams() == 2)
 +      if (autocvar_bot_vs_human && for_whom)
        {
 -              if(autocvar_bot_vs_human > 0)
 +              if (autocvar_bot_vs_human > 0)
                {
                        // find last team available
 -
 -                      if(IS_BOT_CLIENT(for_whom))
 +                      if (IS_BOT_CLIENT(for_whom))
                        {
 -                              if(c4 >= 0) { c3 = c2 = c1 = -1; }
 -                              else if(c3 >= 0) { c4 = c2 = c1 = -1; }
 -                              else { c4 = c3 = c1 = -1; }
 +                              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(c1 >= 0) { c2 = c3 = c4 = -1; }
 -                              else if(c2 >= 0) { c1 = c3 = c4 = -1; }
 -                              else { c1 = c2 = c4 = -1; }
 +                              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 (IS_BOT_CLIENT(for_whom))
                        {
 -                              if(c1 >= 0) { c2 = c3 = c4 = -1; }
 -                              else if(c2 >= 0) { c1 = c3 = c4 = -1; }
 -                              else { c1 = c2 = c4 = -1; }
 +                              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(c4 >= 0) { c3 = c2 = c1 = -1; }
 -                              else if(c3 >= 0) { c4 = c2 = c1 = -1; }
 -                              else { c4 = c3 = c1 = -1; }
 +                              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
                        }
                }
        }
  
 -      if(!for_whom)
 -              return;
 +      if (!for_whom)
 +      {
 +              balance.m_team_balance_state = TEAM_BALANCE_TEAMS_CHECKED;
 +              return balance;
 +      }
  
        // if player has a forced team, ONLY allow that one
 -      if(for_whom.team_forced == NUM_TEAM_1 && c1 >= 0)
 -              c2 = c3 = c4 = -1;
 -      else if(for_whom.team_forced == NUM_TEAM_2 && c2 >= 0)
 -              c1 = c3 = c4 = -1;
 -      else if(for_whom.team_forced == NUM_TEAM_3 && c3 >= 0)
 -              c1 = c2 = c4 = -1;
 -      else if(for_whom.team_forced == NUM_TEAM_4 && c4 >= 0)
 -              c1 = c2 = c3 = -1;
 +      for (int i = 1; i <= NUM_TEAMS; ++i)
 +      {
 +              if (for_whom.team_forced == Team_IndexToTeam(i) &&
 +                      TeamBalance_IsTeamAllowedInternal(balance, i))
 +              {
 +                      TeamBalance_BanTeamsExcept(balance, i);
 +              }
 +              break;
 +      }
 +      balance.m_team_balance_state = TEAM_BALANCE_TEAMS_CHECKED;
 +      return balance;
  }
  
 -float PlayerValue(entity p)
 +void TeamBalance_Destroy(entity balance)
  {
 -      return 1;
 -      // FIXME: it always returns 1...
 +      if (balance == NULL)
 +      {
 +              return;
 +      }
 +      for (int i = 0; i < NUM_TEAMS; ++i)
 +      {
 +              delete(balance.(m_team_balance_team[i]));
 +      }
 +      delete(balance);
  }
  
 -// c1...c4 should be set to -1 (not allowed) or 0 (allowed).
 -// teams that are allowed will now have their player counts stored in c1...c4
 -void GetTeamCounts(entity ignore)
 +int TeamBalance_GetAllowedTeams(entity balance)
  {
 -      if (MUTATOR_CALLHOOK(GetTeamCounts) == true)
 +      if (balance == NULL)
        {
 -              if (c1 >= 0)
 -              {
 -                      MUTATOR_CALLHOOK(GetTeamCount, NUM_TEAM_1, ignore, c1,
 -                              num_bots_team1, lowest_human_team1, lowest_bot_team1);
 -                      c1 = M_ARGV(2, float);
 -                      num_bots_team1 = M_ARGV(3, float);
 -                      lowest_human_team1 = M_ARGV(4, entity);
 -                      lowest_bot_team1 = M_ARGV(5, entity);
 -              }
 -              if (c2 >= 0)
 -              {
 -                      MUTATOR_CALLHOOK(GetTeamCount, NUM_TEAM_2, ignore, c2,
 -                              num_bots_team2, lowest_human_team2, lowest_bot_team2);
 -                      c2 = M_ARGV(2, float);
 -                      num_bots_team2 = M_ARGV(3, float);
 -                      lowest_human_team2 = M_ARGV(4, entity);
 -                      lowest_bot_team2 = M_ARGV(5, entity);
 -              }
 -              if (c3 >= 0)
 +              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))
                {
 -                      MUTATOR_CALLHOOK(GetTeamCount, NUM_TEAM_3, ignore, c3,
 -                              num_bots_team3, lowest_human_team3, lowest_bot_team3);
 -                      c3 = M_ARGV(2, float);
 -                      num_bots_team3 = M_ARGV(3, float);
 -                      lowest_human_team3 = M_ARGV(4, entity);
 -                      lowest_bot_team3 = M_ARGV(5, entity);
 +                      result |= Team_IndexToBit(i);
                }
 -              if (c4 >= 0)
 +      }
 +      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);
 +}
 +
 +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)
                {
 -                      MUTATOR_CALLHOOK(GetTeamCount, NUM_TEAM_4, ignore,
 -                              c4, num_bots_team4, lowest_human_team4, lowest_bot_team4);
 -                      c4 = M_ARGV(2, float);
 -                      num_bots_team4 = M_ARGV(3, float);
 -                      lowest_human_team4 = M_ARGV(4, entity);
 -                      lowest_bot_team4 = M_ARGV(5, entity);
 +                      entity team_ent = TeamBalance_GetTeamFromIndex(balance, i);
 +                      if (TeamBalanceTeam_IsAllowed(team_ent))
 +                      {
 +                              MUTATOR_CALLHOOK(TeamBalance_GetTeamCount, i, ignore);
 +                              team_ent.m_num_players = M_ARGV(2, float);
 +                              team_ent.m_num_bots = M_ARGV(3, float);
 +                      }
                }
        }
        else
        {
 -              float value, bvalue;
 -              // now count how many players are on each team already
 -              float lowest_human_score1 = FLOAT_MAX;
 -              float lowest_bot_score1 = FLOAT_MAX;
 -              float lowest_human_score2 = FLOAT_MAX;
 -              float lowest_bot_score2 = FLOAT_MAX;
 -              float lowest_human_score3 = FLOAT_MAX;
 -              float lowest_bot_score3 = FLOAT_MAX;
 -              float lowest_human_score4 = FLOAT_MAX;
 -              float lowest_bot_score4 = FLOAT_MAX;
 +              // Manually count all players.
                FOREACH_CLIENT(true,
                {
 -                      float t;
 +                      if (it == ignore)
 +                      {
 +                              continue;
 +                      }
 +                      int team_num;
                        if (IS_PLAYER(it) || it.caplayer)
                        {
 -                              t = it.team;
 +                              team_num = it.team;
                        }
                        else if (it.team_forced > 0)
                        {
 -                              t = it.team_forced; // reserve the spot
 +                              team_num = it.team_forced; // reserve the spot
                        }
                        else
                        {
                                continue;
                        }
 -                      if (it == ignore)
 +                      if (!Team_IsValidTeam(team_num))
                        {
                                continue;
                        }
 -                      value = PlayerValue(it);
 -                      if (IS_BOT_CLIENT(it))
 -                      {
 -                              bvalue = value;
 -                      }
 -                      else
 -                      {
 -                              bvalue = 0;
 -                      }
 -                      if (value == 0)
 +                      entity team_ent = TeamBalance_GetTeam(balance, team_num);
 +                      if (!TeamBalanceTeam_IsAllowed(team_ent))
                        {
                                continue;
                        }
 -                      switch (t)
 +                      ++team_ent.m_num_players;
 +                      if (IS_BOT_CLIENT(it))
                        {
 -                              case NUM_TEAM_1:
 -                              {
 -                                      if (c1 < 0)
 -                                      {
 -                                              break;
 -                                      }
 -                                      c1 += value;
 -                                      num_bots_team1 += bvalue;
 -                                      float temp_score = PlayerScore_Get(it, SP_SCORE);
 -                                      if (!bvalue)
 -                                      {
 -                                              if (temp_score < lowest_human_score1)
 -                                              {
 -                                                      lowest_human_team1 = it;
 -                                                      lowest_human_score1 = temp_score;
 -                                              }
 -                                              break;
 -                                      }
 -                                      if (temp_score < lowest_bot_score1)
 -                                      {
 -                                              lowest_bot_team1 = it;
 -                                              lowest_bot_score1 = temp_score;
 -                                      }
 -                                      break;
 -                              }
 -                              case NUM_TEAM_2:
 -                              {
 -                                      if (c2 < 0)
 -                                      {
 -                                              break;
 -                                      }
 -                                      c2 += value;
 -                                      num_bots_team2 += bvalue;
 -                                      float temp_score = PlayerScore_Get(it, SP_SCORE);
 -                                      if (!bvalue)
 -                                      {
 -                                              if (temp_score < lowest_human_score2)
 -                                              {
 -                                                      lowest_human_team2 = it;
 -                                                      lowest_human_score2 = temp_score;
 -                                              }
 -                                              break;
 -                                      }
 -                                      if (temp_score < lowest_bot_score2)
 -                                      {
 -                                              lowest_bot_team2 = it;
 -                                              lowest_bot_score2 = temp_score;
 -                                      }
 -                                      break;
 -                              }
 -                              case NUM_TEAM_3:
 -                              {
 -                                      if (c3 < 0)
 -                                      {
 -                                              break;
 -                                      }
 -                                      c3 += value;
 -                                      num_bots_team3 += bvalue;
 -                                      float temp_score = PlayerScore_Get(it, SP_SCORE);
 -                                      if (!bvalue)
 -                                      {
 -                                              if (temp_score < lowest_human_score3)
 -                                              {
 -                                                      lowest_human_team3 = it;
 -                                                      lowest_human_score3 = temp_score;
 -                                              }
 -                                              break;
 -                                      }
 -                                      if (temp_score < lowest_bot_score3)
 -                                      {
 -                                              lowest_bot_team3 = it;
 -                                              lowest_bot_score3 = temp_score;
 -                                      }
 -                                      break;
 -                              }
 -                              case NUM_TEAM_4:
 -                              {
 -                                      if (c4 < 0)
 -                                      {
 -                                              break;
 -                                      }
 -                                      c4 += value;
 -                                      num_bots_team4 += bvalue;
 -                                      float temp_score = PlayerScore_Get(it, SP_SCORE);
 -                                      if (!bvalue)
 -                                      {
 -                                              if (temp_score < lowest_human_score4)
 -                                              {
 -                                                      lowest_human_team4 = it;
 -                                                      lowest_human_score4 = temp_score;
 -                                              }
 -                                              break;
 -                                      }
 -                                      if (temp_score < lowest_bot_score4)
 -                                      {
 -                                              lowest_bot_team4 = it;
 -                                              lowest_bot_score4 = temp_score;
 -                                      }
 -                                      break;
 -                              }
 +                              ++team_ent.m_num_bots;
                        }
                });
        }
  
        // if the player who has a forced team has not joined yet, reserve the spot
 -      if(autocvar_g_campaign)
 +      if (autocvar_g_campaign)
        {
 -              switch(autocvar_g_campaign_forceteam)
 +              if (Team_IsValidIndex(autocvar_g_campaign_forceteam))
                {
 -                      case 1: if(c1 == num_bots_team1) ++c1; break;
 -                      case 2: if(c2 == num_bots_team2) ++c2; break;
 -                      case 3: if(c3 == num_bots_team3) ++c3; break;
 -                      case 4: if(c4 == num_bots_team4) ++c4; break;
 +                      entity team_ent = TeamBalance_GetTeamFromIndex(balance,
 +                              autocvar_g_campaign_forceteam);
 +                      if (team_ent.m_num_players == team_ent.m_num_bots)
 +                      {
 +                              ++team_ent.m_num_players;
 +                      }
                }
        }
 +      balance.m_team_balance_state = TEAM_BALANCE_TEAM_COUNTS_FILLED;
  }
  
 -bool IsTeamSmallerThanTeam(int team_a, int team_b, entity player,
 -      bool use_score)
 +int TeamBalance_GetNumberOfPlayers(entity balance, int index)
  {
 -      if (!Team_IsValidNumber(team_a))
 +      if (balance == NULL)
        {
 -              LOG_FATALF("IsTeamSmallerThanTeam: team_a is invalid: %f", team_a);
 +              LOG_FATAL("TeamBalance_GetNumberOfPlayers: "
 +                      "Team balance entity is NULL.");
        }
 -      if (!Team_IsValidNumber(team_b))
 +      if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
        {
 -              LOG_FATALF("IsTeamSmallerThanTeam: team_b is invalid: %f", team_b);
 +              LOG_FATAL("TeamBalance_GetNumberOfPlayers: "
 +                      "TeamBalance_GetTeamCounts has not been called.");
        }
 -      if (team_a == team_b)
 +      if (!Team_IsValidIndex(index))
        {
 -              return false;
 +              LOG_FATALF("TeamBalance_GetNumberOfPlayers: Team index is invalid: %f",
 +                      index);
        }
 -      // we assume that CheckAllowedTeams and GetTeamCounts have already been called
 -      int num_players_team_a = -1, num_players_team_b = -1;
 -      int num_bots_team_a = 0, num_bots_team_b = 0;
 -      float score_team_a = 0, score_team_b = 0;
 -      switch (team_a)
 +      return balance.m_team_balance_team[index - 1].m_num_players;
 +}
 +
 +int TeamBalance_FindBestTeam(entity balance, entity player, bool ignore_player)
 +{
 +      if (balance == NULL)
        {
 -              case 1:
 -              {
 -                      num_players_team_a = c1;
 -                      num_bots_team_a = num_bots_team1;
 -                      score_team_a = team1_score;
 -                      break;
 -              }
 -              case 2:
 -              {
 -                      num_players_team_a = c2;
 -                      num_bots_team_a = num_bots_team2;
 -                      score_team_a = team2_score;
 -                      break;
 -              }
 -              case 3:
 -              {
 -                      num_players_team_a = c3;
 -                      num_bots_team_a = num_bots_team3;
 -                      score_team_a = team3_score;
 -                      break;
 -              }
 -              case 4:
 -              {
 -                      num_players_team_a = c4;
 -                      num_bots_team_a = num_bots_team4;
 -                      score_team_a = team4_score;
 -                      break;
 -              }
 +              LOG_FATAL("TeamBalance_FindBestTeam: Team balance entity is NULL.");
        }
 -      switch (team_b)
 +      if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
        {
 -              case 1:
 -              {
 -                      num_players_team_b = c1;
 -                      num_bots_team_b = num_bots_team1;
 -                      score_team_b = team1_score;
 -                      break;
 -              }
 -              case 2:
 -              {
 -                      num_players_team_b = c2;
 -                      num_bots_team_b = num_bots_team2;
 -                      score_team_b = team2_score;
 -                      break;
 -              }
 -              case 3:
 -              {
 -                      num_players_team_b = c3;
 -                      num_bots_team_b = num_bots_team3;
 -                      score_team_b = team3_score;
 -                      break;
 -              }
 -              case 4:
 -              {
 -                      num_players_team_b = c4;
 -                      num_bots_team_b = num_bots_team4;
 -                      score_team_b = team4_score;
 -                      break;
 -              }
 +              LOG_FATAL("TeamBalance_FindBestTeam: "
 +                      "Team balance entity is not initialized.");
        }
 -      // invalid
 -      if (num_players_team_a < 0 || num_players_team_b < 0)
 -      {
 -              return false;
 -      }
 -      if (IS_REAL_CLIENT(player) && bots_would_leave)
 +      // count how many players are in each team
 +      if (ignore_player)
        {
 -              num_players_team_a -= num_bots_team_a;
 -              num_players_team_b -= num_bots_team_b;
 +              TeamBalance_GetTeamCounts(balance, player);
        }
 -      if (!use_score)
 +      else
        {
 -              return num_players_team_a < num_players_team_b;
 +              TeamBalance_GetTeamCounts(balance, NULL);
        }
 -      if (num_players_team_a < num_players_team_b)
 +      int team_bits = TeamBalance_FindBestTeams(balance, player, true);
 +      if (team_bits == 0)
        {
 -              return true;
 +              LOG_FATALF("TeamBalance_FindBestTeam: No teams available for %s\n",
 +                      MapInfo_Type_ToString(MapInfo_CurrentGametype()));
        }
 -      if (num_players_team_a > num_players_team_b)
 +      RandomSelection_Init();
 +      for (int i = 1; i <= NUM_TEAMS; ++i)
        {
 -              return false;
 +              if (team_bits & Team_IndexToBit(i))
 +              {
 +                      RandomSelection_AddFloat(i, 1, 1);
 +              }
        }
 -      return score_team_a < score_team_b;
 +      return RandomSelection_chosen_float;
  }
  
 -bool IsTeamEqualToTeam(int team_a, int team_b, entity player, bool use_score)
 +int TeamBalance_FindBestTeams(entity balance, entity player, bool use_score)
  {
 -      if (!Team_IsValidNumber(team_a))
 +      if (balance == NULL)
        {
 -              LOG_FATALF("IsTeamEqualToTeam: team_a is invalid: %f", team_a);
 +              LOG_FATAL("TeamBalance_FindBestTeams: Team balance entity is NULL.");
        }
 -      if (!Team_IsValidNumber(team_b))
 +      if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
        {
 -              LOG_FATALF("IsTeamEqualToTeam: team_b is invalid: %f", team_b);
 +              LOG_FATAL("TeamBalance_FindBestTeams: "
 +                      "TeamBalance_GetTeamCounts has not been called.");
        }
 -      if (team_a == team_b)
 +      if (MUTATOR_CALLHOOK(TeamBalance_FindBestTeams, player) == true)
        {
 -              return true;
 +              return M_ARGV(1, float);
        }
 -      // we assume that CheckAllowedTeams and GetTeamCounts have already been called
 -      int num_players_team_a = -1, num_players_team_b = -1;
 -      int num_bots_team_a = 0, num_bots_team_b = 0;
 -      float score_team_a = 0, score_team_b = 0;
 -      switch (team_a)
 +      int team_bits = 0;
 +      int previous_team = 0;
 +      for (int i = 1; i <= NUM_TEAMS; ++i)
        {
 -              case 1:
 +              if (!TeamBalance_IsTeamAllowedInternal(balance, i))
                {
 -                      num_players_team_a = c1;
 -                      num_bots_team_a = num_bots_team1;
 -                      score_team_a = team1_score;
 -                      break;
 +                      continue;
                }
 -              case 2:
 +              if (previous_team == 0)
                {
 -                      num_players_team_a = c2;
 -                      num_bots_team_a = num_bots_team2;
 -                      score_team_a = team2_score;
 -                      break;
 +                      team_bits = Team_IndexToBit(i);
 +                      previous_team = i;
 +                      continue;
                }
 -              case 3:
 +              int compare = TeamBalance_CompareTeams(balance, i, previous_team,
 +                      player, use_score);
 +              if (compare == TEAMS_COMPARE_LESS)
                {
 -                      num_players_team_a = c3;
 -                      num_bots_team_a = num_bots_team3;
 -                      score_team_a = team3_score;
 -                      break;
 +                      team_bits = Team_IndexToBit(i);
 +                      previous_team = i;
 +                      continue;
                }
 -              case 4:
 +              if (compare == TEAMS_COMPARE_EQUAL)
                {
 -                      num_players_team_a = c4;
 -                      num_bots_team_a = num_bots_team4;
 -                      score_team_a = team4_score;
 -                      break;
 +                      team_bits |= Team_IndexToBit(i);
 +                      previous_team = i;
                }
        }
 -      switch (team_b)
 +      return team_bits;
 +}
 +
 +void TeamBalance_JoinBestTeam(entity this, bool force_best_team)
 +{
 +      //PrintToChatAll(sprintf("JoinBestTeam: %s, %f", this.netname, force_best_team));
 +      // don't join a team if we're not playing a team game
 +      if (!teamplay)
        {
 -              case 1:
 -              {
 -                      num_players_team_b = c1;
 -                      num_bots_team_b = num_bots_team1;
 -                      score_team_b = team1_score;
 -                      break;
 -              }
 -              case 2:
 -              {
 -                      num_players_team_b = c2;
 -                      num_bots_team_b = num_bots_team2;
 -                      score_team_b = team2_score;
 -                      break;
 -              }
 -              case 3:
 +              return;
 +      }
 +
 +      // find out what teams are available
 +      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_index = -1;
 +              for (int i = 1; i <= NUM_TEAMS; ++i)
                {
 -                      num_players_team_b = c3;
 -                      num_bots_team_b = num_bots_team3;
 -                      score_team_b = team3_score;
 -                      break;
 +                      if (TeamBalance_IsTeamAllowedInternal(balance, i) &&
 +                              (Team_TeamToIndex(this.team) == i))
 +                      {
 +                              selected_team_index = i;
 +                              break;
 +                      }
                }
 -              case 4:
 +              
 +              if (Team_IsValidIndex(selected_team_index))
                {
 -                      num_players_team_b = c4;
 -                      num_bots_team_b = num_bots_team4;
 -                      score_team_b = team4_score;
 -                      break;
 +                      SetPlayerTeam(this, selected_team_index, TEAM_CHANGE_AUTO_RELAXED);
 +                      TeamBalance_Destroy(balance);
 +                      return;
                }
        }
 -      // invalid
 -      if (num_players_team_a < 0 || num_players_team_b < 0)
 -              return false;
 -
 -      if (IS_REAL_CLIENT(player) && bots_would_leave)
 +      // otherwise end up on the smallest team (handled below)
 +      if (this.bot_forced_team)
        {
 -              num_players_team_a -= num_bots_team_a;
 -              num_players_team_b -= num_bots_team_b;
 +              TeamBalance_Destroy(balance);
 +              return;
        }
 -      if (!use_score)
 +      int best_team_index = TeamBalance_FindBestTeam(balance, this, true);
 +      int old_team_index = Team_TeamToIndex(this.team);
 +      TeamBalance_Destroy(balance);
 +      PlayerScore_Clear(this);
 +      if (!SetPlayerTeam(this, best_team_index, TEAM_CHANGE_AUTO))
        {
 -              return num_players_team_a == num_players_team_b;
 +              return;
        }
 -      if (num_players_team_a != num_players_team_b)
 +      if ((old_team_index != -1) && !IS_BOT_CLIENT(this))
        {
 -              return false;
 +              TeamBalance_AutoBalanceBots(best_team_index, old_team_index);
        }
 -      return score_team_a == score_team_b;
  }
  
 -int FindBestTeams(entity player, bool use_score)
 +int TeamBalance_CompareTeams(entity balance, int team_index_a, int team_index_b,
 +      entity player, bool use_score)
  {
 -      if (MUTATOR_CALLHOOK(FindBestTeams, player) == true)
 +      if (balance == NULL)
        {
 -              return M_ARGV(1, float);
 +              LOG_FATAL("TeamBalance_CompareTeams: Team balance entity is NULL.");
        }
 -      int team_bits = 0;
 -      int previous_team = 0;
 -      if (c1 >= 0)
 +      if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
        {
 -              team_bits = BIT(0);
 -              previous_team = 1;
 +              LOG_FATAL("TeamBalance_CompareTeams: "
 +                      "TeamBalance_GetTeamCounts has not been called.");
        }
 -      if (c2 >= 0)
 +      if (!Team_IsValidIndex(team_index_a))
        {
 -              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))
 -              {
 -                      team_bits |= BIT(1);
 -                      previous_team = 2;
 -              }
 +              LOG_FATALF("TeamBalance_CompareTeams: team_index_a is invalid: %f",
 +                      team_index_a);
        }
 -      if (c3 >= 0)
 +      if (!Team_IsValidIndex(team_index_b))
        {
 -              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))
 -              {
 -                      team_bits |= BIT(2);
 -                      previous_team = 3;
 -              }
 +              LOG_FATALF("TeamBalance_CompareTeams: team_index_b is invalid: %f",
 +                      team_index_b);
        }
 -      if (c4 >= 0)
 +      if (team_index_a == team_index_b)
        {
 -              if (previous_team == 0)
 -              {
 -                      team_bits = BIT(3);
 -              }
 -              else if (IsTeamSmallerThanTeam(4, previous_team, player, use_score))
 -              {
 -                      team_bits = BIT(3);
 -              }
 -              else if (IsTeamEqualToTeam(4, previous_team, player, use_score))
 -              {
 -                      team_bits |= BIT(3);
 -              }
 +              return TEAMS_COMPARE_EQUAL;
        }
 -      return team_bits;
 +      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);
  }
  
 -// returns # of smallest team (1, 2, 3, 4)
 -// NOTE: Assumes CheckAllowedTeams has already been called!
 -int FindSmallestTeam(entity player, float ignore_player)
 +void TeamBalance_AutoBalanceBots(int source_team_index,
 +      int destination_team_index)
  {
 -      // count how many players are in each team
 -      if (ignore_player)
 +      if (!Team_IsValidIndex(source_team_index))
        {
 -              GetTeamCounts(player);
 +              LOG_WARNF("TeamBalance_AutoBalanceBots: "
 +                      "Source team index is invalid: %f", source_team_index);
 +              return;
        }
 -      else
 +      if (!Team_IsValidIndex(destination_team_index))
        {
 -              GetTeamCounts(NULL);
 +              LOG_WARNF("TeamBalance_AutoBalanceBots: "
 +                      "Destination team index is invalid: %f", destination_team_index);
 +              return;
        }
 -      int team_bits = FindBestTeams(player, true);
 -      if (team_bits == 0)
 +      if (!autocvar_g_balance_teams ||
 +              !autocvar_g_balance_teams_prevent_imbalance)
        {
 -              error(sprintf("No teams available for %s\n", MapInfo_Type_ToString(MapInfo_CurrentGametype())));
 +              return;
        }
 -      RandomSelection_Init();
 -      if ((team_bits & BIT(0)) != 0)
 +      entity balance = TeamBalance_CheckAllowedTeams(NULL);
 +      TeamBalance_GetTeamCounts(balance, NULL);
 +      entity source_team = TeamBalance_GetTeamFromIndex(balance,
 +              source_team_index);
 +      entity destination_team = TeamBalance_GetTeamFromIndex(balance,
 +              destination_team_index);
 +      if ((source_team.m_num_bots == 0) || (source_team.m_num_players <=
 +              destination_team.m_num_players))
        {
 -              RandomSelection_AddFloat(1, 1, 1);
 +              TeamBalance_Destroy(balance);
 +              return;
        }
 -      if ((team_bits & BIT(1)) != 0)
 +      TeamBalance_Destroy(balance);
 +      entity lowest_bot = NULL;
 +      if (MUTATOR_CALLHOOK(TeamBalance_GetPlayerForTeamSwitch, source_team_index,
 +              destination_team_index, true))
        {
 -              RandomSelection_AddFloat(2, 1, 1);
 +              lowest_bot = M_ARGV(3, entity);
        }
 -      if ((team_bits & BIT(2)) != 0)
 +      else
        {
 -              RandomSelection_AddFloat(3, 1, 1);
 +              float lowest_score = FLOAT_MAX;
 +              FOREACH_CLIENT(IS_BOT_CLIENT(it) && (Entity_GetTeamIndex(it) ==
 +                      source_team_index),
 +              {
 +                      float temp_score = PlayerScore_Get(it, SP_SCORE);
 +                      if (temp_score >= lowest_score)
 +                      {
 +                              continue;
 +                      }
 +                      balance = TeamBalance_CheckAllowedTeams(it);
 +                      if (TeamBalance_IsTeamAllowed(balance, destination_team_index))
 +                      {
 +                              lowest_bot = it;
 +                              lowest_score = temp_score;
 +                      }
 +                      TeamBalance_Destroy(balance);
 +              });
        }
 -      if ((team_bits & BIT(3)) != 0)
 +      if (lowest_bot == NULL)
        {
 -              RandomSelection_AddFloat(4, 1, 1);
 +              return;
        }
 -      return RandomSelection_chosen_float;
 -}
 -
 -void JoinBestTeam(entity this, bool force_best_team)
 -{
 -      // don't join a team if we're not playing a team game
 -      if (!teamplay)
 +      if (!Player_SetTeamIndex(lowest_bot, destination_team_index))
        {
                return;
        }
 +      KillPlayerForTeamChange(lowest_bot);
 +}
  
 -      // find out what teams are available
 -      CheckAllowedTeams(this);
 +bool TeamBalance_IsTeamAllowedInternal(entity balance, int index)
 +{
 +      return balance.m_team_balance_team[index - 1].m_num_players !=
 +              TEAM_NOT_ALLOWED;
 +}
  
 -      // 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)
 +void TeamBalance_BanTeamsExcept(entity balance, int index)
 +{
 +      for (int i = 1; i <= NUM_TEAMS; ++i)
        {
 -              int selected_team;
 -              if ((c1 >= 0) && (this.team == NUM_TEAM_1))
 -              {
 -                      selected_team = this.team;
 -              }
 -              else if ((c2 >= 0) && (this.team == NUM_TEAM_2))
 -              {
 -                      selected_team = this.team;
 -              }
 -              else if ((c3 >= 0) && (this.team == NUM_TEAM_3))
 -              {
 -                      selected_team = this.team;
 -              }
 -              else if ((c4 >= 0) && (this.team == NUM_TEAM_4))
 -              {
 -                      selected_team = this.team;
 -              }
 -              else
 +              if (i != index)
                {
 -                      selected_team = -1;
 +                      balance.m_team_balance_team[i - 1].m_num_players = TEAM_NOT_ALLOWED;
                }
 +      }
 +}
  
 -              if (selected_team > 0)
 -              {
 -                      SetPlayerTeamSimple(this, selected_team);
 -                      LogTeamchange(this.playerid, this.team, 99);
 -                      return;
 -              }
 +entity TeamBalance_GetTeamFromIndex(entity balance, int index)
 +{
 +      if (!Team_IsValidIndex(index))
 +      {
 +              LOG_FATALF("TeamBalance_GetTeamFromIndex: Index is invalid: %f", index);
        }
 -      // otherwise end up on the smallest team (handled below)
 -      if (this.bot_forced_team)
 +      return balance.m_team_balance_team[index - 1];
 +}
 +
 +entity TeamBalance_GetTeam(entity balance, int team_num)
 +{
 +      return TeamBalance_GetTeamFromIndex(balance, Team_TeamToIndex(team_num));
 +}
 +
 +bool TeamBalanceTeam_IsAllowed(entity team_ent)
 +{
 +      return team_ent.m_num_players != TEAM_NOT_ALLOWED;
 +}
 +
 +int TeamBalanceTeam_GetNumberOfPlayers(entity team_ent)
 +{
 +      return team_ent.m_num_players;
 +}
 +
 +int TeamBalanceTeam_GetNumberOfBots(entity team_ent)
 +{
 +      return team_ent.m_num_bots;
 +}
 +
 +int TeamBalance_CompareTeamsInternal(entity team_a, entity team_b,
 +      entity player, bool use_score)
 +{
 +      if (team_a == team_b)
        {
 -              return;
 +              return TEAMS_COMPARE_EQUAL;
 +      }
 +      if (!TeamBalanceTeam_IsAllowed(team_a) ||
 +              !TeamBalanceTeam_IsAllowed(team_b))
 +      {
 +              return TEAMS_COMPARE_INVALID;
 +      }
 +      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)
 +      {
 +              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 TEAMS_COMPARE_EQUAL;
        }
 -      int best_team = FindSmallestTeam(this, true);
 -      best_team = Team_NumberToTeam(best_team);
 -      if (best_team == -1)
 +      if (team_a.m_team_score < team_b.m_team_score)
        {
 -              error("JoinBestTeam: invalid team\n");
 +              return TEAMS_COMPARE_LESS;
        }
 -      int old_team = Team_TeamToNumber(this.team);
 -      TeamchangeFrags(this);
 -      SetPlayerTeamSimple(this, best_team);
 -      LogTeamchange(this.playerid, this.team, 2); // log auto join
 -      if ((old_team != -1) && !IS_BOT_CLIENT(this))
 +      if (team_a.m_team_score > team_b.m_team_score)
        {
 -              AutoBalanceBots(old_team, Team_TeamToNumber(best_team));
 +              return TEAMS_COMPARE_GREATER;
        }
 -      KillPlayerForTeamChange(this);
 +      return TEAMS_COMPARE_EQUAL;
  }
  
 +// Called when the player connects or when they change their color with "color"
 +// command.
  void SV_ChangeTeam(entity this, float _color)
  {
 -      float source_color, destination_color, source_team, destination_team;
 +      //PrintToChatAll(sprintf("SV_ChangeTeam: %s, %f", this.netname, _color));
  
        // in normal deathmatch we can just apply the color and we're done
        if(!teamplay)
        if(!teamplay)
                return;
  
 +      int source_color, destination_color;
 +      int source_team_index, destination_team_index;
 +
        source_color = this.clientcolors & 0x0F;
        destination_color = _color & 0x0F;
  
 -      source_team = Team_TeamToNumber(source_color + 1);
 -      destination_team = Team_TeamToNumber(destination_color + 1);
 +      source_team_index = Team_TeamToIndex(source_color + 1);
 +      destination_team_index = Team_TeamToIndex(destination_color + 1);
  
 -      if (destination_team == -1)
 +      if (destination_team_index == -1)
        {
                return;
        }
  
 -      CheckAllowedTeams(this);
 +      entity balance = TeamBalance_CheckAllowedTeams(this);
  
 -      if (destination_team == 1 && c1 < 0) destination_team = 4;
 -      if (destination_team == 4 && c4 < 0) destination_team = 3;
 -      if (destination_team == 3 && c3 < 0) destination_team = 2;
 -      if (destination_team == 2 && c2 < 0) destination_team = 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, source_team, true);
 +              SetPlayerTeam(this, destination_team_index, TEAM_CHANGE_MANUAL);
 +              TeamBalance_Destroy(balance);
                return;
        }
  
        // 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 - 1) & FindBestTeams(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;
                }
        }
 -      if(IS_PLAYER(this) && source_team != destination_team)
 +      TeamBalance_Destroy(balance);
 +      if (IS_PLAYER(this) && source_team_index != destination_team_index)
        {
                // reduce frags during a team change
 -              TeamchangeFrags(this);
 -      }
 -      if (!SetPlayerTeam(this, destination_team, source_team, !IS_CLIENT(this)))
 -      {
 -              return;
 -      }
 -      AutoBalanceBots(source_team, destination_team);
 -      if (!IS_PLAYER(this) || (source_team == destination_team))
 -      {
 -              return;
 -      }
 -      KillPlayerForTeamChange(this);
 -}
 -
 -void AutoBalanceBots(int source_team, int destination_team)
 -{
 -      if (!Team_IsValidNumber(source_team))
 -      {
 -              LOG_WARNF("AutoBalanceBots: Source team is invalid: %f", source_team);
 -              return;
 -      }
 -      if (!Team_IsValidNumber(destination_team))
 -      {
 -              LOG_WARNF("AutoBalanceBots: Destination team is invalid: %f",
 -                      destination_team);
 -              return;
 -      }
 -      if (!autocvar_g_balance_teams ||
 -              !autocvar_g_balance_teams_prevent_imbalance)
 -      {
 -              return;
 -      }
 -      int num_players_source_team = 0;
 -      int num_players_destination_team = 0;
 -      entity lowest_bot_destination_team = NULL;
 -      switch (source_team)
 -      {
 -              case 1:
 -              {
 -                      num_players_source_team = c1;
 -                      break;
 -              }
 -              case 2:
 -              {
 -                      num_players_source_team = c2;
 -                      break;
 -              }
 -              case 3:
 -              {
 -                      num_players_source_team = c3;
 -                      break;
 -              }
 -              case 4:
 -              {
 -                      num_players_source_team = c4;
 -                      break;
 -              }
 -      }
 -      if (num_players_source_team < 0)
 -      {
 -              return;
 -      }
 -      switch (destination_team)
 -      {
 -              case 1:
 -              {
 -                      num_players_destination_team = c1;
 -                      lowest_bot_destination_team = lowest_bot_team1;
 -                      break;
 -              }
 -              case 2:
 -              {
 -                      num_players_destination_team = c2;
 -                      lowest_bot_destination_team = lowest_bot_team2;
 -                      break;
 -              }
 -              case 3:
 -              {
 -                      num_players_destination_team = c3;
 -                      lowest_bot_destination_team = lowest_bot_team3;
 -                      break;
 -              }
 -              case 4:
 -              {
 -                      num_players_destination_team = c4;
 -                      lowest_bot_destination_team = lowest_bot_team4;
 -                      break;
 -              }
 +              PlayerScore_Clear(this);
        }
 -      if ((num_players_destination_team <= num_players_source_team) ||
 -              (lowest_bot_destination_team == NULL))
 +      if (!SetPlayerTeam(this, destination_team_index, TEAM_CHANGE_MANUAL))
        {
                return;
        }
 -      SetPlayerTeamSimple(lowest_bot_destination_team,
 -              Team_NumberToTeam(source_team));
 -      KillPlayerForTeamChange(lowest_bot_destination_team);
 +      TeamBalance_AutoBalanceBots(destination_team_index, source_team_index);
  }