Source2Toolkit SDK is the core development kit required for building plugins for Source2Toolkit.
It bundles everything you need — headers, SDK, hooking system and build helpers — so you can focus purely on writing your plugin.
👉 Docs & guides: https://www.source2toolkit.net
Source2Toolkit SDK is a lightweight development layer that provides:
- Preconfigured HL2SDK (CS2)
- Integrated KHook hooking (virtual, vtable and function detours), shared with Metamod
- Ready-to-use Source 2 headers & interfaces
- Cross-platform build configuration
- Simple plugin build system
No setup. No hunting dependencies. Just build.
- Zero setup – everything included (SDK, hooks, protobufs)
- Fast builds – optimized CMake configuration
- Clean integration – designed specifically for Source2Toolkit
- Cross-platform – Windows & Linux support out of the box
- Minimal boilerplate – create plugins in seconds
👉 Full docs: https://www.source2toolkit.net
git submodule add https://github.com/SlynxCZ/source2toolkit-sdk.git
git submodule update --init --recursivecmake_minimum_required(VERSION 3.18)
project(my-plugin CXX)
add_subdirectory(source2toolkit-sdk)
add_s2toolkit_plugin(my_plugin plugin.cpp)mkdir build && cd build
cmake ..
cmake --build . --config ReleaseOutput:
my_plugin.stx
- HL2SDK-CS2 (as submodule)
- KHook (virtual, vtable & function hooks) -- as the
vendor/khooksubmodule, headers only - Protobuf definitions
- Tier0 / Tier1 / Mathlib
- Schema system headers
- Preconfigured compiler flags & linking
Hooks go through KHook, Metamod:Source's own detour library. The toolkit is
a Metamod plugin and gets the engine handed to it at load; TOOLKIT_SAVEVARS()
fetches that same engine for your plugin (ToolkitFactory(TOOLKIT_KHOOK_INTERFACE)),
so every hook on the server -- Metamod's, the toolkit's, every plugin's -- runs
on one instance.
The full guide, with examples, is at source2toolkit.net/docs/hooking.
The headers are the SDK's own vendor/khook submodule (git submodule update --init --recursive); nothing of KHook is built and no metamod-source checkout
is needed. The SDK stamps the submodule's commit into your binary as
TOOLKIT_KHOOK_COMMIT, the core reports the commit it was built with
(ToolkitFactory(TOOLKIT_KHOOK_VERSION_INTERFACE)), and TOOLKIT_SAVEVARS()
refuses the load if the two differ -- a plugin built against one KHook never
hooks through another. Nothing to set up on your side:
TOOLKIT_EXPOSE(MyPlugin, g_MyPlugin);
bool MyPlugin::Load(PluginId id, IToolkitAPI* api, char* error, size_t maxlen, bool late)
{
TOOLKIT_SAVEVARS(); // KHook is live from here on
...
}A hook is one line in your class, written with a KHOOK_* macro from
IToolkitHooks.h. The macro takes the hook's type from the handler it names
(so the handler is declared above it), the context is the enclosing object,
and the last two arguments are the Pre and Post callbacks -- nullptr for the
side you do not need. KHOOK_INIT() in Load() resolves and installs every
hook the plugin declared, KHOOK_DESTRUCT() in Unload() takes them down; a
hook that cannot be resolved is logged and skipped.
class MyPlugin final : public IToolkitPlugin
{
KHook::Return<void> Hook_ClientCommand(ISource2GameClients* pThis, CPlayerSlot slot, const CCommand& args);
KHook::Return<int64_t> Hook_TakeDamageOld(CBaseEntity* pThis, CTakeDamageInfo* pInfo, CTakeDamageResult* pResult);
KHOOK_VIRTUAL(m_hClientCommand, &ISource2GameClients::ClientCommand, &g_pSource2GameClients, &MyPlugin::Hook_ClientCommand, nullptr);
KHOOK_MEMBER(m_hTakeDamageOld, ADDR_TAKE_DAMAGE_OLD(), &MyPlugin::Hook_TakeDamageOld, nullptr);
};
bool MyPlugin::Load(...) { TOOLKIT_SAVEVARS(); ...; KHOOK_INIT(); return true; }
bool MyPlugin::Unload(...) { KHOOK_DESTRUCT(); return true; }KHOOK_VIRTUAL(member, function, target, pre, post). The function is
&Class::Method (index read from the pointer), an integer index, or a
gamedata offset name; the target is &pInstance (read at KHOOK_INIT(), so
the global may still be null now), KHOOK_VTABLE(module, class) for a whole
vtable by RTTI name (module as a name or as &pModule),
KHOOK_VTABLE_BASE(module, class, base) for the table of a base that is not
the class's first one, or nullptr to attach it yourself with
m_hX.Init(pInstance) / m_hX.InitGlobal(vtable):
KHOOK_VIRTUAL(m_hSendNetMessage, &CServerSideClientBase::SendNetMessage, KHOOK_VTABLE("engine2", "CServerSideClient"), &MyPlugin::Hook_SendNetMessage, nullptr);
KHOOK_VIRTUAL(m_hRecordingStarted, &IHLTVDirector::OnRecordingStarted, KHOOK_VTABLE_BASE("server", "CCSHLTVDirector", "IHLTVDirector"), nullptr, &MyPlugin::Hook_RecordingStarted);
KHOOK_VIRTUAL(m_hRespawn, "CCSPlayerController::Respawn", KHOOK_VTABLE("server", "CCSPlayerController"), &MyPlugin::Hook_Respawn, nullptr);A missing gamedata offset (-1) is refused rather than installed one slot before the vtable.
Anything a signature scan finds is hookable -- KHOOK_MEMBER when the
function has a this, KHOOK_FUNCTION when it does not. The target is a
gamedata entry name (resolved through IToolkitGameConfig::ResolveSignature,
by symbol or by pattern), an IToolkitMemory, an ADDR_* getter (what the toolkit already
resolved, no second scan -- the target is evaluated at KHOOK_INIT(), not when
the object is constructed), or nullptr for m_hX.Init(address) later:
KHOOK_MEMBER(m_hPostThink, "CCSPlayerPawn::PostThink", &MyPlugin::Hook_PostThink, nullptr);
KHOOK_MEMBER(m_hTakeDamageOld, ADDR_TAKE_DAMAGE_OLD(), &MyPlugin::Hook_TakeDamageOld, nullptr);The member forwards -> to the KHook object underneath, so
m_hX->CallOriginal(pThis, ...) and the rest of KHook are there as before. A
raw KHook object is still an option (KHOOK_NEW in IToolkitTypes.h), with
the attaching, detaching and deleting left to you.
KHOOK_INIT() installs the hooks that exist when it runs. A hook that is a
member of an object you create afterwards registers itself but is not
installed -- call m_hX.Init(), in that object's constructor for instance. It
resolves the target the macro was given, returns false (and logs) when it
cannot, and can simply be called again once the instance exists.
m_hX.Destruct() takes one hook down for good, and so does destroying the
owner: the wrapper's destructor removes the hook and unregisters it, so
KHOOK_DESTRUCT() never touches a hook that is already gone.
class CChatFilter
{
public:
CChatFilter() { m_hClientCommand.Init(); } // KHOOK_INIT() ran long ago
KHook::Return<void> Hook_ClientCommand(ISource2GameClients* pThis, CPlayerSlot slot, const CCommand& args);
KHOOK_VIRTUAL(m_hClientCommand, &ISource2GameClients::ClientCommand, &g_pSource2GameClients, &CChatFilter::Hook_ClientCommand, nullptr);
};
auto* pFilter = new CChatFilter(); // hook installed here
delete pFilter; // and removed hereInstall and remove hooks on the game thread, and never destroy an owner from inside one of its own handlers.
A handler takes the hooked object first, then the function's own parameters,
and returns KHook::Return<T> -- the action, plus the return value when the
function has one:
KHook::Return<bool> MyPlugin::Hook_ClientConnect(ISource2GameClients* pThis, CPlayerSlot slot, const char* pszName, uint64 xuid, const char* pszNetworkID, bool unk1, CBufferString* pRejectReason)
{
if (!V_strcmp(pszName, "rejected"))
return { KHook::Action::Override, false };
return { KHook::Action::Ignore, true };
}CallOriginal(pThis, args...) runs the original from inside a handler,
bypassing the chain; return Supersede afterwards so it does not run twice.
KHook::Action |
meaning |
|---|---|
Ignore |
did nothing |
Override |
original runs, your return value wins |
Supersede |
original is skipped entirely |
The toolkit's own listener callbacks (RegisterConListener, HookGameEvent,
net message hooks, entity output listeners) return the same type: Action
(IToolkitTypes.h) is an alias of KHook::Action, so a KHook handler hands a
listener's answer straight through. Timing there is a plain bool post:
false runs before the original, true after.
add_s2toolkit_plugin(my_plugin plugin.cpp)Automatically:
- Links SDK
- Sets correct output (
.stx) - Applies all required flags
- CMake 3.18+
- C++20 compiler
- Source2Toolkit installed on server
- Docs: https://www.source2toolkit.net
- Getting Started: https://www.source2toolkit.net/docs
- API Reference: https://www.source2toolkit.net/docs
This project is licensed under the GNU General Public License v3.0, with a linking exception for Valve's engines and games and a dual-licensing exception for derivative works. See LICENSE_INFO.txt for the terms and LICENSE for the full GPLv3 text.