TUTORIAL 9 - Saving your favourite server

von HypoThermia


Dieses Tutorial untersucht den eingebauten Serverbrowser von Quake3. So wie dieser momentan funktioniert, musst du erst mit einem Server verbunden sein, bevor du ihn in deine Favoritenliste aufnehmen kannst. Nicht sehr brauchbar, wenn der Server voll ist...

Wir wollen dem Browser einen Knopf hinzufügen, der Server einfach und schnell in der Favoritenliste speichert. Dabei werden wir auch gleich einen Fehler im Quellcode korrigieren.

1. DEN SERVERBROWSER VERSTEHEN
Den Serverbrowser kann man über die Auswahl von "Multiplayer" aus dem Hauptmenü erreichen. Du kannst Server aus dem Internet, MPlayer, lokalem Netzwerk oder aus deiner Favoritenliste anbrowsen. Abgefragte Server sind in einer Tabelle zur Auswahl dargestellt. Der gesamte Serverbrowsercode befindet sich in der Datei q3_ui/ui_server2.c. Lasst uns also diese öffnen und untersuchen.

1.1. Womit anfangen?
Eine sehr wichtige Struktur ist arenaservers_t. Diese beinhaltet alle notwendigen Daten für die Steuerung, die mit dem Server ausgetauschten Daten während des Abfragens, und die Ergebnisse, die dann im Browser dargestellt werden.

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;
menulist_s punkbuster;
menubitmap_s pblogo;
} arenaservers_t;
static arenaservers_t g_arenaservers;

Für uns ist dabei die Liste mit den favorisierten Servern (favoriteaddresses[][] und numfavoriteaddresses) am interessantesten. Diese Daten werden aus der q3config.cfg durch die Funktion ArenaServers_LoadFavorites() geladen, sobald die Browsersteuerung durch die Funktion ArenaServers_MenuInit() initialisiert wird. Es ist dabei jedoch zu beachten, dass diese Daten zuvor schon gecacht wurden, und von daher externe Änderungen mit z.B. einem Texteditor an der q3config.cfg während des Quake3-Betriebs nicht berücksichtigt werden.

1.2. Listen und Serverlisten
Wir müssen zudem wissen, wie die Informationen der Serverlisten gespeichert werden, und wie sie zur Anzeige im Browserfenster aufgebaut werden.

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;

Jedes dieser Felder beinhaltet Informationen zu erfolgreich gepingten ("lebenden") Servern, insbesondere auch wie voll diese sind. Wenn man zwischen den verschiedenen Serverarten (Lokal, Internet...) im Menü hin- und herschaltet, werden automatisch die korrekte "serverlist" und "numservers" in g_arenaservers auf das entsprechende Feld mit seiner Größe aktualisiert. Das bedeuted für uns, dass wir allgemeine Funktionen zum Zugriff auf die Datenlisten schreiben können. Die Übergabe passiert in

void ArenaServers_SetType(int type)

Man beachte, dass zudem der "Delete"-Knopf, der nur bei den favorisierten Servern sichtbar ist, auch durch diese Funktion ein- und ausgeschalten wird.

1.3. Andere interessante Sachen
Das Update der angezeigten Serverliste im Browser wird duch folgende Funktion generiert:

static void ArenaServers_UpdateMenu(void)

Diese erledigt alle Sortierungen und Filtererinschränkungen bevor sie die Ergebnisse in g_arenaservers.table[] zum Anzeigen speichert. Zudem werden die Buttons ein- bzw. ausgeschalten, während der Server Refresh stattfindet oder beendet wurde.

Wenn ein Ereignis im Browser ausgelöst wird - z.B. durch das Drücken eines Knopfes - so wird eine Ereignisnachricht (event message) an den sogenannten "message handler" geschickt, eine Funktion die Ereignisse verarbeitet.

static void ArenaServers_Event( void* ptr, int event )

2. DIE ÄNDERUNGEN DESIGNEN
Wir müssen die Favoritenliste durch weitere Details aus den anderen Listen ergänzen. Zum Abspeichern brauchen wir einen "Save" Knopf.

