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