]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/client/hud/panel/scoreboard.qc
Fix item stats networking by refactoring algorithm that sends item stats
[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 #include <common/items/inventory.qh>
17
18 // Scoreboard (#24)
19
20 const int MAX_SBT_FIELDS = MAX_SCORE;
21
22 PlayerScoreField sbt_field[MAX_SBT_FIELDS + 1];
23 float sbt_field_size[MAX_SBT_FIELDS + 1];
24 string sbt_field_title[MAX_SBT_FIELDS + 1];
25 int sbt_num_fields;
26
27 string autocvar_hud_fontsize;
28 string hud_fontsize_str;
29 float max_namesize;
30
31 float sbt_bg_alpha;
32 float sbt_fg_alpha;
33 float sbt_fg_alpha_self;
34 bool sbt_highlight;
35 float sbt_highlight_alpha;
36 float sbt_highlight_alpha_self;
37
38 // provide basic panel cvars to old clients
39 // TODO remove them after a future release (0.8.2+)
40 noref string autocvar_hud_panel_scoreboard_pos = "0.150000 0.150000";
41 noref string autocvar_hud_panel_scoreboard_size = "0.700000 0.700000";
42 noref string autocvar_hud_panel_scoreboard_bg = "border_default";
43 noref string autocvar_hud_panel_scoreboard_bg_color = "0 0.3 0.5";
44 noref string autocvar_hud_panel_scoreboard_bg_color_team = "";
45 noref string autocvar_hud_panel_scoreboard_bg_alpha = "0.7";
46 noref string autocvar_hud_panel_scoreboard_bg_border = "";
47 noref string autocvar_hud_panel_scoreboard_bg_padding = "";
48
49 float autocvar_hud_panel_scoreboard_fadeinspeed = 10;
50 float autocvar_hud_panel_scoreboard_fadeoutspeed = 5;
51 float autocvar_hud_panel_scoreboard_respawntime_decimals = 1;
52 float autocvar_hud_panel_scoreboard_table_bg_alpha = 0;
53 float autocvar_hud_panel_scoreboard_table_bg_scale = 0.25;
54 float autocvar_hud_panel_scoreboard_table_fg_alpha = 0.9;
55 float autocvar_hud_panel_scoreboard_table_fg_alpha_self = 1;
56 bool autocvar_hud_panel_scoreboard_table_highlight = true;
57 float autocvar_hud_panel_scoreboard_table_highlight_alpha = 0.2;
58 float autocvar_hud_panel_scoreboard_table_highlight_alpha_self = 0.4;
59 float autocvar_hud_panel_scoreboard_bg_teams_color_team = 0;
60 float autocvar_hud_panel_scoreboard_namesize = 15;
61 float autocvar_hud_panel_scoreboard_team_size_position = 0;
62
63 bool autocvar_hud_panel_scoreboard_accuracy = true;
64 bool autocvar_hud_panel_scoreboard_accuracy_doublerows = false;
65 bool autocvar_hud_panel_scoreboard_accuracy_nocolors = false;
66 float autocvar_hud_panel_scoreboard_accuracy_showdelay = 2;
67 float autocvar_hud_panel_scoreboard_accuracy_showdelay_minpos = 0.75;
68
69 bool autocvar_hud_panel_scoreboard_ctf_leaderboard = true;
70
71 bool autocvar_hud_panel_scoreboard_dynamichud = false;
72
73 float autocvar_hud_panel_scoreboard_maxheight = 0.6;
74 bool autocvar_hud_panel_scoreboard_others_showscore = true;
75 bool autocvar_hud_panel_scoreboard_spectators_showping = true;
76 bool autocvar_hud_panel_scoreboard_spectators_aligned = false;
77 float autocvar_hud_panel_scoreboard_minwidth = 0.4;
78
79 // mode 0: returns translated label
80 // mode 1: prints name and description of all the labels
81 string Label_getInfo(string label, int mode)
82 {
83         if (mode == 1)
84                 label = "bckills"; // first case in the switch
85
86         switch(label)
87         {
88                 case "bckills":      if (!mode) return CTX(_("SCO^bckills"));      else LOG_INFO(strcat("^3", "bckills", "            ^7", _("Number of ball carrier kills")));
89                 case "bctime":       if (!mode) return CTX(_("SCO^bctime"));       else LOG_INFO(strcat("^3", "bctime", "             ^7", _("Total amount of time holding the ball in Keepaway")));
90                 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")));
91                 case "captime":      if (!mode) return CTX(_("SCO^captime"));      else LOG_INFO(strcat("^3", "captime", "            ^7", _("Time of fastest capture (CTF)")));
92                 case "deaths":       if (!mode) return CTX(_("SCO^deaths"));       else LOG_INFO(strcat("^3", "deaths", "             ^7", _("Number of deaths")));
93                 case "destroyed":    if (!mode) return CTX(_("SCO^destroyed"));    else LOG_INFO(strcat("^3", "destroyed", "          ^7", _("Number of keys destroyed by pushing them into void")));
94                 case "dmg":          if (!mode) return CTX(_("SCO^damage"));       else LOG_INFO(strcat("^3", "dmg", "                ^7", _("The total damage done")));
95                 case "dmgtaken":     if (!mode) return CTX(_("SCO^dmgtaken"));     else LOG_INFO(strcat("^3", "dmgtaken", "           ^7", _("The total damage taken")));
96                 case "drops":        if (!mode) return CTX(_("SCO^drops"));        else LOG_INFO(strcat("^3", "drops", "              ^7", _("Number of flag drops")));
97                 case "elo":          if (!mode) return CTX(_("SCO^elo"));          else LOG_INFO(strcat("^3", "elo", "                ^7", _("Player ELO")));
98                 case "fastest":      if (!mode) return CTX(_("SCO^fastest"));      else LOG_INFO(strcat("^3", "fastest", "            ^7", _("Time of fastest lap (Race/CTS)")));
99                 case "faults":       if (!mode) return CTX(_("SCO^faults"));       else LOG_INFO(strcat("^3", "faults", "             ^7", _("Number of faults committed")));
100                 case "fckills":      if (!mode) return CTX(_("SCO^fckills"));      else LOG_INFO(strcat("^3", "fckills", "            ^7", _("Number of flag carrier kills")));
101                 case "fps":          if (!mode) return CTX(_("SCO^fps"));          else LOG_INFO(strcat("^3", "fps", "                ^7", _("FPS")));
102                 case "frags":        if (!mode) return CTX(_("SCO^frags"));        else LOG_INFO(strcat("^3", "frags", "              ^7", _("Number of kills minus suicides")));
103                 case "goals":        if (!mode) return CTX(_("SCO^goals"));        else LOG_INFO(strcat("^3", "goals", "              ^7", _("Number of goals scored")));
104                 case "kckills":      if (!mode) return CTX(_("SCO^kckills"));      else LOG_INFO(strcat("^3", "kckills", "            ^7", _("Number of keys carrier kills")));
105                 case "kd":           if (!mode) return CTX(_("SCO^k/d"));          else LOG_INFO(strcat("^3", "kd", "                 ^7", _("The kill-death ratio")));
106                 case "kdr":          if (!mode) return CTX(_("SCO^kdr"));          else LOG_INFO(strcat("^3", "kdr", "                ^7", _("The kill-death ratio")));
107                 case "kdratio":      if (!mode) return CTX(_("SCO^kdratio"));      else LOG_INFO(strcat("^3", "kdratio", "            ^7", _("The kill-death ratio")));
108                 case "kills":        if (!mode) return CTX(_("SCO^kills"));        else LOG_INFO(strcat("^3", "kills", "              ^7", _("Number of kills")));
109                 case "laps":         if (!mode) return CTX(_("SCO^laps"));         else LOG_INFO(strcat("^3", "laps", "               ^7", _("Number of laps finished (Race/CTS)")));
110                 case "lives":        if (!mode) return CTX(_("SCO^lives"));        else LOG_INFO(strcat("^3", "lives", "              ^7", _("Number of lives (LMS)")));
111                 case "losses":       if (!mode) return CTX(_("SCO^losses"));       else LOG_INFO(strcat("^3", "losses", "             ^7", _("Number of times a key was lost")));
112                 case "name":         if (!mode) return CTX(_("SCO^name"));         else LOG_INFO(strcat("^3", "name", "               ^7", _("Player name")));
113                 case "nick":         if (!mode) return CTX(_("SCO^nick"));         else LOG_INFO(strcat("^3", "nick", "               ^7", _("Player name")));
114                 case "objectives":   if (!mode) return CTX(_("SCO^objectives"));   else LOG_INFO(strcat("^3", "objectives", "         ^7", _("Number of objectives destroyed")));
115                 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")));
116                 case "ping":         if (!mode) return CTX(_("SCO^ping"));         else LOG_INFO(strcat("^3", "ping", "               ^7", _("Ping time")));
117                 case "pl":           if (!mode) return CTX(_("SCO^pl"));           else LOG_INFO(strcat("^3", "pl", "                 ^7", _("Packet loss")));
118                 case "pushes":       if (!mode) return CTX(_("SCO^pushes"));       else LOG_INFO(strcat("^3", "pushes", "             ^7", _("Number of players pushed into void")));
119                 case "rank":         if (!mode) return CTX(_("SCO^rank"));         else LOG_INFO(strcat("^3", "rank", "               ^7", _("Player rank")));
120                 case "returns":      if (!mode) return CTX(_("SCO^returns"));      else LOG_INFO(strcat("^3", "returns", "            ^7", _("Number of flag returns")));
121                 case "revivals":     if (!mode) return CTX(_("SCO^revivals"));     else LOG_INFO(strcat("^3", "revivals", "           ^7", _("Number of revivals")));
122                 case "rounds":       if (!mode) return CTX(_("SCO^rounds won"));   else LOG_INFO(strcat("^3", "rounds", "             ^7", _("Number of rounds won")));
123                 case "score":        if (!mode) return CTX(_("SCO^score"));        else LOG_INFO(strcat("^3", "score", "              ^7", _("Total score")));
124                 case "suicides":     if (!mode) return CTX(_("SCO^suicides"));     else LOG_INFO(strcat("^3", "suicides", "           ^7", _("Number of suicides")));
125                 case "sum":          if (!mode) return CTX(_("SCO^sum"));          else LOG_INFO(strcat("^3", "sum", "                ^7", _("Number of kills minus deaths")));
126                 case "takes":        if (!mode) return CTX(_("SCO^takes"));        else LOG_INFO(strcat("^3", "takes", "              ^7", _("Number of domination points taken (Domination)")));
127                 case "teamkills":    if (!mode) return CTX(_("SCO^teamkills"));    else LOG_INFO(strcat("^3", "teamkills", "          ^7", _("Number of teamkills")));
128                 case "ticks":        if (!mode) return CTX(_("SCO^ticks"));        else LOG_INFO(strcat("^3", "ticks", "              ^7", _("Number of ticks (Domination)")));
129                 case "time":         if (!mode) return CTX(_("SCO^time"));         else LOG_INFO(strcat("^3", "time", "               ^7", _("Total time raced (Race/CTS)")));
130                 default: return label;
131         }
132         return label;
133 }
134
135 void PrintScoresLabels() { Label_getInfo(string_null, 1); }
136 string TranslateScoresLabel(string label) { return Label_getInfo(label, 0); }
137
138 void Scoreboard_InitScores()
139 {
140         int i, f;
141
142         ps_primary = ps_secondary = NULL;
143         ts_primary = ts_secondary = -1;
144         FOREACH(Scores, true, {
145                 f = (scores_flags(it) & SFL_SORT_PRIO_MASK);
146                 if(f == SFL_SORT_PRIO_PRIMARY)
147                         ps_primary = it;
148                 if(f == SFL_SORT_PRIO_SECONDARY)
149                         ps_secondary = it;
150         });
151         if(ps_secondary == NULL)
152                 ps_secondary = ps_primary;
153
154         for(i = 0; i < MAX_TEAMSCORE; ++i)
155         {
156                 f = (teamscores_flags(i) & SFL_SORT_PRIO_MASK);
157                 if(f == SFL_SORT_PRIO_PRIMARY)
158                         ts_primary = i;
159                 if(f == SFL_SORT_PRIO_SECONDARY)
160                         ts_secondary = i;
161         }
162         if(ts_secondary == -1)
163                 ts_secondary = ts_primary;
164
165         Cmd_Scoreboard_SetFields(0);
166 }
167
168 //float lastpnum;
169 void Scoreboard_UpdatePlayerTeams()
170 {
171         entity pl, tmp;
172         //int num = 0;
173         for(pl = players.sort_next; pl; pl = pl.sort_next)
174         {
175                 //num += 1;
176                 int Team = entcs_GetScoreTeam(pl.sv_entnum);
177                 if(SetTeam(pl, Team))
178                 {
179                         tmp = pl.sort_prev;
180                         Scoreboard_UpdatePlayerPos(pl);
181                         if(tmp)
182                                 pl = tmp;
183                         else
184                                 pl = players.sort_next;
185                 }
186         }
187         /*
188         if(num != lastpnum)
189                 print(strcat("PNUM: ", ftos(num), "\n"));
190         lastpnum = num;
191         */
192 }
193
194 int Scoreboard_CompareScore(int vl, int vr, int f)
195 {
196         TC(int, vl); TC(int, vr); TC(int, f);
197         if(f & SFL_ZERO_IS_WORST)
198         {
199                 if(vl == 0 && vr != 0)
200                         return 1;
201                 if(vl != 0 && vr == 0)
202                         return 0;
203         }
204         if(vl > vr)
205                 return IS_INCREASING(f);
206         if(vl < vr)
207                 return IS_DECREASING(f);
208         return -1;
209 }
210
211 float Scoreboard_ComparePlayerScores(entity left, entity right)
212 {
213         float vl, vr, r;
214         vl = entcs_GetTeam(left.sv_entnum);
215         vr = entcs_GetTeam(right.sv_entnum);
216
217         if(!left.gotscores)
218                 vl = NUM_SPECTATOR;
219         if(!right.gotscores)
220                 vr = NUM_SPECTATOR;
221
222         if(vl > vr)
223                 return true;
224         if(vl < vr)
225                 return false;
226
227         if(vl == NUM_SPECTATOR)
228         {
229                 // FIRST the one with scores (spectators), THEN the ones without (downloaders)
230                 // no other sorting
231                 if(!left.gotscores && right.gotscores)
232                         return true;
233                 return false;
234         }
235
236         r = Scoreboard_CompareScore(left.scores(ps_primary), right.scores(ps_primary), scores_flags(ps_primary));
237         if (r >= 0)
238                 return r;
239
240         r = Scoreboard_CompareScore(left.scores(ps_secondary), right.scores(ps_secondary), scores_flags(ps_secondary));
241         if (r >= 0)
242                 return r;
243
244         FOREACH(Scores, true, {
245                 r = Scoreboard_CompareScore(left.scores(it), right.scores(it), scores_flags(it));
246                 if (r >= 0) return r;
247         });
248
249         if (left.sv_entnum < right.sv_entnum)
250                 return true;
251
252         return false;
253 }
254
255 void Scoreboard_UpdatePlayerPos(entity player)
256 {
257         entity ent;
258         for(ent = player.sort_next; ent && Scoreboard_ComparePlayerScores(player, ent); ent = player.sort_next)
259         {
260                 SORT_SWAP(player, ent);
261         }
262         for(ent = player.sort_prev; ent != players && Scoreboard_ComparePlayerScores(ent, player); ent = player.sort_prev)
263         {
264                 SORT_SWAP(ent, player);
265         }
266 }
267
268 float Scoreboard_CompareTeamScores(entity left, entity right)
269 {
270         int i, r;
271
272         if(left.team == NUM_SPECTATOR)
273                 return 1;
274         if(right.team == NUM_SPECTATOR)
275                 return 0;
276
277         r = Scoreboard_CompareScore(left.teamscores(ts_primary), right.teamscores(ts_primary), teamscores_flags(ts_primary));
278         if (r >= 0)
279                 return r;
280
281         r = Scoreboard_CompareScore(left.teamscores(ts_secondary), right.teamscores(ts_secondary), teamscores_flags(ts_secondary));
282         if (r >= 0)
283                 return r;
284
285         for(i = 0; i < MAX_TEAMSCORE; ++i)
286         {
287                 r = Scoreboard_CompareScore(left.teamscores(i), right.teamscores(i), teamscores_flags(i));
288                 if (r >= 0)
289                         return r;
290         }
291
292         if (left.team < right.team)
293                 return true;
294
295         return false;
296 }
297
298 void Scoreboard_UpdateTeamPos(entity Team)
299 {
300         entity ent;
301         for(ent = Team.sort_next; ent && Scoreboard_CompareTeamScores(Team, ent); ent = Team.sort_next)
302         {
303                 SORT_SWAP(Team, ent);
304         }
305         for(ent = Team.sort_prev; ent != teams && Scoreboard_CompareTeamScores(ent, Team); ent = Team.sort_prev)
306         {
307                 SORT_SWAP(ent, Team);
308         }
309 }
310
311 void Cmd_Scoreboard_Help()
312 {
313         LOG_INFO(_("You can modify the scoreboard using the ^2scoreboard_columns_set command."));
314         LOG_INFO(_("Usage:"));
315         LOG_INFO("^2scoreboard_columns_set ^3default");
316         LOG_INFO(_("^2scoreboard_columns_set ^3field1 field2 ..."));
317         LOG_INFO(_("^2scoreboard_columns_set ^7without arguments reads the arguments from the cvar scoreboard_columns"));
318         LOG_INFO(_("  ^5Note: ^7scoreboard_columns_set without arguments is executed on every map start"));
319         LOG_INFO(_("^2scoreboard_columns_set ^3expand_default ^7loads default layout and expands it into the cvar scoreboard_columns so you can edit it"));
320         LOG_INFO(_("You can use a ^3|^7 to start the right-aligned fields."));
321         LOG_INFO(_("The following field names are recognized (case insensitive):"));
322         LOG_INFO("");
323
324         PrintScoresLabels();
325         LOG_INFO("");
326
327         LOG_INFO(_("Before a field you can put a + or - sign, then a comma separated list\n"
328                 "of game types, then a slash, to make the field show up only in these\n"
329                 "or in all but these game types. You can also specify 'all' as a\n"
330                 "field to show all fields available for the current game mode."));
331         LOG_INFO("");
332
333         LOG_INFO(_("The special game type names 'teams' and 'noteams' can be used to\n"
334                 "include/exclude ALL teams/noteams game modes."));
335         LOG_INFO("");
336
337         LOG_INFO(_("Example: scoreboard_columns_set name ping pl | +ctf/field3 -dm/field4"));
338         LOG_INFO(_("will display name, ping and pl aligned to the left, and the fields\n"
339                 "right of the vertical bar aligned to the right."));
340         LOG_INFO(_("'field3' will only be shown in CTF, and 'field4' will be shown in all\n"
341                         "other gamemodes except DM."));
342 }
343
344 // NOTE: adding a gametype with ? to not warn for an optional field
345 // make sure it's excluded in a previous exclusive rule, if any
346 // otherwise the previous exclusive rule warns anyway
347 // e.g. -teams,rc,cts,lms/kills ?+rc/kills
348 #define SCOREBOARD_DEFAULT_COLUMNS \
349 "ping pl fps name |" \
350 " -teams,rc,cts,inv,lms/kills +ft,tdm/kills ?+rc,inv/kills" \
351 " -teams,lms/deaths +ft,tdm/deaths" \
352 " +tdm/sum" \
353 " -teams,lms,rc,cts,inv,ka/suicides +ft,tdm/suicides ?+rc,inv/suicides" \
354 " -cts,dm,tdm,ka,ft/frags" /* tdm already has this in "score" */ \
355 " +tdm,ft,dom,ons,as/teamkills"\
356 " -rc,cts,nb/dmg -rc,cts,nb/dmgtaken" \
357 " +ctf/pickups +ctf/fckills +ctf/returns +ctf/caps +ons/takes +ons/caps" \
358 " +lms/lives +lms/rank" \
359 " +kh/kckills +kh/losses +kh/caps" \
360 " ?+rc/laps ?+rc/time +rc,cts/fastest" \
361 " +as/objectives +nb/faults +nb/goals" \
362 " +ka/pickups +ka/bckills +ka/bctime +ft/revivals" \
363 " +dom/ticks +dom/takes" \
364 " -lms,rc,cts,inv,nb/score"
365
366 void Cmd_Scoreboard_SetFields(int argc)
367 {
368         TC(int, argc);
369         int i, slash;
370         string str, pattern;
371         bool have_name = false, have_primary = false, have_secondary = false, have_separator = false;
372         int missing;
373
374         if(!gametype)
375                 return; // do nothing, we don't know gametype and scores yet
376
377         // sbt_fields uses strunzone on the titles!
378         if(!sbt_field_title[0])
379                 for(i = 0; i < MAX_SBT_FIELDS; ++i)
380                         sbt_field_title[i] = strzone("(null)");
381
382         // TODO: re enable with gametype dependant cvars?
383         if(argc < 3) // no arguments provided
384                 argc = tokenizebyseparator(strcat("0 1 ", autocvar_scoreboard_columns), " ");
385
386         if(argc < 3)
387                 argc = tokenizebyseparator(strcat("0 1 ", SCOREBOARD_DEFAULT_COLUMNS), " ");
388
389         if(argc == 3)
390         {
391                 if(argv(2) == "default" || argv(2) == "expand_default")
392                 {
393                         if(argv(2) == "expand_default")
394                                 cvar_set("scoreboard_columns", SCOREBOARD_DEFAULT_COLUMNS);
395                         argc = tokenizebyseparator(strcat("0 1 ", SCOREBOARD_DEFAULT_COLUMNS), " ");
396                 }
397                 else if(argv(2) == "all")
398                 {
399                         string s = "ping pl name |"; // scores without a label
400                         FOREACH(Scores, true, {
401                                 if(it != ps_primary)
402                                 if(it != ps_secondary)
403                                 if(scores_label(it) != "")
404                                         s = strcat(s, " ", scores_label(it));
405                         });
406                         if(ps_secondary != ps_primary)
407                                 s = strcat(s, " ", scores_label(ps_secondary));
408                         s = strcat(s, " ", scores_label(ps_primary));
409                         argc = tokenizebyseparator(strcat("0 1 ", s), " ");
410                 }
411         }
412
413
414         sbt_num_fields = 0;
415
416         hud_fontsize = HUD_GetFontsize("hud_fontsize");
417
418         for(i = 1; i < argc - 1; ++i)
419         {
420                 str = argv(i+1);
421                 bool nocomplain = false;
422                 if(substring(str, 0, 1) == "?")
423                 {
424                         nocomplain = true;
425                         str = substring(str, 1, strlen(str) - 1);
426                 }
427
428                 slash = strstrofs(str, "/", 0);
429                 if(slash >= 0)
430                 {
431                         pattern = substring(str, 0, slash);
432                         str = substring(str, slash + 1, strlen(str) - (slash + 1));
433
434                         if (!isGametypeInFilter(gametype, teamplay, false, pattern))
435                                 continue;
436                 }
437
438                 strcpy(sbt_field_title[sbt_num_fields], TranslateScoresLabel(str));
439                 sbt_field_size[sbt_num_fields] = stringwidth(sbt_field_title[sbt_num_fields], false, hud_fontsize);
440                 str = strtolower(str);
441
442                 PlayerScoreField j;
443                 switch(str)
444                 {
445                         case "ping": sbt_field[sbt_num_fields] = SP_PING; break;
446                         case "pl": sbt_field[sbt_num_fields] = SP_PL; break;
447                         case "kd": case "kdr": case "kdratio": sbt_field[sbt_num_fields] = SP_KDRATIO; break;
448                         case "sum": case "diff": case "k-d": sbt_field[sbt_num_fields] = SP_SUM; break;
449                         case "name": case "nick": sbt_field[sbt_num_fields] = SP_NAME; have_name = true; break;
450                         case "|": sbt_field[sbt_num_fields] = SP_SEPARATOR; have_separator = true; break;
451                         case "elo": sbt_field[sbt_num_fields] = SP_ELO; break;
452                         case "dmg": case "damage": sbt_field[sbt_num_fields] = SP_DMG; break;
453                         case "dmgtaken": case "damagetaken": sbt_field[sbt_num_fields] = SP_DMGTAKEN; break;
454                         case "fps": sbt_field[sbt_num_fields] = SP_FPS; break;
455                         default:
456                         {
457                                 FOREACH(Scores, true, {
458                                         if (str == strtolower(scores_label(it))) {
459                                                 j = it;
460                                                 goto found; // sorry, but otherwise fteqcc -O3 miscompiles this and warns about "unreachable code"
461                                         }
462                                 });
463
464 LABEL(notfound)
465                                 if(str == "frags")
466                                         j = SP_FRAGS;
467                                 else
468                                 {
469                                         if(!nocomplain)
470                                                 LOG_INFOF("^1Error:^7 Unknown score field: '%s'", str);
471                                         continue;
472                                 }
473 LABEL(found)
474                                 sbt_field[sbt_num_fields] = j;
475                                 if(j == ps_primary)
476                                         have_primary = true;
477                                 if(j == ps_secondary)
478                                         have_secondary = true;
479
480                         }
481                 }
482                 ++sbt_num_fields;
483                 if(sbt_num_fields >= MAX_SBT_FIELDS)
484                         break;
485         }
486
487         if(scores_flags(ps_primary) & SFL_ALLOW_HIDE)
488                 have_primary = true;
489         if(scores_flags(ps_secondary) & SFL_ALLOW_HIDE)
490                 have_secondary = true;
491         if(ps_primary == ps_secondary)
492                 have_secondary = true;
493         missing = (!have_primary) + (!have_secondary) + (!have_separator) + (!have_name);
494
495         if(sbt_num_fields + missing < MAX_SBT_FIELDS)
496         {
497                 if(!have_name)
498                 {
499                         strunzone(sbt_field_title[sbt_num_fields]);
500                         for(i = sbt_num_fields; i > 0; --i)
501                         {
502                                 sbt_field_title[i] = sbt_field_title[i-1];
503                                 sbt_field_size[i] = sbt_field_size[i-1];
504                                 sbt_field[i] = sbt_field[i-1];
505                         }
506                         sbt_field_title[0] = strzone(TranslateScoresLabel("name"));
507                         sbt_field[0] = SP_NAME;
508                         ++sbt_num_fields;
509                         LOG_INFO("fixed missing field 'name'");
510
511                         if(!have_separator)
512                         {
513                                 strunzone(sbt_field_title[sbt_num_fields]);
514                                 for(i = sbt_num_fields; i > 1; --i)
515                                 {
516                                         sbt_field_title[i] = sbt_field_title[i-1];
517                                         sbt_field_size[i] = sbt_field_size[i-1];
518                                         sbt_field[i] = sbt_field[i-1];
519                                 }
520                                 sbt_field_title[1] = strzone("|");
521                                 sbt_field[1] = SP_SEPARATOR;
522                                 sbt_field_size[1] = stringwidth("|", false, hud_fontsize);
523                                 ++sbt_num_fields;
524                                 LOG_INFO("fixed missing field '|'");
525                         }
526                 }
527                 else if(!have_separator)
528                 {
529                         strcpy(sbt_field_title[sbt_num_fields], "|");
530                         sbt_field_size[sbt_num_fields] = stringwidth("|", false, hud_fontsize);
531                         sbt_field[sbt_num_fields] = SP_SEPARATOR;
532                         ++sbt_num_fields;
533                         LOG_INFO("fixed missing field '|'");
534                 }
535                 if(!have_secondary)
536                 {
537                         strcpy(sbt_field_title[sbt_num_fields], TranslateScoresLabel(scores_label(ps_secondary)));
538                         sbt_field_size[sbt_num_fields] = stringwidth(sbt_field_title[sbt_num_fields], false, hud_fontsize);
539                         sbt_field[sbt_num_fields] = ps_secondary;
540                         ++sbt_num_fields;
541                         LOG_INFOF("fixed missing field '%s'", scores_label(ps_secondary));
542                 }
543                 if(!have_primary)
544                 {
545                         strcpy(sbt_field_title[sbt_num_fields], TranslateScoresLabel(scores_label(ps_primary)));
546                         sbt_field_size[sbt_num_fields] = stringwidth(sbt_field_title[sbt_num_fields], false, hud_fontsize);
547                         sbt_field[sbt_num_fields] = ps_primary;
548                         ++sbt_num_fields;
549                         LOG_INFOF("fixed missing field '%s'", scores_label(ps_primary));
550                 }
551         }
552
553         sbt_field[sbt_num_fields] = SP_END;
554 }
555
556 // MOVEUP::
557 vector sbt_field_rgb;
558 string sbt_field_icon0;
559 string sbt_field_icon1;
560 string sbt_field_icon2;
561 vector sbt_field_icon0_rgb;
562 vector sbt_field_icon1_rgb;
563 vector sbt_field_icon2_rgb;
564 string Scoreboard_GetName(entity pl)
565 {
566         if(ready_waiting && pl.ready)
567         {
568                 sbt_field_icon0 = "gfx/scoreboard/player_ready";
569         }
570         else if(!teamplay)
571         {
572                 int f = entcs_GetClientColors(pl.sv_entnum);
573                 {
574                         sbt_field_icon0 = "gfx/scoreboard/playercolor_base";
575                         sbt_field_icon1 = "gfx/scoreboard/playercolor_shirt";
576                         sbt_field_icon1_rgb = colormapPaletteColor(floor(f / 16), 0);
577                         sbt_field_icon2 = "gfx/scoreboard/playercolor_pants";
578                         sbt_field_icon2_rgb = colormapPaletteColor(f % 16, 1);
579                 }
580         }
581         return entcs_GetName(pl.sv_entnum);
582 }
583
584 string Scoreboard_GetField(entity pl, PlayerScoreField field)
585 {
586         float tmp, num, denom;
587         int f;
588         string str;
589         sbt_field_rgb = '1 1 1';
590         sbt_field_icon0 = "";
591         sbt_field_icon1 = "";
592         sbt_field_icon2 = "";
593         sbt_field_icon0_rgb = '1 1 1';
594         sbt_field_icon1_rgb = '1 1 1';
595         sbt_field_icon2_rgb = '1 1 1';
596         switch(field)
597         {
598                 case SP_PING:
599                         if (!pl.gotscores)
600                                 return "\xE2\x96\xB6\xE2\x96\xB6\xE2\x96\xB6"; // >>> sign using U+25B6 (Black Right-Pointing Triangle)
601                         //str = getplayerkeyvalue(pl.sv_entnum, "ping");
602                         f = pl.ping;
603                         if(f == 0)
604                                 return _("N/A");
605                         tmp = max(0, min(220, f-80)) / 220;
606                         sbt_field_rgb = '1 1 1' - '0 1 1' * tmp;
607                         return ftos(f);
608
609                 case SP_PL:
610                         if (!pl.gotscores)
611                                 return _("N/A");
612                         f = pl.ping_packetloss;
613                         tmp = pl.ping_movementloss;
614                         if(f == 0 && tmp == 0)
615                                 return "";
616                         str = ftos(ceil(f * 100));
617                         if(tmp != 0)
618                                 str = strcat(str, "~", ftos(ceil(tmp * 100)));
619                         tmp = bound(0, f / 0.2 + tmp / 0.04, 1); // 20% is REALLY BAD pl
620                         sbt_field_rgb = '1 0.5 0.5' - '0 0.5 0.5' * tmp;
621                         return str;
622
623                 case SP_NAME:
624                         return Scoreboard_GetName(pl);
625
626                 case SP_FRAGS:
627                         f = pl.(scores(SP_KILLS));
628                         f -= pl.(scores(SP_SUICIDES));
629                         return ftos(f);
630
631                 case SP_KDRATIO:
632                         num = pl.(scores(SP_KILLS));
633                         denom = pl.(scores(SP_DEATHS));
634
635                         if(denom == 0) {
636                                 sbt_field_rgb = '0 1 0';
637                                 str = sprintf("%d", num);
638                         } else if(num <= 0) {
639                                 sbt_field_rgb = '1 0 0';
640                                 str = sprintf("%.1f", num/denom);
641                         } else
642                                 str = sprintf("%.1f", num/denom);
643                         return str;
644
645                 case SP_SUM:
646                         f = pl.(scores(SP_KILLS));
647                         f -= pl.(scores(SP_DEATHS));
648
649                         if(f > 0) {
650                                 sbt_field_rgb = '0 1 0';
651                         } else if(f == 0) {
652                                 sbt_field_rgb = '1 1 1';
653                         } else {
654                                 sbt_field_rgb = '1 0 0';
655                         }
656                         return ftos(f);
657
658                 case SP_ELO:
659                 {
660                         float elo = pl.(scores(SP_ELO));
661                         switch (elo) {
662                                 case -1: return "...";
663                                 case -2: return _("N/A");
664                                 default: return ftos(elo);
665                         }
666                 }
667
668                 case SP_FPS:
669                 {
670                         float fps = pl.(scores(SP_FPS));
671                         if(fps == 0)
672                         {
673                                 sbt_field_rgb = '1 1 1';
674                                 return ((pl.ping == 0) ? _("N/A") : "..."); // if 0 ping, either connecting or bot (either case can't show proper score)
675                         }
676                         //sbt_field_rgb = HUD_Get_Num_Color(fps, 200);
677                         sbt_field_rgb = '1 0 0' + '0 1 1' * (bound(0, fps, 60) / 60);
678                         return ftos(fps);
679                 }
680
681                 case SP_DMG: case SP_DMGTAKEN:
682                         return sprintf("%.1f k", pl.(scores(field)) / 1000);
683
684                 default: case SP_SCORE:
685                         tmp = pl.(scores(field));
686                         f = scores_flags(field);
687                         if(field == ps_primary)
688                                 sbt_field_rgb = '1 1 0';
689                         else if(field == ps_secondary)
690                                 sbt_field_rgb = '0 1 1';
691                         else
692                                 sbt_field_rgb = '1 1 1';
693                         return ScoreString(f, tmp);
694         }
695         //return "error";
696 }
697
698 float sbt_fixcolumnwidth_len;
699 float sbt_fixcolumnwidth_iconlen;
700 float sbt_fixcolumnwidth_marginlen;
701
702 string Scoreboard_FixColumnWidth(int i, string str)
703 {
704         TC(int, i);
705         float f;
706         vector sz;
707
708         sbt_fixcolumnwidth_iconlen = 0;
709
710         if(sbt_field_icon0 != "")
711         {
712                 sz = draw_getimagesize(sbt_field_icon0);
713                 f = sz.x / sz.y;
714                 if(sbt_fixcolumnwidth_iconlen < f)
715                         sbt_fixcolumnwidth_iconlen = f;
716         }
717
718         if(sbt_field_icon1 != "")
719         {
720                 sz = draw_getimagesize(sbt_field_icon1);
721                 f = sz.x / sz.y;
722                 if(sbt_fixcolumnwidth_iconlen < f)
723                         sbt_fixcolumnwidth_iconlen = f;
724         }
725
726         if(sbt_field_icon2 != "")
727         {
728                 sz = draw_getimagesize(sbt_field_icon2);
729                 f = sz.x / sz.y;
730                 if(sbt_fixcolumnwidth_iconlen < f)
731                         sbt_fixcolumnwidth_iconlen = f;
732         }
733
734         if(sbt_fixcolumnwidth_iconlen != 0)
735         {
736                 sbt_fixcolumnwidth_iconlen *= hud_fontsize.y / hud_fontsize.x; // fix icon aspect
737                 sbt_fixcolumnwidth_marginlen = stringwidth(" ", false, hud_fontsize);
738         }
739         else
740                 sbt_fixcolumnwidth_marginlen = 0;
741
742         if(sbt_field[i] == SP_NAME) // name gets all remaining space
743         {
744                 int j;
745                 float remaining_space = 0;
746                 for(j = 0; j < sbt_num_fields; ++j)
747                         if(j != i)
748                                 if (sbt_field[i] != SP_SEPARATOR)
749                                         remaining_space += sbt_field_size[j] + hud_fontsize.x;
750                 sbt_field_size[i] = panel_size.x - remaining_space;
751
752                 if (sbt_fixcolumnwidth_iconlen != 0)
753                         remaining_space += sbt_fixcolumnwidth_marginlen + sbt_fixcolumnwidth_iconlen * hud_fontsize.x;
754                 float namesize = panel_size.x - remaining_space;
755                 str = textShortenToWidth(str, namesize, hud_fontsize, stringwidth_colors);
756                 sbt_fixcolumnwidth_len = stringwidth(str, true, hud_fontsize);
757
758                 max_namesize = vid_conwidth - remaining_space;
759         }
760         else
761                 sbt_fixcolumnwidth_len = stringwidth(str, false, hud_fontsize);
762
763         f = sbt_fixcolumnwidth_len + sbt_fixcolumnwidth_marginlen + sbt_fixcolumnwidth_iconlen * hud_fontsize.x;
764         if(sbt_field_size[i] < f)
765                 sbt_field_size[i] = f;
766
767         return str;
768 }
769
770 void Scoreboard_initFieldSizes()
771 {
772         for(int i = 0; i < sbt_num_fields; ++i)
773         {
774                 sbt_field_size[i] = stringwidth(sbt_field_title[i], false, hud_fontsize);
775                 Scoreboard_FixColumnWidth(i, "");
776         }
777 }
778
779 vector Scoreboard_DrawHeader(vector pos, vector rgb, bool other_players)
780 {
781         int i;
782         vector column_dim = eY * panel_size.y;
783         if(other_players)
784                 column_dim.y -= 1.25 * hud_fontsize.y;
785         vector text_offset = eY * (1.25 - 1) / 2 * hud_fontsize.y;
786         pos.x += hud_fontsize.x * 0.5;
787         for(i = 0; i < sbt_num_fields; ++i)
788         {
789                 if(sbt_field[i] == SP_SEPARATOR)
790                         break;
791                 column_dim.x = sbt_field_size[i] + hud_fontsize.x;
792                 if (sbt_highlight)
793                         if (i % 2)
794                                 drawfill(pos - eX * hud_fontsize.x * 0.5, column_dim, '0 0 0', sbt_highlight_alpha, DRAWFLAG_NORMAL);
795                 drawstring(pos + text_offset, sbt_field_title[i], hud_fontsize, rgb * 1.5, sbt_fg_alpha, DRAWFLAG_NORMAL);
796                 pos.x += column_dim.x;
797         }
798         if(sbt_field[i] == SP_SEPARATOR)
799         {
800                 pos.x = panel_pos.x + panel_size.x - hud_fontsize.x * 0.5;
801                 for(i = sbt_num_fields - 1; i > 0; --i)
802                 {
803                         if(sbt_field[i] == SP_SEPARATOR)
804                                 break;
805
806                         pos.x -= sbt_field_size[i];
807
808                         if (sbt_highlight)
809                                 if (!(i % 2))
810                                 {
811                                         column_dim.x = sbt_field_size[i] + hud_fontsize.x;
812                                         drawfill(pos - eX * hud_fontsize.x * 0.5, column_dim, '0 0 0', sbt_highlight_alpha, DRAWFLAG_NORMAL);
813                                 }
814
815                         text_offset.x = sbt_field_size[i] - stringwidth(sbt_field_title[i], false, hud_fontsize);
816                         drawstring(pos + text_offset, sbt_field_title[i], hud_fontsize, rgb * 1.5, sbt_fg_alpha, DRAWFLAG_NORMAL);
817                         pos.x -= hud_fontsize.x;
818                 }
819         }
820
821         pos.x = panel_pos.x;
822         pos.y += 1.25 * hud_fontsize.y;
823         return pos;
824 }
825
826 void Scoreboard_DrawItem(vector item_pos, vector rgb, entity pl, bool is_self, int pl_number)
827 {
828         TC(bool, is_self); TC(int, pl_number);
829         string str;
830         bool is_spec = (entcs_GetSpecState(pl.sv_entnum) == ENTCS_SPEC_PURE);
831
832         vector h_pos = item_pos;
833         vector h_size = vec2(panel_size.x, hud_fontsize.y * 1.25);
834         // alternated rows highlighting
835         if(is_self)
836                 drawfill(h_pos, h_size, rgb, sbt_highlight_alpha_self, DRAWFLAG_NORMAL);
837         else if((sbt_highlight) && (!(pl_number % 2)))
838                 drawfill(h_pos, h_size, rgb, sbt_highlight_alpha, DRAWFLAG_NORMAL);
839
840         float fg_alpha = (is_self ? sbt_fg_alpha_self : sbt_fg_alpha);
841
842         vector pos = item_pos;
843         // put a "self indicator" beside the self row, unicode U+25C0 (black left-pointing triangle)
844         if (is_self)
845                 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);
846
847         pos.x += hud_fontsize.x * 0.5;
848         pos.y += (1.25 - 1) / 2 * hud_fontsize.y; // center text vertically
849         vector tmp = '0 0 0';
850         int i;
851         PlayerScoreField field;
852         for(i = 0; i < sbt_num_fields; ++i)
853         {
854                 field = sbt_field[i];
855                 if(field == SP_SEPARATOR)
856                         break;
857
858                 if(is_spec && field != SP_NAME && field != SP_PING) {
859                         pos.x += sbt_field_size[i] + hud_fontsize.x;
860                         continue;
861                 }
862                 str = Scoreboard_GetField(pl, field);
863                 str = Scoreboard_FixColumnWidth(i, str);
864
865                 pos.x += sbt_field_size[i] + hud_fontsize.x;
866
867                 if(field == SP_NAME) {
868                         tmp.x = sbt_field_size[i] - hud_fontsize.x * sbt_fixcolumnwidth_iconlen - sbt_fixcolumnwidth_marginlen + hud_fontsize.x;
869                         drawcolorcodedstring(pos - tmp, str, hud_fontsize, fg_alpha, DRAWFLAG_NORMAL);
870                 } else {
871                         tmp.x = sbt_fixcolumnwidth_len + hud_fontsize.x;
872                         drawstring(pos - tmp, str, hud_fontsize, sbt_field_rgb, fg_alpha, DRAWFLAG_NORMAL);
873                 }
874
875                 tmp.x = sbt_field_size[i] + hud_fontsize.x;
876                 if(sbt_field_icon0 != "")
877                         drawpic(pos - tmp, sbt_field_icon0, vec2(hud_fontsize.x * sbt_fixcolumnwidth_iconlen, hud_fontsize.y), sbt_field_icon1_rgb, fg_alpha, DRAWFLAG_NORMAL);
878                 if(sbt_field_icon1 != "")
879                         drawpic(pos - tmp, sbt_field_icon1, vec2(hud_fontsize.x * sbt_fixcolumnwidth_iconlen, hud_fontsize.y), sbt_field_icon1_rgb, fg_alpha, DRAWFLAG_NORMAL);
880                 if(sbt_field_icon2 != "")
881                         drawpic(pos - tmp, sbt_field_icon2, vec2(hud_fontsize.x * sbt_fixcolumnwidth_iconlen, hud_fontsize.y), sbt_field_icon2_rgb, fg_alpha, DRAWFLAG_NORMAL);
882         }
883
884         if(sbt_field[i] == SP_SEPARATOR)
885         {
886                 pos.x = item_pos.x + panel_size.x - hud_fontsize.x * 0.5;
887                 for(i = sbt_num_fields-1; i > 0; --i)
888                 {
889                         field = sbt_field[i];
890                         if(field == SP_SEPARATOR)
891                                 break;
892
893                         if(is_spec && field != SP_NAME && field != SP_PING) {
894                                 pos.x -= sbt_field_size[i] + hud_fontsize.x;
895                                 continue;
896                         }
897
898                         str = Scoreboard_GetField(pl, field);
899                         str = Scoreboard_FixColumnWidth(i, str);
900
901                         if(field == SP_NAME) {
902                                 tmp.x = sbt_fixcolumnwidth_len; // left or right aligned? let's put it right...
903                                 drawcolorcodedstring(pos - tmp, str, hud_fontsize, fg_alpha, DRAWFLAG_NORMAL);
904                         } else {
905                                 tmp.x = sbt_fixcolumnwidth_len;
906                                 drawstring(pos - tmp, str, hud_fontsize, sbt_field_rgb, fg_alpha, DRAWFLAG_NORMAL);
907                         }
908
909                         tmp.x = sbt_field_size[i];
910                         if(sbt_field_icon0 != "")
911                                 drawpic(pos - tmp, sbt_field_icon0, vec2(hud_fontsize.x * sbt_fixcolumnwidth_iconlen, hud_fontsize.y), sbt_field_icon1_rgb, fg_alpha, DRAWFLAG_NORMAL);
912                         if(sbt_field_icon1 != "")
913                                 drawpic(pos - tmp, sbt_field_icon1, vec2(hud_fontsize.x * sbt_fixcolumnwidth_iconlen, hud_fontsize.y), sbt_field_icon1_rgb, fg_alpha, DRAWFLAG_NORMAL);
914                         if(sbt_field_icon2 != "")
915                                 drawpic(pos - tmp, sbt_field_icon2, vec2(hud_fontsize.x * sbt_fixcolumnwidth_iconlen, hud_fontsize.y), sbt_field_icon2_rgb, fg_alpha, DRAWFLAG_NORMAL);
916                         pos.x -= sbt_field_size[i] + hud_fontsize.x;
917                 }
918         }
919
920         if(pl.eliminated)
921                 drawfill(h_pos, h_size, '0 0 0', 0.5 * panel_fg_alpha, DRAWFLAG_NORMAL);
922 }
923
924 vector Scoreboard_DrawOthers(vector item_pos, vector rgb, int this_team, entity ignored_pl, entity pl, int pl_number)
925 {
926         int i = 0;
927         vector h_pos = item_pos;
928         vector h_size = vec2(panel_size.x, hud_fontsize.y * 1.25);
929
930         bool complete = (this_team == NUM_SPECTATOR);
931
932         if(!complete)
933         if((sbt_highlight) && (!(pl_number % 2)))
934                 drawfill(h_pos, h_size, rgb, sbt_highlight_alpha, DRAWFLAG_NORMAL);
935
936         vector pos = item_pos;
937         pos.x += hud_fontsize.x * 0.5;
938         pos.y += (1.25 - 1) / 2 * hud_fontsize.y; // center text vertically
939
940         float width_limit = item_pos.x + panel_size.x - hud_fontsize.x;
941         if(!complete)
942                 width_limit -= stringwidth("...", false, hud_fontsize);
943         float namesize = autocvar_hud_panel_scoreboard_namesize * hud_fontsize.x;
944         static float max_name_width = 0;
945         string field = "";
946         float fieldsize = 0;
947         float min_fieldsize = 0;
948         float fieldpadding = hud_fontsize.x * 0.25;
949         if(this_team == NUM_SPECTATOR)
950         {
951                 if(autocvar_hud_panel_scoreboard_spectators_showping)
952                         min_fieldsize = stringwidth("999", false, hud_fontsize);
953         }
954         else if(autocvar_hud_panel_scoreboard_others_showscore)
955                 min_fieldsize = stringwidth("99", false, hud_fontsize);
956         for(i = 0; pl; pl = pl.sort_next)
957         {
958                 if(pl.team != this_team)
959                         continue;
960                 if(pl == ignored_pl)
961                         continue;
962
963                 field = "";
964                 if(this_team == NUM_SPECTATOR)
965                 {
966                         if(autocvar_hud_panel_scoreboard_spectators_showping)
967                                 field = Scoreboard_GetField(pl, SP_PING);
968                 }
969                 else if(autocvar_hud_panel_scoreboard_others_showscore)
970                         field = Scoreboard_GetField(pl, SP_SCORE);
971
972                 string str = textShortenToWidth(entcs_GetName(pl.sv_entnum), namesize, hud_fontsize, stringwidth_colors);
973                 float column_width = stringwidth(str, true, hud_fontsize);
974                 if((this_team == NUM_SPECTATOR) && autocvar_hud_panel_scoreboard_spectators_aligned)
975                 {
976                         if(column_width > max_name_width)
977                                 max_name_width = column_width;
978                         column_width = max_name_width;
979                 }
980                 if(field != "")
981                 {
982                         fieldsize = stringwidth(field, false, hud_fontsize);
983                         column_width += hud_fontsize.x * 0.25 + max(fieldsize, min_fieldsize) + 2 * fieldpadding;
984                 }
985
986                 if(pos.x + column_width > width_limit)
987                 {
988                         ++i;
989                         if(!complete)
990                         {
991                                 drawstring(pos, "...", hud_fontsize, '1 1 1', sbt_fg_alpha, DRAWFLAG_NORMAL);
992                                 break;
993                         }
994                         else
995                         {
996                                 pos.x = item_pos.x + hud_fontsize.x * 0.5;
997                                 pos.y += hud_fontsize.y * 1.25;
998                         }
999                 }
1000
1001                 vector name_pos = pos;
1002                 if((this_team == NUM_SPECTATOR) && autocvar_hud_panel_scoreboard_spectators_aligned)
1003                         name_pos.x += max(fieldsize, min_fieldsize) + 2 * fieldpadding + hud_fontsize.x * 0.25;
1004                 drawcolorcodedstring(name_pos, str, hud_fontsize, sbt_fg_alpha, DRAWFLAG_NORMAL);
1005                 if(field != "")
1006                 {
1007                         h_size.x = max(fieldsize, min_fieldsize) + 2 * fieldpadding;
1008                         h_size.y = hud_fontsize.y;
1009                         vector field_pos = pos;
1010                         if(!((this_team == NUM_SPECTATOR) && autocvar_hud_panel_scoreboard_spectators_aligned))
1011                                 field_pos.x += column_width - h_size.x;
1012                         if(sbt_highlight)
1013                                 drawfill(field_pos, h_size, '1 1 1', sbt_highlight_alpha, DRAWFLAG_NORMAL);
1014                         field_pos.x += fieldpadding + (max(fieldsize, min_fieldsize) - fieldsize) * 0.5;
1015                         drawstring(field_pos, field, hud_fontsize, sbt_field_rgb, sbt_fg_alpha, DRAWFLAG_NORMAL);
1016                 }
1017                 if(pl.eliminated)
1018                 {
1019                         h_size.x = column_width + hud_fontsize.x * 0.25;
1020                         h_size.y = hud_fontsize.y;
1021                         drawfill(pos - hud_fontsize.x * 0.25 * eX, h_size, '0 0 0', 0.5 * panel_fg_alpha, DRAWFLAG_NORMAL);
1022                 }
1023                 pos.x += column_width;
1024                 pos.x += hud_fontsize.x;
1025         }
1026         return vec2(item_pos.x, item_pos.y + i * hud_fontsize.y * 1.25);
1027 }
1028
1029 vector Scoreboard_MakeTable(vector pos, entity tm, vector rgb, vector bg_size)
1030 {
1031         int max_players = 999;
1032         if(autocvar_hud_panel_scoreboard_maxheight > 0)
1033         {
1034                 float height = autocvar_hud_panel_scoreboard_maxheight * vid_conheight;
1035                 if(teamplay)
1036                 {
1037                         height -= (panel_bg_padding * 2 + hud_fontsize.y * 1.25) * team_count; // - padding and header
1038                         height -= hud_fontsize.y * (team_count - 1); // - spacing between tables
1039                         height /= team_count;
1040                 }
1041                 else
1042                         height -= panel_bg_padding * 2; // - padding
1043                 max_players = floor(height / (hud_fontsize.y * 1.25));
1044                 if(max_players <= 1)
1045                         max_players = 1;
1046                 if(max_players == tm.team_size)
1047                         max_players = 999;
1048         }
1049
1050         entity pl;
1051         entity me = playerslots[current_player];
1052         panel_pos = pos;
1053         panel_size.y = 1.25 * hud_fontsize.y * (1 + bound(1, tm.team_size, max_players));
1054         panel_size.y += panel_bg_padding * 2;
1055         HUD_Panel_DrawBg();
1056
1057         vector end_pos = panel_pos + eY * (panel_size.y + 0.5* hud_fontsize.y);
1058         if(panel.current_panel_bg != "0")
1059                 end_pos.y += panel_bg_border * 2;
1060
1061         if(panel_bg_padding)
1062         {
1063                 panel_pos += '1 1 0' * panel_bg_padding;
1064                 panel_size -= '2 2 0' * panel_bg_padding;
1065         }
1066
1067         pos = panel_pos;
1068         vector tmp = vec2(panel_size.x, 1.25 * hud_fontsize.y);
1069
1070         // rounded header
1071         if (sbt_bg_alpha)
1072                 drawpic(pos, "gfx/scoreboard/scoreboard_tableheader", tmp, rgb + '0.5 0.5 0.5', sbt_bg_alpha, DRAWFLAG_NORMAL);
1073
1074         pos.y += 1.25 * hud_fontsize.y;
1075
1076         // table background
1077         tmp.y = panel_size.y - 1.25 * hud_fontsize.y;
1078         if (sbt_bg_alpha)
1079                 drawpic_tiled(pos, "gfx/scoreboard/scoreboard_bg", bg_size, tmp, rgb, sbt_bg_alpha, DRAWFLAG_NORMAL);
1080
1081
1082         // print header row and highlight columns
1083         pos = Scoreboard_DrawHeader(panel_pos, rgb, (max_players < tm.team_size));
1084
1085         // fill the table and draw the rows
1086         bool is_self = false;
1087         bool self_shown = false;
1088         int i = 0;
1089         for(pl = players.sort_next; pl; pl = pl.sort_next)
1090         {
1091                 if(pl.team != tm.team)
1092                         continue;
1093                 if(i == max_players - 2 && pl != me)
1094                 {
1095                         if(!self_shown && me.team == tm.team)
1096                         {
1097                                 Scoreboard_DrawItem(pos, rgb, me, true, i);
1098                                 self_shown = true;
1099                                 pos.y += 1.25 * hud_fontsize.y;
1100                                 ++i;
1101                         }
1102                 }
1103                 if(i >= max_players - 1)
1104                 {
1105                         pos = Scoreboard_DrawOthers(pos, rgb, tm.team, (self_shown ? me : NULL), pl, i);
1106                         break;
1107                 }
1108                 is_self = (pl.sv_entnum == current_player);
1109                 Scoreboard_DrawItem(pos, rgb, pl, is_self, i);
1110                 if(is_self)
1111                         self_shown = true;
1112                 pos.y += 1.25 * hud_fontsize.y;
1113                 ++i;
1114         }
1115
1116         panel_size.x += panel_bg_padding * 2; // restore initial width
1117         return end_pos;
1118 }
1119
1120 bool Scoreboard_WouldDraw()
1121 {
1122         if (MUTATOR_CALLHOOK(DrawScoreboard))
1123                 return false;
1124         else if (QuickMenu_IsOpened())
1125                 return false;
1126         else if (HUD_Radar_Clickable())
1127                 return false;
1128         else if (scoreboard_showscores)
1129                 return true;
1130         else if (intermission == 1)
1131                 return true;
1132         else if (intermission == 2)
1133                 return false;
1134         else if (spectatee_status != -1 && STAT(HEALTH) <= 0 && autocvar_cl_deathscoreboard && !ISGAMETYPE(CTS)
1135                 && (!HUD_MinigameMenu_IsOpened() || !active_minigame))
1136         {
1137                 return true;
1138         }
1139         else if (scoreboard_showscores_force)
1140                 return true;
1141         return false;
1142 }
1143
1144 float average_accuracy;
1145 vector Scoreboard_AccuracyStats_Draw(vector pos, vector rgb, vector bg_size)
1146 {
1147         if (frametime)
1148         {
1149                 if (scoreboard_fade_alpha == 1)
1150                         scoreboard_acc_fade_alpha = min(1, scoreboard_acc_fade_alpha + frametime * 10);
1151                 else
1152                         scoreboard_acc_fade_alpha = 1; // sync fading with the scoreboard
1153         }
1154         vector initial_pos = pos;
1155
1156         WepSet weapons_stat = WepSet_GetFromStat();
1157         WepSet weapons_inmap = WepSet_GetFromStat_InMap();
1158         int disownedcnt = 0;
1159         int nHidden = 0;
1160         FOREACH(Weapons, it != WEP_Null, {
1161                 int weapon_stats = weapon_accuracy[i - WEP_FIRST];
1162
1163                 WepSet set = it.m_wepset;
1164                 if(it.spawnflags & WEP_TYPE_OTHER)
1165                 {
1166                         ++nHidden;
1167                         continue;
1168                 }
1169                 if (weapon_stats < 0 && !((weapons_stat & set) || (weapons_inmap & set)))
1170                 {
1171                         if (((it.spawnflags & WEP_FLAG_HIDDEN) || (it.spawnflags & WEP_FLAG_MUTATORBLOCKED)))
1172                                 ++nHidden;
1173                         else
1174                                 ++disownedcnt;
1175                 }
1176         });
1177
1178         int weapon_cnt = (Weapons_COUNT - 1) - disownedcnt - nHidden;
1179         if (weapon_cnt <= 0) return pos;
1180
1181         int rows = 1;
1182         if (autocvar_hud_panel_scoreboard_accuracy_doublerows && weapon_cnt >= floor((Weapons_COUNT - nHidden - 1) * 0.5))
1183                 rows = 2;
1184         int columnns = ceil(weapon_cnt / rows);
1185
1186         float weapon_height = 29;
1187         float height = hud_fontsize.y + weapon_height;
1188
1189         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);
1190         pos.y += 1.25 * hud_fontsize.y;
1191         if(panel.current_panel_bg != "0")
1192                 pos.y += panel_bg_border;
1193
1194         panel_pos = pos;
1195         panel_size.y = height * rows;
1196         panel_size.y += panel_bg_padding * 2;
1197
1198         float panel_bg_alpha_save = panel_bg_alpha;
1199         panel_bg_alpha *= scoreboard_acc_fade_alpha;
1200         HUD_Panel_DrawBg();
1201         panel_bg_alpha = panel_bg_alpha_save;
1202
1203         vector end_pos = panel_pos + eY * (panel_size.y + hud_fontsize.y);
1204         if(panel.current_panel_bg != "0")
1205                 end_pos.y += panel_bg_border * 2;
1206
1207         if(panel_bg_padding)
1208         {
1209                 panel_pos += '1 1 0' * panel_bg_padding;
1210                 panel_size -= '2 2 0' * panel_bg_padding;
1211         }
1212
1213         pos = panel_pos;
1214         vector tmp = panel_size;
1215
1216         float weapon_width = tmp.x / columnns / rows;
1217
1218         if (sbt_bg_alpha)
1219                 drawpic_tiled(pos, "gfx/scoreboard/scoreboard_bg", bg_size, tmp, rgb, sbt_bg_alpha * scoreboard_acc_fade_alpha, DRAWFLAG_NORMAL);
1220
1221         if(sbt_highlight)
1222         {
1223                 // column highlighting
1224                 for (int i = 0; i < columnns; ++i)
1225                         if ((i % 2) == 0)
1226                                 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);
1227
1228                 // row highlighting
1229                 for (int i = 0; i < rows; ++i)
1230                         drawfill(pos + eY * (weapon_height + height * i), vec2(tmp.x, hud_fontsize.y), rgb, sbt_highlight_alpha * scoreboard_acc_fade_alpha, DRAWFLAG_NORMAL);
1231         }
1232
1233         average_accuracy = 0;
1234         int weapons_with_stats = 0;
1235         if (rows == 2)
1236                 pos.x += weapon_width / 2;
1237
1238         if (autocvar_hud_panel_scoreboard_accuracy_nocolors)
1239                 rgb = '1 1 1';
1240         else
1241                 Accuracy_LoadColors();
1242
1243         float oldposx = pos.x;
1244         vector tmpos = pos;
1245
1246         int column = 0;
1247         FOREACH(Weapons, it != WEP_Null, {
1248                 int weapon_stats = weapon_accuracy[i - WEP_FIRST];
1249
1250                 WepSet set = it.m_wepset;
1251                 if (weapon_stats < 0 && !((weapons_stat & set) || (weapons_inmap & set)))
1252                         continue;
1253                 if (it.spawnflags & WEP_TYPE_OTHER)
1254                         continue;
1255
1256                 float weapon_alpha;
1257                 if (weapon_stats >= 0)
1258                         weapon_alpha = sbt_fg_alpha;
1259                 else
1260                         weapon_alpha = 0.2 * sbt_fg_alpha;
1261
1262                 // weapon icon
1263                 drawpic_aspect_skin(tmpos, it.model2, vec2(weapon_width, weapon_height), '1 1 1', weapon_alpha * scoreboard_acc_fade_alpha, DRAWFLAG_NORMAL);
1264                 // the accuracy
1265                 if (weapon_stats >= 0) {
1266                         weapons_with_stats += 1;
1267                         average_accuracy += weapon_stats; // store sum of all accuracies in average_accuracy
1268
1269                         string s;
1270                         s = sprintf("%d%%", weapon_stats * 100);
1271
1272                         float padding;
1273                         padding = (weapon_width - stringwidth(s, false, hud_fontsize)) / 2; // center the accuracy value
1274
1275                         if(!autocvar_hud_panel_scoreboard_accuracy_nocolors)
1276                                 rgb = Accuracy_GetColor(weapon_stats);
1277
1278                         drawstring(tmpos + vec2(padding, weapon_height), s, hud_fontsize, rgb, sbt_fg_alpha * scoreboard_acc_fade_alpha, DRAWFLAG_NORMAL);
1279                 }
1280                 tmpos.x += weapon_width * rows;
1281                 pos.x += weapon_width * rows;
1282                 if (rows == 2 && column == columnns - 1) {
1283                         tmpos.x = oldposx;
1284                         tmpos.y += height;
1285                         pos.y += height;
1286                 }
1287                 ++column;
1288         });
1289
1290         if (weapons_with_stats)
1291                 average_accuracy = floor((average_accuracy * 100 / weapons_with_stats) + 0.5);
1292
1293         panel_size.x += panel_bg_padding * 2; // restore initial width
1294
1295         if (scoreboard_acc_fade_alpha == 1)
1296                 return end_pos;
1297         return initial_pos + (end_pos - initial_pos) * scoreboard_acc_fade_alpha;
1298 }
1299
1300 vector Scoreboard_ItemStats_Draw(vector pos, vector rgb, vector bg_size)
1301 {
1302         float scoreboard_acc_fade_alpha_save = scoreboard_acc_fade_alpha; // debug
1303         scoreboard_acc_fade_alpha = 1; // debug: make Item Stats always visible
1304
1305         float initial_posx = pos.x;
1306         int disownedcnt = 0;
1307         FOREACH(Items, true, {
1308                 int q = g_inventory.inv_items[it.m_id];
1309                 //q = 1; // debug: display all items
1310                 if (!q) ++disownedcnt;
1311         });
1312
1313         int n = Items_COUNT - disownedcnt;
1314         if (n <= 0) return pos;
1315
1316         int rows = (autocvar_hud_panel_scoreboard_accuracy_doublerows && n >= floor(Items_COUNT / 2)) ? 2 : 1;
1317         int columnns = ceil(n / rows);
1318
1319         float height = 40;
1320         float fontsize = height * 1/3;
1321         float item_height = height * 2/3;
1322
1323         drawstring(pos, _("Item stats"), hud_fontsize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
1324         pos.y += 1.25 * hud_fontsize.y;
1325         if(panel.current_panel_bg != "0")
1326                 pos.y += panel_bg_border;
1327
1328         panel_pos = pos;
1329         panel_size.y = height * rows;
1330         panel_size.y += panel_bg_padding * 2;
1331
1332         float panel_bg_alpha_save = panel_bg_alpha;
1333         panel_bg_alpha *= scoreboard_acc_fade_alpha;
1334         HUD_Panel_DrawBg();
1335         panel_bg_alpha = panel_bg_alpha_save;
1336
1337         vector end_pos = panel_pos + eY * (panel_size.y + hud_fontsize.y);
1338         if(panel.current_panel_bg != "0")
1339                 end_pos.y += panel_bg_border * 2;
1340
1341         if(panel_bg_padding)
1342         {
1343                 panel_pos += '1 1 0' * panel_bg_padding;
1344                 panel_size -= '2 2 0' * panel_bg_padding;
1345         }
1346
1347         pos = panel_pos;
1348         vector tmp = panel_size;
1349
1350         float item_width = tmp.x / columnns / rows;
1351
1352         if (sbt_bg_alpha)
1353                 drawpic_tiled(pos, "gfx/scoreboard/scoreboard_bg", bg_size, tmp, rgb, sbt_bg_alpha * scoreboard_acc_fade_alpha, DRAWFLAG_NORMAL);
1354
1355         if(sbt_highlight)
1356         {
1357                 // column highlighting
1358                 for (int i = 0; i < columnns; ++i)
1359                         if ((i % 2) == 0)
1360                                 drawfill(pos + '1 0 0' * item_width * rows * i, '0 1 0' * height * rows + '1 0 0' * item_width * rows, '0 0 0', panel_bg_alpha * 0.2, DRAWFLAG_NORMAL);
1361
1362                 // row highlighting
1363                 for (int i = 0; i < rows; ++i)
1364                         drawfill(pos + '0 1 0' * item_height + '0 1 0' * height * i, '1 0 0' * panel_size.x + '0 1 0' * fontsize, '1 1 1', sbt_highlight_alpha, DRAWFLAG_NORMAL);
1365         }
1366
1367         if (rows == 2)
1368                 pos.x += item_width / 2;
1369
1370         float oldposx = pos.x;
1371         vector tmpos = pos;
1372
1373         int column = 0;
1374         FOREACH(Items, true, {
1375                 int n = g_inventory.inv_items[it.m_id];
1376                 //n = 1 + floor(i * 3 + 4.8) % 7; // debug: display a value for each item
1377                 if (n <= 0) continue;
1378                 drawpic_aspect_skin(tmpos, it.m_icon, '1 0 0' * item_width + '0 1 0' * item_height, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
1379                 string s = ftos(n);
1380                 float padding = (item_width - stringwidth(s, false, '1 0 0' * fontsize)) / 2; // center
1381                 drawstring(tmpos + '1 0 0' * padding + '0 1 0' * item_height, s, '1 1 0' * fontsize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
1382                 tmpos.x += item_width * rows;
1383                 pos.x += item_width * rows;
1384                 if (rows == 2 && column == columnns - 1) {
1385                         tmpos.x = oldposx;
1386                         tmpos.y += height;
1387                         pos.y += height;
1388                 }
1389                 ++column;
1390         });
1391         pos.y += height;
1392         pos.y += 1.25 * hud_fontsize.y;
1393         pos.x = initial_posx;
1394
1395         panel_size.x += panel_bg_padding * 2; // restore initial width
1396
1397         scoreboard_acc_fade_alpha = scoreboard_acc_fade_alpha_save; // debug
1398         return pos;
1399 }
1400
1401 vector MapStats_DrawKeyValue(vector pos, string key, string value) {
1402         float px = pos.x;
1403         pos.x += hud_fontsize.x * 0.25;
1404         drawstring(pos, key, hud_fontsize, '1 1 1', sbt_fg_alpha, DRAWFLAG_NORMAL);
1405         pos.x = panel_pos.x + panel_size.x - stringwidth(value, false, hud_fontsize) - hud_fontsize.x * 0.25;
1406         drawstring(pos, value, hud_fontsize, '1 1 1', sbt_fg_alpha, DRAWFLAG_NORMAL);
1407         pos.x = px;
1408         pos.y += hud_fontsize.y;
1409
1410         return pos;
1411 }
1412
1413 vector Scoreboard_MapStats_Draw(vector pos, vector rgb, vector bg_size) {
1414         float stat_secrets_found, stat_secrets_total;
1415         float stat_monsters_killed, stat_monsters_total;
1416         float rows = 0;
1417         string val;
1418
1419         // get monster stats
1420         stat_monsters_killed = STAT(MONSTERS_KILLED);
1421         stat_monsters_total = STAT(MONSTERS_TOTAL);
1422
1423         // get secrets stats
1424         stat_secrets_found = STAT(SECRETS_FOUND);
1425         stat_secrets_total = STAT(SECRETS_TOTAL);
1426
1427         // get number of rows
1428         if(stat_secrets_total)
1429                 rows += 1;
1430         if(stat_monsters_total)
1431                 rows += 1;
1432
1433         // if no rows, return
1434         if (!rows)
1435                 return pos;
1436
1437         //  draw table header
1438         drawstring(pos + eX * panel_bg_padding, _("Map stats:"), hud_fontsize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
1439         pos.y += 1.25 * hud_fontsize.y;
1440         if(panel.current_panel_bg != "0")
1441                 pos.y += panel_bg_border;
1442
1443         panel_pos = pos;
1444         panel_size.y = hud_fontsize.y * rows;
1445         panel_size.y += panel_bg_padding * 2;
1446         HUD_Panel_DrawBg();
1447
1448         vector end_pos = panel_pos + eY * (panel_size.y + hud_fontsize.y);
1449         if(panel.current_panel_bg != "0")
1450                 end_pos.y += panel_bg_border * 2;
1451
1452         if(panel_bg_padding)
1453         {
1454                 panel_pos += '1 1 0' * panel_bg_padding;
1455                 panel_size -= '2 2 0' * panel_bg_padding;
1456         }
1457
1458         pos = panel_pos;
1459         vector tmp = panel_size;
1460
1461         if (sbt_bg_alpha)
1462                 drawpic_tiled(pos, "gfx/scoreboard/scoreboard_bg", bg_size, tmp, rgb, sbt_bg_alpha, DRAWFLAG_NORMAL);
1463
1464         // draw monsters
1465         if(stat_monsters_total)
1466         {
1467                 val = sprintf("%d/%d", stat_monsters_killed, stat_monsters_total);
1468                 pos = MapStats_DrawKeyValue(pos, _("Monsters killed:"), val);
1469         }
1470
1471         // draw secrets
1472         if(stat_secrets_total)
1473         {
1474                 val = sprintf("%d/%d", stat_secrets_found, stat_secrets_total);
1475                 pos = MapStats_DrawKeyValue(pos, _("Secrets found:"), val);
1476         }
1477
1478         panel_size.x += panel_bg_padding * 2; // restore initial width
1479         return end_pos;
1480 }
1481
1482
1483 vector Scoreboard_Rankings_Draw(vector pos, entity pl, vector rgb, vector bg_size)
1484 {
1485         int i;
1486         RANKINGS_RECEIVED_CNT = 0;
1487         for (i=RANKINGS_CNT-1; i>=0; --i)
1488                 if (grecordtime[i])
1489                         ++RANKINGS_RECEIVED_CNT;
1490
1491         if (RANKINGS_RECEIVED_CNT == 0)
1492                 return pos;
1493
1494         vector hl_rgb = rgb + '0.5 0.5 0.5';
1495
1496         pos.y += hud_fontsize.y;
1497         drawstring(pos + eX * panel_bg_padding, ((ISGAMETYPE(CTF)) ? _("Capture time rankings") : _("Rankings")), hud_fontsize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
1498         pos.y += 1.25 * hud_fontsize.y;
1499         if(panel.current_panel_bg != "0")
1500                 pos.y += panel_bg_border;
1501
1502         panel_pos = pos;
1503
1504         float namesize = 0;
1505         for(i = 0; i < RANKINGS_RECEIVED_CNT; ++i)
1506         {
1507                 float f = stringwidth(ColorTranslateRGB(grecordholder[i]), true, hud_fontsize);
1508                 if(f > namesize)
1509                         namesize = f;
1510         }
1511         bool cut = false;
1512         if(namesize > autocvar_hud_panel_scoreboard_namesize * hud_fontsize.x)
1513         {
1514                 namesize = autocvar_hud_panel_scoreboard_namesize * hud_fontsize.x;
1515                 cut = true;
1516         }
1517
1518         float ranksize = 3 * hud_fontsize.x;
1519         float timesize = 5 * hud_fontsize.x;
1520         vector columnsize = vec2(ranksize + timesize + namesize + hud_fontsize.x, 1.25 * hud_fontsize.y);
1521         int columns = max(1, floor((panel_size.x - 2 * panel_bg_padding) / columnsize.x));
1522         columns = min(columns, RANKINGS_RECEIVED_CNT);
1523
1524         // expand name column to fill the entire row
1525         float available_space = (panel_size.x - 2 * panel_bg_padding - columnsize.x * columns) / columns;
1526         namesize += available_space;
1527         columnsize.x += available_space;
1528
1529         panel_size.y = ceil(RANKINGS_RECEIVED_CNT / columns) * 1.25 * hud_fontsize.y;
1530         panel_size.y += panel_bg_padding * 2;
1531
1532         HUD_Panel_DrawBg();
1533
1534         vector end_pos = panel_pos + eY * (panel_size.y + hud_fontsize.y);
1535         if(panel.current_panel_bg != "0")
1536                 end_pos.y += panel_bg_border * 2;
1537
1538         if(panel_bg_padding)
1539         {
1540                 panel_pos += '1 1 0' * panel_bg_padding;
1541                 panel_size -= '2 2 0' * panel_bg_padding;
1542         }
1543
1544         pos = panel_pos;
1545
1546         if (sbt_bg_alpha)
1547                 drawpic_tiled(pos, "gfx/scoreboard/scoreboard_bg", bg_size, panel_size, rgb, sbt_bg_alpha, DRAWFLAG_NORMAL);
1548
1549         vector text_ofs = vec2(0.5 * hud_fontsize.x, (1.25 - 1) / 2 * hud_fontsize.y); // center text vertically
1550         string str = "";
1551         int column = 0, j = 0;
1552         string zoned_name_self = strzone(strdecolorize(entcs_GetName(player_localnum)));
1553         for(i = 0; i < RANKINGS_RECEIVED_CNT; ++i)
1554         {
1555                 float t;
1556                 t = grecordtime[i];
1557                 if (t == 0)
1558                         continue;
1559
1560                 if(strdecolorize(grecordholder[i]) == zoned_name_self)
1561                         drawfill(pos, columnsize, hl_rgb, sbt_highlight_alpha_self, DRAWFLAG_NORMAL);
1562                 else if(!((j + column) & 1) && sbt_highlight)
1563                         drawfill(pos, columnsize, hl_rgb, sbt_highlight_alpha, DRAWFLAG_NORMAL);
1564
1565                 str = count_ordinal(i+1);
1566                 drawstring(pos + text_ofs, str, hud_fontsize, '1 1 1', sbt_fg_alpha, DRAWFLAG_NORMAL);
1567                 drawstring(pos + text_ofs + eX * ranksize, TIME_ENCODED_TOSTRING(t), hud_fontsize, '1 1 1', sbt_fg_alpha, DRAWFLAG_NORMAL);
1568                 str = ColorTranslateRGB(grecordholder[i]);
1569                 if(cut)
1570                         str = textShortenToWidth(str, namesize, hud_fontsize, stringwidth_colors);
1571                 drawcolorcodedstring(pos + text_ofs + eX * (ranksize + timesize), str, hud_fontsize, sbt_fg_alpha, DRAWFLAG_NORMAL);
1572
1573                 pos.y += 1.25 * hud_fontsize.y;
1574                 j++;
1575                 if(j >= ceil(RANKINGS_RECEIVED_CNT / columns))
1576                 {
1577                         column++;
1578                         j = 0;
1579                         pos.x += panel_size.x / columns;
1580                         pos.y = panel_pos.y;
1581                 }
1582         }
1583         strfree(zoned_name_self);
1584
1585         panel_size.x += panel_bg_padding * 2; // restore initial width
1586         return end_pos;
1587 }
1588
1589 float scoreboard_time;
1590 bool have_weapon_stats;
1591 bool Scoreboard_AccuracyStats_WouldDraw(float ypos)
1592 {
1593         if (ISGAMETYPE(CTS) || ISGAMETYPE(RACE) || ISGAMETYPE(NEXBALL))
1594                 return false;
1595         if (!autocvar_hud_panel_scoreboard_accuracy || warmup_stage || ypos > 0.91 * vid_conheight)
1596                 return false;
1597
1598         if (time < scoreboard_time + autocvar_hud_panel_scoreboard_accuracy_showdelay
1599                 && ypos > autocvar_hud_panel_scoreboard_accuracy_showdelay_minpos * vid_conheight
1600                 && !intermission)
1601         {
1602                 return false;
1603         }
1604
1605         if (!have_weapon_stats)
1606         {
1607                 FOREACH(Weapons, it != WEP_Null, {
1608                         int weapon_stats = weapon_accuracy[i - WEP_FIRST];
1609                         if (weapon_stats >= 0)
1610                         {
1611                                 have_weapon_stats = true;
1612                                 break;
1613                         }
1614                 });
1615                 if (!have_weapon_stats)
1616                         return false;
1617         }
1618
1619         return true;
1620 }
1621
1622 void Scoreboard_Draw()
1623 {
1624         if(!autocvar__hud_configure)
1625         {
1626                 if(!hud_draw_maximized) return;
1627
1628                 // frametime checks allow to toggle the scoreboard even when the game is paused
1629                 if(scoreboard_active) {
1630                         if (scoreboard_fade_alpha < 1)
1631                                 scoreboard_time = time;
1632                         if(hud_configure_menu_open == 1)
1633                                 scoreboard_fade_alpha = 1;
1634                         float scoreboard_fadeinspeed = autocvar_hud_panel_scoreboard_fadeinspeed;
1635                         if (scoreboard_fadeinspeed && frametime)
1636                                 scoreboard_fade_alpha = min(1, scoreboard_fade_alpha + frametime * scoreboard_fadeinspeed);
1637                         else
1638                                 scoreboard_fade_alpha = 1;
1639                         if(hud_fontsize_str != autocvar_hud_fontsize)
1640                         {
1641                                 hud_fontsize = HUD_GetFontsize("hud_fontsize");
1642                                 Scoreboard_initFieldSizes();
1643                                 strcpy(hud_fontsize_str, autocvar_hud_fontsize);
1644                         }
1645                 }
1646                 else {
1647                         float scoreboard_fadeoutspeed = autocvar_hud_panel_scoreboard_fadeoutspeed;
1648                         if (scoreboard_fadeoutspeed && frametime)
1649                                 scoreboard_fade_alpha = max(0, scoreboard_fade_alpha - frametime * scoreboard_fadeoutspeed);
1650                         else
1651                                 scoreboard_fade_alpha = 0;
1652                 }
1653
1654                 if (!scoreboard_fade_alpha)
1655                 {
1656                         scoreboard_acc_fade_alpha = 0;
1657                         return;
1658                 }
1659         }
1660         else
1661                 scoreboard_fade_alpha = 0;
1662
1663         if (autocvar_hud_panel_scoreboard_dynamichud)
1664                 HUD_Scale_Enable();
1665         else
1666                 HUD_Scale_Disable();
1667
1668         if(scoreboard_fade_alpha <= 0)
1669                 return;
1670         panel_fade_alpha *= scoreboard_fade_alpha;
1671         HUD_Panel_LoadCvars();
1672
1673         sbt_bg_alpha = autocvar_hud_panel_scoreboard_table_bg_alpha * panel_fg_alpha;
1674         sbt_highlight = autocvar_hud_panel_scoreboard_table_highlight;
1675         sbt_highlight_alpha = autocvar_hud_panel_scoreboard_table_highlight_alpha * panel_fg_alpha;
1676         sbt_highlight_alpha_self = autocvar_hud_panel_scoreboard_table_highlight_alpha_self * panel_fg_alpha;
1677         sbt_fg_alpha = autocvar_hud_panel_scoreboard_table_fg_alpha * panel_fg_alpha;
1678         sbt_fg_alpha_self = autocvar_hud_panel_scoreboard_table_fg_alpha_self * panel_fg_alpha;
1679
1680         // don't overlap with con_notify
1681         if(!autocvar__hud_configure)
1682                 panel_pos.y = max((autocvar_con_notify * autocvar_con_notifysize), panel_pos.y);
1683
1684         float excess = max(0, max_namesize - autocvar_hud_panel_scoreboard_namesize * hud_fontsize.x);
1685         float fixed_scoreboard_width = bound(vid_conwidth * autocvar_hud_panel_scoreboard_minwidth, vid_conwidth - excess, vid_conwidth * 0.93);
1686         panel_pos.x = 0.5 * (vid_conwidth - fixed_scoreboard_width);
1687         panel_size.x = fixed_scoreboard_width;
1688
1689         Scoreboard_UpdatePlayerTeams();
1690
1691         vector pos = panel_pos;
1692         entity pl, tm;
1693         string str;
1694         vector str_pos;
1695
1696         vector sb_gameinfo_type_fontsize, sb_gameinfo_detail_fontsize;
1697
1698         // Begin of Game Info Section
1699         sb_gameinfo_type_fontsize = hud_fontsize * 2.5;
1700         sb_gameinfo_detail_fontsize = hud_fontsize * 1.3;
1701
1702         // Game Info: Game Type
1703         str = MapInfo_Type_ToText(gametype);
1704         draw_beginBoldFont();
1705         drawcolorcodedstring(pos + '0.5 0 0' * (panel_size.x - stringwidth(str, true, sb_gameinfo_type_fontsize)), str, sb_gameinfo_type_fontsize, panel_fg_alpha, DRAWFLAG_NORMAL);
1706         draw_endBoldFont();
1707
1708         // Game Info: Game Detail
1709         float tl, fl, ll;
1710         str = ""; // optionally "^7Limits: "
1711         tl = STAT(TIMELIMIT);
1712         fl = STAT(FRAGLIMIT);
1713         ll = STAT(LEADLIMIT);
1714         if(tl > 0)
1715                 str = strcat(str, sprintf(_("^3%1.0f minutes"), tl));
1716         if(!ISGAMETYPE(LMS))
1717         {
1718                 if(fl > 0)
1719                 {
1720                         if(tl > 0)
1721                                 str = strcat(str, "^7 / "); // delimiter
1722                         if(teamplay)
1723                         {
1724                                 str = strcat(str, sprintf(_("^5%s %s"), ScoreString(teamscores_flags(ts_primary), fl),
1725                                         (teamscores_label(ts_primary) == "score")   ? CTX(_("SCO^points")) :
1726                                         (teamscores_label(ts_primary) == "fastest") ? CTX(_("SCO^is beaten")) :
1727                                         TranslateScoresLabel(teamscores_label(ts_primary))));
1728                         }
1729                         else
1730                         {
1731                                 str = strcat(str, sprintf(_("^5%s %s"), ScoreString(scores_flags(ps_primary), fl),
1732                                         (scores_label(ps_primary) == "score")   ? CTX(_("SCO^points")) :
1733                                         (scores_label(ps_primary) == "fastest") ? CTX(_("SCO^is beaten")) :
1734                                         TranslateScoresLabel(scores_label(ps_primary))));
1735                         }
1736                 }
1737                 if(ll > 0)
1738                 {
1739                         if(tl > 0 || fl > 0)
1740                                 str = strcat(str, "^7 / "); // delimiter
1741                         if(teamplay)
1742                         {
1743                                 str = strcat(str, sprintf(_("^2+%s %s"), ScoreString(teamscores_flags(ts_primary), ll),
1744                                         (teamscores_label(ts_primary) == "score")   ? CTX(_("SCO^points")) :
1745                                         (teamscores_label(ts_primary) == "fastest") ? CTX(_("SCO^is beaten")) :
1746                                         TranslateScoresLabel(teamscores_label(ts_primary))));
1747                         }
1748                         else
1749                         {
1750                                 str = strcat(str, sprintf(_("^2+%s %s"), ScoreString(scores_flags(ps_primary), ll),
1751                                         (scores_label(ps_primary) == "score")   ? CTX(_("SCO^points")) :
1752                                         (scores_label(ps_primary) == "fastest") ? CTX(_("SCO^is beaten")) :
1753                                         TranslateScoresLabel(scores_label(ps_primary))));
1754                         }
1755                 }
1756         }
1757
1758         pos.y += sb_gameinfo_type_fontsize.y;
1759         drawcolorcodedstring(pos + '1 0 0' * (panel_size.x - stringwidth(str, true, sb_gameinfo_detail_fontsize)), str, sb_gameinfo_detail_fontsize, panel_fg_alpha, DRAWFLAG_NORMAL); // align right
1760         // map name
1761         str = sprintf(_("^7Map: ^2%s"), shortmapname);
1762         drawcolorcodedstring(pos, str, sb_gameinfo_detail_fontsize, panel_fg_alpha, DRAWFLAG_NORMAL); // align left
1763         // End of Game Info Section
1764
1765         pos.y += sb_gameinfo_detail_fontsize.y + hud_fontsize.y * 0.3; // space between Game Info Section and score table
1766         if(panel.current_panel_bg != "0")
1767                 pos.y += panel_bg_border;
1768
1769         // Draw the scoreboard
1770         float scale = autocvar_hud_panel_scoreboard_table_bg_scale;
1771         if(scale <= 0)
1772                 scale = 0.25;
1773         vector bg_size = draw_getimagesize("gfx/scoreboard/scoreboard_bg") * scale;
1774
1775         if(teamplay)
1776         {
1777                 vector panel_bg_color_save = panel_bg_color;
1778                 vector team_score_baseoffset;
1779                 vector team_size_baseoffset;
1780                 if (autocvar_hud_panel_scoreboard_team_size_position != 1) // team size not on left
1781                 {
1782                         // put team score to the left of scoreboard (and team size to the right)
1783                         team_score_baseoffset = eY * hud_fontsize.y - eX * hud_fontsize.x * 0.5;
1784                         team_size_baseoffset = eY * hud_fontsize.y + eX * hud_fontsize.x * 0.5;
1785                         if(panel.current_panel_bg != "0")
1786                         {
1787                                 team_score_baseoffset.x -= panel_bg_border;
1788                                 team_size_baseoffset.x += panel_bg_border;
1789                         }
1790                 }
1791                 else
1792                 {
1793                         // put team score to the right of scoreboard (and team size to the left)
1794                         team_score_baseoffset = eY * hud_fontsize.y + eX * hud_fontsize.x * 0.5;
1795                         team_size_baseoffset = eY * hud_fontsize.y - eX * hud_fontsize.x * 0.5;
1796                         if(panel.current_panel_bg != "0")
1797                         {
1798                                 team_score_baseoffset.x += panel_bg_border;
1799                                 team_size_baseoffset.x -= panel_bg_border;
1800                         }
1801                 }
1802
1803                 int team_size_total = 0;
1804                 if (autocvar_hud_panel_scoreboard_team_size_position != 0) // team size not off
1805                 {
1806                         // calculate team size total (sum of all team sizes)
1807                         for(tm = teams.sort_next; tm; tm = tm.sort_next)
1808                                 if(tm.team != NUM_SPECTATOR)
1809                                         team_size_total += tm.team_size;
1810                 }
1811
1812                 for(tm = teams.sort_next; tm; tm = tm.sort_next)
1813                 {
1814                         if(tm.team == NUM_SPECTATOR)
1815                                 continue;
1816                         if(!tm.team)
1817                                 continue;
1818
1819                         draw_beginBoldFont();
1820                         vector rgb = Team_ColorRGB(tm.team);
1821                         str = ftos(tm.(teamscores(ts_primary)));
1822                         if (autocvar_hud_panel_scoreboard_team_size_position != 1) // team size not on left
1823                         {
1824                                 // team score on the left (default)
1825                                 str_pos = pos + team_score_baseoffset - eX * stringwidth(str, false, hud_fontsize * 1.5);
1826                         }
1827                         else
1828                         {
1829                                 // team score on the right
1830                                 str_pos = pos + team_score_baseoffset + eX * (panel_size.x + hud_fontsize.x * 1.5);
1831                         }
1832                         drawstring(str_pos, str, hud_fontsize * 1.5, rgb, panel_fg_alpha, DRAWFLAG_NORMAL);
1833
1834                         // team size (if set to show on the side)
1835                         if (autocvar_hud_panel_scoreboard_team_size_position != 0) // team size not off
1836                         {
1837                                 // calculate the starting position for the whole team size info string
1838                                 str = sprintf("%d/%d", tm.team_size, team_size_total);
1839                                 if (autocvar_hud_panel_scoreboard_team_size_position == 1)
1840                                 {
1841                                         // team size on the left
1842                                         str_pos = pos + team_size_baseoffset - eX * stringwidth(str, false, hud_fontsize * 1.5);
1843                                 }
1844                                 else
1845                                 {
1846                                         // team size on the right
1847                                         str_pos = pos + team_size_baseoffset + eX * (panel_size.x + hud_fontsize.x * 1.5);
1848                                 }
1849                                 str = sprintf("%d", tm.team_size);
1850                                 drawstring(str_pos, str, hud_fontsize * 1.5, rgb, panel_fg_alpha, DRAWFLAG_NORMAL);
1851                                 str_pos += eX * stringwidth(str, true, hud_fontsize * 1.5) + eY * hud_fontsize.y * .5;
1852                                 str = sprintf("/%d", team_size_total);
1853                                 drawstring(str_pos, str, hud_fontsize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
1854                         }
1855
1856
1857                         // secondary score, e.g. keyhunt
1858                         if(ts_primary != ts_secondary)
1859                         {
1860                                 str = ftos(tm.(teamscores(ts_secondary)));
1861                                 if (autocvar_hud_panel_scoreboard_team_size_position != 1) // team size not on left
1862                                 {
1863                                         // left
1864                                         str_pos = pos + team_score_baseoffset - vec2(stringwidth(str, false, hud_fontsize), hud_fontsize.y * -1.5);
1865                                 }
1866                                 else
1867                                 {
1868                                         // right
1869                                         str_pos = pos + team_score_baseoffset + vec2(panel_size.x + hud_fontsize.x * 1.5, hud_fontsize.y * 1.5);
1870                                 }
1871
1872                                 drawstring(str_pos, str, hud_fontsize, rgb, panel_fg_alpha, DRAWFLAG_NORMAL);
1873                         }
1874                         draw_endBoldFont();
1875                         if(autocvar_hud_panel_scoreboard_bg_teams_color_team > 0)
1876                                 panel_bg_color = rgb * autocvar_hud_panel_scoreboard_bg_teams_color_team;
1877                         else if(panel_bg_color_team > 0)
1878                                 panel_bg_color = rgb * panel_bg_color_team;
1879                         else
1880                                 panel_bg_color = rgb;
1881                         pos = Scoreboard_MakeTable(pos, tm, panel_bg_color, bg_size);
1882                 }
1883                 panel_bg_color = panel_bg_color_save;
1884         }
1885         else
1886         {
1887                 for(tm = teams.sort_next; tm; tm = tm.sort_next)
1888                         if(tm.team != NUM_SPECTATOR)
1889                                 break;
1890
1891                 // display it anyway
1892                 pos = Scoreboard_MakeTable(pos, tm, panel_bg_color, bg_size);
1893         }
1894
1895         if (Scoreboard_AccuracyStats_WouldDraw(pos.y))
1896                 pos = Scoreboard_AccuracyStats_Draw(pos, panel_bg_color, bg_size);
1897         pos = Scoreboard_ItemStats_Draw(pos, panel_bg_color, bg_size);
1898
1899         if(ISGAMETYPE(CTS) || ISGAMETYPE(RACE) || (autocvar_hud_panel_scoreboard_ctf_leaderboard && ISGAMETYPE(CTF) && STAT(CTF_SHOWLEADERBOARD))) {
1900                 if(race_speedaward) {
1901                         drawcolorcodedstring(pos, sprintf(_("Speed award: %d%s ^7(%s^7)"), race_speedaward, race_speedaward_unit, ColorTranslateRGB(race_speedaward_holder)), hud_fontsize, panel_fg_alpha, DRAWFLAG_NORMAL);
1902                         pos.y += 1.25 * hud_fontsize.y;
1903                 }
1904                 if(race_speedaward_alltimebest) {
1905                         drawcolorcodedstring(pos, sprintf(_("All-time fastest: %d%s ^7(%s^7)"), race_speedaward_alltimebest, race_speedaward_alltimebest_unit, ColorTranslateRGB(race_speedaward_alltimebest_holder)), hud_fontsize, panel_fg_alpha, DRAWFLAG_NORMAL);
1906                         pos.y += 1.25 * hud_fontsize.y;
1907                 }
1908                 pos = Scoreboard_Rankings_Draw(pos, playerslots[player_localnum], panel_bg_color, bg_size);
1909         }
1910
1911         pos = Scoreboard_MapStats_Draw(pos, panel_bg_color, bg_size);
1912
1913         // List spectators
1914         for(pl = players.sort_next; pl; pl = pl.sort_next)
1915         {
1916                 if(pl.team == NUM_SPECTATOR)
1917                 {
1918                         for(tm = teams.sort_next; tm; tm = tm.sort_next)
1919                                 if(tm.team == NUM_SPECTATOR)
1920                                         break;
1921                         str = sprintf("%s (%d)", _("Spectators"), tm.team_size);
1922                         draw_beginBoldFont();
1923                         drawstring(pos, str, hud_fontsize, '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL);
1924                         draw_endBoldFont();
1925                         pos.y += 1.25 * hud_fontsize.y;
1926
1927                         pos = Scoreboard_DrawOthers(pos, '0 0 0', pl.team, NULL, pl, 0);
1928                         pos.y += 1.25 * hud_fontsize.y;
1929
1930                         break;
1931                 }
1932         }
1933
1934
1935         // print information about respawn status
1936         float respawn_time = STAT(RESPAWN_TIME);
1937         if(!intermission)
1938         if(respawn_time)
1939         {
1940                 if(respawn_time < 0)
1941                 {
1942                         // a negative number means we are awaiting respawn, time value is still the same
1943                         respawn_time *= -1; // remove mark now that we checked it
1944
1945                         if(respawn_time < time) // it happens for a few frames when server is respawning the player
1946                                 str = ""; // draw an empty string to not change suddenly scoreboard_bottom
1947                         else
1948                                 str = sprintf(_("^1Respawning in ^3%s^1..."),
1949                                         (autocvar_hud_panel_scoreboard_respawntime_decimals ?
1950                                                 count_seconds_decs(respawn_time - time, autocvar_hud_panel_scoreboard_respawntime_decimals)
1951                                                 :
1952                                                 count_seconds(ceil(respawn_time - time))
1953                                         )
1954                                 );
1955                 }
1956                 else if(time < respawn_time)
1957                 {
1958                         str = sprintf(_("You are dead, wait ^3%s^7 before respawning"),
1959                                 (autocvar_hud_panel_scoreboard_respawntime_decimals ?
1960                                         count_seconds_decs(respawn_time - time, autocvar_hud_panel_scoreboard_respawntime_decimals)
1961                                         :
1962                                         count_seconds(ceil(respawn_time - time))
1963                                 )
1964                         );
1965                 }
1966                 else if(time >= respawn_time)
1967                         str = sprintf(_("You are dead, press ^2%s^7 to respawn"), getcommandkey("jump", "+jump"));
1968
1969                 pos.y += 1.2 * hud_fontsize.y;
1970                 drawcolorcodedstring(pos + '0.5 0 0' * (panel_size.x - stringwidth(str, true, hud_fontsize)), str, hud_fontsize, panel_fg_alpha, DRAWFLAG_NORMAL);
1971         }
1972
1973         scoreboard_bottom = pos.y + 2 * hud_fontsize.y;
1974 }