1 /* bzflag
2  * Copyright (c) 1993-2021 Tim Riker
3  *
4  * This package is free software;  you can redistribute it and/or
5  * modify it under the terms of the license found in the file
6  * named COPYING that should have accompanied this file.
7  *
8  * THIS PACKAGE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
9  * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
10  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
11  */
12 
13 /* interface header */
14 #include "ScoreboardRenderer.h"
15 
16 /* common implementation headers */
17 #include "Bundle.h"
18 #include "Team.h"
19 #include "FontManager.h"
20 #include "BZDBCache.h"
21 #include "OpenGLGState.h"
22 #include "TextUtils.h"
23 #include "TimeKeeper.h"
24 
25 /* local implementation headers */
26 #include "LocalPlayer.h"
27 #include "World.h"
28 #include "sound.h"
29 
30 // because of the 'player' crap, we can't  #include "Roaming.h"  easily
31 extern Player* getRoamTargetTank();
32 
33 #define DEBUG_SHOWRATIOS 1
34 
35 std::string ScoreboardRenderer::scoreSpacingLabel("88% 8888 888-888 [88]");
36 std::string ScoreboardRenderer::scoreLabel("Score");
37 std::string ScoreboardRenderer::killSpacingLabel("888~888 Hunt->");
38 std::string ScoreboardRenderer::killLabel(" Kills");
39 std::string ScoreboardRenderer::teamScoreSpacingLabel("88 (888-888) 88");
40 std::string ScoreboardRenderer::teamCountSpacingLabel("888");
41 std::string ScoreboardRenderer::playerLabel("Player");
42 
43 // NOTE: order of sort labels must match SORT_ consts
44 const char* ScoreboardRenderer::sortLabels[] =
45 {
46     "[Score]",
47     "[Normalized Score]",
48     "[Reverse Score]",
49     "[Callsign]",
50     "[Team Kills]",
51     "[TK ratio]",
52     "[Team]",
53     "[1on1]",
54     NULL
55 };
56 
57 int ScoreboardRenderer::sortMode = 0;
58 
59 bool ScoreboardRenderer::alwaysShowTeamScore = 0;
60 
61 
ScoreboardRenderer()62 ScoreboardRenderer::ScoreboardRenderer() :
63     winX(),
64     winY(),
65     winWidth (0.0),
66     winHeight(),
67     dim(false),
68     huntPosition(0),
69     huntSelectEvent(false),
70     huntPositionEvent(0),
71     huntState(HUNT_NONE),
72     huntAddMode(false),
73     teamScoreYVal(0.0f),
74     roaming(false),
75     minorFontFace(),
76     minorFontSize(),
77     labelsFontFace(),
78     labelsFontSize(),
79     scoreLabelWidth(),
80     killsLabelWidth(),
81     teamScoreLabelWidth(),
82     teamCountLabelWidth(),
83     huntArrowWidth(),
84     huntPlusesWidth(),
85     huntedArrowWidth(),
86     tkWarnRatio(),
87     numHunted(0)
88 {
89     // initialize message color (white)
90     messageColor[0] = 1.0f;
91     messageColor[1] = 1.0f;
92     messageColor[2] = 1.0f;
93     sortMode = BZDB.getIntClamped("scoreboardSort", 0, SORT_MAXNUM);
94     alwaysShowTeamScore = (BZDB.getIntClamped("alwaysShowTeamScore", 0, 1) != 0);
95 }
96 
97 
98 /*  Set window size and location to be used to render the scoreboard.
99  *  Values are relative to .... when render() is invoked.
100 */
setWindowSize(float x,float y,float width,float height)101 void  ScoreboardRenderer::setWindowSize (float x, float y, float width, float height)
102 {
103     winX = x;
104     winY = y;
105     winWidth = width;
106     winHeight = height;
107     setMinorFontSize (winHeight);
108     setLabelsFontSize (winHeight);
109 }
110 
111 
112 
~ScoreboardRenderer()113 ScoreboardRenderer::~ScoreboardRenderer()
114 {
115     // no destruction needed
116 }
117 
118 
getSortLabels()119 const char  **ScoreboardRenderer::getSortLabels ()
120 {
121     return sortLabels;
122 }
123 
124 
setSort(int _sortby)125 void ScoreboardRenderer::setSort (int _sortby)
126 {
127     sortMode = _sortby;
128     BZDB.setInt ("scoreboardSort", sortMode);
129 }
130 
getSort()131 int ScoreboardRenderer::getSort ()
132 {
133     return sortMode;
134 }
135 
setAlwaysTeamScore(bool _onoff)136 void    ScoreboardRenderer::setAlwaysTeamScore (bool _onoff)
137 {
138     alwaysShowTeamScore = _onoff;
139     BZDB.set ("alwaysShowTeamScores", _onoff ? "1" : "0");
140 }
141 
getAlwaysTeamScore()142 bool    ScoreboardRenderer::getAlwaysTeamScore ()
143 {
144     return alwaysShowTeamScore;
145 }
146 
setMinorFontSize(float height)147 void ScoreboardRenderer::setMinorFontSize(float height)
148 {
149     FontManager &fm = FontManager::instance();
150     minorFontFace = fm.getFaceID(BZDB.get("consoleFont"));
151 
152     switch (static_cast<int>(BZDB.eval("scorefontsize")))
153     {
154     case 0:   // auto
155     {
156         const float s = height / 72.0f;
157         minorFontSize = floorf(s);
158         break;
159     }
160     case 1: // tiny
161         minorFontSize = 6;
162         break;
163     case 2: // small
164         minorFontSize = 8;
165         break;
166     case 3: // medium
167         minorFontSize = 12;
168         break;
169     case 4: // big
170         minorFontSize = 16;
171         break;
172     }
173 
174     huntArrowWidth = fm.getStrLength(minorFontFace, minorFontSize, "->");
175     huntPlusesWidth = fm.getStrLength(minorFontFace, minorFontSize, "@>");
176     huntedArrowWidth = fm.getStrLength(minorFontFace, minorFontSize, "Hunt->");
177     scoreLabelWidth = fm.getStrLength(minorFontFace, minorFontSize, scoreSpacingLabel);
178     killsLabelWidth = fm.getStrLength(minorFontFace, minorFontSize, killSpacingLabel);
179     teamScoreLabelWidth = fm.getStrLength(minorFontFace, minorFontSize, teamScoreSpacingLabel);
180     teamCountLabelWidth = fm.getStrLength(minorFontFace, minorFontSize, teamCountSpacingLabel);
181     const float spacing = fm.getStrLength(minorFontFace, minorFontSize, " ");
182     scoreLabelWidth += spacing;
183     killsLabelWidth += spacing;
184 }
185 
186 
setLabelsFontSize(float height)187 void ScoreboardRenderer::setLabelsFontSize(float height)
188 {
189     const float s = height / 96.0f;
190     FontManager &fm = FontManager::instance();
191     labelsFontFace = fm.getFaceID(BZDB.get("consoleFont"));
192     labelsFontSize = floorf(s);
193 }
194 
195 
setDim(bool _dim)196 void ScoreboardRenderer::setDim(bool _dim)
197 {
198     dim = _dim;
199 }
200 
201 
202 static const float dimFactor = 0.2f;
203 
hudColor3fv(const GLfloat * c)204 void ScoreboardRenderer::hudColor3fv(const GLfloat* c)
205 {
206     if (dim)
207         glColor3f(dimFactor * c[0], dimFactor * c[1], dimFactor * c[2]);
208     else
209         glColor3fv(c);
210 }
211 
212 
213 
214 
exitSelectState(void)215 void    ScoreboardRenderer::exitSelectState (void)
216 {
217     playLocalSound(SFX_HUNT_SELECT);
218     if (numHunted > 0)
219         setHuntState(HUNT_ENABLED);
220     else
221         setHuntState(HUNT_NONE);
222 }
223 
224 
225 
226 
render(bool forceDisplay)227 void ScoreboardRenderer::render(bool forceDisplay)
228 {
229     FontManager &fm = FontManager::instance();
230     if (dim)
231         fm.setOpacity(dimFactor);
232 
233     if (BZDB.isTrue("displayScore") || forceDisplay)
234     {
235         OpenGLGState::resetState();
236         renderScoreboard();
237     }
238     else
239     {
240         if (getHuntState() == HUNT_SELECTING)   // 'S' pressed while selecting ...
241             exitSelectState ();
242         if (BZDB.isTrue("alwaysShowTeamScores") && World::getWorld()->allowTeams())
243         {
244             OpenGLGState::resetState();
245             renderTeamScores(winWidth, winY,
246                              FontManager::instance().getStrHeight(minorFontFace, minorFontSize, " "));
247         }
248     }
249 
250     if (dim)
251         fm.setOpacity(1.0f);
252 }
253 
254 
teamScoreCompare(const void * _c,const void * _d)255 int ScoreboardRenderer::teamScoreCompare(const void* _c, const void* _d)
256 {
257     Team* c = World::getWorld()->getTeams()+*(const int*)_c;
258     Team* d = World::getWorld()->getTeams()+*(const int*)_d;
259     return (d->getWins()-d->getLosses()) - (c->getWins()-c->getLosses());
260 }
261 
262 // invoked by playing.cxx when 'prev' is pressed
setHuntPrevEvent()263 void ScoreboardRenderer::setHuntPrevEvent()
264 {
265     huntPositionEvent = -1;
266     --huntPosition;
267 }
268 
269 // invoked by playing.cxx when 'next' is pressed
setHuntNextEvent()270 void ScoreboardRenderer::setHuntNextEvent()
271 {
272     huntPositionEvent = 1;
273     ++huntPosition;
274 }
275 
276 // invoked by playing.cxx when select (fire) is pressed
setHuntSelectEvent()277 void ScoreboardRenderer::setHuntSelectEvent ()
278 {
279     huntSelectEvent = true;
280 }
281 
282 // invoked by clientCommands.cxx when '7' or 'U' is pressed
huntKeyEvent(bool isAdd)283 void ScoreboardRenderer::huntKeyEvent (bool isAdd)
284 {
285     if (getHuntState() == HUNT_ENABLED)
286     {
287         if (isAdd)
288         {
289             setHuntState(HUNT_SELECTING);
290             playLocalSound(SFX_HUNT_SELECT);
291         }
292         else
293         {
294             setHuntState(HUNT_NONE);
295             playLocalSound(SFX_HUNT);
296         }
297         huntAddMode = isAdd;
298 
299     }
300     else if (getHuntState() == HUNT_SELECTING)
301         exitSelectState ();
302 
303     else
304     {
305         setHuntState(HUNT_SELECTING);
306         playLocalSound(SFX_HUNT_SELECT);
307         huntAddMode = isAdd;
308         if (!BZDB.isTrue("displayScore"))
309             BZDB.set("displayScore", "1");
310     }
311 }
312 
313 
setHuntState(int _huntState)314 void ScoreboardRenderer::setHuntState (int _huntState)
315 {
316     if (huntState == _huntState)
317         return;
318     if (_huntState != HUNT_SELECTING)
319         huntAddMode = false;
320     if (_huntState==HUNT_NONE)
321         clearHuntedTanks();
322     else if (_huntState==HUNT_SELECTING)
323         huntPosition = 0;
324     huntState = _huntState;
325 }
326 
327 
getHuntState() const328 int ScoreboardRenderer::getHuntState() const
329 {
330     return huntState;
331 }
332 
333 
334 // invoked when joining a server
huntReset()335 void ScoreboardRenderer::huntReset()
336 {
337     huntState = HUNT_NONE;
338     numHunted = 0;
339 }
340 
renderTeamScores(float x,float y,float dy)341 void ScoreboardRenderer::renderTeamScores (float x, float y, float dy)
342 {
343     // print teams sorted by score
344     int teams[NumTeams];
345     int teamCount = 0;
346     int i;
347     float xn, xl;
348     std::string label;
349 
350     if (teamScoreYVal > 0.0)
351         y = teamScoreYVal;
352 
353     if (World::getWorld()->allowRabbit())
354         return;
355 
356     Bundle *bdl = BundleMgr::getCurrentBundle();
357     FontManager &fm = FontManager::instance();
358     hudColor3fv(messageColor);
359 
360     label = bdl->getLocalString("Team Score");
361     xl = xn = x - teamScoreLabelWidth - teamCountLabelWidth;
362     fm.drawString(xl, y, 0, minorFontFace, minorFontSize, label);
363 
364     for (i = RedTeam; i < NumTeams; i++)
365     {
366         if (!Team::isColorTeam(TeamColor(i))) continue;
367         const Team* team = World::getWorld()->getTeams() + i;
368         if (team->size == 0) continue;
369         teams[teamCount++] = i;
370     }
371     qsort(teams, teamCount, sizeof(int), teamScoreCompare);
372     y -= dy;
373 
374     char score[44];
375     for (i = 0 ; i < teamCount; i++)
376     {
377         Team& team = World::getWorld()->getTeam(teams[i]);
378         sprintf(score, "%3d (%3d-%-3d) %3d", team.getWins() - team.getLosses(), team.getWins(), team.getLosses(), team.size);
379         hudColor3fv(Team::getTankColor((TeamColor)teams[i]));
380         fm.drawString(xn, y, 0, minorFontFace, minorFontSize, score);
381         y -= dy;
382     }
383 }
384 
385 
386 // not used yet - new feature coming
renderCtfFlags()387 void ScoreboardRenderer::renderCtfFlags ()
388 {
389     int i;
390     RemotePlayer* player;
391     const int curMaxPlayers = World::getWorld()->getCurMaxPlayers();
392     char flagColor[200];
393 
394     FontManager &fm = FontManager::instance();
395     const float x = winX;
396     const float y = winY;
397     const float dy = fm.getStrHeight(minorFontFace, minorFontSize, " ");
398     float y0 = y - dy;
399 
400     hudColor3fv(messageColor);
401     fm.drawString(x, y, 0, minorFontFace, minorFontSize, "Team Flags");
402 
403     fm.setDimFactor(dimFactor);
404 
405     for (i=0; i < curMaxPlayers; i++)
406     {
407         if ((player = World::getWorld()->getPlayer(i)))
408         {
409             FlagType* flagd = player->getFlag();
410             TeamColor teamIndex = player->getTeam();
411             if (flagd!=Flags::Null && flagd->flagTeam != NoTeam)      // if player has team flag ...
412             {
413                 std::string playerInfo = Team::getAnsiCode(flagd->flagTeam);
414                 snprintf(flagColor, 200, "%-12s", flagd->flagName.c_str());
415                 playerInfo += flagColor;
416                 playerInfo += Team::getAnsiCode(teamIndex);
417                 playerInfo += player->getCallSign();
418 
419                 fm.drawString(x, y0, 0, minorFontFace, minorFontSize, playerInfo);
420                 y0 -= dy;
421             }
422         }
423     }
424     renderTeamScores (winWidth, y, dy);
425 }
426 
427 
clearHuntedTanks()428 void ScoreboardRenderer::clearHuntedTanks ()
429 {
430     World *world = World::getWorld();
431     if (!world)
432         return;
433     const int curMaxPlayers = world->getCurMaxPlayers();
434     Player *p;
435     for (int i=0; i<curMaxPlayers; i++)
436     {
437         if ((p = world->getPlayer(i)))
438             p->setHunted (false);
439     }
440     numHunted = 0;
441 }
442 
443 
renderScoreboard(void)444 void ScoreboardRenderer::renderScoreboard(void)
445 {
446     int i=0;
447     int numPlayers;
448     int mottoLen;
449     Player** players;
450     Player*  player;
451     bool haveObs = false;
452 
453     if ( (players = newSortedList (sortMode, true, &numPlayers)) == NULL)
454         return;
455 
456     LocalPlayer* myTank = LocalPlayer::getMyTank();
457     Bundle *bdl = BundleMgr::getCurrentBundle();
458     FontManager &fm = FontManager::instance();
459 
460     const float x1 = winX;
461     const float x2 = x1 + scoreLabelWidth;
462     const float x3 = x2 + killsLabelWidth;
463     const float y0 = winY;
464     hudColor3fv(messageColor);
465 
466     std::string psLabel = bdl->getLocalString(playerLabel);
467     if (sortMode != SORT_SCORE)
468     {
469         psLabel += "  ";
470         psLabel += sortLabels[sortMode];
471     }
472     fm.drawString(x1, y0, 0, minorFontFace, minorFontSize, bdl->getLocalString(scoreLabel));
473     fm.drawString(x2, y0, 0, minorFontFace, minorFontSize, bdl->getLocalString(killLabel));
474     fm.drawString(x3, y0, 0, minorFontFace, minorFontSize, psLabel);
475     const float dy = fm.getStrHeight(minorFontFace, minorFontSize, " ");
476     float y = y0 - dy;
477 
478     // make room for the status marker
479     const float xs = x3 - fm.getStrLength(minorFontFace, minorFontSize, "+|");
480 
481     if (huntState == HUNT_SELECTING)
482     {
483         std::string huntStr = ColorStrings[YellowColor];
484         huntStr += ColorStrings[PulsatingColor];
485         huntStr += " *SEL*";
486         fm.drawString(xs - huntedArrowWidth, y0, 0, minorFontFace, minorFontSize, huntStr);
487     }
488 
489     // grab the tk warning ratio
490     tkWarnRatio = BZDB.eval("tkwarnratio");
491 
492     if (huntState == HUNT_SELECTING)
493     {
494         if (numPlayers<1 || (numPlayers==1 && players[0]==myTank))
495             setHuntState (HUNT_NONE);
496         else
497         {
498             if (players[huntPosition] == myTank)
499             {
500                 if (huntPositionEvent < 0)
501                     --huntPosition;
502                 else
503                     ++huntPosition;
504             }
505             if (huntPosition>=numPlayers)
506             {
507                 huntPosition = 0;
508                 if (players[0] == myTank)
509                     huntPosition = 1;
510             }
511             if (huntPosition<0)
512             {
513                 huntPosition = numPlayers-1;
514                 if (players[huntPosition] == myTank)
515                     --huntPosition;
516             }
517             if (huntSelectEvent)      // if 'fire' was pressed ...
518             {
519                 if (!huntAddMode)
520                     clearHuntedTanks ();
521                 if (huntAddMode && players[huntPosition]->isHunted())   // UNselect
522                 {
523                     players[huntPosition]->setHunted(false);
524                     if (--numHunted != 0)
525                         playLocalSound(SFX_HUNT_SELECT);
526                 }
527                 else                            // else select
528                 {
529                     players[huntPosition]->setHunted(true);
530                     if (++numHunted == 1)
531                         playLocalSound(SFX_HUNT);
532                     else
533                         playLocalSound(SFX_HUNT_SELECT);
534                 }
535                 huntState = HUNT_ENABLED;
536             }
537         }
538     }
539 
540     mottoLen = BZDB.getIntClamped ("mottoDispLen", 0, 128);
541     huntSelectEvent = false;
542     huntPositionEvent = 0;
543     numHunted = 0;
544 
545     const int maxLines = BZDB.evalInt("maxScoreboardLines");
546     int lines = 0;
547     int hiddenLines = 0;
548     while ((player = players[i]) != NULL)
549     {
550         if ((maxLines > 0) && (lines >= maxLines))
551             hiddenLines++;
552         else
553         {
554             if (player->isHunted())
555                 ++numHunted;
556             if (player->getTeam()==ObserverTeam && !haveObs)
557             {
558                 y -= dy;
559                 haveObs = true;
560             }
561             if (huntState==HUNT_SELECTING && i==huntPosition)
562                 drawPlayerScore(player, x1, x2, x3, xs, (float)y, mottoLen, true);
563             else
564                 drawPlayerScore(player, x1, x2, x3, xs, (float)y, mottoLen, false);
565             y -= dy;
566         }
567         ++i;
568         ++lines;
569     }
570 
571     if (hiddenLines > 0)
572     {
573         char buf[64];
574         snprintf(buf, sizeof(buf), "...%i...", hiddenLines);
575         fm.drawString(x1, y, 0, minorFontFace, minorFontSize, buf);
576         fm.drawString(x2, y, 0, minorFontFace, minorFontSize, buf);
577         fm.drawString(x3, y, 0, minorFontFace, minorFontSize, buf);
578     }
579 
580     if (huntState==HUNT_ENABLED && !numHunted)
581     {
582         huntState = HUNT_NONE;  // last hunted player must have left the game
583         huntAddMode = false;
584         playLocalSound(SFX_HUNT);
585     }
586 
587     delete[] players;
588 
589     if (World::getWorld()->allowTeams())
590         renderTeamScores(winWidth, y0, dy);
591 }
592 
593 
stringAppendNormalized(std::string * s,float n)594 void ScoreboardRenderer::stringAppendNormalized (std::string *s, float n)
595 {
596     char fmtbuf[10];
597     sprintf (fmtbuf, "  [%4.2f]", n);
598     *s += fmtbuf;
599 }
600 
601 
drawRoamTarget(float _x0,float _y0,float _x1,float UNUSED (_y1))602 void ScoreboardRenderer::drawRoamTarget(float _x0, float _y0,
603                                         float _x1, float UNUSED(_y1))
604 {
605     static const TimeKeeper startTime = TimeKeeper::getCurrent();
606 
607     const float x0 = floorf(_x0) + 0.5f;
608     const float y0 = floorf(_y0) + 0.5f;
609     const float x1 = floorf(_x1) + 0.5f;
610     const float y1 = y0 + 1.0f;
611 
612     const float black[4] =
613     {
614         0.0f * (dim ? dimFactor : 1.0f),
615         0.0f * (dim ? dimFactor : 1.0f),
616         0.0f * (dim ? dimFactor : 1.0f),
617         1.0f
618     };
619     const float white[4] =
620     {
621         1.0f * (dim ? dimFactor : 1.0f),
622         1.0f * (dim ? dimFactor : 1.0f),
623         1.0f * (dim ? dimFactor : 1.0f),
624         1.0f
625     };
626     const float* c0 = black;
627     const float* c1 = white;
628 
629     const double diff = (TimeKeeper::getCurrent() - startTime);
630     if (fmod(diff, 0.5) > 0.25)
631     {
632         c0 = white;
633         c1 = black;
634     }
635 
636     glPushAttrib(GL_ALL_ATTRIB_BITS);
637     glDisable(GL_BLEND);
638     glDisable(GL_LIGHTING);
639     glDisable(GL_TEXTURE_2D);
640     glBegin(GL_LINES);
641     glColor4fv(c0);
642     glVertex2f(x0, y1);
643     glVertex2f(x1, y1);
644     glColor4fv(c1);
645     glVertex2f(x0, y0);
646     glVertex2f(x1, y0);
647     glEnd();
648     glPopAttrib();
649 }
650 
651 
drawPlayerScore(const Player * player,float x1,float x2,float x3,float xs,float y,int mottoLen,bool huntCursor)652 void ScoreboardRenderer::drawPlayerScore(const Player* player,
653         float x1, float x2, float x3, float xs, float y,
654         int mottoLen, bool huntCursor)
655 {
656     // score
657     char score[40], kills[40];
658 
659     bool highlightTKratio = false;
660     if (tkWarnRatio > 0.0)
661     {
662         if (((player->getWins() > 0) && (player->getTKRatio() > tkWarnRatio)) ||
663                 ((player->getWins() == 0) && (player->getTeamKills() >= 3)))
664             highlightTKratio = true;
665     }
666 
667     if (World::getWorld()->allowRabbit())
668     {
669         sprintf(score, "%2d%% %4d %3d-%-3d%s[%2d]", player->getRabbitScore(),
670                 player->getScore(), player->getWins(), player->getLosses(),
671                 highlightTKratio ? ColorStrings[CyanColor].c_str() : "",
672                 player->getTeamKills());
673     }
674     else if (World::getWorld()->allowTeams())
675     {
676         sprintf(score, "%4d %4d-%-4d%s[%2d]", player->getScore(),
677                 player->getWins(), player->getLosses(),
678                 highlightTKratio ? ColorStrings[CyanColor].c_str() : "",
679                 player->getTeamKills());
680     }
681     else
682     {
683         sprintf(score, "%4d %4d-%-4d%s", player->getScore(),
684                 player->getWins(), player->getLosses(),
685                 highlightTKratio ? ColorStrings[CyanColor].c_str() : "");
686     }
687 
688     // kills
689     if (LocalPlayer::getMyTank() != player)
690         sprintf(kills, "%3d~%-3d", player->getLocalWins(), player->getLocalLosses());
691     else
692         sprintf(kills, "%4d", player->getSelfKills());
693 
694 
695     // team color
696     TeamColor teamIndex = player->getTeam();
697     std::string teamColor;
698     if (player->getId() < 200)
699         teamColor = Team::getAnsiCode(teamIndex);
700     else
701     {
702         teamColor = ColorStrings[CyanColor];    // replay observers
703     }
704 
705     // authentication status
706     std::string statusInfo;
707     if (BZDBCache::colorful)
708         statusInfo += ColorStrings[CyanColor];
709     else
710         statusInfo += teamColor;
711     if (player->isAdmin())
712         statusInfo += '@';
713     else if (player->isVerified())
714         statusInfo += '+';
715     else if (player->isRegistered())
716         statusInfo += '-';
717     else
718     {
719         statusInfo = "";    // don't print
720     }
721 
722     std::string playerInfo;
723     // team color
724     playerInfo += teamColor;
725     // Slot number only for admins (playerList perm check, in case they have
726     // hideAdmin)
727     LocalPlayer* localPlayer = LocalPlayer::getMyTank();
728     if (localPlayer->isAdmin() || localPlayer->hasPlayerList())
729     {
730         char slot[10];
731         sprintf(slot, "%3d",player->getId());
732         playerInfo += slot;
733         playerInfo += " - ";
734     }
735 
736     if (roaming && BZDB.isTrue("showVelocities"))
737     {
738         float vel[3] = {0};
739         memcpy(vel,player->getVelocity(),sizeof(float)*3);
740 
741         float linSpeed = sqrt(vel[0]*vel[0]+vel[1]*vel[1]);
742 
743         float badFactor = 1.5f;
744         if (linSpeed > player->getMaxSpeed()*badFactor)
745             playerInfo += ColorStrings[RedColor];
746         if (linSpeed > player->getMaxSpeed())
747             playerInfo += ColorStrings[YellowColor];
748         else if (linSpeed < 0.0001f)
749             playerInfo += ColorStrings[GreyColor];
750         else
751             playerInfo += ColorStrings[WhiteColor];
752         playerInfo += TextUtils::format("%5.2f ",linSpeed);
753         playerInfo += teamColor;
754     }
755 
756     // callsign
757     playerInfo += player->getCallSign();
758 
759     // motto in parentheses
760     if (player->getMotto()[0] != '\0' && mottoLen>0)
761     {
762         playerInfo += " (";
763         playerInfo += TextUtils::str_trunc_continued (player->getMotto(), mottoLen);
764         playerInfo += ")";
765     }
766     // carried flag
767     bool coloredFlag = false;
768     FlagType* flagd = player->getFlag();
769     if (flagd != Flags::Null)
770     {
771         // color special flags
772         if (BZDBCache::colorful)
773         {
774             if ((flagd == Flags::ShockWave)
775                     ||  (flagd == Flags::Genocide)
776                     ||  (flagd == Flags::Laser)
777                     ||  (flagd == Flags::GuidedMissile))
778                 playerInfo += ColorStrings[WhiteColor];
779             else if (flagd->flagTeam != NoTeam)
780             {
781                 // use team color for team flags
782                 playerInfo += Team::getAnsiCode(flagd->flagTeam);
783             }
784             coloredFlag = true;
785         }
786         playerInfo += "/";
787         playerInfo += (flagd->endurance == FlagNormal ? flagd->flagName : flagd->flagAbbv);
788         // back to original color
789         if (coloredFlag)
790             playerInfo += teamColor;
791     }
792 
793     // status
794     if (player->isPaused())
795         playerInfo += "[p]";
796     else if (player->isNotResponding())
797         playerInfo += "[nr]";
798     else if (player->isAutoPilot())
799         playerInfo += "[auto]";
800 
801 #if DEBUG_SHOWRATIOS
802     if (player->getTeam() != ObserverTeam)
803     {
804         if (sortMode == SORT_NORMALIZED)
805             stringAppendNormalized (&playerInfo, player->getNormalizedScore());
806         else if (sortMode == SORT_MYRATIO && LocalPlayer::getMyTank() != player)
807             stringAppendNormalized (&playerInfo, player->getLocalNormalizedScore());
808         else if (sortMode == SORT_TKRATIO)
809             stringAppendNormalized (&playerInfo, player->getTKRatio());
810     }
811 #endif
812 
813     FontManager &fm = FontManager::instance();
814     fm.setDimFactor(dimFactor);
815 
816     if (player == getRoamTargetTank())
817     {
818         const float w = fm.getStrLength(minorFontFace, minorFontSize, playerInfo);
819         const float h = fm.getStrHeight(minorFontFace, minorFontSize, playerInfo);
820         drawRoamTarget(x3, y, x3 + w, y + h);
821     }
822 
823     // draw
824     if (player->getTeam() != ObserverTeam)
825     {
826         hudColor3fv(Team::getTankColor(teamIndex));
827         fm.drawString(x1, y, 0, minorFontFace, minorFontSize, score);
828         hudColor3fv(Team::getTankColor(teamIndex));
829         fm.drawString(x2, y, 0, minorFontFace, minorFontSize, kills);
830     }
831     fm.drawString(x3, y, 0, minorFontFace, minorFontSize, playerInfo);
832     if (statusInfo.size() > 0)
833         fm.drawString(xs, y, 0, minorFontFace, minorFontSize, statusInfo);
834     if (BZDB.isTrue("debugHud"))
835     {
836         printf ("playerInfo: %s\n", playerInfo.c_str());    // FIXME
837     }
838 
839     // draw huntEnabled status
840     if (player->isHunted())
841     {
842         std::string huntStr = ColorStrings[WhiteColor];
843         huntStr += "Hunt->";
844         fm.drawString(xs - huntedArrowWidth, y, 0, minorFontFace, minorFontSize,
845                       huntStr.c_str());
846     }
847     else if (huntCursor && !huntAddMode)
848     {
849         std::string huntStr = ColorStrings[WhiteColor];
850         huntStr += ColorStrings[PulsatingColor];
851         huntStr += "->";
852         fm.drawString(xs - huntArrowWidth, y, 0, minorFontFace, minorFontSize,
853                       huntStr.c_str());
854     }
855     if (huntCursor && huntAddMode)
856     {
857         std::string huntStr = ColorStrings[WhiteColor];
858         huntStr += ColorStrings[PulsatingColor];
859         huntStr += "@>";
860         fm.drawString(xs - huntPlusesWidth, y, 0, minorFontFace, minorFontSize,
861                       huntStr.c_str());
862     }
863 }
864 
865 
866 // get current 'leader' (NULL if no player)
getLeader(std::string * label)867 Player*   ScoreboardRenderer::getLeader(std::string *label)
868 {
869     int sortType=sortMode;
870 
871     if (sortMode==SORT_CALLSIGN || sortMode==SORT_MYRATIO || sortMode==SORT_TEAM)
872         sortType = SORT_SCORE;
873     if (label != NULL)
874     {
875         if (sortMode==SORT_TKS)
876             *label = "TK Leader ";
877         else if (sortMode==SORT_TKRATIO)
878             *label = "TK Ratio Leader ";
879         else
880             *label = "Leader ";
881     }
882 
883     Player** list = newSortedList (sortType, true);
884 
885     Player* top;
886     if (!list)
887         top = NULL;
888     else
889         top = list[0];
890 
891     delete[] list;
892 
893     if (top==NULL || top->getTeam()==ObserverTeam)
894         return NULL;
895     return top;
896 }
897 
898 /************************ Sort logic follows .... **************************/
899 
900 struct st_playersort
901 {
902     Player *player;
903     int i1;
904     int i2;
905     const char *cp;
906 };
907 typedef struct st_playersort sortEntry;
908 
909 
sortCompareCp(const void * _a,const void * _b)910 int       ScoreboardRenderer::sortCompareCp(const void* _a, const void* _b)
911 {
912     const sortEntry *a = (const sortEntry *)_a;
913     const sortEntry *b = (const sortEntry *)_b;
914     return strcasecmp (a->cp, b->cp);
915 }
916 
917 
sortCompareI2(const void * _a,const void * _b)918 int       ScoreboardRenderer::sortCompareI2(const void* _a, const void* _b)
919 {
920     const sortEntry *a = (const sortEntry *)_a;
921     const sortEntry *b = (const sortEntry *)_b;
922     if (a->i1 != b->i1 )
923         return b->i1 - a->i1;
924     return b->i2 - a->i2;
925 }
926 
927 
928 // creates (allocates) a null-terminated array of Player*
newSortedList(int sortType,bool obsLast,int * _numPlayers)929 Player **  ScoreboardRenderer::newSortedList (int sortType, bool obsLast, int *_numPlayers)
930 {
931     LocalPlayer *myTank = LocalPlayer::getMyTank();
932     World *world = World::getWorld();
933 
934     if (!myTank || !world)
935         return NULL;
936 
937     const int curMaxPlayers = world->getCurMaxPlayers() +1;
938     int i,j;
939     int numPlayers=0;
940     int numObs=0;
941     Player* p;
942     sortEntry* sorter = new sortEntry [curMaxPlayers];
943 
944     // fill the array with remote players
945     for (i=0; i<curMaxPlayers-1; i++)
946     {
947         if ((p = world->getPlayer(i)))
948         {
949             if (obsLast && p->getTeam()==ObserverTeam)
950                 sorter[curMaxPlayers - (++numObs)].player = p;
951             else
952                 sorter[numPlayers++].player = p;
953         }
954     }
955     // add my tank
956     if (obsLast && myTank->getTeam()==ObserverTeam)
957         sorter[curMaxPlayers - (++numObs)].player = myTank;
958     else
959         sorter[numPlayers++].player = myTank;
960 
961     // sort players ...
962     if (numPlayers > 0)
963     {
964         for (i=0; i<numPlayers; i++)
965         {
966             p = sorter[i].player;
967             switch (sortType)
968             {
969             case SORT_TKS:
970                 sorter[i].i1 = p->getTeamKills();
971                 sorter[i].i2 = 0 - (int)(p->getNormalizedScore() * 100000);
972                 break;
973             case SORT_TKRATIO:
974                 sorter[i].i1 = (int)(p->getTKRatio() * 1000);
975                 sorter[i].i2 = 0 - (int)(p->getNormalizedScore() * 100000);
976                 break;
977             case SORT_TEAM:
978                 sorter[i].i1 = p->getTeam();
979                 sorter[i].i2 = (int)(p->getNormalizedScore() * 100000);
980                 break;
981             case SORT_MYRATIO:
982                 if (p == myTank)
983                     sorter[i].i1 = -100001;
984                 else
985                     sorter[i].i1 = 0 - (int)(p->getLocalNormalizedScore() * 100000);
986                 sorter[i].i2 = (int)(p->getNormalizedScore() * 100000);
987                 break;
988             case SORT_NORMALIZED:
989                 sorter[i].i1 = (int)(p->getNormalizedScore() * 100000);
990                 sorter[i].i2 = 0;
991                 break;
992             case SORT_CALLSIGN:
993                 sorter[i].cp = p->getCallSign();
994                 break;
995             default:
996                 if (world->allowRabbit())
997                     sorter[i].i1 = p->getRabbitScore();
998                 else
999                     sorter[i].i1 = p->getScore();
1000                 sorter[i].i2 = 0;
1001                 if (sortType == SORT_REVERSE)
1002                     sorter[i].i1 *= -1;
1003             }
1004         }
1005         if (sortType == SORT_CALLSIGN)
1006             qsort (sorter, numPlayers, sizeof(sortEntry), sortCompareCp);
1007         else
1008             qsort (sorter, numPlayers, sizeof(sortEntry), sortCompareI2);
1009     }
1010 
1011     // TODO: Sort obs here (by time joined, when that info is available)
1012 
1013     Player** players = new Player *[numPlayers + numObs + 1];
1014     for (i=0; i<numPlayers; i++)
1015         players[i] = sorter[i].player;
1016     for (j=curMaxPlayers-numObs; j<curMaxPlayers; j++)
1017         players[i++] = sorter[j].player;
1018     players[i] = NULL;
1019 
1020     if (_numPlayers != NULL)
1021         *_numPlayers = numPlayers;
1022 
1023     delete[] sorter;
1024     return players;
1025 }
1026 
1027 
getPlayerList(std::vector<Player * > & players)1028 void ScoreboardRenderer::getPlayerList(std::vector<Player*>& players)
1029 {
1030     players.clear();
1031 
1032     int playerCount;
1033     Player** pList = newSortedList(getSort(), true, &playerCount);
1034     if (pList == NULL)
1035         return;
1036     for (int i = 0; i < playerCount; i++)
1037     {
1038         Player* p = pList[i];
1039         if (p && (p->getTeam() != ObserverTeam))
1040             players.push_back(p);
1041     }
1042     delete[] pList;
1043 }
1044 
1045 
1046 // Local Variables: ***
1047 // mode: C++ ***
1048 // tab-width: 4 ***
1049 // c-basic-offset: 4 ***
1050 // indent-tabs-mode: nil ***
1051 // End: ***
1052 // ex: shiftwidth=4 tabstop=4
1053