Rotation System

Rotation files are means to replace old quake cvar-based rotation system. To enable it, just set xp_rotation to the file your rotation script is.

If the xp_rotation cvar is empty, or the file does not exist or it's empty (there are no maps on it, even if the file is not really empty), it falls back to the default quake behavior and executes whatever it is on the nextmap cvar.

Description

In its simplest form, you can just write a list of maps that your server will keep rotating once a map finishes. For example:

// very simple rotation file
q3dm1
q3dm2
q3dm3
q3dm4
q3dm5

The first line of the script is a comment and is ignored by the parser. Everything after double slashes // and a hash mark # until the end of the line is ignored. Additionally, an slash followed by an asterisk /* indicates the start of a comment, which can be spread across multiple lines until an asterisk followed by an slash */ is found.

From the second to last line on the example we have our rotation, which is simply a list of maps. Maps are separated with a newline character, or with a semicolon ;.

Additionally, you can modify the values of server cvars inside your rotation files by adding a dollar-sign $ before the cvar name and after it the equal sign =, followed by its value, optionally enclosed by double quotes ". Also, you can execute server commands, by prefixing it with a slash / or inverted slash \, and is read until the end of the line. This is illustrated in the following example:

// global settings, will be executed always before each map
$g_gametype = 3; // team deathmatch
$timelimit =  20;
$fraglimit =  30;
$xp_config =  "excessive5.cfg"; // strings can also be assigned
 
// kick any bot added in the previous map
/k allbots
 
// actual rotation
q3dm1
q3dm2
q3dm3

To separate multiple cvars you can alternatively use semicolons ;, which allows you to assign several cvars in the same line.

All these cvars and commands will be executed before each map, which is good to ensure that your server is running the right settings all the time. However, you can also set settings and run commands specifically for one map in the rotation, if you enclose these between brackets { } immediately after a map, like the following example shows:

// this will be executed before each map
$timelimit =  20;
$fraglimit =  35;
$g_gametype = 0;
 
q3dm1 {
    $fraglimit = 50;
}
q3dm2
q3dm3
 
q3ctf1 {
    $g_gametype = 4;
    $timelimit = 15;
    $capturelimit = 10;
    /load excessive3
}
 
// this will be ffa! remember the "g_gametype = 0"
q3ctf4

A very common usage of map blocks is to set the g_motd cvar to the information of the next map in the rotation, and the message will be printed to clients in their loading screens:

// this will be executed before each map
$timelimit =  20;
$fraglimit =  35;
$g_gametype = 0;
 
q3dm1 { $g_motd = "^5Next map: ^2q3dm2" }
q3dm2 { $g_motd = "^5Next map: ^2q3dm3" }
q3dm3 { $g_motd = "^5Next map: ^2q3dm1" }

Manual rotation

By default, a map will be rotated only at the end of the map. However, you can additionally use the command /rcon rotate [step] to manually change the current map. The command accepts an optional parameter, which can be:

  • A number, which will be used to rotate the map relatively to the current map. So, for example a value of 1 means rotate to the next map, a 2 to rotate two maps ahead, a -1 to rotate the the previous map and a 0 to restart the current map. If no argument is specified for the rotate command, then the value of xp_rotateStep will be used as the first parameter. Since a rotation is by definition cyclical, there is no problem in using any number as this parameter, even arbitrarily big ones, because for example in a 10 map rotation, the first map is also the 11th, 21th, and so on.
  • The restart keyword, or its shorthand, r, which will rotate to the first map in the rotation.

If you want to let your users the possibility of chose which maps to play on, you can add the rotate string to the xp_vote cvar. However, this will let the user the liberty to specify a parameter for the command. So, you can alternatively just add the nextmap string, which will just execute a /rotate command without any parameter.

Random rotation

As you could read in the latest section, the xp_rotateStep cvar controls how many maps to "jump" with a /rotate command without any parameter, which is what is used at the end of maps. You can use this cvar to modify the flow of your rotation, for example a value of -1 will mean that your rotation will run backward.

However, a more useful application for this xp_rotateStep cvar is the possibility for random rotations. To achieve that effect, just add the following line to your Crontab file:

// randomize rotation
* * * * * /execstr set xp_rotateStep $(sv_serverid)

