Fed up of joining a game part the way through and being unable to win it? Want to show that you’re up there with the best… or that you could have won that match?

Lets take a look at the scoreboard and see how we can get it showing your frags per minute. Along the way we’ll find out about timing and a little on how the server keeps all the clients updated.

1. Working out the fragrate

Start by having a look at cg_scoreboard.c in the cgame directory. It contains all the code used to draw the scoreboard (surprise!), and compared to other source files is quite straightforward to understand. After all we have a very clear picture of how a scoreboard works – don’t we?

The first function that we see is CG_DrawClientScore. This simply draws a line of information that the client knows about the player. Things like the model picture, frags, ping, time connected, and name. It’s made a little more complicated because version 1.15 introduced a second, smaller, scoreboard for a larger number of players.

We have the information we need here to construct a frag rate per minute: the number of frags and the time connected (score->time). Unfortunately this is in minutes: it doesn’t change quickly enough to be useful. Let’s track back to where this information is set and see what we can do.

CG_DrawClientScore is called from CG_TeamScoreboard, and we can see that players are grouped into “teams”: TEAM_RED, TEAM_BLUE, TEAM_FREE and TEAM_SPECTATOR. It also appears that the list is already sorted for us, and there’s nothing in cg_scoreboard.c doing that for us. The stats are all stored in the array cg.scores[] so we need to find where this is modified.

Found it yet? A search shows that it’s only modified in cg_servercmds.c, in response to the server sending out a scores command. All commands sent by the server are processed in this file: so all the sorting and ordering must be done in the server. This makes sense: the server carries the master records and arbitrates the game.

Moving over to the code in the game directory we see that the scores command is sent out by DeathmatchScoreboardMessage() in g_cmds.c. This file handles commands sent to the server by the client (one of which is a request to update the scores stored by the client). This information is only sent out if there’s a scoreboard to update.

Com_sprintf (entry, sizeof(entry),
  " %i %i %i %i %i %i", level.sortedClients[i],
  cl->ps.persistant[PERS_SCORE], ping, (level.time - cl->pers.enterTime)/60000,
  scoreFlags, g_entities[level.sortedClients[i]].s.powerups);

The scores are sent out as a long string of numbers. We can see that the 4th number in each group of six is the time connected to the server in minutes. The “divide by 60000” occurs because the standard unit of time in Quake3 is the millisecond – the conversion to minutes is made at this point.

2. The changes we need to make

We could adjust DeathmatchScoreboardMessage() to send out a calculated frag rate as well, but there’s a more efficient way. Since the number of frags and the time connected are already sent, we’ll just adjust these instead. If we change the time connected to seconds we can then get a fragrate that updates with an accuracy every second (at best).

We’ll store the fragrate in the client as an integer: the actual fragrate per minute multiplied up by 100. I’ve also imposed an (arbitrary) minimum 10 seconds for caclulating the fragrate. This keeps any “logon and frag” luck to a minimum.

The scoreboard will have to be re-sorted by fragrate: done in the client because this is a client preference. This preference will be stored in a system variable.

We’re almost there. One more issue we need to concern ourselves with: the scoreboard can be updated during the intermission (when someone disconnects for example), so we don’t want our time information to keep on changing during this period.

After a little digging around we find that there’s a variable already set up to help us: level.intermissiontime. It’s set to zero when the server is playing a level, and marks the time (in milliseconds) at which the intermission started. It helps the server time the duration of the intermission – and we can make use of it too.

Let’s get our hands dirty.

3. Coding the changes

First of all we’ll make the only server modification that’s required (g_cmds.c) so the connect time is sent in seconds (remembering that the unit of time is the millisecond in Quake3). The use of level.intermissiontime prevents our fragrate times from ticking down while we’re in the intermission.

