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 browserThe 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 startThe 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 serversWe'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 stuffThe 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 modificationWe 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 codeNobody'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!
|