This will change the value of the xp_rotateStep cvar to the value of the sv_serverid cvar, which is pretty much a random number that is used as the identifier for the server, and it changes every map. This allows for a true random map rotation.

Alternatively, if you want to keep your normal rotation, but let a user to callvote a random map, you can use a little quake scripting, and add the following line to the server cfg:

// cvar to hold the command to callvote a random map
/set randomMap "execstr rotate $(sv_serverid)"

The you just have to add the string vstr randomMap to the xp_vote cvar, and then the users can do /callvote "vstr randomMap".

Conditional rotation

To make server rotation even more dynamic, basic if-else structures are supported in rotation scripts. This allows you to rotate to specific maps only if a certain condition is met. To illustrate the syntax of if-else constructions, take a look at the example:

if ( $g_gametype != 3 ) { // NOT team deathmatch
    $timelimit = 15;
    $fraglimit = 50;
} else { // team deathmatch
    $timelimit = 10;
    $fraglimit = 100;
}
 
// just the same..
if ($g_gametype!=3) { $timelimit = 15; $fraglimit = 50; } else { $timelimit = 10; $fraglimit = 100; }
 
// a bit more complex?
if ( $g_gametype == 3 || $g_gametype == 8 ) { // tdm or freeze tag
    $g_quadFactor = 1;
 
    if ( $timelimit >= 20 ) { // 20+ minutes
        $fraglimit = 50;
    } else if ( $timelimit >= 12 ) { // 12+ minutes but less than 20
        $fraglimit = 35;
    } else { // less than 12 minutes
        $fraglimit = 25;
 
        $g_quadFactor = 3.5;
        /say ^1Quad Damage ^7is enabled because of the short timelimit!
    }
}

As you can see, the condition has to be of the form cvar comparison_operator value, where the operator can be one of the following:

  • Is equal to ==
  • Is not equal to !=
  • Is less than <
  • Is less or equal than <=
  • Is greater than >
  • Is greater or equal than >

The starting and closing brackets { } after the condition are mandatory.

The main usage of if-else constructions in rotation files, and actually the reason for its existence, is the possibility to modify the rotation and settings depending on the number of active clients on the server. For this purpose the cvar xp_activeClients exists, which holds the number of non-spectators currently playing on the server:

if ( $xp_activeClients >= 12 ) {
    // big maps, 12 clients or more
    q3dm12
    q3dm14
    q3dm15
} else if ( $xp_activeClients >= 6 ) {
    // normal maps, from 6 to 11 clients
    q3dm6
    q3dm7
    q3dm8
} else {
    // small maps, 5 clients or less
    q3dm2
    q3dm3
    q3dm5
}

You can also use conditions inside map blocks:

$g_gametype = 3;
 
q3dm6 {
    if ( $xp_activeClients > 6 ) { $fraglimit = 100; }
    else { $fraglimit = 50; }
}

Conditional rotation gotcha

To finalize, there is an issue to keep an eye on when using conditions inside the rotation file. To illustrate it, consider the following rotation:

q3dm6
if ( $xp_activeClients > 6 ) { q3dm13 }
q3dm8
q3dm9

It's pretty obvious what is the objective with that rotation file: to rotate to q3dm13 only if there is more than 6 clients on the server, but otherwise rotate directly to q3dm8.

However, a problem arises here, and it's because of the way the rotation works internally: it first counts the number of maps on the rotation, ignoring the maps for which the conditions are not met, and then it executes the map in turn. The effects of this are two problems:

  • If we are currently playing q3dm8 with 4 people, this means we are on the second map of the rotation, but if the number increases up to 8 at the end of the map, on next map we will rotate to the third map of the rotation, which will effectively just restart q3dm8 because it is the third map of the rotation for more than 6 players.
  • If we are currently playing on q3dm13 with 8 players, we are currently at the second map of the rotation, but if the number of players decreases down to 4 at the end of the map, on next map we will rotate to the third map of the rotation, which is q3dm9 for 6 players or less, jumping over q3dm8.

These problems can become worse the more we use that kind of conditions. To fix this, always include an else with the same number of maps of the original condition so that the total number of maps doesn't vary depending on the number of players. Applying this to the example, the fixed rotation would look something like the following:

q3dm6
if ( $xp_activeClients > 6 ) { q3dm13 }
else { q3dm7 }
q3dm8
q3dm9