]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/teamplay.qc
Teamplay: Second pass of autobalance.
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / teamplay.qc
1 #include "teamplay.qh"
2
3 #include "client.qh"
4 #include "race.qh"
5 #include "scores.qh"
6 #include "scores_rules.qh"
7
8 #include "bot/api.qh"
9
10 #include "command/vote.qh"
11
12 #include <server/mutators/_mod.qh>
13
14 #include "../common/deathtypes/all.qh"
15 #include <common/gamemodes/_mod.qh>
16 #include "../common/teams.qh"
17
18 /// \brief Describes a state of team balance entity.
19 enum
20 {
21         TEAM_BALANCE_UNINITIALIZED, ///< The team balance has not been initialized.
22         /// \brief TeamBalance_CheckAllowedTeams has been called.
23         TEAM_BALANCE_TEAMS_CHECKED,
24         /// \brief TeamBalance_GetTeamCounts has been called.
25         TEAM_BALANCE_TEAM_COUNTS_FILLED
26 };
27
28 /// \brief Indicates that the player is not allowed to join a team.
29 const int TEAM_NOT_ALLOWED = -1;
30
31 .float team_forced; // can be a team number to force a team, or 0 for default action, or -1 for forced spectator
32
33 .int m_team_balance_state; ///< Holds the state of the team balance entity.
34 .entity m_team_balance_team[NUM_TEAMS]; ///< ???
35
36 .float m_team_score; ///< The score of the team.
37 .int m_num_players; ///< Number of players (both humans and bots) in a team.
38 .int m_num_bots; ///< Number of bots in a team.
39 .int m_num_players_alive; ///< Number of alive players in a team.
40 .int m_num_control_points; ///< Number of control points owned by a team.
41
42 string autocvar_g_forced_team_red;
43 string autocvar_g_forced_team_blue;
44 string autocvar_g_forced_team_yellow;
45 string autocvar_g_forced_team_pink;
46
47 entity g_team_entities[NUM_TEAMS]; ///< Holds global team entities.
48
49 STATIC_INIT(g_team_entities)
50 {
51         for (int i = 0; i < NUM_TEAMS; ++i)
52         {
53                 g_team_entities[i] = spawn();
54         }
55 }
56
57 entity Team_GetTeamFromIndex(int index)
58 {
59         if (!Team_IsValidIndex(index))
60         {
61                 LOG_FATALF("Team_GetTeamFromIndex: Index is invalid: %f", index);
62         }
63         return g_team_entities[index - 1];
64 }
65
66 entity Team_GetTeam(int team_num)
67 {
68         if (!Team_IsValidTeam(team_num))
69         {
70                 LOG_FATALF("Team_GetTeam: Value is invalid: %f", team_num);
71         }
72         return g_team_entities[Team_TeamToIndex(team_num) - 1];
73 }
74
75 float Team_GetTeamScore(entity team_ent)
76 {
77         return team_ent.m_team_score;
78 }
79
80 void Team_SetTeamScore(entity team_ent, float score)
81 {
82         team_ent.m_team_score = score;
83 }
84
85 int Team_GetNumberOfAlivePlayers(entity team_ent)
86 {
87         return team_ent.m_num_players_alive;
88 }
89
90 void Team_SetNumberOfAlivePlayers(entity team_ent, int number)
91 {
92         team_ent.m_num_players_alive = number;
93 }
94
95 int Team_GetNumberOfAliveTeams()
96 {
97         int result = 0;
98         for (int i = 0; i < NUM_TEAMS; ++i)
99         {
100                 if (g_team_entities[i].m_num_players_alive > 0)
101                 {
102                         ++result;
103                 }
104         }
105         return result;
106 }
107
108 int Team_GetNumberOfControlPoints(entity team_ent)
109 {
110         return team_ent.m_num_control_points;
111 }
112
113 void Team_SetNumberOfControlPoints(entity team_ent, int number)
114 {
115         team_ent.m_num_control_points = number;
116 }
117
118 int Team_GetNumberOfTeamsWithControlPoints()
119 {
120         int result = 0;
121         for (int i = 0; i < NUM_TEAMS; ++i)
122         {
123                 if (g_team_entities[i].m_num_control_points > 0)
124                 {
125                         ++result;
126                 }
127         }
128         return result;
129 }
130
131 void setcolor(entity this, int clr)
132 {
133 #if 0
134         this.clientcolors = clr;
135         this.team = (clr & 15) + 1;
136 #else
137         builtin_setcolor(this, clr);
138 #endif
139 }
140
141 bool Entity_HasValidTeam(entity this)
142 {
143         return Team_IsValidTeam(this.team);
144 }
145
146 int Entity_GetTeamIndex(entity this)
147 {
148         return Team_TeamToIndex(this.team);
149 }
150
151 entity Entity_GetTeam(entity this)
152 {
153         int index = Entity_GetTeamIndex(this);
154         if (!Team_IsValidIndex(index))
155         {
156                 return NULL;
157         }
158         return Team_GetTeamFromIndex(index);
159 }
160
161 void SetPlayerColors(entity player, float _color)
162 {
163         float pants = _color & 0x0F;
164         float shirt = _color & 0xF0;
165         if (teamplay)
166         {
167                 setcolor(player, 16 * pants + pants);
168         }
169         else
170         {
171                 setcolor(player, shirt + pants);
172         }
173 }
174
175 bool Player_SetTeamIndex(entity player, int index)
176 {
177         int new_team = Team_IndexToTeam(index);
178         if (player.team == new_team)
179         {
180                 if (new_team != -1)
181                 {
182                         // This is important when players join the game and one of their
183                         // color matches the team color while other doesn't. For example
184                         // [BOT]Lion.
185                         SetPlayerColors(player, new_team - 1);
186                 }
187                 return true;
188         }
189         int old_index = Team_TeamToIndex(player.team);
190         if (MUTATOR_CALLHOOK(Player_ChangeTeam, player, old_index, index) == true)
191         {
192                 // Mutator has blocked team change.
193                 return false;
194         }
195         if (new_team == -1)
196         {
197                 player.team = -1;
198         }
199         else
200         {
201                 SetPlayerColors(player, new_team - 1);
202         }
203         MUTATOR_CALLHOOK(Player_ChangedTeam, player, old_index, index);
204         return true;
205 }
206
207 bool SetPlayerTeam(entity player, int team_index, int type)
208 {
209         int old_team_index = Entity_GetTeamIndex(player);
210         if (!Player_SetTeamIndex(player, team_index))
211         {
212                 return false;
213         }
214         LogTeamchange(player.playerid, player.team, type);
215         if (team_index != old_team_index)
216         {
217                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(player.team,
218                         INFO_JOIN_PLAY_TEAM), player.netname);
219                 KillPlayerForTeamChange(player);
220         }
221         return true;
222 }
223
224 bool MoveToTeam(entity client, int team_index, int type)
225 {
226         //PrintToChatAll(sprintf("MoveToTeam: %s, %f", client.netname, team_index));
227         int lockteams_backup = lockteams;  // backup any team lock
228         lockteams = 0;  // disable locked teams
229         PlayerScore_Clear(client);
230         if (!SetPlayerTeam(client, team_index, type))
231         {
232                 lockteams = lockteams_backup;  // restore the team lock
233                 return false;
234         }
235         lockteams = lockteams_backup;  // restore the team lock
236         return true;
237 }
238
239 void KillPlayerForTeamChange(entity player)
240 {
241         if (IS_DEAD(player))
242         {
243                 return;
244         }
245         if (MUTATOR_CALLHOOK(Player_ChangeTeamKill, player) == true)
246         {
247                 return;
248         }
249         Damage(player, player, player, 100000, DEATH_TEAMCHANGE.m_id, DMG_NOWEP,
250                 player.origin, '0 0 0');
251 }
252
253 void LogTeamchange(float player_id, float team_number, int type)
254 {
255         if(!autocvar_sv_eventlog)
256                 return;
257
258         if(player_id < 1)
259                 return;
260
261         GameLogEcho(strcat(":team:", ftos(player_id), ":", ftos(team_number), ":", ftos(type)));
262 }
263
264 bool Player_HasRealForcedTeam(entity player)
265 {
266         return player.team_forced > TEAM_FORCE_DEFAULT;
267 }
268
269 int Player_GetForcedTeamIndex(entity player)
270 {
271         return player.team_forced;
272 }
273
274 void Player_SetForcedTeamIndex(entity player, int team_index)
275 {
276         switch (team_index)
277         {
278                 case TEAM_FORCE_SPECTATOR:
279                 case TEAM_FORCE_DEFAULT:
280                 {
281                         player.team_forced = team_index;
282                         break;
283                 }
284                 default:
285                 {
286                         if (!Team_IsValidIndex(team_index))
287                         {
288                                 LOG_FATAL("Player_SetForcedTeamIndex: Invalid team index.");
289                         }
290                         else
291                         {
292                                 player.team_forced = team_index;
293                                 break;
294                         }
295                 }
296         }
297 }
298
299 void Player_DetermineForcedTeam(entity player)
300 {
301         if (autocvar_g_campaign)
302         {
303                 if (IS_REAL_CLIENT(player)) // only players, not bots
304                 {
305                         if (Team_IsValidIndex(autocvar_g_campaign_forceteam))
306                         {
307                                 player.team_forced = autocvar_g_campaign_forceteam;
308                         }
309                         else
310                         {
311                                 player.team_forced = TEAM_FORCE_DEFAULT;
312                         }
313                 }
314         }
315         else if (PlayerInList(player, autocvar_g_forced_team_red))
316         {
317                 player.team_forced = 1;
318         }
319         else if (PlayerInList(player, autocvar_g_forced_team_blue))
320         {
321                 player.team_forced = 2;
322         }
323         else if (PlayerInList(player, autocvar_g_forced_team_yellow))
324         {
325                 player.team_forced = 3;
326         }
327         else if (PlayerInList(player, autocvar_g_forced_team_pink))
328         {
329                 player.team_forced = 4;
330         }
331         else
332         {
333                 switch (autocvar_g_forced_team_otherwise)
334                 {
335                         case "red":
336                         {
337                                 player.team_forced = 1;
338                                 break;
339                         }
340                         case "blue":
341                         {
342                                 player.team_forced = 2;
343                                 break;
344                         }
345                         case "yellow":
346                         {
347                                 player.team_forced = 3;
348                                 break;
349                         }
350                         case "pink":
351                         {
352                                 player.team_forced = 4;
353                                 break;
354                         }
355                         case "spectate":
356                         case "spectator":
357                         {
358                                 player.team_forced = TEAM_FORCE_SPECTATOR;
359                                 break;
360                         }
361                         default:
362                         {
363                                 player.team_forced = TEAM_FORCE_DEFAULT;
364                                 break;
365                         }
366                 }
367         }
368         if (!teamplay && Player_HasRealForcedTeam(player))
369         {
370                 player.team_forced = TEAM_FORCE_DEFAULT;
371         }
372 }
373
374 entity TeamBalance_CheckAllowedTeams(entity for_whom)
375 {
376         entity balance = spawn();
377         for (int i = 0; i < NUM_TEAMS; ++i)
378         {
379                 entity team_ent = balance.m_team_balance_team[i] = spawn();
380                 team_ent.m_team_score = g_team_entities[i].m_team_score;
381                 team_ent.m_num_players = TEAM_NOT_ALLOWED;
382                 team_ent.m_num_bots = 0;
383         }
384         setthink(balance, TeamBalance_Destroy);
385         
386         int teams_mask = 0;     
387         string teament_name = string_null;
388         bool mutator_returnvalue = MUTATOR_CALLHOOK(TeamBalance_CheckAllowedTeams,
389                 teams_mask, teament_name, for_whom);
390         teams_mask = M_ARGV(0, float);
391         teament_name = M_ARGV(1, string);
392         if (mutator_returnvalue)
393         {
394                 for (int i = 0; i < NUM_TEAMS; ++i)
395                 {
396                         if (teams_mask & BIT(i))
397                         {
398                                 balance.m_team_balance_team[i].m_num_players = 0;
399                         }
400                 }
401         }
402
403         if (teament_name)
404         {
405                 entity head = find(NULL, classname, teament_name);
406                 while (head)
407                 {
408                         if (Team_IsValidTeam(head.team))
409                         {
410                                 TeamBalance_GetTeam(balance, head.team).m_num_players = 0;
411                         }
412                         head = find(head, classname, teament_name);
413                 }
414         }
415
416         // TODO: Balance quantity of bots across > 2 teams when bot_vs_human is set (and remove next line)
417         if (AvailableTeams() == 2)
418         if (autocvar_bot_vs_human && for_whom)
419         {
420                 if (autocvar_bot_vs_human > 0)
421                 {
422                         // find last team available
423                         if (IS_BOT_CLIENT(for_whom))
424                         {
425                                 if (TeamBalance_IsTeamAllowedInternal(balance, 4))
426                                 {
427                                         TeamBalance_BanTeamsExcept(balance, 4);
428                                 }
429                                 else if (TeamBalance_IsTeamAllowedInternal(balance, 3))
430                                 {
431                                         TeamBalance_BanTeamsExcept(balance, 3);
432                                 }
433                                 else
434                                 {
435                                         TeamBalance_BanTeamsExcept(balance, 2);
436                                 }
437                                 // no further cases, we know at least 2 teams exist
438                         }
439                         else
440                         {
441                                 if (TeamBalance_IsTeamAllowedInternal(balance, 1))
442                                 {
443                                         TeamBalance_BanTeamsExcept(balance, 1);
444                                 }
445                                 else if (TeamBalance_IsTeamAllowedInternal(balance, 2))
446                                 {
447                                         TeamBalance_BanTeamsExcept(balance, 2);
448                                 }
449                                 else
450                                 {
451                                         TeamBalance_BanTeamsExcept(balance, 3);
452                                 }
453                                 // no further cases, bots have one of the teams
454                         }
455                 }
456                 else
457                 {
458                         // find first team available
459                         if (IS_BOT_CLIENT(for_whom))
460                         {
461                                 if (TeamBalance_IsTeamAllowedInternal(balance, 1))
462                                 {
463                                         TeamBalance_BanTeamsExcept(balance, 1);
464                                 }
465                                 else if (TeamBalance_IsTeamAllowedInternal(balance, 2))
466                                 {
467                                         TeamBalance_BanTeamsExcept(balance, 2);
468                                 }
469                                 else
470                                 {
471                                         TeamBalance_BanTeamsExcept(balance, 3);
472                                 }
473                                 // no further cases, we know at least 2 teams exist
474                         }
475                         else
476                         {
477                                 if (TeamBalance_IsTeamAllowedInternal(balance, 4))
478                                 {
479                                         TeamBalance_BanTeamsExcept(balance, 4);
480                                 }
481                                 else if (TeamBalance_IsTeamAllowedInternal(balance, 3))
482                                 {
483                                         TeamBalance_BanTeamsExcept(balance, 3);
484                                 }
485                                 else
486                                 {
487                                         TeamBalance_BanTeamsExcept(balance, 2);
488                                 }
489                                 // no further cases, bots have one of the teams
490                         }
491                 }
492         }
493
494         if (!for_whom)
495         {
496                 balance.m_team_balance_state = TEAM_BALANCE_TEAMS_CHECKED;
497                 return balance;
498         }
499
500         // if player has a forced team, ONLY allow that one
501         for (int i = 1; i <= NUM_TEAMS; ++i)
502         {
503                 if (for_whom.team_forced == i &&
504                         TeamBalance_IsTeamAllowedInternal(balance, i))
505                 {
506                         TeamBalance_BanTeamsExcept(balance, i);
507                 }
508                 break;
509         }
510         balance.m_team_balance_state = TEAM_BALANCE_TEAMS_CHECKED;
511         return balance;
512 }
513
514 void TeamBalance_Destroy(entity balance)
515 {
516         if (balance == NULL)
517         {
518                 return;
519         }
520         for (int i = 0; i < NUM_TEAMS; ++i)
521         {
522                 delete(balance.(m_team_balance_team[i]));
523         }
524         delete(balance);
525 }
526
527 int TeamBalance_GetAllowedTeams(entity balance)
528 {
529         if (balance == NULL)
530         {
531                 LOG_FATAL("TeamBalance_GetAllowedTeams: Team balance entity is NULL.");
532         }
533         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
534         {
535                 LOG_FATAL("TeamBalance_GetAllowedTeams: "
536                         "Team balance entity is not initialized.");
537         }
538         int result = 0;
539         for (int i = 1; i <= NUM_TEAMS; ++i)
540         {
541                 if (TeamBalance_IsTeamAllowedInternal(balance, i))
542                 {
543                         result |= Team_IndexToBit(i);
544                 }
545         }
546         return result;
547 }
548
549 bool TeamBalance_IsTeamAllowed(entity balance, int index)
550 {
551         if (balance == NULL)
552         {
553                 LOG_FATAL("TeamBalance_IsTeamAllowed: Team balance entity is NULL.");
554         }
555         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
556         {
557                 LOG_FATAL("TeamBalance_IsTeamAllowed: "
558                         "Team balance entity is not initialized.");
559         }
560         if (!Team_IsValidIndex(index))
561         {
562                 LOG_FATALF("TeamBalance_IsTeamAllowed: Team index is invalid: %f",
563                         index);
564         }
565         return TeamBalance_IsTeamAllowedInternal(balance, index);
566 }
567
568 void TeamBalance_GetTeamCounts(entity balance, entity ignore)
569 {
570         if (balance == NULL)
571         {
572                 LOG_FATAL("TeamBalance_GetTeamCounts: Team balance entity is NULL.");
573         }
574         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
575         {
576                 LOG_FATAL("TeamBalance_GetTeamCounts: "
577                         "Team balance entity is not initialized.");
578         }
579         if (MUTATOR_CALLHOOK(TeamBalance_GetTeamCounts) == true)
580         {
581                 // Mutator has overriden the configuration.
582                 for (int i = 1; i <= NUM_TEAMS; ++i)
583                 {
584                         entity team_ent = TeamBalance_GetTeamFromIndex(balance, i);
585                         if (TeamBalanceTeam_IsAllowed(team_ent))
586                         {
587                                 MUTATOR_CALLHOOK(TeamBalance_GetTeamCount, i, ignore);
588                                 team_ent.m_num_players = M_ARGV(2, float);
589                                 team_ent.m_num_bots = M_ARGV(3, float);
590                         }
591                 }
592         }
593         else
594         {
595                 // Manually count all players.
596                 FOREACH_CLIENT(true,
597                 {
598                         if (it == ignore)
599                         {
600                                 continue;
601                         }
602                         int team_num;
603                         // TODO: Reconsider when the player is truly on the team.
604                         if (IS_CLIENT(it) || (it.caplayer))
605                         {
606                                 team_num = it.team;
607                         }
608                         else if (Player_HasRealForcedTeam(it))
609                         {
610                                 // Do we really need this? Probably not.
611                                 team_num = Team_IndexToTeam(it.team_forced); // reserve the spot
612                         }
613                         else
614                         {
615                                 continue;
616                         }
617                         if (!Team_IsValidTeam(team_num))
618                         {
619                                 continue;
620                         }
621                         entity team_ent = TeamBalance_GetTeam(balance, team_num);
622                         if (!TeamBalanceTeam_IsAllowed(team_ent))
623                         {
624                                 continue;
625                         }
626                         ++team_ent.m_num_players;
627                         if (IS_BOT_CLIENT(it))
628                         {
629                                 ++team_ent.m_num_bots;
630                         }
631                 });
632         }
633
634         // if the player who has a forced team has not joined yet, reserve the spot
635         if (autocvar_g_campaign)
636         {
637                 if (Team_IsValidIndex(autocvar_g_campaign_forceteam))
638                 {
639                         entity team_ent = TeamBalance_GetTeamFromIndex(balance,
640                                 autocvar_g_campaign_forceteam);
641                         if (team_ent.m_num_players == team_ent.m_num_bots)
642                         {
643                                 ++team_ent.m_num_players;
644                         }
645                 }
646         }
647         balance.m_team_balance_state = TEAM_BALANCE_TEAM_COUNTS_FILLED;
648 }
649
650 int TeamBalance_GetNumberOfPlayers(entity balance, int index)
651 {
652         if (balance == NULL)
653         {
654                 LOG_FATAL("TeamBalance_GetNumberOfPlayers: "
655                         "Team balance entity is NULL.");
656         }
657         if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
658         {
659                 LOG_FATAL("TeamBalance_GetNumberOfPlayers: "
660                         "TeamBalance_GetTeamCounts has not been called.");
661         }
662         if (!Team_IsValidIndex(index))
663         {
664                 LOG_FATALF("TeamBalance_GetNumberOfPlayers: Team index is invalid: %f",
665                         index);
666         }
667         return balance.m_team_balance_team[index - 1].m_num_players;
668 }
669
670 int TeamBalance_FindBestTeam(entity balance, entity player, bool ignore_player)
671 {
672         if (balance == NULL)
673         {
674                 LOG_FATAL("TeamBalance_FindBestTeam: Team balance entity is NULL.");
675         }
676         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
677         {
678                 LOG_FATAL("TeamBalance_FindBestTeam: "
679                         "Team balance entity is not initialized.");
680         }
681         // count how many players are in each team
682         if (ignore_player)
683         {
684                 TeamBalance_GetTeamCounts(balance, player);
685         }
686         else
687         {
688                 TeamBalance_GetTeamCounts(balance, NULL);
689         }
690         int team_bits = TeamBalance_FindBestTeams(balance, player, true);
691         if (team_bits == 0)
692         {
693                 LOG_FATALF("TeamBalance_FindBestTeam: No teams available for %s\n",
694                         MapInfo_Type_ToString(MapInfo_CurrentGametype()));
695         }
696         RandomSelection_Init();
697         for (int i = 1; i <= NUM_TEAMS; ++i)
698         {
699                 if (team_bits & Team_IndexToBit(i))
700                 {
701                         RandomSelection_AddFloat(i, 1, 1);
702                 }
703         }
704         return RandomSelection_chosen_float;
705 }
706
707 int TeamBalance_FindBestTeams(entity balance, entity player, bool use_score)
708 {
709         if (balance == NULL)
710         {
711                 LOG_FATAL("TeamBalance_FindBestTeams: Team balance entity is NULL.");
712         }
713         if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
714         {
715                 LOG_FATAL("TeamBalance_FindBestTeams: "
716                         "TeamBalance_GetTeamCounts has not been called.");
717         }
718         if (MUTATOR_CALLHOOK(TeamBalance_FindBestTeams, player) == true)
719         {
720                 return M_ARGV(1, float);
721         }
722         int team_bits = 0;
723         int previous_team = 0;
724         for (int i = 1; i <= NUM_TEAMS; ++i)
725         {
726                 if (!TeamBalance_IsTeamAllowedInternal(balance, i))
727                 {
728                         continue;
729                 }
730                 if (previous_team == 0)
731                 {
732                         team_bits = Team_IndexToBit(i);
733                         previous_team = i;
734                         continue;
735                 }
736                 int compare = TeamBalance_CompareTeams(balance, i, previous_team,
737                         player, use_score);
738                 if (compare == TEAMS_COMPARE_LESS)
739                 {
740                         team_bits = Team_IndexToBit(i);
741                         previous_team = i;
742                         continue;
743                 }
744                 if (compare == TEAMS_COMPARE_EQUAL)
745                 {
746                         team_bits |= Team_IndexToBit(i);
747                         previous_team = i;
748                 }
749         }
750         return team_bits;
751 }
752
753 void TeamBalance_JoinBestTeam(entity this)
754 {
755         //PrintToChatAll(sprintf("JoinBestTeam: %s", this.netname));
756         if (!teamplay)
757         {
758                 return;
759         }
760         if (this.bot_forced_team)
761         {
762                 return;
763         }
764         entity balance = TeamBalance_CheckAllowedTeams(this);
765         if (Player_HasRealForcedTeam(this))
766         {
767                 int forced_team_index = this.team_forced;
768                 bool is_team_allowed = TeamBalance_IsTeamAllowedInternal(balance,
769                         forced_team_index);
770                 TeamBalance_Destroy(balance);
771                 if (!is_team_allowed)
772                 {
773                         return;
774                 }
775                 if (!SetPlayerTeam(this, forced_team_index, TEAM_CHANGE_AUTO))
776                 {
777                         return;
778                 }
779                 if (!IS_BOT_CLIENT(this))
780                 {
781                         TeamBalance_AutoBalanceBots();
782                 }
783                 return;
784         }
785         int best_team_index = TeamBalance_FindBestTeam(balance, this, true);
786         TeamBalance_Destroy(balance);
787         PlayerScore_Clear(this);
788         if (!SetPlayerTeam(this, best_team_index, TEAM_CHANGE_AUTO))
789         {
790                 return;
791         }
792         if (!IS_BOT_CLIENT(this))
793         {
794                 TeamBalance_AutoBalanceBots();
795         }
796 }
797
798 int TeamBalance_CompareTeams(entity balance, int team_index_a, int team_index_b,
799         entity player, bool use_score)
800 {
801         if (balance == NULL)
802         {
803                 LOG_FATAL("TeamBalance_CompareTeams: Team balance entity is NULL.");
804         }
805         if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
806         {
807                 LOG_FATAL("TeamBalance_CompareTeams: "
808                         "TeamBalance_GetTeamCounts has not been called.");
809         }
810         if (!Team_IsValidIndex(team_index_a))
811         {
812                 LOG_FATALF("TeamBalance_CompareTeams: team_index_a is invalid: %f",
813                         team_index_a);
814         }
815         if (!Team_IsValidIndex(team_index_b))
816         {
817                 LOG_FATALF("TeamBalance_CompareTeams: team_index_b is invalid: %f",
818                         team_index_b);
819         }
820         if (team_index_a == team_index_b)
821         {
822                 return TEAMS_COMPARE_EQUAL;
823         }
824         entity team_a = TeamBalance_GetTeamFromIndex(balance, team_index_a);
825         entity team_b = TeamBalance_GetTeamFromIndex(balance, team_index_b);
826         return TeamBalance_CompareTeamsInternal(team_a, team_b, player, use_score);
827 }
828
829 void TeamBalance_AutoBalanceBots()
830 {
831         if (!autocvar_g_balance_teams ||
832                 !autocvar_g_balance_teams_prevent_imbalance)
833         {
834                 return;
835         }
836         //PrintToChatAll("TeamBalance_AutoBalanceBots");
837         entity balance = TeamBalance_CheckAllowedTeams(NULL);
838         TeamBalance_GetTeamCounts(balance, NULL);
839         int smallest_team_index = 0;
840         int smallest_team_player_count = 0;
841         int largest_team_index = 0;
842         int largest_team_player_count = 0;
843         for (int i = 1; i <= NUM_TEAMS; ++i)
844         {
845                 entity team_ = TeamBalance_GetTeamFromIndex(balance, i);
846                 if (!TeamBalanceTeam_IsAllowed(team_))
847                 {
848                         continue;
849                 }
850                 int player_count = TeamBalanceTeam_GetNumberOfPlayers(team_);
851                 if (smallest_team_index == 0)
852                 {
853                         smallest_team_index = i;
854                         smallest_team_player_count = player_count;
855                 }
856                 else if (player_count < smallest_team_player_count)
857                 {
858                         smallest_team_index = i;
859                         smallest_team_player_count = player_count;
860                 }
861                 if (largest_team_index == 0)
862                 {
863                         largest_team_index = i;
864                         largest_team_player_count = player_count;
865                 }
866                 else if (player_count > largest_team_player_count)
867                 {
868                         largest_team_index = i;
869                         largest_team_player_count = player_count;
870                 }
871         }
872         TeamBalance_Destroy(balance);
873         //PrintToChatAll(sprintf("Smallest team: %f", smallest_team_index));
874         //PrintToChatAll(sprintf("Largest team: %f", largest_team_index));
875         //PrintToChatAll(sprintf("Smallest team players: %f", smallest_team_player_count));
876         //PrintToChatAll(sprintf("Largest team players: %f", largest_team_player_count));
877         if (smallest_team_index == largest_team_index)
878         {
879                 return;
880         }
881         if (largest_team_player_count - smallest_team_player_count < 2)
882         {
883                 return;
884         }
885         entity lowest_bot = TeamBalance_GetPlayerForTeamSwitch(largest_team_index,
886                 smallest_team_index, true);
887         if (lowest_bot == NULL)
888         {
889                 //PrintToChatAll("No bot found");
890                 return;
891         }
892         if (!Player_SetTeamIndex(lowest_bot, smallest_team_index))
893         {
894                 return;
895         }
896         KillPlayerForTeamChange(lowest_bot);
897 }
898
899 entity TeamBalance_GetPlayerForTeamSwitch(int source_team_index,
900         int destination_team_index, bool is_bot)
901 {
902         if (MUTATOR_CALLHOOK(TeamBalance_GetPlayerForTeamSwitch, source_team_index,
903                 destination_team_index, is_bot))
904         {
905                 return M_ARGV(3, entity);
906         }
907         entity lowest_player = NULL;
908         float lowest_score = FLOAT_MAX;
909         FOREACH_CLIENT(Entity_GetTeamIndex(it) == source_team_index,
910         {
911                 if (IS_BOT_CLIENT(it) != is_bot)
912                 {
913                         continue;
914                 }
915                 float temp_score = PlayerScore_Get(it, SP_SCORE);
916                 if (temp_score >= lowest_score)
917                 {
918                         continue;
919                 }
920                 //PrintToChatAll(sprintf(
921                 //      "Found %s with lowest score, checking allowed teams", it.netname));
922                 entity balance = TeamBalance_CheckAllowedTeams(it);
923                 if (TeamBalance_IsTeamAllowed(balance, source_team_index))
924                 {
925                         //PrintToChatAll("Allowed");
926                         lowest_player = it;
927                         lowest_score = temp_score;
928                 }
929                 else
930                 {
931                         //PrintToChatAll("Not allowed");
932                 }
933                 TeamBalance_Destroy(balance);
934         });
935         return lowest_player;
936 }
937
938 bool TeamBalance_IsTeamAllowedInternal(entity balance, int index)
939 {
940         return balance.m_team_balance_team[index - 1].m_num_players !=
941                 TEAM_NOT_ALLOWED;
942 }
943
944 void TeamBalance_BanTeamsExcept(entity balance, int index)
945 {
946         for (int i = 1; i <= NUM_TEAMS; ++i)
947         {
948                 if (i != index)
949                 {
950                         balance.m_team_balance_team[i - 1].m_num_players = TEAM_NOT_ALLOWED;
951                 }
952         }
953 }
954
955 entity TeamBalance_GetTeamFromIndex(entity balance, int index)
956 {
957         if (!Team_IsValidIndex(index))
958         {
959                 LOG_FATALF("TeamBalance_GetTeamFromIndex: Index is invalid: %f", index);
960         }
961         return balance.m_team_balance_team[index - 1];
962 }
963
964 entity TeamBalance_GetTeam(entity balance, int team_num)
965 {
966         return TeamBalance_GetTeamFromIndex(balance, Team_TeamToIndex(team_num));
967 }
968
969 bool TeamBalanceTeam_IsAllowed(entity team_ent)
970 {
971         return team_ent.m_num_players != TEAM_NOT_ALLOWED;
972 }
973
974 int TeamBalanceTeam_GetNumberOfPlayers(entity team_ent)
975 {
976         return team_ent.m_num_players;
977 }
978
979 int TeamBalanceTeam_GetNumberOfBots(entity team_ent)
980 {
981         return team_ent.m_num_bots;
982 }
983
984 int TeamBalance_CompareTeamsInternal(entity team_a, entity team_b,
985         entity player, bool use_score)
986 {
987         if (team_a == team_b)
988         {
989                 return TEAMS_COMPARE_EQUAL;
990         }
991         if (!TeamBalanceTeam_IsAllowed(team_a) ||
992                 !TeamBalanceTeam_IsAllowed(team_b))
993         {
994                 return TEAMS_COMPARE_INVALID;
995         }
996         int num_players_team_a = team_a.m_num_players;
997         int num_players_team_b = team_b.m_num_players;
998         if (IS_REAL_CLIENT(player) && bots_would_leave)
999         {
1000                 num_players_team_a -= team_a.m_num_bots;
1001                 num_players_team_b -= team_b.m_num_bots;
1002         }
1003         if (num_players_team_a < num_players_team_b)
1004         {
1005                 return TEAMS_COMPARE_LESS;
1006         }
1007         if (num_players_team_a > num_players_team_b)
1008         {
1009                 return TEAMS_COMPARE_GREATER;
1010         }
1011         if (!use_score)
1012         {
1013                 return TEAMS_COMPARE_EQUAL;
1014         }
1015         if (team_a.m_team_score < team_b.m_team_score)
1016         {
1017                 return TEAMS_COMPARE_LESS;
1018         }
1019         if (team_a.m_team_score > team_b.m_team_score)
1020         {
1021                 return TEAMS_COMPARE_GREATER;
1022         }
1023         return TEAMS_COMPARE_EQUAL;
1024 }
1025
1026 // Called when the player connects or when they change their color with "color"
1027 // command.
1028 void SV_ChangeTeam(entity this, float _color)
1029 {
1030         //PrintToChatAll(sprintf("SV_ChangeTeam: %s, %f", this.netname, _color));
1031
1032         // in normal deathmatch we can just apply the color and we're done
1033         if(!teamplay)
1034                 SetPlayerColors(this, _color);
1035
1036         if(!IS_CLIENT(this))
1037         {
1038                 //PrintToChatAll("Not a client yet");
1039                 // since this is an engine function, and gamecode doesn't have any calls earlier than this, do the connecting message here
1040                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CONNECTING, this.netname);
1041                 return;
1042         }
1043
1044         if(!teamplay)
1045                 return;
1046
1047         int source_color, destination_color;
1048         int source_team_index, destination_team_index;
1049
1050         source_color = this.clientcolors & 0x0F;
1051         destination_color = _color & 0x0F;
1052
1053         source_team_index = Team_TeamToIndex(source_color + 1);
1054         destination_team_index = Team_TeamToIndex(destination_color + 1);
1055
1056         if (destination_team_index == -1)
1057         {
1058                 return;
1059         }
1060
1061         entity balance = TeamBalance_CheckAllowedTeams(this);
1062
1063         if (destination_team_index == 1 && !TeamBalance_IsTeamAllowedInternal(
1064                 balance, 1))
1065         {
1066                 destination_team_index = 4;
1067         }
1068         if (destination_team_index == 4 && !TeamBalance_IsTeamAllowedInternal(
1069                 balance, 4))
1070         {
1071                 destination_team_index = 3;
1072         }
1073         if (destination_team_index == 3 && !TeamBalance_IsTeamAllowedInternal(
1074                 balance, 3))
1075         {
1076                 destination_team_index = 2;
1077         }
1078         if (destination_team_index == 2 && !TeamBalance_IsTeamAllowedInternal(
1079                 balance, 2))
1080         {
1081                 destination_team_index = 1;
1082         }
1083
1084         // not changing teams
1085         if (source_color == destination_color)
1086         {
1087                 SetPlayerTeam(this, destination_team_index, TEAM_CHANGE_MANUAL);
1088                 TeamBalance_Destroy(balance);
1089                 TeamBalance_AutoBalanceBots(); // Hack
1090                 return;
1091         }
1092
1093         if((autocvar_g_campaign) || (autocvar_g_changeteam_banned && CS(this).wasplayer)) {
1094                 Send_Notification(NOTIF_ONE, this, MSG_INFO, INFO_TEAMCHANGE_NOTALLOWED);
1095                 return; // changing teams is not allowed
1096         }
1097
1098         // autocvar_g_balance_teams_prevent_imbalance only makes sense if autocvar_g_balance_teams is on, as it makes the team selection dialog pointless
1099         if (autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance)
1100         {
1101                 TeamBalance_GetTeamCounts(balance, this);
1102                 if ((Team_IndexToBit(destination_team_index) &
1103                         TeamBalance_FindBestTeams(balance, this, false)) == 0)
1104                 {
1105                         Send_Notification(NOTIF_ONE, this, MSG_INFO, INFO_TEAMCHANGE_LARGERTEAM);
1106                         TeamBalance_Destroy(balance);
1107                         return;
1108                 }
1109         }
1110         TeamBalance_Destroy(balance);
1111         if (IS_PLAYER(this) && source_team_index != destination_team_index)
1112         {
1113                 // reduce frags during a team change
1114                 PlayerScore_Clear(this);
1115         }
1116         if (!SetPlayerTeam(this, destination_team_index, TEAM_CHANGE_MANUAL))
1117         {
1118                 return;
1119         }
1120         TeamBalance_AutoBalanceBots();
1121 }