Skip to content

Adding game support

Kevin Masterson edited this page Aug 11, 2026 · 14 revisions

Each game that QMM supports (this document will use the placeholder short-code "XYZ") gets a src/game_XYZ.cpp file and a include/game_XYZ.h file. These are the only files in QMM that will include XYZ's SDK, and nothing else in QMM (only plugins) should include game_XYZ.h.

The game_XYZ.h file should define any engine (e.g. G_PRINT) and mod (e.g. GAME_INIT) constants (if they are not defined by the SDK, in particular those that QMM uses for GetGameAPI games), including any additional ones handled by QMM as polyfills.

game_XYZ.cpp

The game_XYZ.cpp file defines a game support class that inherits from the pure virtual GameSupport base class. This game support class must implement several public members functions.

Then, the file must contain an instantiation of the game support class, and store the address in an externally-accessible GameSupport* that must then be referenced in the api_supportedgames list in game_api.cpp.

XYZ_GameSupport:

struct JK2MP_GameSupport : public GameSupport {
	virtual const char* EngMsgName(intptr_t msg);
	virtual const char* ModMsgName(intptr_t msg);
	virtual bool AutoDetect(APIType engine_api);
	virtual void* Entry(void* syscall, void*, APIType engine_api);
	virtual bool ModLoad(void* entry, APIType mod_api);
	virtual void ModUnload();
	virtual int QMMEngMsg(int msg) { return qmm_eng_msgs[msg]; }
	virtual int QMMModMsg(int msg) { return qmm_mod_msgs[msg]; }

	virtual intptr_t syscall_args(intptr_t, intptr_t* args);
	virtual intptr_t vmMain_args(intptr_t, intptr_t* args);

	virtual const char* DefaultDLLName() { return "jk2mpgame" MOD_DLL; }
	virtual const char* DefaultQVMName() { return "vm/jk2mpgame.qvm"; }
	virtual const char* DefaultModDir() { return "base"; }
	virtual const char* ModCvar() { return "fs_game"; }
	virtual const char* GameName() { return "Jedi Knight 2: Jedi Outcast (MP)"; }
	virtual const char* GameCode() { return "JK2MP"; }

	virtual int QVMSyscall(uint8_t* membase, int cmd, int* args);

private:
	const int qmm_eng_msgs[QMM_ENGINE_MSG_COUNT] = GEN_GAME_QMM_ENG_MSGS();
	const int qmm_mod_msgs[QMM_MOD_MSG_COUNT] = GEN_GAME_QMM_MOD_MSGS();
};
All games