/*
==================
DeathmatchScoreboardMessage

==================
*/
void DeathmatchScoreboardMessage( gentity_t *ent ) {
   char      entry[1024];
   char      string[1400];
   int         stringlength;
   int         i, j;
   gclient_t   *cl;
   int         numSorted;
   int         scoreFlags;
   int       playtime;

   // send the latest information on all clients
   string[0] = 0;
   stringlength = 0;
   scoreFlags = 0;

   // don't send more than 32 scores (FIXME?)
   numSorted = level.numConnectedClients;
   if ( numSorted > 32 ) {
      numSorted = 32;
   }

   for (i=0 ; i < numSorted ; i++) {
      int      ping;

      cl = &level.clients[level.sortedClients[i]];

      if ( cl->pers.connected == CON_CONNECTING ) {
         ping = -1;
      } else {
         ping = cl->ps.ping < 999 ? cl->ps.ping : 999;
      }

      // HypoThermia: get the correct time (rate shouldn't change
      // during intermission) 
      playtime = level.time;
      if (level.intermissiontime)
         playtime = level.intermissiontime;

      // Hypothermia: send over time in seconds instead of minutes
      Com_sprintf (entry, sizeof(entry),
         " %i %i %i %i %i %i", level.sortedClients[i],
         cl->ps.persistant[PERS_SCORE], ping, (playtime - cl->pers.enterTime)/1000,
         scoreFlags, g_entities[level.sortedClients[i]].s.powerups);
      j = strlen(entry);
      if (stringlength + j > 1024)
         break;
      strcpy (string + stringlength, entry);
      stringlength += j;
   }

   trap_SendServerCommand( ent-g_entities, va("scores %i %i %i%s", i,
      level.teamScores[TEAM_RED], level.teamScores[TEAM_BLUE],
      string ) );
}

 

From now on it’s client side stuff. We need a new variable in the score_t struct to store the fragrate (cg_local.h):

typedef struct {
   int            client;
   int            score;
   int            ping;
   int            time;
   int            scoreFlags;
   int             fragrate;
} score_t;

 

Next we’re updating cg_servercmds.c so it handles the changed connect time. This is also a good place to calculate the fragrate. Remember that the time is now in seconds and that fragrate stores 100 times the number of frags per minute.

This is in CG_ParseScores():

cg.scores[i].time = atoi( CG_Argv( i * 6 + 7 ) );
cg.scores[i].scoreFlags = atoi( CG_Argv( i * 6 + 8 ) );
powerups = atoi( CG_Argv( i * 6 + 9 ) );

// HypoThermia: fragrate based on minimum of 10 seconds
if (cg.scores[i].time < 10)
   cg.scores[i].fragrate = 600 * cg.scores[i].score;
else
   cg.scores[i].fragrate = (6000 * cg.scores[i].score) / cg.scores[i].time;

// HypoThermia: correct time value back to minutes
cg.scores[i].time /= 60;

if ( cg.scores[i].client < 0 || cg.scores[i].client >= MAX_CLIENTS ) {
   cg.scores[i].client = 0;
}

 

With all the information in place we now need to adapt the scoreboard so it’ll display the fragrate. First of all, though, we’ll add a system variable that stores the type of scoreboard we’ve chosen to display: Back in cg_local.h we add a reference to a global variable:

extern   vmCvar_t      cg_blood;
extern   vmCvar_t      cg_predictItems;
extern   vmCvar_t      cg_deferPlayers;
extern   vmCvar_t      cg_fragRateScoreboard;

 

and the static declaration of this variable into cg_main.c:

vmCvar_t   cg_deferPlayers;
vmCvar_t   cg_drawTeamOverlay;
vmCvar_t   cg_teamOverlayUserinfo;
vmCvar_t   cg_fragRateScoreboard;

and finally link it into the list of client commands (still in cg_main.c). Notice that we don’t need to do any more: we automatically get TAB completion in the console, and the variable is saved from session to session by using the flag CVAR_ARCHIVE.

{ &cg_drawTeamOverlay, "cg_drawTeamOverlay", "0", CVAR_ARCHIVE },
{ &cg_teamOverlayUserinfo, "teamoverlay", "0", CVAR_ROM | CVAR_USERINFO },
{ &cg_stats, "cg_stats", "0", 0 },
{ &cg_fragRateScoreboard, "cg_fragRateScoreboard", "0", CVAR_ARCHIVE },

This variable has a default value of 0. When set to 1 the fragrate will be displayed in the scoreboard.
Finally we get to make the changes to the scoreboard display. We need to convert the fragrate back into a “floating point” display format, and to sort the scoreboard into the correct order.

First the display of the scoreboard. Notice that the changes to the “connecting” and “SPECT” formating strings realign the names properly. We also have to make sure we don’t overstep the formatting limits, and handle the negative numbers properly. We also have to handle the case where the decimal part is less that ten, otherwise we’d get things like 5.9 instead of 5.09.

I don’t think anyone is going to reach 99.99 frags per minute so we cap it there, nor are they going to get a suicide rate below about 10 per minute.

These changes are in cg_scoreboard.c in CG_DrawClientScore().

