Function Hooks

KHOOK_MEMBER and KHOOK_FUNCTION — detouring functions by gamedata name, ADDR_* getter or address, and the hook lifecycle with KHOOK_INIT / KHOOK_DESTRUCT.

Anything a signature scan can find is hookable — no vtable, no interface. A function hook patches the first bytes of the function itself, so it catches every call: through a vtable, direct, from any library.

KHOOK_MEMBER(member, target, pre, post)     // a function with a `this`
KHOOK_FUNCTION(member, target, pre, post)   // a free function
  • KHOOK_MEMBER — the handler's first parameter is the object (this), the rest are the function's own arguments.
  • KHOOK_FUNCTION — the handler takes only the function's own arguments.

Where the address comes from

The target is one of:

targetexampleresolved by
a gamedata entry name"CCSPlayerPawn::PostThink"ISource2ModGameConfig::ResolveSignature — the entry's library, then its symbol or pattern
an ADDR_* getterADDR_TAKE_DAMAGE_OLD()Source2Mod, which scanned it at startup — nothing is scanned twice
an addressan ISource2ModMemory, void* or typed function pointeryou
nullptryou, later, with m_hX.Init(address)
Hooks.h
class CHooks
{
public:
    KHook::Return<int64_t> OnTakeDamageOld(CBaseEntity* pThis, CTakeDamageInfo* pInfo, CTakeDamageResult* pResult);
    KHook::Return<void> OnPostThink(CCSPlayerPawn* pThis);

    // What Source2Mod already resolved.
    KHOOK_MEMBER(m_hTakeDamageOld, ADDR_TAKE_DAMAGE_OLD(), &CHooks::OnTakeDamageOld, nullptr);
    // A gamedata entry.
    KHOOK_MEMBER(m_hPostThink, "CCSPlayerPawn::PostThink", &CHooks::OnPostThink, nullptr);
};

Prefer an ADDR_* getter when one exists: it is already resolved, and its typedef in ISource2ModAddresses.h documents the real prototype — including platform differences. The full list is on Addresses. For a function Source2Mod does not know, add an entry to your gamedata (see Gamedata) and name it.

A pattern you keep in the plugin instead is scanned yourself and passed as an address. WIN_LINUX(win, lin) from ISource2ModModule.h picks the platform's pattern at compile time:

ISource2ModMemory addr = pServer->FindPattern(WIN_LINUX("48 89 7C 24 ? 55", "55 48 89 E5 41 57"));

See Memory & modules. A gamedata entry is still the better choice for anything a game update moves: it is fixed without a rebuild.

Deferred targets

The target is not evaluated when your object is constructed. The macro wraps it in a capture-less lambda that runs at KHOOK_INIT() (or at m_hX.Init()). That is what lets ADDR_TAKE_DAMAGE_OLD() or GAMECONFIG_RESOLVE("...") sit right in the macro even though your hooks object is usually a global, constructed long before Load() when g_pSource2ModAddresses is still null.

Two consequences:

  • the target expression must not capture anything — it cannot name a local or this; globals and function calls are fine;
  • the translation unit needs Source2Mod globals in scope. A header that declares hooks outside the plugin's main file uses SOURCE2MOD_GLOBALVARS() so g_pSource2ModAddresses is known:
hooks/Hooks.h
#include "source2mod/ISource2ModAddresses.h"

// The ADDR_* targets below read g_pSource2ModAddresses at KHOOK_INIT().
SOURCE2MOD_GLOBALVARS();

A target that resolves to null at init — a missing gamedata entry, an optional ADDR_* that was not found — is logged and the hook is skipped.

Installing and removing everything

KHOOK_INIT() in Load() resolves and installs every hook the plugin declared with the macros; KHOOK_DESTRUCT() in Unload() takes them all down:

plugin.cpp
bool Plugin::Load(PluginId id, ISource2ModAPI* api, char* error, size_t maxlen, bool late)
{
    SOURCE2MOD_SAVEVARS();

    g_pSource2Server = api->GetSource2Server();   // anything a hook reads has to be set first

    // The KHook commit check inside returns false out of Load() on a mismatch.
    KHOOK_INIT();

    return true;
}

bool Plugin::Unload(char* error, size_t maxlen)
{
    KHOOK_DESTRUCT();
    return true;
}

A hook whose target cannot be resolved — a gamedata entry that is missing, a vtable not found, an instance still null — is logged and skipped; the rest still go in. (ISource2ModHook::InitAll() is the call that returns false in that case, if you want to know.) A hook declared with a nullptr target is left alone and counts as success.

KHOOK_INIT() is also where the KHook commit check happens: a plugin built against a different KHook than the core runs gets false returned out of Load(), with the reason in error. A plugin that never calls KHOOK_INIT() — no hooks of its own — is not checked.

Order matters: KHOOK_INIT() reads the instance pointers and calls the Source2Mod, so it goes after SOURCE2MOD_SAVEVARS() and after you have set the globals your virtual hooks name.

The list KHOOK_INIT() walks is per plugin binary: every hook wrapper registers itself as it is constructed, whatever file it is in, and KHOOK_DESTRUCT() takes them down last-first.

Hooks installed later

KHOOK_INIT() installs the hooks that exist when it runs. A hook that is a member of an object you create after Load() is registered but not installed — install it yourself with m_hX.Init(), typically in that object's constructor (the members are built before the constructor body runs):

class CChatFilter
{
public:
    KHook::Return<void> Hook_ClientCommand(ISource2GameClients* pThis, CPlayerSlot slot, const CCommand& args);

    CChatFilter()
    {
        m_hClientCommand.Init();   // KHOOK_INIT() ran long ago
    }

private:
    KHOOK_VIRTUAL(m_hClientCommand, &ISource2GameClients::ClientCommand, &g_pSource2GameClients, &CChatFilter::Hook_ClientCommand, nullptr);
};

auto* pFilter = new CChatFilter();   // the hook goes in here
...
delete pFilter;                       // and comes out here

Init() resolves the target the macro was given exactly as KHOOK_INIT() would: it reads &pInstance now, looks the vtable up, resolves the gamedata entry or calls the ADDR_* getter. It returns false and logs when that fails, and it can simply be called again — a hook waiting for an instance that does not exist yet goes in with the first call that finds it. On a hook that is already installed it does nothing; m_hX.IsInstalled() tells you which is which.

A hook declared with a nullptr target is placed the explicit way:

m_hX.Init(address);        // KHOOK_MEMBER / KHOOK_FUNCTION: ISource2ModMemory or typed function pointer
m_hX.Init(pInstance);      // KHOOK_VIRTUAL, one object
m_hX.InitGlobal(vtable);   // KHOOK_VIRTUAL, a whole vtable

Removing hooks

KHOOK_DESTRUCT() detaches every hook and deletes it — deleting is what takes the detour down. One hook can also go on its own: m_hX.Destruct(). That is final: the wrapper no longer owns a KHook object, so it cannot be installed again — to hook the same thing later, create a new owner.

Destroying the owner does the same. Each wrapper's destructor calls Destruct() and takes itself off the plugin's hook list, so deleting an object removes its hooks, and a later KHOOK_DESTRUCT() never touches a hook that is already gone. Destruct() is safe to call twice: an object still alive at Unload() is covered by KHOOK_DESTRUCT(), and deleting it afterwards is harmless.

Install and remove hooks on the game thread, and never destroy an owner from inside one of its own handlers — removing a hook synchronously under its own callback can deadlock or crash. Defer it to the next frame.

Take every hook down before your plugin unloads. A live hook pointing into an unmapped library crashes the server on the next call.

The wrapper's members

What KHOOK_MEMBER / KHOOK_FUNCTION / KHOOK_VIRTUAL declare is a CSource2ModMemberHook, CSource2ModFunctionHook or CSource2ModVirtualHook:

memberdoes
Init()resolves the macro's target and installs; false on failure, retryable
Init(address) / Init(pInstance) / InitGlobal(...)installs on an explicit target
Destruct()detaches and deletes the KHook object; final
IsInstalled()whether it is installed
CallOriginal(...)runs the original past every hook
Get() / operator->the KHook object underneath (KHook::Member, Function or Virtual)

See the ISource2ModHooks reference.

On this page