Merge remote-tracking branch 'origin/master' into Mario/lms_updates
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / scores.qc
1 .entity scorekeeper;
2 entity teamscorekeepers[16];
3 string scores_label[MAX_SCORE];
4 float scores_flags[MAX_SCORE];
5 string teamscores_label[MAX_TEAMSCORE];
6 float teamscores_flags[MAX_TEAMSCORE];
7 float teamscores_entities_count;
8 var .float scores_primary;
9 var .float teamscores_primary;
10 float scores_flags_primary;
11 float teamscores_flags_primary;
12
13 vector ScoreField_Compare(entity t1, entity t2, .float field, float fieldflags, vector previous, float strict) // returns: cmp value, best prio
14 {
15         if(!strict && !(fieldflags & SFL_SORT_PRIO_MASK)) // column does not sort
16                 return previous;
17         if(fieldflags & SFL_SORT_PRIO_MASK < previous_y)
18                 return previous;
19         if(t1.field == t2.field)
20                 return previous;
21
22         previous_y = fieldflags & SFL_SORT_PRIO_MASK;
23
24         if(fieldflags & SFL_ZERO_IS_WORST)
25         {
26                 if(t1.field == 0)
27                 {
28                         previous_x = -1;
29                         return previous;
30                 }
31                 else if(t2.field == 0)
32                 {
33                         previous_x = +1;
34                         return previous;
35                 }
36         }
37
38         if(fieldflags & SFL_LOWER_IS_BETTER)
39                 previous_x = (t2.field - t1.field);
40         else
41                 previous_x = (t1.field - t2.field);
42
43         return previous;
44 }
45
46 /*
47  * teamscore entities
48  */
49
50 float TeamScore_SendEntity(entity to, float sendflags)
51 {
52         float i, p, longflags;
53
54         WriteByte(MSG_ENTITY, ENT_CLIENT_TEAMSCORES);
55         WriteByte(MSG_ENTITY, self.team - 1);
56
57         longflags = 0;
58         for(i = 0, p = 1; i < MAX_TEAMSCORE; ++i, p *= 2)
59                 if(self.(teamscores[i]) > 127 || self.(teamscores[i]) <= -128)
60                         longflags |= p;
61
62 #if MAX_TEAMSCORE <= 8
63         WriteByte(MSG_ENTITY, sendflags);
64         WriteByte(MSG_ENTITY, longflags);
65 #else
66         WriteShort(MSG_ENTITY, sendflags);
67         WriteShort(MSG_ENTITY, longflags);
68 #endif
69         for(i = 0, p = 1; i < MAX_TEAMSCORE; ++i, p *= 2)
70                 if(sendflags & p)
71                 {
72                         if(longflags & p)
73                                 WriteInt24_t(MSG_ENTITY, self.(teamscores[i]));
74                         else
75                                 WriteChar(MSG_ENTITY, self.(teamscores[i]));
76                 }
77
78         return TRUE;
79 }
80
81 void TeamScore_Spawn(float t, string name)
82 {
83         entity ts;
84         ts = spawn();
85         ts.classname = "csqc_score_team";
86         ts.netname = name; // not used yet, FIXME
87         ts.team = t;
88         Net_LinkEntity(ts, FALSE, 0, TeamScore_SendEntity);
89         teamscorekeepers[t - 1] = ts;
90         ++teamscores_entities_count;
91         PlayerStats_AddTeam(t);
92 }
93
94 float TeamScore_AddToTeam(float t, float scorefield, float score)
95 {
96         entity s;
97
98         if(gameover)
99                 score = 0;
100
101         if(!scores_initialized) return 0; // FIXME remove this when everything uses this system
102         if(t <= 0 || t >= 16)
103         {
104                 if(gameover)
105                         return 0;
106                 error("Adding score to invalid team!");
107         }
108         s = teamscorekeepers[t - 1];
109         if(!s)
110         {
111                 if(gameover)
112                         return 0;
113                 error("Adding score to unknown team!");
114         }
115         if(score)
116                 if(teamscores_label[scorefield] != "")
117                         s.SendFlags |= pow(2, scorefield);
118         return (s.(teamscores[scorefield]) += score);
119 }
120
121 float TeamScore_Add(entity player, float scorefield, float score)
122 {
123         return TeamScore_AddToTeam(player.team, scorefield, score);
124 }
125
126 float TeamScore_Compare(entity t1, entity t2, float strict)
127 {
128         if(!t1 || !t2) return (!t2) - !t1;
129
130         vector result = '0 0 0';
131         float i;
132         for(i = 0; i < MAX_TEAMSCORE; ++i)
133         {
134                 var .float f;
135                 f = teamscores[i];
136                 result = ScoreField_Compare(t1, t2, f, teamscores_flags[i], result, strict);
137         }
138
139         if (result_x == 0 && strict)
140                 result_x = t1.team - t2.team;
141
142         return result_x;
143 }
144
145 /*
146  * the scoreinfo entity
147  */
148
149 void ScoreInfo_SetLabel_PlayerScore(float i, string label, float scoreflags)
150 {
151         scores_label[i] = label;
152         scores_flags[i] = scoreflags;
153         if(scoreflags & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_PRIMARY)
154         {
155                 scores_primary = scores[i];
156                 scores_flags_primary = scoreflags;
157         }
158         if(label != "")
159         {
160                 PlayerStats_AddEvent(strcat(PLAYERSTATS_TOTAL, label));
161                 PlayerStats_AddEvent(strcat(PLAYERSTATS_SCOREBOARD, label));
162         }
163 }
164
165 void ScoreInfo_SetLabel_TeamScore(float i, string label, float scoreflags)
166 {
167         teamscores_label[i] = label;
168         teamscores_flags[i] = scoreflags;
169         if(scoreflags & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_PRIMARY)
170         {
171                 teamscores_primary = teamscores[i];
172                 teamscores_flags_primary = scoreflags;
173         }
174 }
175
176 float ScoreInfo_SendEntity(entity to, float sf)
177 {
178         float i;
179         WriteByte(MSG_ENTITY, ENT_CLIENT_SCORES_INFO);
180         WriteInt24_t(MSG_ENTITY, MapInfo_LoadedGametype);
181         for(i = 0; i < MAX_SCORE; ++i)
182         {
183                 WriteString(MSG_ENTITY, scores_label[i]);
184                 WriteByte(MSG_ENTITY, scores_flags[i]);
185         }
186         for(i = 0; i < MAX_TEAMSCORE; ++i)
187         {
188                 WriteString(MSG_ENTITY, teamscores_label[i]);
189                 WriteByte(MSG_ENTITY, teamscores_flags[i]);
190         }
191         return TRUE;
192 }
193
194 void ScoreInfo_Init(float teams)
195 {
196         if(scores_initialized)
197         {
198                 scores_initialized.SendFlags |= 1; // force a resend
199         }
200         else
201         {
202                 scores_initialized = spawn();
203                 scores_initialized.classname = "ent_client_scoreinfo";
204                 Net_LinkEntity(scores_initialized, FALSE, 0, ScoreInfo_SendEntity);
205         }
206         if(teams >= 1)
207                 TeamScore_Spawn(NUM_TEAM_1, "Red");
208         if(teams >= 2)
209                 TeamScore_Spawn(NUM_TEAM_2, "Blue");
210         if(teams >= 3)
211                 TeamScore_Spawn(NUM_TEAM_3, "Yellow");
212         if(teams >= 4)
213                 TeamScore_Spawn(NUM_TEAM_4, "Pink");
214 }
215
216 /*
217  * per-player score entities
218  */
219
220 float PlayerScore_SendEntity(entity to, float sendflags)
221 {
222         float i, p, longflags;
223
224         WriteByte(MSG_ENTITY, ENT_CLIENT_SCORES);
225         WriteByte(MSG_ENTITY, num_for_edict(self.owner));
226
227         longflags = 0;
228         for(i = 0, p = 1; i < MAX_SCORE; ++i, p *= 2)
229                 if(self.(scores[i]) > 127 || self.(scores[i]) <= -128)
230                         longflags |= p;
231
232 #if MAX_SCORE <= 8
233         WriteByte(MSG_ENTITY, sendflags);
234         WriteByte(MSG_ENTITY, longflags);
235 #else
236         WriteShort(MSG_ENTITY, sendflags);
237         WriteShort(MSG_ENTITY, longflags);
238 #endif
239         for(i = 0, p = 1; i < MAX_SCORE; ++i, p *= 2)
240                 if(sendflags & p)
241                 {
242                         if(longflags & p)
243                                 WriteInt24_t(MSG_ENTITY, self.(scores[i]));
244                         else
245                                 WriteChar(MSG_ENTITY, self.(scores[i]));
246                 }
247
248         return TRUE;
249 }
250
251 float PlayerScore_Clear(entity player)
252 {
253         entity sk;
254         float i;
255
256         if(teamscores_entities_count)
257                 return 0;
258
259         if(MUTATOR_CALLHOOK(ForbidPlayerScore_Clear)) return 0;
260
261         if(g_cts) return 0; // in CTS, you don't lose score by observing
262         if(g_race && g_race_qualifying) return 0; // in qualifying, you don't lose score by observing
263
264         sk = player.scorekeeper;
265         for(i = 0; i < MAX_SCORE; ++i)
266         {
267                 if(sk.(scores[i]) != 0)
268                         if(scores_label[i] != "")
269                                 sk.SendFlags |= pow(2, i);
270                 sk.(scores[i]) = 0;
271         }
272
273         return 1;
274 }
275
276 void Score_ClearAll()
277 {
278         entity p, sk;
279         float i, t;
280         FOR_EACH_CLIENTSLOT(p)
281         {
282                 sk = p.scorekeeper;
283                 if(!sk)
284                         continue;
285                 for(i = 0; i < MAX_SCORE; ++i)
286                 {
287                         if(sk.(scores[i]) != 0)
288                                 if(scores_label[i] != "")
289                                         sk.SendFlags |= pow(2, i);
290                         sk.(scores[i]) = 0;
291                 }
292         }
293         for(t = 0; t < 16; ++t)
294         {
295                 sk = teamscorekeepers[t];
296                 if(!sk)
297                         continue;
298                 for(i = 0; i < MAX_TEAMSCORE; ++i)
299                 {
300                         if(sk.(teamscores[i]) != 0)
301                                 if(teamscores_label[i] != "")
302                                         sk.SendFlags |= pow(2, i);
303                         sk.(teamscores[i]) = 0;
304                 }
305         }
306 }
307
308 void PlayerScore_Attach(entity player)
309 {
310         entity sk;
311         if(player.scorekeeper)
312                 error("player already has a scorekeeper");
313         sk = spawn();
314         sk.owner = player;
315         Net_LinkEntity(sk, FALSE, 0, PlayerScore_SendEntity);
316         player.scorekeeper = sk;
317 }
318
319 void PlayerScore_Detach(entity player)
320 {
321         if(!player.scorekeeper)
322                 error("player has no scorekeeper");
323         remove(player.scorekeeper);
324         player.scorekeeper = world;
325 }
326
327 float PlayerScore_Add(entity player, float scorefield, float score)
328 {
329         entity s;
330
331         if(gameover)
332         if not(g_lms && scorefield == SP_LMS_RANK) // allow writing to this field in intermission as it is needed for newly joining players
333                 score = 0;
334
335         if(!scores_initialized) return 0; // FIXME remove this when everything uses this system
336         s = player.scorekeeper;
337         if(!s)
338         {
339                 if(gameover)
340                         return 0;
341                 backtrace("Adding score to unknown player!");
342                 return 0;
343         }
344         if(score)
345                 if(scores_label[scorefield] != "")
346                         s.SendFlags |= pow(2, scorefield);
347         if(!inWarmupStage)
348                 PlayerStats_Event(s.owner, strcat(PLAYERSTATS_TOTAL, scores_label[scorefield]), score);
349         return (s.(scores[scorefield]) += score);
350 }
351
352 float PlayerTeamScore_Add(entity player, float pscorefield, float tscorefield, float score)
353 {
354         float r;
355         r = PlayerScore_Add(player, pscorefield, score);
356         if(teamscores_entities_count) // only for teamplay
357                 r = TeamScore_Add(player, tscorefield, score);
358         return r;
359 }
360
361 float PlayerScore_Compare(entity t1, entity t2, float strict)
362 {
363         if(!t1 || !t2) return (!t2) - !t1;
364
365         vector result = '0 0 0';
366         float i;
367         for(i = 0; i < MAX_SCORE; ++i)
368         {
369                 var .float f;
370                 f = scores[i];
371                 result = ScoreField_Compare(t1, t2, f, scores_flags[i], result, strict);
372         }
373
374         if (result_x == 0 && strict)
375                 result_x = num_for_edict(t1.owner) - num_for_edict(t2.owner);
376
377         return result_x;
378 }
379
380 void WinningConditionHelper()
381 {
382         float c;
383         string s;
384         entity p;
385         float fullstatus;
386         entity winnerscorekeeper;
387         entity secondscorekeeper;
388         entity sk;
389
390         // format:
391         // gametype:P<pure>:S<slots>::plabel,plabel:tlabel,tlabel:teamid:tscore,tscore:teamid:tscore,tscore
392         // score labels always start with a symbol or with lower case
393         // so to match pure, match for :P0:
394         // to match full, match for :S0:
395
396         fullstatus = autocvar_g_full_getstatus_responses;
397
398         s = GetGametype();
399         s = strcat(s, ":", autocvar_g_xonoticversion);
400         s = strcat(s, ":P", ftos(cvar_purechanges_count));
401         s = strcat(s, ":S", ftos(nJoinAllowed(world)));
402         s = strcat(s, ":F", ftos(serverflags));
403         s = strcat(s, ":M", modname);
404         s = strcat(s, "::", GetPlayerScoreString(world, (fullstatus ? 1 : 2)));
405
406         if(teamscores_entities_count)
407         {
408                 float t;
409
410                 s = strcat(s, ":", GetTeamScoreString(0, 1));
411                 for(t = 0; t < 16; ++t)
412                         if(teamscorekeepers[t])
413                                 s = strcat(s, ":", ftos(t+1), ":", GetTeamScoreString(t+1, 1));
414
415                 WinningConditionHelper_winnerteam = -1;
416                 WinningConditionHelper_secondteam = -1;
417                 winnerscorekeeper = world;
418                 secondscorekeeper = world;
419                 for(t = 0; t < 16; ++t)
420                 {
421                         sk = teamscorekeepers[t];
422                         c = TeamScore_Compare(winnerscorekeeper, sk, 1);
423                         if(c < 0)
424                         {
425                                 WinningConditionHelper_secondteam = WinningConditionHelper_winnerteam;
426                                 WinningConditionHelper_winnerteam = t + 1;
427                                 secondscorekeeper = winnerscorekeeper;
428                                 winnerscorekeeper = sk;
429                         }
430                         else
431                         {
432                                 c = TeamScore_Compare(secondscorekeeper, sk, 1);
433                                 if(c < 0)
434                                 {
435                                         WinningConditionHelper_secondteam = t + 1;
436                                         secondscorekeeper = sk;
437                                 }
438                         }
439                 }
440
441                 WinningConditionHelper_equality = (TeamScore_Compare(winnerscorekeeper, secondscorekeeper, 0) == 0);
442                 if(WinningConditionHelper_equality)
443                         WinningConditionHelper_winnerteam = WinningConditionHelper_secondteam = -1;
444
445                 WinningConditionHelper_topscore = winnerscorekeeper.teamscores_primary;
446                 WinningConditionHelper_secondscore = secondscorekeeper.teamscores_primary;
447                 WinningConditionHelper_lowerisbetter = (teamscores_flags_primary & SFL_LOWER_IS_BETTER);
448                 WinningConditionHelper_zeroisworst = (teamscores_flags_primary & SFL_ZERO_IS_WORST);
449
450                 WinningConditionHelper_winner = world; // not supported in teamplay
451                 WinningConditionHelper_second = world; // not supported in teamplay
452         }
453         else
454         {
455                 WinningConditionHelper_winner = world;
456                 WinningConditionHelper_second = world;
457                 winnerscorekeeper = world;
458                 secondscorekeeper = world;
459                 FOR_EACH_PLAYER(p)
460                 {
461                         sk = p.scorekeeper;
462                         c = PlayerScore_Compare(winnerscorekeeper, sk, 1);
463                         if(c < 0)
464                         {
465                                 WinningConditionHelper_second = WinningConditionHelper_winner;
466                                 WinningConditionHelper_winner = p;
467                                 secondscorekeeper = winnerscorekeeper;
468                                 winnerscorekeeper = sk;
469                         }
470                         else
471                         {
472                                 c = PlayerScore_Compare(secondscorekeeper, sk, 1);
473                                 if(c < 0)
474                                 {
475                                         WinningConditionHelper_second = p;
476                                         secondscorekeeper = sk;
477                                 }
478                         }
479                 }
480
481                 WinningConditionHelper_equality = (PlayerScore_Compare(winnerscorekeeper, secondscorekeeper, 0) == 0);
482                 if(WinningConditionHelper_equality)
483                         WinningConditionHelper_winner = WinningConditionHelper_second = world;
484
485                 WinningConditionHelper_topscore = winnerscorekeeper.scores_primary;
486                 WinningConditionHelper_secondscore = secondscorekeeper.scores_primary;
487                 WinningConditionHelper_lowerisbetter = (scores_flags_primary & SFL_LOWER_IS_BETTER);
488                 WinningConditionHelper_zeroisworst = (scores_flags_primary & SFL_ZERO_IS_WORST);
489
490                 WinningConditionHelper_winnerteam = -1; // no teamplay
491                 WinningConditionHelper_secondteam = -1; // no teamplay
492         }
493
494         if(WinningConditionHelper_topscore == 0)
495         {
496                 if(scores_flags_primary & SFL_ZERO_IS_WORST)
497                 {
498                         if(WinningConditionHelper_lowerisbetter)
499                                 WinningConditionHelper_topscore = 999999999;
500                         else
501                                 WinningConditionHelper_topscore = -999999999;
502                 }
503                 WinningConditionHelper_equality = 0;
504         }
505
506         if(WinningConditionHelper_secondscore == 0)
507         {
508                 if(scores_flags_primary & SFL_ZERO_IS_WORST)
509                 {
510                         if(WinningConditionHelper_lowerisbetter)
511                                 WinningConditionHelper_secondscore = 999999999;
512                         else
513                                 WinningConditionHelper_secondscore = -999999999;
514                 }
515         }
516
517         if(worldstatus)
518                 strunzone(worldstatus);
519         worldstatus = strzone(s);
520
521         FOR_EACH_CLIENT(p)
522         {
523                 if(fullstatus)
524                 {
525                         s = GetPlayerScoreString(p, 1);
526                         if(clienttype(p) == CLIENTTYPE_REAL)
527                                 s = strcat(s, ":human");
528                         else
529                                 s = strcat(s, ":bot");
530                         if(p.classname != "player" && !g_arena && p.caplayer != 1 && !g_lms)
531                                 s = strcat(s, ":spectator");
532                 }
533                 else
534                 {
535                         if(p.classname == "player" || g_arena || p.caplayer == 1 || g_lms)
536                                 s = GetPlayerScoreString(p, 2);
537                         else
538                                 s = "-666";
539                 }
540
541                 if(p.clientstatus)
542                         strunzone(p.clientstatus);
543                 p.clientstatus = strzone(s);
544         }
545 }
546
547 string GetScoreLogLabel(string label, float fl)
548 {
549         if(fl & SFL_LOWER_IS_BETTER)
550                 label = strcat(label, "<");
551         if(fl & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_PRIMARY)
552                 label = strcat(label, "!!");
553         else if(fl & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_SECONDARY)
554                 label = strcat(label, "!");
555         return label;
556 }
557
558 string GetPlayerScoreString(entity pl, float shortString)
559 {
560         string out;
561         entity sk;
562         float i, f;
563         string l;
564
565         out = "";
566         if(!pl)
567         {
568                 // label
569                 for(i = 0; i < MAX_SCORE; ++i)
570                         if(scores_flags[i] & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_PRIMARY)
571                         {
572                                 f = scores_flags[i];
573                                 l = scores_label[i];
574                                 out = strcat(out, GetScoreLogLabel(l, f), ",");
575                         }
576                 if(shortString < 2)
577                 for(i = 0; i < MAX_SCORE; ++i)
578                         if(scores_flags[i] & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_SECONDARY)
579                         {
580                                 f = scores_flags[i];
581                                 l = scores_label[i];
582                                 out = strcat(out, GetScoreLogLabel(l, f), ",");
583                         }
584                 if(shortString < 1)
585                 for(i = 0; i < MAX_SCORE; ++i)
586                         if(scores_flags[i] & SFL_SORT_PRIO_MASK != SFL_SORT_PRIO_PRIMARY)
587                         if(scores_flags[i] & SFL_SORT_PRIO_MASK != SFL_SORT_PRIO_SECONDARY)
588                         {
589                                 f = scores_flags[i];
590                                 l = scores_label[i];
591                                 out = strcat(out, GetScoreLogLabel(l, f), ",");
592                         }
593                 out = substring(out, 0, strlen(out) - 1);
594         }
595         else if((sk = pl.scorekeeper))
596         {
597                 for(i = 0; i < MAX_SCORE; ++i)
598                         if(scores_flags[i] & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_PRIMARY)
599                                 out = strcat(out, ftos(sk.(scores[i])), ",");
600                 if(shortString < 2)
601                 for(i = 0; i < MAX_SCORE; ++i)
602                         if(scores_flags[i] & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_SECONDARY)
603                                 out = strcat(out, ftos(sk.(scores[i])), ",");
604                 if(shortString < 1)
605                 for(i = 0; i < MAX_SCORE; ++i)
606                         if(scores_flags[i] & SFL_SORT_PRIO_MASK != SFL_SORT_PRIO_PRIMARY)
607                         if(scores_flags[i] & SFL_SORT_PRIO_MASK != SFL_SORT_PRIO_SECONDARY)
608                                 out = strcat(out, ftos(sk.(scores[i])), ",");
609                 out = substring(out, 0, strlen(out) - 1);
610         }
611         return out;
612 }
613
614 string GetTeamScoreString(float tm, float shortString)
615 {
616         string out;
617         entity sk;
618         float i, f;
619         string l;
620
621         out = "";
622         if(tm == 0)
623         {
624                 // label
625                 for(i = 0; i < MAX_TEAMSCORE; ++i)
626                         if(teamscores_flags[i] & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_PRIMARY)
627                         {
628                                 f = teamscores_flags[i];
629                                 l = teamscores_label[i];
630                                 out = strcat(out, GetScoreLogLabel(l, f), ",");
631                         }
632                 if(shortString < 2)
633                 for(i = 0; i < MAX_TEAMSCORE; ++i)
634                         if(teamscores_flags[i] & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_SECONDARY)
635                         {
636                                 f = teamscores_flags[i];
637                                 l = teamscores_label[i];
638                                 out = strcat(out, GetScoreLogLabel(l, f), ",");
639                         }
640                 if(shortString < 1)
641                 for(i = 0; i < MAX_TEAMSCORE; ++i)
642                         if(teamscores_flags[i] & SFL_SORT_PRIO_MASK != SFL_SORT_PRIO_PRIMARY)
643                         if(teamscores_flags[i] & SFL_SORT_PRIO_MASK != SFL_SORT_PRIO_SECONDARY)
644                         {
645                                 f = teamscores_flags[i];
646                                 l = teamscores_label[i];
647                                 out = strcat(out, GetScoreLogLabel(l, f), ",");
648                         }
649                 out = substring(out, 0, strlen(out) - 1);
650         }
651         else if((sk = teamscorekeepers[tm - 1]))
652         {
653                 for(i = 0; i < MAX_TEAMSCORE; ++i)
654                         if(teamscores_flags[i] & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_PRIMARY)
655                                 out = strcat(out, ftos(sk.(teamscores[i])), ",");
656                 if(shortString < 2)
657                 for(i = 0; i < MAX_TEAMSCORE; ++i)
658                         if(teamscores_flags[i] & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_SECONDARY)
659                                 out = strcat(out, ftos(sk.(teamscores[i])), ",");
660                 if(shortString < 1)
661                 for(i = 0; i < MAX_TEAMSCORE; ++i)
662                         if(teamscores_flags[i] & SFL_SORT_PRIO_MASK != SFL_SORT_PRIO_PRIMARY)
663                         if(teamscores_flags[i] & SFL_SORT_PRIO_MASK != SFL_SORT_PRIO_SECONDARY)
664                                 out = strcat(out, ftos(sk.(teamscores[i])), ",");
665                 out = substring(out, 0, strlen(out) - 1);
666         }
667         return out;
668 }
669
670 float PlayerTeamScore_Compare(entity p1, entity p2, float teams, float strict)
671 {
672         if(teams && teamscores_entities_count)
673         {
674                 if(p1.team != p2.team)
675                 {
676                         entity t1, t2;
677                         float r;
678                         t1 = teamscorekeepers[p1.team - 1];
679                         t2 = teamscorekeepers[p2.team - 1];
680                         r = TeamScore_Compare(t1, t2, ((teams >= 0) ? 1 : strict));
681                         return r;
682                 }
683                 if(teams < 0)
684                         return 0;
685         }
686         
687         return PlayerScore_Compare(p1.scorekeeper, p2.scorekeeper, strict);
688 }
689
690 entity PlayerScore_Sort(.float field, float teams, float strict, float nospectators)
691 {
692         entity p, plist, pprev, pbest, pbestprev, pfirst, plast;
693         float i, j;
694
695         plist = world;
696
697         FOR_EACH_CLIENT(p)
698                 p.field = 0;
699
700         FOR_EACH_CLIENT(p) if(p.scorekeeper)
701         {
702                 if(nospectators)
703                         if(p.frags == FRAGS_SPECTATOR)
704                                 continue;
705
706                 p.chain = plist;
707                 plist = p;
708         }
709         // Now plist points to the whole list.
710         
711         pfirst = plast = world;
712
713         i = j = 0;
714         while(plist)
715         {
716                 pprev = pbestprev = world;
717                 pbest = plist;
718                 for(p = plist; (pprev = p), (p = p.chain); )
719                 {
720                         if(PlayerTeamScore_Compare(p, pbest, teams, strict) > 0)
721                         {
722                                 pbest = p;
723                                 pbestprev = pprev;
724                         }
725                 }
726
727                 // remove pbest out of the chain
728                 if(pbestprev == world)
729                         plist = pbest.chain;
730                 else
731                         pbestprev.chain = pbest.chain;
732                 pbest.chain = world;
733
734                 ++i;
735                 if(!plast || PlayerTeamScore_Compare(plast, pbest, teams, 0))
736                         j = i;
737
738                 pbest.field = j;
739
740                 if not(pfirst)
741                         pfirst = pbest;
742                 if(plast)
743                         plast.chain = pbest;
744                 plast = pbest;
745         }
746
747         return pfirst;
748 }
749
750 float TeamScore_GetCompareValue(float t)
751 {
752         float s;
753         entity sk;
754
755         if(t <= 0 || t >= 16)
756         {
757                 if(gameover)
758                         return 0;
759                 error("Reading score of invalid team!");
760         }
761
762         sk = teamscorekeepers[t - 1];
763         if not(sk)
764                 return -999999999;
765         s = sk.teamscores_primary;
766         if(teamscores_flags_primary & SFL_ZERO_IS_WORST)
767                 if(!s)
768                         return -999999999;
769         if(teamscores_flags_primary & SFL_LOWER_IS_BETTER)
770                 s = -s;
771         return s;
772 }
773
774 #define NAMEWIDTH 22
775 #define SCORESWIDTH 58
776 // TODO put this somewhere in common?
777 string Score_NicePrint_ItemColor(float vflags)
778 {
779         if(vflags & SFL_SORT_PRIO_PRIMARY)
780                 return "^3";
781         else if(vflags & SFL_SORT_PRIO_SECONDARY)
782                 return "^5";
783         else
784                 return "^7";
785 }
786
787 void Score_NicePrint_Team(entity to, float t, float w)
788 {
789         string s, s2;
790         float i;
791         entity sk;
792         float fl, sc;
793         s = "";
794
795         sk = teamscorekeepers[t - 1];
796         if(sk)
797         {
798                 s = strcat(s, Team_ColoredFullName(t));
799                 for(i = 0; i < MAX_TEAMSCORE; ++i)
800                         if(teamscores_label[i] != "")
801                         {
802                                 fl = teamscores_flags[i];
803                                 sc = sk.(teamscores[i]);
804                                 s = strcat(s, " ", Score_NicePrint_ItemColor(fl), ScoreString(fl, sc));
805                         }
806         }
807         else
808                 s = "Scores:";
809
810         s = strcat(s, strpad(max(0, NAMEWIDTH - strlennocol(s)), ""));
811         
812         for(i = 0; i < MAX_SCORE; ++i)
813                 if(scores_label[i] != "")
814                 {
815                         fl = scores_flags[i];
816                         s2 = scores_label[i];
817                         s = strcat(s, " ", Score_NicePrint_ItemColor(fl), strpad(-w, substring(s2, 0, w)));
818                 }
819
820         print_to(to, s);
821 }
822
823 void Score_NicePrint_Player(entity to, entity p, float w)
824 {
825         string s;
826         float i;
827         entity sk;
828         float fl, sc;
829         s = "  ";
830
831         sk = p.scorekeeper;
832
833         s = strcat(s, p.netname);
834         for(;;)
835         {
836                 i = strlennocol(s) - NAMEWIDTH;
837                 if(i > 0)
838                         s = substring(s, 0, strlen(s) - i);
839                 else
840                 {
841                         s = strcat(s, strpad(i, ""));
842                         break;
843                 }
844         }
845         
846         for(i = 0; i < MAX_SCORE; ++i)
847                 if(scores_label[i] != "")
848                 {
849                         fl = scores_flags[i];
850                         sc = sk.(scores[i]);
851                         s = strcat(s, " ", Score_NicePrint_ItemColor(fl), strpad(-w, ScoreString(fl, sc)));
852                 }
853
854         print_to(to, s);
855 }
856
857 void Score_NicePrint_Spectators(entity to)
858 {
859         print_to(to, "Spectators:");
860 }
861
862 void Score_NicePrint_Spectator(entity to, entity p)
863 {
864         print_to(to, strcat("  ", p.netname));
865 }
866
867 .float score_dummyfield;
868 void Score_NicePrint(entity to)
869 {
870         entity p;
871         float t, i;
872         float w;
873
874         t = 0;
875         for(i = 0; i < MAX_SCORE; ++i)
876                 if(scores_label[i] != "")
877                         ++t;
878         w = bound(6, floor(SCORESWIDTH / t - 1), 9);
879
880         p = PlayerScore_Sort(score_dummyfield, 1, 1, 0);
881         t = -1;
882
883         if(!teamscores_entities_count)
884                 Score_NicePrint_Team(to, t, w);
885         while(p)
886         {
887                 if(teamscores_entities_count)
888                         if(t != p.team)
889                                 Score_NicePrint_Team(to, p.team, w);
890                 Score_NicePrint_Player(to, p, w);
891                 t = p.team;
892                 p = p.chain;
893         }
894         
895         t = 0;
896         FOR_EACH_CLIENT(p)
897         if(p.classname != "player")
898         {
899                 if not(t)
900                         Score_NicePrint_Spectators(to);
901                 Score_NicePrint_Spectator(to, p);
902                 t = 1;
903         }
904 }
905
906 void PlayerScore_PlayerStats(entity p)
907 {
908         entity s;
909         float i;
910         s = p.scorekeeper;
911
912         for(i = 0; i < MAX_SCORE; ++i)
913                 if(s.(scores[i]) != 0)
914                         if(scores_label[i] != "")
915                                 PlayerStats_Event(s.owner, strcat(PLAYERSTATS_SCOREBOARD, scores_label[i]), s.(scores[i]));
916 }
917
918 void PlayerScore_TeamStats(void)
919 {
920         entity sk;
921         float t, i;
922         for(t = 0; t < 16; ++t)
923         {
924                 sk = teamscorekeepers[t];
925                 if(!sk)
926                         continue;
927                 for(i = 0; i < MAX_TEAMSCORE; ++i)
928                         if(sk.(teamscores[i]) != 0)
929                                 if(teamscores_label[i] != "")
930                                         PlayerStats_TeamScore(t, strcat(PLAYERSTATS_SCOREBOARD, teamscores_label[i]), sk.(teamscores[i]));
931         }
932 }