Skip to content

How QMM works

Kevin Masterson edited this page Apr 15, 2025 · 19 revisions

Background

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

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:

  1. vmMain / syscall
  2. 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).

vmMain / syscall

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 this syscall are an int cmd which determines what particular engine function the mod wants to call, followed by cmd-specific arguments.
  • The syscall pointer is passed to DLL mods as the sole argument to a dllEntry 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 called vmMain. This acts similarly to syscall, with a cmd argument followed by cmd-specific arguments.
  • The first cmd sent to vmMain is GAME_INIT, which tells the mod to begin initialization.
  • The last cmd sent to vmMain is GAME_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.

GetGameAPI / import+export

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 a GetGameAPI 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 this GetGameAPI function returns NULL, the engine will close with an error.
  • After the engine calls GetGameAPI, it then begins calling functions inside export directly.
  • The first export called is Init, which tells the mod to begin initialization.
  • The last export called is Shutdown, 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

Plugins are DLLs that are specially designed to be loaded by QMM as plugins.

Overview

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 to syscall (or returned from vmMain) - this is 0 for DLL mods. The macros GETPTR(ptr, type) and SETPTR(ptr, type) can help to work with these pointers.
  • 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 if QMM_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's vmMain is called by the engine
    • QMM_vmMain_Post - called in each plugin AFTER the mod's vmMain is called by the engine
    • QMM_syscall - called in each plugin BEFORE the engine's syscall is called by the mod
    • QMM_syscall_Post - called in each plugin AFTER the engine's syscall is called by the engine

QMM Result

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 returns x
  • QMM_RET_IGNORED(x) - sets the QMM result to QMM_IGNORED and returns x
  • QMM_RET_OVERRIDE(x) - sets the QMM result to QMM_OVERRIDE and returns x
  • QMM_RET_SUPERCEDE(x) - sets the QMM result to QMM_SUPERCEDE and returns x

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.

Engine Auto-detection

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.

Mod Auto-detection

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.

Clone this wiki locally