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