Function hooks

Source2ModHooks.HookFunction — hooking any game function from a script through a Source2ModFunction; where the function comes from, parameter types, this, pre and post, actions and return values, and when a hook point or a listener is the better choice.

Source2ModHooks (source2mod/hooks) is the script side of ISource2ModHooks. The typed points — HookPoint_TakeDamage, HookPoint_GameFrame, … — hand the handler a context of their own. Any other function is a Source2ModFunction (source2mod/gameconfig), the object a script also calls game functions through, hooked with HookFunction. This is SourceMod's DHooks.

Look for something ready-made first — see Before you hook.

A first hook

ISource2GameClients::ClientCommand swallowing one client command, the hook cpp_sample and sp_sample both install:

plugin.cpp
static const Source2ModType kClientCommand[] = { Source2ModType::Pointer, Source2ModType::Int32, Source2ModType::Command };
ISource2ModFunction* pClientCommand = g_pSource2ModFunctions->FromVirtual(g_PluginID,
    KHook::GetVtableIndex(&ISource2GameClients::ClientCommand),
    FunctionTypes{ Source2ModType::Void, kClientCommand, 3, true });

g_pSource2ModHooks->Hook(HookPoint::Function(pClientCommand), HookTarget::Instance(g_pSource2GameClients),
                         SOURCE2MOD_MEMBER(this, &MyPlugin::OnClientCommand));

HookReturn<void> MyPlugin::OnClientCommand(FunctionContext& ctx, bool post)
{
    const CCommand* pArgs = ctx.GetArg<const CCommand*>(2);
    if (pArgs && !V_strcmp(pArgs->Arg(0), "blocked_command"))
        return Action::Supersede;    // the engine never sees it

    return Action::Ignore;
}

HookFunction returns an id: Source2ModHooks.Unhook(id) takes the hook down, and everything goes when the plugin unloads.

Where the function is

Create withThe function
Find(name)A gamedata entry with a "function" object — the core's or one you added. The types come from the gamedata: no AddParam.
FromGameConfig(name, returnType)A gamedata entry without one, resolved by its symbol, pattern or — a virtual — offset.
FromAddress(address, returnType)A function the core resolved already (Source2ModAddress, source2mod/addresses), the ADDR_* getters of C++.
FromSignature(module, signature, returnType, section)A pattern of your own in a module ("server"), optionally in one section (".text").
FromSignatureRelative(module, signature, returnType, offset, nextInstructionOffset, section)The target of a call / jmp / lea the pattern matches.
FromSymbol(module, symbol, returnType)An exported or debug symbol.
FromVirtualTable(module, vtableName, vtableIndex, returnType, baseName)Slot vtableIndex of one class's vtable (an RTTI class name); baseName picks the vtable of a secondary base.
FromVirtual(vtableIndex, returnType)Slot vtableIndex of whatever object it is hooked on: hook it with HookFunctionInstance or HookFunctionClass.

Each returns null when the function is not found. A script has no WIN_LINUX(): a pattern for the other platform simply does not resolve, so try both, or put the function into gamedata. Vtable slots come from gamedata too: Source2ModGameConfig.GetOffset(name) returns -1 when there is no entry. See Gamedata.

Targets

Hook withThe handler runs for
HookFunction(fn, handler, post)every call
HookFunctionInstance(fn, instance, handler, post)calls on one object — an entity or a SchemaObject; a hook on one entity goes with the entity
HookFunctionClass(fn, className, handler, post)calls on every object of a C++ class ("CCSPlayerPawn") or one derived from it

For a free function the object is its first argument. A virtual needs a vtable to hook: one from FromVirtualTable, or an instance or class target.

Parameters

AddParam(type) describes the next parameter, in order. They are numbered from 0 in the handler, and a member function's this is a parameter too — the first one, so the function's own arguments start at 1:

plugin.sp
// CBasePlayerWeapon* CCSPlayer_ItemServices::GiveNamedItem(const char*, int, CEconItemView*, bool, void*)
Source2ModFunction give = Source2ModFunction.FromGameConfig("CCSPlayer_ItemServices::GiveNamedItem", Source2ModType_Entity);
if (give != null)
{
	give.AddParam(Source2ModType_Object);    // 0: this, the item services
	give.AddParam(Source2ModType_String);    // 1: the item
	give.AddParam(Source2ModType_Int32);     // 2: subtype
	give.AddParam(Source2ModType_Pointer);   // 3: CEconItemView *
	give.AddParam(Source2ModType_Bool);      // 4: force
	give.AddParam(Source2ModType_Pointer);   // 5
	Source2ModHooks.HookFunction(give, Hook_GiveNamedItemPre);
	Source2ModHooks.HookFunction(give, Hook_GiveNamedItemPost, true);
}

Use Source2ModType_Entity for this when it is an entity (the handler gets an entity handle), Source2ModType_Object when it is a schema object (services), Source2ModType_Pointer for anything else.

