Raw KHook

KHOOK_VIRTUAL, KHOOK_MEMBER and KHOOK_FUNCTION from ISource2ModKHook.h, KHook handlers, KHOOK_INIT / KHOOK_DESTRUCT, KHook objects and the C-style API underneath — for C++ hooks ISource2ModHooks does not cover.

ISource2ModHooks covers the typed game functions, the engine callbacks and any function by gamedata, signature or vtable slot. A C++ plugin can still hook through KHook directly — Metamod 2.0's detour library, the same engine the core uses — with the KHOOK_* macros of source2mod/ISource2ModKHook.h (pulled in by ISource2ModPlugin.h) or with KHook's own classes.

Prefer ISource2ModHooks. A plugin with KHook hooks of its own is raw tier in s2m info: it compiles the hooked function's prototype in, so it needs a rebuild whenever KHook or the engine changes. A plugin on ISource2ModHooks needs only a core update. See Compatibility.

Reach for it when you need what the hook points do not give: a handler typed with the function's own C++ prototype, KHook::Recall, a hook moved between objects at runtime, or KHook's post-return accessors.

A hook is one line

Every hook is a member of your class, declared with a macro. 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_ClientCommand(ISource2GameClients* pThis, CPlayerSlot slot, const CCommand& args);
    KHook::Return<void> Hook_Respawn(CCSPlayerController* pThis);
    KHook::Return<void> Hook_PostThink(CCSPlayerPawn* pThis);
    KHook::Return<void> Hook_SetModel(CBaseModelEntity* pThis, const char* pszModel);

private:
    KHOOK_VIRTUAL(m_hClientCommand, KHOOK_GAMEDATA(&ISource2GameClients::ClientCommand, "ISource2GameClients::ClientCommand"), &g_pSource2GameClients, &Plugin::Hook_ClientCommand, nullptr);
    KHOOK_VIRTUAL(m_hRespawn, "CCSPlayerController::Respawn", KHOOK_VTABLE("server", "CCSPlayerController"), &Plugin::Hook_Respawn, nullptr);
    KHOOK_MEMBER(m_hPostThink, "CCSPlayerPawn::PostThink", &Plugin::Hook_PostThink, nullptr);   // gamedata entry name
    KHOOK_MEMBER(m_hSetModel, ADDR_SET_MODEL(), &Plugin::Hook_SetModel, nullptr);              // address Source2Mod resolved
};

The last two arguments are always the pre and the post callback; nullptr leaves a side empty. KHOOK_INIT() in Load() installs every hook declared this way and KHOOK_DESTRUCT() in Unload() takes them all down — see Installing and removing.

you want to hookmacrotarget
a method of an engine interface you hold a pointer toKHOOK_VIRTUAL&g_pInstance, the function as KHOOK_GAMEDATA(&Class::Method, "name")
a virtual of a game class, for every object of that classKHOOK_VIRTUALKHOOK_VTABLE(module, class), the function as a gamedata offset name
an interface that is not the class's first baseKHOOK_VIRTUALKHOOK_VTABLE_BASE(module, class, base)
a function Source2Mod already resolvesKHOOK_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 KHook objects

KHOOK_VIRTUAL

KHOOK_VIRTUAL(member, function, target, pre, post)

The function says where in the vtable the hook goes:

  • &Class::Method — the index is read out of the member function pointer. The class declaration has to match the game's vtable slot for slot.
  • an integer — a hardcoded index.
  • a gamedata offset name ("CCSPlayerController::Respawn") — read from gamedata at KHOOK_INIT(), per platform.
  • KHOOK_GAMEDATA(&Class::Method, "name") — both at once. Gamedata wins; a difference between the two is logged (one of them is stale), and a missing entry for this platform leaves the header's index in place.

When the method is overloaded, disambiguate the member function pointer with a static_cast to the exact signature, in extra parentheses — it contains commas, and the preprocessor would split it into several macro arguments.

The target says what it is attached to:

targethooks
&pInstancethat one object; a pointer to the instance pointer, read at KHOOK_INIT(), so it may still be null when the hook is constructed
KHOOK_VTABLE(module, class)every object sharing the class's vtable, found by RTTI name; the module as a name ("server", "engine2") or as &pModule (an ISource2ModModule* filled in at load)
KHOOK_VTABLE_BASE(module, class, base)every object, through the vtable of a base further in (IHLTVDirector in CCSHLTVDirector)
nullptrnothing yet: m_hX.Init(pInstance) or m_hX.InitGlobal(vtable) later

A gamedata offset that is missing reads as -1, and a hook configured at -1 lands one slot before the vtable — on the typeinfo pointer. The wrapper refuses a negative index and logs the key instead of installing the hook. And a vtable hook only sees calls that go through the vtable — see Virtual functions.

