Contents by David Walton and Marco Hladik. Edited and compiled by Marco Hladik. Last updated: 10th August 2018, 9:21 PM GMT
Website version created by shadowelite7 in 2024
QuakeC is a language originally created by ID software for use in in the video game Quake. Vanilla QuakeC is quite a limited language, however with qcc improvements and engine extensions, its less annoying to write and also much more capable as a programming language. Today we're dealing with different types of modules. The original QuakeC only supported the modification of game-code run on the server. It was not capable of altering any client-side behaviour. Just send a selection of commands and hope that the client interprets the stuff properly.
This is the classic form of QuakeC, and is the module executed by the server component. SSQC's reason to exist is so that it can deal with networking. It is normally considered authoritive, which is a fancy way of saying that CSQC isn't to be trusted (if only due to packetloss). As such, the SSQC is normally expected to track the state of any entity that has any impact on the outcome of a game. In mods designed to run without CSQC, the SSQC generally does a lot more than just that, however it is also potentially running on a computer on the other side of the planet, so while you can write your entire game in just the SSQC module, doing so means that you have no control over the user's actual screen, and anything that is displayed can be quite laggy which may limit what you can expect to achieve with SSQC alone. There are two major variations of SSQC:
This module runs clientside. Each client potentially has its own instance of CSQC, and thus they'll all end up disagreeing about some part of the game without the SSQC to provide authoritive information - remember this: The SSQC knows best. CSQC normally has full control over the client's screen (via builtins). It is normally the CSQC that decides when and where to draw the various hud elements. The CSQC also has control over the 3d view, and can not only decide where to draw the 3d view, but it can also change the camera as well as directly control the entities that are inserted into that view. However, not every engine supports CSQC, and those that do might have it disabled for whatever reason. There are three variations of CSQC:
CSQC tends to be more procedural than SSQC. Generally there is only one player so that info can all be stored in globals instead of entities (splitscreen may require arrays).
The mythical MenuQC module is responsible for drawing 2d menus. It serves many of the CSQC's roles, except that it does not get purged on map changes and is explicitly designed to remain running even when not connected to a server. Becase MenuQC was implemented in DP first combined with FTE's actual attempts at compatibility, there is only a single standard for MenuQC. However, while MenuQC is standard, both major supporters have their own extensions, but even worse is that no two engines have the same names for the same cvar. Indeed, cvar-hell is why it is generally not recommended for a mod to even try to support more than one engine. Its much easier to just use the engine's menus instead. As such, MenuQC is generally used only for standalone total conversions.
The best way to get started is to find an existing qc-only mod (like id1qc.zip), extract the qc source files somewhere like c:\quake\mymod\src\*.qc, shift-right-click the extracted .src file and select open-with and find fteqccgui.exe. Then in fteqccgui press F7 to compile, then press F5 to run (you'll be prompted to locate the exe you want to run as well as you quake directory. In the engine you can then just load a map. One common suggestion is to open weapons.qc, find the W_FireRocket function, and to then change the velocity multiplier for really slow moving rockets, just to see that you've made a change. There are many many other things you could do instead, of course.
Your mod, your choice.
Many commandline arguments are best migrated to pragmas, or set via fteqccgui's options menu.
All QuakeC starts with a single file - aka the .src. This file comes in two forms.
This is fantastic for confusing decompilers but otherwise should not be used. The accumulate version above sets an implicit variable which is automatically returned once control reaches the end of the function. The other forms will leave the function there and then. For compatibility reasons, fteqcc does not strictly enforce return types, however this is bad practise and you really should fix any related warning if you want to avoid unintended type punning.
goto foo;
unreachablecode();
foo:
QuakeC looks for truth in multiple ways, and all control statements use the same basic form of truth, which depends upon types, but unitialised variables are always FALSE (except field references, which are typically auto-initialised):
HexenC adds an inverted form of if statements:
if not(condition)
break;
Which is especially useful to test the return value of eg fgets, allowing you to detect end-of-file conditions without breaking when encountering empty lines - note that `if(!condition)` tests for empty instead of null, and is thus not suitable in all cases.
do {
statements();
} while( condition );
__state [framenumber, functiontothink]; // quake style, accepts expressions,
however do not use pre-increment
nor post-increment as this is
potentially ambiguous - use
brackets if you need this.
__state [++ firstframe .. lastframe]; // hexen2 style, MUST use immediates
/ frame macros.
For the hexen2-style version, the animation automatically repeats selecting(wrapping) the frame. The cycle_wrapped global will be set according to whether the animation wrapped or not (starting from a frame outside of the range does not count as a wrap).
switch / case / default
switch (expression) {
case 0:
statements;
break;
case 1..5: // ranges are inclusive, so this includes 5 but not 5.001
statements;
break;
case 6: // falls through
case 7:
statements;
break;
default: // if none of the above cases matched (like 8 or .3)
statements;
break;
}
Switches allow a slightly cleaner alternative to massive if-else-if-else chains. The case statements define various possible values, with the following code being executed. The 'default' statement defines a fallback location to execute from if none of the cases matched the expression's value. Note that execution will continue through any later cases, so be sure to use break statements to prevent undesired fall-throughts. Cases may be either a single value, or in the case of a numeric expression they may be a range of values seperated by a double-dot (eg: case 0..1:foo;break;). Cases are not required to be constants, but they must not be expressions (which means fixed array indexes are acceptable, but not dynamic indexes).
__thinktime ent : delay;
is equivelent to
ent.nextthink = time + delay;
When using -Th2 on the commandline, the double-underscores are not required.
__until(condition) {statements;}
A flipped version of while loops - the loop will repeat until the condition
becomes true. When using -Th2 on the commandline, the double-underscores are not
required.
__loop { statements; }
Generally its better to use while(1){} or for(;;){} instead
When using -Th2 on the commandline, the double-underscores are not required.
Quake's objects. Each one has its own copy of each and every field. Except for the world and players, the only difference between each entity is the contents of its fields. Note that collision and pvs state is tracked invisibly, and can be updated by calling setorigin.
These are standard IEEE single precision floats. If you are attempting to store bit-values in a float then go no higher than 24 bits, failure to adhere to this can result in the lower bits getting forgotten due to precision issues.
Float immediates can be specified as decimal (eg: 5.3), or as hex (eg: 0x554336 - note the bit limitation means you should stay with 6 hex digits). Numbers with no decimal point are normally assumed to be floats, but if you wish to be explicit then you can simply add a trailing point.
Additionally, character codes can be inserted as immediates using eg 'x' for the ascii value of the x glyph.
A denormalised float is a very small float with its exponent part set to 0, which is a special case of floats where the mantissa component has no implied leading bit set to 1. These thus have the same representation as an integer with up to 23 bits and can thus be used to manipulate pointers/ents/strings in various hacky ways (as popularised by qccx). Not only are such hacks not recommended due to engine incompatibilties, but many CPUs/binaries are explicitly configured to treat all denormalised floats as 0 for performance reasons and so their use isn't recommended even when not exploiting an engine's QCVM, and any operation that implicitly requires denormalised floats will generate warnings (which can be disabled).
String immediates take the form of eg: "Hello World". Implicit concatenation exists and can be used by simply placing two adjacent immediates. To avoid issues with codepages and unicode conversions, strings should be kept as ascii where possible, this can be achieved with string escapes instead of pasting non-ascii values from an external tool.
There are a few other escapes, but I cba to document them as I'd rather people just used \x instead.
Additionally, FTEQCC supports raw strings. These are specified as eg:
R"delim(Your String Here)delim".
Any and all text within the two brackets is your string - including any new line
characters exactly as they are in the file.
Any escapes are ignored, and will appear in the resulting string as-is
(you'll see the backslashes in-game).
The two 'delim' words in the example must match each other (but can be 0-length) and are used as a way to allow close-bracket+double-quote pairs to exist inside the raw string itself, including nested raw strings (so long as the delimiters differ). Note that file paths in quake should normally always use the more portable forward-slash for path seperators, instead of the microsoft-only backslash seperator.
Some engines support internationalisation. This is achieved by replacing all strings defined as e.g. _("foo") according to the contents of a progs.LANG.po file. Mods that use this mechanism will likely also need to enable utf-8 support in the engine, as well as use a unicode font.
Strings in the QCVM are nominally considered to be indexes from the start of some string table defined by the progs.dat, however there are many special types of string that are special or flawed.
QS behaviour: Cycles between 16 tempstrings.
DP behaviour: Allocates more memory for tempstrings as needed.
Fails if strunzone is called on loaded saved games, spams if
tempstrings are still referenced when saving.
FTEQW behaviour: Does not distinguish between permanent strings, allocated
strings, nor temporary strings. All three have the exact
same behaviour - strzone is an alias for strcat (and
strunzone is a no-op), and tempstrings are collected only
once they are no longer referenced.
This avoids most savedgame issues.
Generally requires opcode extensions. Note that unlike C, QC assumes numeric immediates to be floats. Normally you should use a postfix of i to explicitly make it an int immediate, but you can also enable a specific compiler flag to assume immediates as ints.
Vector immediates traditionally take the form of 'x y z', but using [x, y, z] allows formulas and thus simpler argument passing. Note that vector*vector yields a dot-product, while all other operations are per-channel. Normally vectors act a bit like unions and define both 'vec' and 'vec_[x|y|z]' variables allowing for direct channel access, however the _x etc versions are not always available (eg in arrays). The modern way to access individual channels is with eg vec.x instead. Note that array indexing also works, so vec[0] returns the _x component. Dynamic indexes work also, but with the same performance hit as arrays so it is generally best to avoid that if you can.
This type represents an undefined type no larger than a vector, and should normally be used only for function arguments or return value, but can also be used for type punning.
FTEQCC supports single-dimension arrays.
float foo[] = {1,2,3,4};
will thus define a float array and will infer its length as 4.
If you need to use an explicit length, or you do not wish to initialise the
array, then you must put the needed length inside the square brackets.
Note that arrays are 0-based, so an array defined as float foo[2]; can be
accessed as foo[0] or foo[1], but any other value (including foo[2]) is out of
bounds. Indexes will be rounded down.
foo.length can be read if you wish to know how long an array is (especially if
the length was inferred for some reason)
Dynamic indexes ARE supported in all cases, however they may come at a
significant performance loss if you do not have extended opcodes enabled, so it
is generally preferable to unroll small loops such that constant indexes can be
used.
Dynamic lengths are not supported at this time.
Arrays are also supported within structs, allowing for (clumsy)
multi-dimensional arrays.
Fields are defined by prefixing the field's type with a dot.
Every single entity will then have its own instance of the data, accessed via
eg: self.myfield
Fields are assumed to be constants, with any uninitialsed fields reserving space
in every single entity in order to hold the data. Fields defined as var or
initialised to the value of another field will NOT consume any space within
each entity.
.float foo; // const, allocates storage.
var .float bar = foo;
void() fnar =
{
self.foo = 42;
if (self.bar != 42)
dprintf("the impossible happened\n");
};
Note that field references are variables in their own right. They can be defined as arrays, they can be passed to functions etc (eg the find builtin depends upon it). A word of warning though - field references will not appear in most saved game formats. That's probably fine for deathmatch, but if you're using them for singleplayer then be sure to initialise them (even if to the value of another).
In QuakeC, functions are both a function reference/variable, and typically a function immediate/body, much like a string is both a string reference, and the string data itself. So function definitions are initialised in basically the same way as any other variable, where the function-type syntax is basically just returntype(arglist), eg:
returntype(arglist) name = functionbody;
functionbody can then be either some other function, or a function immediate.
As a special exception for C compatibility, the arglist parenthasis can follow
the function's name instead, and when the initialiser is an immediate, the
equals and trailing semi-colon tokens are optional.
Note that function immediates are what contain the bulk of code, and can only be
present within the context of a function type - meaning either in a function
initialisation or via a cast (creating an annonymous function - note that these
are not safe for saved games, so try to avoid their use in ssqc).
Because functions are really function references, you can define them as var,
and by doing so you can remap/wrap them at any point you wish. function types
can also be used freely within function arguments or anywhere else.
void() somename = {}; // QC-style function
void somename(void) {} // C-style
void() foo = [$newframe, nextfunc] {}; // QC state function ( function
// contains: self.frame = $newframe;
// self.think = nextfunc;
// self.nextthink = time+0.1; )
var void() dynreference; // uninitialised function reference, for
// dynamically switching functions.
var void() dynreference = somename; // initialised function references work
void(vector foo, float bar) func = {}; // function with two arguments.
void(float foo = 4) func = {}; // function that will be passed 4 for the
// first argument if it is omitted.
void(... foo) func = {
for(float f = 0; f < foo; f++)
bprint(va_arg(f, string));
}; // variable-args function that passes each arg to another.
void(optional float foo, ...) func = #0; // name-linked builtin where the
// first arg may be ommitted but
// must otherwise be a float, with
// up to 7 other args.
void(void) func = #0:foo; // unnumbered-but-named builtin.
void(float foo) func : 0; // hexenc builtin syntax
Note that QC can only tell how many arguments were passed using the variable-argument argcount form. Any optional arguments that were omitted will have undefined values. Any argumentsspecified after an optional argument must be omitted if the prior arg is omitted but otherwise must be specified unless they are optional themselves, or preinitialised. Preinitialised non-optional arguments can be omitted, their preinitialised value will be passed in this case. If the argument list is empty (ie: two adjacent commas in the call), then the argument's default value will be passed automatically regardless of whether the argument is considered optional. Builtins can only accept up to 8 args. Non-builtins have a much higher limit. Vectors count as 1 argument (and thus allow you to pack additional information).
Structs can be passed but will be counted as ceil(sizeof(thestruct)/sizeof(vector)) arguments. Named builtins can be used by some (but not all) engines to avoid numbering conflicts. Typically these builtins must also be numbered as 0.
typedef type newname;
Typedefs allow the creation of an alias for types.
enum int
{
FIRST,
SECOND,
THIRD,
NINTH=8
};
Provides an easy way to define a set of constants. enum: Each name will default to one higher than the previous name, with the first defaulting to 0. enumflags: Each name will default to twice the previous name, with the first defaulting to 1, ideal for bit flags. Strongly typed enums are also supported:
enum class foo { cake, pie, sauce };
enum class bar { banana, sauce, pie };
Standard enums do not have their own scope. So you could not reuse the names of inside constants.
Allows you to define a struct, which is useful for boxing multiple related variables.
typedef struct
{
vector rgb;
float a;
} rgba_t;
rgba_t opaque_red = {'1 0 0', 1};
void(rgba_t c) fillscreen =
{
drawfill([0,0], screensize, c.rgb, c.a, 0};
};
void() drawstuff =
{
fillscreen(opaque_red);
};
These are especially useful when combined with pointers, but can also simplify copies.
Equivelent to structs, except all struct members start at the same offset. This can be used for either type punning or compressing mutually-exclusive fields inside complex struct layouts. By nesting structs, unions and arrays, you can get some quite complex data structures. Note that unions and structs define within unions or structs do not need to be named. Members from child structs will automatically be accessed as if they were part of the containing stuct.
struct
{
float type;
union
{
struct
{
float f;
vector bar;
};
struct
{
string s[2];
};
};
} foo[8];
float() foobar =
{
if (foo[4].type)
return stof(foo[4].s[1]);
else
return foo[4].f;
};
class foo : entity
{
float interval;
virtual void() think =
{
centerprint(enemy, this.message);
nextthink = time+interval;
};
nonvirtual void(enemy e) setEnemy =
{
enemy = e;
};
void() foo =
{
nextthink = time+interval;
};
};
void() someiplayerfunction =
{
foo myfoo = spawn(foo, message:"Hello World", interval:5);
myfoo.setEnemy(self);
};
The above is a terrible example, sorry.
If the parent class is omitted, entity will be assumed.
Class constructors double up as spawn functions, and the spawn intrisic works
in the same way - the named members will be set before the constructor is
called, instead of passing arguments to the constructor (which avoids the need
for overloads etc). The 'interval' field above is defined as a class field.
Such class fields are valid ONLY on entities of that class, which allows for
more efficient memory usage.
Member functions can be defined as virtual (such functions are technically
pre-initialised fields, and thus compatible with things like think or touch),
non-virtual (read: non-inherited), or static (where 'this' cannot be used).
Public, private, protected are parsed like in C++, but ignored.
Only a single parent type can be inherited, and it must be a class or the
general entity type.
(Note: not a keyword)
As in C, pointers are defined using an asterisk prefix on the type's name.
float *ptr; // define a pointer-to-float
ptr = &self.frame; // obtain the address of a variable
*ptr = *ptr + 1; // access through the pointer (aka: dereference)
In order to define a field that contains a pointer (instead of a pointer to
a fieldref), use '.*float ptr;', or you can define the field using a typedefed
pointer.
Pointers can be used without qcc extensions, however you can only get the
address of an entity's field, and you can only write. This is still sufficient
to rewrite world's fields, but not particuarly useful otherwise.
It is not possible to pass the address of a local into a child function, due to
QCVM limitations. As a work around, you can define the local as static or use
the alloca intrinsic. Generally you should use __[in]out arguments instead.
Classes can be prototyped with just 'class foo;', but any class-specific fields
will not be usable until the actual class definition, which can mean that
methods must have their code defined outside of the class itself.
A method can include only its prototype within the class, and with the eventual
method being defined as eg: 'void() classname::methodname = {};'.
accessors are a weird and whacky way to invoke functions in a more friendly way.
They allow a reference/handle to provide a number of properties that invoke get
or set functions when used. Many of these functions are excelent candidates for
inlining...
A good example is that of setting up a string-buffer type that invokes an
engine's bufstr_get/set builtins, allowing you to write stringbuffer code as if
it were simply accessing an array.
index types can be any type of variable, so eg hash tables can be accessed as:
hashaccessor["foo"]
Here's an example using the string buffers extension:
accessor strbuf : float
{
inline get float asfloat[float idx] = {
return stof(bufstr_get(this, idx));
};
inline set float asfloat[float idx] = {
bufstr_set(this, idx, ftos(value));
};
// we can get away with directly referencing existing functions/builtins
get string[float] = bufstr_get;
set string[float] = bufstr_set;
get float length = buf_getsize;
};
void() accessorexample =
{
// buf_create normally returns a handle in a float
strbuf b = (strbuf)buf_create();
// We can now use b as if it were defined as string b[];
// There isn't even a limit to the indexes!
b[0] = "This is";
b[1] = "a test";
b[2] = "of stringbuffer access";
// gap!
b.asfloat[4] = 4;
// loop through them all. Note how the length property invokes
// buf_getsize which tells us the maximum valid index.
for (float i = 0; i < b.length; i++)
print(sprintf("%d: %s\n", i, b[i]));
// still needs to be freed though
buf_destroy((float)b);
};
An accessor property defined with an & after the access type specifies that the 'this' inside the code can actually be written to. Otherwise it should be considered const, which is fine in the above case where it is just a handle. In set properties, the value to assign is simply called 'value'. If there is just a type with no name inside the array, then the used key will be named 'index'. The index type can be anything, so long as it is typedefed. Note that eg 'b.foo' is equivelent to 'b["foo"]' when b is a variable of type accessor and 'foo' is not a property of b, b has an unnamed property with an index type of string, and 'foo' isn't an immediate. This is useful with accessors built around hashtables. The unnamed property can be a non-array too - such properties can be accessed only via eg '*b'.
Definitions are either constants or variables. Initialised globals are normally considered constants, while locals are always assumed to be variables (this part differs from vanilla qcc). Uninitialised field and function globals are considered const also (const fields will be auto-initialised, while functions will generate an error if they are not explicitly initialised).
This variable is probably unused. Don't warn about it. These variables may also be stripped completely if they are not referenced anywhere.
This variable is actually used somewhere, even if the qcc cannot tell that (eg: by the engine). There will be no warnings about it being unused, and it will NOT be auto-stripped.
Obsolete prefix that means nothing on its own. Vanilla QC used this to tell the compiler to expect a variable definition inside a function (instead of actual code). However, in FTEQCC this should not normally be needed except with certain rare type modifiers.
Static globals are visible only within the .qc file in which they are defined.
Static locals are visible only within the function in which they are defined
(but also become persistent and do not lose their values between calls - like
globals).
Note that static variables use name mangling that might get renamed between
releases, which can break saved games if you're not careful.
Globals marked as nosave will not appear in saved games. They will thus lose their values in saved games, which might either be undesirable, or a clumsy way to detect reloads. When the game is reloaded, they will typically revert to the values that were set at time=0.2 on account of the weird way that saved games work. nosave is recommended for variables that track objects which cannot be reloaded, like file handles.
Functions marked as inline will be eligable for inlining for a small performance
boost.
FTEQCC's inlining is limited, and should generally only be used for small
functions (eg ones that exist to just call other functions with a different
argument order etc).
This variable / function / field is NOT used. Function bodies will be ignored, and any definitions will be stripped. If the qcc detects that it is still required somewhere, you will get a compile error. This can be used inside #defines to create CSQC-only or ssqc-only functions without needing #ifdefs inside every single function.
Globals marked as shared will have the same value regardless of which dat it was defined in within a single QCVM (read: for mutators, not CSQC).
optional function arguments may be omitted without warnings/errors. Note that only the engine can tell how many args were actually passed, so this should normally only be used on builtins. QC functions should normally use initialised arguments instead. These have well-defined values if the argument is ommitted (slightly slower, but avoids the need for extra signalling).
Valid only for function arguments. By default, all function arguments are __in arguments. However, if they're defined as __inout or __out then any changes the callee made to the argument will be written back to the passed value in the caller. __out arguments cannot accept constants, nor any other expression that cannot be assigned to (like additions etc). This mechanism allows a function to return multiple values without needing to resort to vectors nor structs nor pointers.
Weak symbols will not conflict with other definitions of the same variable. Weak symbols will be ignored if they are already defined, and replaced if followed by a non-weak definition with the same name.
Defines a function that is a wrapper for a prior instance of the function with the same name. There MUST be a function already defined with the same name, you can define one as __weak if there is not. If you combine __weak and _wrap on the same function, then the function will be silently ignored if there was no prior define. Wrappers MUST reference their 'prior' function, but they can do so by discarding it, eg: (void)prior;
Accumultate is a more efficient but limited way of combining functions. Additional definitions of the function will be concatenated onto the end of the prior function. A followed return statement in any of the prior functions will prevent any later accumulations from executing - thus an alternative way to specify return values is recommended, eg: return = 5;
Operator precedence follows somewhat complex rules that do NOT match C's
operator precedence.
As a general rule, QC is more forgiving, at least if you don't expect C
precedence, particuarly around the & and | operators.
Operators are listed in rough order of priority.
Unary operators are normally written with the operator before their argument,
the exception being post-increment/decrement.
childtype foo = (childtype)parentref;
Note that casting floats to ints provides a convienient way to truncate a float towards 0, eg:
float f = (int)5.3;
vector tmp; vec = ( tmp_x = 5, tmp_y = 4,tmp_z = 7, tmp );
Is equivelent to 'vec = [5,4,7];' and thus has limited use in macros or to
express multiple statements without the use of a block and any extra indentation
required by coding style guides.
self.mdl = used_model("progs/foo.mdl");
..is fine.
iD software's modelgen and spritegen tools were designed to read special
commands directly from qc files, ensuring that data was kept in sync.
Those tools are basically irrelevant now, but the qc syntax remains regardless,
and has been extended Model generation commands always start on a new line with
a leading $ char.
These are enabled with -Ffoo or disabled with -Fno-foo. Many (but not all) can also be reconfigured mid-compile, using: #pragma flag [en|dis]able foo
In player_stand1:
if (self.velocity_x || self.velocity_y)
In player_run:
if (!(self.velocity_x || self.velocity_y))
Different engines support different extensions.
These take the form of added builtins, new fields with special side effects (or
even existing fields with new values meaning new stuff), or globals that report
additional results or change the behaviour of existing builtins.
Engine extensions can be useful but they can also restrict which engines your
mod can run on. You'll need to find the right compromise for your mod yourself.
Most of the common important extensions can be queried at run time.
This is done eg as following:
if (cvar("pr_checkextension"))
if (checkextension("FRIK_FILE"))
canopenfiles = TRUE;
Note that QuakeC does not normally early-out, so the two if statements must be
nested and not replaced with an && operator.
FTEQW (and QSS) have a few builtins that have no formal named extension.
These can be queried with eg the following:
if (cvar("pr_checkextension"))
if (checkextension("FTE_QC_CHECKCOMMAND"))
{
if (checkbuiltin(search_getfilemtime))
cancheckmodificationtimes = TRUE;
if (checkcommand("cef"))
canusewebbrowserplugin = TRUE;
}
Upcoming is a list of the engines with extensive extensions.
These files typically contain comments that describe the new stuff either on a
per-feature basis or per-extension basis.
If you don't understand the description of a feature for one engine then you may
find another engine describes it with greater clarity.