-
Notifications
You must be signed in to change notification settings - Fork 1
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:
- 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 thissyscall
are an intcmd
which determines what particular engine function the mod wants to call, followed bycmd
-specific arguments. - The
syscall
pointer is passed to DLL mods as the sole argument to adllEntry
function, 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 calledvmMain
. This acts similarly tosyscall
, with acmd
argument followed bycmd
-specific arguments. - The first
cmd
sent to vmMain isGAME_INIT
, which tells the mod to begin initialization. - The last
cmd
sent 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
import
struct pointer is passed to mods as the sole argument to aGetGameAPI
function, which generally just stores a copy of this struct, and then generates its own struct of function pointers (export
) to return back to the engine. If thisGetGameAPI
function returns NULL, the engine will close with an error. - After the engine calls
GetGameAPI
, it then begins calling functions insideexport
directly. - The first
export
called isInit
, which tells the mod to begin initialization. - The last
export
called isShutdown
, 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 an import
and export
struct are generated where each function pointer is a lambda which calls the main syscall
or vmMain
function with a custom enum for each game 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 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_Attach
is called where QMM provides various pointers:- Engine's
syscall
pointer - Mod's
vmMain
pointer -
presult
pointer, 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
-
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.
- Engine's
-
QMM_Attach
has 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_Attach
was 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'svmMain
is called by the engine -
QMM_vmMain_Post
- called in each plugin AFTER the mod'svmMain
is called by the engine -
QMM_syscall
- called in each plugin BEFORE the engine'ssyscall
is called by the mod -
QMM_syscall_Post
- called in each plugin AFTER the engine'ssyscall
is called by the engine
-
In each of the non-_Post
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.
There is no default result, and a plugin must set a result before returning.
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 |
---|---|---|---|---|---|
Return to Castle Wolfenstein (Singleplayer) | RTCWSP | qagamex86.dll | qagamei386.so | "sp" | |
Star Trek Voyager: Elite Force (Holomatch) | STVOYHM | qagamex86.dll | qagamei386.so | vm/qagame.qvm | "stvoy" |
Quake 3 Arena | Q3A | qagamex86.dll | qagamei386.so | vm/qagame.qvm | "q3ded", "quake3" |
Wolfenstein: Enemy Territory | WET | qagame_mp_x86.dll | qagame.mp.i386.so | "et" | |
Return to Castle Wolfenstein (Multiplayer) | RTCWMP | qagame_mp_x86.dll | qagame.mp.i386.so | "mp" | |
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" |
GetGameAPI
-based games:
Game | Short code | Windows DLL | Linux SO | Exe hints |
---|---|---|---|---|
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" |
Star Trek: Elite Force II | STEF2 | gamex86.dll | gamei386.so | "ef" |
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 compare the DLL/SO name until it finds a match. If a match is not found, auto-detection has failed and it's likely that the game is not supported. If a match is found, QMM then begins looking through the "exe hints" for the matching game. If there are no hints, game detection is completed and this engine is chosen. If there are hints, the binary filename is checked for each hint for a match. If a match is found, game detection is completed and this engine is chosen. If there are no matches, auto-detection continues with the next game engine in the table, hopefully settling on another game engine that matches.
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:
- [filename]
- [qmm_dir]/[filename]
- [exe_dir]/[mod_dir]/[filename]
- ./[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]
- [exe_dir]/[mod_dir]/[dll filename]
- ./[mod_dir]/qmm_[dll filename]
If any of these succeeds, the mod is loaded and QMM continues to load plugins.