Post tutorial Report RSS Quake c - parm* variable storage

Using parm[1-16] efficiently to store player data during level transitions.

Posted by on - Intermediate Server Side Coding

What are parm[1-16] ?

The definition for quake-c is found in defs.qc:

float parm1, parm2, parm3, parm4, parm5, parm6, parm7, parm8, parm9, parm10, parm11, parm12, parm13, parm14, parm15, parm16;

Here we have 16 quake-c floating point variables.
These are part of the quake engine - progs.dat shared data structure "GLOBALVARS_T C STRUCTURE".

What this means is that they can be set in quake-c and referenced in the quake engine C code and vice-versa.

They are significant in that when a level is exited and a new level is loaded they represent the only per-player game data storage outside of console variables. This is very significant to mod authors - if you are enhancing weapons or items, these variables are the only way to store data about what items the player possess when he exits a map.

How are they used? First we need to understand how quake-c represents floating point data.

Float in quake-c is a dual use variable. They can store typical floating point data with mantissa, sign and exponent - this incudes any negative value. They can also store 24 bits of integer data valued 0 - 16777215. In quake-c the integer use is more prevalent as most game data is in integer format.

Here is an example from client.qc:
(These are not functions, just code fragments.)

DecodeLevelParms:
// retrieve stored data after map loads

self.items = parm1;
self.health = parm2;
self.armorvalue = parm3;
self.ammo_shells = parm4;
self.ammo_nails = parm5;
self.ammo_rockets = parm6;
self.ammo_cells = parm7;

SetChangeParms:
// save data on map exit

parm1 = self.items;
parm2 = self.health;
parm3 = self.armorvalue;
if (self.ammo_shells < 25)
parm4 = 25;
else
parm4 = self.ammo_shells;
parm5 = self.ammo_nails;
parm6 = self.ammo_rockets;
parm7 = self.ammo_cells;

While this seems fine, when you are a mod author adding a host of weapons and items you require betwixt level storage - the original code wastes a lot of bits.

This type of coding returns to the early days of assembly language - the Z80 and 6502. Machines often only had 16K of memory and you had to use every last bit wisely.

Health has a max value of 100 in transition. Likewise the armor max is 200 - shells, rockets and cells have a 100 maximum and nails is 200.

If you do your binary math, 8 bits has a max value of 255. You can store 3 of those in a 24 bit (represented in integer form in quake-c) float parm* variable. What does this mean?
You can store the values the vanilla code stores in parm2 - parm7 in parm2 and parm3!
That leaves you 4 extra 24 bit slots for your mod data.

Here are the functions to replace the vanilla quake-c functions. Obviously you will need to change these to fit your mods needs.

This code fragment replaces the following functions in client.qc:
DecodeLevelParms
SetChangeParms
SetNewParms

They are found in a block labeled "LEVEL CHANGING / INTERMISSION"
As a cut and paste replacement the following segment gains are minimal until you add code for a mod.

Two slightly more advanced functions, g_encode and g_decode have been included as examples of more complex encoding of data in other than 8 bit increments. They are not used by this code segment.

It would not be difficult to use them, but check your binary math very carefully. All data must be encoded and decoded exactly the same way. Swap low bits and high bits and your data will be corrupt!

Each new function in this segment is explained in the preceding comments.
If you have any serious questions regarding this code, please leave a comment with your inquiry.
If you have any silly questions you risk a taunting.

--- begin code segment ---

// boundary check function
// take a float value and return:

// < 0 - return 0
// 0 - MAX - return integer value, always rounding down
// > MAX - return MAX
// where MAX is the max bit value, 255 is used with standard encoder below

float(float f, float MAX) f_bound =
{
if (MAX > 16777215) MAX = 16777215;

if (f > MAX)
f = MAX;
else if (f < 0)
f = 0;
f = floor(f);
return f;
};

// parm* value encoding and decoding
// maximize storage of data in parm* values
// most ammo data has a maximum of 200, armor is 200, health is 100, etc.
// this can be encoded into 8 bits -
// the original code uses 1 parm* per data point - a serious waste of bits
// each parm* variable has 24 bits - 3 * 8 bit values

// encode 3 values according to formulas with all values bounded 0 - 255
// values are set into float value 0 - 16777215

// where
// s1 - into high 8 bits
// s2 - into middle 8 bits
// s3 - into low 8 bits

float(float s1, float s2, float s3) encode =
{
local float f;
f = f_bound(s1, 255) * 65536 + f_bound(s2, 255) * 256 + f_bound(s3, 255);
return f;
};

// generic encode

// s1 is value
// bits are max value - 1, 3, 7, 15, 31...255...65535...{n} - a series of 1 bits
// pos is the position in the parm* variable
// - position will be the next bit value above the previous bits
// - i.e. for 3 value bits at position 1, the next position is 4
// - for 7 value bits at position 4, the next position is 32
// binary math equivalent:
// - 00000011 - 3 value at 1
// - 00011100 - 7 value at 4
// - 11100000 - 7 value at 32