Diesen Knopf können wir bei der Wahl nach Internet Servern auf der gleichen Position speichern, die der "Delete" Knopf in der Favoritenliste hat.

Wir werden folgendes tun:

Die Graphik für den Knopf laden (bzw. cachen), den "Save" Knopf zur Anzeige hinzufügen, sicherstellen, dass der "Save" Knopf richtig funktioniert, d.h. dass der markierte Server in die Liste der Favoriten gespeichert wird, indem durch den Knopfdrück ein Ereignis (event) ausgelöst wird.


3. DIE ÄNDERUNGEN PROGRAMMIEREN
Die Änderungen im Code setzen eine "jungfräuliche" Version von ui/ui_servers2.c voraus. Alle Änderungen finden nur in dieser Datei statt.

3.1. Den Knopf hinzufügen
Wir beginnen damit, die Grafik für den Knopf einzustellen. Durch einen glücklichen Zufall ;o) befindet sich unser Knopf bereits in der Datei pak0.pk3, von daher müssen wir uns nur ein Alias (einen eindeutigen Bezeichner) am Anfang der Datei erstellen:

#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"

Die zwei Werte save_0 und save_1 bzw. irgendwas_0 und irgendwas_1 werden normalerweise für den inaktiven Zustand im Falle von _0 und für den aktiven Zustand im Falle von _1 benutzt.

Dazu benötigen wir zur Identifikation folgendes:

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

Die Grafik muß nun gecacht werden:

/*
=================
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 );

Der Vorgang zum Anzeigen des Knopfes ist nicht sonderlich schwer. Zuerst muß der Knopf in die Struktur arenaservers_t eingetragen werden:

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;

Dann kopieren wir den Code für den bereits bestehenden "Delete" Knopf und lassen diesen als unseren neuen Knopf arbeiten:

/*
=================
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;

Wir mussten lediglich generic.name, generic.id und focuspic abändern. Die Position und die Flags bleiben identisch.

Schließlich muß man den Knopf noch zur Anzeige freigeben. Dies geschieht wieder 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. Das Verhalten des neuen Knopfes
Der Save-Button muß nur dann sichtbar sein, wenn die Serverliste nicht gerade neu überprüft wird, und er ist nicht sichtbar während der Delete-Button es ist. Für dieses Verhalten nehmen wir folgende Änderungen vor:

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

und:

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

und:

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

Nun steuern wir das Auftauchen und Verschwinden unseres Knopfes. Dieses benötigt vier Änderungen:

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

und:

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

und:

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

und die vierte Änderung:

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

Wenn ihr Lust habt, könnt ihr jetzt schonmal die Änderungen testen. Der Button sollte nun schon an der gewünschten Stelle sichtbar sein, er hat allerdings noch keinerlei Funktionalität.

3.3. Die Funktionalität einbauen
Die letzten zwei Änderungen im Code verknüpfen das Knopfdruck-Ereignis mit dem Hinzufügen des neuen Servers in die Favoritenliste.

Zuerst ändern wir den "Event Handler" ab, damit er auf unseren Knopfdruck eine Antwort ausgibt:

/*
=================
ArenaServers_Event
=================
*/
case ID_REMOVE:
ArenaServers_Remove();
ArenaServers_UpdateMenu();
break;
case ID_SAVE:
ArenaServers_AddToFavorites();
ArenaServers_SaveChanges();
break;

Schließlich fügen wir die Funktion hinzu, die vom "Event Handler" aus zum Weiterverarbeiten aufgerufen wird. Es ist zu beachten, dass wir den Prototyp dieser statischen Funktion nicht extra deklarieren müssen, wenn wir die Funktion vor ihrem ersten Aufruf im Code platzieren. Es bietet sich daher an, diese z.B. vor die erste Funktion in der Datei zu schreiben (also vor ArenaServers_MaxPing()).

