Sending Values Between Levels

In Quake, very little is tracked from level to level. The only thing that persists is the state of the player, how much ammo, health and armour she has and which weapons she bears. As a result, there isn’t much tech available for mods to use in this way. So this article will have a look at how to send information between levels, and also how to maximize the use of the space so that mods can send extra values.

The main mechanism available is a set of “parms”. These are 16 global floats named parm1 through parm16, and the easy way to think of them is as special globals which are not reset when you change from one level to another. In the standard codebase they are set at the end of a level with the player’s ammo, health, etc. by SetChangeParms in client.qc. The next level starts and the player’s entity starts out with no values set, then DecodeLevelParms is run to take the information back out of the parms and onto the player entity proper.

Of course, the easy way of thinking about them is not entirely correct. In co-operative mode, there’s a bit of a problem with this system. We are using globals, but we actually have more that one player to remember the information about! Without some magic each player will wipe out the previous player’s information when it is being saved, and next level everyone will have the equipment of the final player in the list.

The engine pulls a little trick here – it creates space to store 16 values for each of the players, outside of the QuakeC’s reach. SetChangeParms is called directly for each player in turn, and between each call the engine copies the values of parm1parm16 into this player-specific storage. Similarly, before the engine runs PutClientInServer the engine copies the values back to parm1parm16. It’s worth mentioning the last little detail in this procedure: SetNewParms creates initial values for players starting a map from fresh, which is where 25 shells, a shotgun and an axe are made the default equipment.

There is one other place where information gets forwarded from level to level. This is the global variable serverflags, which is used in stock quake to record bitflags for the 4 runes you collect during the game. Unlike the parm... variables, this one is a straightforward global that gets copied across level loads (although with a bug well described by manoelka from makaqu we link to), no per-player stuff. Because the first 4 bits turn on things in the hud, you don’t have complete flexibility in how you use it, but you can use the higher bits freely.

So that’s the available storage. Each of these values is actually a floating point number (as seen in the infinite ammo hack). In that article we saw that these numbers can store 23 bits in their decimal places. While it is possible to store information in the power of 2, we won’t bother with that today, and just store whole numbers between 0 and 223 – 1. Neither will we use the sign bit as extra storage, as there’s one awkward corner case to deal with…

So we have 19 bits of global storage in the serverflags (ignoring the ones for the rune icons), plus 23 * 16 = 368 bits for each player. The players typically need more storage, but if you find you need to store more information about the world, you can get away with stashing it in the player storage instead (so long as you aren’t worried about theoretical corner cases in co-operative mode).  So lets take a look at how the original quake code uses that space:

	if ( <= 0) 
		SetNewParms ();
// remove items
	self.items = self.items - (self.items & (IT_KEY1 | IT_KEY2 | IT_INVISIBILITY | IT_INVULNERABILITY | IT_SUIT | IT_QUAD) );
// cap super health
	if ( > 100) = 100;
	if ( < 50) = 50;
	parm1 = self.items;
	parm2 =;
	parm3 = self.armorvalue;
	if (self.ammo_shells < 25)
		parm4 = 25;
		parm4 = self.ammo_shells;
	parm5 = self.ammo_nails;
	parm6 = self.ammo_rockets;
	parm7 = self.ammo_cells;
	parm8 = self.weapon;
	parm9 = self.armortype * 100;

This code is doing two things at once, setting restrictions on the things you can carry between levels, and encoding them into the parm storage. The encoding is very simple, just storing a single float in each, and even then we’ve got 7 parms to spare. Still, for fun, we can imagine our mod needs loads of storage. So lets look at how much storage stock Quake actually needs, and see how storage-optimised code might look.

Items: Because keys and powerups don’t carry across levels, we only need to store weapons. My philosophy for today is to make any reasonable assumptions which allow optimisations, and our first assumption will be: the player always has an axe and a shotgun. So there’s only need to record the six other weapons, which requires 6 bits of storage.

Health: Here we can exploit the fact that health is capped at 100 and the player never has less that 50 at level start. This means there are only 51 possible health values, so we can fit that into 6 bits as well.

Armorvalue: This can be as much as 200, so we need 8 bits to store it.

Shells, cells and rockets: These can be stored in 7 bits each, as they are never more than 100. (in fact we can encode all 3 as a 20 bit number but it’s too much more complicated for a 1 bit saving)

Nails: Like armorvalue we need 8 bits here

Weapon: This records which weapon out of 8 is selected, which only requires 3 bits. If we were really pushed for space we could even scrap this, I suspect nobody would notice if their weapon changes across level loads.

Armortype: The above encoding scheme records 2 decimal places of precision, but in fact there are only 4 values armortype ever takes…0, 0.3, 0.6 and 0.8. So it’s not a big assumption to say we can just use 2 bits of storage here.