KHOOK_MEMBER and KHOOK_FUNCTION

KHOOK_MEMBER(member, target, pre, post)     // a function with a `this`
KHOOK_FUNCTION(member, target, pre, post)   // a free function: the handler takes only its arguments
targetexampleresolved by
a gamedata entry name"CCSPlayerPawn::PostThink"ISource2ModGameConfig::ResolveSignature
an ADDR_* getterADDR_TAKE_DAMAGE_OLD()Source2Mod, which scanned it at startup
an addressan ISource2ModMemory, void* or typed function pointeryou
nullptryou, later, with m_hX.Init(address)

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 in the macro of a global constructed long before Load(). The expression must not capture anything, and a file that declares hooks outside the plugin's main file needs SOURCE2MOD_GLOBALVARS() so g_pSource2ModAddresses is known. A target that resolves to null at init is logged and the hook is skipped.

Installing and removing

plugin.cpp
bool Plugin::Load(PluginId id, ISource2ModAPI* api, char* error, size_t maxlen, bool late)
{
    // Sets the engine globals too: anything a hook reads is set before KHOOK_INIT().
    SOURCE2MOD_SAVEVARS();

    // The KHook 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 is logged and skipped; the rest still go in.
  • KHOOK_INIT() is also where the KHook check happens: a plugin built against a different khook.hpp than the core gets false out of Load(), with the reason in error. A plugin using raw KHook objects without any KHOOK_* macro calls KHOOK_CHECK() itself before the first one.
  • KHOOK_INIT() installs the hooks that exist when it runs. A hook that is a member of an object created later is installed with m_hX.Init() — typically in that object's constructor. Init() returns false and logs on failure and can simply be called again.
  • m_hX.Destruct() takes one hook down for good; destroying the owner does the same and takes it off the plugin's list, so KHOOK_DESTRUCT() never touches a hook that is already gone.
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

Install and remove hooks on the game thread, never destroy an owner from inside one of its own handlers, and take every hook down before the plugin unloads — a live hook pointing into an unmapped library crashes the server on the next call.

KHook handlers

A KHook handler is an ordinary member function. The object that was called arrives first; the rest are the function's own arguments, in the order the machine code takes them. The declaration is the prototype KHook uses to read the registers and the stack, and nothing checks it against the game.

// void CCSPlayer_WeaponServices::DropWeapon(CBasePlayerWeapon*, Vector*, Vector*)
KHook::Return<void> OnDropWeapon(CCSPlayer_WeaponServices* pThis, CBasePlayerWeapon* pWeapon, Vector* pVecTarget, Vector* pVelocity);
  • Match the real function. Where Source2Mod resolves it, its typedef in ISource2ModAddresses.h is the checked prototype.
  • Widths matter. A bool where the game passes an int reads the wrong part of the register.
  • Platform differences. The same function does not always take its arguments in the same order on Windows and Linux — TerminateRound swaps the delay and the reason, GroundAccelerate the wish direction and the frame time. The handler needs an #ifdef on the declaration and the definition, following the typedef.
return { KHook::Action::Ignore };             // void handlers
return { KHook::Action::Override, false };    // the original runs, false is returned
return { KHook::Action::Supersede, false };   // the original is skipped, false is returned

When several raw hooks want to decide the return value, the first Override or Supersede wins; KHook does not resolve such conflicts.

Changing the arguments

KHook::Recall carries the call on with new arguments: the hooks after yours, the original and every post handler see them. Your own handler is not called again. Recall hands the return back, so it is the whole return:

KHook::Return<void> Plugin::Hook_SetModel(CBaseModelEntity* pThis, const char* pszModel)
{
    if (V_strcmp(pszModel, "models/weapons/old.vmdl"))
        return { KHook::Action::Ignore };

    return KHook::Recall(ADDR_SET_MODEL(), KHook::Return<void>{ KHook::Action::Ignore }, pThis, "models/weapons/new.vmdl");
}

The first argument only supplies the function's type — a typed function pointer for a detour, the member function pointer for a virtual hook; never an SDK schema helper, whose parameter list has a HookChain. Only call it from inside a handler. (ISource2ModHooks does the same with SetArg on a Function point.)

Calling the original

m_hX.CallOriginal(pThis, args...) runs the function past every hook on it; answer Supersede afterwards from a pre handler, or KHook runs it a second time. Outside a handler KHook::CallOriginal(function, args...) does the same for any function; for Source2Mod's ADDR_* functions there is the shorter SOURCE2MOD_ORIGINAL.

Reading the return value in post

bool original  = KHook::GetOriginalReturn<bool>();    // what the original returned
bool overriden = KHook::GetOverrideReturn<bool>();    // what a pre handler overrode it with
bool current   = KHook::GetCurrentReturn<bool>();     // what the call is going to return
bool skipped   = KHook::WasOriginalFunctionSkipped(); // did a pre handler supersede it

Only valid inside a post handler. GetOriginalReturn is meaningless when the original was superseded.

Threads

KHook is thread-safe, but a handler runs on whatever thread called the hooked function — engine code does not always run on the game thread.

Coming from SourceHook

SourceHookKHook
SH_DECL_HOOK* + SH_ADD_HOOKKHOOK_VIRTUAL(..., &pInstance, ...)
SH_ADD_DVPHOOK / SH_ADD_MANUALDVPHOOKKHOOK_VIRTUAL(..., KHOOK_VTABLE(module, class), ...)
SH_DECL_MANUALHOOK* + SH_MANUALHOOK_RECONFIGUREKHOOK_VIRTUAL with an index or a gamedata offset name
SH_DECL_INLINEHOOK* + SH_ADD_INLINEHOOKKHOOK_MEMBER / KHOOK_FUNCTION
META_IFACEPTR / SH_IFACEPTRthe first handler parameter
RETURN_META_VALUEreturn { action, value }
RETURN_META_NEWPARAMSreturn KHook::Recall(...)
MRES_IGNORED / MRES_HANDLEDKHook::Action::Ignore
MRES_OVERRIDE / MRES_SUPERCEDEKHook::Action::Override / Supersede
SH_CALL(...)m_hX.CallOriginal(pThis, ...) / KHook::CallOriginal(&Class::Method, pThis, ...)
META_RESULT_ORIG_RET(T) / META_RESULT_OVERRIDE_RET(T)KHook::GetOriginalReturn<T>() / GetOverrideReturn<T>()
SH_REMOVE_HOOKKHOOK_DESTRUCT() / m_hX.Destruct()

KHook::Action and META_RES are not numerically compatible — META_RES has an extra MRES_HANDLED at 1. Source2Mod's Action is KHook::Action.

KHook objects

The macros wrap KHook::Virtual, KHook::Member and KHook::Function in CSource2ModVirtualHook, CSource2ModMemberHook and CSource2ModFunctionHook. m_hX.Get() and m_hX-> hand you the object underneath. Go fully raw only when you need to own the object's lifetime yourself.

A raw KHook object carries the function and the callbacks in its type. MSVC cannot derive that with CTAD, so KHOOK_NEW (ISource2ModTypes.h) takes the type from the member it is stored in:

plugin.cpp
// plugin.h: KHook::Virtual<ISource2Server, void, bool, bool, bool>* m_hGameFrame = nullptr;
Plugin::Plugin() :
    KHOOK_NEW(m_hGameFrame, &ISource2Server::GameFrame, this, nullptr, &Plugin::Hook_GameFrame)
{
}

bool Plugin::Load(PluginId id, ISource2ModAPI* api, char* error, size_t maxlen, bool late)
{
    SOURCE2MOD_SAVEVARS();
    KHOOK_CHECK();                                // no KHOOK_* macro in this plugin
    m_hGameFrame->Add(g_pSource2Server);          // attach
    return true;
}

bool Plugin::Unload(char* error, size_t maxlen)
{
    m_hGameFrame->Remove(g_pSource2Server);       // detach
    delete m_hGameFrame;                          // deleting takes the hook down
    m_hGameFrame = nullptr;
    return true;
}
classwhere it goesattach / detach
KHook::Virtual<CLASS, RET, ARGS...>Configure(&CLASS::Method) or Configure(index)Add(pThis) / Remove(pThis) for one object, AddGlobal(pThis) / RemoveGlobal(pThis) for every object sharing pThis's vtable
KHook::Member<CLASS, RET, ARGS...>Configure(address) or Configure(&CLASS::Method)configuring places the detour
KHook::Function<RET, ARGS...>Configure(address) or Configure(&Function)configuring places the detour

AddGlobal only reads the first pointer of what it is handed — the vtable pointer — so a member holding a vtable address stands in for an object; keep it alive, the same pointer is needed for RemoveGlobal.

Finding originals

void* KHook::FindOriginal(void* function);                    // behind a detour
void* KHook::FindOriginalVirtual(void** vtable, int index);   // behind a vtable hook

Both hand the input back unchanged when nothing is hooked. SOURCE2MOD_ORIGINAL and HookChain::Bypass are built on them — see Addresses.

The low-level API

Everything above is built on a handful of C-style functions — KHook::SetupHook, KHook::SetupVirtualHook, KHook::RemoveHook, with callbacks that report through KHook::SaveReturnValue instead of returning. You will very likely never call them; a missed KHook::DestroyReturnValue leaks and leaves KHook mid-call, a wrong stack_size corrupts the arguments. The KHook README and vendor/khook/include/khook.hpp in the SDK are the reference.

See the ISource2ModKHook reference for every class and macro.

On this page