Quake DeveLS - Map Rotation

Author: Mark "Grey" Davies
Difficulty: Easy

Overview:

This tutorial just explains how to add my map rotation code to your mod. My map rotation code takes into account the number of players currently connected and attempts to select an appropriate map.

The code uses the following cvars.

The format of the map file is an based upon id's maps.lst file. Optional information can be added to specify the minimum and maximum players that can play on a map. If a map entry does not specify any values they are assumed to be 0. A map with a maximum set to 0 is ignored.

An example of a modified maps.lst file

Instructions:

Create a file called s_map.c and add the following lines

#include "g_local.h"

typedef struct
{
    char   aFile[MAX_QPATH];
    char   aName[MAX_QPATH];
    int    min;
    int    max;
    int    fVisited;
} MAP_ENTRY;

static MAP_ENTRY   *mdsoft_map       = NULL;
static unsigned int mdsoft_map_size  = 0;
static unsigned int mdsoft_map_last  = 0;


static int mdsoft_read_map_entry(  FILE   *fpFile,
                                   char   *pFile,
                                   char   *pName,
                                   int    *pMin,
                                   int    *pMax );



edict_t *mdsoft_NextMap( void )
{
    edict_t     *ent    = NULL;
    int         count   = 0;
    int         fFound  = 0;
    int         nTimes  = 0;
    cvar_t *map_c = gi.cvar( "md_map_change", "1", CVAR_SERVERINFO );
    cvar_t *map_r = gi.cvar( "md_map_random", "0", CVAR_SERVERINFO );
    cvar_t *map_o = gi.cvar( "md_map_once", "0", CVAR_SERVERINFO );
    cvar_t *map_d = gi.cvar( "md_map_debug", "0", CVAR_SERVERINFO );

    if( (int)map_c->value == 0 )
        return NULL;

    /* Load Maps File */
    if( NULL == mdsoft_map )
    {
        FILE    *fpFile     = NULL;
        cvar_t  *game       = gi.cvar( "gamedir", "mod-1", CVAR_SERVERINFO );
        cvar_t  *base       = gi.cvar( "basedir", ".", CVAR_SERVERINFO );
        cvar_t  *map_f      = gi.cvar( "md_map_file", "maps.lst", CVAR_SERVERINFO );

        /* Load maps.lst file */
        if( game && base )
        {
            char mapfile[MAX_QPATH] = {0};
            char *pFileName = &mapfile[0];

            strcat( mapfile, base->string );
            strcat( mapfile, "\\" );
            strcat( mapfile, game->string );

            if( NULL != map_f )
            {
                strcat( mapfile, "\\" );
                strcat( mapfile, map_f->string );
            }
            else
            {
                strcat( mapfile, "\\maps.lst" );
            }

            fpFile = fopen( pFileName, "r" );
            if( fpFile )
            {
                MAP_ENTRY   temp;
                int         element;

                do
                {
                    temp.min      = 0;
                    temp.max      = 0;
                    temp.fVisited = 0;

                    element = mdsoft_read_map_entry( fpFile,
                        &temp.aFile[0],
                        &temp.aName[0],
                        &temp.min,
                        &temp.max );

                    if( 2 <= element )
                    {
                        MAP_ENTRY *newone;

                        newone = realloc( mdsoft_map,
                                          (mdsoft_map_size+1) * sizeof(*newone) );

                        if( newone )
                        {
                            mdsoft_map = newone;
                            memcpy( &mdsoft_map[mdsoft_map_size], &temp, sizeof(temp) );
                            mdsoft_map_size++;
                        }
                    }
                }while( 2 <= element );

#if 0
                /* The following code will overflow the clients (players) */
                /* And kick them off the server */
                {
                    int i;

                    for( i=0; ivalue > 0 ) )
            {
                mdsoft_map_last = random() * (mdsoft_map_size-1);
                if( mdsoft_map_last < 0 )
                    mdsoft_map_last = 0-mdsoft_map_last;

                if( (NULL != map_d) &&
                    ((int)map_d->value > 0 ) )
                    gi.bprintf( PRINT_HIGH, 
                                "Random Map %d %s\n", 
                                mdsoft_map_last, 
                                mdsoft_map[mdsoft_map_last].aFile);
            }

            /* Choose map */
            {
                int i;
                int point = (mdsoft_map_last+1) % mdsoft_map_size;

                count = 0;
                for (i = 0; i < maxclients->value; i++)
                {
                    if (game.clients[i].pers.connected)
                        count++;
                }

                /*gi.bprintf (PRINT_HIGH, "MAP CHANGE: Count = %d \n", count );*/

                do
                {
                    if( (0 != mdsoft_map[point].max) &&
                        (0 == mdsoft_map[point].fVisited) )
                    {
                        if( (mdsoft_map[point].min <= count) &&
                            (mdsoft_map[point].max >= count) )
                        {
                            mdsoft_map_last = point;
                            point = -1;
                            fFound = 1;

                            if( (NULL != map_d) &&
                                ((int)map_d->value > 0 ) )
                                gi.bprintf( PRINT_HIGH,
                                            "Map Found %s [fVisited = %d]\n",
                                            mdsoft_map[mdsoft_map_last].aFile, 
                                            mdsoft_map[mdsoft_map_last].fVisited);
                        }
                        else
                        {
                            point = (point+1) % mdsoft_map_size;
                        }
                    }
                    else
                    {
                        point = (point+1) % mdsoft_map_size;
                    }
                }while( (point != -1) && (point != mdsoft_map_last) );

                /* Could not select an appropriate map */
                if(point == mdsoft_map_last)
                {
                    if( (NULL != map_d) &&
                        ((int)map_d->value > 0 ) )
                        gi.bprintf(PRINT_HIGH, "Map could not be found\n" );

                    /* Clear visited flags */
                    if( (NULL != map_o) &&
                        ((int)map_o->value > 0 ) )
                    {
                        int i;

                        if( (NULL != map_d) &&
                            ((int)map_d->value > 0 ) )
                            gi.bprintf(PRINT_HIGH, "Clearing Visited flags\n" );

                        for( i=0; i < mdsoft_map_size; i++ )
                            mdsoft_map[i].fVisited = 0;
                    }

                    /* Use next map in list */
                    mdsoft_map_last = (mdsoft_map_last+1) % mdsoft_map_size;
                }
            }

            nTimes++;
        } while( !fFound && (nTimes < 2) );
    }
    else
    {
        mdsoft_map_last = 0;
    }

    if( fFound && !ent )
    {
        /* Set map as visited */
        if( (NULL != map_o) &&
            ((int)map_o->value > 0 ) )
        {
            mdsoft_map[mdsoft_map_last].fVisited = 1;
        }

        /* Set next map */
        ent = G_Spawn ();
        if( ent )
        {
            ent->classname = "target_changelevel";
            ent->map = &mdsoft_map[mdsoft_map_last].aFile[0];

            if( (NULL != map_d) &&
                ((int)map_d->value > 0 ) )
            {
                gi.bprintf (PRINT_HIGH, "MAP CHANGE: %d ", mdsoft_map_last );
                gi.bprintf (PRINT_HIGH, &mdsoft_map[mdsoft_map_last].aFile[0] );

                gi.bprintf (PRINT_HIGH,
                            " [min = %d,max = %d, players = %d]\n",
                            mdsoft_map[mdsoft_map_last].min,
                            mdsoft_map[mdsoft_map_last].max,
                            count );
            }
        }
    }

    return ent;
} /* end of mdsoft_NextMap() */




static int mdsoft_read_map_entry(  FILE   *fpFile,
                                   char   *pFile,
                                   char   *pName,
                                   int    *pMin,
                                   int    *pMax )
{
    char buffer[MAX_QPATH]  = {0};
    int  c;
    int  i                  = 0;
    int  fInQuotes          = 0;
    int  element            = 0;

    do
    {
        c = fgetc( fpFile );

        /* Use buffer */
        if( (i > 0) &&
            (
             (((' ' == c) || ('\t' == c)) && !fInQuotes) ||
             (EOF == c) || ('\n' == c)
            )
          )
        {
            buffer[i] = '\0';

            switch( element )
            {
                case 0:
                {
                    strncpy( pFile, buffer, MAX_QPATH );
                    break;
                }
                case 1:
                {
                    strncpy( pName, buffer, MAX_QPATH );
                    break;
                }
                case 2:
                {
                    *pMin = atoi( buffer );
                    break;
                }
                case 3:
                {
                    *pMax = atoi( buffer );
                    break;
                }
            }

            i = 0;
            element++;
        }
        else
        {
            switch( c )
            {
                case '\"':
                {
                    fInQuotes = 1 - fInQuotes;
                    break;
                }

                case '\t':
                case ' ':
                {
                    if( !fInQuotes )
                        break;
                } /* fallthrough */
                default:
                {
                    if( i < (MAX_QPATH-1) )
                    {
                        buffer[i] = c;
                        i++;
                    }
                    break;
                }
            }
        }
    } while( (c != EOF) && (c != '\n') );

    return element;
} /* end of mdsoft_read_map_entry() */

Create a file called s_map.h and add the following lines

extern edict_t *mdsoft_NextMap( void );

Open the file called g_main.c and replace

#include "g_local.h"

with

#include "g_local.h"
#include "s_map.h"

Find the function EndDMLevel(), it should be located around line 181 and replace it with

void EndDMLevel (void)
{
	edict_t		*ent = NULL;            /* Added '= NULL' - mdavies */


	// stay on same level flag
	if ((int)dmflags->value & DF_SAME_LEVEL)
	{
		ent = G_Spawn ();
		ent->classname = "target_changelevel";
		ent->map = level.mapname;
	}

    /* New code - START - mdavies */
    if( !ent )
    {
        ent = mdsoft_NextMap();
    }
    /* New code - END - mdavies */

    if( !ent )                          /* added line - mdavies */
    {                                   /* added line - mdavies */
        if (level.nextmap[0])           /* changed else if to if - mdavies */
        {	// go to a specific map
            ent = G_Spawn ();
            ent->classname = "target_changelevel";
            ent->map = level.nextmap;
        }
        else
        {	// search for a changeleve
            ent = G_Find (NULL, FOFS(classname), "target_changelevel");
            if (!ent)
            {	// the map designer didn't include a changelevel,
                // so create a fake ent that goes back to the same level
                ent = G_Spawn ();
                ent->classname = "target_changelevel";
                ent->map = level.mapname;
            }
        }
    }                                   /* Added line - mdavies */

	BeginIntermission (ent);
}

Ensure that the file s_map.c is compiled with the rest of your source and that it is linked into your mod. Compile, link and run.

Notes:

The format of the file can be changed if needed. The function mdsoft_read_map_entry() is used to parse a line in the map file. By modifying this function you can control the format of the input file.

Tips:

If your server runs with a time limit then you can get the system to default to a specific map when all players have disconnected. To do this all you need to do is set a map to have a valid maximum and a minimum of 0. For example, if you want your server to default to the map "Jailhouse Frag" then add the following line to your maps.lst file.

If you add more than one map of this type to your maps.lst file then one will be selected. Which one depends on your settings.


This code can be used freely. If you use this code then please email Mark "Grey" Davies and credit him.