Raw hooks

Source2ModHook — hooking any game function from a script, described at runtime; where the address comes from, parameter types, this, pre and post, actions and return values, stack arguments, and when a game hook or a forward is the better choice.

Source2ModHook (source2mod/hooks) is what KHOOK_MEMBER, KHOOK_FUNCTION and KHOOK_VIRTUAL are in C++ — a KHook detour on a game function — for a script. A C++ hook takes its signature from the handler's C++ prototype; a script has no prototype to give, so it describes the function at runtime: where it is, what it returns, and the type of each parameter. The handlers then read and change the arguments and the return value through the objects they are handed.

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
// In the plugin class (plugin.h): the handler, then the hook. KHOOK_INIT()
// in Load() installs it once g_pSource2GameClients is set.
KHook::Return<void> Hook_ClientCommand(ISource2GameClients* pThis, CPlayerSlot slot, const CCommand& args);
KHOOK_VIRTUAL(m_hClientCommand, &ISource2GameClients::ClientCommand, &g_pSource2GameClients, &MyPlugin::Hook_ClientCommand, nullptr);

KHook::Return<void> MyPlugin::Hook_ClientCommand(ISource2GameClients* pThis, CPlayerSlot slot, const CCommand& args)
{
    if (!V_strcmp(args.Arg(0), "blocked_command"))
        return { KHook::Action::Supersede };    // the engine never sees it

    return { KHook::Action::Ignore };
}

A hook is a Handle, not an id: create it, describe its parameters with AddParam, then Enable it. It stays installed until Disable(), delete, or the plugin unloading.

Where the function is

Create withThe function
FromGameConfig(name, returnType)A gamedata entry — the core's or one you added — resolved by its symbol or pattern. null when it does not resolve.
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.
FromVirtual(module, vtableName, vtableIndex, returnType, baseName)A virtual function of every object with that vtable: an RTTI class name and the slot. baseName picks the vtable of a secondary base (KHOOK_VTABLE_BASE).
FromVirtualSecondary(module, vtableName, offset, vtableIndex, returnType)A slot in a secondary vtable, by its offset-to-top.
FromVirtualObject(instance, vtableIndex, returnType)A virtual function of one object (an entity or a SchemaObject): the vtable is hooked, the handlers run only for calls on that object. Disable it before the object goes — a new object at the same address would match.

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 and use FromGameConfig. Vtable slots come from gamedata too: Source2ModGameConfig.GetOffset(name) returns -1 when there is no entry. See Gamedata for writing entries and finding slots.

Parameters

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

plugin.sp
// bool ClientConnect(CPlayerSlot slot, const char *name, uint64 xuid, const char *networkId,
//                    bool unk1, CBufferString *rejectReason) -- five slots before ClientCommand
g_connect = Source2ModHook.FromVirtual("server", "CSource2GameClients", index - 5, Source2ModType_Bool);
if (g_connect == null)
	return;

g_connect.AddParam(Source2ModType_Pointer);        // 1: this
g_connect.AddParam(Source2ModType_Int32);          // 2: CPlayerSlot
g_connect.AddParam(Source2ModType_String);         // 3: name
g_connect.AddParam(Source2ModType_Int64);          // 4: xuid
g_connect.AddParam(Source2ModType_String);         // 5: networkId
g_connect.AddParam(Source2ModType_Bool);           // 6: unk1
g_connect.AddParam(Source2ModType_BufferString);   // 7: rejectReason
g_connect.Enable(Hook_ClientConnect);

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: GetInt64, 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 returns
BufferStringCBufferString*GetString; SetBufferString replaces the text in place
VectorVector*, const Vector&, QAngle*GetVector; SetVector writes in place
Commandconst CCommand&GetCommandArgs — Arg(0) is the command
IntRef, BoolRef, FloatRefint32*, bool*, float* out-parametersGetInt / GetBool / GetFloat read the value it points at, Set* writes it in place

A Vector passed by value (VectorValue) cannot be hooked. The return type is Void, a number, Bool, Entity, Object or Pointer — not a string, a vector, a reference or a command.

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. ClientConnect above has seven integer-class parameters with this, so rejectReason arrives on the stack on Linux — the handler does not notice:

plugin.sp
public Source2ModAction Hook_ClientConnect(Source2ModHookParams params, Source2ModHookReturn ret, bool post)
{
	char name[128], xuid[32];
	params.GetString(3, name, sizeof(name));
	params.GetInt64(4, xuid, sizeof(xuid));

	if (!Source2ModUtils.Equal(name, "rejected"))
		return Action_Ignore;

	params.SetBufferString(7, "You are not welcome here.");   // in place, no action needed
	ret.SetBool(false);
	return Action_Supersede;
}

