-
Notifications
You must be signed in to change notification settings - Fork 3
How QMM works
id Tech 3 games work by having a closed-source game engine (yes, most of these have since been released), generally with an open-source game logic DLL. This is how the modding community generally existed/exists for many of these games.
You can change the client and server game logic, compile the DLLs, put them inside a new folder like "mycoolmod", zip it up and release it. Other people can play it by putting the folder into their game directory (e.g. "C:\Program Files (x86)\Quake 3 Arena\mycoolmod") and load "mycoolmod" as the mod to play.
QMM works by sitting on the server (so no client downloads/changes needed), and it is designed to be loaded as the mod DLL, and then it loads the actual mod DLL provided by the mod author.
So every time the engine calls into the mod, it actually calls QMM. This call is then routed to all the plugins, so they can do stuff, then after every plugin is called, the original mod is called. Then, plugins are called again, with a "Post" version of the function. Finally, the execution path is returned to the engine.
Similarly, when the mod calls into the engine, it actually calls QMM. This is also routed to plugins first, then the engine, then back to plugins again before returning to the mod DLL.
There are 2 primary systems for mod/engine interaction:
- dllEntry / vmMain / syscall
- GetGameAPI / import+export
QMM was originally written when 1 (vmMain) was the only system I knew of for Quake 3 engine-based games. 2 (GetGameAPI) was originally designed for Quake 2 and games based on that engine (including Half-life), but is used by a significant number of Quake 3 engine-based games as well (primarily ones that weren't designed to be modded, such as most single players campaigns or those with scripting-based modding).
The vmMain system generally works as follows:
- The engine call functionality is handled by a function pointer (
syscall) which is given to the mod. The arguments to thissyscallfunction pointer are an intcmdwhich determines what particular engine function the mod wants to call, followed bycmd-specific arguments. - The
syscallpointer is passed to DLL mods as the sole argument to adllEntryfunction, which generally just stores this pointer in a global before returning. - For QVM (Quake Virtual Machine) mods, see the QVM page.
- After the engine calls
dllEntry, it then looks for a function in the DLL calledvmMainwhich is how the engine can tell the mod about certain events. Calls to this function act similarly tosyscall, with acmdargument followed bycmd-specific arguments. - The first
cmdsent to vmMain isGAME_INIT, which tells the mod to begin initialization. - The last
cmdsent to vmMain isGAME_SHUTDOWN, which tells the mod that the server is shutting down, or the map is ending, and it should unload resources.
QMM simply receives these function calls by either side and routes them to plugins before passing them on to the other side.
The GetGameAPI system generally works as follows:
- The engine call functionality is handled by a struct of function pointers (
import) which is given to the mod. Each function pointer in this struct is a separate engine function. - The
importstruct pointer is passed to mods as an argument to aGetGameAPIfunction, which generally just stores a copy of this struct (not just a copy of the pointer, as the import struct is usually a temporary in the engine). It then generates its own struct of function pointers (export) and variables to return back to the engine. If thisGetGameAPIfunction returns NULL, the engine will close with an error. - After the engine calls
GetGameAPI, it then begins calling functions insideexportdirectly. - The first
exportfunction called isInitorInitGame, which tells the mod to begin initialization. - The last
exportfunction called isShutdownorShutdownGame, which tells the mod that the server is shutting down, or the map is ending, and it should unload resources.
In order for the general QMM hooking flow (plugin, original, plugin post) to continue working without much changes, the game-specific handling for GetGameAPI engines has to do a bit more work.
Both a qmm_import and qmm_export struct are generated where each function pointer is a lambda which calls the main syscall or vmMain handler function with a custom enum for each engine that corresponds to the function call, similar to the existing G_x and GAME_x macros used by vmMain games. This way, plugins don't have to have two very different methods for handling calls, and most of the existing QMM code is used without much change.
Also, when plugins are loaded, they are given function pointers to the real syscall and vmMain functions, so that they can get information from the engine or mod. For GetGameAPI games, stub engine-specific syscall and vmMain functions are used which simply take the cmd they were called with and route to the corresponding function from the original import and export structs.
Finally, plugins are typically compiled with separate binaries for each supported game (unlike QMM's universal binary), so it is relatively simple to add support for GetGameAPI-based games.
Plugins are DLLs that are specially designed to be loaded by QMM as plugins.
Each plugin exposes functions called QMM_Query, QMM_Attach, QMM_Detach, QMM_vmMain, QMM_vmMain_Post, QMM_syscall, and QMM_syscall_Post.
- The first function called is
QMM_Query, where a plugin returns a struct of information (name, description, version, plugin interface version, etc) back to QMM, which decides to continue loading the plugin or not based on the interface version. - Then,
QMM_Attachis called where QMM provides various pointers:- Engine's
syscallpointer - Mod's
vmMainpointer -
presultpointer, which points to a variable where a plugin can set the "QMM result" (see QMM Result) - Pointer to a struct of function pointers, where each function is a "helper" function provided by QMM that plugins can use
- Pointer to a struct of variable pointers that plugins may use:
-
vmbase, which is the base address of the QVM memory block that should be added to pointers passed tosyscall(or returned fromvmMain) - this is 0 for DLL mods. The macrosGETPTR(ptr, type)andSETPTR(ptr, type)can help to work with these pointers. -
preturn, which is a pointer to an int that will contain the current value to be returned from the corresponding engine/mod call
-
- Engine's
-
QMM_Attachhas a bool return value. It should return 0 to indicate failure, and that QMM should cancel loading the plugin, or any other value to indicate success, and QMM will continue loading the plugin. - The final function called is
QMM_Detach. This will only be called ifQMM_Attachwas called, and simply allows a plugin a chance to perform some cleanup if necessary. - The remaining hook functions are called whenever there is communication between the engine and mod:
-
QMM_vmMain- called in each plugin BEFORE the mod'svmMainis called by the engine -
QMM_vmMain_Post- called in each plugin AFTER the mod'svmMainis called by the engine -
QMM_syscall- called in each plugin BEFORE the engine'ssyscallis called by the mod -
QMM_syscall_Post- called in each plugin AFTER the engine'ssyscallis called by the engine
-
In each of the hook functions, a plugin can choose to set the "QMM result" before returning. There are also some macros to facilitate this:
-
QMM_RET_ERROR(x)- sets the QMM result to QMM_ERROR and returnsx -
QMM_RET_IGNORED(x)- sets the QMM result to QMM_IGNORED and returnsx -
QMM_RET_OVERRIDE(x)- sets the QMM result to QMM_OVERRIDE and returnsx -
QMM_RET_SUPERCEDE(x)- sets the QMM result to QMM_SUPERCEDE and returnsx
The result values control how QMM handles the return value, and controls the function call flow:
- QMM_ERROR - This indicates that something bad happened. QMM will output an ERROR diagnostic, but will continue.
- QMM_IGNORED - This indicates that QMM should ignore the return value and provide no special handling. This is the usual return value for most calls.
- QMM_OVERRIDE - This indicates that QMM should store the return value and use it as the actual value to return to the original caller (engine or mod). Plugins loaded later have priority as to which return value is used if multiple plugins set QMM_OVERRIDE or QMM_SUPERCEDE.
- QMM_SUPERCEDE - This indicates that QMM should store the return value and use it as the actual value to return to the original caller (engine or mod). Also, the original function will NOT be called. Plugins loaded later have priority as to which return value is used if multiple plugins set QMM_OVERRIDE or QMM_SUPERCEDE, but if any plugin sets QMM_SUPERCEDE, the original function will not be called.
In _Post function hooks, the result value allows a plugin to still alter the value to be returned:
- QMM_OVERRIDE/QMM_SUPERCEDE - This indicates that QMM should store the return value and use it as the actual value to return to the original caller (engine or mod). Plugins loaded later have priority as to which return value is used if multiple plugins set QMM_OVERRIDE or QMM_SUPERCEDE.
There is no default result, and a plugin must set a result in a pre hook function before returning. _Post function hooks have no requirement to set a result before returning.
For more information on writing a plugin, see Writing a QMM plugin.
QMM is designed with a universal engine system. It can be loaded as-is into many different game engines, and it will generally be able to determine which game has loaded it, so that it can utilize the right messages and functions.
When QMM first loads, it is either through the dllEntry function or the GetGameAPI function. This already allows it to narrow down the list of possible engines that it has been loaded in. Then, it asks the OS to tell it the filename of itself, so it knows the DLL/SO filename that it loaded as. Then, it asks the OS to tell it the name of the original binary process that loaded it, which would be the game's dedicated server or the client exe.
Then, QMM loads the config file. This config file has a game option where the user can specify which game engine it is being used with, by utilizing a short code from the table below. The default value for this option is "auto", which will attempt to use auto-detection.
dllEntry-based games:
| Game | Short code | Windows DLL | Linux SO | QVM file | Exe hints |
|---|---|---|---|---|---|
| Call of Duty (Multiplayer) | CODMP | game_mp_x86.dll | game.mp.i386.so | "codmp", "cod_" | |
| Call of Duty: United Offensive (Multiplayer) | CODUOMP | uo_game_mp_x86.dll | game.mp.uo.i386.so | "coduo" | |
| Call of Duty v1.1 (Multiplayer) | COD11MP | game_mp_x86.dll | game.mp.i386.so | <none> | |
| Jedi Knight 2: Jedi Outcast (Multiplayer) | JK2MP | jk2mpgamex86.dll | jk2mpgamei386.so | vm/jk2mpgame.qvm | "jk2" |
| Jedi Knight: Jedi Academy (Multiplayer) | JAMP | jampgamex86.dll | jampgamei386.so | "ja" | |
| Quake 3 Arena | Q3A | qagamex86.dll | qagamei386.so | vm/qagame.qvm | "q3ded", "quake3" |
| Return to Castle Wolfenstein (Multiplayer) | RTCWMP | qagame_mp_x86.dll | qagame.mp.i386.so | vm/qagame.mp.qvm | "mp" |
| Return to Castle Wolfenstein (Singleplayer) | RTCWSP | qagamex86.dll / qagame_sp_x86.dll | qagamei386.so / qagame_sp_i386.so | "sp" | |
| Soldier of Fortune 2 (Multiplayer) | SOF2MP | sof2mp_gamex86.dll | sof2mp_gamei386.so | vm/sof2mp_game.qvm | "mp" |
| Star Trek Voyager: Elite Force (Holomatch) | STVOYHM | qagamex86.dll | qagamei386.so | vm/qagame.qvm | "stvoy" |
| Wolfenstein: Enemy Territory | WET | qagame_mp_x86.dll | qagame.mp.i386.so | "et" |
GetGameAPI-based games:
| Game | Short code | Windows DLL | Linux SO | Exe hints |
|---|---|---|---|---|
| Jedi Knight 2: Jedi Outcast (Singleplayer) | JK2SP | jk2gamex86.dll | jk2gamei386.so | "jk2" |
| Jedi Knight: Jedi Academy (Singleplayer) | JASP | jagamex86.dll | jagamei386.so | "ja" |
| Medal of Honor: Allied Assault | MOHAA | gamex86.dll | gamei386.so | "mohaa" |
| Medal of Honor: Spearhead | MOHSH | gamex86.dll | gamei386.so | "spear" |
| Medal of Honor: Breakthrough | MOHBT | gamex86.dll | gamei386.so | "break" |
| Quake 2 | QUAKE2 | gamex86.dll | gamei386.so | "q2", "quake2" |
| Quake 2: Remastered | Q2R | game_x64.dll | "quake2ex" | |
| SiN | SIN | gamex86.dll | gamei386.so | "sin" |
| Soldier of Fortune 2 (Singleplayer) | SOF2SP | gamex86.dll | "sof2" | |
| Star Trek: Elite Force II | STEF2 | gamex86.dll | gamei386.so | "ef" |
| Star Trek Voyager: Elite Force (Singleplayer) | STVOYSP | efgamex86.dll | efgamei386.so | "stvoy" |
If the user specifies an engine in the config file, QMM will go through the appropriate table (based on which function it initially loaded from), and compare the short codes until it finds a match. If a match is found, the engine detection is over. Keep in mind: if the user selects the wrong engine, the game will likely crash shortly, as QMM trusts that the config file was made correctly.
If the user does not specify an engine in the config file, QMM will begin the auto-detection by going through the appropriate table (based on which function it initially loaded from), and call an "auto-detect" function in each game's support file which compares the QMM DLL/SO name and the server binary filename and return true if it thinks that it is the current game.
If no auto-detect function returns true, then auto-detection fails, and QMM will signal the engine with G_ERROR to shut down the server.
Once the game engine is known, QMM will attempt load the mod file. The config file has a mod option where the user can specify the name of the mod DLL/SO/QVM file. The default value for this option is "auto", which will attempt to use auto-detection.
If the user specifies a mod filename with an absolute path, QMM will attempt to load that filename exactly, and fail if unable to. If the user specifies a mod filename with a relative path, QMM will attempt to load that filename at the following locations:
- [qmm_dir]/[filename]
- [exe_dir]/[mod_dir]/[filename]
If any of these are successful, the mod is loaded. Otherwise, QMM fails.
If the "auto" option is used, QMM will get the default mod DLL/SO and QVM names for the current game from the game engine tables (see previous section). It will then attempt to load the mod with the following list:
- [qvm filename]
- [qmm_dir]/qmm_[dll filename]
- [exe_dir]/[mod_dir]/qmm_[dll filename]
If any of these succeeds, the mod is loaded and QMM continues to load plugins.