Quake3 modding quickstart
NOTE: WORK IN PROGRESS!
Found some mistakes or are some instructions unclear? Please feel free to write
me to:
me <at> pythno <dot> org
Thank you!
Install ioquake3
We are going to use the ioquake3 sourceport. And we’re going to compile it
ourselfes. You will need the development libraries for SDL2. Consult your package
manager how to install them onto your system.
Clone and install ioquake3:
git clone git@github.com:ioquake/ioq3.git
cd ioq3
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
We make a Debug build so that we can step through the code.
Note
For a release build type Release instead of Debug.
Then, compile and link the programs:
Done.
You should have a ioquake3 binary in ./build/Debug/.
Notice the baseq3 folder. This folder contains all the
game-data (textures, models, …) of the Quake 3 Arena game.
If you make your stand-alone game some day you will have
another folder, such as myCoolGame where all of the
gamedata specific to your game go into.
To test our compiled binary out, we can use the Demo-version
of Quake 3 Arena, so you don’t have to buy it (althout it
doesn’t hurt to do so).
Here ist the data for the Quake 3 Arena demo: Demo.
Notice the fileending of pak0.pk3. This is just a regular zip file in
disguise. You can inspect its contents with any ZIP tool on your OS.
Copy the pak0.pk3 into you repos’ ./build/Debug/baseq3 directory.
Now, we also need some more pk3 files, namely, the ones that id-Software
published as patches. You can get them from: https://ioquake3.org/extras/patch-data/.
Copy the extracted pk3 files and also place them into your ./build/Debug/baseq3 directory.
Now try to launch ioquake3 from the command line. If everything went well you should now be
able to play the demo version of Quake 3 Arena. Take some time and shoot some bots. You deserved it.
Map Editors
Netradiant Custom
Netradiant Custom is a derivative version of id Softwares original mapping tool, called Q3radiant.
It runs on all OSes. I had trouble getting the precompiled binary to run on my linux box (it could not load
a shared lib). Thankfully, compiling it yourself is not a big deal!
I did find it a bit hard to get used to NetRadiant’s userinterface.
GitHub: https://github.com/Garux/netradiant-custom
IMPORTANT
Even if you don’t want to use Netradiant Custom
and decide to use TrenchBroom (see below) instead, you still
need some tools that come with Netradiant Custom but not with
TrenchBroom. Thus, downloading it is required.
For the following steps the program q3map2 is needed. Make
sure it is in your system-path so it is callable from anywhere
within your terminal!
TrenchBroom
Trenchbroom is a joy to use as its UI is so clean and most of the functions can be
figured out by just guessing. However, there is a very good documentation available:
https://trenchbroom.github.io/manual/latest/
You can download it from: https://trenchbroom.github.io/.
You will need to tell TrenchBroom where your gamedata lives.
For now, point it to the repo’s directory that has the
baseq3 directory in it, that is the Game Path:

TrenchBroom reads all assets from the pk3 files inside
baseq3. For now, ignore the other two paths you can set
(q3map2 and bspc).
If you navigate to the Face tab inside TrenchBroom you should
now see all the available textures inside the pk3 files that
lie in baseq3.
IMPORTANT
If you are using TrenchBroom on Wayland then start it with:
QT_QPA_PLATFORM=xcb ./trenchbroom
Compiling the Map
Create a new folder called pak10.pk3dir inside baseq3.
Inside pak10.pk3dir create a folder named maps. Save
your map from TrenchBroom or Netradiant Custom inside
baseq3/pak10.pk3dir/maps/ and make it your cwd in
your terminal. Type:
q3map2 -bsp <yourmapname>.map
q3map2 -vis <yourmapname>.bsp
q3map2 -light <yourmapname>.bsp
The first command compiles your map into BSP-tree. BSP
stands for Binary Space Partition. It is a nifty
datastructure that works really well for collision detection
and hidden-surface-removal determination of static geometry.
The second command calculates the PSV, which stands for
Potentially Visible Set. This is another optimization
pass that lets the engine discard all triangles that cannot
be seen from the current viewers camera position inside the
gameworld. Thus, those triangles never get passed to the renderer.
Quite frankly, if you just render Quake 3 Maps with relatively
new hardware (maybe 2014+) it probably is faster to just render
all of the world’s triangles every frame. GPUs have
just become so fast in rasterizing triangles that the cost
of figuring out if you can discard 50k triangles is higher
than just feeding them into your GPU and let it render them.
However, I do believe that the PSV is also used for spatial
Audio playback. But I am not sure yet, I will have to dig
into the code more to be sure.
The third command, finally, creates the lightmap. A lightmap
essentially is a texture that contains diffuse precomputed
illumination. It will get blended with the texture you
have assigned to geometry inside your Map-Editor.
Starting your Map
In order to start your map inside ioquake3 you go into your
build/debug/ folder and type:
./ioquake3 +devmap <yourmapname> +set sv_pure 0
+devmap tells the engine you load your map in debug-mode. That
means you can issue console commands inside the engine (more
on that later) to make yourself invincible, fly around, etc.
+set sv_pure 0 sets the in-engine variable sv_pure to
0 (=false). That means that the engine loads maps, texture, models, etc.
from within folders. They don’t have to be inside a pk3
archive. It would be pretty annoying if you had to first
pack everything before testing it, wouldn’t it?
NOTE
In order for the engine to use a folder as an
assets-resource that folder must have the extension .pk3dir!
+set sv_pure 0 is not enough!
When some “textures” just don’t work.
You might have assigned some textures to surfaces in your
map editor such as clang_floorshiny2. Then, after compiling your
map successfully (q3map2 threw new errors or warnings) that
texture you saw and assigned in the editor appears as a greyish
color with white stripes. That is the engine’s default-texture.
Essentially the texture that will be assigned when it cannot
find necessary data to texture a surface correctly:
The reason for this is that clang_floorshiny2 is not just
a simple texture. It is supposed to be a texture that
is tied to a shader-script. Shader-scripts are simple
textfiles that lie inside baseq3/scripts.
If we search for clang_floorshiny2 inside those files we can
see that it appears in base_floor.shader.
Let’s open it and look for that ominously clang_floorshiny2:
// The shader name. Used by the editor and the
// game engine.
textures/base_floor/clang_floorshiny2
{
// What the editor uses and reads. NOT used by
// anything else. This is handy and can be very
// powerful. You can use this to help with
// layout and texture alignment.
qer_editorimage textures/base_floor/clang_floor.tga
// What the game engine reads. These are rendering
// rules.
{
map $lightmap
tcGen environment
tcmod scale .25 .25
rgbgen identity
}
{
// This texture was never shipped with the game
// and is the reason for the default tetxure
// used in the engine.
map textures/base_floor/clang_floorshiny_alpha2.tga
blendFunc GL_ONE GL_SRC_ALPHA
alphagen wave triangle .98 .02 0 10
rgbGen identity
}
{
map $lightmap
rgbgen identity
blendFunc GL_DST_COLOR GL_ZERO
}
}
We can see that two texture-files are referenced: clang_floor.tga
and clang_floorshiny_alpha2.tga. Let’s search for both of them
inside the unpacked pk3 files.
Let’s look for clang_floor.tga:

What about clang_floorshiny_alpha2.tga?
Well, at least clang_floor exists as a JPG and not as a TGA.
That is fine though, the engine is able to handle multiple
image fileformats!
The problem starts (and ends) with the clang_floorshiny_alpha2
file. It just is nowhere to be found! Why is that? Apparently,
Quake III Arena has some leftover shader-scripts that were
not used in the final game. As such, the required textures
were stripped from the release. Precious CD-ROM space, ya know?
At least that is what is rumored.
I want to take the time and thank @Tig @themuffinator and
especially @xage from the Quake Map-Center Discord for taking
the time and helping me with how these things are tied together
and why some textures (or rather: shader-textures) won’t work
in-engine.
@Tig actually took the time to clean up those shader-files so that
you won’t see any shader-based textures in your editor that
will lead to a missing-texture surprise in ioQuake3:
https://github.com/Tigger-oN/lvl-q3a-clean-shaders/. Thanks a lot!
Debugging the code with Visual Studio Code
It is straightforward to setup VSCode for debugging ioQuake3.
Install the CMake Tools from Microsoft:

NOTE
If you have created a build directory already like in the beginning of the
article, I recommend to delete the CMakeCache.txt inside it as it
might confuse VSCode when configuring the project.
If you now open ioQuake3’s root folder in VSCode, CMake should automatically run
and configure the project, that is, a Makefile on UNIX by default.

Now, have a look on the left toolbar and click onto CMake.

You can select the build type (Debug, Release, etc.) and your toolchain (clang, gcc, …)
which will reconfigure the project. Then, when you’re happy with the settings click the build
icon next to the Dropdown menu that says Build. Building the first time might take
a while. Further build will be significantly faster, though as only the compilation units
that you change will need a recompilation. Linking is generally fast for ioQuake3. Now,
start the game by clicking on the play button, either down at the menu-bar or next to
Launch:

Of course, if you made a Debug-build you can step through the code and learn a lot
about the engine internals :) Isn’t that great? For example open the file:
and find the int main( int argc, char **argv) function, which is the entrypoint of the engine.
Create a breakpoint somewhere in that function by clicking left to the line-number you want
the debugger to halt the program and start by clicking the play button with the little bug:

If you get an error message that the assets are missing then you need to copy the PK3 files again
into the build/Debug/ directory. This might be the case if you deleted the build/Debug folder
as I recommended above. In this case just copy all the PK3 files into baseq3 and you’re good
to go! In my case, the full path is:
/Users/me/repos/ioq3/build/Debug/baseq3
Shared libraries vs. QVM
If you set a breakpoint in eg. g_missile.c inside the fire_rocket function
start a game, pick up the rocket launcher (the map Q3DM1 has a rocket launcher
right in front of your nose when you start it up), and then shoot a rocket
you will notice that the beakpoint is not being hit. This is because,
by default, the engine starts using the QVM version of the gamecode.
QVM stands for Quake Virtual Machine. This is the scripting system
in idTech3. But wait? g_missile.c is a regular C sourcefile, isn’t it?
Correct! The scripting language is regular C-Code. That C-Code is compiled
to QVM Bytecode. Check out the directory: build/Debug/baseq3/vm/. Inside it
you will find 3 files, namely: cgame.qvm, qagame.qvm and ui.qvm. Your game-code
lives inside those files that contain the bytecode. At runtime, this bytecode
is interpreted by the Virtual Machine inside the engine. Conveniently, this bytecode
is platform independent. Any platform that implements the Quake Virtual Machine can
run that code. That means, if you would ship a mod that doesn’t touch the engine’s
internals you just have to hand out those QVM files (along with the assets, of course)
to your players. I don’t know exactly why John Carmack decided to go that route,
using C as the scripting language, but it has a significant advantage over another
language: You can decide not to compile your C-code to QVM bytecode and instead
compile it to native machine-code: Look at the directory: build/Debug/baseq3.
You’re going to find the files: cgame.so/DLL, qagame.so/DLL and ui.so/DLL. They
look very similar to what we’ve seen inside the vm/ subfolder, don’t they?
Well, that is because they are the machine-code native versions of those QVM files.
You now can decide to load the .so/.DLL files when starting up the engine and use your
regular Debugging tools that you would use when debugging the rest of the
codebase. No extra tooling needed to debug your game-code. Genius!
Starting the engine with machine-native libraries
Pass the following parameters to the ioquake3 executable in order
to use machine-native code instead of the QVM-Bytecode:
./ioquake3 +set vm_cgame 0 +set vm_game 0 +set vm_ui 0
That’s it! Now, in order to make use of this inside VSCode, we
have to tell it. You will need a launch.json file for that.
Here is how to set it up:
- From the sidebar, select Run and Debug
- Click Create a launch.json file
- Select C/C++ (gdb) Lauch

A launch.json template file will be created. Change the program and args entries to:
...
"program": "${workspaceFolder}/build/Debug/ioquake3,
"args": ["+set", "vm_cgame", "0", "+set", "vm_game", "0", "+set", "vm_ui", "0", "+set", "sv_pure", "0"],
...
program tells VSCodw what to start. In that case it should run the engine ioquake3*.
As discussed above we also need to pass some arguments to ioquake3 in order
to run it using the shared machine-native libraries instead of the QVM-bytecode so that
we can debug it. That is achieved by passing the arguments as an array to args. And be careful:
You have to pass each string of text delimited by a space as its own element to the
array!
As you can have many ways of launching the engine by adding more configurations
to launch.json I also give it distinctive names that tell me exactly what they do:
"name": "Debug ioquake3 (gdb, shared libs)",
The name will appear in the dropdown menu:

I added another config that lets me launch the game using the QVM-bytecode. Of course,
debugging, won’t be possible then, but it doesn’t hurt to test running your game
using the QVM-files if you intend to ship those.
For completeness, this is what my launch.json looks like:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug ioquake3 (gdb, shared libs)",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/Debug/ioquake3",
"args": ["+set", "vm_cgame", "0", "+set", "vm_game", "0", "+set", "vm_ui", "0", "+set", "sv_pure", "0"],
"stopAtEntry": false,
"cwd": "${fileDirname}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "Set Disassembly Flavor to Intel",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
}
]
},
{
"name": "Debug ioquake3 (gdb, QVM)",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/Debug/ioquake3",
"args": ["+set", "vm_cgame", "2", "+set", "vm_game", "2", "+set", "vm_ui", "2", "+set", "sv_pure", "0"],
"stopAtEntry": false,
"cwd": "${fileDirname}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "Set Disassembly Flavor to Intel",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
}
]
},
]
}