Hooking
Detouring engine and game functions with KHook and Source2Mod's KHOOK_* macros — how a hooked call runs and which macro to pick.
Source2Mod hooks with KHook — Metamod:Source's own detour library, on
the one engine Metamod runs for the whole server. If you have written a
Metamod 2.0 plugin, you already know the API. The SDK wraps it in a set of
KHOOK_* macros so a hook is one line in your class, and Source2Mod does the
resolving, attaching and tearing down for you.
This section goes from the ground up. This page covers what KHook is, how a hooked call runs and which macro fits which target; the subpages go through each part in detail.
Game hooks
Virtual hooks
Function hooks
Writing handlers
Addresses
Gamedata
Raw KHook
KHook in one minute
A detour library lets you run your own code before and after a function of the game or the engine — or instead of it. Metamod ships one and hands it to every plugin, so that all plugins on a server patch functions through the same backend. Two libraries patching the same function independently cannot see each other, and that ends in crashes that are very hard to trace.
For twenty years that library was SourceHook. It was built to solve conflicts between virtual table hooks, and its whole API revolves around virtual functions. Source 2 plugins need much more than that — plenty of what is worth hooking is a plain function found by signature — so plugins started bringing their own detour libraries, and the old conflicts came back.
KHook is the successor, written for Metamod 2.0. It detours both vtable entries and plain function addresses, it is thread-safe, and the goal is for every Metamod 2.0 plugin to use it so Metamod owns detouring again.
The KHook library itself is documented in its README; this section covers it as far as a Source2Mod plugin needs it.
One engine for everyone
Source2Mod does not run a hook engine of its own. It takes KHook from
Metamod when it loads and hands that same engine to every plugin through
SOURCE2MOD_KHOOK_INTERFACE.
This is deliberate. CallOriginal and Supersede walk the handler chain of
the engine that owns the patch. Two independent engines patching the same
address cannot see through each other — one of them silently calls the other's
trampoline instead of the real original. One engine means one chain per
address, so the core's hooks, your plugin's hooks and the hooks of the Metamod
plugins next to you compose correctly.
You never fetch it. SOURCE2MOD_EXPOSE declares the globals and
SOURCE2MOD_SAVEVARS fills them:
Plugin g_Plugin;
SOURCE2MOD_EXPOSE(my_plugin, g_Plugin);
bool Plugin::Load(PluginId id, ISource2ModAPI* api, char* error, size_t maxlen, bool late)
{
SOURCE2MOD_SAVEVARS(); // KHook is live from here on
...
}The KHook headers come from the SDK's vendor/khook submodule. The SDK stamps
that commit into your binary and KHOOK_INIT() refuses the load when it
differs from the one the core was built with, so a plugin built against one
KHook never hooks through another. A plugin with no hooks of its own is never
checked — it does not hook, so it does not care.
How a hooked call runs
Once a function is detoured, every call to it goes through the same loop:
- Every Pre callback runs, with the function's own arguments.
- Each callback reports an action. KHook settles on one for the call.
- Unless a Pre callback superseded it, the original function runs.
- Every Post callback runs — the original has already returned by now.
- The call returns: the original's value, or the one a callback supplied.
KHook::Action | meaning |
|---|---|
Ignore | the callback did nothing |
Override | the original still runs, but the callback's return value is used |
Supersede | the original is skipped, the callback's return value is used |
Two things follow from that loop:
Supersedein a Post callback cannot stop the original — it has already run.- When several hooks want to decide the return value, the first
OverrideorSupersedewins. KHook makes no attempt to resolve such conflicts, so do not rely on being the one that wins.
Source2Mod's own listeners — REGISTER_CON_LISTENER, HOOK_GAME_EVENT, the
net message hooks, entity output listeners — return Action, which is the
same type: ISource2ModTypes.h aliases it to KHook::Action.
Three layers
| layer | what it is | use it when |
|---|---|---|
KHOOK_* macros (ISource2ModHooks.h) | one line per hook, resolved and installed by Source2Mod | almost always |
KHook::Virtual / Member / Function | KHook's own class templates | a hook the macros do not fit — see Raw KHook |
KHook::SetupHook / SetupVirtualHook | the raw C-style API the classes are built on | practically never |
A hook is one line
Every hook is a member of your class, declared with a macro from
ISource2ModHooks.h (pulled in by ISource2ModPlugin.h). The macro takes the
hook's type from the handler it names — the handler's first parameter is
the hooked class, the rest are the function's own arguments — so nothing is
spelled twice. Declare the handler above the macro.
class Plugin final : public ISource2ModPlugin
{
public:
bool Load(PluginId id, ISource2ModAPI* api, char* error, size_t maxlen, bool late) override;
bool Unload(char* error, size_t maxlen) override;
KHook::Return<void> Hook_GameFrame(ISource2Server* pThis, bool simulating, bool bFirstTick, bool bLastTick);
KHook::Return<int> Hook_LoadEventsFromFile(IGameEventManager2* pThis, const char* pszFile, bool bSearchAll);
KHook::Return<void> Hook_PostThink(CCSPlayerPawn* pThis);
KHook::Return<int64_t> Hook_TakeDamageOld(CBaseEntity* pThis, CTakeDamageInfo* pInfo, CTakeDamageResult* pResult);
KHook::Return<void> Hook_SetModel(CBaseModelEntity* pThis, const char* pszModel);
private:
KHOOK_VIRTUAL(m_hGameFrame, &ISource2Server::GameFrame, &g_pSource2Server, nullptr, &Plugin::Hook_GameFrame); // Post only
KHOOK_VIRTUAL(m_hLoadEventsFromFile, &IGameEventManager2::LoadEventsFromFile, KHOOK_VTABLE("server", "CGameEventManager"), &Plugin::Hook_LoadEventsFromFile, nullptr); // Pre only
KHOOK_MEMBER(m_hPostThink, "CCSPlayerPawn::PostThink", &Plugin::Hook_PostThink, nullptr); // gamedata entry name
KHOOK_MEMBER(m_hTakeDamageOld, ADDR_TAKE_DAMAGE_OLD(), &Plugin::Hook_TakeDamageOld, nullptr); // address Source2Mod already resolved
KHOOK_MEMBER(m_hSetModel, ADDR_SET_MODEL(), &Plugin::Hook_SetModel, nullptr);
};The last two arguments are always the Pre and the Post callback; nullptr
leaves a side empty.
A function Source2Mod resolves itself has an ADDR_*() getter in
ISource2ModAddresses.h; pass it instead of a gamedata name and your plugin needs
no gamedata entry, no signature of its own, and follows Source2Mod's gamedata
through game updates. The getter is read when KHOOK_INIT() runs, after
SOURCE2MOD_SAVEVARS() has filled g_pSource2ModAddresses. The full list is on
Addresses. KHOOK_INIT() in Load() installs every hook declared
this way and KHOOK_DESTRUCT() in Unload() takes them all down — see
Function hooks.
Which macro to pick
| you want to hook | use | target |
|---|---|---|
a game function in the core's set (TakeDamage, CanAcquire, PostThink, the movement services…) | ISource2ModGameHooks | a handler — no hook of your own |
an engine call the core already fans out (GameFrame, ClientPutInServer, ClientDisconnect, StartupServer…) | an ISource2ModListener callback | — |
a method of an engine interface you hold a pointer to (ISource2Server::GameFrame) | KHOOK_VIRTUAL | &g_pInstance, the function as KHOOK_GAMEDATA(&Class::Method, "name") to follow the core's gamedata |
a virtual of a game class, for every object of that class (CCSPlayerController::Respawn) | KHOOK_VIRTUAL | KHOOK_VTABLE(module, class), function as a gamedata offset name or &Class::Method |
an interface that is not the class's first base (IHLTVDirector in CCSHLTVDirector) | KHOOK_VIRTUAL | KHOOK_VTABLE_BASE(module, class, base) |
a function Source2Mod already resolves (TakeDamageOld, TerminateRound, movement…) | KHOOK_MEMBER | an ADDR_*() getter |
| a member function found by signature | KHOOK_MEMBER | a gamedata entry name |
a free function (no this) | KHOOK_FUNCTION | a gamedata entry name, ADDR_*() or an address |
| something the macros cannot express | KHook::Virtual / Member / Function | see Raw KHook |
Before you hook anything, check whether Source2Mod already dispatches it. Game hooks and the listener's engine callbacks cover most of what plugins detour; game events (Events), net and user messages (Protobuf), console commands and chat (ConVars & Commands), entity outputs, sounds and transmit all have listener APIs that are already hooked once in the core — using those is cheaper and survives game updates without gamedata of your own.
Coming from SourceHook
Earlier Source2Mod builds ran a private SourceHook engine. The mapping is:
| SourceHook | KHook |
|---|---|
SH_DECL_HOOK* + SH_ADD_HOOK | KHOOK_VIRTUAL(..., &pInstance, ...) |
SH_ADD_DVPHOOK / SH_ADD_MANUALDVPHOOK | KHOOK_VIRTUAL(..., KHOOK_VTABLE(module, class), ...), with a gamedata offset name or an index as the function |
SH_DECL_MANUALHOOK* + SH_MANUALHOOK_RECONFIGURE | KHOOK_VIRTUAL with an index or a gamedata offset name; raw: KHook::Virtual<CLASS, RET, ARGS...> + Configure(index) |
SH_DECL_INLINEHOOK* + SH_ADD_INLINEHOOK | KHOOK_MEMBER / KHOOK_FUNCTION with a gamedata name, an address or an ADDR_* getter |
SH_MEMBER(this, &Class::Handler) | the context — the macros always use the enclosing object |
META_IFACEPTR / SH_IFACEPTR | the first handler parameter |
RETURN_META / RETURN_META_VALUE | return { action } / return { action, value } |
RETURN_META_NEWPARAMS | return KHook::Recall(...) — see Changing the arguments |
MRES_IGNORED / MRES_HANDLED | KHook::Action::Ignore |
MRES_OVERRIDE | KHook::Action::Override |
MRES_SUPERCEDE | KHook::Action::Supersede |
SH_CALL(...) | m_hX->CallOriginal(pThis, ...) / KHook::CallOriginal(&Class::Method, pThis, ...) |
META_RESULT_ORIG_RET(T) | KHook::GetOriginalReturn<T>() |
META_RESULT_OVERRIDE_RET(T) | KHook::GetOverrideReturn<T>() |
SH_REMOVE_HOOK / SH_REMOVE_HOOK_ID | KHOOK_DESTRUCT() / m_hX.Destruct() / deleting the owner |
KHook::Action and META_RES are not numerically compatible — META_RES
has an extra MRES_HANDLED at 1, which shifts everything after it. There is
nothing left to cast between: Source2Mod's Action is KHook::Action.
Checklist
- Set every global a virtual hook names before
KHOOK_INIT(). - Declare the handler above the macro that names it.
- The handler's parameters must match the real function — on both platforms.
- A missing gamedata offset is refused, not installed at
-1— read the log. KHOOK_VTABLEfinds the primary vtable only; for a base further in useKHOOK_VTABLE_BASE.- A silent virtual hook is often a direct call that never goes through the vtable.
- Hooks on objects created after
Load()needm_hX.Init(). Destruct()is final; never destroy a hook's owner from its own handler.- Every hook comes down before the plugin unloads.
See the ISource2ModHooks reference for every class and macro.