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>