]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/teamplay.qc
Merge branch 'martin-t/units' into 'master'
[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 void TeamchangeFrags(entity e)
19 {
20         PlayerScore_Clear(e);
21 }
22
23 void LogTeamchange(float player_id, float team_number, float type)
24 {
25         if(!autocvar_sv_eventlog)
26                 return;
27
28         if(player_id < 1)
29                 return;
30
31         GameLogEcho(strcat(":team:", ftos(player_id), ":", ftos(team_number), ":", ftos(type)));
32 }
33
34 void default_delayedinit(entity this)
35 {
36         if(!scores_initialized)
37                 ScoreRules_generic();
38 }
39
40 void ActivateTeamplay()
41 {
42         serverflags |= SERVERFLAG_TEAMPLAY;
43         teamplay = 1;
44         cvar_set("teamplay", "2");  // DP needs this for sending proper getstatus replies.
45 }
46
47 void InitGameplayMode()
48 {
49         VoteReset();
50
51         // find out good world mins/maxs bounds, either the static bounds found by looking for solid, or the mapinfo specified bounds
52         get_mi_min_max(1);
53         world.mins = mi_min;
54         world.maxs = mi_max;
55         // currently, NetRadiant's limit is 131072 qu for each side
56         // distance from one corner of a 131072qu cube to the opposite corner is approx. 227023 qu
57         // set the distance according to map size but don't go over the limit to avoid issues with float precision
58         // in case somebody makes extremely large maps
59         max_shot_distance = min(230000, vlen(world.maxs - world.mins));
60
61         MapInfo_LoadMapSettings(mapname);
62         serverflags &= ~SERVERFLAG_TEAMPLAY;
63         teamplay = 0;
64         cvar_set("teamplay", "0");  // DP needs this for sending proper getstatus replies.
65
66         if (!cvar_value_issafe(world.fog))
67         {
68                 LOG_INFO("The current map contains a potentially harmful fog setting, ignored\n");
69                 world.fog = string_null;
70         }
71         if(MapInfo_Map_fog != "")
72                 if(MapInfo_Map_fog == "none")
73                         world.fog = string_null;
74                 else
75                         world.fog = strzone(MapInfo_Map_fog);
76         clientstuff = strzone(MapInfo_Map_clientstuff);
77
78         MapInfo_ClearTemps();
79
80         gamemode_name = MapInfo_Type_ToText(MapInfo_LoadedGametype);
81
82         cache_mutatormsg = strzone("");
83         cache_lastmutatormsg = strzone("");
84
85         InitializeEntity(NULL, default_delayedinit, INITPRIO_GAMETYPE_FALLBACK);
86 }
87
88 string GetClientVersionMessage(entity this)
89 {
90         if (CS(this).version_mismatch) {
91                 if(CS(this).version < autocvar_gameversion) {
92                         return strcat("This is Xonotic ", autocvar_g_xonoticversion,
93                                 "\n^3Your client version is outdated.\n\n\n### YOU WON'T BE ABLE TO PLAY ON THIS SERVER ###\n\n\nPlease update!!!^8");
94                 } else {
95                         return strcat("This is Xonotic ", autocvar_g_xonoticversion,
96                                 "\n^3This server is using an outdated Xonotic version.\n\n\n ### THIS SERVER IS INCOMPATIBLE AND THUS YOU CANNOT JOIN ###.^8");
97                 }
98         } else {
99                 return strcat("Welcome to Xonotic ", autocvar_g_xonoticversion);
100         }
101 }
102
103 string getwelcomemessage(entity this)
104 {
105         MUTATOR_CALLHOOK(BuildMutatorsPrettyString, "");
106         string modifications = M_ARGV(0, string);
107
108         if(g_weaponarena)
109         {
110                 if(g_weaponarena_random)
111                         modifications = strcat(modifications, ", ", ftos(g_weaponarena_random), " of ", g_weaponarena_list, " Arena");
112                 else
113                         modifications = strcat(modifications, ", ", g_weaponarena_list, " Arena");
114         }
115         else if(cvar("g_balance_blaster_weaponstartoverride") == 0)
116                 modifications = strcat(modifications, ", No start weapons");
117         if(cvar("sv_gravity") < stof(cvar_defstring("sv_gravity")))
118                 modifications = strcat(modifications, ", Low gravity");
119         if(g_weapon_stay && !g_cts)
120                 modifications = strcat(modifications, ", Weapons stay");
121         if(g_jetpack)
122                 modifications = strcat(modifications, ", Jet pack");
123         if(autocvar_g_powerups == 0)
124                 modifications = strcat(modifications, ", No powerups");
125         if(autocvar_g_powerups > 0)
126                 modifications = strcat(modifications, ", Powerups");
127         modifications = substring(modifications, 2, strlen(modifications) - 2);
128
129         string versionmessage = GetClientVersionMessage(this);
130         string s = strcat(versionmessage, "^8\n^8\nmatch type is ^1", gamemode_name, "^8\n");
131
132         if(modifications != "")
133                 s = strcat(s, "^8\nactive modifications: ^3", modifications, "^8\n");
134
135         if(cache_lastmutatormsg != autocvar_g_mutatormsg)
136         {
137                 if(cache_lastmutatormsg)
138                         strunzone(cache_lastmutatormsg);
139                 if(cache_mutatormsg)
140                         strunzone(cache_mutatormsg);
141                 cache_lastmutatormsg = strzone(autocvar_g_mutatormsg);
142                 cache_mutatormsg = strzone(cache_lastmutatormsg);
143         }
144
145         if (cache_mutatormsg != "") {
146                 s = strcat(s, "\n\n^8special gameplay tips: ^7", cache_mutatormsg);
147         }
148
149         string mutator_msg = "";
150         MUTATOR_CALLHOOK(BuildGameplayTipsString, mutator_msg);
151         mutator_msg = M_ARGV(0, string);
152
153         s = strcat(s, mutator_msg); // trust that the mutator will do proper formatting
154
155         string motd = autocvar_sv_motd;
156         if (motd != "") {
157                 s = strcat(s, "\n\n^8MOTD: ^7", strreplace("\\n", "\n", motd));
158         }
159         return s;
160 }
161
162 void setcolor(entity this, int clr)
163 {
164 #if 0
165         this.clientcolors = clr;
166         this.team = (clr & 15) + 1;
167 #else
168         builtin_setcolor(this, clr);
169 #endif
170 }
171
172 void SetPlayerColors(entity pl, float _color)
173 {
174         /*string s;
175         s = ftos(cl);
176         stuffcmd(pl, strcat("color ", s, " ", s, "\n")  );
177         pl.team = cl + 1;
178         //pl.clientcolors = pl.clientcolors - (pl.clientcolors & 15) + cl;
179         pl.clientcolors = 16*cl + cl;*/
180
181         float pants, shirt;
182         pants = _color & 0x0F;
183         shirt = _color & 0xF0;
184
185
186         if(teamplay) {
187                 setcolor(pl, 16*pants + pants);
188         } else {
189                 setcolor(pl, shirt + pants);
190         }
191 }
192
193 void SetPlayerTeam(entity pl, float t, float s, float noprint)
194 {
195         float _color;
196
197         if(t == 4)
198                 _color = NUM_TEAM_4 - 1;
199         else if(t == 3)
200                 _color = NUM_TEAM_3 - 1;
201         else if(t == 2)
202                 _color = NUM_TEAM_2 - 1;
203         else
204                 _color = NUM_TEAM_1 - 1;
205
206         SetPlayerColors(pl,_color);
207
208         if(t != s) {
209                 LogTeamchange(pl.playerid, pl.team, 3);  // log manual team join
210
211                 if(!noprint)
212                         bprint(playername(pl, false), "^7 has changed from ", Team_NumberToColoredFullName(s), "^7 to ", Team_NumberToColoredFullName(t), "\n");
213         }
214
215 }
216
217 // set c1...c4 to show what teams are allowed
218 void CheckAllowedTeams (entity for_whom)
219 {
220         int teams_mask = 0;
221
222         c1 = c2 = c3 = c4 = -1;
223         cb1 = cb2 = cb3 = cb4 = 0;
224
225         string teament_name = string_null;
226
227         bool mutator_returnvalue = MUTATOR_CALLHOOK(CheckAllowedTeams, teams_mask, teament_name, for_whom);
228         teams_mask = M_ARGV(0, float);
229         teament_name = M_ARGV(1, string);
230
231         if(!mutator_returnvalue)
232         {
233                 if(teams_mask & BIT(0)) c1 = 0;
234                 if(teams_mask & BIT(1)) c2 = 0;
235                 if(teams_mask & BIT(2)) c3 = 0;
236                 if(teams_mask & BIT(3)) c4 = 0;
237         }
238
239         // find out what teams are allowed if necessary
240         if(teament_name)
241         {
242                 entity head = find(NULL, classname, teament_name);
243                 while(head)
244                 {
245                         switch(head.team)
246                         {
247                                 case NUM_TEAM_1: c1 = 0; break;
248                                 case NUM_TEAM_2: c2 = 0; break;
249                                 case NUM_TEAM_3: c3 = 0; break;
250                                 case NUM_TEAM_4: c4 = 0; break;
251                         }
252
253                         head = find(head, classname, teament_name);
254                 }
255         }
256
257         // TODO: Balance quantity of bots across > 2 teams when bot_vs_human is set (and remove next line)
258         if(AvailableTeams() == 2)
259         if(autocvar_bot_vs_human && for_whom)
260         {
261                 if(autocvar_bot_vs_human > 0)
262                 {
263                         // find last team available
264
265                         if(IS_BOT_CLIENT(for_whom))
266                         {
267                                 if(c4 >= 0) { c3 = c2 = c1 = -1; }
268                                 else if(c3 >= 0) { c4 = c2 = c1 = -1; }
269                                 else { c4 = c3 = c1 = -1; }
270                                 // no further cases, we know at least 2 teams exist
271                         }
272                         else
273                         {
274                                 if(c1 >= 0) { c2 = c3 = c4 = -1; }
275                                 else if(c2 >= 0) { c1 = c3 = c4 = -1; }
276                                 else { c1 = c2 = c4 = -1; }
277                                 // no further cases, bots have one of the teams
278                         }
279                 }
280                 else
281                 {
282                         // find first team available
283
284                         if(IS_BOT_CLIENT(for_whom))
285                         {
286                                 if(c1 >= 0) { c2 = c3 = c4 = -1; }
287                                 else if(c2 >= 0) { c1 = c3 = c4 = -1; }
288                                 else { c1 = c2 = c4 = -1; }
289                                 // no further cases, we know at least 2 teams exist
290                         }
291                         else
292                         {
293                                 if(c4 >= 0) { c3 = c2 = c1 = -1; }
294                                 else if(c3 >= 0) { c4 = c2 = c1 = -1; }
295                                 else { c4 = c3 = c1 = -1; }
296                                 // no further cases, bots have one of the teams
297                         }
298                 }
299         }
300
301         if(!for_whom)
302                 return;
303
304         // if player has a forced team, ONLY allow that one
305         if(for_whom.team_forced == NUM_TEAM_1 && c1 >= 0)
306                 c2 = c3 = c4 = -1;
307         else if(for_whom.team_forced == NUM_TEAM_2 && c2 >= 0)
308                 c1 = c3 = c4 = -1;
309         else if(for_whom.team_forced == NUM_TEAM_3 && c3 >= 0)
310                 c1 = c2 = c4 = -1;
311         else if(for_whom.team_forced == NUM_TEAM_4 && c4 >= 0)
312                 c1 = c2 = c3 = -1;
313 }
314
315 float PlayerValue(entity p)
316 {
317         return 1;
318         // FIXME: it always returns 1...
319 }
320
321 // c1...c4 should be set to -1 (not allowed) or 0 (allowed).
322 // teams that are allowed will now have their player counts stored in c1...c4
323 void GetTeamCounts(entity ignore)
324 {
325         float value, bvalue;
326         // now count how many players are on each team already
327
328         // FIXME: also find and memorize the lowest-scoring bot on each team (in case players must be shuffled around)
329         // also remember the lowest-scoring player
330
331         FOREACH_CLIENT(true, LAMBDA(
332                 float t;
333                 if(IS_PLAYER(it) || it.caplayer)
334                         t = it.team;
335                 else if(it.team_forced > 0)
336                         t = it.team_forced; // reserve the spot
337                 else
338                         continue;
339                 if(it != ignore)// && it.netname != "")
340                 {
341                         value = PlayerValue(it);
342                         if(IS_BOT_CLIENT(it))
343                                 bvalue = value;
344                         else
345                                 bvalue = 0;
346                         if(t == NUM_TEAM_1)
347                         {
348                                 if(c1 >= 0)
349                                 {
350                                         c1 = c1 + value;
351                                         cb1 = cb1 + bvalue;
352                                 }
353                         }
354                         else if(t == NUM_TEAM_2)
355                         {
356                                 if(c2 >= 0)
357                                 {
358                                         c2 = c2 + value;
359                                         cb2 = cb2 + bvalue;
360                                 }
361                         }
362                         else if(t == NUM_TEAM_3)
363                         {
364                                 if(c3 >= 0)
365                                 {
366                                         c3 = c3 + value;
367                                         cb3 = cb3 + bvalue;
368                                 }
369                         }
370                         else if(t == NUM_TEAM_4)
371                         {
372                                 if(c4 >= 0)
373                                 {
374                                         c4 = c4 + value;
375                                         cb4 = cb4 + bvalue;
376                                 }
377                         }
378                 }
379         ));
380
381         // if the player who has a forced team has not joined yet, reserve the spot
382         if(autocvar_g_campaign)
383         {
384                 switch(autocvar_g_campaign_forceteam)
385                 {
386                         case 1: if(c1 == cb1) ++c1; break;
387                         case 2: if(c2 == cb2) ++c2; break;
388                         case 3: if(c3 == cb3) ++c3; break;
389                         case 4: if(c4 == cb4) ++c4; break;
390                 }
391         }
392 }
393
394 float TeamSmallerEqThanTeam(float ta, float tb, entity e)
395 {
396         // we assume that CheckAllowedTeams and GetTeamCounts have already been called
397         float f;
398         float ca = -1, cb = -1, cba = 0, cbb = 0, sa = 0, sb = 0;
399
400         switch(ta)
401         {
402                 case 1: ca = c1; cba = cb1; sa = team1_score; break;
403                 case 2: ca = c2; cba = cb2; sa = team2_score; break;
404                 case 3: ca = c3; cba = cb3; sa = team3_score; break;
405                 case 4: ca = c4; cba = cb4; sa = team4_score; break;
406         }
407         switch(tb)
408         {
409                 case 1: cb = c1; cbb = cb1; sb = team1_score; break;
410                 case 2: cb = c2; cbb = cb2; sb = team2_score; break;
411                 case 3: cb = c3; cbb = cb3; sb = team3_score; break;
412                 case 4: cb = c4; cbb = cb4; sb = team4_score; break;
413         }
414
415         // invalid
416         if(ca < 0 || cb < 0)
417                 return false;
418
419         // equal
420         if(ta == tb)
421                 return true;
422
423         if(IS_REAL_CLIENT(e))
424         {
425                 if(bots_would_leave)
426                 {
427                         ca -= cba * 0.999;
428                         cb -= cbb * 0.999;
429                 }
430         }
431
432         // keep teams alive (teams of size 0 always count as smaller, ignoring score)
433         if(ca < 1)
434                 if(cb >= 1)
435                         return true;
436         if(ca >= 1)
437                 if(cb < 1)
438                         return false;
439
440         // first, normalize
441         f = max(ca, cb, 1);
442         ca /= f;
443         cb /= f;
444         f = max(sa, sb, 1);
445         sa /= f;
446         sb /= f;
447
448         // the more we're at the end of the match, the more take scores into account
449         f = bound(0, game_completion_ratio * autocvar_g_balance_teams_scorefactor, 1);
450         ca += (sa - ca) * f;
451         cb += (sb - cb) * f;
452
453         return ca <= cb;
454 }
455
456 // returns # of smallest team (1, 2, 3, 4)
457 // NOTE: Assumes CheckAllowedTeams has already been called!
458 float FindSmallestTeam(entity pl, float ignore_pl)
459 {
460         int totalteams = 0;
461         int t = 1; // initialize with a random team?
462         if(c4 >= 0) t = 4;
463         if(c3 >= 0) t = 3;
464         if(c2 >= 0) t = 2;
465         if(c1 >= 0) t = 1;
466
467         // find out what teams are available
468         //CheckAllowedTeams();
469
470         // make sure there are at least 2 teams to join
471         if(c1 >= 0)
472                 totalteams = totalteams + 1;
473         if(c2 >= 0)
474                 totalteams = totalteams + 1;
475         if(c3 >= 0)
476                 totalteams = totalteams + 1;
477         if(c4 >= 0)
478                 totalteams = totalteams + 1;
479
480         if((autocvar_bot_vs_human || pl.team_forced > 0) && totalteams == 1)
481                 totalteams += 1;
482
483         if(totalteams <= 1)
484         {
485                 if(autocvar_g_campaign && pl && IS_REAL_CLIENT(pl))
486                         return 1; // special case for campaign and player joining
487                 else if(totalteams == 1) // single team
488                         LOG_TRACEF("Only 1 team available for %s, you may need to fix your map", MapInfo_Type_ToString(MapInfo_CurrentGametype()));
489                 else // no teams, major no no
490                         error(sprintf("No teams available for %s\n", MapInfo_Type_ToString(MapInfo_CurrentGametype())));
491         }
492
493         // count how many players are in each team
494         if(ignore_pl)
495                 GetTeamCounts(pl);
496         else
497                 GetTeamCounts(NULL);
498
499         RandomSelection_Init();
500
501         if(TeamSmallerEqThanTeam(1, t, pl))
502                 t = 1;
503         if(TeamSmallerEqThanTeam(2, t, pl))
504                 t = 2;
505         if(TeamSmallerEqThanTeam(3, t, pl))
506                 t = 3;
507         if(TeamSmallerEqThanTeam(4, t, pl))
508                 t = 4;
509
510         // now t is the minimum, or A minimum!
511         if(t == 1 || TeamSmallerEqThanTeam(1, t, pl))
512                 RandomSelection_AddFloat(1, 1, 1);
513         if(t == 2 || TeamSmallerEqThanTeam(2, t, pl))
514                 RandomSelection_AddFloat(2, 1, 1);
515         if(t == 3 || TeamSmallerEqThanTeam(3, t, pl))
516                 RandomSelection_AddFloat(3, 1, 1);
517         if(t == 4 || TeamSmallerEqThanTeam(4, t, pl))
518                 RandomSelection_AddFloat(4, 1, 1);
519
520         return RandomSelection_chosen_float;
521 }
522
523 int JoinBestTeam(entity this, bool only_return_best, bool forcebestteam)
524 {
525         float smallest, selectedteam;
526
527         // don't join a team if we're not playing a team game
528         if(!teamplay)
529                 return 0;
530
531         // find out what teams are available
532         CheckAllowedTeams(this);
533
534         // if we don't care what team he ends up on, put him on whatever team he entered as.
535         // if he's not on a valid team, then let other code put him on the smallest team
536         if(!forcebestteam)
537         {
538                 if(     c1 >= 0 && this.team == NUM_TEAM_1)
539                         selectedteam = this.team;
540                 else if(c2 >= 0 && this.team == NUM_TEAM_2)
541                         selectedteam = this.team;
542                 else if(c3 >= 0 && this.team == NUM_TEAM_3)
543                         selectedteam = this.team;
544                 else if(c4 >= 0 && this.team == NUM_TEAM_4)
545                         selectedteam = this.team;
546                 else
547                         selectedteam = -1;
548
549                 if(selectedteam > 0)
550                 {
551                         if(!only_return_best)
552                         {
553                                 SetPlayerColors(this, selectedteam - 1);
554
555                                 // when JoinBestTeam is called by client.qc/ClientKill_Now_TeamChange the players team is -1 and thus skipped
556                                 // when JoinBestTeam is called by client.qc/ClientConnect the player_id is 0 the log attempt is rejected
557                                 LogTeamchange(this.playerid, this.team, 99);
558                         }
559                         return selectedteam;
560                 }
561                 // otherwise end up on the smallest team (handled below)
562         }
563
564         smallest = FindSmallestTeam(this, true);
565
566         if(!only_return_best && !this.bot_forced_team)
567         {
568                 TeamchangeFrags(this);
569                 if(smallest == 1)
570                 {
571                         SetPlayerColors(this, NUM_TEAM_1 - 1);
572                 }
573                 else if(smallest == 2)
574                 {
575                         SetPlayerColors(this, NUM_TEAM_2 - 1);
576                 }
577                 else if(smallest == 3)
578                 {
579                         SetPlayerColors(this, NUM_TEAM_3 - 1);
580                 }
581                 else if(smallest == 4)
582                 {
583                         SetPlayerColors(this, NUM_TEAM_4 - 1);
584                 }
585                 else
586                 {
587                         error("smallest team: invalid team\n");
588                 }
589
590                 LogTeamchange(this.playerid, this.team, 2); // log auto join
591
592                 if(!IS_DEAD(this))
593                         Damage(this, this, this, 100000, DEATH_TEAMCHANGE.m_id, this.origin, '0 0 0');
594         }
595
596         return smallest;
597 }
598
599 //void() ctf_playerchanged;
600 void SV_ChangeTeam(entity this, float _color)
601 {
602         float scolor, dcolor, steam, dteam; //, dbotcount, scount, dcount;
603
604         // in normal deathmatch we can just apply the color and we're done
605         if(!teamplay)
606                 SetPlayerColors(this, _color);
607
608         if(!IS_CLIENT(this))
609         {
610                 // since this is an engine function, and gamecode doesn't have any calls earlier than this, do the connecting message here
611                 Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CONNECTING, this.netname);
612                 return;
613         }
614
615         if(!teamplay)
616                 return;
617
618         scolor = this.clientcolors & 0x0F;
619         dcolor = _color & 0x0F;
620
621         if(scolor == NUM_TEAM_1 - 1)
622                 steam = 1;
623         else if(scolor == NUM_TEAM_2 - 1)
624                 steam = 2;
625         else if(scolor == NUM_TEAM_3 - 1)
626                 steam = 3;
627         else // if(scolor == NUM_TEAM_4 - 1)
628                 steam = 4;
629         if(dcolor == NUM_TEAM_1 - 1)
630                 dteam = 1;
631         else if(dcolor == NUM_TEAM_2 - 1)
632                 dteam = 2;
633         else if(dcolor == NUM_TEAM_3 - 1)
634                 dteam = 3;
635         else // if(dcolor == NUM_TEAM_4 - 1)
636                 dteam = 4;
637
638         CheckAllowedTeams(this);
639
640         if(dteam == 1 && c1 < 0) dteam = 4;
641         if(dteam == 4 && c4 < 0) dteam = 3;
642         if(dteam == 3 && c3 < 0) dteam = 2;
643         if(dteam == 2 && c2 < 0) dteam = 1;
644
645         // not changing teams
646         if(scolor == dcolor)
647         {
648                 //bprint("same team change\n");
649                 SetPlayerTeam(this, dteam, steam, true);
650                 return;
651         }
652
653         if((autocvar_g_campaign) || (autocvar_g_changeteam_banned && CS(this).wasplayer)) {
654                 Send_Notification(NOTIF_ONE, this, MSG_INFO, INFO_TEAMCHANGE_NOTALLOWED);
655                 return; // changing teams is not allowed
656         }
657
658         // autocvar_g_balance_teams_prevent_imbalance only makes sense if autocvar_g_balance_teams is on, as it makes the team selection dialog pointless
659         if(autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance)
660         {
661                 GetTeamCounts(this);
662                 if(!TeamSmallerEqThanTeam(dteam, steam, this))
663                 {
664                         Send_Notification(NOTIF_ONE, this, MSG_INFO, INFO_TEAMCHANGE_LARGERTEAM);
665                         return;
666                 }
667         }
668
669 //      bprint("allow change teams from ", ftos(steam), " to ", ftos(dteam), "\n");
670
671         if(IS_PLAYER(this) && steam != dteam)
672         {
673                 // reduce frags during a team change
674                 TeamchangeFrags(this);
675         }
676
677         MUTATOR_CALLHOOK(Player_ChangeTeam, this, steam, dteam);
678
679         SetPlayerTeam(this, dteam, steam, !IS_CLIENT(this));
680
681         if(IS_PLAYER(this) && steam != dteam)
682         {
683                 // kill player when changing teams
684                 if(!IS_DEAD(this))
685                         Damage(this, this, this, 100000, DEATH_TEAMCHANGE.m_id, this.origin, '0 0 0');
686         }
687 }
688
689 void ShufflePlayerOutOfTeam (float source_team)
690 {
691         float smallestteam, smallestteam_count, steam;
692         float lowest_bot_score, lowest_player_score;
693         entity lowest_bot, lowest_player, selected;
694
695         smallestteam = 0;
696         smallestteam_count = 999999999;
697
698         if(c1 >= 0 && c1 < smallestteam_count)
699         {
700                 smallestteam = 1;
701                 smallestteam_count = c1;
702         }
703         if(c2 >= 0 && c2 < smallestteam_count)
704         {
705                 smallestteam = 2;
706                 smallestteam_count = c2;
707         }
708         if(c3 >= 0 && c3 < smallestteam_count)
709         {
710                 smallestteam = 3;
711                 smallestteam_count = c3;
712         }
713         if(c4 >= 0 && c4 < smallestteam_count)
714         {
715                 smallestteam = 4;
716                 smallestteam_count = c4;
717         }
718
719         if(!smallestteam)
720         {
721                 bprint("warning: no smallest team\n");
722                 return;
723         }
724
725         if(source_team == 1)
726                 steam = NUM_TEAM_1;
727         else if(source_team == 2)
728                 steam = NUM_TEAM_2;
729         else if(source_team == 3)
730                 steam = NUM_TEAM_3;
731         else // if(source_team == 4)
732                 steam = NUM_TEAM_4;
733
734         lowest_bot = NULL;
735         lowest_bot_score = 999999999;
736         lowest_player = NULL;
737         lowest_player_score = 999999999;
738
739         // find the lowest-scoring player & bot of that team
740         FOREACH_CLIENT(IS_PLAYER(it) && it.team == steam, LAMBDA(
741                 if(it.isbot)
742                 {
743                         if(it.totalfrags < lowest_bot_score)
744                         {
745                                 lowest_bot = it;
746                                 lowest_bot_score = it.totalfrags;
747                         }
748                 }
749                 else
750                 {
751                         if(it.totalfrags < lowest_player_score)
752                         {
753                                 lowest_player = it;
754                                 lowest_player_score = it.totalfrags;
755                         }
756                 }
757         ));
758
759         // prefers to move a bot...
760         if(lowest_bot != NULL)
761                 selected = lowest_bot;
762         // but it will move a player if it has to
763         else
764                 selected = lowest_player;
765         // don't do anything if it couldn't find anyone
766         if(!selected)
767         {
768                 bprint("warning: couldn't find a player to move from team\n");
769                 return;
770         }
771
772         // smallest team gains a member
773         if(smallestteam == 1)
774         {
775                 c1 = c1 + 1;
776         }
777         else if(smallestteam == 2)
778         {
779                 c2 = c2 + 1;
780         }
781         else if(smallestteam == 3)
782         {
783                 c3 = c3 + 1;
784         }
785         else if(smallestteam == 4)
786         {
787                 c4 = c4 + 1;
788         }
789         else
790         {
791                 bprint("warning: destination team invalid\n");
792                 return;
793         }
794         // source team loses a member
795         if(source_team == 1)
796         {
797                 c1 = c1 + 1;
798         }
799         else if(source_team == 2)
800         {
801                 c2 = c2 + 2;
802         }
803         else if(source_team == 3)
804         {
805                 c3 = c3 + 3;
806         }
807         else if(source_team == 4)
808         {
809                 c4 = c4 + 4;
810         }
811         else
812         {
813                 bprint("warning: source team invalid\n");
814                 return;
815         }
816
817         // move the player to the new team
818         TeamchangeFrags(selected);
819         SetPlayerTeam(selected, smallestteam, source_team, false);
820
821         if(!IS_DEAD(selected))
822                 Damage(selected, selected, selected, 100000, DEATH_AUTOTEAMCHANGE.m_id, selected.origin, '0 0 0');
823         Send_Notification(NOTIF_ONE, selected, MSG_CENTER, CENTER_DEATH_SELF_AUTOTEAMCHANGE, selected.team);
824 }