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()), or nullptr.
  • 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) -- or nullptr.

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

FieldType
pszModuleconst char*
ppModuleISource2ModModule**
pszClassconst char*
pszBaseconst 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

FieldType
mfpMFP
pszNameconst 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.

ParameterType
pInstanceCLASS*

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.

ParameterTypeDefault
vtableISource2ModMemory
pModuleISource2ModModule*
pszClassconst char*
pszBaseconst char*nullptr
pszModuleconst 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.

ParameterType
pThisCLASS*
argsARGS...

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).

ParameterType
addressISource2ModMemory
pfnFunctionR (*)(A...)

Destruct

void Destruct() override;

IsInstalled

bool IsInstalled() const override;

Get

HookType* Get() const;

CallOriginal

RETURN CallOriginal(CLASS* pThis, ARGS... args);
ParameterType
pThisCLASS*
argsARGS...

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...));
ParameterType
addressISource2ModMemory
pfnFunctionR (*)(A...)

Destruct

void Destruct() override;

IsInstalled

bool IsInstalled() const override;

Get

HookType* Get() const;

CallOriginal

RETURN CallOriginal(ARGS... args);
ParameterType
argsARGS...

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...));
ParameterType
addressISource2ModMemory
pfnFunctionR (*)(A...)

Source2ModVirtualIndex

template <typename MFP> constexpr CSource2ModVirtualIndex<MFP> Source2ModVirtualIndex(MFP mfp, const char* pszName);
ParameterType
mfpMFP
pszNameconst 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

MacroExpands to
KHOOK_INIT()do { KHOOK_CHECK(); ::ISource2ModHook::InitAll(); } while (0)

On this page