Hooking
ISource2ModHooks — one call to hook a game function, an engine callback or any function by gamedata, signature or vtable slot, the same from C++ and SourcePawn.
Source2Mod has one way to hook anything, the same in C++ and in SourcePawn:
Hook = Point (what) + Target (on which object) + Handler (with its context)g_pSource2ModHooks->Hook(point, target, handler, post, filter) in C++,
Source2ModHooks.Hook / HookInstance / HookClass in a script. The core
resolves the function from its gamedata, places the detour and runs your
handler; every call returns an id for Unhook, and what a plugin still holds
when it unloads the core drops for it.
#include "source2mod/ISource2ModHooks.h"
bool MyPlugin::Load(PluginId id, ISource2ModAPI* api, char* error, size_t maxlen, bool late)
{
SOURCE2MOD_SAVEVARS();
// Half damage for every player pawn. Pre: the original sees the change.
g_pSource2ModHooks->Hook(HookPoint::TakeDamage, HookTarget::Class("CCSPlayerPawn"),
[](TakeDamageContext& ctx, bool post) -> HookReturn<std::int64_t>
{
ctx.info->m_flDamage /= 2;
return Action::Ignore;
});
// Every tick, after the game's own work.
g_pSource2ModHooks->Hook(HookPoint::GameFrame, HookTarget::All(), SOURCE2MOD_MEMBER(this, &MyPlugin::OnGameFrame), true);
// One input of one kind of entity.
g_pSource2ModHooks->Hook(HookPoint::AcceptInput, HookTarget::All(), OnRelayTrigger, false, HookFilter{ "logic_relay", "Trigger" });
return true;
}public void OnPluginStart()
{
// Half damage for every player pawn. Pre: the original sees the change.
Source2ModHooks.HookClass(HookPoint_TakeDamage, "CCSPlayerPawn", OnTakeDamage);
// Every tick, after the game's own work.
Source2ModHooks.Hook(HookPoint_GameFrame, OnGameFrame, true);
// One input of one kind of entity.
Source2ModHooks.Hook(HookPoint_AcceptInput, OnRelayTrigger, false, "logic_relay", "Trigger");
}
public Source2ModAction OnTakeDamage(TakeDamageContext ctx, bool post, int &result)
{
CTakeDamageInfo info = ctx.info;
info.m_flDamage /= 2.0;
return Action_Ignore;
}No KHOOK_INIT(), no signature, no prototype of your own: a plugin that
hooks this way follows the core's gamedata through game updates and needs
only a core update when Valve moves a function. That is the
stable tier.
Hook points
Function hooks
Virtual functions
Writing handlers
Calling game functions
Addresses
Gamedata
Raw KHook
Points
A point is what is hooked. Each one hands its handler a context of its
own, so the compiler (or spcomp) checks the handler against the point.
| Group | Points |
|---|---|
| Game functions | TakeDamage, CanAcquire, CanMove, CanUse, PostThink, ProcessUsercmds, SimulateUserCommands, RunCommand, AcceptInput, Touch, DropWeapon, the movement services (AirAccelerate … WaterMove), the jump components (OnJumpLegacy, OnJumpModern, CheckJumpButtonLegacy, CheckJumpButtonModern) |
| Engine callbacks | GameFrame, StartupServer, ClientConnect, ClientConnected, ClientPutInServer, ClientDisconnect, ClientVoice, ClientSettingsChanged, GameServerSteamAPIActivated, GameServerSteamAPIDeactivated, LoadEventsFromFile |
| Any function | HookPoint::Function(fn), fn an ISource2ModFunction: a gamedata entry, an address, a signature, a symbol, a vtable slot — see Function hooks |
C++ spells them HookPoint::TakeDamage, SourcePawn HookPoint_TakeDamage;
the numbers are the same (HookPointId). Every point and its context is on
Hook points.
Targets
A target is on which object. The object is the function's this — for a
free function, its first argument.
| C++ | SourcePawn | Hooks |
|---|---|---|
HookTarget::All() | Hook / HookFunction | every call |
HookTarget::Instance(object) | HookInstance / HookFunctionInstance | calls on one object: an entity, a CServerSideClient, a services object, the game rules, an engine interface |
HookTarget::Class("CCSPlayerPawn") | HookClass / HookFunctionClass | calls on every object of that C++ class or one derived from it (RTTI) |
A virtual is hooked once, on the vtable, and the core compares the object
before a handler runs. Engine callbacks take All() only: Hook() returns 0
for another target in C++, and HookInstance on an engine point is a script
error. A hook on one entity goes with the entity.
Filters
A point whose object is an entity can be narrowed further by the entity's
designer name ("player", "logic_relay"); AcceptInput also by the
input's name. Both are compared before the handler runs; nullptr or ""
matches anything.
g_pSource2ModHooks->Hook(HookPoint::AcceptInput, HookTarget::All(), OnRelayTrigger, false, HookFilter{ "logic_relay", "Trigger" });Source2ModHooks.Hook(HookPoint_AcceptInput, OnRelayTrigger, false, "logic_relay", "Trigger");How a hooked call runs
Once a function is hooked, every call to it goes through the same loop:
- Every pre handler runs, in registration order, with the function's own arguments.
- Each handler answers an action.
- Unless a pre handler superseded it, the original function runs.
- Every post handler runs — the original has already returned by now.
- The call returns: the original's value, or the one a handler supplied.
| Action | Pre | Post |
|---|---|---|
Ignore | nothing changed | nothing changed |
Override | the original still runs, the function returns the handler's value | the function returns the handler's value instead of the original's |
Supersede | the original does not run, the function returns the handler's value; the remaining pre handlers are skipped | — |
Several handlers on one point: Supersede stops the chain, otherwise the
strongest action wins. A value a handler returns lands in the context right
away, so the next handler sees it. There is no Handled: Ignore is the
answer of a handler that only looks. In C++ Action is KHook::Action; in
SourcePawn it is Source2ModAction (Action_Ignore, Action_Override,
Action_Supersede). Writing handlers covers the
answers in detail.
Source2Mod's other listeners — command listeners, game events, net messages,
entity outputs, sounds — answer the same Action.
Ids, unhooking and unloading
| C++ | SourcePawn | |
|---|---|---|
Hook(...) → Source2ModHookId | Hook*(...) → int | 0 when the point does not exist on this game, the target does not fit the point, or the function of a Function point is null |
Unhook(id) | Source2ModHooks.Unhook(id) | drops one hook; false when there is no such hook |
IsAvailable(HookPointId::X) | Source2ModHooks.IsAvailable(HookPoint_X) | whether the point resolves on the running game (its gamedata matched) |
Nothing has to be undone in Unload() / OnPluginEnd(): what a plugin still
holds when it unloads, the core drops for it.
A typed game-function point is one detour per function, placed the first time a plugin hooks it and kept until the server shuts down; without handlers it only checks an empty list. A Function point's hook goes in at once — KHook falls back to its worker thread only when the function is running at that moment.
KHook underneath
Source2Mod hooks through KHook, Metamod:Source 2.0's detour library, on
the one engine Metamod runs for the whole server. It does not run a hook
engine of its own: CallOriginal and Supersede walk the handler chain of
the engine that owns the patch, and two engines patching the same address
cannot see through each other. 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.
C++ plugins can still use KHook directly — KHOOK_VIRTUAL, KHOOK_MEMBER,
KHOOK_FUNCTION from source2mod/ISource2ModKHook.h — for the rare hook
ISource2ModHooks does not cover. A plugin with KHook hooks of its own is
raw tier in s2m info and needs a rebuild whenever KHook or the engine
changes; see Raw KHook.
Before you hook
Check whether Source2Mod already has a listener for it. These are hooked once in the core and survive game updates without gamedata of your own:
| You want | Use |
|---|---|
| Entities created, spawned, deleted; entity outputs | Entity listeners (AddEntityListener, AddEntityOutputListener) |
| Game events (deaths, round start, bomb) | Events |
| Console commands and chat, another plugin's command | ConVars & Commands, command listeners |
| Net and user messages | Protobuf |
| Map start and end, plugins loading, Steam authorisation, permissions | Listeners |
Coming from SourceHook, SDKHooks and DHooks
| Before | Source2Mod |
|---|---|
SH_ADD_HOOK(..., pInstance, ...) | Hook(point, HookTarget::Instance(pInstance), ...) |
SH_ADD_DVPHOOK / SH_ADD_MANUALDVPHOOK | HookTarget::Class(...), or a Function point from FromVirtualTable |
SH_DECL_INLINEHOOK* / a detour | a Function point from Find, FromSignature, FromSymbol, Create |
SH_MEMBER(this, &Class::Handler) | SOURCE2MOD_MEMBER(this, &Class::Handler) |
MRES_IGNORED / MRES_HANDLED | Action::Ignore |
MRES_OVERRIDE / MRES_SUPERCEDE | Action::Override / Action::Supersede |
RETURN_META_VALUE(MRES_SUPERCEDE, v) | return { Action::Supersede, v }; |
RETURN_META_NEWPARAMS | ctx.SetArg(i, v) in a pre Function handler |
SH_CALL(...) | ctx.CallOriginal(), or fn->CallOriginal<Sig>(...) |
SH_REMOVE_HOOK_ID(id) | Unhook(id) |
SDKHook(entity, SDKHook_OnTakeDamage, cb) | Source2ModHooks.HookInstance(HookPoint_TakeDamage, entity, cb) |
SDKHook_Spawn, _Use, _StartTouch, _EndTouch, _Blocked, _Think | HookPoint_Spawn, Use, StartTouch, EndTouch, Blocked, Think (entity virtuals) |
SDKHook_WeaponEquip / _WeaponCanUse, SDKHook_WeaponSwitch | HookPoint_WeaponPickup, HookPoint_WeaponSwitch |
DHookCreateDetour / DynamicDetour | Source2ModFunction.Find / FromSignature + Source2ModHooks.HookFunction |
DHookCreate / DynamicHook (virtual) | Source2ModFunction.FromVirtual + HookFunctionInstance / HookFunctionClass |
See the ISource2ModHooks reference for every type, the contexts and ISource2ModFunctions.