Function Hooks
HookPoint::Function — hooking any game function by gamedata entry, address, signature, symbol or vtable slot through ISource2ModFunctions; SourceMod's DHooks.
What has no typed point of its own is still one
call away. HookPoint::Function(fn) hooks any function an
ISource2ModFunction describes: where it is and what it takes. The same
object is what you call a game function
through, so a function described once can be called and hooked. This is
SourceMod's DHooks.
#include "source2mod/ISource2ModFunctions.h"
#include "source2mod/ISource2ModHooks.h"
// A gamedata entry with a "function" object: Find() and nothing else.
if (ISource2ModFunction* pSwitchTeam = g_pSource2ModFunctions->Find("CCSPlayerController::SwitchTeam"))
g_pSource2ModHooks->Hook(HookPoint::Function(pSwitchTeam), HookTarget::All(), SOURCE2MOD_MEMBER(this, &MyPlugin::OnSwitchTeam));
HookReturn<void> MyPlugin::OnSwitchTeam(FunctionContext& ctx, bool post)
{
// `this` is argument 0 of a member function, the team 1.
auto* pController = ctx.GetArg<CCSPlayerController*>(0);
const int nTeam = ctx.GetArg<int>(1);
if (pController && pController->IsBot() && nTeam == 3)
return Action::Supersede; // bots stay where they are
return Action::Ignore;
}public void OnPluginStart()
{
// A gamedata entry with a "function" object: Find and nothing else.
Source2ModFunction switchTeam = Source2ModFunction.Find("CCSPlayerController::SwitchTeam");
if (switchTeam != null)
Source2ModHooks.HookFunction(switchTeam, Hook_SwitchTeam);
}
public Source2ModAction Hook_SwitchTeam(FunctionContext ctx, bool post)
{
// `this` is argument 0 of a member function, the team 1.
CCSPlayerController controller = view_as<CCSPlayerController>(ctx.GetEntity(0));
if (controller != NULL_ENTITY && controller.IsBot() && ctx.GetInt(1) == 3)
return Action_Supersede; // bots stay where they are
return Action_Ignore;
}Where the function comes from
C++ (g_pSource2ModFunctions->) | SourcePawn (Source2ModFunction.) | The function |
|---|---|---|
Find(name) | Find(name) | A gamedata entry with a "function" object — the core's or one you added. The types come from the gamedata. |
FromGameConfig(owner, name, types) | FromGameConfig(name, returnType) + AddParam | A gamedata entry without one, by its signature, symbol or — a virtual — offset. |
FromSignature(owner, module, signature, types, section) | FromSignature(module, signature, returnType, section) | A pattern of your own in a module ("server"), optionally in one section (".text"). |
FromSignatureRelative(owner, module, signature, offset, nextInstructionOffset, types, section) | FromSignatureRelative(...) | The target of a call / jmp / lea the pattern matches. |
FromSymbol(owner, module, symbol, types) | FromSymbol(module, symbol, returnType) | An exported or debug symbol. |
FromVirtualTable(owner, module, className, index, types, baseName) | FromVirtualTable(module, vtableName, vtableIndex, returnType, baseName) | Slot index of one class's vtable (an RTTI name); baseName picks a secondary base's vtable. See Virtual functions. |
FromVirtual(owner, index, types) | FromVirtual(vtableIndex, returnType) | Slot index of whatever object it is called or hooked on — an engine interface, say. Hook it with an Instance or Class target. |
Create(owner, address, vtable, index, isMember, ret, params, count) | FromAddress(address, returnType) | An address you or the core resolved (an ADDR_* getter, Source2ModAddress). |
Find returns a function owned by the core, valid until the server shuts
down. The others are the plugin's (owner is g_PluginID in C++): freed with
Destroy() or when it unloads. Each returns null when the function is not
found; the reason is on the console.
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. See
Gamedata for writing entries and finding slots.
Describing the types
A function from Find is described by its gamedata "function" object —
see Calling game functions.
Every other one is described where it is created: in C++ a FunctionTypes
(return type, parameter types with this first for a member function, their
count, whether it is a member), in SourcePawn one AddParam per parameter,
this first.
// CBasePlayerWeapon* CCSPlayer_ItemServices::GiveNamedItem(const char*, int, CEconItemView*, bool, void*)
static const Source2ModType kGiveNamedItem[] = { Source2ModType::Pointer, Source2ModType::String, Source2ModType::Int32,
Source2ModType::Pointer, Source2ModType::Bool, Source2ModType::Pointer };
ISource2ModFunction* pGiveNamedItem = g_pSource2ModFunctions->FromGameConfig(g_PluginID, "CCSPlayer_ItemServices::GiveNamedItem",
FunctionTypes{ Source2ModType::Entity, kGiveNamedItem, 6, true });
g_pSource2ModHooks->Hook(HookPoint::Function(pGiveNamedItem), HookTarget::All(), SOURCE2MOD_MEMBER(this, &MyPlugin::OnGiveNamedItem));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_GiveNamedItem);
}Use Entity for this when it is an entity (a script then gets an entity
handle), Object when it is a schema object (services), Pointer for
anything else.
Source2ModType | C++ | In a script's 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 | GetEntity, SetEntity |
Object | a schema object pointer | 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 writes in place |
Vector, VectorValue | Vector* / const Vector&, Vector by value | GetVector; SetVector |
Command | const CCommand& | GetCommandArgs — Arg(0) is the command; a function with one can be hooked, not called |
IntRef, BoolRef, FloatRef | int32*, bool*, float* out-parameters | the value it points at |
Arguments that do not fit in registers are read from the stack: on Linux six integer-class and eight floating-point arguments go in registers, then eight more of either kind on the stack; on Windows at most fourteen in all.
The handler
A Function point's handler gets a FunctionContext: the arguments by
index, as the function describes them — this of a member function is
index 0 — and the return value.
| C++ | SourcePawn | |
|---|---|---|
ctx.This() | ctx.GetEntity(0) / GetObject(0) | this of a member function, the first argument otherwise |
ctx.GetArg<T>(i) / SetArg<T>(i, v) | GetInt, GetFloat, GetEntity, GetString, GetVector, … / Set* | an argument; a Vector or string argument is read as a pointer in C++ (GetArg<Vector*>) |
ctx.GetReturn<T>() / SetReturn<T>(v) | GetReturnInt, GetReturnEntity, … / SetReturn* | the return value |
ctx.CallOriginal<T>() | ctx.CallOriginal() | the function now, with the current arguments, past every hook on it |
SetArgin a pre handler changes what the original — and the later handlers — get, whatever the answer (Ignoretoo; withSupersedethe original does not run). This is the way to change an argument passed by value.OverridewithoutSetReturnmeans "run the original with these arguments"; the caller gets what it returns.SetReturnwithOverride/Supersedeis what the caller gets. In a post handlerGetReturnis what the original returned.CallOriginalin a pre handler really runs the function: answerSupersedeafterwards (withSetReturn), or the game calls it a second time.
HookReturn<void> MyPlugin::OnGiveNamedItem(FunctionContext& ctx, bool post)
{
const char* pszItem = ctx.GetArg<const char*>(1);
if (pszItem && !V_strcmp(pszItem, "weapon_awp"))
{
ctx.SetReturn<CBasePlayerWeapon*>(nullptr); // the caller gets no weapon
return Action::Supersede; // and the game's function does not run
}
return Action::Ignore;
}public Source2ModAction Hook_GiveNamedItem(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
}A Function handler returns HookReturn<void> in C++: the value goes through
SetReturn, the action alone through the return.
Targets
HookTarget::All() (HookFunction) is every call. Instance(object)
(HookFunctionInstance) and Class(name) (HookFunctionClass) narrow it to
calls on one object or one class; for a free function the object is its first
argument. A virtual needs a vtable to hook: one from FromVirtualTable, or an
Instance / Class target — see Virtual functions.
// One client's string commands.
ISource2ModFunction* fn = g_pSource2ModFunctions->Find("CServerSideClient::ExecuteStringCommand");
g_pSource2ModHooks->Hook(HookPoint::Function(fn), HookTarget::Instance(pClient), OnStringCommand);When the hook goes in
A Function point's hook is inserted immediately: the next call runs your
handler. KHook falls back to its worker thread only when the function is
running at that very moment. Unhook(id) takes it out again; what is still
hooked when the plugin unloads goes with it.
In SourcePawn a Source2ModFunction is a handle. Deleting one from Find
does not remove its hooks — the gamedata function is shared, and Unhook
or the plugin unloading takes them down. Deleting a function the script owns
(From*) drops the hooks on it.
Reference
ISource2ModFunctions—Find,Create,From*,FunctionTypes,Source2ModTypeISource2ModHooks—HookPoint::Function,FunctionContextsource2mod/gameconfig.inc(Source2ModFunction),source2mod/hooks.inc(FunctionContext,HookFunction*)
Virtual Functions
Hooking a vtable slot with ISource2ModHooks — on one object, on every object of a class, or on an engine interface — and the vtable caveats that come with it.
Writing Handlers
Handler signatures, contexts, HookReturn and the three actions, calling the original, changing arguments and return values, ids, owners and recursion.