]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/teamplay.qc
Teamplay: Added proper messages when joining spectator team.
[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                 if (team_index != -1)
218                 {
219                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(
220                                 player.team, INFO_JOIN_PLAY_TEAM), player.netname);
221                 }
222                 else
223                 {
224                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_SPECTATE,
225                                 player.netname);
226                 }
227                 KillPlayerForTeamChange(player);
228         }
229         return true;
230 }
231
232 bool MoveToTeam(entity client, int team_index, int type)
233 {
234         //PrintToChatAll(sprintf("MoveToTeam: %s, %f", client.netname, team_index));
235         int lockteams_backup = lockteams;  // backup any team lock
236         lockteams = 0;  // disable locked teams
237         PlayerScore_Clear(client);
238         if (!SetPlayerTeam(client, team_index, type))
239         {
240                 lockteams = lockteams_backup;  // restore the team lock
241                 return false;
242         }
243         lockteams = lockteams_backup;  // restore the team lock
244         return true;
245 }
246
247 void KillPlayerForTeamChange(entity player)
248 {
249         if (IS_DEAD(player))
250         {
251                 return;
252         }
253         if (MUTATOR_CALLHOOK(Player_ChangeTeamKill, player) == true)
254         {
255                 return;
256         }
257         Damage(player, player, player, 100000, DEATH_TEAMCHANGE.m_id, DMG_NOWEP,
258                 player.origin, '0 0 0');
259 }
260
261 void LogTeamchange(float player_id, float team_number, int type)
262 {
263         if(!autocvar_sv_eventlog)
264                 return;
265
266         if(player_id < 1)
267                 return;
268
269         GameLogEcho(strcat(":team:", ftos(player_id), ":", ftos(team_number), ":", ftos(type)));
270 }
271
272 bool Player_HasRealForcedTeam(entity player)
273 {
274         return player.team_forced > TEAM_FORCE_DEFAULT;
275 }
276
277 int Player_GetForcedTeamIndex(entity player)
278 {
279         return player.team_forced;
280 }
281
282 void Player_SetForcedTeamIndex(entity player, int team_index)
283 {
284         switch (team_index)
285         {
286                 case TEAM_FORCE_SPECTATOR:
287                 case TEAM_FORCE_DEFAULT:
288                 {
289                         player.team_forced = team_index;
290                         break;
291                 }
292                 default:
293                 {
294                         if (!Team_IsValidIndex(team_index))
295                         {
296                                 LOG_FATAL("Player_SetForcedTeamIndex: Invalid team index.");
297                         }
298                         else
299                         {
300                                 player.team_forced = team_index;
301                                 break;
302                         }
303                 }
304         }
305 }
306
307 void Player_DetermineForcedTeam(entity player)
308 {
309         if (autocvar_g_campaign)
310         {
311                 if (IS_REAL_CLIENT(player)) // only players, not bots
312                 {
313                         if (Team_IsValidIndex(autocvar_g_campaign_forceteam))
314                         {
315                                 player.team_forced = autocvar_g_campaign_forceteam;
316                         }
317                         else
318                         {
319                                 player.team_forced = TEAM_FORCE_DEFAULT;
320                         }
321                 }
322         }
323         else if (PlayerInList(player, autocvar_g_forced_team_red))
324         {
325                 player.team_forced = 1;
326         }
327         else if (PlayerInList(player, autocvar_g_forced_team_blue))
328         {
329                 player.team_forced = 2;
330         }
331         else if (PlayerInList(player, autocvar_g_forced_team_yellow))
332         {
333                 player.team_forced = 3;
334         }
335         else if (PlayerInList(player, autocvar_g_forced_team_pink))
336         {
337                 player.team_forced = 4;
338         }
339         else
340         {
341                 switch (autocvar_g_forced_team_otherwise)
342                 {
343                         case "red":
344                         {
345                                 player.team_forced = 1;
346                                 break;
347                         }
348                         case "blue":
349                         {
350                                 player.team_forced = 2;
351                                 break;
352                         }
353                         case "yellow":
354                         {
355                                 player.team_forced = 3;
356                                 break;
357                         }
358                         case "pink":
359                         {
360                                 player.team_forced = 4;
361                                 break;
362                         }
363                         case "spectate":
364                         case "spectator":
365                         {
366                                 player.team_forced = TEAM_FORCE_SPECTATOR;
367                                 break;
368                         }
369                         default:
370                         {
371                                 player.team_forced = TEAM_FORCE_DEFAULT;
372                                 break;
373                         }
374                 }
375         }
376         if (!teamplay && Player_HasRealForcedTeam(player))
377         {
378                 player.team_forced = TEAM_FORCE_DEFAULT;
379         }
380 }
381
382 entity TeamBalance_CheckAllowedTeams(entity for_whom)
383 {
384         entity balance = spawn();
385         for (int i = 0; i < NUM_TEAMS; ++i)
386         {
387                 entity team_ent = balance.m_team_balance_team[i] = spawn();
388                 team_ent.m_team_score = g_team_entities[i].m_team_score;
389                 team_ent.m_num_players = TEAM_NOT_ALLOWED;
390                 team_ent.m_num_bots = 0;
391         }
392         setthink(balance, TeamBalance_Destroy);
393         
394         int teams_mask = 0;     
395         string teament_name = string_null;
396         bool mutator_returnvalue = MUTATOR_CALLHOOK(TeamBalance_CheckAllowedTeams,
397                 teams_mask, teament_name, for_whom);
398         teams_mask = M_ARGV(0, float);
399         teament_name = M_ARGV(1, string);
400         if (mutator_returnvalue)
401         {
402                 for (int i = 0; i < NUM_TEAMS; ++i)
403                 {
404                         if (teams_mask & BIT(i))
405                         {
406                                 balance.m_team_balance_team[i].m_num_players = 0;
407                         }
408                 }
409         }
410
411         if (teament_name)
412         {
413                 entity head = find(NULL, classname, teament_name);
414                 while (head)
415                 {
416                         if (Team_IsValidTeam(head.team))
417                         {
418                                 TeamBalance_GetTeam(balance, head.team).m_num_players = 0;
419                         }
420                         head = find(head, classname, teament_name);
421                 }
422         }
423
424         // TODO: Balance quantity of bots across > 2 teams when bot_vs_human is set (and remove next line)
425         if (AvailableTeams() == 2)
426         if (autocvar_bot_vs_human && for_whom)
427         {
428                 if (autocvar_bot_vs_human > 0)
429                 {
430                         // find last team available
431                         if (IS_BOT_CLIENT(for_whom))
432                         {
433                                 if (TeamBalance_IsTeamAllowedInternal(balance, 4))
434                                 {
435                                         TeamBalance_BanTeamsExcept(balance, 4);
436                                 }
437                                 else if (TeamBalance_IsTeamAllowedInternal(balance, 3))
438                                 {
439                                         TeamBalance_BanTeamsExcept(balance, 3);
440                                 }
441                                 else
442                                 {
443                                         TeamBalance_BanTeamsExcept(balance, 2);
444                                 }
445                                 // no further cases, we know at least 2 teams exist
446                         }
447                         else
448                         {
449                                 if (TeamBalance_IsTeamAllowedInternal(balance, 1))
450                                 {
451                                         TeamBalance_BanTeamsExcept(balance, 1);
452                                 }
453                                 else if (TeamBalance_IsTeamAllowedInternal(balance, 2))
454                                 {
455                                         TeamBalance_BanTeamsExcept(balance, 2);
456                                 }
457                                 else
458                                 {
459                                         TeamBalance_BanTeamsExcept(balance, 3);
460                                 }
461                                 // no further cases, bots have one of the teams
462                         }
463                 }
464                 else
465                 {
466                         // find first team available
467                         if (IS_BOT_CLIENT(for_whom))
468                         {
469                                 if (TeamBalance_IsTeamAllowedInternal(balance, 1))
470                                 {
471                                         TeamBalance_BanTeamsExcept(balance, 1);
472                                 }
473                                 else if (TeamBalance_IsTeamAllowedInternal(balance, 2))
474                                 {
475                                         TeamBalance_BanTeamsExcept(balance, 2);
476                                 }
477                                 else
478                                 {
479                                         TeamBalance_BanTeamsExcept(balance, 3);
480                                 }
481                                 // no further cases, we know at least 2 teams exist
482                         }
483                         else
484                         {
485                                 if (TeamBalance_IsTeamAllowedInternal(balance, 4))
486                                 {
487                                         TeamBalance_BanTeamsExcept(balance, 4);
488                                 }
489                                 else if (TeamBalance_IsTeamAllowedInternal(balance, 3))
490                                 {
491                                         TeamBalance_BanTeamsExcept(balance, 3);
492                                 }
493                                 else
494                                 {
495                                         TeamBalance_BanTeamsExcept(balance, 2);
496                                 }
497                                 // no further cases, bots have one of the teams
498                         }
499                 }
500         }
501
502         if (!for_whom)
503         {
504                 balance.m_team_balance_state = TEAM_BALANCE_TEAMS_CHECKED;
505                 return balance;
506         }
507
508         // if player has a forced team, ONLY allow that one
509         for (int i = 1; i <= NUM_TEAMS; ++i)
510         {
511                 if (for_whom.team_forced == i &&
512                         TeamBalance_IsTeamAllowedInternal(balance, i))
513                 {
514                         TeamBalance_BanTeamsExcept(balance, i);
515                 }
516                 break;
517         }
518         balance.m_team_balance_state = TEAM_BALANCE_TEAMS_CHECKED;
519         return balance;
520 }
521
522 void TeamBalance_Destroy(entity balance)
523 {
524         if (balance == NULL)
525         {
526                 return;
527         }
528         for (int i = 0; i < NUM_TEAMS; ++i)
529         {
530                 delete(balance.(m_team_balance_team[i]));
531         }
532         delete(balance);
533 }
534
535 int TeamBalance_GetAllowedTeams(entity balance)
536 {
537         if (balance == NULL)
538         {
539                 LOG_FATAL("TeamBalance_GetAllowedTeams: Team balance entity is NULL.");
540         }
541         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
542         {
543                 LOG_FATAL("TeamBalance_GetAllowedTeams: "
544                         "Team balance entity is not initialized.");
545         }
546         int result = 0;
547         for (int i = 1; i <= NUM_TEAMS; ++i)
548         {
549                 if (TeamBalance_IsTeamAllowedInternal(balance, i))
550                 {
551                         result |= Team_IndexToBit(i);
552                 }
553         }
554         return result;
555 }
556
557 bool TeamBalance_IsTeamAllowed(entity balance, int index)
558 {
559         if (balance == NULL)
560         {
561                 LOG_FATAL("TeamBalance_IsTeamAllowed: Team balance entity is NULL.");
562         }
563         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
564         {
565                 LOG_FATAL("TeamBalance_IsTeamAllowed: "
566                         "Team balance entity is not initialized.");
567         }
568         if (!Team_IsValidIndex(index))
569         {
570                 LOG_FATALF("TeamBalance_IsTeamAllowed: Team index is invalid: %f",
571                         index);
572         }
573         return TeamBalance_IsTeamAllowedInternal(balance, index);
574 }
575
576 void TeamBalance_GetTeamCounts(entity balance, entity ignore)
577 {
578         if (balance == NULL)
579         {
580                 LOG_FATAL("TeamBalance_GetTeamCounts: Team balance entity is NULL.");
581         }
582         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
583         {
584                 LOG_FATAL("TeamBalance_GetTeamCounts: "
585                         "Team balance entity is not initialized.");
586         }
587         if (MUTATOR_CALLHOOK(TeamBalance_GetTeamCounts) == true)
588         {
589                 // Mutator has overriden the configuration.
590                 for (int i = 1; i <= NUM_TEAMS; ++i)
591                 {
592                         entity team_ent = TeamBalance_GetTeamFromIndex(balance, i);
593                         if (TeamBalanceTeam_IsAllowed(team_ent))
594                         {
595                                 MUTATOR_CALLHOOK(TeamBalance_GetTeamCount, i, ignore);
596                                 team_ent.m_num_players = M_ARGV(2, float);
597                                 team_ent.m_num_bots = M_ARGV(3, float);
598                         }
599                 }
600         }
601         else
602         {
603                 // Manually count all players.
604                 FOREACH_CLIENT(true,
605                 {
606                         if (it == ignore)
607                         {
608                                 continue;
609                         }
610                         int team_num;
611                         // TODO: Reconsider when the player is truly on the team.
612                         if (IS_CLIENT(it) || (it.caplayer))
613                         {
614                                 team_num = it.team;
615                         }
616                         else if (Player_HasRealForcedTeam(it))
617                         {
618                                 // Do we really need this? Probably not.
619                                 team_num = Team_IndexToTeam(it.team_forced); // reserve the spot
620                         }
621                         else
622                         {
623                                 continue;
624                         }
625                         if (!Team_IsValidTeam(team_num))
626                         {
627                                 continue;
628                         }
629                         entity team_ent = TeamBalance_GetTeam(balance, team_num);
630                         if (!TeamBalanceTeam_IsAllowed(team_ent))
631                         {
632                                 continue;
633                         }
634                         ++team_ent.m_num_players;
635                         if (IS_BOT_CLIENT(it))
636                         {
637                                 ++team_ent.m_num_bots;
638                         }
639                 });
640         }
641
642         // if the player who has a forced team has not joined yet, reserve the spot
643         if (autocvar_g_campaign)
644         {
645                 if (Team_IsValidIndex(autocvar_g_campaign_forceteam))
646                 {
647                         entity team_ent = TeamBalance_GetTeamFromIndex(balance,
648                                 autocvar_g_campaign_forceteam);
649                         if (team_ent.m_num_players == team_ent.m_num_bots)
650                         {
651                                 ++team_ent.m_num_players;
652                         }
653                 }
654         }
655         balance.m_team_balance_state = TEAM_BALANCE_TEAM_COUNTS_FILLED;
656 }
657
658 int TeamBalance_GetNumberOfPlayers(entity balance, int index)
659 {
660         if (balance == NULL)
661         {
662                 LOG_FATAL("TeamBalance_GetNumberOfPlayers: "
663                         "Team balance entity is NULL.");
664         }
665         if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
666         {
667                 LOG_FATAL("TeamBalance_GetNumberOfPlayers: "
668                         "TeamBalance_GetTeamCounts has not been called.");
669         }
670         if (!Team_IsValidIndex(index))
671         {
672                 LOG_FATALF("TeamBalance_GetNumberOfPlayers: Team index is invalid: %f",
673                         index);
674         }
675         return balance.m_team_balance_team[index - 1].m_num_players;
676 }
677
678 int TeamBalance_FindBestTeam(entity balance, entity player, bool ignore_player)
679 {
680         if (balance == NULL)
681         {
682                 LOG_FATAL("TeamBalance_FindBestTeam: Team balance entity is NULL.");
683         }
684         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
685         {
686                 LOG_FATAL("TeamBalance_FindBestTeam: "
687                         "Team balance entity is not initialized.");
688         }
689         // count how many players are in each team
690         if (ignore_player)
691         {
692                 TeamBalance_GetTeamCounts(balance, player);
693         }
694         else
695         {
696                 TeamBalance_GetTeamCounts(balance, NULL);
697         }
698         int team_bits = TeamBalance_FindBestTeams(balance, player, true);
699         if (team_bits == 0)
700         {
701                 LOG_FATALF("TeamBalance_FindBestTeam: No teams available for %s\n",
702                         MapInfo_Type_ToString(MapInfo_CurrentGametype()));
703         }
704         RandomSelection_Init();
705         for (int i = 1; i <= NUM_TEAMS; ++i)
706         {
707                 if (team_bits & Team_IndexToBit(i))
708                 {
709                         RandomSelection_AddFloat(i, 1, 1);
710                 }
711         }
712         return RandomSelection_chosen_float;
713 }
714
715 int TeamBalance_FindBestTeams(entity balance, entity player, bool use_score)
716 {
717         if (balance == NULL)
718         {
719                 LOG_FATAL("TeamBalance_FindBestTeams: Team balance entity is NULL.");
720         }
721         if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
722         {
723                 LOG_FATAL("TeamBalance_FindBestTeams: "
724                         "TeamBalance_GetTeamCounts has not been called.");
725         }
726         if (MUTATOR_CALLHOOK(TeamBalance_FindBestTeams, player) == true)
727         {
728                 return M_ARGV(1, float);
729         }
730         int team_bits = 0;
731         int previous_team = 0;
732         for (int i = 1; i <= NUM_TEAMS; ++i)
733         {
734                 if (!TeamBalance_IsTeamAllowedInternal(balance, i))
735                 {
736                         continue;
737                 }
738                 if (previous_team == 0)
739                 {
740                         team_bits = Team_IndexToBit(i);
741                         previous_team = i;
742                         continue;
743                 }
744                 int compare = TeamBalance_CompareTeams(balance, i, previous_team,
745                         player, use_score);
746                 if (compare == TEAMS_COMPARE_LESS)
747                 {
748                         team_bits = Team_IndexToBit(i);
749                         previous_team = i;
750                         continue;
751                 }
752                 if (compare == TEAMS_COMPARE_EQUAL)
753                 {
754                         team_bits |= Team_IndexToBit(i);
755                         previous_team = i;
756                 }
757         }
758         return team_bits;
759 }
760
761 void TeamBalance_JoinBestTeam(entity this)
762 {
763         //PrintToChatAll(sprintf("JoinBestTeam: %s", this.netname));
764         if (!teamplay)
765         {
766                 return;
767         }
768         if (this.bot_forced_team)
769         {
770                 return;
771         }
772         entity balance = TeamBalance_CheckAllowedTeams(this);
773         if (Player_HasRealForcedTeam(this))
774         {
775                 int forced_team_index = this.team_forced;
776                 bool is_team_allowed = TeamBalance_IsTeamAllowedInternal(balance,
777                         forced_team_index);
778                 TeamBalance_Destroy(balance);
779                 if (!is_team_allowed)
780                 {
781                         return;
782                 }
783                 if (!SetPlayerTeam(this, forced_team_index, TEAM_CHANGE_AUTO))
784                 {
785                         return;
786                 }
787                 if (!IS_BOT_CLIENT(this))
788                 {
789                         TeamBalance_AutoBalanceBots();
790                 }
791                 return;
792         }
793         int best_team_index = TeamBalance_FindBestTeam(balance, this, true);
794         TeamBalance_Destroy(balance);
795         PlayerScore_Clear(this);
796         if (!SetPlayerTeam(this, best_team_index, TEAM_CHANGE_AUTO))
797         {
798                 return;
799         }
800         if (!IS_BOT_CLIENT(this))
801         {
802                 TeamBalance_AutoBalanceBots();
803         }
804 }
805
806 int TeamBalance_CompareTeams(entity balance, int team_index_a, int team_index_b,
807         entity player, bool use_score)
808 {
809         if (balance == NULL)
810         {
811                 LOG_FATAL("TeamBalance_CompareTeams: Team balance entity is NULL.");
812         }
813         if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
814         {
815                 LOG_FATAL("TeamBalance_CompareTeams: "
816                         "TeamBalance_GetTeamCounts has not been called.");
817         }
818         if (!Team_IsValidIndex(team_index_a))
819         {
820                 LOG_FATALF("TeamBalance_CompareTeams: team_index_a is invalid: %f",
821                         team_index_a);
822         }
823         if (!Team_IsValidIndex(team_index_b))
824         {
825                 LOG_FATALF("TeamBalance_CompareTeams: team_index_b is invalid: %f",
826                         team_index_b);
827         }
828         if (team_index_a == team_index_b)
829         {
830                 return TEAMS_COMPARE_EQUAL;
831         }
832         entity team_a = TeamBalance_GetTeamFromIndex(balance, team_index_a);
833         entity team_b = TeamBalance_GetTeamFromIndex(balance, team_index_b);
834         return TeamBalance_CompareTeamsInternal(team_a, team_b, player, use_score);
835 }
836
837 void TeamBalance_AutoBalanceBots()
838 {
839         if (!autocvar_g_balance_teams ||
840                 !autocvar_g_balance_teams_prevent_imbalance)
841         {
842                 return;
843         }
844         //PrintToChatAll("TeamBalance_AutoBalanceBots");
845         entity balance = TeamBalance_CheckAllowedTeams(NULL);
846         TeamBalance_GetTeamCounts(balance, NULL);
847         int smallest_team_index = 0;
848         int smallest_team_player_count = 0;
849         for (int i = 1; i <= NUM_TEAMS; ++i)
850         {
851                 entity team_ = TeamBalance_GetTeamFromIndex(balance, i);
852                 if (!TeamBalanceTeam_IsAllowed(team_))
853                 {
854                         continue;
855                 }
856                 int player_count = TeamBalanceTeam_GetNumberOfPlayers(team_);
857                 if (smallest_team_index == 0)
858                 {
859                         smallest_team_index = i;
860                         smallest_team_player_count = player_count;
861                 }
862                 else if (player_count < smallest_team_player_count)
863                 {
864                         smallest_team_index = i;
865                         smallest_team_player_count = player_count;
866                 }
867         }
868         //PrintToChatAll(sprintf("Smallest team: %f", smallest_team_index));
869         //PrintToChatAll(sprintf("Smallest team players: %f", smallest_team_player_count));
870         entity switchable_bot = NULL;
871         int teams = BITS(NUM_TEAMS);
872         while (teams != 0)
873         {
874                 int largest_team_index = TeamBalance_GetLargestTeamIndex(balance,
875                         teams);
876                 if (smallest_team_index == largest_team_index)
877                 {
878                         TeamBalance_Destroy(balance);
879                         return;
880                 }
881                 entity largest_team = TeamBalance_GetTeamFromIndex(balance,
882                         largest_team_index);
883                 int largest_team_player_count = TeamBalanceTeam_GetNumberOfPlayers(
884                         largest_team);
885                 if (largest_team_player_count - smallest_team_player_count < 2)
886                 {
887                         TeamBalance_Destroy(balance);
888                         return;
889                 }
890                 //PrintToChatAll(sprintf("Largest team: %f", largest_team_index));
891                 //PrintToChatAll(sprintf("Largest team players: %f", largest_team_player_count));
892                 switchable_bot = TeamBalance_GetPlayerForTeamSwitch(largest_team_index,
893                         smallest_team_index, true);
894                 if (switchable_bot != NULL)
895                 {
896                         break;
897                 }
898                 teams &= ~Team_IndexToBit(largest_team_index);
899         }
900         TeamBalance_Destroy(balance);
901         if (switchable_bot == NULL)
902         {
903                 //PrintToChatAll("No bot found after searching through all the teams");
904                 return;
905         }
906         SetPlayerTeam(switchable_bot, smallest_team_index, TEAM_CHANGE_AUTO);
907 }
908
909 int TeamBalance_GetLargestTeamIndex(entity balance, int teams)
910 {
911         int largest_team_index = 0;
912         int largest_team_player_count = 0;
913         for (int i = 1; i <= NUM_TEAMS; ++i)
914         {
915                 if (!(Team_IndexToBit(i) & teams))
916                 {
917                         continue;
918                 }
919                 entity team_ = TeamBalance_GetTeamFromIndex(balance, i);
920                 if (!TeamBalanceTeam_IsAllowed(team_))
921                 {
922                         continue;
923                 }
924                 int player_count = TeamBalanceTeam_GetNumberOfPlayers(team_);
925                 if (largest_team_index == 0)
926                 {
927                         largest_team_index = i;
928                         largest_team_player_count = player_count;
929                 }
930                 else if (player_count > largest_team_player_count)
931                 {
932                         largest_team_index = i;
933                         largest_team_player_count = player_count;
934                 }
935         }
936         return largest_team_index;
937 }
938
939 entity TeamBalance_GetPlayerForTeamSwitch(int source_team_index,
940         int destination_team_index, bool is_bot)
941 {
942         if (MUTATOR_CALLHOOK(TeamBalance_GetPlayerForTeamSwitch, source_team_index,
943                 destination_team_index, is_bot))
944         {
945                 return M_ARGV(3, entity);
946         }
947         entity lowest_player = NULL;
948         float lowest_score = FLOAT_MAX;
949         FOREACH_CLIENT(Entity_GetTeamIndex(it) == source_team_index,
950         {
951                 if (IS_BOT_CLIENT(it) != is_bot)
952                 {
953                         continue;
954                 }
955                 float temp_score = PlayerScore_Get(it, SP_SCORE);
956                 if (temp_score >= lowest_score)
957                 {
958                         continue;
959                 }
960                 //PrintToChatAll(sprintf(
961                 //      "Found %s with lowest score, checking allowed teams", it.netname));
962                 entity balance = TeamBalance_CheckAllowedTeams(it);
963                 if (TeamBalance_IsTeamAllowed(balance, source_team_index))
964                 {
965                         //PrintToChatAll("Allowed");
966                         lowest_player = it;
967                         lowest_score = temp_score;
968                 }
969                 else
970                 {
971                         //PrintToChatAll("Not allowed");
972                 }
973                 TeamBalance_Destroy(balance);
974         });
975         return lowest_player;
976 }
977
978 bool TeamBalance_IsTeamAllowedInternal(entity balance, int index)
979 {
980         return balance.m_team_balance_team[index - 1].m_num_players !=
981                 TEAM_NOT_ALLOWED;
982 }
983
984 void TeamBalance_BanTeamsExcept(entity balance, int index)
985 {
986         for (int i = 1; i <= NUM_TEAMS; ++i)
987         {
988                 if (i != index)
989                 {
990                         balance.m_team_balance_team[i - 1].m_num_players = TEAM_NOT_ALLOWED;
991                 }
992         }
993 }
994
995 entity TeamBalance_GetTeamFromIndex(entity balance, int index)
996 {
997         if (!Team_IsValidIndex(index))
998         {
999                 LOG_FATALF("TeamBalance_GetTeamFromIndex: Index is invalid: %f", index);
1000         }
1001         return balance.m_team_balance_team[index - 1];
1002 }
1003
1004 entity TeamBalance_GetTeam(entity balance, int team_num)
1005 {
1006         return TeamBalance_GetTeamFromIndex(balance, Team_TeamToIndex(team_num));
1007 }
1008
1009 bool TeamBalanceTeam_IsAllowed(entity team_ent)
1010 {
1011         return team_ent.m_num_players != TEAM_NOT_ALLOWED;
1012 }
1013
1014 int TeamBalanceTeam_GetNumberOfPlayers(entity team_ent)
1015 {
1016         return team_ent.m_num_players;
1017 }
1018
1019 int TeamBalanceTeam_GetNumberOfBots(entity team_ent)
1020 {
1021         return team_ent.m_num_bots;
1022 }
1023
1024 int TeamBalance_CompareTeamsInternal(entity team_a, entity team_b,
1025         entity player, bool use_score)
1026 {
1027         if (team_a == team_b)
1028         {
1029                 return TEAMS_COMPARE_EQUAL;
1030         }
1031         if (!TeamBalanceTeam_IsAllowed(team_a) ||
1032                 !TeamBalanceTeam_IsAllowed(team_b))
1033         {
1034                 return TEAMS_COMPARE_INVALID;
1035         }
1036         int num_players_team_a = team_a.m_num_players;
1037         int num_players_team_b = team_b.m_num_players;
1038         if (IS_REAL_CLIENT(player) && bots_would_leave)
1039         {
1040                 num_players_team_a -= team_a.m_num_bots;
1041                 num_players_team_b -= team_b.m_num_bots;
1042         }
1043         if (num_players_team_a < num_players_team_b)
1044         {
1045                 return TEAMS_COMPARE_LESS;
1046         }
1047         if (num_players_team_a > num_players_team_b)
1048         {
1049                 return TEAMS_COMPARE_GREATER;
1050         }
1051         if (!use_score)
1052         {
1053                 return TEAMS_COMPARE_EQUAL;
1054         }
1055         if (team_a.m_team_score < team_b.m_team_score)
1056         {
1057                 return TEAMS_COMPARE_LESS;
1058         }
1059         if (team_a.m_team_score > team_b.m_team_score)
1060         {
1061                 return TEAMS_COMPARE_GREATER;
1062         }
1063         return TEAMS_COMPARE_EQUAL;
1064 }
1065
1066 // Called when the player connects or when they change their color with "color"
1067 // command.
1068 void SV_ChangeTeam(entity this, float _color)
1069 {
1070         //PrintToChatAll(sprintf("SV_ChangeTeam: %s, %f", this.netname, _color));
1071
1072         // in normal deathmatch we can just apply the color and we're done
1073         if(!teamplay)
1074                 SetPlayerColors(this, _color);
1075
1076         if(!IS_CLIENT(this))
1077         {
1078                 //PrintToChatAll("Not a client yet");
1079                 // since this is an engine function, and gamecode doesn't have any calls earlier than this, do the connecting message here
1080                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CONNECTING, this.netname);
1081                 return;
1082         }
1083
1084         if(!teamplay)
1085                 return;
1086
1087         int source_color, destination_color;
1088         int source_team_index, destination_team_index;
1089
1090         source_color = this.clientcolors & 0x0F;
1091         destination_color = _color & 0x0F;
1092
1093         source_team_index = Team_TeamToIndex(source_color + 1);
1094         destination_team_index = Team_TeamToIndex(destination_color + 1);
1095
1096         if (destination_team_index == -1)
1097         {
1098                 return;
1099         }
1100
1101         entity balance = TeamBalance_CheckAllowedTeams(this);
1102
1103         if (destination_team_index == 1 && !TeamBalance_IsTeamAllowedInternal(
1104                 balance, 1))
1105         {
1106                 destination_team_index = 4;
1107         }
1108         if (destination_team_index == 4 && !TeamBalance_IsTeamAllowedInternal(
1109                 balance, 4))
1110         {
1111                 destination_team_index = 3;
1112         }
1113         if (destination_team_index == 3 && !TeamBalance_IsTeamAllowedInternal(
1114                 balance, 3))
1115         {
1116                 destination_team_index = 2;
1117         }
1118         if (destination_team_index == 2 && !TeamBalance_IsTeamAllowedInternal(
1119                 balance, 2))
1120         {
1121                 destination_team_index = 1;
1122         }
1123
1124         // not changing teams
1125         if (source_color == destination_color)
1126         {
1127                 SetPlayerTeam(this, destination_team_index, TEAM_CHANGE_MANUAL);
1128                 TeamBalance_Destroy(balance);
1129                 TeamBalance_AutoBalanceBots(); // Hack
1130                 return;
1131         }
1132
1133         if((autocvar_g_campaign) || (autocvar_g_changeteam_banned && CS(this).wasplayer)) {
1134                 Send_Notification(NOTIF_ONE, this, MSG_INFO, INFO_TEAMCHANGE_NOTALLOWED);
1135                 return; // changing teams is not allowed
1136         }
1137
1138         // autocvar_g_balance_teams_prevent_imbalance only makes sense if autocvar_g_balance_teams is on, as it makes the team selection dialog pointless
1139         if (autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance)
1140         {
1141                 TeamBalance_GetTeamCounts(balance, this);
1142                 if ((Team_IndexToBit(destination_team_index) &
1143                         TeamBalance_FindBestTeams(balance, this, false)) == 0)
1144                 {
1145                         Send_Notification(NOTIF_ONE, this, MSG_INFO, INFO_TEAMCHANGE_LARGERTEAM);
1146                         TeamBalance_Destroy(balance);
1147                         return;
1148                 }
1149         }
1150         TeamBalance_Destroy(balance);
1151         if (IS_PLAYER(this) && source_team_index != destination_team_index)
1152         {
1153                 // reduce frags during a team change
1154                 PlayerScore_Clear(this);
1155         }
1156         if (!SetPlayerTeam(this, destination_team_index, TEAM_CHANGE_MANUAL))
1157         {
1158                 return;
1159         }
1160         TeamBalance_AutoBalanceBots();
1161 }