
Quake DeveLS - Server Maplist Rotation/Manipulation v1.0
Author: L. Allan Campbell (Geist)
Difficulty: Hard
*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: So far, this
only works with non-dedicated servers, as right now the easiest way to
implement this command is through ClientCommand(). It could probably be done
using cvars, but according to John Carmack's Jan10,'98 .plan:
A new API entry point has been added to
the game dll that
gets
called whenever an "sv" command
is
issued on the server console.
This is to allow you to create commands for the server operator
to
type, as opposed to commands that a client would type (which are defined in g_cmds.c).
Therefore,
I will update this document with dedicated-server-specific code after the
above-mentioned update is available, and I've had plenty of time to test
it. All-in-all, it shouldn't be a drastic update for this code.
Now then, on with the code
implementation. You will be making modifications to the following files:
q_shared.h
g_local.h
g_cmds.c
g_main.c
And you
will be creating these files (you can just copy/paste from the end of this doc,
to make it easier):
fileio.c
fileio.h
maplist.h
maplist.c
And
finally, a sample data file called mysever.ini is provided.
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 818):
...
#define
DF_FORCE_RESPAWN 1024
#define
DF_NO_ARMOR 2048
//LAC+++
#define
DF_MAP_LIST 4096
//LAC---
...
*Note:
If I modify the original source, I always mark the modified code (with my
initials, etc). This makes it much easier to go back and find my
modifications later. For a single line, I usually use //LAC, but for blocks of code, //LAC+++ and //LAC--- are used.
Other mod authors will
eventually add entries to this list. DF_MAP_LIST does not have to be 4096,
but it must be an unused power of 2 that an 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 705):
...
//
//
g_main.c
//
void
SaveClientData (void);
void
FetchClientEntData (edict_t *ent);
//LAC+++
void
EndDMLevel (void); // not sure why id didn't include this
//
//
fileio.c
//
#include
"fileio.h"
//
//
maplist.c
//
#include
"maplist.h"
//LAC---
//============================================================================
// client_t->anim_priority
#define
ANIM_BASIC 0 // stand / run
...
This just
prototypes the EndDMLevel() function that id included in g_main.c (we'll use it in maplist.c), and makes our new #defines,
structs, prototypes, etc. available to all other files in the .dll project.
g_cmds.c (modification)
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 651):
...
else if (Q_stricmp (cmd, "gameversion") == 0)
{
gi.cprintf (ent, PRINT_HIGH, "%s : %s\n", GAMEVERSION, __DATE__);
}
//LAC+++
else if (Q_stricmp (cmd, "maplist") == 0)
{
Cmd_Maplist_f (ent);
}
//LAC---
else
gi.cprintf (ent, PRINT_HIGH, "Bad command: %s\n", cmd);
}
...
Very
simple. When a player types /cmd maplist * at the console, Cmd_Maplist_f() is called to parse the line and
execute the request.
g_main.c (modification)
Here we go. The EndDMLevel() function is called whenever a timelimit or
fraglimit is reached in DeathMatch play. ExitLevel() is called whenever the the player
reaches the exit in single-player mode (and most likely co-op mode in the
future). Since right now we're only concerned with DM, we'll only modify EndDMLevel() -- as follows (around line 165):
...
/*
=================
EndDMLevel
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)
{
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)
{ // go to a specific map
...
All we did
here was test our newly assigned bit in dmflags->value, and if it is set, we have a
maplist to follow. So we procede to pick the next map based on the type
of rotation specified when the maplist was created (we'll get to that
later). I decided to use a switch() here, in case I want to test for
more values in the future. Then an entity of class "target_changelevel" is created and it's .map variable is set to the name of our
chosen map from the list. (This is nearly identical to the level-changing code
in the rest of the function)
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:
//
//
fileio.c -- file access routines
//
//
//
1/98 - L. Allan Campbell (Geist)
//
// 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:
//
ent - entity (client) to print diagnostic
messages to.
//
filename - name of file to open.
//
//
Return: file handle of open file stream.
//
Returns NULL if file could not be opened.
//
FILE
*OpenFile(edict_t *ent, char *filename)
{
FILE *fp = NULL;
if ((fp = fopen(filename, "r")) ==
NULL) // test to see
if file opened
{
// file did not load
gi.cprintf (ent, PRINT_HIGH, "Could not open file
\"%s\".\n", filename);
return NULL;
}
return fp;
}
//
//
CloseFile
//
//
Closes a file that was previously opened with OpenFile().
//
//
Args:
//
ent - entity (client) to print diagnostic messages to.
//
fp - file handle of file stream to close.
//
//
Return: (none)
//
void
CloseFile(edict_t *ent, FILE *fp)
{
if (fp) // if the file
is open
{
fclose(fp);
}
else // no file is opened
gi.cprintf(ent, PRINT_HIGH, "ERROR -- CloseFile() exception.\n");
}
Albeit, these
functions are extremely limited. And as they exist now, you'd probably be
better off just using fopen() and fclose() in your code. However, you
can fleshed them out to provide more features, and they illustrate passing of
file handles between functions.
fileio.h (new file)
Ok, here's the VERY short file, prototyping the functions we used in fileio.c:
//
//
fileio.h
//
// PROTOTYPES ///////////////////////////////////////////////
FILE *OpenFile(edict_t *ent, char *filename);
void
CloseFile(edict_t *ent, FILE *fp);
Again, you
can always add more functions later for file IO.
maplist.h (new file)
And another short file. This one has a bit more usefulness,
however. Here's the whole file:
//
//
maplist.h
//
// 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
{
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(edict_t *ent, char *filename);
void
ClearMapList(edict_t *ent);
void
Cmd_Maplist_f (edict_t *ent);
void
Display_Maplist_Usage(edict_t *ent);
This file
introduces a new structure that is typedef'd as maplist_t. This structure will hold
the total number of maps in the rotation, their names, an index to the current
map, and a rotationflag that specifies how the maps will progress, whether
sequentially, or at random, etc. (you can add more values to this). Then
a global maplist is declared.
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.
//
//
maplist.c -- server maplist rotation/manipulation routines
//
//
//
1/98 - L. Allan Campbell (Geist)
//
// 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:
//
ent - entity (client) to print diagnostic
messages to.
//
filename - name of file containing maplist.
//
//
Return: 0 = normal exit, maplist loaded
//
1 = abnormal exit
//
int
LoadMapList(edict_t *ent, char *filename)
{
FILE *fp;
int i=0;
char szLineIn[80];
fp = OpenFile(ent, 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.cprintf (ent, PRINT_HIGH,
"-------------------------------------\n");
gi.cprintf (ent, PRINT_HIGH, "ERROR - No [maplist] section in
\"%s\".\n", filename);
gi.cprintf (ent, PRINT_HIGH,
"-------------------------------------\n");
}
else
{
gi.cprintf (ent, PRINT_HIGH,
"-------------------------------------\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.cprintf(ent, PRINT_HIGH, "...%s\n", maplist.mapnames[i]);
i++;
}
}
CloseFile(ent, fp);
if (i == 0)
{
gi.cprintf (ent, PRINT_HIGH, "No maps listed in [maplist] section of
%s\n", filename);
gi.cprintf (ent, PRINT_HIGH,
"-------------------------------------\n");
return 0; // abnormal exit -- no maps in file
}
gi.cprintf (ent, PRINT_HIGH,
"%i map(s) loaded.\n", i);
gi.cprintf (ent, PRINT_HIGH,
"-------------------------------------\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:
//
ent - entity (client) to print diagnostic
messages to (future development).
//
//
Return: (none)
//
void
ClearMapList(edict_t *ent)
{
maplist.nummaps = 0;
}
//
//
DisplayMaplistUsage
//
//
Displays current command options for maplists.
//
//
Args:
//
ent - entity (client) to display help screen
(usage) to.
//
//
Return: (none)
//
void
DisplayMaplistUsage(edict_t *ent)
{
gi.cprintf (ent, PRINT_HIGH,
"-------------------------------------\n");
gi.cprintf (ent, PRINT_HIGH, "usage:\n");
gi.cprintf (ent, PRINT_HIGH, "/CMD MAPLIST <filename> [<rot_flag>]\n");
gi.cprintf (ent, PRINT_HIGH, " <filename> - server ini
file\n");
gi.cprintf (ent, PRINT_HIGH, " <rot_flag> - 0 = sequential
(def)\n");
gi.cprintf (ent, PRINT_HIGH,
"
1 = random\n");
gi.cprintf (ent, PRINT_HIGH, "/CMD MAPLIST START to go to 1st
map\n");
gi.cprintf (ent, PRINT_HIGH, "/CMD MAPLIST NEXT to go to next
map\n");
gi.cprintf (ent, PRINT_HIGH, "/CMD MAPLIST to display current
list\n");
gi.cprintf (ent, PRINT_HIGH, "/CMD MAPLIST OFF to clear/disable\n");
gi.cprintf (ent, PRINT_HIGH, "/CMD MAPLIST HELP for this screen\n");
gi.cprintf (ent, PRINT_HIGH,
"-------------------------------------\n");
}
//
//
Cmd_Maplist_f
//
//
Main command line parsing function. Enables/parses/diables maplists.
//
//
Args:
//
ent - entity (client) to display messages to, if
necessary.
//
//
Return: (none)
//
//
TODO: change "client 0" privs to be for server only, if dedicated.
//
only allow other clients to view list and see HELP screen.
//
(waiting for point release for this feature)
//
void
Cmd_Maplist_f (edict_t *ent)
{
int i; // looping and temp variable
char *filename;
switch (gi.argc())
{
case 2: // various commands, or enable and assume rotationflag default
if (Q_stricmp(gi.argv(1), "HELP") == 0)
{
DisplayMaplistUsage(ent);
break;
}
// only allow if client 0
if (ent != &g_edicts[1])
{
gi.cprintf (ent, PRINT_HIGH, "/CMD MAPLIST options locked by
server.\n");
break;
}
if (Q_stricmp(gi.argv(1),
"START") == 0)
{
if (maplist.nummaps > 0) // does a maplist exist?
EndDMLevel();
else
DisplayMaplistUsage(ent);
break;
}
else if (Q_stricmp(gi.argv(1), "NEXT") == 0)
{
if (maplist.nummaps > 0) // does a maplist exist?
EndDMLevel();
else
DisplayMaplistUsage(ent);
break;
}
else if (Q_stricmp(gi.argv(1), "OFF") == 0)
{
if (maplist.nummaps > 0) // does a maplist exist?
{
ClearMapList(ent);
dmflags->value = (int) dmflags->value & ~DF_MAP_LIST;
gi.cprintf (ent, PRINT_HIGH, "Maplist cleared/disabled.\n");
}
else
{
// maplist doesn't exist, so display usage
DisplayMaplistUsage(ent);
}
break;
}
else
maplist.rotationflag = 0;
// no break here is intentional; supposed to fall though to case 3
case 3: // enable
maplist - all args explicitly stated on command line
// only allow if client 0
if (ent != &g_edicts[1])
{
gi.cprintf (ent, PRINT_HIGH, "/CMD MAPLIST options locked by
server.\n");
break;
}
if (gi.argc() == 3) // this is
required, because it can still = 2
{
i = atoi(gi.argv(2));
if
(Q_stricmp(gi.argv(1), "GOTO") == 0)
{
// user trying to goto specified map # in list
if ((i<1) || (i>maplist.nummaps))
DisplayMaplistUsage(ent);
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(ent);
break;
}
else
{
maplist.rotationflag = atoi(gi.argv(2));
}
}
}
filename =
gi.argv(1); // get filename from command line
if ((int) dmflags->value &
DF_MAP_LIST)
{
// tell user to cancel current maplist before starting new maplist
gi.cprintf (ent, PRINT_HIGH, "You must disable current maplist first.
(/CMD MAPLIST OFF)\n");
}
else
{
// load new maplist
if (LoadMapList(ent, filename)) // return 1 = success
{
dmflags->value = (int) dmflags->value | DF_MAP_LIST;
gi.cprintf (ent, PRINT_HIGH, "Maplist created/enabled. You can now use
START or NEXT.\n");
maplist.currentmap = -1;
}
}
break;
case 1: // display
current maplist
if (maplist.nummaps > 0) // does a maplist exist?
{
gi.cprintf (ent, PRINT_HIGH,
"-------------------------------------\n");
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");
break;
}
// this is when the command is
"/cmd maplist", but no maplist exists to display
DisplayMaplistUsage(ent);
break;
default:
DisplayMaplistUsage(ent);
} // end switch
}
To get a
brief explanation of what each function does (but now how it does it), check
out the output of DisplayMaplistUsage(). It's the same explanation a player would see when
typing /cmd
maplist help.
(or just about any invalid command. I have tried to catch data errors of
any type, and I've tested it quite a bit. Right now, client 0 (the
machine that is running the non-dedicated server) is the only player that can
enable, disable, start, advance, or skip levels in the maplist system.
All clients, however, can display the list of maps, and the HELP screen.
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 (client 0) can /cmd maplist myserver.ini or /cmd maplist myserver.ini 1, then /cmd 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?
[maplist]
base2
fact3
jail1
mine2
###
The [maplist] section can be anywhere in the
file, but be sure not to put any comments between [maplist] and ###. (blank lines are ok, though)
Don't forget to add fileio.c and maplist.c to your project (if necessary).
Compile it, and send me any questions/suggestions for improvement. (email
address at top) It's not the best out there, but it seems to be one of
the first. (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)
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. I will update it in the future with dedicated
server support, and possibly a password feature (so you can leave your server
unattended or access it remotely). The latter will most likely be part of
a larger remote server mod, however.
Copyright 1998 - L. Allan Campbell (Geist)
GreyShades
<geist@mindspring.com>