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 functionKHOOK_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:
| target | example | resolved by |
|---|---|---|
| a gamedata entry name | "CCSPlayerPawn::PostThink" | ISource2ModGameConfig::ResolveSignature — the entry's library, then its symbol or pattern |
an ADDR_* getter | ADDR_TAKE_DAMAGE_OLD() | Source2Mod, which scanned it at startup — nothing is scanned twice |
| an address | an ISource2ModMemory, void* or typed function pointer | you |
nullptr | you, later, with m_hX.Init(address) |
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()sog_pSource2ModAddressesis known:
#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:
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 hereInit() 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 vtableRemoving 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:
| member | does |
|---|---|
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.