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