Sending Messages
Allocating a net message by id or name, filling its protobuf fields, and sending it to one player, a set of players or everyone.
Sending a message is four steps: allocate it, set its fields, send it, free it. The message stays yours throughout — sending does not consume it, so one message can go out as many times as you like before you free it.
#include "usermessages.pb.h" // UM_Shake
REGISTER_CON_COMMAND("sample_shake", [](const Source2ModCommandContext& context, const Source2ModCommandArgs& args, bool post)
{
void* pMsg = NET_MSG_ALLOC_BY_ID(UM_Shake);
if (!pMsg)
return;
// CUserMessageShake, usermessages.proto
g_pSource2ModNetworkMessages->SetUInt32(pMsg, "command", 0); // SHAKE_START
g_pSource2ModNetworkMessages->SetFloat(pMsg, "amplitude", args.ArgC() >= 2 ? strtof(args.Arg(1), nullptr) : 10.0f);
g_pSource2ModNetworkMessages->SetFloat(pMsg, "frequency", 150.0f);
g_pSource2ModNetworkMessages->SetFloat(pMsg, "duration", 2.0f);
const CPlayerSlot slot = context.GetPlayerSlot();
if (slot.IsValid())
NET_MSG_SEND(pMsg, UM_Shake, slot); // the caller only
else
NET_MSG_SEND_PLAYERS(pMsg, UM_Shake, ~0ull); // from the console: everybody
NET_MSG_FREE(pMsg);
});public void OnPluginStart()
{
Source2ModCommands.RegisterConCommand("sample_shake", Cmd_Shake);
}
public void Cmd_Shake(Source2ModCommandContext context, Source2ModCommandArgs args, bool post)
{
NetMessage msg = Source2ModNetworkMessages.AllocateNetMessageByID(UM_Shake);
if (msg == null)
return;
char amplitude[16];
if (args.ArgC() >= 2)
args.Arg(1, amplitude, sizeof(amplitude));
// CUserMessageShake, usermessages.proto
Source2ModNetworkMessages.SetUInt32(msg, "command", 0); // SHAKE_START
Source2ModNetworkMessages.SetFloat(msg, "amplitude", amplitude[0] ? Source2ModUtils.StringToFloat(amplitude) : 10.0);
Source2ModNetworkMessages.SetFloat(msg, "frequency", 150.0);
Source2ModNetworkMessages.SetFloat(msg, "duration", 2.0);
CPlayerSlot slot = context.GetPlayerSlot();
if (slot.IsValid())
{
Source2ModNetworkMessages.SendMessage(msg, UM_Shake, slot); // the caller only
}
else
{
int everybody[2];
PlayerMask_All(everybody);
Source2ModNetworkMessages.SendMessageToPlayers(msg, UM_Shake, everybody); // from the console: everybody
}
Source2ModNetworkMessages.DeallocateNetMessage(msg);
}That is the sample plugin's sample_shake command, and it is the whole
pattern.
Allocating
| macro | method | |
|---|---|---|
NET_MSG_ALLOC_BY_ID(id) | AllocateNetMessageByID(int msgid) | by the message id — preferred |
NET_MSG_ALLOC_BY_NAME(name) | AllocateNetMessageByPartialName(const char* name) | by protobuf class name, partial match |
NET_MSG_FREE(msg) | DeallocateNetMessage(void* msg) | frees what either returned |
Both return nullptr when no message matches. The returned void* is an opaque
handle — it is the google::protobuf::Message itself, and every field accessor
takes it. Always free it with NET_MSG_FREE, never with delete: the engine
object around the protobuf starts at a different address.
In a script the same calls are static methods of Source2ModNetworkMessages
and return a NetMessage handle (null when no message matches); free it with
DeallocateNetMessage (or delete).
Allocating by name is a partial match — see Ids and names — and you still need the id to send, so allocating by id is simpler whenever you have the enum.
Setting fields
Fields are set by their .proto names through reflection. There is a getter
and setter family per type:
type in the .proto | methods |
|---|---|
int32, sint32, sfixed32, enums | GetInt32 / SetInt32 / AddInt32 |
int64, sint64, sfixed64 | GetInt64 / SetInt64 / AddInt64 |
uint32, fixed32 | GetUInt32 / SetUInt32 / AddUInt32 |
uint64, fixed64 | GetUInt64 / SetUInt64 / AddUInt64 |
bool | GetBool / SetBool / AddBool |
float | GetFloat / SetFloat / AddFloat |
double | GetDouble / SetDouble / AddDouble |
string | GetString / SetString / AddString |
bytes | GetBytes / SetBytes / AddBytes |
CMsgVector | GetVector / SetVector / AddVector |
CMsgVector2D | GetVector2D / SetVector2D / AddVector2D |
CMsgQAngle | GetQAngle / SetQAngle / AddQAngle |
CMsgRGBA | GetColor / SetColor / AddColor |
| any other nested message | GetNestedMessage / AddNestedMessage |
Every family also has GetRepeatedX(msg, field, index) and
SetRepeatedX(msg, field, index, value) for an existing element of a
repeated field; AddX appends one.
Pick the method by the field's C++ type. An enum field goes through
SetInt32, which checks the value against the enum and ignores one it does not
know. A fixed32 colour (as in CUserMessageFade) is a uint32, not a
CMsgRGBA — SetColor is only for fields declared as CMsgRGBA.
A field name the message does not have is ignored (a getter returns 0 or an
empty value), and so is a singular setter on a repeated field or the other
way round. If a message arrives empty on the client, check the names and the
repeated markers against the .proto.
A script has the same families on Source2ModNetworkMessages. There 64-bit
fields are decimal text, double fields go through float, and vectors,
angles and colours are arrays (float[3], int[4]).
Repeated fields
// CUserMessageTextMsg: optional uint32 dest = 1; repeated string param = 2;
void* pMsg = NET_MSG_ALLOC_BY_ID(UM_TextMsg);
g_pSource2ModNetworkMessages->SetUInt32(pMsg, "dest", 3); // chat
g_pSource2ModNetworkMessages->AddString(pMsg, "param", "Hello!"); // param[0]
int count = g_pSource2ModNetworkMessages->GetRepeatedFieldSize(pMsg, "param");
g_pSource2ModNetworkMessages->ClearRepeatedField(pMsg, "param");// CUserMessageTextMsg: optional uint32 dest = 1; repeated string param = 2;
NetMessage msg = Source2ModNetworkMessages.AllocateNetMessageByID(UM_TextMsg);
Source2ModNetworkMessages.SetUInt32(msg, "dest", 3); // chat
Source2ModNetworkMessages.AddString(msg, "param", "Hello!"); // param[0]
int count = Source2ModNetworkMessages.GetRepeatedFieldSize(msg, "param");
Source2ModNetworkMessages.ClearRepeatedField(msg, "param");Nested messages
GetNestedMessage returns a handle to a singular sub-message (creating it if
it was not set), AddNestedMessage appends one to a repeated message field and
returns it, GetRepeatedNestedMessage returns an existing element. The
returned handle takes the same accessors:
// CUserMessageShakeDir: optional CUserMessageShake shake = 1; optional CMsgVector direction = 2;
void* pMsg = NET_MSG_ALLOC_BY_ID(UM_ShakeDir);
void* pShake = g_pSource2ModNetworkMessages->GetNestedMessage(pMsg, "shake");
g_pSource2ModNetworkMessages->SetFloat(pShake, "amplitude", 8.0f);
g_pSource2ModNetworkMessages->SetFloat(pShake, "duration", 1.0f);
g_pSource2ModNetworkMessages->SetVector(pMsg, "direction", Vector(0.0f, 0.0f, 1.0f));// CUserMessageShakeDir: optional CUserMessageShake shake = 1; optional CMsgVector direction = 2;
NetMessage msg = Source2ModNetworkMessages.AllocateNetMessageByID(UM_ShakeDir);
NetMessage shake = Source2ModNetworkMessages.GetNestedMessage(msg, "shake");
Source2ModNetworkMessages.SetFloat(shake, "amplitude", 8.0);
Source2ModNetworkMessages.SetFloat(shake, "duration", 1.0);
float direction[3] = { 0.0, 0.0, 1.0 };
Source2ModNetworkMessages.SetVector(msg, "direction", direction);Nested handles belong to their parent — free only the top-level message.
HasField(msg, field) tells whether a singular field is set, and Clear(msg)
resets every field, which is handy to reuse one allocation for different
contents.
Using the generated class
The handle is the protobuf itself, so if you prefer the generated class over field names, cast it:
#include "usermessages.pb.h"
auto* pShake = static_cast<CUserMessageShake*>(static_cast<google::protobuf::Message*>(pMsg));
pShake->set_amplitude(10.0f);Cast through google::protobuf::Message* exactly like that. Casting the handle
straight to an engine type (CNetMessage*) is wrong — the protobuf sits at an
offset inside the engine object.
C++ only — a script always goes through the field accessors above.
Sending
| macro | method | recipients |
|---|---|---|
NET_MSG_SEND(msg, id, slot) | SendMessage(void* msg, int msgid, CPlayerSlot slot) | one player slot |
NET_MSG_SEND_PLAYERS(msg, id, mask) | SendMessageToPlayers(void* msg, int msgid, uint64_t playermask) | a 64-bit mask, bit N = slot N |
The id must be the message's own id — the engine serialises the message by
the type it looks up from that id. Both go out reliably.
In a script the mask is two cells, int mask[2] (slots 0–31, then 32–63),
built with PlayerMask_Add / PlayerMask_All from source2mod/types.inc.
One player
if (CCSPlayerController* pPlayer = CCSPlayerController::FromSlot(iSlot))
NET_MSG_SEND(pMsg, UM_Shake, CPlayerSlot(iSlot));if (CCSPlayerController.FromSlot(CPlayerSlot(slot)) != NULL_ENTITY)
Source2ModNetworkMessages.SendMessage(msg, UM_Shake, CPlayerSlot(slot));Several players
Build the mask from slots:
uint64_t mask = 0;
for (int i = 0; i < 64; ++i)
{
CCSPlayerController* pPlayer = CCSPlayerController::FromSlot(i);
if (!pPlayer || pPlayer->m_iConnected() != PlayerConnectedState::Connected)
continue;
if (pPlayer->m_iTeamNum() == 3) // counter-terrorists only
mask |= uint64_t(1) << i;
}
if (mask)
NET_MSG_SEND_PLAYERS(pMsg, UM_Fade, mask);int mask[2]; // bit N of the 64 = slot N, two cells
for (int i = 0; i < 64; ++i)
{
CCSPlayerController player = CCSPlayerController.FromSlot(CPlayerSlot(i));
if (player == NULL_ENTITY || player.m_iConnected != 0) // 0: PlayerConnectedState::Connected
continue;
if (player.m_iTeamNum == 3) // counter-terrorists only
PlayerMask_Add(mask, i);
}
if (mask[0] || mask[1])
Source2ModNetworkMessages.SendMessageToPlayers(msg, UM_Fade, mask);~0ull sends to every slot (PlayerMask_All(mask) in a script).
Recipient filters
source2mod/schema/recipientfilter.h has CRecipientFilter and
CSingleRecipientFilter, the engine's IRecipientFilter implementations. The
Source2Mod's send functions do not need them — they build one from your slot or
mask — but the SDK helpers that take an IRecipientFilter* (such as
CBaseEntity::DispatchParticleEffect) and raw PostEventAbstract calls do:
#include "source2mod/schema/recipientfilter.h"
CRecipientFilter filter; // reliable by default
filter.AddRecipient(CPlayerSlot(2));
filter.AddRecipient(CPlayerSlot(5));
CRecipientFilter everyone;
everyone.SetFromBitmask(~0ull);
CSingleRecipientFilter one(CPlayerSlot(2));C++ only — a script's send calls take a slot or a mask and need no filter.
Things to know
- Your hooks see your messages. A message sent with
NET_MSG_SENDgoes throughPostEventAbstract, so every plugin'sHOOK_SERVER_MESSAGE— yours included — sees it and may change or block it. Do not send the same message type from inside your own server hook without a guard, or it recurses. - Send on the game thread. Commands, timers, game event callbacks and hooks on game functions are all fine.
- Free what you allocate, also on the early-return paths. A message that is sent once per tick and never freed is a leak per tick.
The raw way
Source2Mod wraps the engine's own interfaces, which you can also use directly
— FUNPLAY's plugins do, with the generated classes. ISource2ModAPI hands out
GetNetworkMessages() (INetworkMessages*) and GetGameEventSystem()
(IGameEventSystem*):
#include "cstrike15_usermessages.pb.h"
#include "source2mod/schema/recipientfilter.h"
INetworkMessageInternal* pNetMsg = g_Source2ModAPI->GetNetworkMessages()->FindNetworkMessagePartial("CCSUsrMsg_Shake");
if (!pNetMsg)
return;
CNetMessage* pData = pNetMsg->AllocateMessage();
auto* pShake = pData->ToPB<CCSUsrMsg_Shake>(); // never reinterpret_cast the CNetMessage*
pShake->set_command(0);
pShake->set_local_amplitude(10.0f);
pShake->set_frequency(1.0f);
pShake->set_duration(1.0f);
CSingleRecipientFilter filter(CPlayerSlot(iSlot));
g_Source2ModAPI->GetGameEventSystem()->PostEventAbstract(-1, false, &filter, pNetMsg, pData, 0);
delete pData;ToPB<T>() is the only correct way from the engine object to the protobuf:
the protobuf is the engine object's second base, so a reinterpret_cast
lands on the wrong address. Source2Mod API does this for you, which is the
main reason to prefer it.
This whole section is C++ only — scripts use Source2ModNetworkMessages as
shown above.
Net Messages
What network and user messages are in Source 2, how they are identified by id and by name, and how Source2Mod lets you send and hook them.
Hooking Messages
Reading, changing, redirecting and blocking outgoing and incoming net messages with Source2Mod's hooks, and the raw PostEventAbstract alternative.