]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/teamplay.qc
Removed global alive variables.
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / teamplay.qc
1 #include "teamplay.qh"
2
3 #include "client.qh"
4 #include "race.qh"
5 #include "scores.qh"
6 #include "scores_rules.qh"
7
8 #include "bot/api.qh"
9
10 #include "command/vote.qh"
11
12 #include "mutators/_mod.qh"
13
14 #include "../common/deathtypes/all.qh"
15 #include "../common/gamemodes/_mod.qh"
16 #include "../common/teams.qh"
17
18 /// \brief Describes a state of team balance entity.
19 enum
20 {
21         TEAM_BALANCE_UNINITIALIZED, ///< The team balance has not been initialized.
22         /// \brief TeamBalance_CheckAllowedTeams has been called.
23         TEAM_BALANCE_TEAMS_CHECKED,
24         /// \brief TeamBalance_GetTeamCounts has been called.
25         TEAM_BALANCE_TEAM_COUNTS_FILLED
26 };
27
28 /// \brief Indicates that the player is not allowed to join a team.
29 const int TEAM_NOT_ALLOWED = -1;
30
31 .int m_team_balance_state; ///< Holds the state of the team balance entity.
32 .entity m_team_balance_team[NUM_TEAMS]; ///< ???
33
34 .float m_team_score; ///< The score of the team.
35 .int m_num_players; ///< Number of players (both humans and bots) in a team.
36 .int m_num_bots; ///< Number of bots in a team.
37 .int m_num_players_alive; ///< Number of alive players in a team.
38 .entity m_lowest_human; ///< Human with the lowest score in a team.
39 .entity m_lowest_bot; ///< Bot with the lowest score in a team.
40
41 entity g_team_entities[NUM_TEAMS]; ///< Holds global team entities.
42
43 STATIC_INIT(g_team_entities)
44 {
45         for (int i = 0; i < NUM_TEAMS; ++i)
46         {
47                 g_team_entities[i] = spawn();
48         }
49 }
50
51 entity Team_GetTeamFromIndex(int index)
52 {
53         if (!Team_IsValidIndex(index))
54         {
55                 LOG_FATALF("Team_GetTeamFromIndex: Index is invalid: %f", index);
56         }
57         return g_team_entities[index - 1];
58 }
59
60 entity Team_GetTeam(int team_num)
61 {
62         if (!Team_IsValidTeam(team_num))
63         {
64                 LOG_FATALF("Team_GetTeam: Value is invalid: %f", team_num);
65         }
66         return g_team_entities[Team_TeamToIndex(team_num) - 1];
67 }
68
69 float Team_GetTeamScore(entity team_)
70 {
71         return team_.m_team_score;
72 }
73
74 void Team_SetTeamScore(entity team_, float score)
75 {
76         team_.m_team_score = score;
77 }
78
79 int Team_GetNumberOfAlivePlayers(entity team_)
80 {
81         return team_.m_num_players_alive;
82 }
83
84 void Team_SetNumberOfAlivePlayers(entity team_, int number)
85 {
86         team_.m_num_players_alive = number;
87 }
88
89 int Team_GetNumberOfAliveTeams()
90 {
91         int result = 0;
92         for (int i = 0; i < NUM_TEAMS; ++i)
93         {
94                 if (g_team_entities[i].m_num_players_alive > 0)
95                 {
96                         ++result;
97                 }
98         }
99         return result;
100 }
101
102 void TeamchangeFrags(entity e)
103 {
104         PlayerScore_Clear(e);
105 }
106
107 void LogTeamchange(float player_id, float team_number, float type)
108 {
109         if(!autocvar_sv_eventlog)
110                 return;
111
112         if(player_id < 1)
113                 return;
114
115         GameLogEcho(strcat(":team:", ftos(player_id), ":", ftos(team_number), ":", ftos(type)));
116 }
117
118 void default_delayedinit(entity this)
119 {
120         if(!scores_initialized)
121                 ScoreRules_generic();
122 }
123
124 void InitGameplayMode()
125 {
126         VoteReset();
127
128         // find out good world mins/maxs bounds, either the static bounds found by looking for solid, or the mapinfo specified bounds
129         get_mi_min_max(1);
130         // assign reflectively to avoid "assignment to world" warning
131         int done = 0; for (int i = 0, n = numentityfields(); i < n; ++i) {
132             string k = entityfieldname(i); vector v = (k == "mins") ? mi_min : (k == "maxs") ? mi_max : '0 0 0';
133             if (v) {
134             putentityfieldstring(i, world, sprintf("%v", v));
135             if (++done == 2) break;
136         }
137         }
138         // currently, NetRadiant's limit is 131072 qu for each side
139         // distance from one corner of a 131072qu cube to the opposite corner is approx. 227023 qu
140         // set the distance according to map size but don't go over the limit to avoid issues with float precision
141         // in case somebody makes extremely large maps
142         max_shot_distance = min(230000, vlen(world.maxs - world.mins));
143
144         MapInfo_LoadMapSettings(mapname);
145         GameRules_teams(false);
146
147         if (!cvar_value_issafe(world.fog))
148         {
149                 LOG_INFO("The current map contains a potentially harmful fog setting, ignored");
150                 world.fog = string_null;
151         }
152         if(MapInfo_Map_fog != "")
153                 if(MapInfo_Map_fog == "none")
154                         world.fog = string_null;
155                 else
156                         world.fog = strzone(MapInfo_Map_fog);
157         clientstuff = strzone(MapInfo_Map_clientstuff);
158
159         MapInfo_ClearTemps();
160
161         gamemode_name = MapInfo_Type_ToText(MapInfo_LoadedGametype);
162
163         cache_mutatormsg = strzone("");
164         cache_lastmutatormsg = strzone("");
165
166         InitializeEntity(NULL, default_delayedinit, INITPRIO_GAMETYPE_FALLBACK);
167 }
168
169 string GetClientVersionMessage(entity this)
170 {
171         if (CS(this).version_mismatch) {
172                 if(CS(this).version < autocvar_gameversion) {
173                         return strcat("This is Xonotic ", autocvar_g_xonoticversion,
174                                 "\n^3Your client version is outdated.\n\n\n### YOU WON'T BE ABLE TO PLAY ON THIS SERVER ###\n\n\nPlease update!!!^8");
175                 } else {
176                         return strcat("This is Xonotic ", autocvar_g_xonoticversion,
177                                 "\n^3This server is using an outdated Xonotic version.\n\n\n ### THIS SERVER IS INCOMPATIBLE AND THUS YOU CANNOT JOIN ###.^8");
178                 }
179         } else {
180                 return strcat("Welcome to Xonotic ", autocvar_g_xonoticversion);
181         }
182 }
183
184 string getwelcomemessage(entity this)
185 {
186         MUTATOR_CALLHOOK(BuildMutatorsPrettyString, "");
187         string modifications = M_ARGV(0, string);
188
189         if(g_weaponarena)
190         {
191                 if(g_weaponarena_random)
192                         modifications = strcat(modifications, ", ", ftos(g_weaponarena_random), " of ", g_weaponarena_list, " Arena");
193                 else
194                         modifications = strcat(modifications, ", ", g_weaponarena_list, " Arena");
195         }
196         else if(cvar("g_balance_blaster_weaponstartoverride") == 0)
197                 modifications = strcat(modifications, ", No start weapons");
198         if(cvar("sv_gravity") < stof(cvar_defstring("sv_gravity")))
199                 modifications = strcat(modifications, ", Low gravity");
200         if(g_weapon_stay && !g_cts)
201                 modifications = strcat(modifications, ", Weapons stay");
202         if(g_jetpack)
203                 modifications = strcat(modifications, ", Jet pack");
204         if(autocvar_g_powerups == 0)
205                 modifications = strcat(modifications, ", No powerups");
206         if(autocvar_g_powerups > 0)
207                 modifications = strcat(modifications, ", Powerups");
208         modifications = substring(modifications, 2, strlen(modifications) - 2);
209
210         string versionmessage = GetClientVersionMessage(this);
211         string s = strcat(versionmessage, "^8\n^8\nmatch type is ^1", gamemode_name, "^8\n");
212
213         if(modifications != "")
214                 s = strcat(s, "^8\nactive modifications: ^3", modifications, "^8\n");
215
216         if(cache_lastmutatormsg != autocvar_g_mutatormsg)
217         {
218                 if(cache_lastmutatormsg)
219                         strunzone(cache_lastmutatormsg);
220                 if(cache_mutatormsg)
221                         strunzone(cache_mutatormsg);
222                 cache_lastmutatormsg = strzone(autocvar_g_mutatormsg);
223                 cache_mutatormsg = strzone(cache_lastmutatormsg);
224         }
225
226         if (cache_mutatormsg != "") {
227                 s = strcat(s, "\n\n^8special gameplay tips: ^7", cache_mutatormsg);
228         }
229
230         string mutator_msg = "";
231         MUTATOR_CALLHOOK(BuildGameplayTipsString, mutator_msg);
232         mutator_msg = M_ARGV(0, string);
233
234         s = strcat(s, mutator_msg); // trust that the mutator will do proper formatting
235
236         string motd = autocvar_sv_motd;
237         if (motd != "") {
238                 s = strcat(s, "\n\n^8MOTD: ^7", strreplace("\\n", "\n", motd));
239         }
240         return s;
241 }
242
243 void setcolor(entity this, int clr)
244 {
245 #if 0
246         this.clientcolors = clr;
247         this.team = (clr & 15) + 1;
248 #else
249         builtin_setcolor(this, clr);
250 #endif
251 }
252
253 bool Entity_HasValidTeam(entity this)
254 {
255         return Team_IsValidTeam(this.team);
256 }
257
258 int Entity_GetTeamIndex(entity this)
259 {
260         return Team_TeamToIndex(this.team);
261 }
262
263 entity Entity_GetTeam(entity this)
264 {
265         int index = Entity_GetTeamIndex(this);
266         if (!Team_IsValidIndex(index))
267         {
268                 return NULL;
269         }
270         return Team_GetTeamFromIndex(index);
271 }
272
273 void SetPlayerColors(entity player, float _color)
274 {
275         float pants = _color & 0x0F;
276         float shirt = _color & 0xF0;
277         if (teamplay)
278         {
279                 setcolor(player, 16 * pants + pants);
280         }
281         else
282         {
283                 setcolor(player, shirt + pants);
284         }
285 }
286
287 bool Player_SetTeamIndex(entity player, int index)
288 {
289         int new_team = Team_IndexToTeam(index);
290         if (player.team == new_team)
291         {
292                 // This is important when players join the game and one of their color
293                 // matches the team color while other doesn't. For example [BOT]Lion.
294                 SetPlayerColors(player, new_team - 1);
295                 return true;
296         }
297         int old_index = Team_TeamToIndex(player.team);
298         if (MUTATOR_CALLHOOK(Player_ChangeTeam, player, old_index, index) == true)
299         {
300                 // Mutator has blocked team change.
301                 return false;
302         }
303         SetPlayerColors(player, new_team - 1);
304         MUTATOR_CALLHOOK(Player_ChangedTeam, player, old_index, index);
305         return true;
306 }
307
308 bool SetPlayerTeam(entity player, int destination_team_index,
309         int source_team_index, bool no_print)
310 {
311         if (!Player_SetTeamIndex(player, destination_team_index))
312         {
313                 return false;
314         }
315         LogTeamchange(player.playerid, player.team, 3);  // log manual team join
316         if (no_print)
317         {
318                 return true;
319         }
320         bprint(playername(player, false), "^7 has changed from ",
321                 Team_IndexToColoredFullName(source_team_index), "^7 to ",
322                 Team_IndexToColoredFullName(destination_team_index), "\n");
323         return true;
324 }
325
326 void KillPlayerForTeamChange(entity player)
327 {
328         if (IS_DEAD(player))
329         {
330                 return;
331         }
332         if (MUTATOR_CALLHOOK(Player_ChangeTeamKill, player) == true)
333         {
334                 return;
335         }
336         Damage(player, player, player, 100000, DEATH_TEAMCHANGE.m_id, DMG_NOWEP,
337                 player.origin, '0 0 0');
338 }
339
340 entity TeamBalance_CheckAllowedTeams(entity for_whom)
341 {
342         entity balance = spawn();
343         for (int i = 0; i < NUM_TEAMS; ++i)
344         {
345                 entity team_ = balance.m_team_balance_team[i] = spawn();
346                 team_.m_team_score = g_team_entities[i].m_team_score;
347                 team_.m_num_players = TEAM_NOT_ALLOWED;
348                 team_.m_num_bots = 0;
349                 team_.m_lowest_human = NULL;
350                 team_.m_lowest_bot = NULL;
351         }
352         
353         int teams_mask = 0;     
354         string teament_name = string_null;
355         bool mutator_returnvalue = MUTATOR_CALLHOOK(TeamBalance_CheckAllowedTeams,
356                 teams_mask, teament_name, for_whom);
357         teams_mask = M_ARGV(0, float);
358         teament_name = M_ARGV(1, string);
359         if (mutator_returnvalue)
360         {
361                 for (int i = 0; i < NUM_TEAMS; ++i)
362                 {
363                         if (teams_mask & BIT(i))
364                         {
365                                 balance.m_team_balance_team[i].m_num_players = 0;
366                         }
367                 }
368         }
369
370         if (teament_name)
371         {
372                 entity head = find(NULL, classname, teament_name);
373                 while (head)
374                 {
375                         if (Team_IsValidTeam(head.team))
376                         {
377                                 TeamBalance_GetTeam(balance, head.team).m_num_players = 0;
378                         }
379                         head = find(head, classname, teament_name);
380                 }
381         }
382
383         // TODO: Balance quantity of bots across > 2 teams when bot_vs_human is set (and remove next line)
384         if (AvailableTeams() == 2)
385         if (autocvar_bot_vs_human && for_whom)
386         {
387                 if (autocvar_bot_vs_human > 0)
388                 {
389                         // find last team available
390                         if (IS_BOT_CLIENT(for_whom))
391                         {
392                                 if (TeamBalance_IsTeamAllowedInternal(balance, 4))
393                                 {
394                                         TeamBalance_BanTeamsExcept(balance, 4);
395                                 }
396                                 else if (TeamBalance_IsTeamAllowedInternal(balance, 3))
397                                 {
398                                         TeamBalance_BanTeamsExcept(balance, 3);
399                                 }
400                                 else
401                                 {
402                                         TeamBalance_BanTeamsExcept(balance, 2);
403                                 }
404                                 // no further cases, we know at least 2 teams exist
405                         }
406                         else
407                         {
408                                 if (TeamBalance_IsTeamAllowedInternal(balance, 1))
409                                 {
410                                         TeamBalance_BanTeamsExcept(balance, 1);
411                                 }
412                                 else if (TeamBalance_IsTeamAllowedInternal(balance, 2))
413                                 {
414                                         TeamBalance_BanTeamsExcept(balance, 2);
415                                 }
416                                 else
417                                 {
418                                         TeamBalance_BanTeamsExcept(balance, 3);
419                                 }
420                                 // no further cases, bots have one of the teams
421                         }
422                 }
423                 else
424                 {
425                         // find first team available
426                         if (IS_BOT_CLIENT(for_whom))
427                         {
428                                 if (TeamBalance_IsTeamAllowedInternal(balance, 1))
429                                 {
430                                         TeamBalance_BanTeamsExcept(balance, 1);
431                                 }
432                                 else if (TeamBalance_IsTeamAllowedInternal(balance, 2))
433                                 {
434                                         TeamBalance_BanTeamsExcept(balance, 2);
435                                 }
436                                 else
437                                 {
438                                         TeamBalance_BanTeamsExcept(balance, 3);
439                                 }
440                                 // no further cases, we know at least 2 teams exist
441                         }
442                         else
443                         {
444                                 if (TeamBalance_IsTeamAllowedInternal(balance, 4))
445                                 {
446                                         TeamBalance_BanTeamsExcept(balance, 4);
447                                 }
448                                 else if (TeamBalance_IsTeamAllowedInternal(balance, 3))
449                                 {
450                                         TeamBalance_BanTeamsExcept(balance, 3);
451                                 }
452                                 else
453                                 {
454                                         TeamBalance_BanTeamsExcept(balance, 2);
455                                 }
456                                 // no further cases, bots have one of the teams
457                         }
458                 }
459         }
460
461         if (!for_whom)
462         {
463                 balance.m_team_balance_state = TEAM_BALANCE_TEAMS_CHECKED;
464                 return balance;
465         }
466
467         // if player has a forced team, ONLY allow that one
468         for (int i = 1; i <= NUM_TEAMS; ++i)
469         {
470                 if (for_whom.team_forced == Team_IndexToTeam(i) &&
471                         TeamBalance_IsTeamAllowedInternal(balance, i))
472                 {
473                         TeamBalance_BanTeamsExcept(balance, i);
474                 }
475                 break;
476         }
477         balance.m_team_balance_state = TEAM_BALANCE_TEAMS_CHECKED;
478         return balance;
479 }
480
481 void TeamBalance_Destroy(entity balance)
482 {
483         if (balance == NULL)
484         {
485                 return;
486         }
487         for (int i = 0; i < NUM_TEAMS; ++i)
488         {
489                 remove(balance.(m_team_balance_team[i]));
490         }
491         remove(balance);
492 }
493
494 int TeamBalance_GetAllowedTeams(entity balance)
495 {
496         if (balance == NULL)
497         {
498                 LOG_FATAL("TeamBalance_GetAllowedTeams: Team balance entity is NULL.");
499         }
500         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
501         {
502                 LOG_FATAL("TeamBalance_GetAllowedTeams: "
503                         "Team balance entity is not initialized.");
504         }
505         int result = 0;
506         for (int i = 1; i <= NUM_TEAMS; ++i)
507         {
508                 if (TeamBalance_IsTeamAllowedInternal(balance, i))
509                 {
510                         result |= Team_IndexToBit(i);
511                 }
512         }
513         return result;
514 }
515
516 bool TeamBalance_IsTeamAllowed(entity balance, int index)
517 {
518         if (balance == NULL)
519         {
520                 LOG_FATAL("TeamBalance_IsTeamAllowed: Team balance entity is NULL.");
521         }
522         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
523         {
524                 LOG_FATAL("TeamBalance_IsTeamAllowed: "
525                         "Team balance entity is not initialized.");
526         }
527         if (!Team_IsValidIndex(index))
528         {
529                 LOG_FATALF("TeamBalance_IsTeamAllowed: Team index is invalid: %f",
530                         index);
531         }
532         return TeamBalance_IsTeamAllowedInternal(balance, index);
533 }
534
535 void TeamBalance_GetTeamCounts(entity balance, entity ignore)
536 {
537         if (balance == NULL)
538         {
539                 LOG_FATAL("TeamBalance_GetTeamCounts: Team balance entity is NULL.");
540         }
541         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
542         {
543                 LOG_FATAL("TeamBalance_GetTeamCounts: "
544                         "Team balance entity is not initialized.");
545         }
546         if (MUTATOR_CALLHOOK(TeamBalance_GetTeamCounts) == true)
547         {
548                 // Mutator has overriden the configuration.
549                 for (int i = 1; i <= NUM_TEAMS; ++i)
550                 {
551                         entity team_ = TeamBalance_GetTeamFromIndex(balance, i);
552                         if (TeamBalanceTeam_IsAllowed(team_))
553                         {
554                                 MUTATOR_CALLHOOK(TeamBalance_GetTeamCount, i, ignore,
555                                         team_.m_num_players, team_.m_num_bots, team_.m_lowest_human,
556                                         team_.m_lowest_bot);
557                                 team_.m_num_players = M_ARGV(2, float);
558                                 team_.m_num_bots = M_ARGV(3, float);
559                                 team_.m_lowest_human = M_ARGV(4, entity);
560                                 team_.m_lowest_bot = M_ARGV(5, entity);
561                         }
562                 }
563         }
564         else
565         {
566                 // Manually count all players.
567                 FOREACH_CLIENT(true,
568                 {
569                         if (it == ignore)
570                         {
571                                 continue;
572                         }
573                         int team_num;
574                         if (IS_PLAYER(it) || it.caplayer)
575                         {
576                                 team_num = it.team;
577                         }
578                         else if (it.team_forced > 0)
579                         {
580                                 team_num = it.team_forced; // reserve the spot
581                         }
582                         else
583                         {
584                                 continue;
585                         }
586                         if (!Team_IsValidTeam(team_num))
587                         {
588                                 continue;
589                         }
590                         entity team_ = TeamBalance_GetTeam(balance, team_num);
591                         if (!TeamBalanceTeam_IsAllowed(team_))
592                         {
593                                 continue;
594                         }
595                         ++team_.m_num_players;
596                         if (IS_BOT_CLIENT(it))
597                         {
598                                 ++team_.m_num_bots;
599                         }
600                         float temp_score = PlayerScore_Get(it, SP_SCORE);
601                         if (!IS_BOT_CLIENT(it))
602                         {
603                                 if (team_.m_lowest_human == NULL)
604                                 {
605                                         team_.m_lowest_human = it;
606                                         continue;
607                                 }
608                                 if (temp_score < PlayerScore_Get(team_.m_lowest_human,
609                                         SP_SCORE))
610                                 {
611                                         team_.m_lowest_human = it;
612                                 }
613                                 continue;
614                         }
615                         if (team_.m_lowest_bot == NULL)
616                         {
617                                 team_.m_lowest_bot = it;
618                                 continue;
619                         }
620                         if (temp_score < PlayerScore_Get(team_.m_lowest_bot, SP_SCORE))
621                         {
622                                 team_.m_lowest_bot = it;
623                         }
624                 });
625         }
626
627         // if the player who has a forced team has not joined yet, reserve the spot
628         if (autocvar_g_campaign)
629         {
630                 if (Team_IsValidIndex(autocvar_g_campaign_forceteam))
631                 {
632                         entity team_ = TeamBalance_GetTeamFromIndex(balance,
633                                 autocvar_g_campaign_forceteam);
634                         if (team_.m_num_players == team_.m_num_bots)
635                         {
636                                 ++team_.m_num_players;
637                         }
638                 }
639         }
640         balance.m_team_balance_state = TEAM_BALANCE_TEAM_COUNTS_FILLED;
641 }
642
643 int TeamBalance_GetNumberOfPlayers(entity balance, int index)
644 {
645         if (balance == NULL)
646         {
647                 LOG_FATAL("TeamBalance_GetNumberOfPlayers: "
648                         "Team balance entity is NULL.");
649         }
650         if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
651         {
652                 LOG_FATAL("TeamBalance_GetNumberOfPlayers: "
653                         "TeamBalance_GetTeamCounts has not been called.");
654         }
655         if (!Team_IsValidIndex(index))
656         {
657                 LOG_FATALF("TeamBalance_GetNumberOfPlayers: Team index is invalid: %f",
658                         index);
659         }
660         return balance.m_team_balance_team[index - 1].m_num_players;
661 }
662
663 int TeamBalance_FindBestTeam(entity balance, entity player, bool ignore_player)
664 {
665         if (balance == NULL)
666         {
667                 LOG_FATAL("TeamBalance_FindBestTeam: Team balance entity is NULL.");
668         }
669         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
670         {
671                 LOG_FATAL("TeamBalance_FindBestTeam: "
672                         "Team balance entity is not initialized.");
673         }
674         // count how many players are in each team
675         if (ignore_player)
676         {
677                 TeamBalance_GetTeamCounts(balance, player);
678         }
679         else
680         {
681                 TeamBalance_GetTeamCounts(balance, NULL);
682         }
683         int team_bits = TeamBalance_FindBestTeams(balance, player, true);
684         if (team_bits == 0)
685         {
686                 LOG_FATALF("TeamBalance_FindBestTeam: No teams available for %s\n",
687                         MapInfo_Type_ToString(MapInfo_CurrentGametype()));
688         }
689         RandomSelection_Init();
690         for (int i = 1; i <= NUM_TEAMS; ++i)
691         {
692                 if (team_bits & Team_IndexToBit(i))
693                 {
694                         RandomSelection_AddFloat(i, 1, 1);
695                 }
696         }
697         return RandomSelection_chosen_float;
698 }
699
700 int TeamBalance_FindBestTeams(entity balance, entity player, bool use_score)
701 {
702         if (balance == NULL)
703         {
704                 LOG_FATAL("TeamBalance_FindBestTeams: Team balance entity is NULL.");
705         }
706         if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
707         {
708                 LOG_FATAL("TeamBalance_FindBestTeams: "
709                         "TeamBalance_GetTeamCounts has not been called.");
710         }
711         if (MUTATOR_CALLHOOK(TeamBalance_FindBestTeams, player) == true)
712         {
713                 return M_ARGV(1, float);
714         }
715         int team_bits = 0;
716         int previous_team = 0;
717         for (int i = 1; i <= NUM_TEAMS; ++i)
718         {
719                 if (!TeamBalance_IsTeamAllowedInternal(balance, i))
720                 {
721                         continue;
722                 }
723                 if (previous_team == 0)
724                 {
725                         team_bits = Team_IndexToBit(i);
726                         previous_team = i;
727                         continue;
728                 }
729                 int compare = TeamBalance_CompareTeams(balance, i, previous_team,
730                         player, use_score);
731                 if (compare == TEAMS_COMPARE_LESS)
732                 {
733                         team_bits = Team_IndexToBit(i);
734                         previous_team = i;
735                         continue;
736                 }
737                 if (compare == TEAMS_COMPARE_EQUAL)
738                 {
739                         team_bits |= Team_IndexToBit(i);
740                         previous_team = i;
741                 }
742         }
743         return team_bits;
744 }
745
746 void TeamBalance_JoinBestTeam(entity this, bool force_best_team)
747 {
748         // don't join a team if we're not playing a team game
749         if (!teamplay)
750         {
751                 return;
752         }
753
754         // find out what teams are available
755         entity balance = TeamBalance_CheckAllowedTeams(this);
756
757         // if we don't care what team they end up on, put them on whatever team they entered as.
758         // if they're not on a valid team, then let other code put them on the smallest team
759         if (!force_best_team)
760         {
761                 int selected_team_index = -1;
762                 for (int i = 1; i <= NUM_TEAMS; ++i)
763                 {
764                         if (TeamBalance_IsTeamAllowedInternal(balance, i) &&
765                                 (Team_TeamToIndex(this.team) == i))
766                         {
767                                 selected_team_index = i;
768                                 break;
769                         }
770                 }
771                 
772                 if (Team_IsValidIndex(selected_team_index))
773                 {
774                         Player_SetTeamIndex(this, selected_team_index);
775                         LogTeamchange(this.playerid, this.team, 99);
776                         TeamBalance_Destroy(balance);
777                         return;
778                 }
779         }
780         // otherwise end up on the smallest team (handled below)
781         if (this.bot_forced_team)
782         {
783                 TeamBalance_Destroy(balance);
784                 return;
785         }
786         int best_team_index = TeamBalance_FindBestTeam(balance, this, true);
787         int old_team_index = Team_TeamToIndex(this.team);
788         TeamchangeFrags(this);
789         Player_SetTeamIndex(this, best_team_index);
790         LogTeamchange(this.playerid, this.team, 2); // log auto join
791         if ((old_team_index != -1) && !IS_BOT_CLIENT(this))
792         {
793                 TeamBalance_AutoBalanceBots(balance, old_team_index, best_team_index);
794         }
795         KillPlayerForTeamChange(this);
796         TeamBalance_Destroy(balance);
797 }
798
799 int TeamBalance_CompareTeams(entity balance, int team_index_a, int team_index_b,
800         entity player, bool use_score)
801 {
802         if (balance == NULL)
803         {
804                 LOG_FATAL("TeamBalance_CompareTeams: Team balance entity is NULL.");
805         }
806         if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
807         {
808                 LOG_FATAL("TeamBalance_CompareTeams: "
809                         "TeamBalance_GetTeamCounts has not been called.");
810         }
811         if (!Team_IsValidIndex(team_index_a))
812         {
813                 LOG_FATALF("TeamBalance_CompareTeams: team_index_a is invalid: %f",
814                         team_index_a);
815         }
816         if (!Team_IsValidIndex(team_index_b))
817         {
818                 LOG_FATALF("TeamBalance_CompareTeams: team_index_b is invalid: %f",
819                         team_index_b);
820         }
821         if (team_index_a == team_index_b)
822         {
823                 return TEAMS_COMPARE_EQUAL;
824         }
825         entity team_a = TeamBalance_GetTeamFromIndex(balance, team_index_a);
826         entity team_b = TeamBalance_GetTeamFromIndex(balance, team_index_b);
827         return TeamBalance_CompareTeamsInternal(team_a, team_b, player, use_score);
828 }
829
830 void TeamBalance_AutoBalanceBots(entity balance, int source_team_index,
831         int destination_team_index)
832 {
833         if (balance == NULL)
834         {
835                 LOG_FATAL("TeamBalance_AutoBalanceBots: Team balance entity is NULL.");
836         }
837         if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
838         {
839                 LOG_FATAL("TeamBalance_AutoBalanceBots: "
840                         "TeamBalance_GetTeamCounts has not been called.");
841         }
842         if (!Team_IsValidIndex(source_team_index))
843         {
844                 LOG_WARNF("AutoBalanceBots: Source team index is invalid: %f",
845                         source_team_index);
846                 return;
847         }
848         if (!Team_IsValidIndex(destination_team_index))
849         {
850                 LOG_WARNF("AutoBalanceBots: Destination team index is invalid: %f",
851                         destination_team_index);
852                 return;
853         }
854         if (!autocvar_g_balance_teams ||
855                 !autocvar_g_balance_teams_prevent_imbalance)
856         {
857                 return;
858         }
859         entity source_team = TeamBalance_GetTeamFromIndex(balance,
860                 source_team_index);
861         if (!TeamBalanceTeam_IsAllowed(source_team))
862         {
863                 return;
864         }
865         entity destination_team = TeamBalance_GetTeamFromIndex(balance,
866                 destination_team_index);
867         if ((destination_team.m_num_players <= source_team.m_num_players) ||
868                 (destination_team.m_lowest_bot == NULL))
869         {
870                 return;
871         }
872         Player_SetTeamIndex(destination_team.m_lowest_bot, source_team_index);
873         KillPlayerForTeamChange(destination_team.m_lowest_bot);
874 }
875
876 bool TeamBalance_IsTeamAllowedInternal(entity balance, int index)
877 {
878         return balance.m_team_balance_team[index - 1].m_num_players !=
879                 TEAM_NOT_ALLOWED;
880 }
881
882 void TeamBalance_BanTeamsExcept(entity balance, int index)
883 {
884         for (int i = 1; i <= NUM_TEAMS; ++i)
885         {
886                 if (i != index)
887                 {
888                         balance.m_team_balance_team[i - 1].m_num_players = TEAM_NOT_ALLOWED;
889                 }
890         }
891 }
892
893 entity TeamBalance_GetTeamFromIndex(entity balance, int index)
894 {
895         if (!Team_IsValidIndex(index))
896         {
897                 LOG_FATALF("TeamBalance_GetTeamFromIndex: Index is invalid: %f", index);
898         }
899         return balance.m_team_balance_team[index - 1];
900 }
901
902 entity TeamBalance_GetTeam(entity balance, int team_num)
903 {
904         return TeamBalance_GetTeamFromIndex(balance, Team_TeamToIndex(team_num));
905 }
906
907 bool TeamBalanceTeam_IsAllowed(entity team_)
908 {
909         return team_.m_num_players != TEAM_NOT_ALLOWED;
910 }
911
912 int TeamBalanceTeam_GetNumberOfPlayers(entity team_)
913 {
914         return team_.m_num_players;
915 }
916
917 int TeamBalanceTeam_GetNumberOfBots(entity team_)
918 {
919         return team_.m_num_bots;
920 }
921
922 entity TeamBalanceTeam_GetLowestHuman(entity team_)
923 {
924         return team_.m_lowest_human;
925 }
926
927 entity TeamBalanceTeam_GetLowestBot(entity team_)
928 {
929         return team_.m_lowest_bot;
930 }
931
932 int TeamBalance_CompareTeamsInternal(entity team_a, entity team_b,
933         entity player, bool use_score)
934 {
935         if (team_a == team_b)
936         {
937                 return TEAMS_COMPARE_EQUAL;
938         }
939         if (!TeamBalanceTeam_IsAllowed(team_a) ||
940                 !TeamBalanceTeam_IsAllowed(team_b))
941         {
942                 return TEAMS_COMPARE_INVALID;
943         }
944         int num_players_team_a = team_a.m_num_players;
945         int num_players_team_b = team_b.m_num_players;
946         if (IS_REAL_CLIENT(player) && bots_would_leave)
947         {
948                 num_players_team_a -= team_a.m_num_bots;
949                 num_players_team_b -= team_b.m_num_bots;
950         }
951         if (num_players_team_a < num_players_team_b)
952         {
953                 return TEAMS_COMPARE_LESS;
954         }
955         if (num_players_team_a > num_players_team_b)
956         {
957                 return TEAMS_COMPARE_GREATER;
958         }
959         if (!use_score)
960         {
961                 return TEAMS_COMPARE_EQUAL;
962         }
963         if (team_a.m_team_score < team_b.m_team_score)
964         {
965                 return TEAMS_COMPARE_LESS;
966         }
967         if (team_a.m_team_score > team_b.m_team_score)
968         {
969                 return TEAMS_COMPARE_GREATER;
970         }
971         return TEAMS_COMPARE_EQUAL;
972 }
973
974 void SV_ChangeTeam(entity this, float _color)
975 {
976         int source_color, destination_color;
977         int source_team_index, destination_team_index;
978
979         // in normal deathmatch we can just apply the color and we're done
980         if(!teamplay)
981                 SetPlayerColors(this, _color);
982
983         if(!IS_CLIENT(this))
984         {
985                 // since this is an engine function, and gamecode doesn't have any calls earlier than this, do the connecting message here
986                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CONNECTING, this.netname);
987                 return;
988         }
989
990         if(!teamplay)
991                 return;
992
993         source_color = this.clientcolors & 0x0F;
994         destination_color = _color & 0x0F;
995
996         source_team_index = Team_TeamToIndex(source_color + 1);
997         destination_team_index = Team_TeamToIndex(destination_color + 1);
998
999         if (destination_team_index == -1)
1000         {
1001                 return;
1002         }
1003
1004         entity balance = TeamBalance_CheckAllowedTeams(this);
1005
1006         if (destination_team_index == 1 && !TeamBalance_IsTeamAllowedInternal(
1007                 balance, 1))
1008         {
1009                 destination_team_index = 4;
1010         }
1011         if (destination_team_index == 4 && !TeamBalance_IsTeamAllowedInternal(
1012                 balance, 4))
1013         {
1014                 destination_team_index = 3;
1015         }
1016         if (destination_team_index == 3 && !TeamBalance_IsTeamAllowedInternal(
1017                 balance, 3))
1018         {
1019                 destination_team_index = 2;
1020         }
1021         if (destination_team_index == 2 && !TeamBalance_IsTeamAllowedInternal(
1022                 balance, 2))
1023         {
1024                 destination_team_index = 1;
1025         }
1026
1027         // not changing teams
1028         if (source_color == destination_color)
1029         {
1030                 SetPlayerTeam(this, destination_team_index, source_team_index, true);
1031                 TeamBalance_Destroy(balance);
1032                 return;
1033         }
1034
1035         if((autocvar_g_campaign) || (autocvar_g_changeteam_banned && CS(this).wasplayer)) {
1036                 Send_Notification(NOTIF_ONE, this, MSG_INFO, INFO_TEAMCHANGE_NOTALLOWED);
1037                 return; // changing teams is not allowed
1038         }
1039
1040         // autocvar_g_balance_teams_prevent_imbalance only makes sense if autocvar_g_balance_teams is on, as it makes the team selection dialog pointless
1041         if (autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance)
1042         {
1043                 TeamBalance_GetTeamCounts(balance, this);
1044                 if ((Team_IndexToBit(destination_team_index) &
1045                         TeamBalance_FindBestTeams(balance, this, false)) == 0)
1046                 {
1047                         Send_Notification(NOTIF_ONE, this, MSG_INFO, INFO_TEAMCHANGE_LARGERTEAM);
1048                         TeamBalance_Destroy(balance);
1049                         return;
1050                 }
1051         }
1052         if (IS_PLAYER(this) && source_team_index != destination_team_index)
1053         {
1054                 // reduce frags during a team change
1055                 TeamchangeFrags(this);
1056         }
1057         if (!SetPlayerTeam(this, destination_team_index, source_team_index,
1058                 !IS_CLIENT(this)))
1059         {
1060                 TeamBalance_Destroy(balance);
1061                 return;
1062         }
1063         TeamBalance_AutoBalanceBots(balance, source_team_index,
1064                 destination_team_index);
1065         if (!IS_PLAYER(this) || (source_team_index == destination_team_index))
1066         {
1067                 TeamBalance_Destroy(balance);
1068                 return;
1069         }
1070         KillPlayerForTeamChange(this);
1071         TeamBalance_Destroy(balance);
1072 }