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:
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;
}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)
Source2ModFunction command = Source2ModFunction.FromVirtualTable("server", "CSource2GameClients", index, Source2ModType_Void);
if (command == null)
return;
command.AddParam(Source2ModType_Pointer); // 0: this
command.AddParam(Source2ModType_Int32); // 1: CPlayerSlot
command.AddParam(Source2ModType_Command); // 2: const CCommand &
Source2ModHooks.HookFunction(command, Hook_ClientCommand);
}
public Source2ModAction Hook_ClientCommand(FunctionContext ctx, bool post)
{
Source2ModCommandArgs args = ctx.GetCommandArgs(2);
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;
}HookFunction returns an id: Source2ModHooks.Unhook(id) takes the hook
down, and everything goes when the plugin unloads.
Where the function is
| Create with | The 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 with | The 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:
// 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.
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: GetString, 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 is over |
BufferString | CBufferString* | GetString; SetBufferString replaces the text in place |
Vector, VectorValue | Vector* / const Vector&, Vector by value | GetVector; SetVector |
Command | const CCommand& | GetCommandArgs — Arg(0) is the command. Hook only: calling a function with one is an error. |
IntRef, BoolRef, FloatRef | int32*, bool*, float* out-parameters | the 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, andSetReturn*withAction_Overridestill replaces it.
Actions and return values
A handler answers a Source2ModAction:
| Answer | The original | What the caller gets |
|---|---|---|
Action_Ignore | Runs | What the original returns |
Action_Override | Runs | The value set with SetReturn*; without one, what the original returns |
Action_Supersede (pre) | Does not run | The 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.
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.
// 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:
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
Findis the gamedata's function, shared with every plugin. Deleting the handle does not remove its hooks;Unhookor 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 want | Use |
|---|---|
| Damage, item pickup, use, movement, jumps, think, user commands, inputs, touch, weapon drops | Hook 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 away | HookPoint_ClientConnect — ctx.SetRejectReason(…), result = false, Action_Supersede |
| Clients joining and leaving, the frame | HookPoint_ClientPutInServer, HookPoint_ClientDisconnect (pre and post), HookPoint_GameFrame |
| Map start and end, plugins loading | The listener forwards (Source2Mod_OnLevelInit, …) |
| A console command — the game's or another plugin's | Source2ModCommands.HookConCommand; pre may answer Action_Supersede |
| Entities being created, spawned, removed; entity outputs | Source2ModEntities.AddEntityListener, AddEntityOutputListener |
| Calling a game function rather than hooking it | Source2ModFunction.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.