ezQuake Manual: Code structure

Introduction

This starts as a pure brainstorm without any future plans on how to extend it.
I've always wanted to have some guide for new developers who have learned C language but need to understand QW clients code without needing to read tons of undocumented code trying to understand it.
However I never knew with what to start, where to publish it, what structure to choose, who else invite to help me with that. So I've decided to do at least this and decide later how to extend it.

Definitions and declarations

You must perfectly know what's the difference between these two.

Definition of a function contains function name, arguments, return type and function body.
Example:

int main(int argc, char** argv) {
    printf("Hello!\n");
    return 0;
}

Definition of a variable contains it's name and type, optionally initialization value.
Example:

double speed;
cvar_t scr_gameclock = {"cl_gameclock", "0"};

Definition of a type contains the keyword "typedef", name of the new type and the description of the defined type
Example:

typedef unsigned long steps;
typedef struct window_s {
  int x, y, width, height;
} window_t;

Definition of a macro starts with #define preprocessor instruction, macro name, optionally it's arguments and a body of the macro.
Example:

#define MY_PI (3.142)
#define EQUAL_STRINGS(a,b) (strcmp((a),(b)) == 0)

Declaration of a function contains function name, arguments type, return type, optionally also arguments names.
Example:

int main(int, char**);

Declaration of a variable contains the keyword "extern", variable type and varialbe name.
Example:

extern double speed;
extern cvar_t scr_gameclock;

There aren't any "declarations of types" or "declarations of macros". Declaration basically doesn't allocate any space for variable or code in memory, while definition does do that. Also before you can use a variable or a function in your code it has to be either defined or declared somewhere above in the same source file.

Modularization, Encapsulation

The alpha and omega of maintaining a big project. You need to understand:

With module we mean single *.c file, header is it's *.h file.

The only tool for encapsulation or modularization of code in C language are modules and headers. A module should be your "black box", where things work together. The rest of the world can only see things that you have declared in the header file associated with given module.

That means, in module you should put:

In header files you should put:

It is a very good practice to use keyword "static" for private entries. All functions and variables that you don't want to be used outside of your module should start with this keyword.
Example:

static int error_counter;

static void My_Error(const char* err)
{
    error_counter++;
    printf("Error: %s\n", err);
}

int GetErrorsCount(void)
{
    return error_counter;
}

Headers should be used as an interface to your modules. It should tell the world how it can communicate with your "black box", represented by your *.c file.

Breaching the rules

There are situations where you need to use some variable or function from other module, but it's not declared in it's header file and adding it there is not possible for some reasons.

You should always think twice in such situation. No, you should think THRICE in such situation :-)
And if even after that you are sure that you will not add the declaration of that function you need into the appropriate header file, you can use function declaration or variable declaration of function/variable from other module in your own module.
Example:
There is a "int smallvar;" in module "x.c" and you would like to use it in "y.c" module.
However adding a declaration "extern int smallvar;" in "x.h" is not possible at the moment (In 95% you are just lazy to do that. In such case, slap yourself and add that declaration in there!)
If your reasons are really valid, you will add "extern int smallvar;" into your module. If you can, put it inside of the function where you need to use it, not outside of it.

Headers and Modules in ezQuake

Usually together with one module (*.c file) there is also a header file (*.h) with the same name. For example cl_cam.c and cl_cam.h.

Sometimes one header file is an interface for more then one module. For example sys.h, sys_win.c, sys_linux.c and sys_mac.c. These modules implement functions and contain variables declared in sys.h. Same situation is for OpenGL and non-OpenGL drawing. The header draw.h contains declarations of functions that are defined (implemented) differently in OpenGL and non-OpenGL version of the client in different modules.

Also cl_screen.c and sbar.c have declarations of their functions and variables in one common screen.h.
But there is also sbar.h, which contains only declarations of things from sbar.c.

Usually one module has one header file (1:1). In situations where your module serves more then one purpose in the whole project, it might be good to create more headers for it (1:N). Also one header can stand for more modules, especially in situations where one big functionality has been separated into these modules and one header file sums it up (N:1).

Most important headers in ezQuake

As you should understand now, header files are the key to understand the global structure of the code.
If you need to know how things work inside, you should read the contents of *.c modules. But now we will describe most important headers of the ezQuake project.

Main headers