If we add that up, we need 6 + 6 + 8 + 7 + 7 + 7 + 8 + 2 = 51 bits to store all the vital information. This is just too much to squeeze into two parms(without working much harder to get more out of the floating point format). So instead we’ll pack as much as we can into the first two parms; storing armourvalue, shells and nails in parm1, health, armourtype, rockets and cells in parm2, and items alone in parm3.

void(float item_bits, float health_bits,
	float armor_bits, float armor_type,
	float shell_bits, float nail_bits,
	float rocket_bits, float cell_bits) EncodeParms =
	parm1 = armor_bits + 256 * shell_bits + 256 * 128 * nail_bits;
	parm2 = health_bits + 64 * armor_type + 256 * rocket_bits
	        + 256 * 128 * cell_bits;
	parm3 = item_bits;

This function does the actual packing, but we need to convert the real values to representations as bits:

void() SetChangeParms =
	float item_bits, health_bits, armor_bits, armor_type;
	float shell_bits, nail_bits, rocket_bits, cell_bits;

	if ( <= 0) 
		SetNewParms ();
// keep only bits 2 through 7
	self.items = self.items & 126;
	item_bits = self.items * 0.5;
// cap super health
	if ( > 100) = 100;
	if ( < 50) = 50;
	health_bits = floor( - 50);
	armor_bits = floor(self.armorvalue) & 255;
	armor_type = (self.armortype > 0) + (self.armortype > 0.3)
	             + (self.armortype > 0.6);

	if (self.ammo_shells < 25)
		shell_bits = 25;
		shell_bits = floor(self.ammo_shells) & 127;
	nail_bits = floor(self.ammo_nails) & 255;
	rocket_bits = floor(self.ammo_rockets) & 127;
	cell_bits = floor(self.ammo_cells) & 127;

	EncodeParms(item_bits, health_bits, armor_bits, armor_type,
	            shell_bits, nail_bits, rocket_bits, cell_bits);

void() SetNewParms =
	EncodeParms(0, 50, 0, 0, 25, 0, 0, 0);

The armor_type line is a bit obscure, but it just sets armor_type to 3, 2, 1 or 0 for having red, yellow, green or no armour respectively. We’re also fairly defensive about making sure nothing is a decimal, even though under normal circumstances nothing should be. Also notice the SetNewParms – it’s important to note that the health_bits parameter encodes the number of hp above 50, hence why we set it to 50 rather than 100.

Decoding these parameters is similar, but it’s slightly more complicated than just running this code in reverse. Firstly, we can’t create a helper function which decodes the parms all at once in the same way we did with encoding, because QuakeC functions can accept 8 floats as inputs, but can only return one value at a time. So we do everything inline, messy but not a big deal. The other complication is that when we encoded the values we actually discarded lots of information that needs restoring, like the ammo and armour icons and selected weapon, but we can reconstruct that from the values we do have with a bit of extra code.

void() DecodeLevelParms =
	float bits;
	if (serverflags)
		if (world.model == "maps/start.bsp")
			SetNewParms ();		// take away all stuff on starting new episode

	bits = parm1 & 255;
	self.armorvalue = bits;
	parm1 = (parm1 - bits) / 256;

	bits = parm1 & 127;
	self.ammo_shells = bits;
	parm1 = (parm1 - bits) / 128;

	bits = parm1 & 255;
	self.ammo_nails = bits;

	bits = parm2 & 63; = bits + 50;
	parm2 = (parm2 - bits) / 64;

	bits = parm2 & 3;
	if(bits == 1)
		self.armortype = 0.3;
		self.items = IT_ARMOR1;
	else if (bits == 2)
		self.armortype = 0.6;
		self.items = IT_ARMOR2;
	else if (bits == 3)
		self.armortype = 0.8;
		self.items = IT_ARMOR3;
	parm2 = (parm2 - bits) / 4;

	bits = parm2 & 127;
	self.ammo_rockets = bits;
	parm2 = (parm2 - bits) / 128;

	bits = parm2 & 127;
	self.ammo_cells = bits;

	bits = parm3 & 127;
	self.items = self.items | (bits * 2) | IT_SHOTGUN | IT_AXE;

	self.weapon = W_BestWeapon ();
	W_SetCurrentAmmo ();

That’s all for today, with this code you have 13 whole parms free to store data in, not to mention the remaining bits in parm3. Why, there’s plenty of space to put the current weapon back in! Hopefully it’s also illustrated some of the ways you can maximize the value you get from the storage, by discarding anything you can afford to recalculate, and storing values as blocks of bits which can be combined. Have fun!


One thought on “Sending Values Between Levels

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )


Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.