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!
|