// you could encode any values in any position with this function
// it is included here as an example if you have values smaller or larger than 255 to encode
// such as the painkeep carry items with a limit of 3 on most inventory

float(float s1, float bits, float pos) g_encode =
{
local float f;
f = f_bound(s1, bits) * pos;
return f;
};

// recover an 8 bit value (0 - 255) from parm encoding

// parmval - passed value of parm* variable

// where which is
// 1 - return high 8 bits
// 2 - return middle 8 bits
// 3 - return low 8 bits
// any other value - return low 8 bits

// use these handy codes to avoid confusion
float HI8 = 1; // s1 value of encode() call
float MID8 = 2; // s2 value of encode() call
float LOW8 = 3; // s3 value of encode() call

float(float parmval, float which) decode =
{
local float f;

if (which == 1)
f = (parmval / 65536) & 255;
else if (which == 2)
f = (parmval / 256) & 255;
else
f = parmval & 255;
return f;
};

// generic decode - extract data from generic encode above

// parmval is the parm* variable data
// bits are max value - 1, 3, 7, 15, 31...255...65535...{n} - a series of 1 bits
// pos is the position in the parm* variable

// this uses the same bits & pos to extract any generic encoded data
// see notes under g_encode for more details

float(float parmval, float bits, float pos) g_decode =
{
local float f;

f = (parmval / pos) & bits;
return f;
};

// replacement code for client.qc encode / decode functions
// these functions are called on map exit / map load to save player data in between levels
// all other data is zeroed!

// *** NOTE: care must be taken to encode and decode data from the same position in the parm* variables this way
// a simple notation error in encoding or decoding will corrupt data !

// the original code used 9 parms - leaving 7 for mods
// this code uses 4 parms for the original 9 and leaves one 8 bit slot in parm4 and 12 more parms free

void() SetNewParms =
{
parm1 = IT_SHOTGUN | IT_AXE;
parm2 = encode(25, 0, 100);
parm3 = 0; // dont need to encode 0 values
parm4 = 1; // dont need to encode - self.weapon defaults to 1 and it goes in LOW8
};

void() SetChangeParms =
{
if (self.health <= 0)
{
SetNewParms ();
return;
}

// remove items
self.items = self.items - (self.items &
(IT_KEY1 | IT_KEY2 | IT_INVISIBILITY | IT_INVULNERABILITY | IT_SUIT | IT_QUAD) );

// cap super health
if (self.health > 100)
self.health = 100;
if (self.health < 50)
self.health = 50;

// lower bound on shells
if (self.ammo_shells < 25)
self.ammo_shells = 25;

parm1 = self.items; // no encoding - items is potentially 24 bits

parm2 = encode(self.ammo_shells, self.armorvalue, self.health);
parm3 = encode(self.ammo_cells, self.ammo_rockets, self.ammo_nails);

// armortype is 0.3, 0.6, or 0.8 depending on armor and needs to be converted to an int value to store
parm4 = encode(0, self.armortype * 100, self.weapon);
};

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

self.items = parm1;

self.health = decode(parm2, LOW8);
self.armorvalue = decode(parm2, MID8);
self.ammo_shells = decode(parm2, HI8);

self.ammo_nails = decode(parm3, LOW8);
self.ammo_rockets = decode(parm3, MID8);
self.ammo_cells = decode(parm3, HI8);

self.weapon = decode(parm4, LOW8);
self.armortype = decode(parm4, MID8) * 0.01;
};

Post comment Comments
LordHavoc
LordHavoc - - 11 comments

The lhbitparms code in util.qc in dpmod may also be of use, it allows some extremely fine grained packing of up to 31 bits per parm or 30 bits per cvar (there are a bunch of gotchas on cvars though).

lhbitparms can also extend to using cvars for even more storage (as seen in Prydon Gate), but means you need to copy those cvars to globals, and have a RestoreGame() function that stores the fields back to cvars so that any "restart" commands will have the cvars set properly before the DecodeLevelParms is executed.

Another thing to explore is using fopen (part of FRIK_FILE extension) and load/save character information in a separate file (Prydon Gate does this for multiplayer mode).

Reply Good karma Bad karma+2 votes
numbersix Author
numbersix - - 2,244 comments

Excellent. I'll look into these and perhaps post some examples of their use. Thanks.

Reply Good karma+1 vote
numbersix Author
numbersix - - 2,244 comments

Quake-c Manual ver 3.4 entry for parm*:

7.2 Global Variables

These variables are accessible in every function.

Variable: parm1...parm16

float parm1; // items bit flag (IT_SHOTGUN | IT_AXE )
float parm2; // health
float parm3; // armorvalue
float parm4, parm5, parm6, parm7; // ammo
float parm8; // weapon
float parm9; // armortype*100
float parm10, parm11, parm12, parm13, parm14, parm15, parm16;
Those parameters seem to be a bit of hack. They are used when a client connects.
Spawnparms are used to encode information about clients across server level changes

Reply Good karma+1 vote
Post a comment

Your comment will be anonymous unless you join the community. Or sign in with your social account: