Hooking Messages
Reading, changing, redirecting and blocking outgoing and incoming net messages with Source2Mod's hooks, and the raw PostEventAbstract alternative.
Source2Mod hooks the engine's message paths once and gives every plugin a
callback on each. A callback sees the message before it goes out (or before
the server processes it), may read and change its fields, and returns an
Action: Action::Ignore lets it through, Action::Supersede drops it.
| macro | callback | sees |
|---|---|---|
HOOK_SERVER_MESSAGE(handler) | Action(uint64_t* clients, int messageid, void* msg) | every message the server posts to clients, with the recipient mask |
HOOK_CLIENT_MESSAGE(handler) | Action(CPlayerSlot slot, int messageid, void* msg) | every message a client sends to the server |
HOOK_SERVER_INTERNAL_MESSAGE(handler) | Action(CPlayerSlot slot, int messageid, void* msg) | messages as they are sent on one client's channel |
Each returns a Source2ModHookId and has an UNHOOK_*() counterpart. The handlers
are Source2ModCallbacks — a lambda (capturing this is fine), a free or static
function, or an object and a method (SOURCE2MOD_MEMBER(this, &MyPlugin::OnMessage)).
There is no plugin id to pass; the core reads which plugin a hook belongs to off
the handler.
Scripts hook the same three with Source2ModNetworkMessages.HookServerMessage,
HookClientMessage and HookServerInternalMessage, passing a named public
function that returns a Source2ModAction; the recipient mask is
int clients[2] there (slots 0–31, then 32–63), and the hooks return an int id.
Outgoing: HOOK_SERVER_MESSAGE
Every message the server posts through IGameEventSystem::PostEventAbstract —
user messages, sounds, temp entities, and the messages plugins send with
NET_MSG_SEND, yours included — passes through this hook, once, with the
recipients as a 64-bit mask (bit N = slot N).
- Change a field: set it on
msg. The clients get what is left when every plugin's hook has run. - Change the recipients: edit
*clients(clients[0]/clients[1]in a script). Clear a bit and that player does not get the message; the rest still do. - Block it for everybody: return
Action::Supersede.
This is the sample plugin, capping screen shakes and honouring a per-player
!noshake:
HOOK_SERVER_MESSAGE([this](uint64_t* clients, int messageid, void* msg) -> Action
{
if (messageid != UM_Shake)
return Action::Ignore;
// Changing a field: no shake stronger than sample_shake_max.
const float flMax = sample_shake_max.Get();
if (g_pSource2ModNetworkMessages->GetFloat(msg, "amplitude") > flMax)
g_pSource2ModNetworkMessages->SetFloat(msg, "amplitude", flMax);
// Changing the recipients: not to those who typed !noshake.
*clients &= ~m_NoShakeMask;
return Action::Ignore;
});// in OnPluginStart()
Source2ModNetworkMessages.HookServerMessage(OnServerMessage);
int g_NoShakeMask[2]; // filled by !noshake
public Source2ModAction OnServerMessage(int clients[2], int messageId, NetMessage msg)
{
if (messageId != UM_Shake)
return Action_Ignore;
// Changing a field: no shake stronger than sample_shake_max.
float max = sample_shake_max.FloatValue;
if (Source2ModNetworkMessages.GetFloat(msg, "amplitude") > max)
Source2ModNetworkMessages.SetFloat(msg, "amplitude", max);
// Changing the recipients: not to those who typed !noshake.
clients[0] &= ~g_NoShakeMask[0];
clients[1] &= ~g_NoShakeMask[1];
return Action_Ignore;
}The msg handle takes the same accessors as a message you allocated
(Setting fields), or cast it to the
generated class:
auto* pShake = static_cast<CUserMessageShake*>(static_cast<google::protobuf::Message*>(msg));
pShake->set_amplitude(flMax);The cast is C++ only — a script uses the accessors.
This hook runs for every message the server posts, many per tick. Check
messageid first and return Action::Ignore straight away for the ones you
do not handle. Do not keep msg after the callback returns — it belongs to
the engine.
Sounds the game starts (GE_SosStartSoundEvent) reach the sound hooks
(HOOK_SOUND, ISource2ModSounds) first, decoded;
what they leave of it is what this hook then sees, and a sound one of them
blocked never gets here.
Incoming: HOOK_CLIENT_MESSAGE
Every message a client sends reaches this hook before the server looks at it,
with the sender's slot. Action::Supersede drops the message — the server never
processes it. The sample plugin mutes players this way:
// The packets of a muted player are dropped before the server looks at them,
// so nobody hears them.
HOOK_CLIENT_MESSAGE([this](CPlayerSlot slot, int messageid, void* msg) -> Action
{
if (messageid == clc_VoiceData && ((m_VoiceMutedMask >> slot.Get()) & 1))
return Action::Supersede;
return Action::Ignore;
});// in OnPluginStart()
Source2ModNetworkMessages.HookClientMessage(OnClientMessage);
int g_VoiceMutedMask[2];
// The packets of a muted player are dropped before the server looks at them,
// so nobody hears them.
public Source2ModAction OnClientMessage(CPlayerSlot slot, int messageId, NetMessage msg)
{
if (messageId == clc_VoiceData && PlayerMask_Has(g_VoiceMutedMask, slot.Get()))
return Action_Supersede;
return Action_Ignore;
}clc_VoiceData comes from netmessages.pb.h. Fields of an incoming message
can be read the same way (GetString(msg, "command") on a net_StringCmd,
say) and changed before the server processes it — but treat everything a client
sends as untrusted input.
Per client: HOOK_SERVER_INTERNAL_MESSAGE
This one sits on CServerSideClientBase::SendNetMessage, the call that puts a
message on one client's network channel. The callback gets that client's slot,
so it is the place for per-recipient decisions on messages that do not come with
a recipient mask. Action::Supersede drops the message for that client only.
HOOK_SERVER_INTERNAL_MESSAGE([](CPlayerSlot slot, int messageid, void* msg) -> Action
{
if (messageid == svc_Print && g_bQuietConsole[slot.Get()])
return Action::Supersede;
return Action::Ignore;
});// in OnPluginStart()
Source2ModNetworkMessages.HookServerInternalMessage(OnInternalMessage);
bool g_bQuietConsole[64];
public Source2ModAction OnInternalMessage(CPlayerSlot slot, int messageId, NetMessage msg)
{
if (messageId == svc_Print && g_bQuietConsole[slot.Get()])
return Action_Supersede;
return Action_Ignore;
}Several handlers, several plugins
-
As many handlers as you like. A plugin may hook each kind as often as it wants; every handler runs. To remove one, pass the id the hook returned to
g_pSource2ModNetworkMessages->UnhookServerMessage(id)(and friends), or pass the same function or object and method again toUNHOOK_SERVER_MESSAGE(handler)— a lambda has no identity and only goes by id. A script unhooks by id only:Source2ModHookId id = HOOK_SERVER_MESSAGE([this](uint64_t* clients, int messageid, void* msg) -> Action { /* ... */ return Action::Ignore; }); g_pSource2ModNetworkMessages->UnhookServerMessage(id); HOOK_CLIENT_MESSAGE(SOURCE2MOD_MEMBER(this, &MyPlugin::OnClientMessage)); UNHOOK_CLIENT_MESSAGE(SOURCE2MOD_MEMBER(this, &MyPlugin::OnClientMessage));int id = Source2ModNetworkMessages.HookServerMessage(OnServerMessage); Source2ModNetworkMessages.UnhookServerMessage(id); int clientId = Source2ModNetworkMessages.HookClientMessage(OnClientMessage); Source2ModNetworkMessages.UnhookClientMessage(clientId); -
Removed on unload. Source2Mod drops what a plugin still has hooked when it unloads, so a forgotten unhook never leaves the engine calling into a closed library.
-
Every handler runs. Source2Mod calls all of them in turn with the same message — changes one plugin makes are visible to the next — and keeps the strongest answer: one
Supersedeblocks the message whatever the others returned. -
Pre only. There is no post callback; the message has not been sent (or processed) yet when your handler runs. Return
Action::IgnoreorAction::Supersede;Action::Overridehas no defined meaning for these hooks.
Reference: ISource2ModNetworkMessages.
The raw alternative: hooking PostEventAbstract
Source2Mod's hooks hand you a protobuf and a mask. When you need the engine's
full call — the INetworkMessageInternal*, the buffer type, the client count —
or want the typed message without the reflection layer, hook
IGameEventSystem::PostEventAbstract yourself with
KHOOK_VIRTUAL. The AntiCheat plugin does this to
read the server's own te_firebullets broadcast, which carries the weapon's
exact spread for each shot:
class CListeners
{
public:
KHook::Return<void> OnPostEventAbstract(IGameEventSystem* pThis, CSplitScreenSlot slot, bool bLocalOnly, int nClientCount, const uint64* clients, INetworkMessageInternal* event, const CNetMessage* data, unsigned long size, NetChannelBufType_t bufType);
private:
// PostEventAbstract is overloaded, so the member function pointer has
// to be disambiguated explicitly.
KHOOK_VIRTUAL(m_hPostEventAbstract, (static_cast<void (IGameEventSystem::*)(CSplitScreenSlot, bool, int, const uint64*, INetworkMessageInternal*, const CNetMessage*, unsigned long, NetChannelBufType_t)>(&IGameEventSystem::PostEventAbstract)), &globals::gameEventSystem, &CListeners::OnPostEventAbstract, nullptr);
};#include "cs_gameevents.pb.h"
KHook::Return<void> CListeners::OnPostEventAbstract(IGameEventSystem* pThis, CSplitScreenSlot slot, bool bLocalOnly, int nClientCount, const uint64* clients, INetworkMessageInternal* event, const CNetMessage* data, unsigned long size, NetChannelBufType_t bufType)
{
if (!event || !data)
return { KHook::Action::Ignore };
// Fires for every message the engine posts: check the id before doing any work.
NetMessageInfo_t* info = event->GetNetMessageInfo();
if (!info || info->m_MessageId != GE_FireBulletsId)
return { KHook::Action::Ignore };
const CMsgTEFireBullets& bullets = *data->ToPB<CMsgTEFireBullets>();
// ... read bullets ...
return { KHook::Action::Ignore };
}globals::gameEventSystem is set from api->GetGameEventSystem() in Load(),
before KHOOK_INIT(). data->ToPB<T>() is the way from the engine object to
the protobuf — never reinterpret_cast it. Returning
{ KHook::Action::Supersede } drops the message, like Source2Mod hook does.
For incoming messages the equivalent is a function hook on
ADDR_FILTER_MESSAGE() (INetworkMessageProcessingPreFilter::FilterMessage),
which is what Source2Mod itself hooks. The object is the client's pre-filter
sub-object; INetworkMessageProcessingPreFilterCustom
(source2mod/schema/serversideclient.h) reads the slot out of it:
KHook::Return<bool> Hook_FilterMessage(INetworkMessageProcessingPreFilterCustom* pThis, const CNetMessage* pData, INetChannel* pChannel);
KHOOK_MEMBER(m_hFilterMessage, ADDR_FILTER_MESSAGE(), &Plugin::Hook_FilterMessage, nullptr);Prefer the s2m hooks unless you need what only the raw call has: they are
already installed, cost nothing extra per plugin, and compose with every other
plugin's hooks on the same messages. The raw hooks are C++ only; scripts use
the Source2ModNetworkMessages hooks above.