Writing Handlers

Handler signatures, contexts, HookReturn and the three actions, calling the original, changing arguments and return values, ids, owners and recursion.

A handler is what ISource2ModHooks calls for a hooked point: it gets the point's context and whether this is the post pass, and answers an action.

The signature

// A function with a return value: HookReturn of that type.
HookReturn<TakeDamageContext::Return> OnTakeDamage(TakeDamageContext& ctx, bool post);   // std::int64_t
HookReturn<bool> OnClientConnect(ClientConnectContext& ctx, bool post);

// Without one, and every engine callback but ClientConnect: HookReturn<void>.
HookReturn<void> OnPostThink(PostThinkContext& ctx, bool post);
HookReturn<void> OnGameFrame(GameFrameContext& ctx, bool post);

// A Function point: FunctionContext, the value through SetReturn.
HookReturn<void> OnSwitchTeam(FunctionContext& ctx, bool post);

Hook() checks the handler against the point at compile time in C++, and spcomp against the Source2ModHookHandler typeset in SourcePawn. Each context's Return is the function's return type; TakeDamageContext::Return saves spelling it.

A C++ handler is a Source2ModCallback: a lambda, a free or static function, or an object and one of its methods — SourceHook's SH_STATIC / SH_MEMBER, spelt SOURCE2MOD_STATIC(func) / SOURCE2MOD_MEMBER(this, &Class::Method). Whose it is, the core reads off the handler: its code lies in the plugin's library, and that is the owner. No call takes a plugin id.

Returning an action

return Action::Ignore;                               // nothing changed
return { Action::Supersede, 0 };                     // skip the original, it returns 0
return { Action::Override, AcquireResult::NotAllowedByMode };
return { Action::Supersede, ctx.CallOriginal() };    // it ran here: do not run it again
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—

There are three actions and no fourth: Ignore is also the answer of a handler that looked and did something on the side. In C++ an action alone takes its value from ctx.result, so a handler that sets the field and returns Override / Supersede works too — keep the two in one block: an if without braces around ctx.result = x; return Action::Supersede; guards only the first of them.

Several handlers on one point run in registration order, pre before the original and post after it. Supersede stops the remaining pre handlers; otherwise the strongest action wins. A value a handler returns lands in ctx.result right away, so the next handler sees it. In post, ctx.result holds what the original returned.

Changing what the original sees

Through pointers

Arguments passed by pointer — the damage info, the move data, the weapon, a vector — can be changed in place in a pre handler: the original runs afterwards and reads what you left there.

HookReturn<TakeDamageContext::Return> MyPlugin::OnTakeDamage(TakeDamageContext& ctx, bool post)
{
    if (ctx.info && sample_damage_scale.Get() != 1.0f)
        ctx.info->m_flDamage *= sample_damage_scale.Get();

    return Action::Ignore;
}

Arguments passed by value

A typed point's scalar fields (a speed, a flag) are read-only: changing them in the context changes nothing. A Function point on the same function can: ctx.SetArg(i, v) (ctx.SetInt(i, v), …) in a pre handler changes what the original and the later handlers get, whatever the answer (Ignore too). Override without a return value means "run the original with these arguments"; the caller gets what it returns.

HookReturn<void> MyPlugin::OnSwitchTeam(FunctionContext& ctx, bool post)
{
    auto* pController = ctx.GetArg<CCSPlayerController*>(0);
    if (!pController || !pController->IsBot())
        return Action::Ignore;

    ctx.SetArg<int>(1, 2);   // bots always go to the terrorists
    return Action::Override;
}

Calling the original

ctx.CallOriginal() runs the game's function now, past every hook on it, and returns what it returned — SourceHook's SH_CALL. Use it to act on the original's result from inside a pre handler, to run it with changed global state around it, or to run it more than once:

const std::int64_t dealt = ctx.CallOriginal();
// ... ctx.damageResult is filled in now ...
return { Action::Supersede, dealt };

In a pre handler the function really runs: answer Supersede with the value, or the game runs it a second time after the handlers. A typed context calls it with the arguments the game passed (a changed pointed-to object counts); a FunctionContext with the current, possibly changed, ones.

Supersede skips the original for every remaining pre handler too. If you called the original yourself and only want to stop it running a second time, supersede is still the right answer — but handlers registered after yours will not run.

Recursion

A handler that calls the function it hooks — through the game, a schema helper or an ADDR_* pointer — runs straight back into its own hook. Call the original instead:

from inside the hook of…call it as
a typed point or a Function pointctx.CallOriginal()
an ISource2ModFunctionfn->CallOriginal<Sig>(...) (fn.CallOriginal(...))
an ADDR_* functionSOURCE2MOD_ORIGINAL(ADDR_...())(...)
a function a schema helper wrapsthe helper with HookChain::Bypass
// Inside a TerminateRound hook: end the round now, without re-entering the hook.
pGameRules->TerminateRound(5.0f, static_cast<int32_t>(nReason), 0, HookChain::Bypass);

CCSGameRules::TerminateRound and CCSPlayerController::TakeDamage already default to HookChain::Bypass; every other helper defaults to HookChain::Run. See Addresses.

The opposite case is fine: calling another hooked function from a handler runs that function's hooks as usual.

Ids and owners

Hook() returns a Source2ModHookId (an int in a script) — 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. A plugin may put several handlers on one point — a pre and a post, two features that both care about damage — and drop any one of them with Unhook(id):

Source2ModHookId m_hDamage = 0;

m_hDamage = g_pSource2ModHooks->Hook(HookPoint::TakeDamage, HookTarget::All(), OnTakeDamage);
...
g_pSource2ModHooks->Unhook(m_hDamage);

What a plugin still holds when it unloads, the core drops for it — nothing to do in Unload() / OnPluginEnd(). A hook on one entity goes with the entity.

Threads and cost

Hot handlers run often. PostThink, the movement functions and GameFrame run for every player or every tick; return early on the cases you do not care about. A plugin that does not hook a point costs nothing for it.

On this page