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