Source2ModTypeC++In the handler
Bool, Int8, Int16, Int32bool, small integers, int32, uint32, enums, CPlayerSlotGetInt / GetBool, SetInt / SetBool
Int64int64, uint64Decimal text: GetString, SetInt64
Float, Doublefloat, doubleGetFloat, SetFloat
EntityCEntityInstance* and every entity classAn entity handle: GetEntity, SetEntity
ObjectA schema object pointer (services, the scene node)A SchemaObject: GetObject, SetObject
PointerAny other pointerAn opaque SchemaObject
Stringconst char*GetString; SetString points it at a copy that lives until the call is over
BufferStringCBufferString*GetString; SetBufferString replaces the text in place
Vector, VectorValueVector* / const Vector&, Vector by valueGetVector; SetVector
Commandconst CCommand&GetCommandArgs — Arg(0) is the command. Hook only: calling a function with one is an error.
IntRef, BoolRef, FloatRefint32*, bool*, float* out-parametersthe value it points at

ctx.ParamCount is the number of parameters, this included.

Stack arguments

Arguments that do not fit in registers are supported. On Linux the integer and pointer arguments take six registers and the floating-point ones eight; what is left over goes on the stack, eight more slots. Windows passes four arguments in registers and the rest on the stack, up to fourteen in all. The handler does not notice.

Pre and post

HookFunction(fn, handler, post) hooks one side; hook twice for both. One function may serve as both — post says which call it is.

  • Pre runs before the game's function. It can change the arguments, set the return value, and keep the function from running.
  • Post runs after it. GetReturn* is what the function returned, and SetReturn* with Action_Override still replaces it.

Actions and return values

A handler answers a Source2ModAction:

AnswerThe originalWhat the caller gets
Action_IgnoreRunsWhat the original returns
Action_OverrideRunsThe value set with SetReturn*; without one, what the original returns
Action_Supersede (pre)Does not runThe value set with SetReturn*; without one, 0 / null

Arguments changed with Set* in a pre handler reach the later handlers and the original whatever the answer — Ignore and Override alike (with Supersede the original does not run). Override without SetReturn* is therefore "run the original with these arguments". SetVector works on a VectorValue (a Vector passed by value) as well.

plugin.sp
public Source2ModAction Hook_GiveNamedItemPre(FunctionContext ctx, bool post)
{
	char item[64];
	ctx.GetString(1, item, sizeof(item));
	if (!Source2ModUtils.Equal(item, "weapon_awp"))
		return Action_Ignore;

	ctx.SetReturnEntity(NULL_ENTITY);   // the caller gets no weapon
	return Action_Supersede;            // and the game's function does not run
}

public Source2ModAction Hook_GiveNamedItemPost(FunctionContext ctx, bool post)
{
	CBaseEntity weapon = view_as<CBaseEntity>(ctx.GetReturnEntity());
	if (weapon != NULL_ENTITY)
	{
		char classname[64];
		weapon.GetClassname(classname, sizeof(classname));
		Source2ModApi.Log("Given: %s", classname);
	}
	return Action_Ignore;
}

Changing an argument is Set* and Action_Override in pre: every later hook and the original see the new value.

plugin.sp
// void CCSPlayerController::SwitchTeam(int team)
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;
}

What is written through a pointer — SetVector, SetBufferString, the *Ref out-parameters — changes the memory the original reads.

Calling the original

ctx.CallOriginal() (pre) runs the game's function now, past every hook, with the current — possibly changed — arguments; its value becomes the return value. Answer Action_Supersede afterwards, or it runs a second time:

plugin.sp
public Source2ModAction Hook_GiveNamedItemCallOriginal(FunctionContext ctx, bool post)
{
	CBaseEntity weapon = ctx.CallOriginal();
	if (weapon != NULL_ENTITY)
		Source2ModApi.Log("The game gave entity %d", weapon.GetEntityIndex());

	return Action_Supersede;
}

The handle and its hooks

A Source2ModFunction is a handle, and what deleting it does depends on where it came from:

  • One from Find is the gamedata's function, shared with every plugin. Deleting the handle does not remove its hooks; Unhook or the plugin unloading takes them down.
  • One the script owns (FromGameConfig, FromSignature, …) goes with its hooks: deleting it drops them.

AddParam describes the function: describe every parameter before hooking or calling it.

Before you hook

A function hook on a pattern or slot of your own ties the script to the game's binaries: a CS2 update that moves it breaks the hook until the gamedata is fixed. Prefer, in order:

You wantUse
Damage, item pickup, use, movement, jumps, think, user commands, inputs, touch, weapon dropsHook points: Source2ModHooks.Hook(HookPoint_TakeDamage, …), HookInstance, HookClass — the core keeps their gamedata. Source2ModHooks.IsAvailable says whether one is there on this platform.
A connecting player, turning a connection awayHookPoint_ClientConnect — ctx.SetRejectReason(…), result = false, Action_Supersede
Clients joining and leaving, the frameHookPoint_ClientPutInServer, HookPoint_ClientDisconnect (pre and post), HookPoint_GameFrame
Map start and end, plugins loadingThe listener forwards (Source2Mod_OnLevelInit, …)
A console command — the game's or another plugin'sSource2ModCommands.HookConCommand; pre may answer Action_Supersede
Entities being created, spawned, removed; entity outputsSource2ModEntities.AddEntityListener, AddEntityOutputListener
Calling a game function rather than hooking itSource2ModFunction.Call — Calling game functions

sp_sample hooks ClientCommand only because nothing above swallows an arbitrary client command before the engine sees it.

The C++ side of the same mechanism is on Hooking and Function hooks.

On this page