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