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;
// disable controls during refresh
g_arenaservers.save.generic.flags |= QMF_GRAYED;
g_arenaservers.master.generic.flags |= QMF_GRAYED;
// 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);
case AS_GLOBAL:
g_arenaservers.save.generic.flags &= ~(QMF_INACTIVE|QMF_HIDDEN);
g_arenaservers.remove.generic.flags |= (QMF_INACTIVE|QMF_HIDDEN);
case AS_FAVORITES:
g_arenaservers.save.generic.flags |= (QMF_INACTIVE|QMF_HIDDEN);
g_arenaservers.remove.generic.flags &= ~(QMF_INACTIVE|QMF_HIDDEN);
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!
|