Sicher hast du schon beobachtet, dass wir sobald wir einen neuen Servereintrag in der Favoritenliste haben, wir dies sofort in der Datei q3config.cfg durch den Aufruf der Funktion ArenaServers_SaveChanges() speichern. Wir müssen in diesem Moment nicht extra das Menü aktualisieren, da wir nur eine Liste modifiziert haben, die in diesem Augenblick ohnehin nicht angezeigt wird. Doch nun aber zu der letzten Änderung:

/*
=================
ArenaServers_AddToFavorites
=================
*/

static void ArenaServers_AddToFavorites(void) {
servernode_t* servernodeptr;
int i;
// sicherstellen, dass die Liste der favorisierten Server nicht voll ist
if (g_numfavoriteservers == MAX_FAVORITESERVERS)
return;
// überprüfen, ob momentan eine Serverliste überhaupt zur Verfügung steht
if (!g_arenaservers.list.numitems)
return;
// überprüfen, ob der Server schon in der favorisierten Serverliste steht
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;
// kopieren der Serverdetails
strcpy(g_arenaservers.favoriteaddresses[g_numfavoriteservers], servernodeptr->adrstr);
memcpy( &g_favoriteserverlist[g_numfavoriteservers], servernodeptr, sizeof(servernode_t));
g_numfavoriteservers++;
g_arenaservers.numfavoriteaddresses = g_numfavoriteservers;
}

Die Funktion beginnt mit einigen Überprüfungen, ob der Server überhaupt abgespeichert werden kann. Die Details des Servers werden zum einfacheren Zugriff mit einem Zeiger auf die Struktur servernode_t gespeichert. Mit diesem wird die Liste zur Suche nach Duplikaten durchlaufen. Da der Server sich bereits in der angezeigten Liste am Bildschirm befindet, können wir von diesem einfach die notwendigen Details in die Liste der favorisierten Server kopieren.

Danach müssen wir sowohl die aktuelle Anzahl der Server in der favorisierten Serverliste in g_arenaservers, als auch in der außerhalb davon gespeicherten Variable g_numfavoriteservers erhöhen.

Man beachte: Es wird keine einzige Variable geändert, die sich auf die aktuell angezeigte Liste bezieht.

Okay, das war es (fast)!


4. EINEN FEHLER IM QUELLCODE BEHEBEN
Nobody's perfect. Nicht einmal John Carmack. Wenn du schon öfter mit dem Serverbrowser herumgespielt hast, ist dir vielleicht schonmal ein kleines Problem aufgefallen. Manchmal, nachdem man einen favorisierten Server löscht, wird noch ein anderer Server gelöscht, und man bekommt irgend etwas Seltsames dafür. Bei einem Refresh gibt es ebenfalls keine Besserung dieses Problems. Schaut man sich nun die q3config.cfg näher an, so bemerkt man, dass die gespeichrten IP Adressen ungültig sind.

Diesen Fehler behebt man mit ein wenig Detektivarbeit.

Der Fehler tritt also auf, wenn man einen Server löscht. Es hat etwas mit den Werten in q3config.cfg zu tun, und ein kurzer Blick in void ArenaServers_SaveChanges( void ) zeigt, dass die Daten in g_arenaservers.favoriteaddresses[] liegen.

Blicken wir in die Funktion zum Löschen der Server, es ist static void ArenaServers_Remove( void ). Hier sind die Zeilen, die etwas mit g_arenaservers.favoriteaddresses[] anstellen:

// 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--;
}

Hast du den Fehler entdeckt?

Das letzte Argument in memcpy() gibt an, wieviel Speicher kopiert werden soll. Nachdem jedes Array eine Größe von MAX_ADDRESSLENGTH hat, liefert der Befehl sizeof(MAX_ADDRESSLENGTH) einen viel zu kleinen Wert zurück. sizeof() dient normal zur Bestimmung der Größen von Variablen oder Strukturen. MAX_ADDRESSLENGTH steht allerdings nur für den Zahlenwert 64, der Aufruf sizeof(64) liefert nichts Sinnvolles zurück.

Also ändert man den hinteren Teil einfach auf:

(g_arenaservers.numfavoriteaddresses - i - 1)*MAX_ADDRESSLENGTH

Fertig! Der Serverbrowser wird nun besser als je zuvor funktionieren!