text: // creation of scoreboard
  #include "game.h"
  
  namespace game
  {
      VARP(scoreboard2d, 0, 1, 1);
      VARP(showservinfo, 0, 1, 1);
      VARP(showclientnum, 0, 0, 1);
      VARP(showpj, 0, 0, 1);
      VARP(showping, 0, 1, 2);
      VARP(showspectators, 0, 1, 1);
      VARP(showspectatorping, 0, 0, 1);
      VARP(highlightscore, 0, 1, 1);
      VARP(showconnecting, 0, 0, 1);
      VARP(hidefrags, 0, 1, 1);
  
      static hashset<teaminfo> teaminfos;
  
      void clearteaminfo()
      {
          teaminfos.clear();
      }
  
      void setteaminfo(const char *team, int frags)
      {
          teaminfo *t = teaminfos.access(team);
          if(!t) { t = &teaminfos[team]; copystring(t->team, team, sizeof(t->team)); }
          t->frags = frags;
      }
  
      static inline bool playersort(const fpsent *a, const fpsent *b)
      {
          if(a->state==CS_SPECTATOR)
          {
              if(b->state==CS_SPECTATOR) return strcmp(a->name, b->name) < 0;
              else return false;
          }
          else if(b->state==CS_SPECTATOR) return true;
          if(m_ctf || m_collect)
          {
              if(a->flags > b->flags) return true;
              if(a->flags < b->flags) return false;
          }
          if(a->frags > b->frags) return true;
          if(a->frags < b->frags) return false;
          return strcmp(a->name, b->name) < 0;
      }
  
      void getbestplayers(vector<fpsent *> &best)
      {
          loopv(players)
          {
              fpsent *o = players[i];
              if(o->state!=CS_SPECTATOR) best.add(o);
          }
          best.sort(playersort);
          while(best.length() > 1 && best.last()->frags < best[0]->frags) best.drop();
      }
  
      void getbestteams(vector<const char *> &best)
      {
          if(cmode && cmode->hidefrags())
          {
              vector<teamscore> teamscores;
              cmode->getteamscores(teamscores);
              teamscores.sort(teamscore::compare);
              while(teamscores.length() > 1 && teamscores.last().score < teamscores[0].score) teamscores.drop();
              loopv(teamscores) best.add(teamscores[i].team);
          }
          else
          {
              int bestfrags = INT_MIN;
              enumerate(teaminfos, teaminfo, t, bestfrags = max(bestfrags, t.frags));
              if(bestfrags <= 0) loopv(players)
              {
                  fpsent *o = players[i];
                  if(o->state!=CS_SPECTATOR && !teaminfos.access(o->team) && best.htfind(o->team) < 0) { bestfrags = 0; best.add(o->team); }
              }
              enumerate(teaminfos, teaminfo, t, if(t.frags >= bestfrags) best.add(t.team));
          }
      }
  
      struct scoregroup : teamscore
      {
          vector<fpsent *> players;
      };
      static vector<scoregroup *> groups;
      static vector<fpsent *> spectators;
  
      static inline bool scoregroupcmp(const scoregroup *x, const scoregroup *y)
      {
          if(!x->team)
          {
              if(y->team) return false;
          }
          else if(!y->team) return true;
          if(x->score > y->score) return true;
          if(x->score < y->score) return false;
          if(x->players.length() > y->players.length()) return true;
          if(x->players.length() < y->players.length()) return false;
          return x->team && y->team && strcmp(x->team, y->team) < 0;
      }
  
      static int groupplayers()
      {
          int numgroups = 0;
          spectators.setsize(0);
          loopv(players)
          {
              fpsent *o = players[i];
              if(!showconnecting && !o->name[0]) continue;
              if(o->state==CS_SPECTATOR) { spectators.add(o); continue; }
              const char *team = m_teammode && o->team[0] ? o->team : NULL;
              bool found = false;
              loopj(numgroups)
              {
                  scoregroup &g = *groups[j];
                  if(team!=g.team && (!team || !g.team || strcmp(team, g.team))) continue;
                  g.players.add(o);
                  found = true;
              }
              if(found) continue;
              if(numgroups>=groups.length()) groups.add(new scoregroup);
              scoregroup &g = *groups[numgroups++];
              g.team = team;
              if(!team) g.score = 0;
              else if(cmode && cmode->hidefrags()) g.score = cmode->getteamscore(o->team);
              else { teaminfo *ti = teaminfos.access(team); g.score = ti ? ti->frags : 0; }
              g.players.setsize(0);
              g.players.add(o);
          }
          loopi(numgroups) groups[i]->players.sort(playersort);
          spectators.sort(playersort);
          groups.sort(scoregroupcmp, 0, numgroups);
          return numgroups;
      }
  
      int statuscolor(fpsent *d, int color)
      {
          if(d->privilege)
          {
              color = d->privilege>=PRIV_ADMIN ? 0xFF8000 : (d->privilege>=PRIV_AUTH ? 0xC040C0 : 0x40FF80);
              if(d->state==CS_DEAD) color = (color>>1)&0x7F7F7F;
          }
          else if(d->state==CS_DEAD) color = 0x606060;
          return color;
      }
  
      void renderscoreboard(g3d_gui &g, bool firstpass)
      {
          const ENetAddress *address = connectedpeer();
          if(showservinfo && address)
          {
              string hostname;
              if(enet_address_get_host_ip(address, hostname, sizeof(hostname)) >= 0)
              {
                  if(servinfo[0]) g.titlef("%.25s", 0xFFFF80, NULL, servinfo);
                  else g.titlef("%s:%d", 0xFFFF80, NULL, hostname, address->port);
              }
          }
  
          g.pushlist();
          g.spring();
          g.text(server::modename(gamemode), 0xFFFF80);
          g.separator();
          const char *mname = getclientmap();
          g.text(mname[0] ? mname : "[new map]", 0xFFFF80);
          extern int gamespeed;
          if(gamespeed != 100) { g.separator(); g.textf("%d.%02dx", 0xFFFF80, NULL, gamespeed/100, gamespeed%100); }
          if(m_timed && mname[0] && (maplimit >= 0 || intermission))
          {
              g.separator();
              if(intermission) g.text("intermission", 0xFFFF80);
              else
              {
                  int secs = max(maplimit-lastmillis, 0)/1000, mins = secs/60;
                  secs %= 60;
                  g.pushlist();
                  g.strut(mins >= 10 ? 4.5f : 3.5f);
                  g.textf("%d:%02d", 0xFFFF80, NULL, mins, secs);
                  g.poplist();
              }
          }
          if(ispaused()) { g.separator(); g.text("paused", 0xFFFF80); }
          g.spring();
          g.poplist();
  
          g.separator();
  
          int numgroups = groupplayers();
          loopk(numgroups)
          {
              if((k%2)==0) g.pushlist(); // horizontal
  
              scoregroup &sg = *groups[k];
              int bgcolor = sg.team && m_teammode ? (isteam(player1->team, sg.team) ? 0x3030C0 : 0xC03030) : 0,
                  fgcolor = 0xFFFF80;
  
              g.pushlist(); // vertical
              g.pushlist(); // horizontal
  
              #define loopscoregroup(o, b) \
                  loopv(sg.players) \
                  { \
                      fpsent *o = sg.players[i]; \
                      b; \
                  }
  
              g.pushlist();
              if(sg.team && m_teammode)
              {
                  g.pushlist();
                  g.background(bgcolor, numgroups>1 ? 3 : 5);
                  g.strut(1);
                  g.poplist();
              }
              g.text("", 0, " ");
              loopscoregroup(o,
              {
                  if(o==player1 && highlightscore && (multiplayer(false) || demoplayback || players.length() > 1))
                  {
                      g.pushlist();
                      g.background(0x808080, numgroups>1 ? 3 : 5);
                  }
                  const playermodelinfo &mdl = getplayermodelinfo(o);
                  const char *icon = sg.team && m_teammode ? (isteam(player1->team, sg.team) ? mdl.blueicon : mdl.redicon) : mdl.ffaicon;
                  g.text("", 0, icon);
                  if(o==player1 && highlightscore && (multiplayer(false) || demoplayback || players.length() > 1)) g.poplist();
              });
              g.poplist();
  
              if(sg.team && m_teammode)
              {
                  g.pushlist(); // vertical
  
                  if(sg.score>=10000) g.textf("%s: WIN", fgcolor, NULL, sg.team);
                  else g.textf("%s: %d", fgcolor, NULL, sg.team, sg.score);
  
                  g.pushlist(); // horizontal
              }
  
              if(!cmode || !cmode->hidefrags() || !hidefrags)
              {
                  g.pushlist();
                  g.strut(6);
                  g.text("frags", fgcolor);
                  loopscoregroup(o, g.textf("%d", 0xFFFFDD, NULL, o->frags));
                  g.poplist();
              }
  
              g.pushlist();
              g.text("name", fgcolor);
              g.strut(12);
              loopscoregroup(o,
              {
                  g.textf("%s ", statuscolor(o, 0xFFFFDD), NULL, colorname(o));
              });
              g.poplist();
  
              if(multiplayer(false) || demoplayback)
              {
                  if(showpj || showping) g.space(1);
  
                  if(showpj && showping <= 1)
                  {
                      g.pushlist();
                      g.strut(6);
                      g.text("pj", fgcolor);
                      loopscoregroup(o,
                      {
                          if(o->state==CS_LAGGED) g.text("LAG", 0xFFFFDD);
                          else g.textf("%d", 0xFFFFDD, NULL, o->plag);
                      });
                      g.poplist();
                  }
  
                  if(showping > 1)
                  {
                      g.pushlist();
                      g.strut(6);
  
                      g.pushlist();
                      g.text("ping", fgcolor);
                      g.space(1);
                      g.spring();
                      g.text("pj", fgcolor);
                      g.poplist();
  
                      loopscoregroup(o,
                      {
                          fpsent *p = o->ownernum >= 0 ? getclient(o->ownernum) : o;
                          if(!p) p = o;
                          g.pushlist();
                          if(p->state==CS_LAGGED) g.text("LAG", 0xFFFFDD);
                          else
                          {
                              g.textf("%d", 0xFFFFDD, NULL, p->ping);
                              g.space(1);
                              g.spring();
                              g.textf("%d", 0xFFFFDD, NULL, o->plag);
                          }
                          g.poplist();
  
                      });
                      g.poplist();
                  }
                  else if(showping)
                  {
                      g.pushlist();
                      g.text("ping", fgcolor);
                      g.strut(6);
                      loopscoregroup(o,
                      {
                          fpsent *p = o->ownernum >= 0 ? getclient(o->ownernum) : o;
                          if(!p) p = o;
                          if(!showpj && p->state==CS_LAGGED) g.text("LAG", 0xFFFFDD);
                          else g.textf("%d", 0xFFFFDD, NULL, p->ping);
                      });
                      g.poplist();
                  }
              }
  
              if(showclientnum || player1->privilege>=PRIV_MASTER)
              {
                  g.space(1);
                  g.pushlist();
                  g.text("cn", fgcolor);
                  loopscoregroup(o, g.textf("%d", 0xFFFFDD, NULL, o->clientnum));
                  g.poplist();
              }
  
              if(sg.team && m_teammode)
              {
                  g.poplist(); // horizontal
                  g.poplist(); // vertical
              }
  
              g.poplist(); // horizontal
              g.poplist(); // vertical
  
              if(k+1<numgroups && (k+1)%2) g.space(2);
              else g.poplist(); // horizontal
          }
  
          if(showspectators && spectators.length())
          {
              if(showclientnum || player1->privilege>=PRIV_MASTER)
              {
                  g.pushlist();
  
                  g.pushlist();
                  g.text("spectator", 0xFFFF80, " ");
                  g.strut(12);
                  loopv(spectators)
                  {
                      fpsent *o = spectators[i];
                      if(o==player1 && highlightscore)
                      {
                          g.pushlist();
                          g.background(0x808080, 3);
                      }
                      g.text(colorname(o), statuscolor(o, 0xFFFFDD), "spectator");
                      if(o==player1 && highlightscore) g.poplist();
                  }
                  g.poplist();
  
                  if((multiplayer(false) || demoplayback) && showspectatorping)
                  {
                      g.space(1);
                      g.pushlist();
                      g.text("ping", 0xFFFF80);
                      g.strut(6);
                      loopv(spectators)
                      {
                          fpsent *o = spectators[i];
                          fpsent *p = o->ownernum >= 0 ? getclient(o->ownernum) : o;
                          if(!p) p = o;
                          if(p->state==CS_LAGGED) g.text("LAG", 0xFFFFDD);
                          else g.textf("%d", 0xFFFFDD, NULL, p->ping);
                      }
                      g.poplist();
                  }
  
                  g.space(1);
                  g.pushlist();
                  g.text("cn", 0xFFFF80);
                  loopv(spectators) g.textf("%d", 0xFFFFDD, NULL, spectators[i]->clientnum);
                  g.poplist();
  
                  g.poplist();
              }
              else
              {
                  g.textf("%d spectator%s", 0xFFFF80, " ", spectators.length(), spectators.length()!=1 ? "s" : "");
                  loopv(spectators)
                  {
                      if((i%3)==0)
                      {
                          g.pushlist();
                          g.text("", 0xFFFFDD, "spectator");
                      }
                      fpsent *o = spectators[i];
                      if(o==player1 && highlightscore)
                      {
                          g.pushlist();
                          g.background(0x808080);
                      }
                      g.text(colorname(o), statuscolor(o, 0xFFFFDD));
                      if(o==player1 && highlightscore) g.poplist();
                      if(i+1<spectators.length() && (i+1)%3) g.space(1);
                      else g.poplist();
                  }
              }
          }
      }
  
      struct scoreboardgui : g3d_callback
      {
          bool showing;
          vec menupos;
          int menustart;
  
          scoreboardgui() : showing(false) {}
  
          void show(bool on)
          {
              if(!showing && on)
              {
                  menupos = menuinfrontofplayer();
                  menustart = starttime();
              }
              showing = on;
          }
  
          void gui(g3d_gui &g, bool firstpass)
          {
              g.start(menustart, 0.03f, NULL, false);
              renderscoreboard(g, firstpass);
              g.end();
          }
  
          void render()
          {
              if(showing) g3d_addgui(this, menupos, (scoreboard2d ? GUI_FORCE_2D : GUI_2D | GUI_FOLLOW) | GUI_BOTTOM);
          }
  
      } scoreboard;
  
      void g3d_gamemenus()
      {
          scoreboard.render();
      }
  
      VARFN(scoreboard, showscoreboard, 0, 0, 1, scoreboard.show(showscoreboard!=0));
  
      void showscores(bool on)
      {
          showscoreboard = on ? 1 : 0;
          scoreboard.show(on);
      }
      ICOMMAND(showscores, "D", (int *down), showscores(*down!=0));
  
      VARP(hudscore, 0, 0, 1);
      FVARP(hudscorescale, 1e-3f, 1.0f, 1e3f);
      VARP(hudscorealign, -1, 0, 1);
      FVARP(hudscorex, 0, 0.50f, 1);
      FVARP(hudscorey, 0, 0.03f, 1);
      HVARP(hudscoreplayercolour, 0, 0x60A0FF, 0xFFFFFF);
      HVARP(hudscoreenemycolour, 0, 0xFF4040, 0xFFFFFF);
      VARP(hudscorealpha, 0, 255, 255);
      VARP(hudscoresep, 0, 200, 1000);
  
      void drawhudscore(int w, int h)
      {
          int numgroups = groupplayers();
          if(!numgroups) return;
  
          fpsent *p = followingplayer();
          if(!p) p = player1;
          scoregroup *g = groups[0];
          int score = INT_MIN, score2 = INT_MIN;
          bool best = false;
          if(m_teammode)
          {
              score = g->score;
              best = isteam(p->team, g->team);
              if(numgroups > 1)
              {
                  if(best) score2 = groups[1]->score;
                  else for(int i = 1; i < groups.length(); ++i) if(isteam(p->team, groups[i]->team)) { score2 = groups[i]->score; break; }
              }
          }
          else
          {
              score = g->players[0]->frags;
              best = p == g->players[0];
              if(g->players.length() > 1)
              {
                  if(best) score2 = g->players[1]->frags;
                  else score2 = p->frags;
              }
          }
          if(score == score2 && !best) best = true;
  
          score = clamp(score, -999, 9999);
          defformatstring(buf, "%d", score);
          int tw = 0, th = 0;
          text_bounds(buf, tw, th);
  
          string buf2;
          int tw2 = 0, th2 = 0;
          if(score2 > INT_MIN)
          {
              score2 = clamp(score2, -999, 9999);
              formatstring(buf2, "%d", score2);
              text_bounds(buf2, tw2, th2);
          }
  
          int fw = 0, fh = 0;
          text_bounds("00", fw, fh);
          fw = max(fw, max(tw, tw2));
  
          vec2 offset = vec2(hudscorex, hudscorey).mul(vec2(w, h).div(hudscorescale));
          if(hudscorealign == 1) offset.x -= 2*fw + hudscoresep;
          else if(hudscorealign == 0) offset.x -= (2*fw + hudscoresep) / 2.0f;
          vec2 offset2 = offset;
          offset.x += (fw-tw)/2.0f;
          offset.y -= th/2.0f;
          offset2.x += fw + hudscoresep + (fw-tw2)/2.0f;
          offset2.y -= th2/2.0f;
  
          pushhudmatrix();
          hudmatrix.scale(hudscorescale, hudscorescale, 1);
          flushhudmatrix();
  
          int color = hudscoreplayercolour, color2 = hudscoreenemycolour;
          if(!best) swap(color, color2);
  
          draw_text(buf, int(offset.x), int(offset.y), (color>>16)&0xFF, (color>>8)&0xFF, color&0xFF, hudscorealpha);
          if(score2 > INT_MIN) draw_text(buf2, int(offset2.x), int(offset2.y), (color2>>16)&0xFF, (color2>>8)&0xFF, color2&0xFF, hudscorealpha);
  
          pophudmatrix();
      }
  }
  
  