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