]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/teamplay.qc
Merge branch 'master' into Mario/ons_updates
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / teamplay.qc
1 #include "teamplay.qh"
2
3 void TeamchangeFrags(entity e)
4 {
5         PlayerScore_Clear(e);
6 }
7
8 void LogTeamchange(float player_id, float team_number, float type)
9 {
10         if(!autocvar_sv_eventlog)
11                 return;
12
13         if(player_id < 1)
14                 return;
15
16         GameLogEcho(strcat(":team:", ftos(player_id), ":", ftos(team_number), ":", ftos(type)));
17 }
18
19 void default_delayedinit()
20 {
21         if(!scores_initialized)
22                 ScoreRules_generic();
23 }
24
25 void ActivateTeamplay()
26 {
27         serverflags |= SERVERFLAG_TEAMPLAY;
28         teamplay = 1;
29 }
30
31 void InitGameplayMode()
32 {
33         float fraglimit_override, timelimit_override, leadlimit_override, qualifying_override;
34
35         qualifying_override = -1;
36
37         VoteReset();
38
39         // find out good world mins/maxs bounds, either the static bounds found by looking for solid, or the mapinfo specified bounds
40         get_mi_min_max(1);
41         world.mins = mi_min;
42         world.maxs = mi_max;
43
44         MapInfo_LoadMapSettings(mapname);
45         teamplay = 0;
46         serverflags &= ~SERVERFLAG_TEAMPLAY;
47
48         if (!cvar_value_issafe(world.fog))
49         {
50                 print("The current map contains a potentially harmful fog setting, ignored\n");
51                 world.fog = string_null;
52         }
53         if(MapInfo_Map_fog != "")
54                 if(MapInfo_Map_fog == "none")
55                         world.fog = string_null;
56                 else
57                         world.fog = strzone(MapInfo_Map_fog);
58         clientstuff = strzone(MapInfo_Map_clientstuff);
59
60         MapInfo_ClearTemps();
61
62         // set both here, gamemode can override it later
63         timelimit_override = autocvar_timelimit_override;
64         fraglimit_override = autocvar_fraglimit_override;
65         leadlimit_override = autocvar_leadlimit_override;
66         gamemode_name = MapInfo_Type_ToText(MapInfo_LoadedGametype);
67
68         if(g_dm)
69         {
70         }
71
72         if(g_tdm)
73         {
74                 ActivateTeamplay();
75                 fraglimit_override = autocvar_g_tdm_point_limit;
76                 leadlimit_override = autocvar_g_tdm_point_leadlimit;
77                 if(autocvar_g_tdm_team_spawns)
78                         have_team_spawns = -1; // request team spawns
79                 MUTATOR_ADD(gamemode_tdm);
80         }
81
82         if(g_domination)
83         {
84                 ActivateTeamplay();
85                 fraglimit_override = autocvar_g_domination_point_limit;
86                 leadlimit_override = autocvar_g_domination_point_leadlimit;
87                 if(autocvar_g_domination_roundbased && autocvar_g_domination_roundbased_point_limit)
88                         fraglimit_override = autocvar_g_domination_roundbased_point_limit;
89                 have_team_spawns = -1; // request team spawns
90                 MUTATOR_ADD(gamemode_domination);
91         }
92
93         if(g_ctf)
94         {
95                 ActivateTeamplay();
96                 fraglimit_override = autocvar_capturelimit_override;
97                 leadlimit_override = autocvar_captureleadlimit_override;
98                 have_team_spawns = -1; // request team spawns
99                 MUTATOR_ADD(gamemode_ctf);
100         }
101
102         if(g_lms)
103         {
104                 fraglimit_override = autocvar_g_lms_lives_override;
105                 leadlimit_override = 0; // not supported by LMS
106                 if(fraglimit_override == 0)
107                         fraglimit_override = -1;
108                 MUTATOR_ADD(gamemode_lms);
109         }
110
111         if(g_ca)
112         {
113                 ActivateTeamplay();
114                 fraglimit_override = autocvar_g_ca_point_limit;
115                 leadlimit_override = autocvar_g_ca_point_leadlimit;
116                 if(autocvar_g_ca_team_spawns)
117                         have_team_spawns = -1; // request team spawns
118                 MUTATOR_ADD(gamemode_ca);
119         }
120
121         if(g_keyhunt)
122         {
123                 ActivateTeamplay();
124                 fraglimit_override = autocvar_g_keyhunt_point_limit;
125                 leadlimit_override = autocvar_g_keyhunt_point_leadlimit;
126                 MUTATOR_ADD(gamemode_keyhunt);
127         }
128
129         if(g_freezetag)
130         {
131                 ActivateTeamplay();
132                 fraglimit_override = autocvar_g_freezetag_point_limit;
133                 leadlimit_override = autocvar_g_freezetag_point_leadlimit;
134                 if(autocvar_g_freezetag_team_spawns)
135                         have_team_spawns = -1; // request team spawns
136                 MUTATOR_ADD(gamemode_freezetag);
137         }
138
139         if(g_assault)
140         {
141                 ActivateTeamplay();
142                 have_team_spawns = -1; // request team spawns
143                 MUTATOR_ADD(gamemode_assault);
144         }
145
146         if(g_onslaught)
147         {
148                 ActivateTeamplay();
149                 fraglimit_override = autocvar_g_onslaught_point_limit;
150                 have_team_spawns = -1; // request team spawns
151                 MUTATOR_ADD(gamemode_onslaught);
152         }
153
154         if(g_race)
155         {
156                 if(autocvar_g_race_teams)
157                 {
158                         ActivateTeamplay();
159                         race_teams = bound(2, autocvar_g_race_teams, 4);
160                         have_team_spawns = -1; // request team spawns
161                 }
162                 else
163                         race_teams = 0;
164                 qualifying_override = autocvar_g_race_qualifying_timelimit_override;
165                 fraglimit_override = autocvar_g_race_laps_limit;
166                 leadlimit_override = 0; // currently not supported by race
167
168                 // we need to find out the correct value for g_race_qualifying
169                 float want_qualifying = ((qualifying_override >= 0) ? qualifying_override : autocvar_g_race_qualifying_timelimit) > 0;
170
171                 if(autocvar_g_campaign)
172                 {
173                         g_race_qualifying = 1;
174                         independent_players = 1;
175                 }
176                 else if(!autocvar_g_campaign && want_qualifying)
177                 {
178                         g_race_qualifying = 2;
179                         independent_players = 1;
180                         race_fraglimit = (race_fraglimit >= 0) ? fraglimit_override : autocvar_fraglimit;
181                         race_leadlimit = (race_leadlimit >= 0) ? leadlimit_override : autocvar_leadlimit;
182                         race_timelimit = (race_timelimit >= 0) ? timelimit_override : autocvar_timelimit;
183                         fraglimit_override = 0;
184                         leadlimit_override = 0;
185                         timelimit_override = autocvar_g_race_qualifying_timelimit;
186                 }
187                 else
188                 {
189                         g_race_qualifying = 0;
190                 }
191
192                 MUTATOR_ADD(gamemode_race);
193         }
194
195         if(g_cts)
196         {
197                 g_race_qualifying = 1;
198                 fraglimit_override = 0;
199                 leadlimit_override = 0;
200                 independent_players = 1;
201                 MUTATOR_ADD(gamemode_cts);
202         }
203
204         if(g_nexball)
205         {
206                 fraglimit_override = autocvar_g_nexball_goallimit;
207                 leadlimit_override = autocvar_g_nexball_goalleadlimit;
208                 ActivateTeamplay();
209                 have_team_spawns = -1; // request team spawns
210                 MUTATOR_ADD(gamemode_nexball);
211         }
212
213         if(g_keepaway)
214         {
215                 MUTATOR_ADD(gamemode_keepaway);
216         }
217
218         if(g_invasion)
219         {
220                 fraglimit_override = autocvar_g_invasion_point_limit;
221                 if(autocvar_g_invasion_teams >= 2)
222                 {
223                         ActivateTeamplay();
224                         if(autocvar_g_invasion_team_spawns)
225                                 have_team_spawns = -1; // request team spawns
226                 }
227                 MUTATOR_ADD(gamemode_invasion);
228         }
229
230         if(teamplay)
231                 entcs_init();
232
233         cache_mutatormsg = strzone("");
234         cache_lastmutatormsg = strzone("");
235
236         // enforce the server's universal frag/time limits
237         if(!autocvar_g_campaign)
238         {
239                 if(fraglimit_override >= 0)
240                         cvar_set("fraglimit", ftos(fraglimit_override));
241                 if(timelimit_override >= 0)
242                         cvar_set("timelimit", ftos(timelimit_override));
243                 if(leadlimit_override >= 0)
244                         cvar_set("leadlimit", ftos(leadlimit_override));
245                 if(qualifying_override >= 0)
246                         cvar_set("g_race_qualifying_timelimit", ftos(qualifying_override));
247         }
248
249         InitializeEntity(world, default_delayedinit, INITPRIO_GAMETYPE_FALLBACK);
250 }
251
252 string GetClientVersionMessage() {
253         string versionmsg;
254         if (self.version_mismatch) {
255                 if(self.version < autocvar_gameversion) {
256                         versionmsg = "^3Your client version is outdated.\n\n\n### YOU WON'T BE ABLE TO PLAY ON THIS SERVER ###\n\n\nPlease update!!!^8";
257                 } else {
258                         versionmsg = "^3This server is using an outdated Xonotic version.\n\n\n ### THIS SERVER IS INCOMPATIBLE AND THUS YOU CANNOT JOIN ###.^8";
259                 }
260         } else {
261                 versionmsg = "^2client version and server version are compatible.^8";
262         }
263         return versionmsg;
264 }
265
266 string getwelcomemessage(void)
267 {
268         string s, modifications, motd;
269
270         ret_string = "";
271         MUTATOR_CALLHOOK(BuildMutatorsPrettyString);
272         modifications = ret_string;
273
274         if(g_weaponarena)
275         {
276                 if(g_weaponarena_random)
277                         modifications = strcat(modifications, ", ", ftos(g_weaponarena_random), " of ", g_weaponarena_list, " Arena");
278                 else
279                         modifications = strcat(modifications, ", ", g_weaponarena_list, " Arena");
280         }
281         if(cvar("g_balance_blaster_weaponstart") == 0)
282                 modifications = strcat(modifications, ", No start weapons");
283         if(cvar("sv_gravity") < stof(cvar_defstring("sv_gravity")))
284                 modifications = strcat(modifications, ", Low gravity");
285         if(g_cloaked && !g_cts)
286                 modifications = strcat(modifications, ", Cloaked");
287         if(g_grappling_hook)
288                 modifications = strcat(modifications, ", Hook");
289         if(g_weapon_stay && !g_cts)
290                 modifications = strcat(modifications, ", Weapons stay");
291         if(g_jetpack)
292                 modifications = strcat(modifications, ", Jet pack");
293         if(autocvar_g_powerups == 0)
294                 modifications = strcat(modifications, ", No powerups");
295         if(autocvar_g_powerups > 0)
296                 modifications = strcat(modifications, ", Powerups");
297         modifications = substring(modifications, 2, strlen(modifications) - 2);
298
299         string versionmessage;
300         versionmessage = GetClientVersionMessage();
301
302         s = strcat("This is Xonotic ", autocvar_g_xonoticversion, "\n", versionmessage);
303         s = strcat(s, "^8\n\nmatch type is ^1", gamemode_name, "^8\n");
304
305         if(modifications != "")
306                 s = strcat(s, "^8\nactive modifications: ^3", modifications, "^8\n");
307
308         if (g_grappling_hook)
309                 s = strcat(s, "\n\n^3grappling hook^8 is enabled, press 'e' to use it\n");
310
311         if (cvar("g_nades"))
312                 s = strcat(s, "\n\n^3nades^8 are enabled, press 'g' to use them\n");
313
314         if(cache_lastmutatormsg != autocvar_g_mutatormsg)
315         {
316                 if(cache_lastmutatormsg)
317                         strunzone(cache_lastmutatormsg);
318                 if(cache_mutatormsg)
319                         strunzone(cache_mutatormsg);
320                 cache_lastmutatormsg = strzone(autocvar_g_mutatormsg);
321                 cache_mutatormsg = strzone(cache_lastmutatormsg);
322         }
323
324         if (cache_mutatormsg != "") {
325                 s = strcat(s, "\n\n^8special gameplay tips: ^7", cache_mutatormsg);
326         }
327
328         motd = autocvar_sv_motd;
329         if (motd != "") {
330                 s = strcat(s, "\n\n^8MOTD: ^7", strreplace("\\n", "\n", motd));
331         }
332         return s;
333 }
334
335 void SetPlayerColors(entity pl, float _color)
336 {
337         /*string s;
338         s = ftos(cl);
339         stuffcmd(pl, strcat("color ", s, " ", s, "\n")  );
340         pl.team = cl + 1;
341         //pl.clientcolors = pl.clientcolors - (pl.clientcolors & 15) + cl;
342         pl.clientcolors = 16*cl + cl;*/
343
344         float pants, shirt;
345         pants = _color & 0x0F;
346         shirt = _color & 0xF0;
347
348
349         if(teamplay) {
350                 setcolor(pl, 16*pants + pants);
351         } else {
352                 setcolor(pl, shirt + pants);
353         }
354 }
355
356 void SetPlayerTeam(entity pl, float t, float s, float noprint)
357 {
358         float _color;
359
360         if(t == 4)
361                 _color = NUM_TEAM_4 - 1;
362         else if(t == 3)
363                 _color = NUM_TEAM_3 - 1;
364         else if(t == 2)
365                 _color = NUM_TEAM_2 - 1;
366         else
367                 _color = NUM_TEAM_1 - 1;
368
369         SetPlayerColors(pl,_color);
370
371         if(t != s) {
372                 LogTeamchange(pl.playerid, pl.team, 3);  // log manual team join
373
374                 if(!noprint)
375                 bprint(pl.netname, "^7 has changed from ", Team_NumberToColoredFullName(s), "^7 to ", Team_NumberToColoredFullName(t), "\n");
376         }
377
378 }
379
380 // set c1...c4 to show what teams are allowed
381 void CheckAllowedTeams (entity for_whom)
382 {
383         float dm;
384         entity head;
385         string teament_name;
386
387         c1 = c2 = c3 = c4 = -1;
388         cb1 = cb2 = cb3 = cb4 = 0;
389
390         teament_name = string_null;
391         if(g_onslaught)
392         {
393                 // onslaught is special
394                 head = findchain(classname, "onslaught_generator");
395                 while (head)
396                 {
397                         if (head.team == NUM_TEAM_1) c1 = 0;
398                         if (head.team == NUM_TEAM_2) c2 = 0;
399                         if (head.team == NUM_TEAM_3) c3 = 0;
400                         if (head.team == NUM_TEAM_4) c4 = 0;
401                         head = head.chain;
402                 }
403         }
404         else if(g_domination)
405                 teament_name = "dom_team";
406         else if(g_ctf)
407                 teament_name = "ctf_team";
408         else if(g_tdm)
409                 teament_name = "tdm_team";
410         else if(g_nexball)
411                 teament_name = "nexball_team";
412         else if(g_assault)
413                 c1 = c2 = 0; // Assault always has 2 teams
414         else
415         {
416                 // cover anything else by treating it like tdm with no teams spawned
417                 dm = 2;
418
419                 ret_float = dm;
420                 MUTATOR_CALLHOOK(GetTeamCount);
421                 dm = ret_float;
422
423                 if(dm >= 4)
424                         c1 = c2 = c3 = c4 = 0;
425                 else if(dm >= 3)
426                         c1 = c2 = c3 = 0;
427                 else
428                         c1 = c2 = 0;
429         }
430
431         // find out what teams are allowed if necessary
432         if(teament_name)
433         {
434                 head = find(world, classname, teament_name);
435                 while(head)
436                 {
437                         if(!(g_domination && head.netname == ""))
438                         {
439                                 if(head.team == NUM_TEAM_1)
440                                         c1 = 0;
441                                 else if(head.team == NUM_TEAM_2)
442                                         c2 = 0;
443                                 else if(head.team == NUM_TEAM_3)
444                                         c3 = 0;
445                                 else if(head.team == NUM_TEAM_4)
446                                         c4 = 0;
447                         }
448                         head = find(head, classname, teament_name);
449                 }
450         }
451
452         // TODO: Balance quantity of bots across > 2 teams when bot_vs_human is set (and remove next line)
453         if(c3==-1 && c4==-1)
454         if(autocvar_bot_vs_human && for_whom)
455         {
456                 if(autocvar_bot_vs_human > 0)
457                 {
458                         // bots are all blue
459                         if(IS_BOT_CLIENT(for_whom))
460                                 c1 = c3 = c4 = -1;
461                         else
462                                 c2 = -1;
463                 }
464                 else
465                 {
466                         // bots are all red
467                         if(IS_BOT_CLIENT(for_whom))
468                                 c2 = c3 = c4 = -1;
469                         else
470                                 c1 = -1;
471                 }
472         }
473
474         // if player has a forced team, ONLY allow that one
475         if(self.team_forced == NUM_TEAM_1 && c1 >= 0)
476                 c2 = c3 = c4 = -1;
477         else if(self.team_forced == NUM_TEAM_2 && c2 >= 0)
478                 c1 = c3 = c4 = -1;
479         else if(self.team_forced == NUM_TEAM_3 && c3 >= 0)
480                 c1 = c2 = c4 = -1;
481         else if(self.team_forced == NUM_TEAM_4 && c4 >= 0)
482                 c1 = c2 = c3 = -1;
483 }
484
485 float PlayerValue(entity p)
486 {
487         return 1;
488         // FIXME: it always returns 1...
489 }
490
491 // c1...c4 should be set to -1 (not allowed) or 0 (allowed).
492 // teams that are allowed will now have their player counts stored in c1...c4
493 void GetTeamCounts(entity ignore)
494 {
495         entity head;
496         float value, bvalue;
497         // now count how many players are on each team already
498
499         // FIXME: also find and memorize the lowest-scoring bot on each team (in case players must be shuffled around)
500         // also remember the lowest-scoring player
501
502         FOR_EACH_CLIENT(head)
503         {
504                 float t;
505                 if(IS_PLAYER(head) || head.caplayer)
506                         t = head.team;
507                 else if(head.team_forced > 0)
508                         t = head.team_forced; // reserve the spot
509                 else
510                         continue;
511                 if(head != ignore)// && head.netname != "")
512                 {
513                         value = PlayerValue(head);
514                         if(IS_BOT_CLIENT(head))
515                                 bvalue = value;
516                         else
517                                 bvalue = 0;
518                         if(t == NUM_TEAM_1)
519                         {
520                                 if(c1 >= 0)
521                                 {
522                                         c1 = c1 + value;
523                                         cb1 = cb1 + bvalue;
524                                 }
525                         }
526                         if(t == NUM_TEAM_2)
527                         {
528                                 if(c2 >= 0)
529                                 {
530                                         c2 = c2 + value;
531                                         cb2 = cb2 + bvalue;
532                                 }
533                         }
534                         if(t == NUM_TEAM_3)
535                         {
536                                 if(c3 >= 0)
537                                 {
538                                         c3 = c3 + value;
539                                         cb3 = cb3 + bvalue;
540                                 }
541                         }
542                         if(t == NUM_TEAM_4)
543                         {
544                                 if(c4 >= 0)
545                                 {
546                                         c4 = c4 + value;
547                                         cb4 = cb4 + bvalue;
548                                 }
549                         }
550                 }
551         }
552
553         // if the player who has a forced team has not joined yet, reserve the spot
554         if(autocvar_g_campaign)
555         {
556                 switch(autocvar_g_campaign_forceteam)
557                 {
558                         case 1: if(c1 == cb1) ++c1; break;
559                         case 2: if(c2 == cb2) ++c2; break;
560                         case 3: if(c3 == cb3) ++c3; break;
561                         case 4: if(c4 == cb4) ++c4; break;
562                 }
563         }
564 }
565
566 float TeamSmallerEqThanTeam(float ta, float tb, entity e)
567 {
568         // we assume that CheckAllowedTeams and GetTeamCounts have already been called
569         float f;
570         float ca = -1, cb = -1, cba = 0, cbb = 0, sa = 0, sb = 0;
571
572         switch(ta)
573         {
574                 case 1: ca = c1; cba = cb1; sa = team1_score; break;
575                 case 2: ca = c2; cba = cb2; sa = team2_score; break;
576                 case 3: ca = c3; cba = cb3; sa = team3_score; break;
577                 case 4: ca = c4; cba = cb4; sa = team4_score; break;
578         }
579         switch(tb)
580         {
581                 case 1: cb = c1; cbb = cb1; sb = team1_score; break;
582                 case 2: cb = c2; cbb = cb2; sb = team2_score; break;
583                 case 3: cb = c3; cbb = cb3; sb = team3_score; break;
584                 case 4: cb = c4; cbb = cb4; sb = team4_score; break;
585         }
586
587         // invalid
588         if(ca < 0 || cb < 0)
589                 return false;
590
591         // equal
592         if(ta == tb)
593                 return true;
594
595         if(IS_REAL_CLIENT(e))
596         {
597                 if(bots_would_leave)
598                 {
599                         ca -= cba * 0.999;
600                         cb -= cbb * 0.999;
601                 }
602         }
603
604         // keep teams alive (teams of size 0 always count as smaller, ignoring score)
605         if(ca < 1)
606                 if(cb >= 1)
607                         return true;
608         if(ca >= 1)
609                 if(cb < 1)
610                         return false;
611
612         // first, normalize
613         f = max(ca, cb, 1);
614         ca /= f;
615         cb /= f;
616         f = max(sa, sb, 1);
617         sa /= f;
618         sb /= f;
619
620         // the more we're at the end of the match, the more take scores into account
621         f = bound(0, game_completion_ratio * autocvar_g_balance_teams_scorefactor, 1);
622         ca += (sa - ca) * f;
623         cb += (sb - cb) * f;
624
625         return ca <= cb;
626 }
627
628 // returns # of smallest team (1, 2, 3, 4)
629 // NOTE: Assumes CheckAllowedTeams has already been called!
630 float FindSmallestTeam(entity pl, float ignore_pl)
631 {
632         float totalteams, t;
633         totalteams = 0;
634
635         // find out what teams are available
636         //CheckAllowedTeams();
637
638         // make sure there are at least 2 teams to join
639         if(c1 >= 0)
640                 totalteams = totalteams + 1;
641         if(c2 >= 0)
642                 totalteams = totalteams + 1;
643         if(c3 >= 0)
644                 totalteams = totalteams + 1;
645         if(c4 >= 0)
646                 totalteams = totalteams + 1;
647
648         if((autocvar_bot_vs_human || pl.team_forced > 0) && totalteams == 1)
649                 totalteams += 1;
650
651         if(totalteams <= 1)
652         {
653                 if(autocvar_g_campaign && pl && IS_REAL_CLIENT(pl))
654                         return 1; // special case for campaign and player joining
655                 else
656                         error(sprintf("Too few teams available for %s\n", MapInfo_Type_ToString(MapInfo_CurrentGametype())));
657         }
658
659         // count how many players are in each team
660         if(ignore_pl)
661                 GetTeamCounts(pl);
662         else
663                 GetTeamCounts(world);
664
665         RandomSelection_Init();
666
667         t = 1;
668         if(TeamSmallerEqThanTeam(2, t, pl))
669                 t = 2;
670         if(TeamSmallerEqThanTeam(3, t, pl))
671                 t = 3;
672         if(TeamSmallerEqThanTeam(4, t, pl))
673                 t = 4;
674
675         // now t is the minimum, or A minimum!
676         if(t == 1 || TeamSmallerEqThanTeam(1, t, pl))
677                 RandomSelection_Add(world, 1, string_null, 1, 1);
678         if(t == 2 || TeamSmallerEqThanTeam(2, t, pl))
679                 RandomSelection_Add(world, 2, string_null, 1, 1);
680         if(t == 3 || TeamSmallerEqThanTeam(3, t, pl))
681                 RandomSelection_Add(world, 3, string_null, 1, 1);
682         if(t == 4 || TeamSmallerEqThanTeam(4, t, pl))
683                 RandomSelection_Add(world, 4, string_null, 1, 1);
684
685         return RandomSelection_chosen_float;
686 }
687
688 float JoinBestTeam(entity pl, float only_return_best, float forcebestteam)
689 {
690         float smallest, selectedteam;
691
692         // don't join a team if we're not playing a team game
693         if(!teamplay)
694                 return 0;
695
696         // find out what teams are available
697         CheckAllowedTeams(pl);
698
699         // if we don't care what team he ends up on, put him on whatever team he entered as.
700         // if he's not on a valid team, then let other code put him on the smallest team
701         if(!forcebestteam)
702         {
703                 if(     c1 >= 0 && pl.team == NUM_TEAM_1)
704                         selectedteam = pl.team;
705                 else if(c2 >= 0 && pl.team == NUM_TEAM_2)
706                         selectedteam = pl.team;
707                 else if(c3 >= 0 && pl.team == NUM_TEAM_3)
708                         selectedteam = pl.team;
709                 else if(c4 >= 0 && pl.team == NUM_TEAM_4)
710                         selectedteam = pl.team;
711                 else
712                         selectedteam = -1;
713
714                 if(selectedteam > 0)
715                 {
716                         if(!only_return_best)
717                         {
718                                 SetPlayerColors(pl, selectedteam - 1);
719
720                                 // when JoinBestTeam is called by client.qc/ClientKill_Now_TeamChange the players team is -1 and thus skipped
721                                 // when JoinBestTeam is called by cl_client.qc/ClientConnect the player_id is 0 the log attempt is rejected
722                                 LogTeamchange(pl.playerid, pl.team, 99);
723                         }
724                         return selectedteam;
725                 }
726                 // otherwise end up on the smallest team (handled below)
727         }
728
729         smallest = FindSmallestTeam(pl, true);
730
731         if(!only_return_best && !pl.bot_forced_team)
732         {
733                 TeamchangeFrags(self);
734                 if(smallest == 1)
735                 {
736                         SetPlayerColors(pl, NUM_TEAM_1 - 1);
737                 }
738                 else if(smallest == 2)
739                 {
740                         SetPlayerColors(pl, NUM_TEAM_2 - 1);
741                 }
742                 else if(smallest == 3)
743                 {
744                         SetPlayerColors(pl, NUM_TEAM_3 - 1);
745                 }
746                 else if(smallest == 4)
747                 {
748                         SetPlayerColors(pl, NUM_TEAM_4 - 1);
749                 }
750                 else
751                 {
752                         error("smallest team: invalid team\n");
753                 }
754
755                 LogTeamchange(pl.playerid, pl.team, 2); // log auto join
756
757                 if(pl.deadflag == DEAD_NO)
758                         Damage(pl, pl, pl, 100000, DEATH_TEAMCHANGE, pl.origin, '0 0 0');
759         }
760
761         return smallest;
762 }
763
764 //void() ctf_playerchanged;
765 void SV_ChangeTeam(float _color)
766 {
767         float scolor, dcolor, steam, dteam; //, dbotcount, scount, dcount;
768
769         // in normal deathmatch we can just apply the color and we're done
770         if(!teamplay) {
771                 SetPlayerColors(self, _color);
772                 return;
773         }
774
775         scolor = self.clientcolors & 0x0F;
776         dcolor = _color & 0x0F;
777
778         if(scolor == NUM_TEAM_1 - 1)
779                 steam = 1;
780         else if(scolor == NUM_TEAM_2 - 1)
781                 steam = 2;
782         else if(scolor == NUM_TEAM_3 - 1)
783                 steam = 3;
784         else // if(scolor == NUM_TEAM_4 - 1)
785                 steam = 4;
786         if(dcolor == NUM_TEAM_1 - 1)
787                 dteam = 1;
788         else if(dcolor == NUM_TEAM_2 - 1)
789                 dteam = 2;
790         else if(dcolor == NUM_TEAM_3 - 1)
791                 dteam = 3;
792         else // if(dcolor == NUM_TEAM_4 - 1)
793                 dteam = 4;
794
795         CheckAllowedTeams(self);
796
797         if(dteam == 1 && c1 < 0) dteam = 4;
798         if(dteam == 4 && c4 < 0) dteam = 3;
799         if(dteam == 3 && c3 < 0) dteam = 2;
800         if(dteam == 2 && c2 < 0) dteam = 1;
801
802         // not changing teams
803         if(scolor == dcolor)
804         {
805                 //bprint("same team change\n");
806                 SetPlayerTeam(self, dteam, steam, true);
807                 return;
808         }
809
810         if((autocvar_g_campaign) || (autocvar_g_changeteam_banned && self.wasplayer)) {
811                 Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TEAMCHANGE_NOTALLOWED);
812                 return; // changing teams is not allowed
813         }
814
815         // autocvar_g_balance_teams_prevent_imbalance only makes sense if autocvar_g_balance_teams is on, as it makes the team selection dialog pointless
816         if(autocvar_g_balance_teams && autocvar_g_balance_teams_prevent_imbalance)
817         {
818                 GetTeamCounts(self);
819                 if(!TeamSmallerEqThanTeam(dteam, steam, self))
820                 {
821                         Send_Notification(NOTIF_ONE, self, MSG_INFO, INFO_TEAMCHANGE_LARGERTEAM);
822                         return;
823                 }
824         }
825
826 //      bprint("allow change teams from ", ftos(steam), " to ", ftos(dteam), "\n");
827
828         if(IS_PLAYER(self) && steam != dteam)
829         {
830                 // reduce frags during a team change
831                 TeamchangeFrags(self);
832         }
833
834         SetPlayerTeam(self, dteam, steam, false);
835
836         if(IS_PLAYER(self) && steam != dteam)
837         {
838                 // kill player when changing teams
839                 if(self.deadflag == DEAD_NO)
840                         Damage(self, self, self, 100000, DEATH_TEAMCHANGE, self.origin, '0 0 0');
841         }
842 }
843
844 void ShufflePlayerOutOfTeam (float source_team)
845 {
846         float smallestteam, smallestteam_count, steam;
847         float lowest_bot_score, lowest_player_score;
848         entity head, lowest_bot, lowest_player, selected;
849
850         smallestteam = 0;
851         smallestteam_count = 999999999;
852
853         if(c1 >= 0 && c1 < smallestteam_count)
854         {
855                 smallestteam = 1;
856                 smallestteam_count = c1;
857         }
858         if(c2 >= 0 && c2 < smallestteam_count)
859         {
860                 smallestteam = 2;
861                 smallestteam_count = c2;
862         }
863         if(c3 >= 0 && c3 < smallestteam_count)
864         {
865                 smallestteam = 3;
866                 smallestteam_count = c3;
867         }
868         if(c4 >= 0 && c4 < smallestteam_count)
869         {
870                 smallestteam = 4;
871                 smallestteam_count = c4;
872         }
873
874         if(!smallestteam)
875         {
876                 bprint("warning: no smallest team\n");
877                 return;
878         }
879
880         if(source_team == 1)
881                 steam = NUM_TEAM_1;
882         else if(source_team == 2)
883                 steam = NUM_TEAM_2;
884         else if(source_team == 3)
885                 steam = NUM_TEAM_3;
886         else // if(source_team == 4)
887                 steam = NUM_TEAM_4;
888
889         lowest_bot = world;
890         lowest_bot_score = 999999999;
891         lowest_player = world;
892         lowest_player_score = 999999999;
893
894         // find the lowest-scoring player & bot of that team
895         FOR_EACH_PLAYER(head)
896         {
897                 if(head.team == steam)
898                 {
899                         if(head.isbot)
900                         {
901                                 if(head.totalfrags < lowest_bot_score)
902                                 {
903                                         lowest_bot = head;
904                                         lowest_bot_score = head.totalfrags;
905                                 }
906                         }
907                         else
908                         {
909                                 if(head.totalfrags < lowest_player_score)
910                                 {
911                                         lowest_player = head;
912                                         lowest_player_score = head.totalfrags;
913                                 }
914                         }
915                 }
916         }
917
918         // prefers to move a bot...
919         if(lowest_bot != world)
920                 selected = lowest_bot;
921         // but it will move a player if it has to
922         else
923                 selected = lowest_player;
924         // don't do anything if it couldn't find anyone
925         if(!selected)
926         {
927                 bprint("warning: couldn't find a player to move from team\n");
928                 return;
929         }
930
931         // smallest team gains a member
932         if(smallestteam == 1)
933         {
934                 c1 = c1 + 1;
935         }
936         else if(smallestteam == 2)
937         {
938                 c2 = c2 + 1;
939         }
940         else if(smallestteam == 3)
941         {
942                 c3 = c3 + 1;
943         }
944         else if(smallestteam == 4)
945         {
946                 c4 = c4 + 1;
947         }
948         else
949         {
950                 bprint("warning: destination team invalid\n");
951                 return;
952         }
953         // source team loses a member
954         if(source_team == 1)
955         {
956                 c1 = c1 + 1;
957         }
958         else if(source_team == 2)
959         {
960                 c2 = c2 + 2;
961         }
962         else if(source_team == 3)
963         {
964                 c3 = c3 + 3;
965         }
966         else if(source_team == 4)
967         {
968                 c4 = c4 + 4;
969         }
970         else
971         {
972                 bprint("warning: source team invalid\n");
973                 return;
974         }
975
976         // move the player to the new team
977         TeamchangeFrags(selected);
978         SetPlayerTeam(selected, smallestteam, source_team, false);
979
980         if(selected.deadflag == DEAD_NO)
981                 Damage(selected, selected, selected, 100000, DEATH_AUTOTEAMCHANGE, selected.origin, '0 0 0');
982         Send_Notification(NOTIF_ONE, selected, MSG_CENTER, CENTER_DEATH_SELF_AUTOTEAMCHANGE, selected.team);
983 }