]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/scores.qc
Merge remote-tracking branch 'origin/master' into samual/notification_rewrite
[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(FL_TEAM_1, "Red");
208         if(teams >= 2)
209                 TeamScore_Spawn(FL_TEAM_2, "Blue");
210         if(teams >= 3)
211                 TeamScore_Spawn(FL_TEAM_3, "Yellow");
212         if(teams >= 4)
213                 TeamScore_Spawn(FL_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 void PlayerScore_Clear(entity player)
252 {
253         entity sk;
254         float i;
255
256         if(teamscores_entities_count)
257                 return;
258
259         if(g_lms) return;
260         if(g_arena || g_ca) return;
261         if(g_cts) return; // in CTS, you don't lose score by observing
262         if(g_race && g_race_qualifying) return; // 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
274 void Score_ClearAll()
275 {
276         entity p, sk;
277         float i, t;
278         FOR_EACH_CLIENTSLOT(p)
279         {
280                 sk = p.scorekeeper;
281                 if(!sk)
282                         continue;
283                 for(i = 0; i < MAX_SCORE; ++i)
284                 {
285                         if(sk.(scores[i]) != 0)
286                                 if(scores_label[i] != "")
287                                         sk.SendFlags |= pow(2, i);
288                         sk.(scores[i]) = 0;
289                 }
290         }
291         for(t = 0; t < 16; ++t)
292         {
293                 sk = teamscorekeepers[t];
294                 if(!sk)
295                         continue;
296                 for(i = 0; i < MAX_TEAMSCORE; ++i)
297                 {
298                         if(sk.(teamscores[i]) != 0)
299                                 if(teamscores_label[i] != "")
300                                         sk.SendFlags |= pow(2, i);
301                         sk.(teamscores[i]) = 0;
302                 }
303         }
304 }
305
306 void PlayerScore_Attach(entity player)
307 {
308         entity sk;
309         if(player.scorekeeper)
310                 error("player already has a scorekeeper");
311         sk = spawn();
312         sk.owner = player;
313         Net_LinkEntity(sk, FALSE, 0, PlayerScore_SendEntity);
314         player.scorekeeper = sk;
315 }
316
317 void PlayerScore_Detach(entity player)
318 {
319         if(!player.scorekeeper)
320                 error("player has no scorekeeper");
321         remove(player.scorekeeper);
322         player.scorekeeper = world;
323 }
324
325 float PlayerScore_Add(entity player, float scorefield, float score)
326 {
327         entity s;
328
329         if(gameover)
330         if not(g_lms && scorefield == SP_LMS_RANK) // allow writing to this field in intermission as it is needed for newly joining players
331                 score = 0;
332
333         if(!scores_initialized) return 0; // FIXME remove this when everything uses this system
334         s = player.scorekeeper;
335         if(!s)
336         {
337                 if(gameover)
338                         return 0;
339                 backtrace("Adding score to unknown player!");
340                 return 0;
341         }
342         if(score)
343                 if(scores_label[scorefield] != "")
344                         s.SendFlags |= pow(2, scorefield);
345         if(!inWarmupStage)
346                 PlayerStats_Event(s.owner, strcat(PLAYERSTATS_TOTAL, scores_label[scorefield]), score);
347         return (s.(scores[scorefield]) += score);
348 }
349
350 float PlayerTeamScore_Add(entity player, float pscorefield, float tscorefield, float score)
351 {
352         float r;
353         r = PlayerScore_Add(player, pscorefield, score);
354         if(teamscores_entities_count) // only for teamplay
355                 r = TeamScore_Add(player, tscorefield, score);
356         return r;
357 }
358
359 float PlayerScore_Compare(entity t1, entity t2, float strict)
360 {
361         if(!t1 || !t2) return (!t2) - !t1;
362
363         vector result = '0 0 0';
364         float i;
365         for(i = 0; i < MAX_SCORE; ++i)
366         {
367                 var .float f;
368                 f = scores[i];
369                 result = ScoreField_Compare(t1, t2, f, scores_flags[i], result, strict);
370         }
371
372         if (result_x == 0 && strict)
373                 result_x = num_for_edict(t1.owner) - num_for_edict(t2.owner);
374
375         return result_x;
376 }
377
378 void WinningConditionHelper()
379 {
380         float c;
381         string s;
382         entity p;
383         float fullstatus;
384         entity winnerscorekeeper;
385         entity secondscorekeeper;
386         entity sk;
387
388         // format:
389         // gametype:P<pure>:S<slots>::plabel,plabel:tlabel,tlabel:teamid:tscore,tscore:teamid:tscore,tscore
390         // score labels always start with a symbol or with lower case
391         // so to match pure, match for :P0:
392         // to match full, match for :S0:
393
394         s = GetGametype();
395         s = strcat(s, ":", autocvar_g_xonoticversion);
396         s = strcat(s, ":P", ftos(cvar_purechanges_count));
397         s = strcat(s, ":S", ftos(nJoinAllowed(world)));
398         s = strcat(s, ":F", ftos(serverflags));
399         s = strcat(s, ":M", modname);
400         s = strcat(s, "::", GetPlayerScoreString(world, 1)); // make this 1 once we can, note: this doesn't contain any :<letter>
401
402         fullstatus = autocvar_g_full_getstatus_responses;
403
404         if(teamscores_entities_count)
405         {
406                 float t;
407
408                 s = strcat(s, ":", GetTeamScoreString(0, 1));
409                 for(t = 0; t < 16; ++t)
410                         if(teamscorekeepers[t])
411                                 s = strcat(s, ":", ftos(t+1), ":", GetTeamScoreString(t+1, 1));
412
413                 WinningConditionHelper_winnerteam = -1;
414                 WinningConditionHelper_secondteam = -1;
415                 winnerscorekeeper = world;
416                 secondscorekeeper = world;
417                 for(t = 0; t < 16; ++t)
418                 {
419                         sk = teamscorekeepers[t];
420                         c = TeamScore_Compare(winnerscorekeeper, sk, 1);
421                         if(c < 0)
422                         {
423                                 WinningConditionHelper_secondteam = WinningConditionHelper_winnerteam;
424                                 WinningConditionHelper_winnerteam = t + 1;
425                                 secondscorekeeper = winnerscorekeeper;
426                                 winnerscorekeeper = sk;
427                         }
428                         else
429                         {
430                                 c = TeamScore_Compare(secondscorekeeper, sk, 1);
431                                 if(c < 0)
432                                 {
433                                         WinningConditionHelper_secondteam = t + 1;
434                                         secondscorekeeper = sk;
435                                 }
436                         }
437                 }
438
439                 WinningConditionHelper_equality = (TeamScore_Compare(winnerscorekeeper, secondscorekeeper, 0) == 0);
440                 if(WinningConditionHelper_equality)
441                         WinningConditionHelper_winnerteam = WinningConditionHelper_secondteam = -1;
442
443                 WinningConditionHelper_topscore = winnerscorekeeper.teamscores_primary;
444                 WinningConditionHelper_secondscore = secondscorekeeper.teamscores_primary;
445                 WinningConditionHelper_lowerisbetter = (teamscores_flags_primary & SFL_LOWER_IS_BETTER);
446                 WinningConditionHelper_zeroisworst = (teamscores_flags_primary & SFL_ZERO_IS_WORST);
447
448                 WinningConditionHelper_winner = world; // not supported in teamplay
449                 WinningConditionHelper_second = world; // not supported in teamplay
450         }
451         else
452         {
453                 WinningConditionHelper_winner = world;
454                 WinningConditionHelper_second = world;
455                 winnerscorekeeper = world;
456                 secondscorekeeper = world;
457                 FOR_EACH_PLAYER(p)
458                 {
459                         sk = p.scorekeeper;
460                         c = PlayerScore_Compare(winnerscorekeeper, sk, 1);
461                         if(c < 0)
462                         {
463                                 WinningConditionHelper_second = WinningConditionHelper_winner;
464                                 WinningConditionHelper_winner = p;
465                                 secondscorekeeper = winnerscorekeeper;
466                                 winnerscorekeeper = sk;
467                         }
468                         else
469                         {
470                                 c = PlayerScore_Compare(secondscorekeeper, sk, 1);
471                                 if(c < 0)
472                                 {
473                                         WinningConditionHelper_second = p;
474                                         secondscorekeeper = sk;
475                                 }
476                         }
477                 }
478
479                 WinningConditionHelper_equality = (PlayerScore_Compare(winnerscorekeeper, secondscorekeeper, 0) == 0);
480                 if(WinningConditionHelper_equality)
481                         WinningConditionHelper_winner = WinningConditionHelper_second = world;
482
483                 WinningConditionHelper_topscore = winnerscorekeeper.scores_primary;
484                 WinningConditionHelper_secondscore = secondscorekeeper.scores_primary;
485                 WinningConditionHelper_lowerisbetter = (scores_flags_primary & SFL_LOWER_IS_BETTER);
486                 WinningConditionHelper_zeroisworst = (scores_flags_primary & SFL_ZERO_IS_WORST);
487
488                 WinningConditionHelper_winnerteam = -1; // no teamplay
489                 WinningConditionHelper_secondteam = -1; // no teamplay
490         }
491
492         if(WinningConditionHelper_topscore == 0)
493         {
494                 if(scores_flags_primary & SFL_ZERO_IS_WORST)
495                 {
496                         if(WinningConditionHelper_lowerisbetter)
497                                 WinningConditionHelper_topscore = 999999999;
498                         else
499                                 WinningConditionHelper_topscore = -999999999;
500                 }
501                 WinningConditionHelper_equality = 0;
502         }
503
504         if(WinningConditionHelper_secondscore == 0)
505         {
506                 if(scores_flags_primary & SFL_ZERO_IS_WORST)
507                 {
508                         if(WinningConditionHelper_lowerisbetter)
509                                 WinningConditionHelper_secondscore = 999999999;
510                         else
511                                 WinningConditionHelper_secondscore = -999999999;
512                 }
513         }
514
515         if(worldstatus)
516                 strunzone(worldstatus);
517         worldstatus = strzone(s);
518
519         FOR_EACH_CLIENT(p)
520         {
521                 if(fullstatus)
522                 {
523                         s = GetPlayerScoreString(p, 1);
524                         if(clienttype(p) == CLIENTTYPE_REAL)
525                                 s = strcat(s, ":human");
526                         else
527                                 s = strcat(s, ":bot");
528                         if(p.classname != "player" && !g_arena && !g_ca && !g_lms)
529                                 s = strcat(s, ":spectator");
530                 }
531                 else
532                 {
533                         if(p.classname == "player" || g_arena || g_ca || g_lms)
534                                 s = GetPlayerScoreString(p, 2);
535                         else
536                                 s = "-666";
537                 }
538
539                 if(p.clientstatus)
540                         strunzone(p.clientstatus);
541                 p.clientstatus = strzone(s);
542         }
543 }
544
545 string GetScoreLogLabel(string label, float fl)
546 {
547         if(fl & SFL_LOWER_IS_BETTER)
548                 label = strcat(label, "<");
549         if(fl & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_PRIMARY)
550                 label = strcat(label, "!!");
551         else if(fl & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_SECONDARY)
552                 label = strcat(label, "!");
553         return label;
554 }
555
556 string GetPlayerScoreString(entity pl, float shortString)
557 {
558         string out;
559         entity sk;
560         float i, f;
561         string l;
562
563         out = "";
564         if(!pl)
565         {
566                 // label
567                 for(i = 0; i < MAX_SCORE; ++i)
568                         if(scores_flags[i] & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_PRIMARY)
569                         {
570                                 f = scores_flags[i];
571                                 l = scores_label[i];
572                                 out = strcat(out, GetScoreLogLabel(l, f), ",");
573                         }
574                 if(shortString < 2)
575                 for(i = 0; i < MAX_SCORE; ++i)
576                         if(scores_flags[i] & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_SECONDARY)
577                         {
578                                 f = scores_flags[i];
579                                 l = scores_label[i];
580                                 out = strcat(out, GetScoreLogLabel(l, f), ",");
581                         }
582                 if(shortString < 1)
583                 for(i = 0; i < MAX_SCORE; ++i)
584                         if(scores_flags[i] & SFL_SORT_PRIO_MASK != SFL_SORT_PRIO_PRIMARY)
585                         if(scores_flags[i] & SFL_SORT_PRIO_MASK != SFL_SORT_PRIO_SECONDARY)
586                         {
587                                 f = scores_flags[i];
588                                 l = scores_label[i];
589                                 out = strcat(out, GetScoreLogLabel(l, f), ",");
590                         }
591                 out = substring(out, 0, strlen(out) - 1);
592         }
593         else if((sk = pl.scorekeeper))
594         {
595                 for(i = 0; i < MAX_SCORE; ++i)
596                         if(scores_flags[i] & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_PRIMARY)
597                                 out = strcat(out, ftos(sk.(scores[i])), ",");
598                 if(shortString < 2)
599                 for(i = 0; i < MAX_SCORE; ++i)
600                         if(scores_flags[i] & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_SECONDARY)
601                                 out = strcat(out, ftos(sk.(scores[i])), ",");
602                 if(shortString < 1)
603                 for(i = 0; i < MAX_SCORE; ++i)
604                         if(scores_flags[i] & SFL_SORT_PRIO_MASK != SFL_SORT_PRIO_PRIMARY)
605                         if(scores_flags[i] & SFL_SORT_PRIO_MASK != SFL_SORT_PRIO_SECONDARY)
606                                 out = strcat(out, ftos(sk.(scores[i])), ",");
607                 out = substring(out, 0, strlen(out) - 1);
608         }
609         return out;
610 }
611
612 string GetTeamScoreString(float tm, float shortString)
613 {
614         string out;
615         entity sk;
616         float i, f;
617         string l;
618
619         out = "";
620         if(tm == 0)
621         {
622                 // label
623                 for(i = 0; i < MAX_SCORE; ++i)
624                         if(teamscores_flags[i] & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_PRIMARY)
625                         {
626                                 f = teamscores_flags[i];
627                                 l = teamscores_label[i];
628                                 out = strcat(out, GetScoreLogLabel(l, f), ",");
629                         }
630                 if(shortString < 2)
631                 for(i = 0; i < MAX_SCORE; ++i)
632                         if(teamscores_flags[i] & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_SECONDARY)
633                         {
634                                 f = teamscores_flags[i];
635                                 l = teamscores_label[i];
636                                 out = strcat(out, GetScoreLogLabel(l, f), ",");
637                         }
638                 if(shortString < 1)
639                 for(i = 0; i < MAX_SCORE; ++i)
640                         if(teamscores_flags[i] & SFL_SORT_PRIO_MASK != SFL_SORT_PRIO_PRIMARY)
641                         if(teamscores_flags[i] & SFL_SORT_PRIO_MASK != SFL_SORT_PRIO_SECONDARY)
642                         {
643                                 f = teamscores_flags[i];
644                                 l = teamscores_label[i];
645                                 out = strcat(out, GetScoreLogLabel(l, f), ",");
646                         }
647                 out = substring(out, 0, strlen(out) - 1);
648         }
649         else if((sk = teamscorekeepers[tm - 1]))
650         {
651                 for(i = 0; i < MAX_TEAMSCORE; ++i)
652                         if(teamscores_flags[i] & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_PRIMARY)
653                                 out = strcat(out, ftos(sk.(teamscores[i])), ",");
654                 if(shortString < 2)
655                 for(i = 0; i < MAX_TEAMSCORE; ++i)
656                         if(teamscores_flags[i] & SFL_SORT_PRIO_MASK == SFL_SORT_PRIO_SECONDARY)
657                                 out = strcat(out, ftos(sk.(teamscores[i])), ",");
658                 if(shortString < 1)
659                 for(i = 0; i < MAX_TEAMSCORE; ++i)
660                         if(teamscores_flags[i] & SFL_SORT_PRIO_MASK != SFL_SORT_PRIO_PRIMARY)
661                         if(teamscores_flags[i] & SFL_SORT_PRIO_MASK != SFL_SORT_PRIO_SECONDARY)
662                                 out = strcat(out, ftos(sk.(teamscores[i])), ",");
663                 out = substring(out, 0, strlen(out) - 1);
664         }
665         return out;
666 }
667
668 float PlayerTeamScore_Compare(entity p1, entity p2, float strict)
669 {
670         if(teamscores_entities_count)
671                 if(p1.team != p2.team)
672                 {
673                         entity t1, t2;
674                         float r;
675                         t1 = teamscorekeepers[p1.team - 1];
676                         t2 = teamscorekeepers[p2.team - 1];
677                         r = TeamScore_Compare(t1, t2, strict);
678                         return r;
679                 }
680         
681         return PlayerScore_Compare(p1.scorekeeper, p2.scorekeeper, strict);
682 }
683
684 entity PlayerScore_Sort(.float field, float strict)
685 {
686         entity p, plist, pprev, pbest, pbestprev, pfirst, plast;
687         float i, j;
688
689         plist = world;
690
691         FOR_EACH_CLIENT(p)
692                 p.field = 0;
693
694         FOR_EACH_PLAYER(p) if(p.scorekeeper)
695         {
696                 p.chain = plist;
697                 plist = p;
698         }
699         // Now plist points to the whole list.
700         
701         pfirst = plast = world;
702
703         i = j = 0;
704         while(plist)
705         {
706                 pprev = pbestprev = world;
707                 pbest = plist;
708                 for(p = plist; (pprev = p), (p = p.chain); )
709                 {
710                         if(PlayerTeamScore_Compare(p, pbest, strict) > 0)
711                         {
712                                 pbest = p;
713                                 pbestprev = pprev;
714                         }
715                 }
716
717                 // remove pbest out of the chain
718                 if(pbestprev == world)
719                         plist = pbest.chain;
720                 else
721                         pbestprev.chain = pbest.chain;
722                 pbest.chain = world;
723
724                 ++i;
725                 if(!plast || PlayerTeamScore_Compare(plast, pbest, 0))
726                         j = i;
727
728                 pbest.field = j;
729
730                 if not(pfirst)
731                         pfirst = pbest;
732                 if(plast)
733                         plast.chain = pbest;
734                 plast = pbest;
735         }
736
737         return pfirst;
738 }
739
740 float TeamScore_GetCompareValue(float t)
741 {
742         float s;
743         entity sk;
744
745         if(t <= 0 || t >= 16)
746         {
747                 if(gameover)
748                         return 0;
749                 error("Reading score of invalid team!");
750         }
751
752         sk = teamscorekeepers[t - 1];
753         if not(sk)
754                 return -999999999;
755         s = sk.teamscores_primary;
756         if(teamscores_flags_primary & SFL_ZERO_IS_WORST)
757                 if(!s)
758                         return -999999999;
759         if(teamscores_flags_primary & SFL_LOWER_IS_BETTER)
760                 s = -s;
761         return s;
762 }
763
764 #define NAMEWIDTH 22
765 #define SCORESWIDTH 58
766 // TODO put this somewhere in common?
767 string Score_NicePrint_ItemColor(float vflags)
768 {
769         if(vflags & SFL_SORT_PRIO_PRIMARY)
770                 return "^3";
771         else if(vflags & SFL_SORT_PRIO_SECONDARY)
772                 return "^5";
773         else
774                 return "^7";
775 }
776
777 void Score_NicePrint_Team(entity to, float t, float w)
778 {
779         string s, s2;
780         float i;
781         entity sk;
782         float fl, sc;
783         s = "";
784
785         sk = teamscorekeepers[t - 1];
786         if(sk)
787         {
788                 s = strcat(s, Team_ColoredFullName(t));
789                 for(i = 0; i < MAX_TEAMSCORE; ++i)
790                         if(teamscores_label[i] != "")
791                         {
792                                 fl = teamscores_flags[i];
793                                 sc = sk.(teamscores[i]);
794                                 s = strcat(s, " ", Score_NicePrint_ItemColor(fl), ScoreString(fl, sc));
795                         }
796         }
797         else
798                 s = "Scores:";
799
800         s = strcat(s, strpad(max(0, NAMEWIDTH - strlennocol(s)), ""));
801         
802         for(i = 0; i < MAX_SCORE; ++i)
803                 if(scores_label[i] != "")
804                 {
805                         fl = scores_flags[i];
806                         s2 = scores_label[i];
807                         s = strcat(s, " ", Score_NicePrint_ItemColor(fl), strpad(-w, substring(s2, 0, w)));
808                 }
809
810         print_to(to, s);
811 }
812
813 void Score_NicePrint_Player(entity to, entity p, float w)
814 {
815         string s;
816         float i;
817         entity sk;
818         float fl, sc;
819         s = "  ";
820
821         sk = p.scorekeeper;
822
823         s = strcat(s, p.netname);
824         for(;;)
825         {
826                 i = strlennocol(s) - NAMEWIDTH;
827                 if(i > 0)
828                         s = substring(s, 0, strlen(s) - i);
829                 else
830                 {
831                         s = strcat(s, strpad(i, ""));
832                         break;
833                 }
834         }
835         
836         for(i = 0; i < MAX_SCORE; ++i)
837                 if(scores_label[i] != "")
838                 {
839                         fl = scores_flags[i];
840                         sc = sk.(scores[i]);
841                         s = strcat(s, " ", Score_NicePrint_ItemColor(fl), strpad(-w, ScoreString(fl, sc)));
842                 }
843
844         print_to(to, s);
845 }
846
847 void Score_NicePrint_Spectators(entity to)
848 {
849         print_to(to, "Spectators:");
850 }
851
852 void Score_NicePrint_Spectator(entity to, entity p)
853 {
854         print_to(to, strcat("  ", p.netname));
855 }
856
857 .float score_dummyfield;
858 void Score_NicePrint(entity to)
859 {
860         entity p;
861         float t, i;
862         float w;
863
864         t = 0;
865         for(i = 0; i < MAX_SCORE; ++i)
866                 if(scores_label[i] != "")
867                         ++t;
868         w = bound(6, floor(SCORESWIDTH / t - 1), 9);
869
870         p = PlayerScore_Sort(score_dummyfield, 1);
871         t = -1;
872
873         if(!teamscores_entities_count)
874                 Score_NicePrint_Team(to, t, w);
875         while(p)
876         {
877                 if(teamscores_entities_count)
878                         if(t != p.team)
879                                 Score_NicePrint_Team(to, p.team, w);
880                 Score_NicePrint_Player(to, p, w);
881                 t = p.team;
882                 p = p.chain;
883         }
884         
885         t = 0;
886         FOR_EACH_CLIENT(p)
887         if(p.classname != "player")
888         {
889                 if not(t)
890                         Score_NicePrint_Spectators(to);
891                 Score_NicePrint_Spectator(to, p);
892                 t = 1;
893         }
894 }
895
896 void PlayerScore_PlayerStats(entity p)
897 {
898         entity s;
899         float i;
900         s = p.scorekeeper;
901
902         for(i = 0; i < MAX_SCORE; ++i)
903                 if(s.(scores[i]) != 0)
904                         if(scores_label[i] != "")
905                                 PlayerStats_Event(s.owner, strcat(PLAYERSTATS_SCOREBOARD, scores_label[i]), s.(scores[i]));
906 }
907
908 void PlayerScore_TeamStats(void)
909 {
910         entity sk;
911         float t, i;
912         for(t = 0; t < 16; ++t)
913         {
914                 sk = teamscorekeepers[t];
915                 if(!sk)
916                         continue;
917                 for(i = 0; i < MAX_TEAMSCORE; ++i)
918                         if(sk.(teamscores[i]) != 0)
919                                 if(teamscores_label[i] != "")
920                                         PlayerStats_TeamScore(t, strcat(PLAYERSTATS_SCOREBOARD, teamscores_label[i]), sk.(teamscores[i]));
921         }
922 }