*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:
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 818):
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):
//
// 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)
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):
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 165):
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
...
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:
// 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");
}
fileio.h
(new file)
Ok, here's the VERY short file, prototyping the functions we used in
fileio.c:
// PROTOTYPES ///////////////////////////////////////////////
FILE *OpenFile(edict_t *ent, char *filename);
void CloseFile(edict_t *ent, 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
{
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);
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:
// 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
}
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?
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.