ISource2ModHooks
One-line KHook hooks: declared, resolved, attached and taken down from a single place.
A raw KHook hook needs four places: the type in the header, new in the constructor, the address lookup plus Configure/Add in Init, and Remove/delete in Unload. The wrappers here fold all of that into one member declared with a macro in the class body:
class CHooks
{
public:
KHook::Return<void> OnPostThink(CCSPlayerPawn* pThis);
KHook::Return<void> OnGameFrame(ISource2Server* pThis, bool a, bool b, bool c);
KHook::Return<int> OnLoadEventsFromFile(IGameEventManager2* pThis, const char* pszFile, bool bSearchAll);
KHook::Return<void> OnRespawn(CCSPlayerController* pThis);
KHOOK_MEMBER(m_hPostThink, "CCSPlayerPawn::PostThink", &CHooks::OnPostThink, nullptr);
KHOOK_VIRTUAL(m_hGameFrame, &ISource2Server::GameFrame, &g_pSource2Server, nullptr, &CHooks::OnGameFrame);
KHOOK_VIRTUAL(m_hLoadEventsFromFile, &IGameEventManager2::LoadEventsFromFile, KHOOK_VTABLE("server", "CGameEventManager"), &CHooks::OnLoadEventsFromFile, nullptr);
KHOOK_VIRTUAL(m_hRespawn, "CCSPlayerController::Respawn", KHOOK_VTABLE("server", "CCSPlayerController"), &CHooks::OnRespawn, nullptr);
};The hook's type is taken from the handler's signature (the first parameter is the hooked class, the rest are the function's own arguments), so nothing is spelled twice and MSVC needs no CTAD. The handler has to be declared above the macro that names it. The context is the enclosing object.
KHOOK_INIT() in Load() (after SOURCE2MOD_SAVEVARS()) resolves and installs every hook declared this way; KHOOK_DESTRUCT() in Unload() takes them all down. A hook whose target is nullptr is left to the caller: m_hX.Init(address), m_hX.Init(pInstance) or m_hX.InitGlobal(vtable).
KHOOK_INIT() only sees the hooks that exist when it runs. A hook that is a member of an object created later (new CFeature() after Load()) registers itself but stays uninstalled until m_hX.Init() -- in that object's constructor, say -- resolves the target the macro was given. m_hX.Destruct() takes one hook down for good; destroying the owner does the same and unregisters it, so KHOOK_DESTRUCT() never touches a hook that is already gone.
Targets:
- KHOOK_MEMBER / KHOOK_FUNCTION -- a gamedata entry name (resolved through ISource2ModGameConfig::ResolveSignature), an ISource2ModMemory / void* / typed function pointer (
ADDR_TAKE_DAMAGE_OLD()-- what Source2Mod already resolved, evaluated at KHOOK_INIT()), ornullptr. - KHOOK_VIRTUAL -- the function is a member function pointer (index read from the vtable), an integer index, or a gamedata offset name; the target is a pointer to the instance pointer (
&g_pSource2Server, read at Init so it may still be null when the hook is constructed),KHOOK_VTABLE(module, class)for a whole vtable by RTTI name -- the module as a name or as&pModule(an ISource2ModModule* filled in at load) -- ornullptr.
KHOOK_NEW (ISource2ModTypes.h) is still there for a raw KHook object with none of this on top.
Header source2mod/ISource2ModHooks.h
Types
ResolvedType
template <typename RESOLVER> using ResolvedType = std::decay_t<decltype(Evaluate(RESOLVER{}()))>;VirtualHookFor
template <typename PRE, typename POST> using VirtualHookFor = typename MemberTraits<typename Callback<PRE, POST>::type>::Virtual;MemberHookFor
template <typename PRE, typename POST> using MemberHookFor = typename MemberTraits<typename Callback<PRE, POST>::type>::Member;FunctionHookFor
template <typename PRE, typename POST> using FunctionHookFor = typename FunctionTraits<typename Callback<PRE, POST>::type>::Function;ISource2ModHook
A hook the plugin declared through one of the KHOOK_* macros.
Every instance registers itself on construction; KHOOK_INIT() and KHOOK_DESTRUCT() walk that list.
Init
virtual bool Init() = 0;Resolves the target given at construction and installs the hook.
KHOOK_INIT() calls this on every hook that exists at that point; a hook constructed later is installed by calling it directly. Once installed it does nothing, and after a failure (an instance still null, say) it can simply be called again.
Returns bool — false when the target could not be resolved. A hook with no target (nullptr) is left alone and counts as success.
Destruct
virtual void Destruct() = 0;Detaches and deletes the underlying KHook object.
Deleting is what takes the detour down. Safe to call more than once, and final: the hook cannot be installed again afterwards. The wrapper's destructor calls it too, so destroying the owning object takes its hooks down.
IsInstalled
virtual bool IsInstalled() const = 0;Whether the hook is currently installed.
InitAll
static bool InitAll();Init() on every hook declared in this plugin.
Returns bool — false when any of them failed; the rest are installed anyway.
DestructAll
static void DestructAll();Destruct() on every hook declared in this plugin, last first.
Count
static int Count();How many hooks this plugin declared -- ISource2ModPlugin::GetRawHookCount().
CSource2ModVTableName
The vtable a global virtual hook attaches to: an RTTI class name inside a module ("server", "engine2", ...).
Fields
| Field | Type |
|---|---|
pszModule | const char* |
ppModule | ISource2ModModule** |
pszClass | const char* |
pszBase | const char* |
Constructors
CSource2ModVTableName() = default;
CSource2ModVTableName(const char* module, const char* name);
CSource2ModVTableName(ISource2ModModule** module, const char* name);
CSource2ModVTableName(const char* module, const char* name, const char* base);
CSource2ModVTableName(ISource2ModModule** module, const char* name, const char* base);The module by name -- loaded through ISource2ModModule::New at Init.
The module through a pointer to an ISource2ModModule* the plugin fills in at load (read at Init, so it may still be null now).
The table of a base class sub-object inside the class -- for an interface that is not its first base (ISource2ModModule::GetVirtualTableByBase).
CSource2ModVirtualIndex
A virtual hook's slot given both ways: the member function pointer the interface header compiles to, and the name of a gamedata offset for the same function. Gamedata wins at KHOOK_INIT(); a difference between the two is reported (one of them is stale), and a missing entry for this platform leaves the interface's index in place. KHOOK_GAMEDATA() makes one.
Fields
| Field | Type |
|---|---|
mfp | MFP |
pszName | const char* |
CSource2ModVirtualHook
Inherits ISource2ModHook
Constructors
template <typename FUNCTION, typename TARGET, typename CONTEXT, typename PRE, typename POST> CSource2ModVirtualHook(FUNCTION function, TARGET target, CONTEXT* pContext, PRE pre, POST post);Init
bool Init() override;
bool Init(CLASS* pInstance);Installs the hook on one instance.
| Parameter | Type |
|---|---|
pInstance | CLASS* |
InitGlobal
bool InitGlobal(ISource2ModMemory vtable);
bool InitGlobal(ISource2ModModule* pModule, const char* pszClass, const char* pszBase = nullptr);
bool InitGlobal(const char* pszModule, const char* pszClass, const char* pszBase = nullptr);Installs the hook on a whole vtable -- every object sharing it.
Installs the hook on the vtable of an RTTI class name in a module, or of its base pszBase.
Same, with the module loaded by name for the lookup.
| Parameter | Type | Default |
|---|---|---|
vtable | ISource2ModMemory | |
pModule | ISource2ModModule* | |
pszClass | const char* | |
pszBase | const char* | nullptr |
pszModule | const char* |
Destruct
void Destruct() override;IsInstalled
bool IsInstalled() const override;Get
HookType* Get() const;The KHook object itself, for anything the wrapper does not cover.
CallOriginal
RETURN CallOriginal(CLASS* pThis, ARGS... args);Runs the original on pThis, bypassing the hook chain.
| Parameter | Type |
|---|---|
pThis | CLASS* |
args | ARGS... |
HookType
using HookType = Source2ModKHook::Virtual<CLASS, RETURN, ARGS...>;CSource2ModMemberHook
A KHook::Member (a function with a this, found by address) plus where its address comes from.
Inherits ISource2ModHook
Constructors
template <typename TARGET, typename CONTEXT, typename PRE, typename POST> CSource2ModMemberHook(TARGET target, CONTEXT* pContext, PRE pre, POST post);Init
bool Init() override;
bool Init(ISource2ModMemory address);
template <typename R, typename... A> bool Init(R (*pfnFunction)(A...));Places the detour at an address.
Same, from a typed function pointer (an ADDR_* getter, say).
| Parameter | Type |
|---|---|
address | ISource2ModMemory |
pfnFunction | R (*)(A...) |
Destruct
void Destruct() override;IsInstalled
bool IsInstalled() const override;Get
HookType* Get() const;CallOriginal
RETURN CallOriginal(CLASS* pThis, ARGS... args);| Parameter | Type |
|---|---|
pThis | CLASS* |
args | ARGS... |
HookType
using HookType = KHook::Member<CLASS, RETURN, ARGS...>;CSource2ModFunctionHook
A KHook::Function (a free function, found by address) plus where its address comes from.
Inherits ISource2ModHook
Constructors
template <typename TARGET, typename CONTEXT, typename PRE, typename POST> CSource2ModFunctionHook(TARGET target, CONTEXT* pContext, PRE pre, POST post);Init
bool Init() override;
bool Init(ISource2ModMemory address);
template <typename R, typename... A> bool Init(R (*pfnFunction)(A...));| Parameter | Type |
|---|---|
address | ISource2ModMemory |
pfnFunction | R (*)(A...) |
Destruct
void Destruct() override;IsInstalled
bool IsInstalled() const override;Get
HookType* Get() const;CallOriginal
RETURN CallOriginal(ARGS... args);| Parameter | Type |
|---|---|
args | ARGS... |
HookType
using HookType = KHook::Function<RETURN, ARGS...>;Functions
ToMemory
inline ISource2ModMemory ToMemory(ISource2ModMemory address);
template <typename R, typename... A> inline ISource2ModMemory ToMemory(R (*pfnFunction)(A...));| Parameter | Type |
|---|---|
address | ISource2ModMemory |
pfnFunction | R (*)(A...) |
Source2ModVirtualIndex
template <typename MFP> constexpr CSource2ModVirtualIndex<MFP> Source2ModVirtualIndex(MFP mfp, const char* pszName);| Parameter | Type |
|---|---|
mfp | MFP |
pszName | const char* |
Source2ModRawHookCount
inline int Source2ModRawHookCount();See ISource2ModPlugin::GetRawHookCount().
Macros
KHOOK_VTABLE
#define KHOOK_VTABLE(module, name) ::CSource2ModVTableName{ module, name }A whole vtable as a virtual hook's target: an RTTI class name in a module -- the module's name ("server", "engine2", "tier0", ...) or a pointer to an ISource2ModModule* the plugin fills in at load (&modules::server).
KHOOK_VTABLE_BASE
#define KHOOK_VTABLE_BASE(module, name, base) ::CSource2ModVTableName{ module, name, base }The vtable of a base class sub-object inside an RTTI class: for an interface that is not the class's first base, whose table KHOOK_VTABLE cannot reach. The base's position comes from the RTTI. Needs a core with ISource2ModModule::GetVirtualTableByBase.
KHOOK_VIRTUAL
#define KHOOK_VIRTUAL(member, function, target, pre, post) ::source2modhook::VirtualHookFor<decltype(pre), decltype(post)> member { function, target, this, pre, post }Declares a virtual hook as a class member.
Which form to pick: a gamedata name (alone or with KHOOK_GAMEDATA) follows the core's gamedata, so an engine update that moves the slot is fixed by updating the core; a bare &Class::Method compiles the slot in from the SDK's header and needs a rebuild when it moves.
KHOOK_VIRTUAL(m_hGameFrame, &ISource2Server::GameFrame, &g_pSource2Server, nullptr, &Plugin::Hook_GameFrame);
KHOOK_VIRTUAL(m_hGameFrame, KHOOK_GAMEDATA(&ISource2Server::GameFrame, "ISource2Server::GameFrame"), &g_pSource2Server, nullptr, &Plugin::Hook_GameFrame);KHOOK_GAMEDATA
#define KHOOK_GAMEDATA(function, name) ::Source2ModVirtualIndex(function, name)A virtual hook's slot from the interface header and from gamedata at once, for KHOOK_VIRTUAL's function. See there.
KHOOK_MEMBER
#define KHOOK_MEMBER(member, target, pre, post) ::source2modhook::MemberHookFor<decltype(pre), decltype(post)> member { [] { return (target); }, this, pre, post }Declares a hook on a member function found by address.
The target is evaluated at KHOOK_INIT(), not here, so an expression that needs Source2Mod -- ADDR_TAKE_DAMAGE_OLD(), a GAMECONFIG_RESOLVE() -- is fine even though the owner is usually a global constructed long before Load().
KHOOK_MEMBER(m_hPostThink, "CCSPlayerPawn::PostThink", &Plugin::Hook_PostThink, nullptr);
KHOOK_MEMBER(m_hTakeDamageOld, ADDR_TAKE_DAMAGE_OLD(), &Plugin::Hook_TakeDamageOld, nullptr);KHOOK_FUNCTION
#define KHOOK_FUNCTION(member, target, pre, post) ::source2modhook::FunctionHookFor<decltype(pre), decltype(post)> member { [] { return (target); }, this, pre, post }Declares a hook on a free function found by address. Same parameters as KHOOK_MEMBER; the handler takes no hooked object.
KHOOK_CHECK
#define KHOOK_CHECK() \
do {
const char* source2modKHookNote = nullptr;
if (!::Source2ModKHookVersionMatches(g_Source2ModCoreKHookHeader, g_Source2ModCoreKHookCommit, error, maxlen, &source2modKHookNote))
return false;
if (source2modKHookNote)
g_Source2ModAPI->Log(g_PluginAPI, "%s\n", source2modKHookNote);
} while (0)Installs every hook declared with the macros above. Call in Load(), after SOURCE2MOD_SAVEVARS() and once the instance pointers hooks read are set.
This is also where the KHook check happens: a plugin that hooks through KHook shares the detour engine with the core and must have been built against the same khook.hpp (IKHook and the template layout; a KHook update confined to its src/ does not count), so a mismatch returns false out of Load() with the reason in error. A plugin that never calls this is not checked -- it does not hook, so it does not care. A plugin using raw KHook objects (KHOOK_NEW) without any KHOOK_* macro must call KHOOK_CHECK() itself before the first one.
Each hook that could not be resolved is logged and skipped.
KHOOK_DESTRUCT
#define KHOOK_DESTRUCT() ::ISource2ModHook::DestructAll()Takes every hook down. Call in Unload().
Other macros
| Macro | Expands to |
|---|---|
KHOOK_INIT() | do { KHOOK_CHECK(); ::ISource2ModHook::InitAll(); } while (0) |