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:
// 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 };
}Source2ModHook g_command;
void HookClientCommand()
{
// ISource2GameClients::ClientCommand is in the core's gamedata.
int index = Source2ModGameConfig.GetOffset("ISource2GameClients::ClientCommand");
if (index < 0)
return;
// void ClientCommand(CPlayerSlot slot, const CCommand &args)
g_command = Source2ModHook.FromVirtual("server", "CSource2GameClients", index, Source2ModType_Void);
if (g_command == null)
return;
g_command.AddParam(Source2ModType_Pointer); // 1: this
g_command.AddParam(Source2ModType_Int32); // 2: CPlayerSlot
g_command.AddParam(Source2ModType_Command); // 3: const CCommand &
g_command.Enable(Hook_ClientCommand);
}
public Source2ModAction Hook_ClientCommand(Source2ModHookParams params, Source2ModHookReturn ret, bool post)
{
Source2ModCommandArgs args = params.GetCommandArgs(3);
char command[64];
args.Arg(0, command, sizeof(command));
if (Source2ModUtils.Equal(command, "blocked_command"))
return Action_Supersede; // the engine never sees it
return 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 with | The 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:
// 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.
Source2ModType | C++ | In the handler |
|---|---|---|
Bool, Int8, Int16, Int32 | bool, small integers, int32, uint32, enums, CPlayerSlot | GetInt / GetBool, SetInt / SetBool |
Int64 | int64, uint64 | Decimal text: GetInt64, SetInt64 |
Float, Double | float, double | GetFloat, SetFloat |
Entity | CEntityInstance* and every entity class | An entity handle: GetEntity, SetEntity |
Object | A schema object pointer (services, the scene node) | A SchemaObject: GetObject, SetObject |
Pointer | Any other pointer | An opaque SchemaObject |
String | const char* | GetString; SetString points it at a copy that lives until the call returns |
BufferString | CBufferString* | GetString; SetBufferString replaces the text in place |
Vector | Vector*, const Vector&, QAngle* | GetVector; SetVector writes in place |
Command | const CCommand& | GetCommandArgs — Arg(0) is the command |
IntRef, BoolRef, FloatRef | int32*, bool*, float* out-parameters | GetInt / 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:
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.
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.
retholds what the function returned, and can still be replaced withAction_Override.
Actions and return values
A handler answers a Source2ModAction, KHook's action:
| Answer | The original | What the caller gets |
|---|---|---|
Action_Ignore | Runs | What the original returns. Changed arguments (other than the in-place ones below) and a value set on ret are dropped. |
Action_Override | Runs — with the changed arguments | The value in ret |
Action_Supersede (pre) | Does not run | The 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.
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.
// 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:
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
g_give.Disable(); // the handlers stop now; Enable() puts it back
delete g_switchTeam; // disabled and goneDisable 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 want | Use |
|---|---|
| Damage, item pickup, use, movement, jumps, think, user commands, inputs, touch, weapon drops | Game 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 away | Source2Mod_OnClientConnect — return false and write the reason into rejectReason |
| Clients joining, leaving, the frame, map start and end | The listener forwards (Source2Mod_OnClientPutInServer, Source2Mod_OnGameFrame, …) |
| A console command — the game's or another plugin's | Source2ModCommands.RegisterConListener; pre may answer Action_Supersede |
| Entities being created, spawned, removed; entity outputs | Source2ModEntities.AddEntityListener, AddEntityIOListener |
| Calling a game function rather than hooking it | Source2ModFunction (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.