// draw the score line
if ( score->ping == -1 ) {
   Com_sprintf(string, sizeof(string),
      " connecting     %s", ci->name);
} else if ( ci->team == TEAM_SPECTATOR ) {
   Com_sprintf(string, sizeof(string),
      " SPECT%4i %4i %s", score->ping, score->time, ci->name);
} else if (cg_fragRateScoreboard.integer) {
   // HypoThermia: display fractional fragrate
   int whole,frac;
   char* fmt;   // format string used

   if (score->fragrate < 0)
   {
      frac = ( -score->fragrate) % 100;
      whole = -( -score->fragrate - frac) / 100;
   }
   else if (score->fragrate < 9999)
   {
      frac = score->fragrate % 100;
      whole = score->fragrate / 100;
   }
   else
   {
      whole = 99;
      frac = 99;
   }

   if (frac < 10)
      fmt = "%2i.0%1i %4i %4i %s";
   else
      fmt = "%2i.%2i %4i %4i %s";

   Com_sprintf(string, sizeof(string),
      fmt, whole, frac, score->ping, score->time, ci->name);
}
else {
   Com_sprintf(string, sizeof(string),
      "%5i %4i %4i %s", score->score, score->ping, score->time, ci->name);
}

and a small modification to the highlight showing your score so it doesn’t leave a text overhang on the left:

} else {
   hcolor[0] = 0.7;
   hcolor[1] = 0.7;
   hcolor[2] = 0.7;
}

hcolor[3] = fade * 0.7;
CG_FillRect( SB_SCORELINE_X, y,
   640 - SB_SCORELINE_X - BIGCHAR_WIDTH, BIGCHAR_HEIGHT+1, hcolor );

Last of all we need to sort the scores. As the results are already sorted by total frags, we need to re-sort it for fragrate. I’ve only done this for standard deathmatch (TEAM_FREE), it doesn’t really make much sense for other game types. The changes go into CG_TeamScoreboard() as this constructs each type of team for the scoreboard.

Instead of checking each item on the list and displaying only the correct ones (as the original CG_TeamScoreboard() did), we construct a list of items to be displayed. This list is sorted if needed, and then displayed. The variable count has been removed, it returns the number of lines drawn, and has been superceded by indexcount.

Although there is a sort being performed each frame the scoreboard is drawn, it’s a quick one (rarely more than 16 items), and only moves pointers around in an array.

/*
=================
CG_TeamScoreboard
=================
*/
static int CG_TeamScoreboard( int y, team_t team, float fade, int maxClients, int lineHeight ) {
   int      i, j;
   score_t   *score;
   float   color[4];
   clientInfo_t   *ci;
   score_t * scorelist[MAX_CLIENTS];
   int indexcount;

   color[0] = color[1] = color[2] = 1.0;
   color[3] = fade;

   // HypoThermia: build an indexed array into given team type
   indexcount = 0;
   for ( i = 0; i < cg.numScores && indexcount < maxClients; i++ )
   {
      score = &cg.scores[i];
      if ( team != cgs.clientinfo[ score->client ].team )
         continue;

      scorelist[indexcount] = score;
      indexcount++;
   }

   // HypoThermia: sort the score by frag rate for FREE players only
   // use a quick and dirty sort because we're moving pointers around
   if ( team == TEAM_FREE && cg_fragRateScoreboard.integer)
   {
      for ( i = 0; i < indexcount - 1; i++ )
         for ( j = i + 1; j < indexcount; j++ )
            if (scorelist[j]->fragrate > scorelist[i]->fragrate)
            {
               score_t *t;
               t = scorelist[i];
               scorelist[i] = scorelist[j];
               scorelist[j] = t;
            }
   }

   for ( i = 0 ; i < indexcount ; i++ ) {
      CG_DrawClientScore( y + lineHeight * i, scorelist[i], color, fade, lineHeight == SB_NORMAL_HEIGHT );
   }

   return indexcount;
}

That’s it! Compile the changes and try out the new scoreboard.

4. Following on

There are a number of changes that you might want to make. Displaying the fragrate and total number of frags is easily done, but the original scoreboard is too wide to do this. You’ll have to drop back to permanently using the scoreboard for large numbers of players because of its smaller font.

Of more interest is a game that relies only on fragrate to determine the winner. This requires more server side modifications, and the trick of storing 100 times the actual fragrate in an integer may be useful.

Finally you might want a fragrate on your HUD. This is a little more involved as this tutorial uses information that is only sent when the scoreboard is displayed.