*Code color conventions: red = original code; green = author's optional comments; blue = new code
This document is not primarily a tutorial, but instructions for implementing my .dll mod in your source. Although a lot of "tutorials" out there do this, I do not really call it a tutorial, but a procedure (or recipe, if you will). Once I have some time to do so, I will (or anyone else can) break down the code and explain it in detail, but I tried to be generous with my inline comments.
*Note: Version 1.01 of this document was written for v3.05 of id's source release, and it only worked with listen servers. Now that we have v3.14 of the source, user-defined dedicated server commands are now available. Also, when you run a listen server, you are actually running a server AND a client -- and on some systems (including mine), you can actually get a performance gain by running a dedicated server, then running a SEPARATE client to connect. Therefore, I have rewritten this code to ONLY be controlled by a dedicated server. If you want to control it from a client, you can set a rcon_password from dedicated console, then use "rcon <password> sv maplist <whatever>" for your command line. Also, from my tests, it seems that aliases on the server do not work properly with user-defined commands. But if you find a way, let me know ;)
Now then, on with the code implementation. You will be making modifications to the following files:
You will notice that some of these files are extremely small, but they provide modularity, and plenty of room for future expansion. So lets get started!
q_shared.h
(modification)
Just add one line at the end of the #define
DF_* section (around line 855):
Other mod authors will eventually add entries to this list. DF_MAP_LIST does not have to be 65536, but it must be an unused power of 2 that an unsigned int can hold. (It's a bit-positioned value) All this really does is give our code a bit/slot in dmflags->value to flag when a maplist is in effect.
g_local.h
(modification)
Add the following lines, just after the function prototypes for g_main.c
(around line 754):
//
// fileio.c
//
#include "fileio.h"
//
// maplist.c
//
#include "maplist.h"
//LAC---
//============================================================================
// client_t->anim_priority
#define ANIM_BASIC
0 // stand / run
...
g_cmds.c
(modification)
In ClientCommand(), you can insert
these lines basically anywhere in the else
if() statements, as long as you obey the structure of the
existing blocks (i.e. put it after the {}). I chose to put it just
before that last else statement
(around line 905 -- the end of the file):
g_svcmds.c
(modification)
Basically the same modification that we just did to g_cmds.c.
Except this change enables the dedicated server maplist command.
Add this code to ServerCommand()
just before the else (around
line 24):
g_main.c
(modification)
Here we go. The EndDMLevel()
function is called whenever a timelimit or fraglimit is reached in DeathMatch
play. Here's the modified code (around line 164):
The timelimit or fraglimit has been exceeded
=================
*/
void EndDMLevel (void)
{
edict_t *ent;
int
i; //LAC
// stay on same level flag
if ((int)dmflags->value &
DF_SAME_LEVEL)
{
ent = G_Spawn
();
ent->classname
= "target_changelevel";
ent->map =
level.mapname;
}
//LAC+++
// if you also want this to
happen in co-op, you will probably
// have to put similar code
in ExitLevel().
else if ((int)dmflags->value
& DF_MAP_LIST) // maplist active?
{
switch (maplist.rotationflag)
// choose next map in list
{
case ML_ROTATE_SEQ:
// sequential rotation
i = (maplist.currentmap + 1) % maplist.nummaps;
break;
case ML_ROTATE_RANDOM:
// random rotation
i = (int) (random() * maplist.nummaps);
break;
default:
// should never happen, but set to first map
if it does
i=0;
} //
end switch
maplist.currentmap
= i;
ent = G_Spawn
();
ent->classname
= "target_changelevel";
ent->map =
maplist.mapnames[i];
}
//LAC---
else if (level.nextmap[0])
{ // go to a specific map
...
fileio.c
(new file)
We're going to use an external ASCII text file for the maplist names.
This way you can change the names in the file and reload them into Quake2
without even leaving the game or the server! So then, we obviously
need functions to open and close files. Here is fileio.c
in its entirety:
// INCLUDES /////////////////////////////////////////////////
#include "g_local.h"
// FUNCTIONS ////////////////////////////////////////////////
//
// OpenFile
//
// Opens a file for reading. This function
will probably need
// a major overhaul in future versions so
that it will handle
// writing, appending, etc.
//
// Args:
// filename - name of file to
open.
//
// Return: file handle of open file stream.
//
Returns NULL if file could not be opened.
//
FILE *OpenFile(char *filename)
{
FILE *fp = NULL;
if ((fp = fopen(filename, "r"))
== NULL)
// test to see if file opened
{
// file did
not load
gi.dprintf
("Could not open file \"%s\".\n", filename);
return NULL;
}
return fp;
}
//
// CloseFile
//
// Closes a file that was previously opened
with OpenFile().
//
// Args:
// fp - file handle of
file stream to close.
//
// Return: (none)
//
void CloseFile(FILE *fp)
{
if (fp)
// if the file is open
{
fclose(fp);
}
else
// no file is opened
gi.dprintf
("ERROR -- CloseFile() exception.\n");
}
fileio.h
(new file)
Ok, here's the VERY short file, prototyping the functions we used in
fileio.c:
// PROTOTYPES ///////////////////////////////////////////////
FILE *OpenFile (char *filename);
void CloseFile (FILE *fp);
maplist.h
(new file)
And another short file. This one has a bit more usefulness, however.
Here's the whole file:
// DEFINES //////////////////////////////////////////////////
#define MAX_MAPS
16
#define MAX_MAPNAME_LEN
16
#define ML_ROTATE_SEQ
0
#define ML_ROTATE_RANDOM
1
#define ML_ROTATE_NUM_CHOICES 2
// STRUCTURES ///////////////////////////////////////////////
typedef struct
{
char filename[21];
// filename on server (20-char max length)
int nummaps;
// number of maps in list
char mapnames[MAX_MAPS][MAX_MAPNAME_LEN];
char rotationflag;
// set to ML_ROTATE_*
int currentmap;
// index to current map
} maplist_t;
// GLOBALS //////////////////////////////////////////////////
maplist_t maplist;
// PROTOTYPES ///////////////////////////////////////////////
int LoadMapList
(char *filename);
void ClearMapList
();
void Cmd_Maplist_f
(edict_t *ent);
void Svcmd_Maplist_f
();
void DisplayMaplistUsage (edict_t *ent);
void ShowCurrentMaplist (edict_t *ent);
maplist.c
(new file)
Ok, here's the big one. Below it is displayed as a whole, but
I feel that it should be commented well enough for most coders to see what
it does. If I get enough questions about it, however, I will provide
more explanation in this procedure.
// INCLUDES /////////////////////////////////////////////////
#include "g_local.h"
// FUNCTIONS ////////////////////////////////////////////////
//
// LoadMapList
//
// Opens the specified file and scans/loads
the maplist names
// from the file's [maplist] section. (list
is terminated with
// "###")
//
// Args:
// filename - name of file containing
maplist.
//
// Return: 0 = normal exit, maplist loaded
//
1 = abnormal exit
//
int LoadMapList(char *filename)
{
FILE *fp;
int i=0;
char szLineIn[80];
fp = OpenFile(filename);
if (fp)
// opened successfully?
{
// scan for [maplist] section
do
{
fscanf(fp, "%s", szLineIn);
} while (!feof(fp)
&& (Q_stricmp(szLineIn, "[maplist]") != 0));
if (feof(fp))
{
// no [maplist] section
gi.dprintf ("-------------------------------------\n");
gi.dprintf ("ERROR - No [maplist] section in \"%s\".\n", filename);
gi.dprintf ("-------------------------------------\n");
}
else
{
gi.dprintf ("-------------------------------------\n");
// read map names into array
while ((!feof(fp)) && (i<MAX_MAPS))
{
fscanf(fp, "%s", szLineIn);
if (Q_stricmp(szLineIn, "###") == 0)
// terminator is "###"
break;
// TODO: check that maps exist before adding to list
// (might be difficult to search a
.pak file for these)
strncpy(maplist.mapnames[i], szLineIn, MAX_MAPNAME_LEN);
gi.dprintf("...%s\n", maplist.mapnames[i]);
i++;
}
strncpy(maplist.filename, filename, 20);
}
CloseFile(fp);
if (i == 0)
{
gi.dprintf ("No maps listed in [maplist] section of %s\n", filename);
gi.dprintf ("-------------------------------------\n");
return 0; // abnormal exit -- no maps
in file
}
gi.dprintf
("%i map(s) loaded.\n", i);
gi.dprintf
("-------------------------------------\n");
maplist.nummaps
= i;
return 1;
// normal exit
}
return 0;
// abnormal exit -- couldn't open file
}
//
// ClearMapList
//
// Clears/invalidates maplist. Might add
more features in the future,
// but resetting .nummaps to 0 will suffice
for now.
//
// Args: (none)
//
// Return: (none)
//
void ClearMapList()
{
maplist.nummaps = 0;
dmflags->value = (int) dmflags->value
& ~DF_MAP_LIST;
gi.dprintf ("Maplist cleared/disabled.\n");
}
//
// DisplayMaplistUsage
//
// Displays current command options for maplists.
If not dedicated console (ent==NULL),
// then do not display all options.
//
// Args:
// ent
- entity (client) to display help screen (usage) to.
//
if NULL, will print to dedicated console.
//
// Return: (none)
//
void DisplayMaplistUsage(edict_t *ent)
{
gi.cprintf (ent, PRINT_HIGH,
"-------------------------------------\n");
gi.cprintf (ent, PRINT_HIGH,
"usage:\n");
if (ent==NULL)
{
gi.cprintf
(ent, PRINT_HIGH, "MAPLIST <filename> [<rotate_f>]\n");
gi.cprintf
(ent, PRINT_HIGH, " <filename> - server ini file\n");
gi.cprintf
(ent, PRINT_HIGH, " <rotate_f> - 0 = sequential (def)\n");
gi.cprintf
(ent, PRINT_HIGH, "
1 = random\n");
gi.cprintf
(ent, PRINT_HIGH, "MAPLIST START - go to 1st map\n");
gi.cprintf
(ent, PRINT_HIGH, "MAPLIST NEXT - go to next map\n");
gi.cprintf
(ent, PRINT_HIGH, "MAPLIST GOTO <n> - go to map #<n>\n");
}
gi.cprintf (ent, PRINT_HIGH,
"MAPLIST - show current
list\n");
gi.cprintf (ent, PRINT_HIGH,
"MAPLIST HELP - (this screen)\n");
if (ent==NULL)
{
gi.cprintf
(ent, PRINT_HIGH, "MAPLIST OFF - clear/disable\n");
}
gi.cprintf (ent, PRINT_HIGH,
"-------------------------------------\n");
}
//
// ShowCurrentMaplist
//
// Displays current maplist.
//
// Args:
// ent
- entity (client) to display help screen (usage) to.
//
if NULL, will print to dedicated console.
//
// Return: (none)
//
void ShowCurrentMaplist(edict_t *ent)
{
int i;
gi.cprintf (ent, PRINT_HIGH, "-------------------------------------\n");
if (ent==NULL)
// only show filename to server
gi.dprintf
("FILENAME: %s\n", maplist.filename);
for (i=0; i<maplist.nummaps;
i++)
{
gi.cprintf
(ent, PRINT_HIGH, "#%2d \"%s\"\n", i+1, maplist.mapnames[i]);
}
gi.cprintf (ent, PRINT_HIGH, "%i map(s) in list.\n", i);
gi.cprintf (ent, PRINT_HIGH,
"Rotation flag = %i ", maplist.rotationflag);
switch (maplist.rotationflag)
{
case ML_ROTATE_SEQ:
gi.cprintf
(ent, PRINT_HIGH, "\"sequential\"\n");
break;
case ML_ROTATE_RANDOM:
gi.cprintf
(ent, PRINT_HIGH, "\"random\"\n");
break;
default:
gi.cprintf
(ent, PRINT_HIGH, "(ERROR)\n");
}
// end switch
if (maplist.currentmap == -1)
{
gi.cprintf
(ent, PRINT_HIGH, "Current map = #-1 (not started)\n");
}
else
{
gi.cprintf
(ent, PRINT_HIGH, "Current map = #%i \"%s\"\n",
maplist.currentmap+1, maplist.mapnames[maplist.currentmap]);
}
gi.cprintf (ent, PRINT_HIGH,
"-------------------------------------\n");
}
//
// Cmd_Maplist_f
//
// Client command line parsing function.
Either displays the current list, or
// the syntax of the command.
//
// Args:
// ent
- entity (client) to display messages to, if necessary.
//
// Return: (none)
//
void Cmd_Maplist_f (edict_t *ent)
{
switch (gi.argc())
{
case 1:
// display current maplist
if (maplist.nummaps > 0) // does a maplist
exist?
{
ShowCurrentMaplist(ent);
}
else
// no maplist
{
gi.cprintf (ent, PRINT_HIGH, "*** No MAPLIST active ***\n");
DisplayMaplistUsage(ent);
}
break;
case 2:
if (Q_stricmp(gi.argv(1),
"HELP") == 0)
{
DisplayMaplistUsage(ent);
}
else
// no other parameters allowed for clients
{
gi.cprintf (ent, PRINT_HIGH, "MAPLIST options locked by server.\n");
}
break;
default:
DisplayMaplistUsage(ent);
}
// end switch
}
//
// Svcmd_Maplist_f
//
// Main command line parsing function. Enables/parses/diables
maplists.
//
// Args: (none)
//
// Return: (none)
//
void Svcmd_Maplist_f ()
{
int i;
// temp variable
char *filename;
edict_t *ent;
// for map changing, if necessary
switch (gi.argc())
{
case 3:
// various commands, or enable and assume rotationflag default
if (Q_stricmp(gi.argv(2),
"HELP") == 0)
{
DisplayMaplistUsage(NULL);
break;
}
if (Q_stricmp(gi.argv(2),
"START") == 0)
{
if (maplist.nummaps > 0) // does a maplist
exist?
EndDMLevel();
else
DisplayMaplistUsage(NULL);
break;
}
else if (Q_stricmp(gi.argv(2),
"NEXT") == 0)
{
if (maplist.nummaps > 0) // does a maplist
exist?
EndDMLevel();
else
DisplayMaplistUsage(NULL);
break;
}
else if (Q_stricmp(gi.argv(2),
"OFF") == 0)
{
if (maplist.nummaps > 0) // does a maplist
exist?
{
ClearMapList();
}
else
{
// maplist doesn't exist, so display usage
DisplayMaplistUsage(NULL);
}
break;
}
else
maplist.rotationflag = 0;
// no break here is intentional; supposed to fall though to case
3
case 4:
// enable maplist - all args explicitly stated on command line
if (gi.argc()
== 4) // this is required, because it
can still = 2
{
i = atoi(gi.argv(3));
if (Q_stricmp(gi.argv(2), "GOTO") == 0)
{
// user trying to goto specified map # in list
if ((i<1) || (i>maplist.nummaps))
{
gi.dprintf("*** Map# out of range ***\n");
ShowCurrentMaplist(NULL);
}
else
{
ent = G_Spawn ();
ent->classname = "target_changelevel";
ent->map = maplist.mapnames[i-1];
maplist.currentmap = i-1;
BeginIntermission(ent);
}
break;
}
else
{
// user trying to specify new maplist
if ((i<0) || (i>=ML_ROTATE_NUM_CHOICES))
// check for valid rotationflag
{
// outside acceptable values for rotationflag
DisplayMaplistUsage(NULL);
break;
}
else
{
maplist.rotationflag = atoi(gi.argv(3));
}
}
}
filename = gi.argv(2); // get filename from command line
if ((int) dmflags->value
& DF_MAP_LIST)
{
// tell user to cancel current maplist before starting new maplist
gi.dprintf ("You must disable current maplist first. (SV MAPLIST OFF)\n");
}
else
{
// load new maplist
if (LoadMapList(filename)) // return
1 = success
{
dmflags->value = (int) dmflags->value | DF_MAP_LIST;
gi.dprintf ("Maplist created/enabled. You can now use START or NEXT.\n");
maplist.currentmap = -1;
}
}
break;
case 2:
// display current maplist
if (maplist.nummaps
> 0) // does a maplist exist?
{
ShowCurrentMaplist(NULL);
}
else
{
DisplayMaplistUsage(NULL);
}
break;
default:
DisplayMaplistUsage(NULL);
}
// end switch
}
myserver.ini
(new runtime sample file)
I chose a Windows .ini format for the data file, with the mapnames
listed in the [maplist] section.
If you place the following file in your Quake2 base directory (e.g. C:\Quake2\),
then you (server-op) can sv maplist myserver.ini or sv
maplist myserver.ini 1, then sv mapserver start
(or next -- currently does the same thing as start)
at the console to begin the rotation.
*Note: You can use almost anything for the filename -- except for the maplist commands such as start, goto, etc. But who names a file one of those?
Don't forget to add fileio.c and maplist.c to your project (if necessary). Compile and test it, and send me any questions/suggestions for improvement. (email address at top and bottom of this page) It's not the best out there, but it seems to be one of the first "tutorials" on the subject. (I never really had a desire to do this mod, but someone asked for it, and I never saw anyone else take up the request) By no means do I consider this a complete server mod. I intentionally left features out, because I wanted this doc to concentrate only on the maplist function. You will probably even find a better way to do maplists, as well, but at least I've provided somewhere to start -- and that's good enough for me.
For your protection and mine, I will not provide a compiled version of this code, because it could eventually pass through malicious hands, and my reputation could be tarnished. And I figure that if you don't have a compiler or you're not looking to learn a little programming, you probably wont be reading this anyway. :)
As for future development of this mod, it is so basic that anyone can use it and modify it in any (non-malicious) way they want. I would appreciate an honorable mention (and an email about your use of it), but it's no biggie. However, if you reproduce this procedure (or "tutorial", if you're so inclined), you must credit me as a source.