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);// A function with a return value: the value is the third parameter.
public Source2ModAction OnTakeDamage(TakeDamageContext ctx, bool post, int &result)
public Source2ModAction OnClientConnect(ClientConnectContext ctx, bool post, bool &result)
// Without one, and every engine callback but ClientConnect.
public Source2ModAction OnPostThink(PostThinkContext ctx, bool post)
public Source2ModAction OnGameFrame(GameFrameContext ctx, bool post)
// A Function point: FunctionContext, the value through SetReturn*.
public Source2ModAction Hook_SwitchTeam(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 againreturn Action_Ignore; // nothing changed
result = 0; // skip the original, it returns 0
return Action_Supersede;
result = AcquireResult_NotAllowedByMode;
return Action_Override;| 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 | — |
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;
}public Source2ModAction OnTakeDamage(TakeDamageContext ctx, bool post, int &result)
{
CTakeDamageInfo info = ctx.info;
float scale = sp_sample_damage_scale.FloatValue;
if (scale != 1.0)
info.m_flDamage *= scale;
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;
}public Source2ModAction Hook_SwitchTeam(FunctionContext ctx, bool post)
{
CCSPlayerController player = view_as<CCSPlayerController>(ctx.GetEntity(0));
if (player == NULL_ENTITY || !player.IsBot())
return Action_Ignore;
ctx.SetInt(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 point | ctx.CallOriginal() |
an ISource2ModFunction | fn->CallOriginal<Sig>(...) (fn.CallOriginal(...)) |
an ADDR_* function | SOURCE2MOD_ORIGINAL(ADDR_...())(...) |
| a function a schema helper wraps | the 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);int g_damageHook;
g_damageHook = Source2ModHooks.Hook(HookPoint_TakeDamage, OnTakeDamage);
...
Source2ModHooks.Unhook(g_damageHook);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.