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.

plugin.cpp
#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;
}

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.

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.

GroupPoints
Game functionsTakeDamage, CanAcquire, CanMove, CanUse, PostThink, ProcessUsercmds, SimulateUserCommands, RunCommand, AcceptInput, Touch, DropWeapon, the movement services (AirAccelerate … WaterMove), the jump components (OnJumpLegacy, OnJumpModern, CheckJumpButtonLegacy, CheckJumpButtonModern)
Engine callbacksGameFrame, StartupServer, ClientConnect, ClientConnected, ClientPutInServer, ClientDisconnect, ClientVoice, ClientSettingsChanged, GameServerSteamAPIActivated, GameServerSteamAPIDeactivated, LoadEventsFromFile
Any functionHookPoint::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++SourcePawnHooks
HookTarget::All()Hook / HookFunctionevery call
HookTarget::Instance(object)HookInstance / HookFunctionInstancecalls on one object: an entity, a CServerSideClient, a services object, the game rules, an engine interface
HookTarget::Class("CCSPlayerPawn")HookClass / HookFunctionClasscalls 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" });

How a hooked call runs

Once a function is hooked, every call to it goes through the same loop:

  1. Every pre handler runs, in registration order, with the function's own arguments.
  2. Each handler answers an action.
  3. Unless a pre handler superseded it, the original function runs.
  4. Every post handler runs — the original has already returned by now.
  5. The call returns: the original's value, or the one a handler supplied.
ActionPrePost
Ignorenothing changednothing changed
Overridethe original still runs, the function returns the handler's valuethe function returns the handler's value instead of the original's
Supersedethe 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(...) → Source2ModHookIdHook*(...) → int0 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 wantUse
Entities created, spawned, deleted; entity outputsEntity listeners (AddEntityListener, AddEntityOutputListener)
Game events (deaths, round start, bomb)Events
Console commands and chat, another plugin's commandConVars & Commands, command listeners
Net and user messagesProtobuf
Map start and end, plugins loading, Steam authorisation, permissionsListeners

Coming from SourceHook, SDKHooks and DHooks

BeforeSource2Mod
SH_ADD_HOOK(..., pInstance, ...)Hook(point, HookTarget::Instance(pInstance), ...)
SH_ADD_DVPHOOK / SH_ADD_MANUALDVPHOOKHookTarget::Class(...), or a Function point from FromVirtualTable
SH_DECL_INLINEHOOK* / a detoura Function point from Find, FromSignature, FromSymbol, Create
SH_MEMBER(this, &Class::Handler)SOURCE2MOD_MEMBER(this, &Class::Handler)
MRES_IGNORED / MRES_HANDLEDAction::Ignore
MRES_OVERRIDE / MRES_SUPERCEDEAction::Override / Action::Supersede
RETURN_META_VALUE(MRES_SUPERCEDE, v)return { Action::Supersede, v };
RETURN_META_NEWPARAMSctx.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, _ThinkHookPoint_Spawn, Use, StartTouch, EndTouch, Blocked, Think (entity virtuals)
SDKHook_WeaponEquip / _WeaponCanUse, SDKHook_WeaponSwitchHookPoint_WeaponPickup, HookPoint_WeaponSwitch
DHookCreateDetour / DynamicDetourSource2ModFunction.Find / FromSignature + Source2ModHooks.HookFunction
DHookCreate / DynamicHook (virtual)Source2ModFunction.FromVirtual + HookFunctionInstance / HookFunctionClass

See the ISource2ModHooks reference for every type, the contexts and ISource2ModFunctions.

On this page