sv_maxidle_playertospectator: fix joining players getting moved to spec immediately
[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("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_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 0
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("Player_SetForcedTeamIndex: 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 && AvailableTeams() == 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("TeamBalance_GetAllowedTeams: Team balance entity is NULL.");
644         }
645         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
646         {
647                 LOG_FATAL("TeamBalance_GetAllowedTeams: "
648                         "Team balance entity is not initialized.");
649         }
650         int result = 0;
651         for (int i = 1; i <= NUM_TEAMS; ++i)
652         {
653                 if (TeamBalance_IsTeamAllowedInternal(balance, i))
654                 {
655                         result |= Team_IndexToBit(i);
656                 }
657         }
658         return result;
659 }
660
661 bool TeamBalance_IsTeamAllowed(entity balance, int index)
662 {
663         if (balance == NULL)
664         {
665                 LOG_FATAL("TeamBalance_IsTeamAllowed: Team balance entity is NULL.");
666         }
667         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
668         {
669                 LOG_FATAL("TeamBalance_IsTeamAllowed: "
670                         "Team balance entity is not initialized.");
671         }
672         if (!Team_IsValidIndex(index))
673         {
674                 LOG_FATALF("TeamBalance_IsTeamAllowed: Team index is invalid: %f",
675                         index);
676         }
677         return TeamBalance_IsTeamAllowedInternal(balance, index);
678 }
679
680 void TeamBalance_GetTeamCounts(entity balance, entity ignore)
681 {
682         if (balance == NULL)
683         {
684                 LOG_FATAL("TeamBalance_GetTeamCounts: Team balance entity is NULL.");
685         }
686         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
687         {
688                 LOG_FATAL("TeamBalance_GetTeamCounts: "
689                         "Team balance entity is not initialized.");
690         }
691         if (MUTATOR_CALLHOOK(TeamBalance_GetTeamCounts) == true)
692         {
693                 // Mutator has overriden the configuration.
694                 for (int i = 1; i <= NUM_TEAMS; ++i)
695                 {
696                         entity team_ent = TeamBalance_GetTeamFromIndex(balance, i);
697                         if (TeamBalanceTeam_IsAllowed(team_ent))
698                         {
699                                 MUTATOR_CALLHOOK(TeamBalance_GetTeamCount, i, ignore);
700                                 team_ent.m_num_players = M_ARGV(2, float);
701                                 team_ent.m_num_bots = M_ARGV(3, float);
702                         }
703                 }
704         }
705         else
706         {
707                 // Manually count all players.
708                 FOREACH_CLIENT(true,
709                 {
710                         if (it == ignore)
711                         {
712                                 continue;
713                         }
714                         int team_num;
715                         // TODO: Reconsider when the player is truly on the team.
716                         if (IS_CLIENT(it) || INGAME(it))
717                         {
718                                 team_num = it.team;
719                         }
720                         else if (Player_HasRealForcedTeam(it))
721                         {
722                                 // Do we really need this? Probably not.
723                                 team_num = Team_IndexToTeam(it.team_forced); // reserve the spot
724                         }
725                         else
726                         {
727                                 continue;
728                         }
729                         if (!Team_IsValidTeam(team_num))
730                         {
731                                 continue;
732                         }
733                         entity team_ent = TeamBalance_GetTeam(balance, team_num);
734                         if (!TeamBalanceTeam_IsAllowed(team_ent))
735                         {
736                                 continue;
737                         }
738                         ++team_ent.m_num_players;
739                         if (IS_BOT_CLIENT(it))
740                         {
741                                 ++team_ent.m_num_bots;
742                         }
743                 });
744         }
745
746         // if the player who has a forced team has not joined yet, reserve the spot
747         if (autocvar_g_campaign)
748         {
749                 if (Team_IsValidIndex(autocvar_g_campaign_forceteam))
750                 {
751                         entity team_ent = TeamBalance_GetTeamFromIndex(balance,
752                                 autocvar_g_campaign_forceteam);
753                         if (team_ent.m_num_players == team_ent.m_num_bots)
754                         {
755                                 ++team_ent.m_num_players;
756                         }
757                 }
758         }
759         balance.m_team_balance_state = TEAM_BALANCE_TEAM_COUNTS_FILLED;
760 }
761
762 int TeamBalance_GetNumberOfPlayers(entity balance, int index)
763 {
764         if (balance == NULL)
765         {
766                 LOG_FATAL("TeamBalance_GetNumberOfPlayers: "
767                         "Team balance entity is NULL.");
768         }
769         if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
770         {
771                 LOG_FATAL("TeamBalance_GetNumberOfPlayers: "
772                         "TeamBalance_GetTeamCounts has not been called.");
773         }
774         if (!Team_IsValidIndex(index))
775         {
776                 LOG_FATALF("TeamBalance_GetNumberOfPlayers: Team index is invalid: %f",
777                         index);
778         }
779         return balance.m_team_balance_team[index - 1].m_num_players;
780 }
781
782 int TeamBalance_FindBestTeam(entity balance, entity player, bool ignore_player)
783 {
784         if (balance == NULL)
785         {
786                 LOG_FATAL("TeamBalance_FindBestTeam: Team balance entity is NULL.");
787         }
788         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
789         {
790                 LOG_FATAL("TeamBalance_FindBestTeam: "
791                         "Team balance entity is not initialized.");
792         }
793         // count how many players are in each team
794         if (ignore_player)
795         {
796                 TeamBalance_GetTeamCounts(balance, player);
797         }
798         else
799         {
800                 TeamBalance_GetTeamCounts(balance, NULL);
801         }
802         int team_bits = TeamBalance_FindBestTeams(balance, player, true);
803         if (team_bits == 0)
804         {
805                 LOG_FATALF("TeamBalance_FindBestTeam: No teams available for %s\n",
806                         MapInfo_Type_ToString(MapInfo_CurrentGametype()));
807         }
808         RandomSelection_Init();
809         for (int i = 1; i <= NUM_TEAMS; ++i)
810         {
811                 if (team_bits & Team_IndexToBit(i))
812                 {
813                         RandomSelection_AddFloat(i, 1, 1);
814                 }
815         }
816         return RandomSelection_chosen_float;
817 }
818
819 int TeamBalance_FindBestTeams(entity balance, entity player, bool use_score)
820 {
821         if (balance == NULL)
822         {
823                 LOG_FATAL("TeamBalance_FindBestTeams: Team balance entity is NULL.");
824         }
825         if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
826         {
827                 LOG_FATAL("TeamBalance_FindBestTeams: "
828                         "TeamBalance_GetTeamCounts has not been called.");
829         }
830         if (MUTATOR_CALLHOOK(TeamBalance_FindBestTeams, player) == true)
831         {
832                 return M_ARGV(1, float);
833         }
834         int team_bits = 0;
835         int previous_team = 0;
836         for (int i = 1; i <= NUM_TEAMS; ++i)
837         {
838                 if (!TeamBalance_IsTeamAllowedInternal(balance, i))
839                 {
840                         continue;
841                 }
842                 if (previous_team == 0)
843                 {
844                         team_bits = Team_IndexToBit(i);
845                         previous_team = i;
846                         continue;
847                 }
848                 int compare = TeamBalance_CompareTeams(balance, i, previous_team,
849                         player, use_score);
850                 if (compare == TEAMS_COMPARE_LESS)
851                 {
852                         team_bits = Team_IndexToBit(i);
853                         previous_team = i;
854                         continue;
855                 }
856                 if (compare == TEAMS_COMPARE_EQUAL)
857                 {
858                         team_bits |= Team_IndexToBit(i);
859                         previous_team = i;
860                 }
861         }
862         return team_bits;
863 }
864
865 int TeamBalance_CompareTeams(entity balance, int team_index_a, int team_index_b,
866         entity player, bool use_score)
867 {
868         if (balance == NULL)
869         {
870                 LOG_FATAL("TeamBalance_CompareTeams: Team balance entity is NULL.");
871         }
872         if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
873         {
874                 LOG_FATAL("TeamBalance_CompareTeams: "
875                         "TeamBalance_GetTeamCounts has not been called.");
876         }
877         if (!Team_IsValidIndex(team_index_a))
878         {
879                 LOG_FATALF("TeamBalance_CompareTeams: team_index_a is invalid: %f",
880                         team_index_a);
881         }
882         if (!Team_IsValidIndex(team_index_b))
883         {
884                 LOG_FATALF("TeamBalance_CompareTeams: team_index_b is invalid: %f",
885                         team_index_b);
886         }
887         if (team_index_a == team_index_b)
888         {
889                 return TEAMS_COMPARE_EQUAL;
890         }
891         entity team_a = TeamBalance_GetTeamFromIndex(balance, team_index_a);
892         entity team_b = TeamBalance_GetTeamFromIndex(balance, team_index_b);
893         return TeamBalance_CompareTeamsInternal(team_a, team_b, player, use_score);
894 }
895
896 void TeamBalance_AutoBalanceBots()
897 {
898         // checks disabled because we always want auto-balanced bots
899         //if (!(autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance))
900         //      return;
901
902         entity balance = TeamBalance_CheckAllowedTeams(NULL);
903         TeamBalance_GetTeamCounts(balance, NULL);
904         int smallest_team_index = 0;
905         int smallest_team_player_count = 0;
906         for (int i = 1; i <= NUM_TEAMS; ++i)
907         {
908                 entity team_ = TeamBalance_GetTeamFromIndex(balance, i);
909                 if (!TeamBalanceTeam_IsAllowed(team_))
910                 {
911                         continue;
912                 }
913                 int playercount = TeamBalanceTeam_GetNumberOfPlayers(team_);
914                 if (smallest_team_index == 0)
915                 {
916                         smallest_team_index = i;
917                         smallest_team_player_count = playercount;
918                 }
919                 else if (playercount < smallest_team_player_count)
920                 {
921                         smallest_team_index = i;
922                         smallest_team_player_count = playercount;
923                 }
924         }
925         //PrintToChatAll(sprintf("Smallest team: %f", smallest_team_index));
926         //PrintToChatAll(sprintf("Smallest team players: %f", smallest_team_player_count));
927         entity switchable_bot = NULL;
928         int teams = BITS(NUM_TEAMS);
929         while (teams != 0)
930         {
931                 int largest_team_index = TeamBalance_GetLargestTeamIndex(balance,
932                         teams);
933                 if (smallest_team_index == largest_team_index)
934                 {
935                         TeamBalance_Destroy(balance);
936                         return;
937                 }
938                 entity largest_team = TeamBalance_GetTeamFromIndex(balance,
939                         largest_team_index);
940                 int largest_team_player_count = TeamBalanceTeam_GetNumberOfPlayers(
941                         largest_team);
942                 if (largest_team_player_count - smallest_team_player_count < 2)
943                 {
944                         TeamBalance_Destroy(balance);
945                         return;
946                 }
947                 //PrintToChatAll(sprintf("Largest team: %f", largest_team_index));
948                 //PrintToChatAll(sprintf("Largest team players: %f", largest_team_player_count));
949                 switchable_bot = TeamBalance_GetPlayerForTeamSwitch(largest_team_index,
950                         smallest_team_index, true);
951                 if (switchable_bot != NULL)
952                 {
953                         break;
954                 }
955                 teams &= ~Team_IndexToBit(largest_team_index);
956         }
957         TeamBalance_Destroy(balance);
958         if (switchable_bot == NULL)
959         {
960                 //PrintToChatAll("No bot found after searching through all the teams");
961                 return;
962         }
963         SetPlayerTeam(switchable_bot, smallest_team_index, TEAM_CHANGE_AUTO);
964 }
965
966 int TeamBalance_GetLargestTeamIndex(entity balance, int teams)
967 {
968         int largest_team_index = 0;
969         int largest_team_player_count = 0;
970         for (int i = 1; i <= NUM_TEAMS; ++i)
971         {
972                 if (!(Team_IndexToBit(i) & teams))
973                 {
974                         continue;
975                 }
976                 entity team_ = TeamBalance_GetTeamFromIndex(balance, i);
977                 if (!TeamBalanceTeam_IsAllowed(team_))
978                 {
979                         continue;
980                 }
981                 int playercount = TeamBalanceTeam_GetNumberOfPlayers(team_);
982                 if (largest_team_index == 0)
983                 {
984                         largest_team_index = i;
985                         largest_team_player_count = playercount;
986                 }
987                 else if (playercount > largest_team_player_count)
988                 {
989                         largest_team_index = i;
990                         largest_team_player_count = playercount;
991                 }
992         }
993         return largest_team_index;
994 }
995
996 entity TeamBalance_GetPlayerForTeamSwitch(int source_team_index,
997         int destination_team_index, bool is_bot)
998 {
999         if (MUTATOR_CALLHOOK(TeamBalance_GetPlayerForTeamSwitch, source_team_index,
1000                 destination_team_index, is_bot))
1001         {
1002                 return M_ARGV(3, entity);
1003         }
1004         entity lowest_player = NULL;
1005         float lowest_score = FLOAT_MAX;
1006         FOREACH_CLIENT(Entity_GetTeamIndex(it) == source_team_index,
1007         {
1008                 if (IS_BOT_CLIENT(it) != is_bot)
1009                 {
1010                         continue;
1011                 }
1012                 float temp_score = PlayerScore_Get(it, SP_SCORE);
1013                 if (temp_score >= lowest_score)
1014                 {
1015                         continue;
1016                 }
1017                 //PrintToChatAll(sprintf(
1018                 //      "Found %s with lowest score, checking allowed teams", it.netname));
1019                 entity balance = TeamBalance_CheckAllowedTeams(it);
1020                 if (TeamBalance_IsTeamAllowed(balance, source_team_index))
1021                 {
1022                         //PrintToChatAll("Allowed");
1023                         lowest_player = it;
1024                         lowest_score = temp_score;
1025                 }
1026                 else
1027                 {
1028                         //PrintToChatAll("Not allowed");
1029                 }
1030                 TeamBalance_Destroy(balance);
1031         });
1032         return lowest_player;
1033 }
1034
1035 void LogTeamChange(float player_id, float team_number, int type)
1036 {
1037         if (!autocvar_sv_eventlog)
1038         {
1039                 return;
1040         }
1041         if (player_id < 1)
1042         {
1043                 return;
1044         }
1045         GameLogEcho(sprintf(":team:%d:%d:%d", player_id, team_number, type));
1046 }
1047
1048 void KillPlayerForTeamChange(entity player)
1049 {
1050         if (IS_DEAD(player))
1051         {
1052                 return;
1053         }
1054         if (MUTATOR_CALLHOOK(Player_ChangeTeamKill, player) == true)
1055         {
1056                 return;
1057         }
1058         Damage(player, player, player, 100000, DEATH_TEAMCHANGE.m_id, DMG_NOWEP,
1059                 player.origin, '0 0 0');
1060 }
1061
1062 bool TeamBalance_IsTeamAllowedInternal(entity balance, int index)
1063 {
1064         return balance.m_team_balance_team[index - 1].m_num_players !=
1065                 TEAM_NOT_ALLOWED;
1066 }
1067
1068 void TeamBalance_BanTeamsExcept(entity balance, int index)
1069 {
1070         for (int i = 1; i <= NUM_TEAMS; ++i)
1071         {
1072                 if (i != index)
1073                 {
1074                         balance.m_team_balance_team[i - 1].m_num_players = TEAM_NOT_ALLOWED;
1075                 }
1076         }
1077 }
1078
1079 entity TeamBalance_GetTeamFromIndex(entity balance, int index)
1080 {
1081         if (!Team_IsValidIndex(index))
1082         {
1083                 LOG_FATALF("TeamBalance_GetTeamFromIndex: Index is invalid: %f", index);
1084         }
1085         return balance.m_team_balance_team[index - 1];
1086 }
1087
1088 entity TeamBalance_GetTeam(entity balance, int team_num)
1089 {
1090         return TeamBalance_GetTeamFromIndex(balance, Team_TeamToIndex(team_num));
1091 }
1092
1093 bool TeamBalanceTeam_IsAllowed(entity team_ent)
1094 {
1095         return team_ent.m_num_players != TEAM_NOT_ALLOWED;
1096 }
1097
1098 int TeamBalanceTeam_GetNumberOfPlayers(entity team_ent)
1099 {
1100         return team_ent.m_num_players;
1101 }
1102
1103 int TeamBalanceTeam_GetNumberOfBots(entity team_ent)
1104 {
1105         return team_ent.m_num_bots;
1106 }
1107
1108 int TeamBalance_CompareTeamsInternal(entity team_a, entity team_b,
1109         entity player, bool use_score)
1110 {
1111         if (team_a == team_b)
1112         {
1113                 return TEAMS_COMPARE_EQUAL;
1114         }
1115         if (!TeamBalanceTeam_IsAllowed(team_a) ||
1116                 !TeamBalanceTeam_IsAllowed(team_b))
1117         {
1118                 return TEAMS_COMPARE_INVALID;
1119         }
1120         int num_players_team_a = team_a.m_num_players;
1121         int num_players_team_b = team_b.m_num_players;
1122         if (IS_REAL_CLIENT(player) && bots_would_leave)
1123         {
1124                 num_players_team_a -= team_a.m_num_bots;
1125                 num_players_team_b -= team_b.m_num_bots;
1126         }
1127         if (num_players_team_a < num_players_team_b)
1128         {
1129                 return TEAMS_COMPARE_LESS;
1130         }
1131         if (num_players_team_a > num_players_team_b)
1132         {
1133                 return TEAMS_COMPARE_GREATER;
1134         }
1135         if (!use_score)
1136         {
1137                 return TEAMS_COMPARE_EQUAL;
1138         }
1139         if (team_a.m_team_score < team_b.m_team_score)
1140         {
1141                 return TEAMS_COMPARE_LESS;
1142         }
1143         if (team_a.m_team_score > team_b.m_team_score)
1144         {
1145                 return TEAMS_COMPARE_GREATER;
1146         }
1147         return TEAMS_COMPARE_EQUAL;
1148 }
1149
1150 void SV_ChangeTeam(entity player, int new_color)
1151 {
1152         if (!teamplay)
1153         {
1154                 SetPlayerColors(player, new_color);
1155         }
1156         if(!IS_CLIENT(player))
1157         {
1158                 return;
1159         }
1160         if (!teamplay)
1161         {
1162                 return;
1163         }
1164         Player_SetTeamIndexChecked(player, Team_TeamToIndex((new_color & 0x0F) + 1));
1165 }