Click for more information!

Code3Arena

PlanetQuake | Code3Arena | Tutorials | << Prev | Tutorial9 | Next >>

TUTORIAL 9 - Saving your favourite server
by HypoThermia

This tutorial examines the server browser built into Quake3. As it exists at the moment, you have to connect to a server before you can add it to your list of favourites. Not very useful if the server is full.

We'll add a button to the browser that allows a server to be added to the favourites list - easily and quickly. Along the way we'll also fix a bug in the source code.
 

1. Understanding the server browser

The server browser is accessed by selecting a Multiplayer game from the main menu. You can browse servers running on the Internet, MPlayer, locally, or from your list of favourites. Servers that have been queried are displayed in a listbox for selection.

All of the server code is contained in the source file ui/ui_servers2.c. Lets open it up and take a look.
 

1.1. Where to start

The core data structure is arenaservers_t. It carries the data required for each control, intermediate data used while servers are being pinged, and the results that are to be displayed in the browser.
typedef struct {
   menuframework_s   menu;

   menutext_s        banner;

   menulist_s        master;
   menulist_s        gametype;
   menulist_s        sortkey;
   menuradiobutton_s showfull;
   menuradiobutton_s showempty;

   menulist_s        list;
   menubitmap_s      mappic;
   menubitmap_s      arrows;
   menubitmap_s      up;
   menubitmap_s      down;
   menutext_s        status;
   menutext_s        statusbar;

   menubitmap_s      remove;
   menubitmap_s      back;
   menubitmap_s      refresh;
   menubitmap_s      specify;
   menubitmap_s      create;
   menubitmap_s      go;

   pinglist_t        pinglist[MAX_PINGREQUESTS];
   table_t           table[MAX_LISTBOXITEMS];
   char*             items[MAX_LISTBOXITEMS];
   int               numqueriedservers;
   int               *numservers;
   servernode_t      *serverlist;
   int               currentping;
   qboolean          refreshservers;
   int               nextpingtime;
   int               maxservers;
   int               refreshtime;
   char              favoriteaddresses[MAX_FAVORITESERVERS][MAX_ADDRESSLENGTH];
   int               numfavoriteaddresses;
} arenaservers_t;

static arenaservers_t   g_arenaservers;

Of immediate interest to us is the list of favourite servers (favoriteaddresses[][] and numfavoriteaddresses). This data is loaded from q3config.cfg by the function ArenaServers_LoadFavorites() when the browser controls are initialized in ArenaServers_MenuInit(). Any previously cached data is used - there might have been changes to the configuration file from elsewhere.
 

1.2. Lists and lists of servers

We'll also need to know how the lists of server information are stored, and how they're built for display in the browser listbox.
static servernode_t g_globalserverlist[MAX_GLOBALSERVERS];
static int g_numglobalservers;
static servernode_t g_localserverlist[MAX_LOCALSERVERS];
static int g_numlocalservers;
static servernode_t g_favoriteserverlist[MAX_FAVORITESERVERS];
static int g_numfavoriteservers;
static servernode_t g_mplayerserverlist[MAX_GLOBALSERVERS];
static int g_nummplayerservers;

Each of these arrays carries the information on the the servers successfully pinged, and there is an associated variable to indicate how full they are. When you change the server type the aliases serverlist and numservers in g_arenaservers are updated to the appropriate array and its' size. This means generic functions can be written that automatically access the correct data list. The handover is done in

void ArenaServers_SetType(int type)
Notice also that the Delete button - only visible when looking at favourite servers - is enabled and disabled in this function.
 

1.3. Other interesting stuff

The update to the list of servers displayed in the browser listbox is done by
static void ArenaServers_UpdateMenu(void)
It does all of the sorting and filtering, storing the results in g_arenaservers.table[] for display. It also enables and disables controls when the server refresh is starting or has finished.

Finally, when an event occurs in the browser - a button is pushed for example - then an event message is sent to the message handler:

static void ArenaServers_Event( void* ptr, int event )

2. Designing the modification

We need to update the favourites list by adding server details from the other lists. A "Save" control is needed to do this. In fact, since we don't need to add something on the favourites list back to itself, we can place the control as a button in the same place as the existing Delete button.

The modifications we need to make are:

  • Cache the button graphics,
  • add the "Save" button to the display,
  • ensure the save button behaves properly,
  • save the highlighted server to the list of favourites,
  • tie the button pressed event to the save.
Let's get to it...
 

3. Coding the changes

These coding changes assume a "virgin" installation of ui/ui_servers2.c. All modifications are to this file only.
 

