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