]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/teamplay.qc
Merge branch 'master' into Lyberta/TeamplayOverhaul2
[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                         if (IS_PLAYER(it) || it.caplayer)
604                         {
605                                 team_num = it.team;
606                         }
607                         else if (Player_HasRealForcedTeam(it))
608                         {
609                                 team_num = Team_IndexToTeam(it.team_forced); // reserve the spot
610                         }
611                         else
612                         {
613                                 continue;
614                         }
615                         if (!Team_IsValidTeam(team_num))
616                         {
617                                 continue;
618                         }
619                         entity team_ent = TeamBalance_GetTeam(balance, team_num);
620                         if (!TeamBalanceTeam_IsAllowed(team_ent))
621                         {
622                                 continue;
623                         }
624                         ++team_ent.m_num_players;
625                         if (IS_BOT_CLIENT(it))
626                         {
627                                 ++team_ent.m_num_bots;
628                         }
629                 });
630         }
631
632         // if the player who has a forced team has not joined yet, reserve the spot
633         if (autocvar_g_campaign)
634         {
635                 if (Team_IsValidIndex(autocvar_g_campaign_forceteam))
636                 {
637                         entity team_ent = TeamBalance_GetTeamFromIndex(balance,
638                                 autocvar_g_campaign_forceteam);
639                         if (team_ent.m_num_players == team_ent.m_num_bots)
640                         {
641                                 ++team_ent.m_num_players;
642                         }
643                 }
644         }
645         balance.m_team_balance_state = TEAM_BALANCE_TEAM_COUNTS_FILLED;
646 }
647
648 int TeamBalance_GetNumberOfPlayers(entity balance, int index)
649 {
650         if (balance == NULL)
651         {
652                 LOG_FATAL("TeamBalance_GetNumberOfPlayers: "
653                         "Team balance entity is NULL.");
654         }
655         if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
656         {
657                 LOG_FATAL("TeamBalance_GetNumberOfPlayers: "
658                         "TeamBalance_GetTeamCounts has not been called.");
659         }
660         if (!Team_IsValidIndex(index))
661         {
662                 LOG_FATALF("TeamBalance_GetNumberOfPlayers: Team index is invalid: %f",
663                         index);
664         }
665         return balance.m_team_balance_team[index - 1].m_num_players;
666 }
667
668 int TeamBalance_FindBestTeam(entity balance, entity player, bool ignore_player)
669 {
670         if (balance == NULL)
671         {
672                 LOG_FATAL("TeamBalance_FindBestTeam: Team balance entity is NULL.");
673         }
674         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
675         {
676                 LOG_FATAL("TeamBalance_FindBestTeam: "
677                         "Team balance entity is not initialized.");
678         }
679         // count how many players are in each team
680         if (ignore_player)
681         {
682                 TeamBalance_GetTeamCounts(balance, player);
683         }
684         else
685         {
686                 TeamBalance_GetTeamCounts(balance, NULL);
687         }
688         int team_bits = TeamBalance_FindBestTeams(balance, player, true);
689         if (team_bits == 0)
690         {
691                 LOG_FATALF("TeamBalance_FindBestTeam: No teams available for %s\n",
692                         MapInfo_Type_ToString(MapInfo_CurrentGametype()));
693         }
694         RandomSelection_Init();
695         for (int i = 1; i <= NUM_TEAMS; ++i)
696         {
697                 if (team_bits & Team_IndexToBit(i))
698                 {
699                         RandomSelection_AddFloat(i, 1, 1);
700                 }
701         }
702         return RandomSelection_chosen_float;
703 }
704
705 int TeamBalance_FindBestTeams(entity balance, entity player, bool use_score)
706 {
707         if (balance == NULL)
708         {
709                 LOG_FATAL("TeamBalance_FindBestTeams: Team balance entity is NULL.");
710         }
711         if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
712         {
713                 LOG_FATAL("TeamBalance_FindBestTeams: "
714                         "TeamBalance_GetTeamCounts has not been called.");
715         }
716         if (MUTATOR_CALLHOOK(TeamBalance_FindBestTeams, player) == true)
717         {
718                 return M_ARGV(1, float);
719         }
720         int team_bits = 0;
721         int previous_team = 0;
722         for (int i = 1; i <= NUM_TEAMS; ++i)
723         {
724                 if (!TeamBalance_IsTeamAllowedInternal(balance, i))
725                 {
726                         continue;
727                 }
728                 if (previous_team == 0)
729                 {
730                         team_bits = Team_IndexToBit(i);
731                         previous_team = i;
732                         continue;
733                 }
734                 int compare = TeamBalance_CompareTeams(balance, i, previous_team,
735                         player, use_score);
736                 if (compare == TEAMS_COMPARE_LESS)
737                 {
738                         team_bits = Team_IndexToBit(i);
739                         previous_team = i;
740                         continue;
741                 }
742                 if (compare == TEAMS_COMPARE_EQUAL)
743                 {
744                         team_bits |= Team_IndexToBit(i);
745                         previous_team = i;
746                 }
747         }
748         return team_bits;
749 }
750
751 void TeamBalance_JoinBestTeam(entity this)
752 {
753         //PrintToChatAll(sprintf("JoinBestTeam: %s", this.netname));
754         if (!teamplay)
755         {
756                 return;
757         }
758         if (this.bot_forced_team)
759         {
760                 return;
761         }
762         int old_team_index = Team_TeamToIndex(this.team);
763         entity balance = TeamBalance_CheckAllowedTeams(this);
764         if (Player_HasRealForcedTeam(this))
765         {
766                 int forced_team_index = this.team_forced;
767                 bool is_team_allowed = TeamBalance_IsTeamAllowedInternal(balance,
768                         forced_team_index);
769                 TeamBalance_Destroy(balance);
770                 if (!is_team_allowed)
771                 {
772                         return;
773                 }
774                 if (!SetPlayerTeam(this, forced_team_index, TEAM_CHANGE_AUTO))
775                 {
776                         return;
777                 }
778                 if ((old_team_index != -1) && !IS_BOT_CLIENT(this))
779                 {
780                         TeamBalance_AutoBalanceBots(forced_team_index, old_team_index);
781                 }
782                 return;
783         }
784         int best_team_index = TeamBalance_FindBestTeam(balance, this, true);
785         TeamBalance_Destroy(balance);
786         PlayerScore_Clear(this);
787         if (!SetPlayerTeam(this, best_team_index, TEAM_CHANGE_AUTO))
788         {
789                 return;
790         }
791         if ((old_team_index != -1) && !IS_BOT_CLIENT(this))
792         {
793                 TeamBalance_AutoBalanceBots(best_team_index, old_team_index);
794         }
795 }
796
797 int TeamBalance_CompareTeams(entity balance, int team_index_a, int team_index_b,
798         entity player, bool use_score)
799 {
800         if (balance == NULL)
801         {
802                 LOG_FATAL("TeamBalance_CompareTeams: Team balance entity is NULL.");
803         }
804         if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
805         {
806                 LOG_FATAL("TeamBalance_CompareTeams: "
807                         "TeamBalance_GetTeamCounts has not been called.");
808         }
809         if (!Team_IsValidIndex(team_index_a))
810         {
811                 LOG_FATALF("TeamBalance_CompareTeams: team_index_a is invalid: %f",
812                         team_index_a);
813         }
814         if (!Team_IsValidIndex(team_index_b))
815         {
816                 LOG_FATALF("TeamBalance_CompareTeams: team_index_b is invalid: %f",
817                         team_index_b);
818         }
819         if (team_index_a == team_index_b)
820         {
821                 return TEAMS_COMPARE_EQUAL;
822         }
823         entity team_a = TeamBalance_GetTeamFromIndex(balance, team_index_a);
824         entity team_b = TeamBalance_GetTeamFromIndex(balance, team_index_b);
825         return TeamBalance_CompareTeamsInternal(team_a, team_b, player, use_score);
826 }
827
828 void TeamBalance_AutoBalanceBots(int source_team_index,
829         int destination_team_index)
830 {
831         if (!Team_IsValidIndex(source_team_index))
832         {
833                 LOG_WARNF("TeamBalance_AutoBalanceBots: "
834                         "Source team index is invalid: %f", source_team_index);
835                 return;
836         }
837         if (!Team_IsValidIndex(destination_team_index))
838         {
839                 LOG_WARNF("TeamBalance_AutoBalanceBots: "
840                         "Destination team index is invalid: %f", destination_team_index);
841                 return;
842         }
843         if (!autocvar_g_balance_teams ||
844                 !autocvar_g_balance_teams_prevent_imbalance)
845         {
846                 return;
847         }
848         entity balance = TeamBalance_CheckAllowedTeams(NULL);
849         TeamBalance_GetTeamCounts(balance, NULL);
850         entity source_team = TeamBalance_GetTeamFromIndex(balance,
851                 source_team_index);
852         entity destination_team = TeamBalance_GetTeamFromIndex(balance,
853                 destination_team_index);
854         if ((source_team.m_num_bots == 0) || (source_team.m_num_players <=
855                 destination_team.m_num_players))
856         {
857                 TeamBalance_Destroy(balance);
858                 return;
859         }
860         TeamBalance_Destroy(balance);
861         entity lowest_bot = NULL;
862         if (MUTATOR_CALLHOOK(TeamBalance_GetPlayerForTeamSwitch, source_team_index,
863                 destination_team_index, true))
864         {
865                 lowest_bot = M_ARGV(3, entity);
866         }
867         else
868         {
869                 float lowest_score = FLOAT_MAX;
870                 FOREACH_CLIENT(IS_BOT_CLIENT(it) && (Entity_GetTeamIndex(it) ==
871                         source_team_index),
872                 {
873                         float temp_score = PlayerScore_Get(it, SP_SCORE);
874                         if (temp_score >= lowest_score)
875                         {
876                                 continue;
877                         }
878                         balance = TeamBalance_CheckAllowedTeams(it);
879                         if (TeamBalance_IsTeamAllowed(balance, destination_team_index))
880                         {
881                                 lowest_bot = it;
882                                 lowest_score = temp_score;
883                         }
884                         TeamBalance_Destroy(balance);
885                 });
886         }
887         if (lowest_bot == NULL)
888         {
889                 return;
890         }
891         if (!Player_SetTeamIndex(lowest_bot, destination_team_index))
892         {
893                 return;
894         }
895         KillPlayerForTeamChange(lowest_bot);
896 }
897
898 bool TeamBalance_IsTeamAllowedInternal(entity balance, int index)
899 {
900         return balance.m_team_balance_team[index - 1].m_num_players !=
901                 TEAM_NOT_ALLOWED;
902 }
903
904 void TeamBalance_BanTeamsExcept(entity balance, int index)
905 {
906         for (int i = 1; i <= NUM_TEAMS; ++i)
907         {
908                 if (i != index)
909                 {
910                         balance.m_team_balance_team[i - 1].m_num_players = TEAM_NOT_ALLOWED;
911                 }
912         }
913 }
914
915 entity TeamBalance_GetTeamFromIndex(entity balance, int index)
916 {
917         if (!Team_IsValidIndex(index))
918         {
919                 LOG_FATALF("TeamBalance_GetTeamFromIndex: Index is invalid: %f", index);
920         }
921         return balance.m_team_balance_team[index - 1];
922 }
923
924 entity TeamBalance_GetTeam(entity balance, int team_num)
925 {
926         return TeamBalance_GetTeamFromIndex(balance, Team_TeamToIndex(team_num));
927 }
928
929 bool TeamBalanceTeam_IsAllowed(entity team_ent)
930 {
931         return team_ent.m_num_players != TEAM_NOT_ALLOWED;
932 }
933
934 int TeamBalanceTeam_GetNumberOfPlayers(entity team_ent)
935 {
936         return team_ent.m_num_players;
937 }
938
939 int TeamBalanceTeam_GetNumberOfBots(entity team_ent)
940 {
941         return team_ent.m_num_bots;
942 }
943
944 int TeamBalance_CompareTeamsInternal(entity team_a, entity team_b,
945         entity player, bool use_score)
946 {
947         if (team_a == team_b)
948         {
949                 return TEAMS_COMPARE_EQUAL;
950         }
951         if (!TeamBalanceTeam_IsAllowed(team_a) ||
952                 !TeamBalanceTeam_IsAllowed(team_b))
953         {
954                 return TEAMS_COMPARE_INVALID;
955         }
956         int num_players_team_a = team_a.m_num_players;
957         int num_players_team_b = team_b.m_num_players;
958         if (IS_REAL_CLIENT(player) && bots_would_leave)
959         {
960                 num_players_team_a -= team_a.m_num_bots;
961                 num_players_team_b -= team_b.m_num_bots;
962         }
963         if (num_players_team_a < num_players_team_b)
964         {
965                 return TEAMS_COMPARE_LESS;
966         }
967         if (num_players_team_a > num_players_team_b)
968         {
969                 return TEAMS_COMPARE_GREATER;
970         }
971         if (!use_score)
972         {
973                 return TEAMS_COMPARE_EQUAL;
974         }
975         if (team_a.m_team_score < team_b.m_team_score)
976         {
977                 return TEAMS_COMPARE_LESS;
978         }
979         if (team_a.m_team_score > team_b.m_team_score)
980         {
981                 return TEAMS_COMPARE_GREATER;
982         }
983         return TEAMS_COMPARE_EQUAL;
984 }
985
986 // Called when the player connects or when they change their color with "color"
987 // command.
988 void SV_ChangeTeam(entity this, float _color)
989 {
990         //PrintToChatAll(sprintf("SV_ChangeTeam: %s, %f", this.netname, _color));
991
992         // in normal deathmatch we can just apply the color and we're done
993         if(!teamplay)
994                 SetPlayerColors(this, _color);
995
996         if(!IS_CLIENT(this))
997         {
998                 // since this is an engine function, and gamecode doesn't have any calls earlier than this, do the connecting message here
999                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CONNECTING, this.netname);
1000                 return;
1001         }
1002
1003         if(!teamplay)
1004                 return;
1005
1006         int source_color, destination_color;
1007         int source_team_index, destination_team_index;
1008
1009         source_color = this.clientcolors & 0x0F;
1010         destination_color = _color & 0x0F;
1011
1012         source_team_index = Team_TeamToIndex(source_color + 1);
1013         destination_team_index = Team_TeamToIndex(destination_color + 1);
1014
1015         if (destination_team_index == -1)
1016         {
1017                 return;
1018         }
1019
1020         entity balance = TeamBalance_CheckAllowedTeams(this);
1021
1022         if (destination_team_index == 1 && !TeamBalance_IsTeamAllowedInternal(
1023                 balance, 1))
1024         {
1025                 destination_team_index = 4;
1026         }
1027         if (destination_team_index == 4 && !TeamBalance_IsTeamAllowedInternal(
1028                 balance, 4))
1029         {
1030                 destination_team_index = 3;
1031         }
1032         if (destination_team_index == 3 && !TeamBalance_IsTeamAllowedInternal(
1033                 balance, 3))
1034         {
1035                 destination_team_index = 2;
1036         }
1037         if (destination_team_index == 2 && !TeamBalance_IsTeamAllowedInternal(
1038                 balance, 2))
1039         {
1040                 destination_team_index = 1;
1041         }
1042
1043         // not changing teams
1044         if (source_color == destination_color)
1045         {
1046                 SetPlayerTeam(this, destination_team_index, TEAM_CHANGE_MANUAL);
1047                 TeamBalance_Destroy(balance);
1048                 return;
1049         }
1050
1051         if((autocvar_g_campaign) || (autocvar_g_changeteam_banned && CS(this).wasplayer)) {
1052                 Send_Notification(NOTIF_ONE, this, MSG_INFO, INFO_TEAMCHANGE_NOTALLOWED);
1053                 return; // changing teams is not allowed
1054         }
1055
1056         // autocvar_g_balance_teams_prevent_imbalance only makes sense if autocvar_g_balance_teams is on, as it makes the team selection dialog pointless
1057         if (autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance)
1058         {
1059                 TeamBalance_GetTeamCounts(balance, this);
1060                 if ((Team_IndexToBit(destination_team_index) &
1061                         TeamBalance_FindBestTeams(balance, this, false)) == 0)
1062                 {
1063                         Send_Notification(NOTIF_ONE, this, MSG_INFO, INFO_TEAMCHANGE_LARGERTEAM);
1064                         TeamBalance_Destroy(balance);
1065                         return;
1066                 }
1067         }
1068         TeamBalance_Destroy(balance);
1069         if (IS_PLAYER(this) && source_team_index != destination_team_index)
1070         {
1071                 // reduce frags during a team change
1072                 PlayerScore_Clear(this);
1073         }
1074         if (!SetPlayerTeam(this, destination_team_index, TEAM_CHANGE_MANUAL))
1075         {
1076                 return;
1077         }
1078         if (source_team_index == -1)
1079         {
1080                 return;
1081         }
1082         TeamBalance_AutoBalanceBots(destination_team_index, source_team_index);
1083 }