Game Hooks
ISource2ModGameHooks — the game functions plugins hook most, detoured once by the core from its gamedata; a handler and a context instead of a KHook, a signature and a prototype of your own.
Most plugins hook the same few dozen game functions: TakeDamage,
CanAcquire, PostThink, the movement services, the jump code. Every one of
them written as a KHOOK_MEMBER ties the plugin to that function's exact
prototype and to a signature of its own — and when Valve moves an argument or
changes the order between Windows and Linux, every plugin has to be rebuilt.
ISource2ModGameHooks holds those hooks in the core. You register a handler; the
core resolves the function from its gamedata, places the detour the first
time anybody listens and takes it out again once the last listener is gone. A
changed prototype is fixed in the core's one call site, and the plugins keep
running. This is what puts a plugin on the stable tier — see
Compatibility.
#include "source2mod/ISource2ModGameHooks.h"
bool MyPlugin::Load(PluginId id, ISource2ModAPI* api, char* error, size_t maxlen, bool late)
{
SOURCE2MOD_SAVEVARS();
// Pre: halve every hit on a player.
m_hDamage = g_pSource2ModGameHooks->HookTakeDamage([](TakeDamageContext& ctx, bool post) -> Action
{
if (ctx.entity && ctx.entity->IsPlayerPawn())
ctx.info->m_flDamage = ctx.info->m_flDamage() * 0.5f; // by pointer: the original sees it
return Action::Ignore;
}, false);
// Post: react to what CanAcquire decided -- a method as the handler.
g_pSource2ModGameHooks->HookCanAcquire(SOURCE2MOD_MEMBER(this, &MyPlugin::OnCanAcquire), true);
return true;
}There is no KHOOK_INIT() in this: the plugin declared no hook of its own. No
plugin id either — whose a handler is, the core reads off the handler itself
(see Handlers and owners).
The handler
Every hook hands its handler a context — a plain struct with the hooked
object, the function's arguments and, where the function returns something,
result — plus post, which pass this is. The handler answers the way a
KHook handler does: an action, and for a function with a return value the
value that goes with it (GameHookReturn).
| Answer | Pre | Post |
|---|---|---|
Action::Ignore | nothing changed | nothing changed |
{ Action::Override, value } | the original still runs, the function returns value | the function returns value instead of the original's |
{ Action::Supersede, value } | the original does not run, the function returns value; the remaining pre handlers are skipped | — |
// Nobody moves while frozen: CanMove returns false.
g_pSource2ModGameHooks->HookCanMove([](CanMoveContext& ctx, bool post) -> GameHookReturn<CanMoveContext::Return>
{
if (IsFrozen(ctx.pawn))
return { Action::Supersede, false };
return Action::Ignore;
}, false);An action alone takes its value from ctx.result, so setting the field and
returning Action::Supersede works too — but keep the two in one statement:
an if without braces around ctx.result = x; return Action::Supersede;
guards only the first of them. A function without a return value (PostThink,
the movement functions) answers the action alone.
Arguments passed by pointer (the damage info, the move data, the weapon)
can be changed in place and the original sees the change. Arguments passed
by value (a speed, a flag) cannot; a handler that needs to change one keeps
a raw KHOOK_MEMBER of its own and uses KHook::Recall.
Handlers run in registration order, pre before the original, post after it. In
post, ctx.result holds what the original returned. A value a handler returns
lands in ctx.result right away, so the next handler sees it. A Supersede
stops the remaining pre handlers — the same rule as
command listeners.
What the game would do: CallOriginal
ctx.CallOriginal() runs the game's function now, with the arguments the game
passed, past every hook on it — SourceHook's SH_CALL — and returns what it
returned. From a pre handler that is how you learn what the game would answer:
g_pSource2ModGameHooks->HookCanAcquire([](CanAcquireContext& ctx, bool post) -> GameHookReturn<CanAcquireContext::Return>
{
const AcquireResult game = ctx.CallOriginal();
if (game == AcquireResult::Allowed && IsBlockedHere(ctx))
return { Action::Supersede, AcquireResult::NotAllowedByMode };
return { Action::Supersede, game }; // it has run already -- do not let it run twice
}, false);In a pre handler the function really runs, side effects and all, so a
handler that calls it answers Supersede with the value; with Ignore the
game would run it a second time. A changed pointed-to object (the damage info,
the move data) counts in that call; a scalar field changed in the context does
not. In a post handler ctx.result already holds the game's answer.
The same trick without a value — the game's own code, run with a setting flipped for one call:
CConVarRef<bool> sv_autobunnyhopping("sv_autobunnyhopping");
g_pSource2ModGameHooks->HookCheckJumpButtonLegacy([](LegacyJumpContext& ctx, bool post) -> Action
{
if (sv_autobunnyhopping.Get())
return Action::Ignore;
sv_autobunnyhopping.Set(true);
ctx.CallOriginal();
sv_autobunnyhopping.Set(false);
return Action::Supersede;
}, false);Which functions
The set is the one Swiftly hooks, which is what most plugins reach for:
| Group | Hooks |
|---|---|
| Damage and items | TakeDamage, CanAcquire, CanUse, DropWeapon |
| Player | CanMove, PostThink, ProcessUsercmds, SimulateUserCommands, RunCommand |
| Entities | AcceptInput, Touch |
| Movement services | AirAccelerate, AirMove, CanUnduck, CategorizePosition, CheckFalling, CheckParameters, CheckVelocity, CheckWater, Duck, Friction, FullWalkMove, GroundAccelerate, LadderMove, MoveInit, PlayerMove, ProcessMovement, SetupMove, TryPlayerMove, WalkMove, WaterMove |
| Jump | OnJumpLegacy, OnJumpModern, CheckJumpButtonLegacy, CheckJumpButtonModern |
Every context is listed in the ISource2ModGameHooks reference. Two things to know:
GroundAccelerate's arguments come in a different order on Windows and Linux. The context always has them in one order; that swap is the core's.Touchis a vtable hook onCBaseEntity's own table, so an entity class that overridesTouchis not seen.CanUseandDropWeaponare vtable hooks on the weapon services; everything else is found by signature.
IsAvailable(GameHook::X) says whether the core's gamedata has the function on
this platform. Handlers can be registered either way; on a missing entry they
never run.
Handlers and owners
A 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) (or just func
and { 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, and what a plugin still holds when it unloads, the core drops for
it — nothing to do in Unload().
HookX() returns a GameHookId. A plugin may put several handlers on one
function — a pre and a post, two features that both care about damage — and
drop any one of them with UnhookX(id), or with the same function or method
it hooked with (a lambda has no such identity; it goes by its id):
GameHookId m_hDamage = 0;
m_hDamage = g_pSource2ModGameHooks->HookTakeDamage(OnTakeDamage, false);
g_pSource2ModGameHooks->HookPostThink(SOURCE2MOD_MEMBER(this, &MyPlugin::OnPostThink), true);
...
g_pSource2ModGameHooks->UnhookTakeDamage(m_hDamage);
g_pSource2ModGameHooks->UnhookPostThink(SOURCE2MOD_MEMBER(this, &MyPlugin::OnPostThink), true);Unhooking from inside the handler is fine: the detour itself comes out on the next frame, never from inside its own dispatch.
Debugging
s2m hookdebug <part of a hook name|all|off> in the server console logs
every call of the matching game hooks: each plugin's answer, the outcome and
the return value; TakeDamage adds the victim, the damage and the damage bits.
Source2Mod hookdebug TakeDamage
... hookdebug CBaseEntity::TakeDamageOld pre: JailBreak.smx -> Supersede
... hookdebug CBaseEntity::TakeDamageOld pre: Supersede (return 0) victim=func_breakable damage=25.000000 bits=4
Source2Mod hookdebug offWhen to still hook yourself
- The function is not in the set.
- You need to change an argument passed by value, or call the original with
different arguments (
KHook::Recall).
Those are raw hooks — KHOOK_MEMBER,
KHOOK_VIRTUAL — and they put the plugin on the
raw tier: it hooks through KHook itself and is rebuilt when the function
changes. s2m list shows which tier each plugin is on.
Reference
ISource2ModGameHooks— every hook, context and enumISource2ModTypes—Source2ModCallback,SOURCE2MOD_STATIC,SOURCE2MOD_MEMBERISource2ModApi::GameHooks()— the same interface through the API