]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/teamplay.qc
Create queue system to prevent team imbalance in teamplay
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / teamplay.qc
1 #include "teamplay.qh"
2
3 #include <common/deathtypes/all.qh>
4 #include <common/gamemodes/_mod.qh>
5 #include <common/teams.qh>
6 #include <server/bot/api.qh>
7 #include <server/bot/default/cvars.qh>
8 #include <server/campaign.qh>
9 #include <server/client.qh>
10 #include <server/command/vote.qh>
11 #include <server/damage.qh>
12 #include <server/gamelog.qh>
13 #include <server/mutators/_mod.qh>
14 #include <server/race.qh>
15 #include <server/scores.qh>
16 #include <server/scores_rules.qh>
17
18 /// \brief Describes a state of team balance entity.
19 enum
20 {
21         TEAM_BALANCE_UNINITIALIZED, ///< The team balance has not been initialized.
22         /// \brief TeamBalance_CheckAllowedTeams has been called.
23         TEAM_BALANCE_TEAMS_CHECKED,
24         /// \brief TeamBalance_GetTeamCounts has been called.
25         TEAM_BALANCE_TEAM_COUNTS_FILLED
26 };
27
28 /// \brief Indicates that the player is not allowed to join a team.
29 const int TEAM_NOT_ALLOWED = -1;
30
31 .int m_team_balance_state; ///< Holds the state of the team balance entity.
32 .entity m_team_balance_team[NUM_TEAMS]; ///< ???
33
34 .float m_team_score; ///< The score of the team.
35 .int m_num_players; ///< Number of players (both humans and bots) in a team.
36 .int m_num_bots; ///< Number of bots in a team.
37 .int m_num_players_alive; ///< Number of alive players in a team.
38 .int m_num_owned_items; ///< Number of items owned by a team.
39
40 string autocvar_g_forced_team_red;
41 string autocvar_g_forced_team_blue;
42 string autocvar_g_forced_team_yellow;
43 string autocvar_g_forced_team_pink;
44
45 entity g_team_entities[NUM_TEAMS]; ///< Holds global team entities.
46
47 void Team_InitTeams()
48 {
49         if (g_team_entities[0])
50                 return;
51         for (int i = 0; i < NUM_TEAMS; ++i)
52         {
53                 g_team_entities[i] = new_pure(team_entity);
54         }
55 }
56
57 entity Team_GetTeamFromIndex(int index)
58 {
59         if (!Team_IsValidIndex(index))
60         {
61                 LOG_FATALF("Index is invalid: %f", index);
62         }
63         return g_team_entities[index - 1];
64 }
65
66 entity Team_GetTeam(int team_num)
67 {
68         if (!Team_IsValidTeam(team_num))
69         {
70                 LOG_FATALF("Value is invalid: %f", team_num);
71         }
72         return g_team_entities[Team_TeamToIndex(team_num) - 1];
73 }
74
75 float Team_GetTeamScore(entity team_ent)
76 {
77         return team_ent.m_team_score;
78 }
79
80 void Team_SetTeamScore(entity team_ent, float score)
81 {
82         team_ent.m_team_score = score;
83 }
84
85 int Team_GetNumberOfAlivePlayers(entity team_ent)
86 {
87         return team_ent.m_num_players_alive;
88 }
89
90 void Team_SetNumberOfAlivePlayers(entity team_ent, int number)
91 {
92         team_ent.m_num_players_alive = number;
93 }
94
95 int Team_GetWinnerAliveTeam()
96 {
97         int winner = 0;
98         for (int i = 0; i < NUM_TEAMS; ++i)
99         {
100                 if (g_team_entities[i].m_num_players_alive > 0)
101                 {
102                         if (winner)
103                                 return 0;
104                         winner = Team_IndexToTeam(i + 1);
105                 }
106         }
107         return (winner ? winner : -1);
108 }
109
110 int Team_GetNumberOfAliveTeams()
111 {
112         int result = 0;
113         for (int i = 0; i < NUM_TEAMS; ++i)
114         {
115                 if (g_team_entities[i].m_num_players_alive > 0)
116                 {
117                         ++result;
118                 }
119         }
120         return result;
121 }
122
123 int Team_GetWinnerTeam_WithOwnedItems(int min_control_points)
124 {
125         int winner = 0;
126         for (int i = 0; i < NUM_TEAMS; ++i)
127         {
128                 if (g_team_entities[i].m_num_owned_items >= min_control_points)
129                 {
130                         if (winner)
131                                 return 0;
132                         winner = Team_IndexToTeam(i + 1);
133                 }
134         }
135         return (winner ? winner : -1);
136 }
137
138 int Team_GetNumberOfOwnedItems(entity team_ent)
139 {
140         return team_ent.m_num_owned_items;
141 }
142
143 void Team_SetNumberOfOwnedItems(entity team_ent, int number)
144 {
145         team_ent.m_num_owned_items = number;
146 }
147
148 int Team_GetNumberOfTeamsWithOwnedItems()
149 {
150         int result = 0;
151         for (int i = 0; i < NUM_TEAMS; ++i)
152         {
153                 if (g_team_entities[i].m_num_owned_items > 0)
154                 {
155                         ++result;
156                 }
157         }
158         return result;
159 }
160
161 void setcolor(entity this, int clr)
162 {
163 #if 1
164         this.clientcolors = clr;
165         this.team = (clr & 15) + 1;
166 #else
167         builtin_setcolor(this, clr);
168 #endif
169 }
170
171 bool Entity_HasValidTeam(entity this)
172 {
173         return Team_IsValidTeam(this.team);
174 }
175
176 int Entity_GetTeamIndex(entity this)
177 {
178         return Team_TeamToIndex(this.team);
179 }
180
181 entity Entity_GetTeam(entity this)
182 {
183         int index = Entity_GetTeamIndex(this);
184         if (!Team_IsValidIndex(index))
185         {
186                 return NULL;
187         }
188         return Team_GetTeamFromIndex(index);
189 }
190
191 void SetPlayerColors(entity player, float _color)
192 {
193         float pants = _color & 0x0F;
194         float shirt = _color & 0xF0;
195         if (teamplay)
196         {
197                 setcolor(player, 16 * pants + pants);
198         }
199         else
200         {
201                 setcolor(player, shirt + pants);
202         }
203 }
204
205 bool Player_SetTeamIndex(entity player, int index)
206 {
207         int new_team = Team_IndexToTeam(index);
208         if (player.team == new_team)
209         {
210                 if (new_team != -1)
211                 {
212                         // This is important when players join the game and one of their
213                         // color matches the team color while other doesn't. For example
214                         // [BOT]Lion: color 0 4.
215                         SetPlayerColors(player, new_team - 1);
216                 }
217                 return true;
218         }
219         int old_index = Team_TeamToIndex(player.team);
220         if (MUTATOR_CALLHOOK(Player_ChangeTeam, player, old_index, index) == true)
221         {
222                 // Mutator has blocked team change.
223                 return false;
224         }
225         if (new_team == -1)
226         {
227                 player.team = -1;
228         }
229         else
230         {
231                 SetPlayerColors(player, new_team - 1);
232         }
233         MUTATOR_CALLHOOK(Player_ChangedTeam, player, old_index, index);
234         return true;
235 }
236
237 /** Returns true when enough players are queued that the next will join directly
238  * to the only available team (also triggering the joins of the queued players).
239  * Optionally only counts players who selected a specific team when joining the queue.
240  */
241 bool QueuedPlayersReady(entity this, bool checkspecificteam)
242 {
243         int numplayersqueued = 0;
244
245         FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != this
246         && (checkspecificteam ? it.wants_join > 0 : it.wants_join),
247         {
248                 LOG_DEBUGF("Player %s is waiting to join team %d", it.netname, it.wants_join);
249                 ++numplayersqueued;
250                 if (numplayersqueued >= AVAILABLE_TEAMS - 1)
251                         return true;
252         });
253
254         LOG_DEBUG("No players waiting to join.");
255         return false;
256 }
257
258 bool SetPlayerTeam(entity player, int team_index, int type)
259 {
260         int old_team_index = Entity_GetTeamIndex(player);
261
262         if (!Player_SetTeamIndex(player, team_index))
263                 return false;
264
265         LogTeamChange(player.playerid, player.team, type);
266
267         if (team_index != old_team_index)
268         {
269                 KillPlayerForTeamChange(player);
270                 PlayerScore_Clear(player); // works only in game modes without teams
271
272                 if (!IS_BOT_CLIENT(player))
273                         TeamBalance_AutoBalanceBots();
274
275                 if (team_index != -1)
276                 {
277                         if (!queuePlayer(player, team_index))
278                         {
279                                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(player.team, INFO_JOIN_PLAY_TEAM), player.netname);
280                                 player.team_selected = true; // no autoselect in Join()
281                         }
282                 }
283         }
284
285         if (team_index == -1)
286         {
287                 if (autocvar_sv_maxidle_playertospectator > 0 && CS(player).idlekick_lasttimeleft)
288                 {
289                         // this done here so it happens even when manually speccing during the countdown
290                         Kill_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CPID_IDLING);
291                         CS(player).idlekick_lasttimeleft = 0;
292                 }
293                 else if (!CS(player).just_joined && player.frags != FRAGS_SPECTATOR)
294                 {
295                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_QUIT_SPECTATE, player.netname);
296                 }
297         }
298
299         return true;
300 }
301
302 void Player_SetTeamIndexChecked(entity player, int team_index)
303 {
304         if (!teamplay)
305         {
306                 return;
307         }
308         if (!Team_IsValidIndex(team_index))
309         {
310                 return;
311         }
312         if ((autocvar_g_campaign) || (autocvar_g_changeteam_banned &&
313                 CS(player).wasplayer))
314         {
315                 Send_Notification(NOTIF_ONE, player, MSG_INFO,
316                         INFO_TEAMCHANGE_NOTALLOWED);
317                 return;
318         }
319         entity balance = TeamBalance_CheckAllowedTeams(player);
320         if (team_index == 1 && !TeamBalance_IsTeamAllowedInternal(balance, 1))
321         {
322                 team_index = 4;
323         }
324         if (team_index == 4 && !TeamBalance_IsTeamAllowedInternal(balance, 4))
325         {
326                 team_index = 3;
327         }
328         if (team_index == 3 && !TeamBalance_IsTeamAllowedInternal(balance, 3))
329         {
330                 team_index = 2;
331         }
332         if (team_index == 2 && !TeamBalance_IsTeamAllowedInternal(balance, 2))
333         {
334                 team_index = 1;
335         }
336         // autocvar_g_balance_teams_prevent_imbalance only makes sense if autocvar_g_balance_teams is on, as it makes the team selection dialog pointless
337         if (autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance)
338         {
339                 TeamBalance_GetTeamCounts(balance, player);
340                 if ((Team_IndexToBit(team_index) & TeamBalance_FindBestTeams(balance,
341                         player, false)) == 0)
342                 {
343                         Send_Notification(NOTIF_ONE, player, MSG_INFO,
344                                 INFO_TEAMCHANGE_LARGERTEAM);
345                         TeamBalance_Destroy(balance);
346                         return;
347                 }
348         }
349         TeamBalance_Destroy(balance);
350
351         // g_balance_teams_queue: before joining the queue,
352         // check if a queued player already chose the selected team
353         if (!IS_BOT_CLIENT(player) && IS_QUEUE_NEEDED(player))
354         {
355                 FOREACH_CLIENT(IS_REAL_CLIENT(it) && it != player && it.wants_join == team_index,
356                 {
357                         Send_Notification(NOTIF_ONE, player, MSG_CENTER, APP_TEAM_NUM(Team_IndexToTeam(team_index), CENTER_JOIN_PREVENT_QUEUE_TEAM_FAIL), it.netname);
358                         player.team_selected = false;
359                         return;
360                 });
361         }
362
363         SetPlayerTeam(player, team_index, TEAM_CHANGE_MANUAL);
364 }
365
366 bool MoveToTeam(entity client, int team_index, int type)
367 {
368         //PrintToChatAll(sprintf("MoveToTeam: %s, %f", client.netname, team_index));
369         int lockteams_backup = lockteams;  // backup any team lock
370         lockteams = 0;  // disable locked teams
371         if (!SetPlayerTeam(client, team_index, type))
372         {
373                 lockteams = lockteams_backup;  // restore the team lock
374                 return false;
375         }
376         lockteams = lockteams_backup;  // restore the team lock
377         return true;
378 }
379
380 bool Player_HasRealForcedTeam(entity player)
381 {
382         return player.team_forced > TEAM_FORCE_DEFAULT;
383 }
384
385 int Player_GetForcedTeamIndex(entity player)
386 {
387         return player.team_forced;
388 }
389
390 void Player_SetForcedTeamIndex(entity player, int team_index)
391 {
392         switch (team_index)
393         {
394                 case TEAM_FORCE_SPECTATOR:
395                 case TEAM_FORCE_DEFAULT:
396                 {
397                         player.team_forced = team_index;
398                         break;
399                 }
400                 default:
401                 {
402                         if (!Team_IsValidIndex(team_index))
403                         {
404                                 LOG_FATAL("Invalid team index.");
405                         }
406                         else
407                         {
408                                 player.team_forced = team_index;
409                                 break;
410                         }
411                 }
412         }
413 }
414
415 void Player_DetermineForcedTeam(entity player)
416 {
417         if (autocvar_g_campaign)
418         {
419                 if (IS_REAL_CLIENT(player)) // only players, not bots
420                 {
421                         if (Team_IsValidIndex(autocvar_g_campaign_forceteam))
422                         {
423                                 player.team_forced = autocvar_g_campaign_forceteam;
424                         }
425                         else
426                         {
427                                 player.team_forced = TEAM_FORCE_DEFAULT;
428                         }
429                 }
430         }
431         else if (PlayerInList(player, autocvar_g_forced_team_red))
432         {
433                 player.team_forced = 1;
434         }
435         else if (PlayerInList(player, autocvar_g_forced_team_blue))
436         {
437                 player.team_forced = 2;
438         }
439         else if (PlayerInList(player, autocvar_g_forced_team_yellow))
440         {
441                 player.team_forced = 3;
442         }
443         else if (PlayerInList(player, autocvar_g_forced_team_pink))
444         {
445                 player.team_forced = 4;
446         }
447         else
448         {
449                 switch (autocvar_g_forced_team_otherwise)
450                 {
451                         case "red":
452                         {
453                                 player.team_forced = 1;
454                                 break;
455                         }
456                         case "blue":
457                         {
458                                 player.team_forced = 2;
459                                 break;
460                         }
461                         case "yellow":
462                         {
463                                 player.team_forced = 3;
464                                 break;
465                         }
466                         case "pink":
467                         {
468                                 player.team_forced = 4;
469                                 break;
470                         }
471                         case "spectate":
472                         case "spectator":
473                         {
474                                 player.team_forced = TEAM_FORCE_SPECTATOR;
475                                 break;
476                         }
477                         default:
478                         {
479                                 player.team_forced = TEAM_FORCE_DEFAULT;
480                                 break;
481                         }
482                 }
483         }
484         if (!teamplay && Player_HasRealForcedTeam(player))
485         {
486                 player.team_forced = TEAM_FORCE_DEFAULT;
487         }
488 }
489
490 void TeamBalance_JoinBestTeam(entity player)
491 {
492         //PrintToChatAll(sprintf("TeamBalance_JoinBestTeam: %s", player.netname));
493         if (!teamplay)
494         {
495                 return;
496         }
497         if (player.bot_forced_team)
498         {
499                 return;
500         }
501         entity balance = TeamBalance_CheckAllowedTeams(player);
502         if (Player_HasRealForcedTeam(player))
503         {
504                 int forced_team_index = player.team_forced;
505                 bool is_team_allowed = TeamBalance_IsTeamAllowedInternal(balance,
506                         forced_team_index);
507                 TeamBalance_Destroy(balance);
508                 if (!is_team_allowed)
509                 {
510                         return;
511                 }
512                 if (!SetPlayerTeam(player, forced_team_index, TEAM_CHANGE_AUTO))
513                 {
514                         return;
515                 }
516                 return;
517         }
518         int best_team_index = TeamBalance_FindBestTeam(balance, player, true);
519         TeamBalance_Destroy(balance);
520         if (!SetPlayerTeam(player, best_team_index, TEAM_CHANGE_AUTO))
521         {
522                 return;
523         }
524 }
525
526 entity TeamBalance_CheckAllowedTeams(entity for_whom)
527 {
528         entity balance = spawn();
529         for (int i = 0; i < NUM_TEAMS; ++i)
530         {
531                 entity team_ent = balance.m_team_balance_team[i] = spawn();
532                 team_ent.m_team_score = g_team_entities[i].m_team_score;
533                 team_ent.m_num_players = TEAM_NOT_ALLOWED;
534                 team_ent.m_num_bots = 0;
535         }
536         setthink(balance, TeamBalance_Destroy);
537         balance.nextthink = time;
538
539         int teams_mask = 0;
540         string teament_name = string_null;
541         bool mutator_returnvalue = MUTATOR_CALLHOOK(TeamBalance_CheckAllowedTeams,
542                 teams_mask, teament_name, for_whom);
543         teams_mask = M_ARGV(0, float);
544         teament_name = M_ARGV(1, string);
545         if (mutator_returnvalue)
546         {
547                 for (int i = 0; i < NUM_TEAMS; ++i)
548                 {
549                         if (teams_mask & BIT(i))
550                         {
551                                 balance.m_team_balance_team[i].m_num_players = 0;
552                         }
553                 }
554         }
555
556         if (teament_name)
557         {
558                 entity head = find(NULL, classname, teament_name);
559                 while (head)
560                 {
561                         if (Team_IsValidTeam(head.team))
562                         {
563                                 TeamBalance_GetTeam(balance, head.team).m_num_players = 0;
564                         }
565                         head = find(head, classname, teament_name);
566                 }
567         }
568
569         // TODO: Balance quantity of bots across > 2 teams when bot_vs_human is set (and remove next line)
570         if (autocvar_bot_vs_human && AVAILABLE_TEAMS == 2 && for_whom)
571         {
572                 if (autocvar_bot_vs_human > 0)
573                 {
574                         // find last team available
575                         if (IS_BOT_CLIENT(for_whom))
576                         {
577                                 if (TeamBalance_IsTeamAllowedInternal(balance, 4))
578                                 {
579                                         TeamBalance_BanTeamsExcept(balance, 4);
580                                 }
581                                 else if (TeamBalance_IsTeamAllowedInternal(balance, 3))
582                                 {
583                                         TeamBalance_BanTeamsExcept(balance, 3);
584                                 }
585                                 else
586                                 {
587                                         TeamBalance_BanTeamsExcept(balance, 2);
588                                 }
589                                 // no further cases, we know at least 2 teams exist
590                         }
591                         else
592                         {
593                                 if (TeamBalance_IsTeamAllowedInternal(balance, 1))
594                                 {
595                                         TeamBalance_BanTeamsExcept(balance, 1);
596                                 }
597                                 else if (TeamBalance_IsTeamAllowedInternal(balance, 2))
598                                 {
599                                         TeamBalance_BanTeamsExcept(balance, 2);
600                                 }
601                                 else
602                                 {
603                                         TeamBalance_BanTeamsExcept(balance, 3);
604                                 }
605                                 // no further cases, bots have one of the teams
606                         }
607                 }
608                 else
609                 {
610                         // find first team available
611                         if (IS_BOT_CLIENT(for_whom))
612                         {
613                                 if (TeamBalance_IsTeamAllowedInternal(balance, 1))
614                                 {
615                                         TeamBalance_BanTeamsExcept(balance, 1);
616                                 }
617                                 else if (TeamBalance_IsTeamAllowedInternal(balance, 2))
618                                 {
619                                         TeamBalance_BanTeamsExcept(balance, 2);
620                                 }
621                                 else
622                                 {
623                                         TeamBalance_BanTeamsExcept(balance, 3);
624                                 }
625                                 // no further cases, we know at least 2 teams exist
626                         }
627                         else
628                         {
629                                 if (TeamBalance_IsTeamAllowedInternal(balance, 4))
630                                 {
631                                         TeamBalance_BanTeamsExcept(balance, 4);
632                                 }
633                                 else if (TeamBalance_IsTeamAllowedInternal(balance, 3))
634                                 {
635                                         TeamBalance_BanTeamsExcept(balance, 3);
636                                 }
637                                 else
638                                 {
639                                         TeamBalance_BanTeamsExcept(balance, 2);
640                                 }
641                                 // no further cases, bots have one of the teams
642                         }
643                 }
644         }
645
646         if (!for_whom)
647         {
648                 balance.m_team_balance_state = TEAM_BALANCE_TEAMS_CHECKED;
649                 return balance;
650         }
651
652         // if player has a forced team, ONLY allow that one
653         for (int i = 1; i <= NUM_TEAMS; ++i)
654         {
655                 if (for_whom.team_forced == i &&
656                         TeamBalance_IsTeamAllowedInternal(balance, i))
657                 {
658                         TeamBalance_BanTeamsExcept(balance, i);
659                         break;
660                 }
661         }
662         balance.m_team_balance_state = TEAM_BALANCE_TEAMS_CHECKED;
663         return balance;
664 }
665
666 void TeamBalance_Destroy(entity balance)
667 {
668         if (balance == NULL)
669         {
670                 return;
671         }
672         for (int i = 0; i < NUM_TEAMS; ++i)
673         {
674                 delete(balance.(m_team_balance_team[i]));
675         }
676         delete(balance);
677 }
678
679 int TeamBalance_GetAllowedTeams(entity balance)
680 {
681         if (balance == NULL)
682         {
683                 LOG_FATAL("Team balance entity is NULL.");
684         }
685         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
686         {
687                 LOG_FATAL("Team balance entity is not initialized.");
688         }
689         int result = 0;
690         for (int i = 1; i <= NUM_TEAMS; ++i)
691         {
692                 if (TeamBalance_IsTeamAllowedInternal(balance, i))
693                 {
694                         result |= Team_IndexToBit(i);
695                 }
696         }
697         return result;
698 }
699
700 bool TeamBalance_AreEqual(entity ignore, bool would_leave)
701 {
702         entity balance = TeamBalance_CheckAllowedTeams(ignore);
703         TeamBalance_GetTeamCounts(balance, ignore);
704
705         bool equality = true;
706         int total;
707         int prev_total = 0;
708         int bots = 0;
709
710         for(int i = 1; i <= AVAILABLE_TEAMS; ++i)
711         {
712                 total = TeamBalance_GetTeamFromIndex(balance, i).m_num_players;
713                 bots += TeamBalance_GetTeamFromIndex(balance, i).m_num_bots;
714                 if(i > 1 && total != prev_total)
715                 {
716                         equality = false;
717                         break;
718                 }
719                 prev_total = total;
720         }
721         TeamBalance_Destroy(balance);
722
723         // Ignore if there are "ghost" bots that would leave if anyone joined
724         if (would_leave && bots > autocvar_bot_number)
725                 return false;
726
727         return equality;
728 }
729
730 entity remove_countdown;
731 void Remove_Countdown(entity this)
732 {
733         if(this.lifetime <= 0 || TeamBalance_AreEqual(NULL, false))
734         {
735                 if(this.lifetime <= 0)
736                 {
737                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MOVETOSPEC_REMOVE, playername(remove_countdown.enemy.netname, remove_countdown.enemy.team, true));
738                         PutObserverInServer(remove_countdown.enemy, true, true);
739                 }
740
741                 Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_REMOVE);
742
743                 delete(this);
744                 remove_countdown = NULL;
745
746                 TeamBalance_RemoveExcessPlayers(NULL); // Check again for excess players in case someone also left while in countdown
747                 return;
748         }
749
750         --this.lifetime;
751         this.nextthink = time + 1;
752 }
753
754 // FIXME: support more than 2 teams, the notification will be... awkward
755 // FIXME: also don't kick the fc/bc/kc lol
756 void TeamBalance_RemoveExcessPlayers(entity ignore)
757 {
758         if(AVAILABLE_TEAMS != 2 || autocvar_g_campaign) return;
759
760         entity balance = TeamBalance_CheckAllowedTeams(ignore);
761         TeamBalance_GetTeamCounts(balance, ignore);
762
763         int min = 0;
764
765         for(int i = 1; i <= AVAILABLE_TEAMS; ++i)
766         {
767                 int cur = TeamBalance_GetTeamFromIndex(balance, i).m_num_players;
768                 if(i == 1 || cur < min)
769                         min = cur;
770         }
771
772         for(int tmi = 1; tmi <= AVAILABLE_TEAMS; ++tmi)
773         {
774                 int cur = TeamBalance_GetTeamFromIndex(balance, tmi).m_num_players;
775                 if(cur > 0 && cur > min) // If this team has excess players
776                 {
777                         // Get newest player
778                         int latest_join = 0;
779                         entity latest_join_pl = NULL;
780
781                         FOREACH_CLIENT(IS_REAL_CLIENT(it) || INGAME(it), {
782                                 if(it.team == Team_IndexToTeam(tmi) && CS(it).startplaytime > latest_join)
783                                 {
784                                         latest_join = CS(it).startplaytime;
785                                         latest_join_pl = it;
786                                 }
787                         });
788
789                         // Force player to spectate
790                         if(latest_join_pl)
791                         {
792                                 // Send player to spectate
793                                 if(autocvar_g_balance_teams_remove_wait)
794                                 {
795                                         // Give a warning before moving to spect
796                                         if (!remove_countdown)
797                                         {
798                                                 remove_countdown = new_pure(remove_countdown);
799                                                 setthink(remove_countdown, Remove_Countdown);
800                                                 remove_countdown.nextthink = time;
801                                                 Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_MOVETOSPEC_REMOVE, playername(latest_join_pl.netname, latest_join_pl.team, true), autocvar_g_balance_teams_remove_wait);
802                                         }
803                                         remove_countdown.enemy = latest_join_pl;
804                                         remove_countdown.lifetime = autocvar_g_balance_teams_remove_wait;
805                                 }
806                                 else
807                                 {
808                                         // Move to spects immediately
809                                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_MOVETOSPEC_REMOVE, latest_join_pl.netname);
810                                         PutObserverInServer(latest_join_pl, true, true);
811                                 }
812                         }
813                 }
814         }
815
816         TeamBalance_Destroy(balance);
817 }
818
819 bool TeamBalance_QueuedPlayersTagIn(entity ignore)
820 {
821         if (!teamplay)
822                 return true;
823
824         bool balanced = true;
825         int j, teamplayers, teamplayers_max = 0;
826         entity balance = TeamBalance_CheckAllowedTeams(ignore);
827
828         TeamBalance_GetTeamCounts(balance, ignore);
829
830         // find the target player count
831         for (j = 1; j <= AVAILABLE_TEAMS; ++j)
832         {
833                 teamplayers = TeamBalance_GetTeamFromIndex(balance, j).m_num_players;
834                 if(teamplayers > teamplayers_max)
835                         teamplayers_max = teamplayers;
836         }
837
838         for (j = 1; j <= AVAILABLE_TEAMS; ++j)
839         {
840                 teamplayers = TeamBalance_GetTeamFromIndex(balance, j).m_num_players;
841                 // first pass: find player(s) who want to play on this specific team
842                 FOREACH_CLIENT(it != ignore && it.wants_join == j,
843                 {
844                         if (teamplayers >= teamplayers_max)
845                                 break;
846                         Join(it, false);
847                         ++teamplayers;
848                 });
849                 // second pass: find player(s) who want to play on any team
850                 FOREACH_CLIENT(it != ignore && it.wants_join < 0,
851                 {
852                         if (teamplayers >= teamplayers_max)
853                                 break;
854                         Join(it, false);
855                         ++teamplayers;
856                 });
857                 if (teamplayers < teamplayers_max)
858                         balanced = false;
859         }
860
861         TeamBalance_Destroy(balance);
862         return balanced;
863 }
864
865 bool TeamBalance_IsTeamAllowed(entity balance, int index)
866 {
867         if (balance == NULL)
868         {
869                 LOG_FATAL("Team balance entity is NULL.");
870         }
871         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
872         {
873                 LOG_FATAL("Team balance entity is not initialized.");
874         }
875         if (!Team_IsValidIndex(index))
876         {
877                 LOG_FATALF("Team index is invalid: %f",
878                         index);
879         }
880         return TeamBalance_IsTeamAllowedInternal(balance, index);
881 }
882
883 void TeamBalance_GetTeamCounts(entity balance, entity ignore)
884 {
885         if (balance == NULL)
886         {
887                 LOG_FATAL("Team balance entity is NULL.");
888         }
889         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
890         {
891                 LOG_FATAL("Team balance entity is not initialized.");
892         }
893         if (MUTATOR_CALLHOOK(TeamBalance_GetTeamCounts) == true)
894         {
895                 // Mutator has overriden the configuration.
896                 for (int i = 1; i <= NUM_TEAMS; ++i)
897                 {
898                         entity team_ent = TeamBalance_GetTeamFromIndex(balance, i);
899                         if (TeamBalanceTeam_IsAllowed(team_ent))
900                         {
901                                 MUTATOR_CALLHOOK(TeamBalance_GetTeamCount, i, ignore);
902                                 team_ent.m_num_players = M_ARGV(2, float);
903                                 team_ent.m_num_bots = M_ARGV(3, float);
904                         }
905                 }
906         }
907         else
908         {
909                 // Manually count all players.
910                 FOREACH_CLIENT(true,
911                 {
912                         if (it == ignore)
913                         {
914                                 continue;
915                         }
916                         if (it.wants_join)
917                         {
918                                 continue; // Queued players aren't actually in the game.
919                         }
920                         int team_num;
921                         // TODO: Reconsider when the player is truly on the team.
922                         if (IS_CLIENT(it) || INGAME(it))
923                         {
924                                 team_num = it.team;
925                         }
926                         else if (Player_HasRealForcedTeam(it))
927                         {
928                                 // Do we really need this? Probably not.
929                                 team_num = Team_IndexToTeam(it.team_forced); // reserve the spot
930                         }
931                         else
932                         {
933                                 continue;
934                         }
935                         if (!Team_IsValidTeam(team_num))
936                         {
937                                 continue;
938                         }
939                         entity team_ent = TeamBalance_GetTeam(balance, team_num);
940                         if (!TeamBalanceTeam_IsAllowed(team_ent))
941                         {
942                                 continue;
943                         }
944                         ++team_ent.m_num_players;
945                         if (IS_BOT_CLIENT(it))
946                         {
947                                 ++team_ent.m_num_bots;
948                         }
949                 });
950         }
951
952         // if the player who has a forced team has not joined yet, reserve the spot
953         if (autocvar_g_campaign)
954         {
955                 if (Team_IsValidIndex(autocvar_g_campaign_forceteam))
956                 {
957                         entity team_ent = TeamBalance_GetTeamFromIndex(balance,
958                                 autocvar_g_campaign_forceteam);
959                         if (team_ent.m_num_players == team_ent.m_num_bots)
960                         {
961                                 ++team_ent.m_num_players;
962                         }
963                 }
964         }
965         balance.m_team_balance_state = TEAM_BALANCE_TEAM_COUNTS_FILLED;
966 }
967
968 int TeamBalance_GetNumberOfPlayers(entity balance, int index)
969 {
970         if (balance == NULL)
971         {
972                 LOG_FATAL("Team balance entity is NULL.");
973         }
974         if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
975         {
976                 LOG_FATAL("TeamBalance_GetTeamCounts has not been called.");
977         }
978         if (!Team_IsValidIndex(index))
979         {
980                 LOG_FATALF("Team index is invalid: %f", index);
981         }
982         return balance.m_team_balance_team[index - 1].m_num_players;
983 }
984
985 int TeamBalance_FindBestTeam(entity balance, entity player, bool ignore_player)
986 {
987         if (balance == NULL)
988         {
989                 LOG_FATAL("Team balance entity is NULL.");
990         }
991         if (balance.m_team_balance_state == TEAM_BALANCE_UNINITIALIZED)
992         {
993                 LOG_FATAL("Team balance entity is not initialized.");
994         }
995         // count how many players are in each team
996         if (ignore_player)
997         {
998                 TeamBalance_GetTeamCounts(balance, player);
999         }
1000         else
1001         {
1002                 TeamBalance_GetTeamCounts(balance, NULL);
1003         }
1004         int team_bits = TeamBalance_FindBestTeams(balance, player, true);
1005         if (team_bits == 0)
1006         {
1007                 LOG_FATALF("No teams available for %s\n", GetGametype());
1008         }
1009         RandomSelection_Init();
1010         for (int i = 1; i <= NUM_TEAMS; ++i)
1011         {
1012                 if (team_bits & Team_IndexToBit(i))
1013                 {
1014                         RandomSelection_AddFloat(i, 1, 1);
1015                 }
1016         }
1017         return RandomSelection_chosen_float;
1018 }
1019
1020 int TeamBalance_FindBestTeams(entity balance, entity player, bool use_score)
1021 {
1022         if (balance == NULL)
1023         {
1024                 LOG_FATAL("Team balance entity is NULL.");
1025         }
1026         if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
1027         {
1028                 LOG_FATAL("TeamBalance_GetTeamCounts has not been called.");
1029         }
1030         if (MUTATOR_CALLHOOK(TeamBalance_FindBestTeams, player) == true)
1031         {
1032                 return M_ARGV(1, float);
1033         }
1034         int team_bits = 0;
1035         int previous_team = 0;
1036         for (int i = 1; i <= NUM_TEAMS; ++i)
1037         {
1038                 if (!TeamBalance_IsTeamAllowedInternal(balance, i))
1039                 {
1040                         continue;
1041                 }
1042                 if (previous_team == 0)
1043                 {
1044                         team_bits = Team_IndexToBit(i);
1045                         previous_team = i;
1046                         continue;
1047                 }
1048                 int compare = TeamBalance_CompareTeams(balance, i, previous_team,
1049                         player, use_score);
1050                 if (compare == TEAMS_COMPARE_LESS)
1051                 {
1052                         team_bits = Team_IndexToBit(i);
1053                         previous_team = i;
1054                         continue;
1055                 }
1056                 if (compare == TEAMS_COMPARE_EQUAL)
1057                 {
1058                         team_bits |= Team_IndexToBit(i);
1059                         previous_team = i;
1060                 }
1061         }
1062         return team_bits;
1063 }
1064
1065 int TeamBalance_CompareTeams(entity balance, int team_index_a, int team_index_b,
1066         entity player, bool use_score)
1067 {
1068         if (balance == NULL)
1069         {
1070                 LOG_FATAL("Team balance entity is NULL.");
1071         }
1072         if (balance.m_team_balance_state != TEAM_BALANCE_TEAM_COUNTS_FILLED)
1073         {
1074                 LOG_FATAL("TeamBalance_GetTeamCounts has not been called.");
1075         }
1076         if (!Team_IsValidIndex(team_index_a))
1077         {
1078                 LOG_FATALF("team_index_a is invalid: %f",
1079                         team_index_a);
1080         }
1081         if (!Team_IsValidIndex(team_index_b))
1082         {
1083                 LOG_FATALF("team_index_b is invalid: %f",
1084                         team_index_b);
1085         }
1086         if (team_index_a == team_index_b)
1087         {
1088                 return TEAMS_COMPARE_EQUAL;
1089         }
1090         entity team_a = TeamBalance_GetTeamFromIndex(balance, team_index_a);
1091         entity team_b = TeamBalance_GetTeamFromIndex(balance, team_index_b);
1092         return TeamBalance_CompareTeamsInternal(team_a, team_b, player, use_score);
1093 }
1094
1095 void TeamBalance_AutoBalanceBots()
1096 {
1097         // checks disabled because we always want auto-balanced bots
1098         //if (!(autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance))
1099         //      return;
1100
1101         entity balance = TeamBalance_CheckAllowedTeams(NULL);
1102         TeamBalance_GetTeamCounts(balance, NULL);
1103         int smallest_team_index = 0;
1104         int smallest_team_player_count = 0;
1105         for (int i = 1; i <= NUM_TEAMS; ++i)
1106         {
1107                 entity team_ = TeamBalance_GetTeamFromIndex(balance, i);
1108                 if (!TeamBalanceTeam_IsAllowed(team_))
1109                 {
1110                         continue;
1111                 }
1112                 int playercount = TeamBalanceTeam_GetNumberOfPlayers(team_);
1113                 if (smallest_team_index == 0)
1114                 {
1115                         smallest_team_index = i;
1116                         smallest_team_player_count = playercount;
1117                 }
1118                 else if (playercount < smallest_team_player_count)
1119                 {
1120                         smallest_team_index = i;
1121                         smallest_team_player_count = playercount;
1122                 }
1123         }
1124         //PrintToChatAll(sprintf("Smallest team: %f", smallest_team_index));
1125         //PrintToChatAll(sprintf("Smallest team players: %f", smallest_team_player_count));
1126         entity switchable_bot = NULL;
1127         int teams = BITS(NUM_TEAMS);
1128         while (teams != 0)
1129         {
1130                 int largest_team_index = TeamBalance_GetLargestTeamIndex(balance,
1131                         teams);
1132                 if (smallest_team_index == largest_team_index)
1133                 {
1134                         TeamBalance_Destroy(balance);
1135                         return;
1136                 }
1137                 entity largest_team = TeamBalance_GetTeamFromIndex(balance,
1138                         largest_team_index);
1139                 int largest_team_player_count = TeamBalanceTeam_GetNumberOfPlayers(
1140                         largest_team);
1141                 if (largest_team_player_count - smallest_team_player_count < 2)
1142                 {
1143                         TeamBalance_Destroy(balance);
1144                         return;
1145                 }
1146                 //PrintToChatAll(sprintf("Largest team: %f", largest_team_index));
1147                 //PrintToChatAll(sprintf("Largest team players: %f", largest_team_player_count));
1148                 switchable_bot = TeamBalance_GetPlayerForTeamSwitch(largest_team_index,
1149                         smallest_team_index, true);
1150                 if (switchable_bot != NULL)
1151                 {
1152                         break;
1153                 }
1154                 teams &= ~Team_IndexToBit(largest_team_index);
1155         }
1156         TeamBalance_Destroy(balance);
1157         if (switchable_bot == NULL)
1158         {
1159                 //PrintToChatAll("No bot found after searching through all the teams");
1160                 return;
1161         }
1162         SetPlayerTeam(switchable_bot, smallest_team_index, TEAM_CHANGE_AUTO);
1163 }
1164
1165 int TeamBalance_GetLargestTeamIndex(entity balance, int teams)
1166 {
1167         int largest_team_index = 0;
1168         int largest_team_player_count = 0;
1169         for (int i = 1; i <= NUM_TEAMS; ++i)
1170         {
1171                 if (!(Team_IndexToBit(i) & teams))
1172                 {
1173                         continue;
1174                 }
1175                 entity team_ = TeamBalance_GetTeamFromIndex(balance, i);
1176                 if (!TeamBalanceTeam_IsAllowed(team_))
1177                 {
1178                         continue;
1179                 }
1180                 int playercount = TeamBalanceTeam_GetNumberOfPlayers(team_);
1181                 if (largest_team_index == 0)
1182                 {
1183                         largest_team_index = i;
1184                         largest_team_player_count = playercount;
1185                 }
1186                 else if (playercount > largest_team_player_count)
1187                 {
1188                         largest_team_index = i;
1189                         largest_team_player_count = playercount;
1190                 }
1191         }
1192         return largest_team_index;
1193 }
1194
1195 entity TeamBalance_GetPlayerForTeamSwitch(int source_team_index,
1196         int destination_team_index, bool is_bot)
1197 {
1198         if (MUTATOR_CALLHOOK(TeamBalance_GetPlayerForTeamSwitch, source_team_index,
1199                 destination_team_index, is_bot))
1200         {
1201                 return M_ARGV(3, entity);
1202         }
1203         entity lowest_player = NULL;
1204         float lowest_score = FLOAT_MAX;
1205         FOREACH_CLIENT(Entity_GetTeamIndex(it) == source_team_index,
1206         {
1207                 if (IS_BOT_CLIENT(it) != is_bot)
1208                 {
1209                         continue;
1210                 }
1211                 float temp_score = PlayerScore_Get(it, SP_SCORE);
1212                 if (temp_score >= lowest_score)
1213                 {
1214                         continue;
1215                 }
1216                 //PrintToChatAll(sprintf(
1217                 //      "Found %s with lowest score, checking allowed teams", it.netname));
1218                 entity balance = TeamBalance_CheckAllowedTeams(it);
1219                 if (TeamBalance_IsTeamAllowed(balance, source_team_index))
1220                 {
1221                         //PrintToChatAll("Allowed");
1222                         lowest_player = it;
1223                         lowest_score = temp_score;
1224                 }
1225                 else
1226                 {
1227                         //PrintToChatAll("Not allowed");
1228                 }
1229                 TeamBalance_Destroy(balance);
1230         });
1231         return lowest_player;
1232 }
1233
1234 void LogTeamChange(float player_id, float team_number, int type)
1235 {
1236         if (!autocvar_sv_eventlog)
1237         {
1238                 return;
1239         }
1240         if (player_id < 1)
1241         {
1242                 return;
1243         }
1244         GameLogEcho(sprintf(":team:%d:%d:%d", player_id, team_number, type));
1245 }
1246
1247 void KillPlayerForTeamChange(entity player)
1248 {
1249         if (IS_DEAD(player))
1250         {
1251                 return;
1252         }
1253         if (MUTATOR_CALLHOOK(Player_ChangeTeamKill, player) == true)
1254         {
1255                 return;
1256         }
1257         Damage(player, player, player, 100000, DEATH_TEAMCHANGE.m_id, DMG_NOWEP,
1258                 player.origin, '0 0 0');
1259 }
1260
1261 bool TeamBalance_IsTeamAllowedInternal(entity balance, int index)
1262 {
1263         return balance.m_team_balance_team[index - 1].m_num_players !=
1264                 TEAM_NOT_ALLOWED;
1265 }
1266
1267 void TeamBalance_BanTeamsExcept(entity balance, int index)
1268 {
1269         for (int i = 1; i <= NUM_TEAMS; ++i)
1270         {
1271                 if (i != index)
1272                 {
1273                         balance.m_team_balance_team[i - 1].m_num_players = TEAM_NOT_ALLOWED;
1274                 }
1275         }
1276 }
1277
1278 entity TeamBalance_GetTeamFromIndex(entity balance, int index)
1279 {
1280         if (!Team_IsValidIndex(index))
1281         {
1282                 LOG_FATALF("Index is invalid: %f", index);
1283         }
1284         return balance.m_team_balance_team[index - 1];
1285 }
1286
1287 entity TeamBalance_GetTeam(entity balance, int team_num)
1288 {
1289         return TeamBalance_GetTeamFromIndex(balance, Team_TeamToIndex(team_num));
1290 }
1291
1292 bool TeamBalanceTeam_IsAllowed(entity team_ent)
1293 {
1294         return team_ent.m_num_players != TEAM_NOT_ALLOWED;
1295 }
1296
1297 int TeamBalanceTeam_GetNumberOfPlayers(entity team_ent)
1298 {
1299         return team_ent.m_num_players;
1300 }
1301
1302 int TeamBalanceTeam_GetNumberOfBots(entity team_ent)
1303 {
1304         return team_ent.m_num_bots;
1305 }
1306
1307 int TeamBalance_CompareTeamsInternal(entity team_a, entity team_b,
1308         entity player, bool use_score)
1309 {
1310         if (team_a == team_b)
1311         {
1312                 return TEAMS_COMPARE_EQUAL;
1313         }
1314         if (!TeamBalanceTeam_IsAllowed(team_a) ||
1315                 !TeamBalanceTeam_IsAllowed(team_b))
1316         {
1317                 return TEAMS_COMPARE_INVALID;
1318         }
1319         int num_players_team_a = team_a.m_num_players;
1320         int num_players_team_b = team_b.m_num_players;
1321         if (IS_REAL_CLIENT(player) && bots_would_leave)
1322         {
1323                 num_players_team_a -= team_a.m_num_bots;
1324                 num_players_team_b -= team_b.m_num_bots;
1325         }
1326         if (num_players_team_a < num_players_team_b)
1327         {
1328                 return TEAMS_COMPARE_LESS;
1329         }
1330         if (num_players_team_a > num_players_team_b)
1331         {
1332                 return TEAMS_COMPARE_GREATER;
1333         }
1334         if (!use_score)
1335         {
1336                 return TEAMS_COMPARE_EQUAL;
1337         }
1338         if (team_a.m_team_score < team_b.m_team_score)
1339         {
1340                 return TEAMS_COMPARE_LESS;
1341         }
1342         if (team_a.m_team_score > team_b.m_team_score)
1343         {
1344                 return TEAMS_COMPARE_GREATER;
1345         }
1346         return TEAMS_COMPARE_EQUAL;
1347 }
1348
1349 void SV_ChangeTeam(entity player, int new_color)
1350 {
1351         if (!teamplay)
1352         {
1353                 SetPlayerColors(player, new_color);
1354         }
1355         if(!IS_CLIENT(player))
1356         {
1357                 return;
1358         }
1359         if (!teamplay)
1360         {
1361                 return;
1362         }
1363         Player_SetTeamIndexChecked(player, Team_TeamToIndex((new_color & 0x0F) + 1));
1364 }