All games should implement:

  • const char* EngMsgName(intptr_t msg)
    This is called with an engine message constant (e.g. G_PRINT) and you should return the string form of it. The GEN_CASE macro is useful in this function.

  • const char* ModMsgName(intptr_t msg)
    This is called with a mod message constsant (e.g. GAME_INIT) and you should return the string form of it. The GEN_CASE macro is useful in this function.

  • bool AutoDetect(APIType engine_api)
    This is called by QMM for each game when trying to auto-detect which engine QMM is loaded in. The engine_api argument is a value that says what engine type QMM was loaded as (currently QMM_API_DLLENTRY, QMM_API_GETGAMEAPI, or QMM_API_GETMODULEAPI).

    Return true if the game engine has been determined to be XYZ, false otherwise.

    This function is also called after the XYZ game is loaded from being manually set in the "game" config option.

  • void* Entry(void* syscall, void*, APIType engine_api)
    This is called by QMM's dllEntry, GetGameAPI, and GetModuleAPI entry points once the game engine is determined.

    This function allows you to store the real engine syscall pointer or import table to forward calls to it in XYZ_syscall.

    engine_api is an enum which corresponds to the API that the engine used to load QMM. This can be QMM_API_DLLENTRY, QMM_API_GETGAMEAPI or QMM_API_GETMODULEAPI.

    If engine_api is QMM_API_DLLENTRY, then arg0 is the syscall pointer passed from the engine to QMM's dllEntry function, while arg1 is null.

    If engine_api is QMM_API_GETGAMEAPI, then arg0 and arg1 are the values passed to QMM's GetGameAPI (or GetModuleAPI) function. Typically, that means arg0 is the engine's import struct pointer, while arg1 is undefined. In SOF2SP, arg0 will be an apiversion int while arg1 is the import pointer.

    If engine_api is QMM_API_GETMODULEAPI, then arg0 is an apiversion int and arg1 is the engine's import struct pointer.

  • bool ModLoad(void* entry, APIType mod_api)
    This is called just after a (potential) mod file is loaded.

    This function allows you to store the real vmMain pointer or real game export table to forward calls to the mod in XYZ_vmMain.

    entry is a pointer to the mod's entry function, depending on the API type which is given by the mod_api parameter.

    mod_api is an enum which corresponds to the API that QMM used to load the mod file. This can be QMM_API_QVM, QMM_API_DLLENTRY, QMM_API_GETGAMEAPI, or QMM_API_GETMODULEAPI.

    If mod_api is QMM_API_QVM, then entry is a stub vmMain-like function pointer that calls into the QVM.

    If mod_api is QMM_API_DLLENTRY, then entry is the vmMain function pointer from the mod DLL.

    If mod_api is QMM_API_GETGAMEAPI, then entry is the GetGameAPI function pointer from the mod DLL.

    If mod_api is QMM_API_GETMODULEAPI, then entry is the GetModuleAPI function pointer from the mod DLL.

    Return true to consider the mod load successful, otherwise return false to continue with the next step of the mod loading sequence.

  • void XYZ_ModUnload()
    This is called just before the mod file is unloaded.

    This function allows you to clear the stored mod entry pointer from XYZ_ModLoad to safely check before calling into an unloaded mod, and to clean up any resources if applicable.

  • int QMMEngMsg(int msg)
    This is called to get the game-specific value for an engine message that QMM uses internally.

    Typically, the implementation should just be: return qmm_eng_msgs[msg];

  • int QMMModMsg(int msg)
    This is called to get the game-specific value for a mod message that QMM uses internally.

    Typically, the implementation should just be: return qmm_mod_msgs[msg];

  • intptr_t XYZ_syscall_args(intptr_t cmd, intptr_t* args)
    This is called by QMM and by plugins in order to pass calls to the engine.

    This is where plugin polyfills should be handled, if needed.

    This should directly call into the engine.

  • intptr_t XYZ_vmMain_args(intptr_t cmd, intptr_t* args)
    This is called by QMM and by plugins in order to pass calls to the mod.

    This should directly call into the mod.

  • const char* DefaultDLLName()
    This function should return the mod DLL name expected by the engine.

    Several macros are available to handle different names in Windows vs Linux:

    • MOD_DLL
      This contains the DLL suffix and extension used by most games based on architecture and operating system. This can include "x86.dll", "i386.so", "x86_64.dll", or "x86_64.so".

    • X64_DLL
      This contains the DLL suffix and extension used by some games based on architecture and operating system. This can include "x86.dll", "i386.so", "x64.dll", or "x86_64.so".

    • SP_DLL
      This contains a substring found in many single-player game DLL names based on operating system. This can include "_sp_" or ".sp.".

    • MP_DLL
      This contains a substring found in many multi-player game DLL names based on operating system. This can include "_mp_" or ".mp.".

  • const char* DefaultModDir()
    This function should return the default mod directory used by the game (e.g. "baseq3", "main", etc).

  • const char* ModCvar() This function should return the cvar name the engine uses to set the mod.

    The base class' implementation of this function returns "fs_game" which is used by most Quake 3-based engines. You only need to include an implementation for this function if the game uses a different cvar.

  • const char* GameName()
    This function should return the full name of the game.

  • const char* GameCode()
    This function should return the QMM short code of the game (e.g. "XYZ").

  • const int qmm_eng_msgs[QMM_ENGINE_MSG_COUNT]
    This array stores the game-specific values for engine messages used internally by QMM. This array is referenced by the QMMEngMsg member function.

    You can initialize this array with the GEN_GAME_QMM_ENG_MSGS() macro to keep it aligned with the values that QMM expects.

  • const int qmm_mod_msgs[QMM_MOD_MSG_COUNT]
    This array stores the game-specific values for mod messages used internally by QMM. This array is referenced by the QMMModMsg member function.

    You can initialize this array with the GEN_GAME_QMM_MOD_MSGS() macro to keep it aligned with the values that QMM expects.

QVM-supporting games only:

QVM games need to implement:

  • const char* DefaultQVMName()
    This function should return the QVM file path expected by the engine.

    The base class' implementation of this function returns nullptr, which signals to QMM that this game does not support QVMs.

  • int QVMSyscall(uint8_t* membase, int cmd, int* args)
    This is called by qvm_exec whenever the QVM calls an engine syscall/trap.

    All pointer arguments that get read by the engine (and QMM+plugins) need to be converted to real pointers, which is simply adding the membase argument if the pointer is not null.

    The VMARG and VMPTR macros are useful in this function. This function should adjust pointers based on the syscall and then pass to qmm_syscall to be handled like any other call out of the mod.

To create an object of type XYZ_GameSupport and export it, you can use the following macro:

GEN_GAME_OBJ(XYZ);
Already-defined functions:

These functions are defined inline, and you do not need to implement your own:

  • intptr_t XYZ_syscall(intptr_t cmd, ...)
    This forwards arguments to XYZ_syscall_args.

  • intptr_t XYZ_vmMain_args(intptr_t cmd, intptr_t* args)
    This forwards arguments to XYZ_vmMain_args.

game_api.cpp

You also need to add some lines in game_api.cpp:

  • GEN_GAME_EXTS(XYZ);
    This macro forward-declares the GameSupport* pointer for the game's support object for use in the api_supportedgames list.

  • GET_GAME_OBJ(XYZ),
    You need to include the game's GameSupport* pointer in the api_supportedgames list to allow it to be used by QMM. You can use this macro inside the api_supportedgames list.

Clone this wiki locally