3.1. Adding the button

We'll start by getting the button graphics set up. The button we're going to use is already available to us in the PAK0.PK3 file, so we just need to define an alias near the start of the code, and a unique identifier:

#define ART_REMOVE0 "menu/art/delete_0"
#define ART_REMOVE1 "menu/art/delete_1"
#define ART_SAVE0   "menu/art/save_0"
#define ART_SAVE1   "menu/art/save_1"

The two values save_0 and save_1 are the art for normal and selected states respectively.

The identifier follows on in sequence to the existing controls:

#define ID_CONNECT 22
#define ID_REMOVE 23
#define ID_SAVE 24

The graphics then need to be loaded for display:

/*
=================
ArenaServers_Cache
=================
*/
void ArenaServers_Cache( void ) {
   trap_R_RegisterShaderNoMip( ART_SAVE0);
   trap_R_RegisterShaderNoMip( ART_SAVE1);
   trap_R_RegisterShaderNoMip( ART_BACK0 );
   trap_R_RegisterShaderNoMip( ART_BACK1 );

Adding the save button to the display isn't too difficult, first we add the button details to arenaservers_t:

typedef struct {
   menuframework_s    menu;

   menutext_s         banner;

   menulist_s         master;
   menulist_s         gametype;
   menulist_s         sortkey;
   menuradiobutton_s  showfull;
   menuradiobutton_s  showempty;

   menulist_s         list;
   menubitmap_s       save;
   menubitmap_s       mappic;

Then we copy the code for the existing delete button and make make it refer to our new button:

/*
=================
ArenaServers_MenuInit
=================
*/
g_arenaservers.remove.width = 128;
g_arenaservers.remove.height = 64;
g_arenaservers.remove.focuspic = ART_REMOVE1;

g_arenaservers.save.generic.type = MTYPE_BITMAP;
g_arenaservers.save.generic.name = ART_SAVE0;
g_arenaservers.save.generic.flags = QMF_LEFT_JUSTIFY|QMF_PULSEIFFOCUS;
g_arenaservers.save.generic.callback = ArenaServers_Event;
g_arenaservers.save.generic.id = ID_SAVE;
g_arenaservers.save.generic.x = 440;
g_arenaservers.save.generic.y = 88;
g_arenaservers.save.width = 128;
g_arenaservers.save.height = 64;
g_arenaservers.save.focuspic = ART_SAVE1;

We only had to change the generic.name, generic.id and focuspic. The position and default flags stay the same.

The button then needs to be registered for display. Again in ArenaServers_MenuInit():

Menu_AddItem( &g_arenaservers.menu, (void*) &g_arenaservers.remove);
Menu_AddItem( &g_arenaservers.menu, (void*) &g_arenaservers.save);
Menu_AddItem( &g_arenaservers.menu, (void*) &g_arenaservers.back);

3.2. Setting the button behaviour

The save button only needs to be active when the server list is no longer refreshing, and visible when the delete button isn't. We make three changes to the state of the button in this function:

/*
=================
ArenaServers_UpdateMenu
=================
*/

// all servers pinged - enable controls
g_arenaservers.save.generic.flags &= ~QMF_GRAYED;
g_arenaservers.master.generic.flags &= ~QMF_GRAYED;

and:

// disable controls during refresh
g_arenaservers.save.generic.flags |= QMF_GRAYED;
g_arenaservers.master.generic.flags |= QMF_GRAYED;

and:

// end of refresh - set control state
g_arenaservers.save.generic.flags |= QMF_GRAYED;
g_arenaservers.master.generic.flags &= ~QMF_GRAYED;

We then handle the hiding and showing of the button, this time in four places in this function:

/*
=================
ArenaServers_SetType
=================
*/

case AS_LOCAL:
   g_arenaservers.save.generic.flags &= ~(QMF_INACTIVE|QMF_HIDDEN);
   g_arenaservers.remove.generic.flags |= (QMF_INACTIVE|QMF_HIDDEN);

and:

case AS_GLOBAL:
   g_arenaservers.save.generic.flags &= ~(QMF_INACTIVE|QMF_HIDDEN);
   g_arenaservers.remove.generic.flags |= (QMF_INACTIVE|QMF_HIDDEN);

and:

case AS_FAVORITES:
   g_arenaservers.save.generic.flags |= (QMF_INACTIVE|QMF_HIDDEN);
   g_arenaservers.remove.generic.flags &= ~(QMF_INACTIVE|QMF_HIDDEN);

and the fourth change:

case AS_MPLAYER:
   g_arenaservers.save.generic.flags &= ~(QMF_INACTIVE|QMF_HIDDEN);
   g_arenaservers.remove.generic.flags |= (QMF_INACTIVE|QMF_HIDDEN);

If you test and run the code at this stage you should find that the button is present and behaves, but has no functionality.
 

3.3. Implementing the button function

Our final two code changes link the events generated by the button to the actual addition of the current server to the favourites.

We first modify the event handler to include a response to our button:

/*
=================
ArenaServers_Event
=================
*/

case ID_REMOVE:
   ArenaServers_Remove();
   ArenaServers_UpdateMenu();
   break;

case ID_SAVE:
   ArenaServers_AddToFavorites();
   ArenaServers_SaveChanges();
   break;

and finally add the new function that handles the event. Note that, as a static function, we can avoid using a declaration for it if we place the definition before it's first use. I placed it just before the first function ArenaServers_MaxPing().

Notice that once we have adjusted the favourites list we save it immediately to q3config.cfg file using ArenaServers_SaveChanges(). We don't need to update the menu because we've been modifying a list that isn't displayed at the moment. The behaviour of the button outlined earlier guarantees this.

/*
=================
ArenaServers_AddToFavorites
=================
*/
static void ArenaServers_AddToFavorites(void)
{
   servernode_t* servernodeptr;
   int i;

   // check favourite server list isn't full
   if (g_numfavoriteservers == MAX_FAVORITESERVERS)
      return;

   // check we have a server list available
   if (!g_arenaservers.list.numitems)
      return;

   // check the server isn't on the favourites list already
   servernodeptr=g_arenaservers.table[g_arenaservers.list.curvalue].servernode;
   for (i=0; i < g_numfavoriteservers; i++)
   if (!Q_stricmp(g_arenaservers.favoriteaddresses[i],servernodeptr->adrstr))
      return;

   // we already have a responsive server, no further sanity checks required
   strcpy(g_arenaservers.favoriteaddresses[g_numfavoriteservers],
      servernodeptr->adrstr);

   // copy over server details
   memcpy( &g_favoriteserverlist[g_numfavoriteservers],
      servernodeptr,sizeof(servernode_t));

   g_numfavoriteservers++;
   g_arenaservers.numfavoriteaddresses = g_numfavoriteservers;
}

The function starts by performing some checks that the copying to the favourite list can be done. Details on the currently selected server node are stored in a pointer for easy access, and we check for duplicates on the list of favourites. As the server is already in the current display list we know it has recent details for us to copy over.

We have to update both the list of favourite servers stored in g_arenaservers, and the array outside g_arenaservers that holds a copy of the server details (if the server has been recently browsed).

Notice that there are no variables being changed that refer to the current list being displayed.

That's (almost) it!
 

4. Fixing a bug in the source code

Nobody's perfect. If you've played around with the browser you might have noticed there's a small problem in the code.

Sometimes, after a favourite server has been deleted, you lose another server and get a "phantom" one instead. It doesn't respond to a refresh either. If you make a note of the values in q3config.cfg before and after you might see that some of the IP addresses are being corrupted.

Lets fix this with a little detective work.

The problem appears to be caused by deleting a server. It must be in the values that are saved to q3config.cfg, and a quick look at ArenaServers_SaveChanges() shows it must be tampering in g_arenaservers.favoriteaddresses[].

The first place to start is in the function that does this work. It's ArenaServers_Remove(). Here are the lines that modify g_arenaservers.favouriteaddresses[]:

// delete address from master list
if (i <= g_arenaservers.numfavoriteaddresses-1)
{
   if (i < g_arenaservers.numfavoriteaddresses-1)
   {
      // shift items up
      memcpy( &g_arenaservers.favoriteaddresses[i],
      &g_arenaservers.favoriteaddresses[i+1],
      (g_arenaservers.numfavoriteaddresses - i - 1)*
      sizeof(MAX_ADDRESSLENGTH));
   }
   g_arenaservers.numfavoriteaddresses--;
}

Have you seen it?

The last argument of the memcpy() function is the amount of memory to move. As each address is stored in an array of size MAX_ADDRESSLENGTH then using sizeof() will move too little memory.

Adjust the final argument to this:

(g_arenaservers.numfavoriteaddresses - i - 1)*MAX_ADDRESSLENGTH

Done. Your server browser will now be working better than ever before!

PlanetQuake | Code3Arena | Tutorials | << Prev | Tutorial9 | Next >>