There are three main headers in the project. They are sorted - if you include the top header (quakedef.h) you have also included common.h. Also by including common.h in your module, you automatically include q_shared.h. When writing a new module, you should make a wise choice which header to include. Hardcore Quake-engine related client modules usually include the top one (quakedef.h), modules that apply both for client and server include common.h. Modules that do not depend on other Quake engine functions and only add some independent extra functionality might only q_shared.h.

quakedef.h
Header that includes all important project header for you. It's good to include this header if you are going to work on something that works with various modules from different areas and also draws something on screen.
Besides common.h and q_shared.h described below it also includes for you following headers: vid.h, screen.h, render.h, draw.h, console.h, cl_view.h, client.h
You can guess the meaning of those headers by their name or by opening them and looking on the set of function they offer. All of them have something to do with what you can see in the game.

common.h
This header also includes most important Quake headers for you and also contains definitions of some very common and basic Quake-related functions. This header however doesn't contain anything related to screen drawing or rendering.
Besides q_shared.h described below it also includes for you the following headers: zone.h, cvar.h (includes cvar_groups.h), cmd.h, net.h, protocol.h, cmodel.h (includes bspfile.h)
All these headers have something to do with the very internal parts of the Quake engine.

q_shared.h
The "q" at the beginning of the name of this file might be a big confusing. This header is on the lowest level of the hierarchy of the three big headers described in here. It does contain code not related to other Quake code part, usually functions that you could successfully use in other non-Quake projects.
It includes following headers for you: math.h, string.h, stdarg.h, crtdefs.h, stdio.h, stdlib.h, ctype.h, assert.h, mathlib.h (includes asmlib.h), sys.h, localtime.h (incluides time.h)
Almost all these headers are C standard library headers.

Other usefull headers

If you want to use functionality declared in following headers, you need to include them, they are not included in the main three headers described above.
fs.h - File System functions
image.h - Images handling functions
utils.h - Other usefull functions, sometimes related to other Quake code, sometimes independent on other Quake features, e.g. string handling functions, regexp stuff, player numbering, TF coloring
Ctrl.h - Common GUI drawing functions
common_draw.h - Drawing functions common for both OpenGL and non-OpenGL version
ez_controls.h - New GUI drawing framework, not used yet
parser.h - for evaluating (math) expressions
teamplay.h - teamplay and scripting related structures and functions
server.h - server structures and functions

Project configurations

This part applies for Windows version of the client. With some minor differences it applies also for other platforms too though.
Client can be compiled in Debug and Release version and in OpenGL and non-OpenGL version - that gives 4 possible target versions of the binary:

Binary filename Configuration name Release / Debug OpenGL
ezquake-debug.exe Debug Debug no
ezquake.exe Release Release no
ezquake-gl-debug.exe GLDebug Debug yes
ezquake-gl.exe GLRelease Release yes

Debug version is used only by developers while programming and fixing bugs. It can print additional stuff into the console, also the result binary is not optimized (gives lower performance) and allows the developer to watch how the code is being executed line by line.
OpenGL version uses 3D graphics accelerators functions, non-OpenGL versions are intended for users with missing graphics acceleration cards. Modern games do not contain such functionality, but some players are still using this and we do not want to lose them.

Using the Preprocessor

OpenGL and non-OpenGL code

When writing code that is only capable of running in OpenGL version of the client, you need to check if the module (*.c) in which you are adding the code is included also in the Software (*Release) version of the project. For example all gl_*.c files are not part of the non-OpenGL version of the project and therefore following lines do not apply to them.

But if you are editing a module that will be compiled for all versions of the client, you need to use preprocessor instructions to separate parts of the code for OpenGL and non-OpenGL version.
Example: This function will use different parts of code in OpenGL and non-OpenGL versions of the client.

void Draw_MyExplosion(vec3_t position)
{
#ifdef GLQUAKE
    // following code will be used for OpenGL version of the client
    // ...
#else
    // following code will be used for non-OpenGL version of the client
    // ...
#endif
}

What does the code mean: for OpenGL configurations there is a preprocessor constant "GLQUAKE" defined in the project properties and the preprocessor will remove all parts of code marked with "#ifdef GLQUAKE" and keep all parts of code marked with "#ifndef GLQUAKE" for non-OpenGL configuration and vice versa.

Debug and Release code

As stated earlier, Debug version of the client is intended mainly for developers to help programming new features and track down bugs. Debug configurations have preprocessor constant "_DEBUG" defined. This is an example of a function that will print error output only in the Debug version of the client:

