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