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.
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 hook | macro | target |
|---|---|---|
| a method of an engine interface you hold a pointer to | KHOOK_VIRTUAL | &g_pInstance, the function as KHOOK_GAMEDATA(&Class::Method, "name") |
| a virtual of a game class, for every object of that class | KHOOK_VIRTUAL | KHOOK_VTABLE(module, class), the function as a gamedata offset name |
| an interface that is not the class's first base | KHOOK_VIRTUAL | KHOOK_VTABLE_BASE(module, class, base) |
| a function Source2Mod already resolves | 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 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 atKHOOK_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:
| target | hooks |
|---|---|
&pInstance | that 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) |
nullptr | nothing 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| target | example | resolved by |
|---|---|---|
| a gamedata entry name | "CCSPlayerPawn::PostThink" | ISource2ModGameConfig::ResolveSignature |
an ADDR_* getter | ADDR_TAKE_DAMAGE_OLD() | Source2Mod, which scanned it at startup |
| an address | an ISource2ModMemory, void* or typed function pointer | you |
nullptr | you, 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
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 differentkhook.hppthan the core getsfalseout ofLoad(), with the reason inerror. A plugin using raw KHook objects without anyKHOOK_*macro callsKHOOK_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 withm_hX.Init()— typically in that object's constructor.Init()returnsfalseand 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, soKHOOK_DESTRUCT()never touches a hook that is already gone.
| 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 |
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.his the checked prototype. - Widths matter. A
boolwhere the game passes anintreads 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 —
TerminateRoundswaps the delay and the reason,GroundAcceleratethe wish direction and the frame time. The handler needs an#ifdefon 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 returnedWhen 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 itOnly 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
| SourceHook | KHook |
|---|---|
SH_DECL_HOOK* + SH_ADD_HOOK | KHOOK_VIRTUAL(..., &pInstance, ...) |
SH_ADD_DVPHOOK / SH_ADD_MANUALDVPHOOK | KHOOK_VIRTUAL(..., KHOOK_VTABLE(module, class), ...) |
SH_DECL_MANUALHOOK* + SH_MANUALHOOK_RECONFIGURE | KHOOK_VIRTUAL with an index or a gamedata offset name |
SH_DECL_INLINEHOOK* + SH_ADD_INLINEHOOK | KHOOK_MEMBER / KHOOK_FUNCTION |
META_IFACEPTR / SH_IFACEPTR | the first handler parameter |
RETURN_META_VALUE | return { action, value } |
RETURN_META_NEWPARAMS | return KHook::Recall(...) |
MRES_IGNORED / MRES_HANDLED | KHook::Action::Ignore |
MRES_OVERRIDE / MRES_SUPERCEDE | KHook::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_HOOK | KHOOK_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.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;
}| class | where it goes | attach / 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 hookBoth 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.