double My_Division(int a, int b)
{
    if (b)
        return ((double) a) / ((double) b);
    else
#ifdef _DEBUG
        Com_Printf("An error occured in My_Division: division by zero!\n");
#endif
        return 0;
}

Headers locking

This is a well known preprocessor trick. If you have a "module.h" file and place following lines on the beginning and the end of such file, you will avoid compilation problems when someone accidentaly includes your module.h file more than once.

#ifndef __MODULE_H__
#define __MODULE_H__
...
...
#endif

Quake variables

Here you will learn how variables that you can type into the console are represented in the source code. Each Quake variable is represented by it's corresponding variable in the source code, that means there is a piece of memory allocated for each such variable. The type of that variable is "cvar_t", it is a structure with lots of entries. The most important ones are: name, default value, flags, onchange function, current float value, current string value, current integer value.
All variables are allocated statically, that means there is a definition for every C variable representing associated the Quake variable.

Defining variables

When defining the variable, you always also specify it's name and default value, sometimes also flags and the onchange function.
Example:

cvar_t scr_gameclock = {"cl_gameclock", "0"};
qbool OnChange_r_fullbrightSkins (cvar_t *var, char *value);
cvar_t    r_fullbrightSkins = {"r_fullbrightSkins", "1", 0, OnChange_r_fullbrightSkins};

In this example we first define scr_gameclock variable as a C variable, which will represent cl_gameclock as a Quake variable. That means to access cl_gameclock you will always use "scr_gameclock" in the Quake code. Luckily for most variables it's Quake and C name are the same, this was just an example to denote that it is not necessary to be the same. The default value of the value will be set to "0".
On the second line there is a declaration of OnChange_r_fullbrightSkins function which takes two parameters - pointer to a variable and a string and returns true/false value.
On the third line we define r_fullbrightskins variable with default value set to "1". Flags attribute is set to 0 and finally the onchange function is set to the function declared at the previous line.

Onchange function

If a variable has onchange function set, that function will be called when something changes the value of the variable (either config gets executed, user types new value in the console, some script changes it, ...). The function will receive the new value of the variable in the second argument. It is up to the function to allow or disallow the change of the variable. If the function decides to disallow the new value, it should return true, otherwise return false.

Registering variables

After you define the variable as a piece of memory, you also need to tell the engine at some place that this is a Quake variable, otherwise it would just be a piece of memory and the engine would have no idea it should e.g. include it in it's variable lists. To register the variable you need to call Cvar_Register() function.
Example:

Cvar_SetCurrentGroup(CVAR_GROUP_SCREEN);
Cvar_Register (&v_gamma);
Cvar_Register (&v_contrast);
Cvar_ResetCurrentGroup();

Such pieces of code can usually be found in *_Init() function of different client modules. In this piece of code we have first told the engine that the next registered variables should be added to the "Screen Settings" group. On line 2 and 3 we have registered variables v_gamma and v_contrast. The last line is optional and it sets current variable group to none making it impossible to register other variables until other Cvar_SetCurrentGroup is called.
Now the variables are added to internal variables list, that means you can e.g. tab-complete them when writing in the console, you can view their default value, etc.

Accessing variable value

Variables do not have types - technically. The are all three values of a variable stored in the memory: string, integer and float value. They are always "equal", if you set the variable value to "2.0", string value will be "2.0", float value will be 2.00000.., integer value will be 2. Following piece of code demonstrates how to access all three values of the same variable:

char* a = scr_gameclock.string;
int b = scr_gameclock.integer;
float c = scr_gameclock.value;

Quake commands

If you've understand how variables are done in Quake, commands will be very easy to understand. Command is only being registered by a Cmd_AddCommand() call.
Example:

void V_BonusFlash_f (void) { ... }
Cmd_AddCommand ("bf", V_BonusFlash_f);

The first argument is the name of the command, the second argument is the function associated to this command. The function should have zero arguments and not return any value (both represented by keyword "void"). In the function itself you can work with arguments given to the command. You can check how many arguments have been passed by calling Cmd_Argc() function and access each of then with Cmd_Argv(i) function. Cmd_Argv(0) always returns the name of the command, Cmd_Argv(1) returns first argument, etc.
Example:
bf hello "my darling"
Cmd_Argc() will return 2
Cmd_Argv(0) will return "bf"
Cmd_Argv(1) will return "hello"
Cmd_Argv(2) will return "my darling"

Last update: 20.10.2010 13:54 UTC
ezQDocs

SourceForge.net Logo