scoreboard: try "&" instead of "and"
[xonotic/xonotic-data.pk3dir.git] / qcsrc / client / hud / panel / scoreboard.qc
1 #include "scoreboard.qh"
2
3 #include <client/autocvars.qh>
4 #include <client/defs.qh>
5 #include <client/main.qh>
6 #include <client/miscfunctions.qh>
7 #include "quickmenu.qh"
8 #include <common/ent_cs.qh>
9 #include <common/constants.qh>
10 #include <common/net_linked.qh>
11 #include <common/mapinfo.qh>
12 #include <common/minigames/cl_minigames.qh>
13 #include <common/scores.qh>
14 #include <common/stats.qh>
15 #include <common/teams.qh>
16
17 // Scoreboard (#24)
18
19 const int MAX_SBT_FIELDS = MAX_SCORE;
20
21 PlayerScoreField sbt_field[MAX_SBT_FIELDS + 1];
22 float sbt_field_size[MAX_SBT_FIELDS + 1];
23 string sbt_field_title[MAX_SBT_FIELDS + 1];
24 int sbt_num_fields;
25
26 string autocvar_hud_fontsize;
27 string hud_fontsize_str;
28 float max_namesize;
29
30 float sbt_bg_alpha;
31 float sbt_fg_alpha;
32 float sbt_fg_alpha_self;
33 bool sbt_highlight;
34 float sbt_highlight_alpha;
35 float sbt_highlight_alpha_self;
36
37 // provide basic panel cvars to old clients
38 // TODO remove them after a future release (0.8.2+)
39 noref string autocvar_hud_panel_scoreboard_pos = "0.150000 0.150000";
40 noref string autocvar_hud_panel_scoreboard_size = "0.700000 0.700000";
41 noref string autocvar_hud_panel_scoreboard_bg = "border_default";
42 noref string autocvar_hud_panel_scoreboard_bg_color = "0 0.3 0.5";
43 noref string autocvar_hud_panel_scoreboard_bg_color_team = "";
44 noref string autocvar_hud_panel_scoreboard_bg_alpha = "0.7";
45 noref string autocvar_hud_panel_scoreboard_bg_border = "";
46 noref string autocvar_hud_panel_scoreboard_bg_padding = "";
47
48 float autocvar_hud_panel_scoreboard_fadeinspeed = 10;
49 float autocvar_hud_panel_scoreboard_fadeoutspeed = 5;
50 float autocvar_hud_panel_scoreboard_respawntime_decimals = 1;
51 float autocvar_hud_panel_scoreboard_table_bg_alpha = 0;
52 float autocvar_hud_panel_scoreboard_table_bg_scale = 0.25;
53 float autocvar_hud_panel_scoreboard_table_fg_alpha = 0.9;
54 float autocvar_hud_panel_scoreboard_table_fg_alpha_self = 1;
55 bool autocvar_hud_panel_scoreboard_table_highlight = true;
56 float autocvar_hud_panel_scoreboard_table_highlight_alpha = 0.2;
57 float autocvar_hud_panel_scoreboard_table_highlight_alpha_self = 0.4;
58 float autocvar_hud_panel_scoreboard_bg_teams_color_team = 0;
59 float autocvar_hud_panel_scoreboard_namesize = 15;
60 float autocvar_hud_panel_scoreboard_team_size_position = 0;
61
62 bool autocvar_hud_panel_scoreboard_accuracy = true;
63 bool autocvar_hud_panel_scoreboard_accuracy_doublerows = false;
64 bool autocvar_hud_panel_scoreboard_accuracy_nocolors = false;
65 float autocvar_hud_panel_scoreboard_accuracy_showdelay = 2;
66 float autocvar_hud_panel_scoreboard_accuracy_showdelay_minpos = 0.75;
67
68 bool autocvar_hud_panel_scoreboard_ctf_leaderboard = true;
69
70 bool autocvar_hud_panel_scoreboard_dynamichud = false;
71
72 float autocvar_hud_panel_scoreboard_maxheight = 0.6;
73 bool autocvar_hud_panel_scoreboard_others_showscore = true;
74 bool autocvar_hud_panel_scoreboard_spectators_showping = true;
75 bool autocvar_hud_panel_scoreboard_spectators_aligned = false;
76 float autocvar_hud_panel_scoreboard_minwidth = 0.4;
77
78 // mode 0: returns translated label
79 // mode 1: prints name and description of all the labels
80 string Label_getInfo(string label, int mode)
81 {
82         if (mode == 1)
83                 label = "bckills"; // first case in the switch
84
85         switch(label)
86         {
87                 case "bckills":      if (!mode) return CTX(_("SCO^bckills"));      else LOG_INFO(strcat("^3", "bckills", "            ^7", _("Number of ball carrier kills")));
88                 case "bctime":       if (!mode) return CTX(_("SCO^bctime"));       else LOG_INFO(strcat("^3", "bctime", "             ^7", _("Total amount of time holding the ball in Keepaway")));
89                 case "caps":         if (!mode) return CTX(_("SCO^caps"));         else LOG_INFO(strcat("^3", "caps", "               ^7", _("How often a flag (CTF) or a key (KeyHunt) was captured")));
90                 case "captime":      if (!mode) return CTX(_("SCO^captime"));      else LOG_INFO(strcat("^3", "captime", "            ^7", _("Time of fastest capture (CTF)")));
91                 case "deaths":       if (!mode) return CTX(_("SCO^deaths"));       else LOG_INFO(strcat("^3", "deaths", "             ^7", _("Number of deaths")));
92                 case "destroyed":    if (!mode) return CTX(_("SCO^destroyed"));    else LOG_INFO(strcat("^3", "destroyed", "          ^7", _("Number of keys destroyed by pushing them into void")));
93                 case "dmg":          if (!mode) return CTX(_("SCO^damage"));       else LOG_INFO(strcat("^3", "dmg", "                ^7", _("The total damage done")));
94                 case "dmgtaken":     if (!mode) return CTX(_("SCO^dmgtaken"));     else LOG_INFO(strcat("^3", "dmgtaken", "           ^7", _("The total damage taken")));
95                 case "drops":        if (!mode) return CTX(_("SCO^drops"));        else LOG_INFO(strcat("^3", "drops", "              ^7", _("Number of flag drops")));
96                 case "elo":          if (!mode) return CTX(_("SCO^elo"));          else LOG_INFO(strcat("^3", "elo", "                ^7", _("Player ELO")));
97                 case "fastest":      if (!mode) return CTX(_("SCO^fastest"));      else LOG_INFO(strcat("^3", "fastest", "            ^7", _("Time of fastest lap (Race/CTS)")));
98                 case "faults":       if (!mode) return CTX(_("SCO^faults"));       else LOG_INFO(strcat("^3", "faults", "             ^7", _("Number of faults committed")));
99                 case "fckills":      if (!mode) return CTX(_("SCO^fckills"));      else LOG_INFO(strcat("^3", "fckills", "            ^7", _("Number of flag carrier kills")));
100                 case "fps":          if (!mode) return CTX(_("SCO^fps"));          else LOG_INFO(strcat("^3", "fps", "                ^7", _("FPS")));
101                 case "frags":        if (!mode) return CTX(_("SCO^frags"));        else LOG_INFO(strcat("^3", "frags", "              ^7", _("Number of kills minus suicides")));
102                 case "goals":        if (!mode) return CTX(_("SCO^goals"));        else LOG_INFO(strcat("^3", "goals", "              ^7", _("Number of goals scored")));
103                 case "kckills":      if (!mode) return CTX(_("SCO^kckills"));      else LOG_INFO(strcat("^3", "kckills", "            ^7", _("Number of keys carrier kills")));
104                 case "kd":           if (!mode) return CTX(_("SCO^k/d"));          else LOG_INFO(strcat("^3", "kd", "                 ^7", _("The kill-death ratio")));
105                 case "kdr":          if (!mode) return CTX(_("SCO^kdr"));          else LOG_INFO(strcat("^3", "kdr", "                ^7", _("The kill-death ratio")));
106                 case "kdratio":      if (!mode) return CTX(_("SCO^kdratio"));      else LOG_INFO(strcat("^3", "kdratio", "            ^7", _("The kill-death ratio")));
107                 case "kills":        if (!mode) return CTX(_("SCO^kills"));        else LOG_INFO(strcat("^3", "kills", "              ^7", _("Number of kills")));
108                 case "laps":         if (!mode) return CTX(_("SCO^laps"));         else LOG_INFO(strcat("^3", "laps", "               ^7", _("Number of laps finished (Race/CTS)")));
109                 case "lives":        if (!mode) return CTX(_("SCO^lives"));        else LOG_INFO(strcat("^3", "lives", "              ^7", _("Number of lives (LMS)")));
110                 case "losses":       if (!mode) return CTX(_("SCO^losses"));       else LOG_INFO(strcat("^3", "losses", "             ^7", _("Number of times a key was lost")));
111                 case "name":         if (!mode) return CTX(_("SCO^name"));         else LOG_INFO(strcat("^3", "name", "               ^7", _("Player name")));
112                 case "nick":         if (!mode) return CTX(_("SCO^nick"));         else LOG_INFO(strcat("^3", "nick", "               ^7", _("Player name")));
113                 case "objectives":   if (!mode) return CTX(_("SCO^objectives"));   else LOG_INFO(strcat("^3", "objectives", "         ^7", _("Number of objectives destroyed")));
114                 case "pickups":      if (!mode) return CTX(_("SCO^pickups"));      else LOG_INFO(strcat("^3", "pickups", "            ^7", _("How often a flag (CTF) or a key (KeyHunt) or a ball (Keepaway) was picked up")));
115                 case "ping":         if (!mode) return CTX(_("SCO^ping"));         else LOG_INFO(strcat("^3", "ping", "               ^7", _("Ping time")));
116                 case "pl":           if (!mode) return CTX(_("SCO^pl"));           else LOG_INFO(strcat("^3", "pl", "                 ^7", _("Packet loss")));
117                 case "pushes":       if (!mode) return CTX(_("SCO^pushes"));       else LOG_INFO(strcat("^3", "pushes", "             ^7", _("Number of players pushed into void")));
118                 case "rank":         if (!mode) return CTX(_("SCO^rank"));         else LOG_INFO(strcat("^3", "rank", "               ^7", _("Player rank")));
119                 case "returns":      if (!mode) return CTX(_("SCO^returns"));      else LOG_INFO(strcat("^3", "returns", "            ^7", _("Number of flag returns")));
120                 case "revivals":     if (!mode) return CTX(_("SCO^revivals"));     else LOG_INFO(strcat("^3", "revivals", "           ^7", _("Number of revivals")));
121                 case "rounds":       if (!mode) return CTX(_("SCO^rounds won"));   else LOG_INFO(strcat("^3", "rounds", "             ^7", _("Number of rounds won")));
122                 case "score":        if (!mode) return CTX(_("SCO^score"));        else LOG_INFO(strcat("^3", "score", "              ^7", _("Total score")));
123                 case "suicides":     if (!mode) return CTX(_("SCO^suicides"));     else LOG_INFO(strcat("^3", "suicides", "           ^7", _("Number of suicides")));
124                 case "sum":          if (!mode) return CTX(_("SCO^sum"));          else LOG_INFO(strcat("^3", "sum", "                ^7", _("Number of kills minus deaths")));
125                 case "takes":        if (!mode) return CTX(_("SCO^takes"));        else LOG_INFO(strcat("^3", "takes", "              ^7", _("Number of domination points taken (Domination)")));
126                 case "teamkills":    if (!mode) return CTX(_("SCO^teamkills"));    else LOG_INFO(strcat("^3", "teamkills", "          ^7", _("Number of teamkills")));
127                 case "ticks":        if (!mode) return CTX(_("SCO^ticks"));        else LOG_INFO(strcat("^3", "ticks", "              ^7", _("Number of ticks (Domination)")));
128                 case "time":         if (!mode) return CTX(_("SCO^time"));         else LOG_INFO(strcat("^3", "time", "               ^7", _("Total time raced (Race/CTS)")));
129                 default: return label;
130         }
131         return label;
132 }
133
134 void PrintScoresLabels() { Label_getInfo(string_null, 1); }
135 string TranslateScoresLabel(string label) { return Label_getInfo(label, 0); }
136
137 void Scoreboard_InitScores()
138 {
139         int i, f;
140
141         ps_primary = ps_secondary = NULL;
142         ts_primary = ts_secondary = -1;
143         FOREACH(Scores, true, {
144                 f = (scores_flags(it) & SFL_SORT_PRIO_MASK);
145                 if(f == SFL_SORT_PRIO_PRIMARY)
146                         ps_primary = it;
147                 if(f == SFL_SORT_PRIO_SECONDARY)
148                         ps_secondary = it;
149         });
150         if(ps_secondary == NULL)
151                 ps_secondary = ps_primary;
152
153         for(i = 0; i < MAX_TEAMSCORE; ++i)
154         {
155                 f = (teamscores_flags(i) & SFL_SORT_PRIO_MASK);
156                 if(f == SFL_SORT_PRIO_PRIMARY)
157                         ts_primary = i;
158                 if(f == SFL_SORT_PRIO_SECONDARY)
159                         ts_secondary = i;
160         }
161         if(ts_secondary == -1)
162                 ts_secondary = ts_primary;
163
164         Cmd_Scoreboard_SetFields(0);
165 }
166
167 //float lastpnum;
168 void Scoreboard_UpdatePlayerTeams()
169 {
170         entity pl, tmp;
171         //int num = 0;
172         for(pl = players.sort_next; pl; pl = pl.sort_next)
173         {
174                 //num += 1;
175                 int Team = entcs_GetScoreTeam(pl.sv_entnum);
176                 if(SetTeam(pl, Team))
177                 {
178                         tmp = pl.sort_prev;
179                         Scoreboard_UpdatePlayerPos(pl);
180                         if(tmp)
181                                 pl = tmp;
182                         else
183                                 pl = players.sort_next;
184                 }
185         }
186         /*
187         if(num != lastpnum)
188                 print(strcat("PNUM: ", ftos(num), "\n"));
189         lastpnum = num;
190         */
191 }
192
193 int Scoreboard_CompareScore(int vl, int vr, int f)
194 {
195         TC(int, vl); TC(int, vr); TC(int, f);
196         if(f & SFL_ZERO_IS_WORST)
197         {
198                 if(vl == 0 && vr != 0)
199                         return 1;
200                 if(vl != 0 && vr == 0)
201                         return 0;
202         }
203         if(vl > vr)
204                 return IS_INCREASING(f);
205         if(vl < vr)
206                 return IS_DECREASING(f);
207         return -1;
208 }
209
210 float Scoreboard_ComparePlayerScores(entity left, entity right)
211 {
212         float vl, vr, r;
213         vl = entcs_GetTeam(left.sv_entnum);
214         vr = entcs_GetTeam(right.sv_entnum);
215
216         if(!left.gotscores)
217                 vl = NUM_SPECTATOR;
218         if(!right.gotscores)
219                 vr = NUM_SPECTATOR;
220
221         if(vl > vr)
222                 return true;
223         if(vl < vr)
224                 return false;
225
226         if(vl == NUM_SPECTATOR)
227         {
228                 // FIRST the one with scores (spectators), THEN the ones without (downloaders)
229                 // no other sorting
230                 if(!left.gotscores && right.gotscores)
231                         return true;
232                 return false;
233         }
234
235         r = Scoreboard_CompareScore(left.scores(ps_primary), right.scores(ps_primary), scores_flags(ps_primary));
236         if (r >= 0)
237                 return r;
238
239         r = Scoreboard_CompareScore(left.scores(ps_secondary), right.scores(ps_secondary), scores_flags(ps_secondary));
240         if (r >= 0)
241                 return r;
242
243         FOREACH(Scores, true, {
244                 r = Scoreboard_CompareScore(left.scores(it), right.scores(it), scores_flags(it));
245                 if (r >= 0) return r;
246         });
247
248         if (left.sv_entnum < right.sv_entnum)
249                 return true;
250
251         return false;
252 }
253
254 void Scoreboard_UpdatePlayerPos(entity player)
255 {
256         entity ent;
257         for(ent = player.sort_next; ent && Scoreboard_ComparePlayerScores(player, ent); ent = player.sort_next)
258         {
259                 SORT_SWAP(player, ent);
260         }
261         for(ent = player.sort_prev; ent != players && Scoreboard_ComparePlayerScores(ent, player); ent = player.sort_prev)
262         {
263                 SORT_SWAP(ent, player);
264         }
265 }
266
267 float Scoreboard_CompareTeamScores(entity left, entity right)
268 {
269         int i, r;
270
271         if(left.team == NUM_SPECTATOR)
272                 return 1;
273         if(right.team == NUM_SPECTATOR)
274                 return 0;
275
276         r = Scoreboard_CompareScore(left.teamscores(ts_primary), right.teamscores(ts_primary), teamscores_flags(ts_primary));
277         if (r >= 0)
278                 return r;
279
280         r = Scoreboard_CompareScore(left.teamscores(ts_secondary), right.teamscores(ts_secondary), teamscores_flags(ts_secondary));
281         if (r >= 0)
282                 return r;
283
284         for(i = 0; i < MAX_TEAMSCORE; ++i)
285         {
286                 r = Scoreboard_CompareScore(left.teamscores(i), right.teamscores(i), teamscores_flags(i));
287                 if (r >= 0)
288                         return r;
289         }
290
291         if (left.team < right.team)
292                 return true;
293
294         return false;
295 }
296
297 void Scoreboard_UpdateTeamPos(entity Team)
298 {
299         entity ent;
300         for(ent = Team.sort_next; ent && Scoreboard_CompareTeamScores(Team, ent); ent = Team.sort_next)
301         {
302                 SORT_SWAP(Team, ent);
303         }
304         for(ent = Team.sort_prev; ent != teams && Scoreboard_CompareTeamScores(ent, Team); ent = Team.sort_prev)
305         {
306                 SORT_SWAP(ent, Team);
307         }
308 }
309
310 void Cmd_Scoreboard_Help()
311 {
312         LOG_INFO(_("You can modify the scoreboard using the ^2scoreboard_columns_set command."));
313         LOG_INFO(_("Usage:"));
314         LOG_INFO("^2scoreboard_columns_set ^3default");
315         LOG_INFO(_("^2scoreboard_columns_set ^3field1 field2 ..."));
316         LOG_INFO(_("^2scoreboard_columns_set ^7without arguments reads the arguments from the cvar scoreboard_columns"));
317         LOG_INFO(_("  ^5Note: ^7scoreboard_columns_set without arguments is executed on every map start"));
318         LOG_INFO(_("^2scoreboard_columns_set ^3expand_default ^7loads default layout and expands it into the cvar scoreboard_columns so you can edit it"));
319         LOG_INFO(_("You can use a ^3|^7 to start the right-aligned fields."));
320         LOG_INFO(_("The following field names are recognized (case insensitive):"));
321         LOG_INFO("");
322
323         PrintScoresLabels();
324         LOG_INFO("");
325
326         LOG_INFO(_("Before a field you can put a + or - sign, then a comma separated list\n"
327                 "of game types, then a slash, to make the field show up only in these\n"
328                 "or in all but these game types. You can also specify 'all' as a\n"
329                 "field to show all fields available for the current game mode."));
330         LOG_INFO("");
331
332         LOG_INFO(_("The special game type names 'teams' and 'noteams' can be used to\n"
333                 "include/exclude ALL teams/noteams game modes."));
334         LOG_INFO("");
335
336         LOG_INFO(_("Example: scoreboard_columns_set name ping pl | +ctf/field3 -dm/field4"));
337         LOG_INFO(_("will display name, ping and pl aligned to the left, and the fields\n"
338                 "right of the vertical bar aligned to the right."));
339         LOG_INFO(_("'field3' will only be shown in CTF, and 'field4' will be shown in all\n"
340                         "other gamemodes except DM."));
341 }
342
343 // NOTE: adding a gametype with ? to not warn for an optional field
344 // make sure it's excluded in a previous exclusive rule, if any
345 // otherwise the previous exclusive rule warns anyway
346 // e.g. -teams,rc,cts,lms/kills ?+rc/kills
347 #define SCOREBOARD_DEFAULT_COLUMNS \
348 "ping pl fps name |" \
349 " -teams,rc,cts,inv,lms/kills +ft,tdm/kills ?+rc,inv/kills" \
350 " -teams,lms/deaths +ft,tdm/deaths" \
351 " +tdm/sum" \
352 " -teams,lms,rc,cts,inv,ka/suicides +ft,tdm/suicides ?+rc,inv/suicides" \
353 " -cts,dm,tdm,ka,ft/frags" /* tdm already has this in "score" */ \
354 " +tdm,ft,dom,ons,as/teamkills"\
355 " -rc,cts,nb/dmg -rc,cts,nb/dmgtaken" \
356 " +ctf/pickups +ctf/fckills +ctf/returns +ctf/caps +ons/takes +ons/caps" \
357 " +lms/lives +lms/rank" \
358 " +kh/kckills +kh/losses +kh/caps" \
359 " ?+rc/laps ?+rc/time +rc,cts/fastest" \
360 " +as/objectives +nb/faults +nb/goals" \
361 " +ka/pickups +ka/bckills +ka/bctime +ft/revivals" \
362 " +dom/ticks +dom/takes" \
363 " -lms,rc,cts,inv,nb/score"
364
365 void Cmd_Scoreboard_SetFields(int argc)
366 {
367         TC(int, argc);
368         int i, slash;
369         string str, pattern;
370         bool have_name = false, have_primary = false, have_secondary = false, have_separator = false;
371         int missing;
372
373         if(!gametype)
374                 return; // do nothing, we don't know gametype and scores yet
375
376         // sbt_fields uses strunzone on the titles!
377         if(!sbt_field_title[0])
378                 for(i = 0; i < MAX_SBT_FIELDS; ++i)
379                         sbt_field_title[i] = strzone("(null)");
380
381         // TODO: re enable with gametype dependant cvars?
382         if(argc < 3) // no arguments provided
383                 argc = tokenizebyseparator(strcat("0 1 ", autocvar_scoreboard_columns), " ");
384
385         if(argc < 3)
386                 argc = tokenizebyseparator(strcat("0 1 ", SCOREBOARD_DEFAULT_COLUMNS), " ");
387
388         if(argc == 3)
389         {
390                 if(argv(2) == "default" || argv(2) == "expand_default")
391                 {
392                         if(argv(2) == "expand_default")
393                                 cvar_set("scoreboard_columns", SCOREBOARD_DEFAULT_COLUMNS);
394                         argc = tokenizebyseparator(strcat("0 1 ", SCOREBOARD_DEFAULT_COLUMNS), " ");
395                 }
396                 else if(argv(2) == "all")
397                 {
398                         string s = "ping pl name |"; // scores without a label
399                         FOREACH(Scores, true, {
400                                 if(it != ps_primary)
401                                 if(it != ps_secondary)
402                                 if(scores_label(it) != "")
403                                         s = strcat(s, " ", scores_label(it));
404                         });
405                         if(ps_secondary != ps_primary)
406                                 s = strcat(s, " ", scores_label(ps_secondary));
407                         s = strcat(s, " ", scores_label(ps_primary));
408                         argc = tokenizebyseparator(strcat("0 1 ", s), " ");
409                 }
410         }
411
412
413         sbt_num_fields = 0;
414
415         hud_fontsize = HUD_GetFontsize("hud_fontsize");
416
417         for(i = 1; i < argc - 1; ++i)
418         {
419                 str = argv(i+1);
420                 bool nocomplain = false;
421                 if(substring(str, 0, 1) == "?")
422                 {
423                         nocomplain = true;
424                         str = substring(str, 1, strlen(str) - 1);
425                 }
426
427                 slash = strstrofs(str, "/", 0);
428                 if(slash >= 0)
429                 {
430                         pattern = substring(str, 0, slash);
431                         str = substring(str, slash + 1, strlen(str) - (slash + 1));
432
433                         if (!isGametypeInFilter(gametype, teamplay, false, pattern))
434                                 continue;
435                 }
436
437                 strcpy(sbt_field_title[sbt_num_fields], TranslateScoresLabel(str));
438                 sbt_field_size[sbt_num_fields] = stringwidth(sbt_field_title[sbt_num_fields], false, hud_fontsize);
439                 str = strtolower(str);
440
441                 PlayerScoreField j;
442                 switch(str)
443                 {
444                         case "ping": sbt_field[sbt_num_fields] = SP_PING; break;
445                         case "pl": sbt_field[sbt_num_fields] = SP_PL; break;
446                         case "kd": case "kdr": case "kdratio": sbt_field[sbt_num_fields] = SP_KDRATIO; break;
447                         case "sum": case "diff": case "k-d": sbt_field[sbt_num_fields] = SP_SUM; break;
448                         case "name": case "nick": sbt_field[sbt_num_fields] = SP_NAME; have_name = true; break;
449                         case "|": sbt_field[sbt_num_fields] = SP_SEPARATOR; have_separator = true; break;
450                         case "elo": sbt_field[sbt_num_fields] = SP_ELO; break;
451                         case "dmg": case "damage": sbt_field[sbt_num_fields] = SP_DMG; break;
452                         case "dmgtaken": case "damagetaken": sbt_field[sbt_num_fields] = SP_DMGTAKEN; break;
453                         case "fps": sbt_field[sbt_num_fields] = SP_FPS; break;
454                         default:
455                         {
456                                 FOREACH(Scores, true, {
457                                         if (str == strtolower(scores_label(it))) {
458                                                 j = it;
459                                                 goto found; // sorry, but otherwise fteqcc -O3 miscompiles this and warns about "unreachable code"
460                                         }
461                                 });
462
463 LABEL(notfound)
464                                 if(str == "frags")
465                                         j = SP_FRAGS;
466                                 else
467                                 {
468                                         if(!nocomplain)
469                                                 LOG_INFOF("^1Error:^7 Unknown score field: '%s'", str);
470                                         continue;
471                                 }
472 LABEL(found)
473                                 sbt_field[sbt_num_fields] = j;
474                                 if(j == ps_primary)
475                                         have_primary = true;
476                                 if(j == ps_secondary)
477                                         have_secondary = true;
478
479                         }
480                 }
481                 ++sbt_num_fields;
482                 if(sbt_num_fields >= MAX_SBT_FIELDS)
483                         break;
484         }
485
486         if(scores_flags(ps_primary) & SFL_ALLOW_HIDE)
487                 have_primary = true;
488         if(scores_flags(ps_secondary) & SFL_ALLOW_HIDE)
489                 have_secondary = true;
490         if(ps_primary == ps_secondary)
491                 have_secondary = true;
492         missing = (!have_primary) + (!have_secondary) + (!have_separator) + (!have_name);
493
494         if(sbt_num_fields + missing < MAX_SBT_FIELDS)
495         {
496                 if(!have_name)
497                 {
498                         strunzone(sbt_field_title[sbt_num_fields]);
499                         for(i = sbt_num_fields; i > 0; --i)
500                         {
501                                 sbt_field_title[i] = sbt_field_title[i-1];
502                                 sbt_field_size[i] = sbt_field_size[i-1];
503                                 sbt_field[i] = sbt_field[i-1];
504                         }
505                         sbt_field_title[0] = strzone(TranslateScoresLabel("name"));
506                         sbt_field[0] = SP_NAME;
507                         ++sbt_num_fields;
508                         LOG_INFO("fixed missing field 'name'");
509
510                         if(!have_separator)
511                         {
512                                 strunzone(sbt_field_title[sbt_num_fields]);
513                                 for(i = sbt_num_fields; i > 1; --i)
514                                 {
515                                         sbt_field_title[i] = sbt_field_title[i-1];
516                                         sbt_field_size[i] = sbt_field_size[i-1];
517                                         sbt_field[i] = sbt_field[i-1];
518                                 }
519                                 sbt_field_title[1] = strzone("|");
520                                 sbt_field[1] = SP_SEPARATOR;
521                                 sbt_field_size[1] = stringwidth("|", false, hud_fontsize);
522                                 ++sbt_num_fields;
523                                 LOG_INFO("fixed missing field '|'");
524                         }
525                 }
526                 else if(!have_separator)
527                 {
528                         strcpy(sbt_field_title[sbt_num_fields], "|");
529                         sbt_field_size[sbt_num_fields] = stringwidth("|", false, hud_fontsize);
530                         sbt_field[sbt_num_fields] = SP_SEPARATOR;
531                         ++sbt_num_fields;
532                         LOG_INFO("fixed missing field '|'");
533                 }
534                 if(!have_secondary)
535                 {
536                         strcpy(sbt_field_title[sbt_num_fields], TranslateScoresLabel(scores_label(ps_secondary)));
537                         sbt_field_size[sbt_num_fields] = stringwidth(sbt_field_title[sbt_num_fields], false, hud_fontsize);
538                         sbt_field[sbt_num_fields] = ps_secondary;
539                         ++sbt_num_fields;
540                         LOG_INFOF("fixed missing field '%s'", scores_label(ps_secondary));
541                 }
542                 if(!have_primary)
543                 {
544                         strcpy(sbt_field_title[sbt_num_fields], TranslateScoresLabel(scores_label(ps_primary)));
545                         sbt_field_size[sbt_num_fields] = stringwidth(sbt_field_title[sbt_num_fields], false, hud_fontsize);
546                         sbt_field[sbt_num_fields] = ps_primary;
547                         ++sbt_num_fields;
548                         LOG_INFOF("fixed missing field '%s'", scores_label(ps_primary));
549                 }
550         }
551
552         sbt_field[sbt_num_fields] = SP_END;
553 }
554
555 // MOVEUP::
556 vector sbt_field_rgb;
557 string sbt_field_icon0;
558 string sbt_field_icon1;
559 string sbt_field_icon2;
560 vector sbt_field_icon0_rgb;
561 vector sbt_field_icon1_rgb;
562 vector sbt_field_icon2_rgb;
563 string Scoreboard_GetName(entity pl)
564 {
565         if(ready_waiting && pl.ready)
566         {
567                 sbt_field_icon0 = "gfx/scoreboard/player_ready";
568         }
569         else if(!teamplay)
570         {
571                 int f = entcs_GetClientColors(pl.sv_entnum);
572                 {
573                         sbt_field_icon0 = "gfx/scoreboard/playercolor_base";
574                         sbt_field_icon1 = "gfx/scoreboard/playercolor_shirt";
575                         sbt_field_icon1_rgb = colormapPaletteColor(floor(f / 16), 0);
576                         sbt_field_icon2 = "gfx/scoreboard/playercolor_pants";
577                         sbt_field_icon2_rgb = colormapPaletteColor(f % 16, 1);
578                 }
579         }
580         return entcs_GetName(pl.sv_entnum);
581 }
582
583 string Scoreboard_GetField(entity pl, PlayerScoreField field)
584 {
585         float tmp, num, denom;
586         int f;
587         string str;
588         sbt_field_rgb = '1 1 1';
589         sbt_field_icon0 = "";
590         sbt_field_icon1 = "";
591         sbt_field_icon2 = "";
592         sbt_field_icon0_rgb = '1 1 1';
593         sbt_field_icon1_rgb = '1 1 1';
594         sbt_field_icon2_rgb = '1 1 1';
595         switch(field)
596         {
597                 case SP_PING:
598                         if (!pl.gotscores)
599                                 return "\xE2\x96\xB6\xE2\x96\xB6\xE2\x96\xB6"; // >>> sign using U+25B6 (Black Right-Pointing Triangle)
600                         //str = getplayerkeyvalue(pl.sv_entnum, "ping");
601                         f = pl.ping;
602                         if(f == 0)
603                                 return _("N/A");
604                         tmp = max(0, min(220, f-80)) / 220;
605                         sbt_field_rgb = '1 1 1' - '0 1 1' * tmp;
606                         return ftos(f);
607
608                 case SP_PL:
609                         if (!pl.gotscores)
610                                 return _("N/A");
611                         f = pl.ping_packetloss;
612                         tmp = pl.ping_movementloss;
613                         if(f == 0 && tmp == 0)
614                                 return "";
615                         str = ftos(ceil(f * 100));
616                         if(tmp != 0)
617                                 str = strcat(str, "~", ftos(ceil(tmp * 100)));
618                         tmp = bound(0, f / 0.2 + tmp / 0.04, 1); // 20% is REALLY BAD pl
619                         sbt_field_rgb = '1 0.5 0.5' - '0 0.5 0.5' * tmp;
620                         return str;
621
622                 case SP_NAME:
623                         return Scoreboard_GetName(pl);
624
625                 case SP_FRAGS:
626                         f = pl.(scores(SP_KILLS));
627                         f -= pl.(scores(SP_SUICIDES));
628                         return ftos(f);
629
630                 case SP_KDRATIO:
631                         num = pl.(scores(SP_KILLS));
632                         denom = pl.(scores(SP_DEATHS));
633
634                         if(denom == 0) {
635                                 sbt_field_rgb = '0 1 0';
636                                 str = sprintf("%d", num);
637                         } else if(num <= 0) {
638                                 sbt_field_rgb = '1 0 0';
639                                 str = sprintf("%.1f", num/denom);
640                         } else
641                                 str = sprintf("%.1f", num/denom);
642                         return str;
643
644                 case SP_SUM:
645                         f = pl.(scores(SP_KILLS));
646                         f -= pl.(scores(SP_DEATHS));
647
648                         if(f > 0) {
649                                 sbt_field_rgb = '0 1 0';
650                         } else if(f == 0) {
651                                 sbt_field_rgb = '1 1 1';
652                         } else {
653                                 sbt_field_rgb = '1 0 0';
654                         }
655                         return ftos(f);
656
657                 case SP_ELO:
658                 {
659                         float elo = pl.(scores(SP_ELO));
660                         switch (elo) {
661                                 case -1: return "...";
662                                 case -2: return _("N/A");
663                                 default: return ftos(elo);
664                         }
665                 }
666
667                 case SP_FPS:
668                 {
669                         float fps = pl.(scores(SP_FPS));
670                         if(fps == 0)
671                         {
672                                 sbt_field_rgb = '1 1 1';
673                                 return ((pl.ping == 0) ? _("N/A") : "..."); // if 0 ping, either connecting or bot (either case can't show proper score)
674                         }
675                         //sbt_field_rgb = HUD_Get_Num_Color(fps, 200);
676                         sbt_field_rgb = '1 0 0' + '0 1 1' * (bound(0, fps, 60) / 60);
677                         return ftos(fps);
678                 }
679
680                 case SP_DMG: case SP_DMGTAKEN:
681                         return sprintf("%.1f k", pl.(scores(field)) / 1000);
682
683                 default: case SP_SCORE:
684                         tmp = pl.(scores(field));
685                         f = scores_flags(field);
686                         if(field == ps_primary)
687                                 sbt_field_rgb = '1 1 0';
688                         else if(field == ps_secondary)
689                                 sbt_field_rgb = '0 1 1';
690                         else
691                                 sbt_field_rgb = '1 1 1';
692                         return ScoreString(f, tmp);
693         }
694         //return "error";
695 }
696
697 float sbt_fixcolumnwidth_len;
698 float sbt_fixcolumnwidth_iconlen;
699 float sbt_fixcolumnwidth_marginlen;
700
701 string Scoreboard_FixColumnWidth(int i, string str)
702 {
703         TC(int, i);
704         float f;
705         vector sz;
706
707         sbt_fixcolumnwidth_iconlen = 0;
708
709         if(sbt_field_icon0 != "")
710         {
711                 sz = draw_getimagesize(sbt_field_icon0);
712                 f = sz.x / sz.y;
713                 if(sbt_fixcolumnwidth_iconlen < f)
714                         sbt_fixcolumnwidth_iconlen = f;
715         }
716
717         if(sbt_field_icon1 != "")
718         {
719                 sz = draw_getimagesize(sbt_field_icon1);
720                 f = sz.x / sz.y;
721                 if(sbt_fixcolumnwidth_iconlen < f)
722                         sbt_fixcolumnwidth_iconlen = f;
723         }
724
725         if(sbt_field_icon2 != "")
726         {
727                 sz = draw_getimagesize(sbt_field_icon2);
728                 f = sz.x / sz.y;
729                 if(sbt_fixcolumnwidth_iconlen < f)
730                         sbt_fixcolumnwidth_iconlen = f;
731         }
732
733         if(sbt_fixcolumnwidth_iconlen != 0)
734         {
735                 sbt_fixcolumnwidth_iconlen *= hud_fontsize.y / hud_fontsize.x; // fix icon aspect
736                 sbt_fixcolumnwidth_marginlen = stringwidth(" ", false, hud_fontsize);
737         }
738         else
739                 sbt_fixcolumnwidth_marginlen = 0;
740
741         if(sbt_field[i] == SP_NAME) // name gets all remaining space
742         {
743                 int j;
744                 float remaining_space = 0;
745                 for(j = 0; j < sbt_num_fields; ++j)
746                         if(j != i)
747                                 if (sbt_field[i] != SP_SEPARATOR)
748                                         remaining_space += sbt_field_size[j] + hud_fontsize.x;
749                 sbt_field_size[i] = panel_size.x - remaining_space;
750
751                 if (sbt_fixcolumnwidth_iconlen != 0)
752                         remaining_space += sbt_fixcolumnwidth_marginlen + sbt_fixcolumnwidth_iconlen * hud_fontsize.x;
753                 float namesize = panel_size.x - remaining_space;
754                 str = textShortenToWidth(str, namesize, hud_fontsize, stringwidth_colors);
755                 sbt_fixcolumnwidth_len = stringwidth(str, true, hud_fontsize);
756
757                 max_namesize = vid_conwidth - remaining_space;
758         }
759         else
760                 sbt_fixcolumnwidth_len = stringwidth(str, false, hud_fontsize);
761
762         f = sbt_fixcolumnwidth_len + sbt_fixcolumnwidth_marginlen + sbt_fixcolumnwidth_iconlen * hud_fontsize.x;
763         if(sbt_field_size[i] < f)
764                 sbt_field_size[i] = f;
765
766         return str;
767 }
768
769 void Scoreboard_initFieldSizes()
770 {
771         for(int i = 0; i < sbt_num_fields; ++i)
772         {
773                 sbt_field_size[i] = stringwidth(sbt_field_title[i], false, hud_fontsize);
774                 Scoreboard_FixColumnWidth(i, "");
775         }
776 }
777
778 vector Scoreboard_DrawHeader(vector pos, vector rgb, bool other_players)
779 {
780         int i;
781         vector column_dim = eY * panel_size.y;
782         if(other_players)
783                 column_dim.y -= 1.25 * hud_fontsize.y;
784         vector text_offset = eY * (1.25 - 1) / 2 * hud_fontsize.y;
785         pos.x += hud_fontsize.x * 0.5;
786         for(i = 0; i < sbt_num_fields; ++i)
787         {
788                 if(sbt_field[i] == SP_SEPARATOR)
789                         break;
790                 column_dim.x = sbt_field_size[i] + hud_fontsize.x;
791                 if (sbt_highlight)
792                         if (i % 2)
793                                 drawfill(pos - eX * hud_fontsize.x * 0.5, column_dim, '0 0 0', sbt_highlight_alpha, DRAWFLAG_NORMAL);
794                 drawstring(pos + text_offset, sbt_field_title[i], hud_fontsize, rgb * 1.5, sbt_fg_alpha, DRAWFLAG_NORMAL);
795                 pos.x += column_dim.x;
796         }
797         if(sbt_field[i] == SP_SEPARATOR)
798         {
799                 pos.x = panel_pos.x + panel_size.x - hud_fontsize.x * 0.5;
800                 for(i = sbt_num_fields - 1; i > 0; --i)
801                 {
802                         if(sbt_field[i] == SP_SEPARATOR)
803                                 break;
804
805                         pos.x -= sbt_field_size[i];
806
807                         if (sbt_highlight)
808                                 if (!(i % 2))
809                                 {
810                                         column_dim.x = sbt_field_size[i] + hud_fontsize.x;
811                                         drawfill(pos - eX * hud_fontsize.x * 0.5, column_dim, '0 0 0', sbt_highlight_alpha, DRAWFLAG_NORMAL);
812                                 }
813
814                         text_offset.x = sbt_field_size[i] - stringwidth(sbt_field_title[i], false, hud_fontsize);
815                         drawstring(pos + text_offset, sbt_field_title[i], hud_fontsize, rgb * 1.5, sbt_fg_alpha, DRAWFLAG_NORMAL);
816                         pos.x -= hud_fontsize.x;
817                 }
818         }
819
820         pos.x = panel_pos.x;
821         pos.y += 1.25 * hud_fontsize.y;
822         return pos;
823 }
824
825 void Scoreboard_DrawItem(vector item_pos, vector rgb, entity pl, bool is_self, int pl_number)
826 {
827         TC(bool, is_self); TC(int, pl_number);
828         string str;
829         bool is_spec = (entcs_GetSpecState(pl.sv_entnum) == ENTCS_SPEC_PURE);
830
831         vector h_pos = item_pos;
832         vector h_size = vec2(panel_size.x, hud_fontsize.y * 1.25);
833         // alternated rows highlighting
834         if(is_self)
835                 drawfill(h_pos, h_size, rgb, sbt_highlight_alpha_self, DRAWFLAG_NORMAL);
836         else if((sbt_highlight) && (!(pl_number % 2)))
837                 drawfill(h_pos, h_size, rgb, sbt_highlight_alpha, DRAWFLAG_NORMAL);
838
839         float fg_alpha = (is_self ? sbt_fg_alpha_self : sbt_fg_alpha);
840
841         vector pos = item_pos;
842         // put a "self indicator" beside the self row, unicode U+25C0 (black left-pointing triangle)
843         if (is_self)
844                 drawstring(pos+eX*(panel_size.x+.5*hud_fontsize.x)+eY, "\xE2\x97\x80", vec2(hud_fontsize.x, hud_fontsize.y), rgb, panel_fg_alpha, DRAWFLAG_NORMAL);
845
846         pos.x += hud_fontsize.x * 0.5;
847         pos.y += (1.25 - 1) / 2 * hud_fontsize.y; // center text vertically
848         vector tmp = '0 0 0';
849         int i;
850         PlayerScoreField field;
851         for(i = 0; i < sbt_num_fields; ++i)
852         {
853                 field = sbt_field[i];
854                 if(field == SP_SEPARATOR)
855                         break;
856
857                 if(is_spec && field != SP_NAME && field != SP_PING) {
858                         pos.x += sbt_field_size[i] + hud_fontsize.x;
859                         continue;
860                 }
861                 str = Scoreboard_GetField(pl, field);
862                 str = Scoreboard_FixColumnWidth(i, str);
863
864                 pos.x += sbt_field_size[i] + hud_fontsize.x;
865
866                 if(field == SP_NAME) {
867                         tmp.x = sbt_field_size[i] - hud_fontsize.x * sbt_fixcolumnwidth_iconlen - sbt_fixcolumnwidth_marginlen + hud_fontsize.x;
868                         drawcolorcodedstring(pos - tmp, str, hud_fontsize, fg_alpha, DRAWFLAG_NORMAL);
869                 } else {
870                         tmp.x = sbt_fixcolumnwidth_len + hud_fontsize.x;
871                         drawstring(pos - tmp, str, hud_fontsize, sbt_field_rgb, fg_alpha, DRAWFLAG_NORMAL);
872                 }
873
874                 tmp.x = sbt_field_size[i] + hud_fontsize.x;
875                 if(sbt_field_icon0 != "")
876                         drawpic(pos - tmp, sbt_field_icon0, vec2(hud_fontsize.x * sbt_fixcolumnwidth_iconlen, hud_fontsize.y), sbt_field_icon1_rgb, fg_alpha, DRAWFLAG_NORMAL);
877                 if(sbt_field_icon1 != "")
878                         drawpic(pos - tmp, sbt_field_icon1, vec2(hud_fontsize.x * sbt_fixcolumnwidth_iconlen, hud_fontsize.y), sbt_field_icon1_rgb, fg_alpha, DRAWFLAG_NORMAL);
879                 if(sbt_field_icon2 != "")
880                         drawpic(pos - tmp, sbt_field_icon2, vec2(hud_fontsize.x * sbt_fixcolumnwidth_iconlen, hud_fontsize.y), sbt_field_icon2_rgb, fg_alpha, DRAWFLAG_NORMAL);
881         }
882
883         if(sbt_field[i] == SP_SEPARATOR)
884         {
885                 pos.x = item_pos.x + panel_size.x - hud_fontsize.x * 0.5;
886                 for(i = sbt_num_fields-1; i > 0; --i)
887                 {
888                         field = sbt_field[i];
889                         if(field == SP_SEPARATOR)
890                                 break;
891
892                         if(is_spec && field != SP_NAME && field != SP_PING) {
893                                 pos.x -= sbt_field_size[i] + hud_fontsize.x;
894                                 continue;
895                         }
896
897                         str = Scoreboard_GetField(pl, field);
898                         str = Scoreboard_FixColumnWidth(i, str);
899
900                         if(field == SP_NAME) {
901                                 tmp.x = sbt_fixcolumnwidth_len; // left or right aligned? let's put it right...
902                                 drawcolorcodedstring(pos - tmp, str, hud_fontsize, fg_alpha, DRAWFLAG_NORMAL);
903                         } else {
904                                 tmp.x = sbt_fixcolumnwidth_len;
905                                 drawstring(pos - tmp, str, hud_fontsize, sbt_field_rgb, fg_alpha, DRAWFLAG_NORMAL);
906                         }
907
908                         tmp.x = sbt_field_size[i];
909                         if(sbt_field_icon0 != "")
910                                 drawpic(pos - tmp, sbt_field_icon0, vec2(hud_fontsize.x * sbt_fixcolumnwidth_iconlen, hud_fontsize.y), sbt_field_icon1_rgb, fg_alpha, DRAWFLAG_NORMAL);
911                         if(sbt_field_icon1 != "")
912                                 drawpic(pos - tmp, sbt_field_icon1, vec2(hud_fontsize.x * sbt_fixcolumnwidth_iconlen, hud_fontsize.y), sbt_field_icon1_rgb, fg_alpha, DRAWFLAG_NORMAL);
913                         if(sbt_field_icon2 != "")
914                                 drawpic(pos - tmp, sbt_field_icon2, vec2(hud_fontsize.x * sbt_fixcolumnwidth_iconlen, hud_fontsize.y), sbt_field_icon2_rgb, fg_alpha, DRAWFLAG_NORMAL);
915                         pos.x -= sbt_field_size[i] + hud_fontsize.x;
916                 }
917         }
918
919         if(pl.eliminated)
920                 drawfill(h_pos, h_size, '0 0 0', 0.5 * panel_fg_alpha, DRAWFLAG_NORMAL);
921 }
922
923 vector Scoreboard_DrawOthers(vector item_pos, vector rgb, int this_team, entity ignored_pl, entity pl, int pl_number)
924 {
925         int i = 0;
926         vector h_pos = item_pos;
927         vector h_size = vec2(panel_size.x, hud_fontsize.y * 1.25);
928
929         bool complete = (this_team == NUM_SPECTATOR);
930
931         if(!complete)
932         if((sbt_highlight) && (!(pl_number % 2)))
933                 drawfill(h_pos, h_size, rgb, sbt_highlight_alpha, DRAWFLAG_NORMAL);
934
935         vector pos = item_pos;
936         pos.x += hud_fontsize.x * 0.5;
937         pos.y += (1.25 - 1) / 2 * hud_fontsize.y; // center text vertically
938
939         float width_limit = item_pos.x + panel_size.x - hud_fontsize.x;
940         if(!complete)
941                 width_limit -= stringwidth("...", false, hud_fontsize);
942         float namesize = autocvar_hud_panel_scoreboard_namesize * hud_fontsize.x;
943         static float max_name_width = 0;
944         string field = "";
945         float fieldsize = 0;
946         float min_fieldsize = 0;
947         float fieldpadding = hud_fontsize.x * 0.25;
948         if(this_team == NUM_SPECTATOR)
949         {
950                 if(autocvar_hud_panel_scoreboard_spectators_showping)
951                         min_fieldsize = stringwidth("999", false, hud_fontsize);
952         }
953         else if(autocvar_hud_panel_scoreboard_others_showscore)
954                 min_fieldsize = stringwidth("99", false, hud_fontsize);
955         for(i = 0; pl; pl = pl.sort_next)
956         {
957                 if(pl.team != this_team)
958                         continue;
959                 if(pl == ignored_pl)
960                         continue;
961
962                 field = "";
963                 if(this_team == NUM_SPECTATOR)
964                 {
965                         if(autocvar_hud_panel_scoreboard_spectators_showping)
966                                 field = Scoreboard_GetField(pl, SP_PING);
967                 }
968                 else if(autocvar_hud_panel_scoreboard_others_showscore)
969                         field = Scoreboard_GetField(pl, SP_SCORE);
970
971                 string str = textShortenToWidth(entcs_GetName(pl.sv_entnum), namesize, hud_fontsize, stringwidth_colors);
972                 float column_width = stringwidth(str, true, hud_fontsize);
973                 if((this_team == NUM_SPECTATOR) && autocvar_hud_panel_scoreboard_spectators_aligned)
974                 {
975                         if(column_width > max_name_width)
976                                 max_name_width = column_width;
977                         column_width = max_name_width;
978                 }
979                 if(field != "")
980                 {
981                         fieldsize = stringwidth(field, false, hud_fontsize);
982                         column_width += hud_fontsize.x * 0.25 + max(fieldsize, min_fieldsize) + 2 * fieldpadding;
983                 }
984
985                 if(pos.x + column_width > width_limit)
986                 {
987                         ++i;
988                         if(!complete)
989                         {
990                                 drawstring(pos, "...", hud_fontsize, '1 1 1', sbt_fg_alpha, DRAWFLAG_NORMAL);
991                                 break;
992                         }
993                         else
994                         {
995                                 pos.x = item_pos.x + hud_fontsize.x * 0.5;
996                                 pos.y += hud_fontsize.y * 1.25;
997                         }
998                 }
999
1000                 vector name_pos = pos;
1001                 if((this_team == NUM_SPECTATOR) && autocvar_hud_panel_scoreboard_spectators_aligned)
1002                         name_pos.x += max(fieldsize, min_fieldsize) + 2 * fieldpadding + hud_fontsize.x * 0.25;
1003                 drawcolorcodedstring(name_pos, str, hud_fontsize, sbt_fg_alpha, DRAWFLAG_NORMAL);
1004                 if(field != "")
1005                 {
1006                         h_size.x = max(fieldsize, min_fieldsize) + 2 * fieldpadding;
1007                         h_size.y = hud_fontsize.y;
1008                         vector field_pos = pos;
1009                         if(!((this_team == NUM_SPECTATOR) && autocvar_hud_panel_scoreboard_spectators_aligned))
1010                                 field_pos.x += column_width - h_size.x;
1011                         if(sbt_highlight)
1012                                 drawfill(field_pos, h_size, '1 1 1', sbt_highlight_alpha, DRAWFLAG_NORMAL);
1013                         field_pos.x += fieldpadding + (max(fieldsize, min_fieldsize) - fieldsize) * 0.5;
1014                         drawstring(field_pos, field, hud_fontsize, sbt_field_rgb, sbt_fg_alpha, DRAWFLAG_NORMAL);
1015                 }
1016                 if(pl.eliminated)
1017                 {
1018                         h_size.x = column_width + hud_fontsize.x * 0.25;
1019                         h_size.y = hud_fontsize.y;
1020                         drawfill(pos - hud_fontsize.x * 0.25 * eX, h_size, '0 0 0', 0.5 * panel_fg_alpha, DRAWFLAG_NORMAL);
1021                 }
1022                 pos.x += column_width;
1023                 pos.x += hud_fontsize.x;
1024         }
1025         return vec2(item_pos.x, item_pos.y + i * hud_fontsize.y * 1.25);
1026 }
1027
1028 vector Scoreboard_MakeTable(vector pos, entity tm, vector rgb, vector bg_size)
1029 {
1030         int max_players = 999;
1031         if(autocvar_hud_panel_scoreboard_maxheight > 0)
1032         {
1033                 float height = autocvar_hud_panel_scoreboard_maxheight * vid_conheight;
1034                 if(teamplay)
1035                 {
1036                         height -= (panel_bg_padding * 2 + hud_fontsize.y * 1.25) * team_count; // - padding and header
1037                         height -= hud_fontsize.y * (team_count - 1); // - spacing between tables
1038                         height /= team_count;
1039                 }
1040                 else
1041                         height -= panel_bg_padding * 2; // - padding
1042                 max_players = floor(height / (hud_fontsize.y * 1.25));
1043                 if(max_players <= 1)
1044                         max_players = 1;
1045                 if(max_players == tm.team_size)
1046                         max_players = 999;
1047         }
1048
1049         entity pl;
1050         entity me = playerslots[current_player];
1051         panel_pos = pos;
1052         panel_size.y = 1.25 * hud_fontsize.y * (1 + bound(1, tm.team_size, max_players));
1053         panel_size.y += panel_bg_padding * 2;
1054         HUD_Panel_DrawBg();
1055
1056         vector end_pos = panel_pos + eY * (panel_size.y + 0.5* hud_fontsize.y);
1057         if(panel.current_panel_bg != "0")
1058                 end_pos.y += panel_bg_border * 2;
1059
1060         if(panel_bg_padding)
1061         {
1062                 panel_pos += '1 1 0' * panel_bg_padding;
1063                 panel_size -= '2 2 0' * panel_bg_padding;
1064         }
1065
1066         pos = panel_pos;
1067         vector tmp = vec2(panel_size.x, 1.25 * hud_fontsize.y);
1068
1069         // rounded header
1070         if (sbt_bg_alpha)
1071                 drawpic(pos, "gfx/scoreboard/scoreboard_tableheader", tmp, rgb + '0.5 0.5 0.5', sbt_bg_alpha, DRAWFLAG_NORMAL);
1072
1073         pos.y += 1.25 * hud_fontsize.y;
1074
1075         // table background
1076         tmp.y = panel_size.y - 1.25 * hud_fontsize.y;
1077         if (sbt_bg_alpha)
1078                 drawpic_tiled(pos, "gfx/scoreboard/scoreboard_bg", bg_size, tmp, rgb, sbt_bg_alpha, DRAWFLAG_NORMAL);
1079
1080
1081         // print header row and highlight columns
1082         pos = Scoreboard_DrawHeader(panel_pos, rgb, (max_players < tm.team_size));
1083
1084         // fill the table and draw the rows
1085         bool is_self = false;
1086         bool self_shown = false;
1087         int i = 0;
1088         for(pl = players.sort_next; pl; pl = pl.sort_next)
1089         {
1090                 if(pl.team != tm.team)
1091                         continue;
1092                 if(i == max_players - 2 && pl != me)
1093                 {
1094                         if(!self_shown && me.team == tm.team)
1095                         {
1096                                 Scoreboard_DrawItem(pos, rgb, me, true, i);
1097                                 self_shown = true;
1098                                 pos.y += 1.25 * hud_fontsize.y;
1099                                 ++i;
1100                         }
1101                 }
1102                 if(i >= max_players - 1)
1103                 {
1104                         pos = Scoreboard_DrawOthers(pos, rgb, tm.team, (self_shown ? me : NULL), pl, i);
1105                         break;
1106                 }
1107                 is_self = (pl.sv_entnum == current_player);
1108                 Scoreboard_DrawItem(pos, rgb, pl, is_self, i);
1109                 if(is_self)
1110                         self_shown = true;
1111                 pos.y += 1.25 * hud_fontsize.y;
1112                 ++i;
1113         }
1114
1115         panel_size.x += panel_bg_padding * 2; // restore initial width
1116         return end_pos;
1117 }
1118
1119 bool Scoreboard_WouldDraw()
1120 {
1121         if (MUTATOR_CALLHOOK(DrawScoreboard))
1122                 return false;
1123         else if (QuickMenu_IsOpened())
1124                 return false;
1125         else if (HUD_Radar_Clickable())
1126                 return false;
1127         else if (scoreboard_showscores)
1128                 return true;
1129         else if (intermission == 1)
1130                 return true;
1131         else if (intermission == 2)
1132                 return false;
1133         else if (spectatee_status != -1 && STAT(HEALTH) <= 0 && autocvar_cl_deathscoreboard && !ISGAMETYPE(CTS)
1134                 && (!HUD_MinigameMenu_IsOpened() || !active_minigame))
1135         {
1136                 return true;
1137         }
1138         else if (scoreboard_showscores_force)
1139                 return true;
1140         return false;
1141 }
1142
1143 float average_accuracy;
1144 vector Scoreboard_AccuracyStats_Draw(vector pos, vector rgb, vector bg_size)
1145 {
1146         if (frametime)
1147         {
1148                 if (scoreboard_fade_alpha == 1)
1149                         scoreboard_acc_fade_alpha = min(1, scoreboard_acc_fade_alpha + frametime * 10);
1150                 else
1151                         scoreboard_acc_fade_alpha = 1; // sync fading with the scoreboard
1152         }
1153         vector initial_pos = pos;
1154
1155         WepSet weapons_stat = WepSet_GetFromStat();
1156         WepSet weapons_inmap = WepSet_GetFromStat_InMap();
1157         int disownedcnt = 0;
1158         int nHidden = 0;
1159         FOREACH(Weapons, it != WEP_Null, {
1160                 int weapon_stats = weapon_accuracy[i - WEP_FIRST];
1161
1162                 WepSet set = it.m_wepset;
1163                 if(it.spawnflags & WEP_TYPE_OTHER)
1164                 {
1165                         ++nHidden;
1166                         continue;
1167                 }
1168                 if (weapon_stats < 0 && !((weapons_stat & set) || (weapons_inmap & set)))
1169                 {
1170                         if (it.spawnflags & (WEP_FLAG_HIDDEN | WEP_FLAG_MUTATORBLOCKED | WEP_FLAG_SPECIALATTACK))
1171                                 ++nHidden;
1172                         else
1173                                 ++disownedcnt;
1174                 }
1175         });
1176
1177         int weapon_cnt = (Weapons_COUNT - 1) - disownedcnt - nHidden;
1178         if (weapon_cnt <= 0) return pos;
1179
1180         int rows = 1;
1181         if (autocvar_hud_panel_scoreboard_accuracy_doublerows && weapon_cnt >= floor((Weapons_COUNT - nHidden - 1) * 0.5))
1182                 rows = 2;
1183         int columnns = ceil(weapon_cnt / rows);
1184
1185         float weapon_height = 29;
1186         float height = hud_fontsize.y + weapon_height;
1187
1188         drawstring(pos + eX * panel_bg_padding, sprintf(_("Accuracy stats (average %d%%)"), average_accuracy), hud_fontsize, '1 1 1', panel_fg_alpha * scoreboard_acc_fade_alpha, DRAWFLAG_NORMAL);
1189         pos.y += 1.25 * hud_fontsize.y;
1190         if(panel.current_panel_bg != "0")
1191                 pos.y += panel_bg_border;
1192
1193         panel_pos = pos;
1194         panel_size.y = height * rows;
1195         panel_size.y += panel_bg_padding * 2;
1196
1197         float panel_bg_alpha_save = panel_bg_alpha;
1198         panel_bg_alpha *= scoreboard_acc_fade_alpha;
1199         HUD_Panel_DrawBg();
1200         panel_bg_alpha = panel_bg_alpha_save;
1201
1202         vector end_pos = panel_pos + eY * (panel_size.y + hud_fontsize.y);
1203         if(panel.current_panel_bg != "0")
1204                 end_pos.y += panel_bg_border * 2;
1205
1206         if(panel_bg_padding)
1207         {
1208                 panel_pos += '1 1 0' * panel_bg_padding;
1209                 panel_size -= '2 2 0' * panel_bg_padding;
1210         }
1211
1212         pos = panel_pos;
1213         vector tmp = panel_size;
1214
1215         float weapon_width = tmp.x / columnns / rows;
1216
1217         if (sbt_bg_alpha)
1218                 drawpic_tiled(pos, "gfx/scoreboard/scoreboard_bg", bg_size, tmp, rgb, sbt_bg_alpha * scoreboard_acc_fade_alpha, DRAWFLAG_NORMAL);
1219
1220         if(sbt_highlight)
1221         {
1222                 // column highlighting
1223                 for (int i = 0; i < columnns; ++i)
1224                         if ((i % 2) == 0)
1225                                 drawfill(pos + eX * weapon_width * rows * i, vec2(weapon_width * rows, height * rows), '0 0 0', sbt_highlight_alpha * scoreboard_acc_fade_alpha, DRAWFLAG_NORMAL);
1226
1227                 // row highlighting
1228                 for (int i = 0; i < rows; ++i)
1229                         drawfill(pos + eY * (weapon_height + height * i), vec2(tmp.x, hud_fontsize.y), rgb, sbt_highlight_alpha * scoreboard_acc_fade_alpha, DRAWFLAG_NORMAL);
1230         }
1231
1232         average_accuracy = 0;
1233         int weapons_with_stats = 0;
1234         if (rows == 2)
1235                 pos.x += weapon_width / 2;
1236
1237         if (autocvar_hud_panel_scoreboard_accuracy_nocolors)
1238                 rgb = '1 1 1';
1239         else
1240                 Accuracy_LoadColors();
1241
1242         float oldposx = pos.x;
1243         vector tmpos = pos;
1244
1245         int column = 0;
1246         FOREACH(Weapons, it != WEP_Null, {
1247                 int weapon_stats = weapon_accuracy[i - WEP_FIRST];
1248
1249                 WepSet set = it.m_wepset;
1250                 if (weapon_stats < 0 && !((weapons_stat & set) || (weapons_inmap & set)))
1251                         continue;
1252                 if (it.spawnflags & WEP_TYPE_OTHER)
1253                         continue;
1254
1255                 float weapon_alpha;
1256                 if (weapon_stats >= 0)
1257                         weapon_alpha = sbt_fg_alpha;
1258                 else
1259                         weapon_alpha = 0.2 * sbt_fg_alpha;
1260
1261                 // weapon icon
1262                 drawpic_aspect_skin(tmpos, it.model2, vec2(weapon_width, weapon_height), '1 1 1', weapon_alpha * scoreboard_acc_fade_alpha, DRAWFLAG_NORMAL);
1263                 // the accuracy
1264                 if (weapon_stats >= 0) {
1265                         weapons_with_stats += 1;
1266                         average_accuracy += weapon_stats; // store sum of all accuracies in average_accuracy
1267
1268                         string s;
1269                         s = sprintf("%d%%", weapon_stats * 100);
1270
1271                         float padding;
1272                         padding = (weapon_width - stringwidth(s, false, hud_fontsize)) / 2; // center the accuracy value
1273
1274                         if(!autocvar_hud_panel_scoreboard_accuracy_nocolors)
1275                                 rgb = Accuracy_GetColor(weapon_stats);
1276
1277                         drawstring(tmpos + vec2(padding, weapon_height), s, hud_fontsize, rgb, sbt_fg_alpha * scoreboard_acc_fade_alpha, DRAWFLAG_NORMAL);
1278                 }
1279                 tmpos.x += weapon_width * rows;
1280                 pos.x += weapon_width * rows;
1281                 if (rows == 2 && column == columnns - 1) {
1282                         tmpos.x = oldposx;
1283                         tmpos.y += height;
1284                         pos.y += height;
1285                 }
1286                 ++column;
1287         });
1288
1289         if (weapons_with_stats)
1290                 average_accuracy = floor((average_accuracy * 100 / weapons_with_stats) + 0.5);
1291
1292         panel_size.x += panel_bg_padding * 2; // restore initial width
1293
1294         if (scoreboard_acc_fade_alpha == 1)
1295                 return end_pos;
1296         return initial_pos + (end_pos - initial_pos) * scoreboard_acc_fade_alpha;
1297 }
1298
1299 vector MapStats_DrawKeyValue(vector pos, string key, string value) {
1300         float px = pos.x;
1301         pos.x += hud_fontsize.x * 0.25;
1302         drawstring(pos, key, hud_fontsize, '1 1 1', sbt_fg_alpha, DRAWFLAG_NORMAL);
1303         pos.x = panel_pos.x + panel_size.x - stringwidth(value, false, hud_fontsize) - hud_fontsize.x * 0.25;
1304         drawstring(pos, value, hud_fontsize, '1 1 1', sbt_fg_alpha, DRAWFLAG_NORMAL);
1305         pos.x = px;
1306         pos.y += hud_fontsize.y;
1307
1308         return pos;
1309 }
1310
1311 vector Scoreboard_MapStats_Draw(vector pos, vector rgb, vector bg_size) {
1312         float stat_secrets_found, stat_secrets_total;
1313         float stat_monsters_killed, stat_monsters_total;
1314         float rows = 0;
1315         string val;
1316
1317         // get monster stats
1318         stat_monsters_killed = STAT(MONSTERS_KILLED);
1319         stat_monsters_total = STAT(MONSTERS_TOTAL);
1320
1321         // get secrets stats
1322         stat_secrets_found = STAT(SECRETS_FOUND);
1323         stat_secrets_total = STAT(SECRETS_TOTAL);
1324
1325         // get number of rows
1326         if(stat_secrets_total)
1327                 rows += 1;
1328         if(stat_monsters_total)
1329                 rows += 1;
1330
1331         // if no rows, return
1332         if (!rows)
1333                 return pos;
1334
1335         //  draw table header
1336         drawstring(pos + eX * panel_bg_padding, _("Map stats:"), hud_fontsize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
1337         pos.y += 1.25 * hud_fontsize.y;
1338         if(panel.current_panel_bg != "0")
1339                 pos.y += panel_bg_border;
1340
1341         panel_pos = pos;
1342         panel_size.y = hud_fontsize.y * rows;
1343         panel_size.y += panel_bg_padding * 2;
1344         HUD_Panel_DrawBg();
1345
1346         vector end_pos = panel_pos + eY * (panel_size.y + hud_fontsize.y);
1347         if(panel.current_panel_bg != "0")
1348                 end_pos.y += panel_bg_border * 2;
1349
1350         if(panel_bg_padding)
1351         {
1352                 panel_pos += '1 1 0' * panel_bg_padding;
1353                 panel_size -= '2 2 0' * panel_bg_padding;
1354         }
1355
1356         pos = panel_pos;
1357         vector tmp = panel_size;
1358
1359         if (sbt_bg_alpha)
1360                 drawpic_tiled(pos, "gfx/scoreboard/scoreboard_bg", bg_size, tmp, rgb, sbt_bg_alpha, DRAWFLAG_NORMAL);
1361
1362         // draw monsters
1363         if(stat_monsters_total)
1364         {
1365                 val = sprintf("%d/%d", stat_monsters_killed, stat_monsters_total);
1366                 pos = MapStats_DrawKeyValue(pos, _("Monsters killed:"), val);
1367         }
1368
1369         // draw secrets
1370         if(stat_secrets_total)
1371         {
1372                 val = sprintf("%d/%d", stat_secrets_found, stat_secrets_total);
1373                 pos = MapStats_DrawKeyValue(pos, _("Secrets found:"), val);
1374         }
1375
1376         panel_size.x += panel_bg_padding * 2; // restore initial width
1377         return end_pos;
1378 }
1379
1380
1381 vector Scoreboard_Rankings_Draw(vector pos, entity pl, vector rgb, vector bg_size)
1382 {
1383         int i;
1384         RANKINGS_RECEIVED_CNT = 0;
1385         for (i=RANKINGS_CNT-1; i>=0; --i)
1386                 if (grecordtime[i])
1387                         ++RANKINGS_RECEIVED_CNT;
1388
1389         if (RANKINGS_RECEIVED_CNT == 0)
1390                 return pos;
1391
1392         vector hl_rgb = rgb + '0.5 0.5 0.5';
1393
1394         pos.y += hud_fontsize.y;
1395         drawstring(pos + eX * panel_bg_padding, ((ISGAMETYPE(CTF)) ? _("Capture time rankings") : _("Rankings")), hud_fontsize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
1396         pos.y += 1.25 * hud_fontsize.y;
1397         if(panel.current_panel_bg != "0")
1398                 pos.y += panel_bg_border;
1399
1400         panel_pos = pos;
1401
1402         float namesize = 0;
1403         for(i = 0; i < RANKINGS_RECEIVED_CNT; ++i)
1404         {
1405                 float f = stringwidth(ColorTranslateRGB(grecordholder[i]), true, hud_fontsize);
1406                 if(f > namesize)
1407                         namesize = f;
1408         }
1409         bool cut = false;
1410         if(namesize > autocvar_hud_panel_scoreboard_namesize * hud_fontsize.x)
1411         {
1412                 namesize = autocvar_hud_panel_scoreboard_namesize * hud_fontsize.x;
1413                 cut = true;
1414         }
1415
1416         float ranksize = 3 * hud_fontsize.x;
1417         float timesize = 5 * hud_fontsize.x;
1418         vector columnsize = vec2(ranksize + timesize + namesize + hud_fontsize.x, 1.25 * hud_fontsize.y);
1419         int columns = max(1, floor((panel_size.x - 2 * panel_bg_padding) / columnsize.x));
1420         columns = min(columns, RANKINGS_RECEIVED_CNT);
1421
1422         // expand name column to fill the entire row
1423         float available_space = (panel_size.x - 2 * panel_bg_padding - columnsize.x * columns) / columns;
1424         namesize += available_space;
1425         columnsize.x += available_space;
1426
1427         panel_size.y = ceil(RANKINGS_RECEIVED_CNT / columns) * 1.25 * hud_fontsize.y;
1428         panel_size.y += panel_bg_padding * 2;
1429
1430         HUD_Panel_DrawBg();
1431
1432         vector end_pos = panel_pos + eY * (panel_size.y + hud_fontsize.y);
1433         if(panel.current_panel_bg != "0")
1434                 end_pos.y += panel_bg_border * 2;
1435
1436         if(panel_bg_padding)
1437         {
1438                 panel_pos += '1 1 0' * panel_bg_padding;
1439                 panel_size -= '2 2 0' * panel_bg_padding;
1440         }
1441
1442         pos = panel_pos;
1443
1444         if (sbt_bg_alpha)
1445                 drawpic_tiled(pos, "gfx/scoreboard/scoreboard_bg", bg_size, panel_size, rgb, sbt_bg_alpha, DRAWFLAG_NORMAL);
1446
1447         vector text_ofs = vec2(0.5 * hud_fontsize.x, (1.25 - 1) / 2 * hud_fontsize.y); // center text vertically
1448         string str = "";
1449         int column = 0, j = 0;
1450         string zoned_name_self = strzone(strdecolorize(entcs_GetName(player_localnum)));
1451         for(i = 0; i < RANKINGS_RECEIVED_CNT; ++i)
1452         {
1453                 float t;
1454                 t = grecordtime[i];
1455                 if (t == 0)
1456                         continue;
1457
1458                 if(strdecolorize(grecordholder[i]) == zoned_name_self)
1459                         drawfill(pos, columnsize, hl_rgb, sbt_highlight_alpha_self, DRAWFLAG_NORMAL);