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.

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.cpp
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:

  1. Every Pre callback runs, with the function's own arguments.
  2. Each callback reports an action. KHook settles on one for the call.
  3. Unless a Pre callback superseded it, the original function runs.
  4. Every Post callback runs — the original has already returned by now.
  5. The call returns: the original's value, or the one a callback supplied.
KHook::Actionmeaning
Ignorethe callback did nothing
Overridethe original still runs, but the callback's return value is used
Supersedethe original is skipped, the callback's return value is used

Two things follow from that loop:

  • Supersede in a Post callback cannot stop the original — it has already run.
  • When several hooks want to decide the return value, the first Override or Supersede wins. 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

layerwhat it isuse it when
KHOOK_* macros (ISource2ModHooks.h)one line per hook, resolved and installed by Source2Modalmost always
KHook::Virtual / Member / FunctionKHook's own class templatesa hook the macros do not fit — see Raw KHook
KHook::SetupHook / SetupVirtualHookthe raw C-style API the classes are built onpractically 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.

plugin.h
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 hookusetarget
a game function in the core's set (TakeDamage, CanAcquire, PostThink, the movement services…)ISource2ModGameHooksa 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_VIRTUALKHOOK_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_VIRTUALKHOOK_VTABLE_BASE(module, class, base)
a function Source2Mod already resolves (TakeDamageOld, TerminateRound, movement…)KHOOK_MEMBERan ADDR_*() getter
a member function found by signatureKHOOK_MEMBERa gamedata entry name
a free function (no this)KHOOK_FUNCTIONa gamedata entry name, ADDR_*() or an address
something the macros cannot expressKHook::Virtual / Member / Functionsee 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:

SourceHookKHook
SH_DECL_HOOK* + SH_ADD_HOOKKHOOK_VIRTUAL(..., &pInstance, ...)
SH_ADD_DVPHOOK / SH_ADD_MANUALDVPHOOKKHOOK_VIRTUAL(..., KHOOK_VTABLE(module, class), ...), with a gamedata offset name or an index as the function
SH_DECL_MANUALHOOK* + SH_MANUALHOOK_RECONFIGUREKHOOK_VIRTUAL with an index or a gamedata offset name; raw: KHook::Virtual<CLASS, RET, ARGS...> + Configure(index)
SH_DECL_INLINEHOOK* + SH_ADD_INLINEHOOKKHOOK_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_IFACEPTRthe first handler parameter
RETURN_META / RETURN_META_VALUEreturn { action } / return { action, value }
RETURN_META_NEWPARAMSreturn KHook::Recall(...) — see Changing the arguments
MRES_IGNORED / MRES_HANDLEDKHook::Action::Ignore
MRES_OVERRIDEKHook::Action::Override
MRES_SUPERCEDEKHook::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_IDKHOOK_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_VTABLE finds the primary vtable only; for a base further in use KHOOK_VTABLE_BASE.
  • A silent virtual hook is often a direct call that never goes through the vtable.
  • Hooks on objects created after Load() need m_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.

On this page