A list that does not fit is refused when the hook is enabled, with the reason in the error.

Pre and post

Enable(pre, post) takes two handlers; either may be INVALID_FUNCTION, and one function may serve as both — post says which call it is. Enable can be called again to swap the handlers; AddParam only works while the hook is disabled.

plugin.sp
g_give = Source2ModHook.FromGameConfig("CCSPlayer_ItemServices::GiveNamedItem", Source2ModType_Entity);
if (g_give == null)
	return;

g_give.AddParam(Source2ModType_Object);    // 1: this, the item services
g_give.AddParam(Source2ModType_String);    // 2: the item
g_give.AddParam(Source2ModType_Int32);     // 3: subtype
g_give.AddParam(Source2ModType_Pointer);   // 4: CEconItemView *
g_give.AddParam(Source2ModType_Bool);      // 5: force
g_give.AddParam(Source2ModType_Pointer);   // 6
g_give.Enable(Hook_GiveNamedItemPre, Hook_GiveNamedItemPost);
  • 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. ret holds what the function returned, and can still be replaced with Action_Override.

Actions and return values

A handler answers a Source2ModAction, KHook's action:

AnswerThe originalWhat the caller gets
Action_IgnoreRunsWhat the original returns. Changed arguments (other than the in-place ones below) and a value set on ret are dropped.
Action_OverrideRuns — with the changed argumentsThe value in ret
Action_Supersede (pre)Does not runThe value set on ret

ret starts out empty in pre and as the original's value in post. Answer Action_Override in pre on a function that returns something only after setting ret — with one exception: changed arguments and no value set means "run the original with these", and the caller gets what it returns.

plugin.sp
public Source2ModAction Hook_GiveNamedItemPre(Source2ModHookParams params, Source2ModHookReturn ret, bool post)
{
	char item[64];
	params.GetString(2, item, sizeof(item));
	if (!Source2ModUtils.Equal(item, "weapon_awp"))
		return Action_Ignore;

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

public Source2ModAction Hook_GiveNamedItemPost(Source2ModHookParams params, Source2ModHookReturn ret, bool post)
{
	CBaseEntity weapon = view_as<CBaseEntity>(ret.GetEntity());
	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(Source2ModHookParams params, Source2ModHookReturn ret, bool post)
{
	CCSPlayerController player = view_as<CCSPlayerController>(params.GetEntity(1));
	if (player == NULL_ENTITY || !player.IsBot())
		return Action_Ignore;

	params.SetInt(2, 2);        // bots always go to the terrorists
	return Action_Override;
}

What is written through a pointer needs no action, because the original reads the same memory: SetVector, SetBufferString and the *Ref out-parameters change it in place.

Calling the original

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

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

	ret.SetEntity(weapon);
	return Action_Supersede;
}

Turning a hook off

plugin.sp
g_give.Disable();      // the handlers stop now; Enable() puts it back
delete g_switchTeam;   // disabled and gone

Disable stops the handlers at once; the detour itself comes down when no call is inside it, so enabling the same hook again right away can be an error ("still being removed") — do it on the next frame. Enabled says whether the hook is installed. Everything goes when the plugin unloads.

Before you hook

A raw hook ties the script to the game's binaries: a signature or slot that a CS2 update moves breaks it until the gamedata is fixed. Prefer, in order:

You wantUse
Damage, item pickup, use, movement, jumps, think, user commands, inputs, touch, weapon dropsGame hooks: Source2ModGameHooks.HookTakeDamage, HookCanAcquire, HookPostThink, HookRunCommand, … — the core keeps their gamedata. Source2ModGameHooks.IsAvailable says whether one is there on this platform.
A connecting player, turning a connection awaySource2Mod_OnClientConnect — return false and write the reason into rejectReason
Clients joining, leaving, the frame, map start and endThe listener forwards (Source2Mod_OnClientPutInServer, Source2Mod_OnGameFrame, …)
A console command — the game's or another plugin'sSource2ModCommands.RegisterConListener; pre may answer Action_Supersede
Entities being created, spawned, removed; entity outputsSource2ModEntities.AddEntityListener, AddEntityIOListener
Calling a game function rather than hooking itSource2ModFunction (FromGameConfig, FromAddress, …)

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

The C++ side of the same mechanism — handlers, actions, recursion — is on Hooks, Virtual hooks and Function hooks.

On this page