Game Events
Hooking game events before and after the engine, reading and changing their fields, keeping them from clients, cancelling them, and firing your own.
Game events are how CS2 announces what happens in a match: player_death,
round_start, bomb_planted. Source2Mod hooks the engine's
IGameEventManager2::FireEvent once and hands every event to the plugins that
asked for it — before the engine processes it, after, or both.
Hooking an event
#include "source2mod/ISource2ModEvents.h"
#include "source2mod/schema/entity/classes/CCSPlayerController.h"
bool MyPlugin::Load(PluginId id, ISource2ModAPI* api, char* error, size_t maxlen, bool late)
{
SOURCE2MOD_SAVEVARS();
HOOK_GAME_EVENT("player_death", [](IGameEvent* event, bool post, bool& dontBroadcast) -> Action
{
auto* pVictim = static_cast<CCSPlayerController*>(event->GetPlayerController("userid"));
auto* pAttacker = static_cast<CCSPlayerController*>(event->GetPlayerController("attacker"));
SOURCE2MOD_LOG(&g_Plugin, "%s killed %s with %s%s\n",
pAttacker ? pAttacker->GetPlayerName() : "world",
pVictim ? pVictim->GetPlayerName() : "?",
event->GetString("weapon"),
event->GetBool("headshot") ? " (headshot)" : "");
return Action::Ignore;
}, true);
return true;
}public void OnPluginStart()
{
Source2ModEvents.HookGameEvent("player_death", Event_PlayerDeath, true);
}
public Source2ModAction Event_PlayerDeath(IGameEvent event, bool post, bool &dontBroadcast)
{
CCSPlayerController victim = event.GetPlayerController("userid");
CCSPlayerController attacker = event.GetPlayerController("attacker");
char victimName[64] = "?", attackerName[64] = "world", weapon[64];
if (victim != NULL_ENTITY)
victim.GetPlayerName(victimName, sizeof(victimName));
if (attacker != NULL_ENTITY)
attacker.GetPlayerName(attackerName, sizeof(attackerName));
event.GetString("weapon", weapon, sizeof(weapon));
Source2ModApi.Log("%s killed %s with %s%s", attackerName, victimName, weapon,
event.GetBool("headshot") ? " (headshot)" : "");
return Action_Ignore;
}The API:
using GameEventHandler = Source2ModCallback<Action(IGameEvent* event, bool post, bool& dontBroadcast)>;
virtual Source2ModHookId HookGameEvent(const char* pchName, GameEventHandler handler, bool post) = 0;
virtual bool UnhookGameEvent(const char* pchName, const GameEventHandler& handler, bool post) = 0;
virtual bool UnhookGameEvent(Source2ModHookId id) = 0;
#define HOOK_GAME_EVENT(passname, passfunc, post) g_pSource2ModEvents->HookGameEvent(passname, passfunc, post)
#define UNHOOK_GAME_EVENT(passname, passfunc, post) g_pSource2ModEvents->UnhookGameEvent(passname, passfunc, post)pchName— the event name, exactly as the game defines it (case sensitive). The event reference lists the common ones.handler— a lambda, a free or static function, or an object and a method (SOURCE2MOD_MEMBER(this, &MyPlugin::OnPlayerDeath)). There is no plugin id argument; the core reads which plugin the hook belongs to off the handler.post—falsefor a pre hook,truefor a post hook. For both, hook twice.- The same event may be hooked as often as a plugin likes; each handler runs.
- Hook in
Load(). If the game's event manager does not exist yet, Source2Mod keeps the hook and attaches it once it does. - Hooking an event also registers a server-side listener for it, so the game creates the event even when nothing else on the server listens for it.
- In a script:
Source2ModEvents.HookGameEvent(name, handler, post)with a namedpublicfunction; it returns0when the event does not exist.
Everything is dropped when your plugin unloads. To remove one hook earlier,
pass the Source2ModHookId HOOK_GAME_EVENT returned to
g_pSource2ModEvents->UnhookGameEvent(id), or hand the same function or object
and method to UNHOOK_GAME_EVENT — a lambda has no identity and only goes by
id. A script unhooks by id only:
HOOK_GAME_EVENT("round_end", SOURCE2MOD_MEMBER(this, &MyPlugin::OnRoundEnd), true);
// ...
UNHOOK_GAME_EVENT("round_end", SOURCE2MOD_MEMBER(this, &MyPlugin::OnRoundEnd), true);
Source2ModHookId id = HOOK_GAME_EVENT("round_start", [](IGameEvent* event, bool post, bool& dontBroadcast) -> Action
{
return Action::Ignore;
}, true);
// ...
g_pSource2ModEvents->UnhookGameEvent(id);int id = Source2ModEvents.HookGameEvent("round_start", Event_RoundStart, true);
// ...
Source2ModEvents.UnhookGameEvent(id);Full reference: ISource2ModEvents.
Pre and post
Pre (post == false) | Post (post == true) | |
|---|---|---|
| When | The event is filled in, nobody has seen it yet. | The engine has processed and sent it. |
The event | The real event. | A copy taken after the pre hooks ran. |
| Change fields | Yes — the engine and clients see your values. | Pointless — it is a copy. |
dontBroadcast | Yes, see below. | No effect. |
Action::Supersede | Cancels the event. | Only skips the remaining post hooks. |
Rule of thumb: pre to change or stop, post to react. A post hook sees the event after the game's own handling, so state the event caused — money, score, team — is already updated.
Hooks for the same event run in the order they were registered, across all plugins.
Reading fields
IGameEvent is s2sdk's interface. Field names are plain strings:
| Getter | For field type |
|---|---|
GetInt(key, default = 0) | short, long, byte |
GetBool(key, default = false) | bool |
GetFloat(key, default = 0.0f) | float |
GetString(key, default = "") | string |
GetUint64(key, default = 0) | uint64 (SteamIDs) |
GetPlayerController(key) | player fields — the controller as CEntityInstance* |
GetPlayerPawn(key) | player fields — the pawn as CEntityInstance* |
GetPlayerSlot(key) | player fields — the CPlayerSlot |
GetEntity(key), GetEntityIndex(key), GetEHandle(key) | entity fields |
Player fields (userid, attacker, assister) are typed
player_controller, player_pawn or player_controller_and_pawn in the
game's event files, so the type is known: static_cast the result (a
dynamic_cast works too, it only checks what is already certain):
auto* pController = static_cast<CCSPlayerController*>(event->GetPlayerController("userid"));
if (!pController)
return Action::Ignore; // world, a disconnected player, ...
CCSPlayerPawn* pPawn = pController->GetPlayerPawn();CCSPlayerController controller = event.GetPlayerController("userid");
if (controller == NULL_ENTITY)
return Action_Ignore; // world, a disconnected player, ...
CCSPlayerPawn pawn = controller.GetPlayerPawn();GetPlayerController returns nullptr when the field names no player — an
attacker that was the world, a player who already left — so always check. event->GetName() gives the event name, which helps when one handler
serves several events.
In a script the player getters return the typed handle (CCSPlayerController,
CCSPlayerPawn) and NULL_ENTITY when there is no player, so there is nothing
to cast; GetString, GetName and GetUint64 copy into a buffer you pass (the
64-bit value as decimal text).
Changing fields
In a pre hook, set fields with the matching setter before the engine sees the
event: SetInt, SetBool, SetFloat, SetString, SetUint64, SetEntity,
SetPlayer(key, CPlayerSlot).
// Every kill shows as a headshot in the killfeed.
HOOK_GAME_EVENT("player_death", [](IGameEvent* event, bool post, bool& dontBroadcast) -> Action
{
event->SetBool("headshot", true);
return Action::Override;
}, false);Source2ModEvents.HookGameEvent("player_death", Event_PlayerDeathPre, false);
// Every kill shows as a headshot in the killfeed.
public Source2ModAction Event_PlayerDeathPre(IGameEvent event, bool post, bool &dontBroadcast)
{
event.SetBool("headshot", true);
return Action_Override;
}Return Action::Override when you changed something. The dispatcher treats it
like Ignore — the event goes on — but it documents intent, and it is what the
rest of Source2Mod's callbacks use.
Keeping an event from the clients
Set dontBroadcast = true in a pre hook. The server still processes the event —
the game's own handling, other plugins' post hooks — but no client receives it:
// Bot deaths never show up in the killfeed.
HOOK_GAME_EVENT("player_death", [](IGameEvent* event, bool post, bool& dontBroadcast) -> Action
{
auto* pVictim = static_cast<CCSPlayerController*>(event->GetPlayerController("userid"));
if (pVictim && pVictim->IsBot())
{
dontBroadcast = true;
return Action::Override;
}
return Action::Ignore;
}, false);Source2ModEvents.HookGameEvent("player_death", Event_PlayerDeathPre, false);
// Bot deaths never show up in the killfeed.
public Source2ModAction Event_PlayerDeathPre(IGameEvent event, bool post, bool &dontBroadcast)
{
CCSPlayerController victim = event.GetPlayerController("userid");
if (victim != NULL_ENTITY && victim.IsBot())
{
dontBroadcast = true;
return Action_Override;
}
return Action_Ignore;
}FUNPLAY's JailBreak does the same with player_team and player_radio — the
game still switches the team, the clients just never get the event:
HOOK_GAME_EVENT("player_radio", [](IGameEvent* event, bool post, bool& dontBroadcast) -> Action
{
dontBroadcast = true;
return Action::Ignore;
}, false);Source2ModEvents.HookGameEvent("player_radio", Event_PlayerRadio, false);
public Source2ModAction Event_PlayerRadio(IGameEvent event, bool post, bool &dontBroadcast)
{
dontBroadcast = true;
return Action_Ignore;
}The flag works both ways: setting it to false on an event the game fired
server-only sends it to the clients after all.
Cancelling an event
Return Action::Supersede from a pre hook and the event is not fired at all:
the remaining pre hooks are skipped, the engine never processes it, no client
receives it and no post hook runs.
HOOK_GAME_EVENT("player_jump", [](IGameEvent* event, bool post, bool& dontBroadcast) -> Action
{
auto* pPlayer = static_cast<CCSPlayerController*>(event->GetPlayerController("userid"));
// Nobody hears about bots jumping -- not the game, not the clients, not later hooks.
if (pPlayer && pPlayer->IsBot())
return Action::Supersede;
return Action::Ignore;
}, false);Source2ModEvents.HookGameEvent("player_jump", Event_PlayerJump, false);
public Source2ModAction Event_PlayerJump(IGameEvent event, bool post, bool &dontBroadcast)
{
CCSPlayerController player = event.GetPlayerController("userid");
// Nobody hears about bots jumping -- not the game, not the clients, not later hooks.
if (player != NULL_ENTITY && player.IsBot())
return Action_Supersede;
return Action_Ignore;
}Cancelling also hides the event from every other plugin that hooked it later.
If all you want is to keep something off players' screens, dontBroadcast is
the gentler tool.
Firing your own event
Create the event through the engine's event manager (in a script, the static
methods of IGameEventManager2), fill it in, then send it one of two ways:
IGameEventManager2::FireEvent(event, bDontBroadcast)— to the server and every client, through every plugin's hooks, exactly like a game-fired event.FireEventtakes ownership and frees the event.CBasePlayerController::FireEventToClient(event)— to one player only. The server never processes it and no hooks run. The event is still yours: free it afterwards.
IGameEventManager2* pEventManager = g_Source2ModAPI->GetGameEventManager();
// To everybody.
if (IGameEvent* pEvent = pEventManager->CreateEvent("round_announce_match_start", true))
pEventManager->FireEvent(pEvent); // freed by FireEvent
// To one player (pCaller, a CCSPlayerController*): HUD text through show_survival_respawn_status.
if (IGameEvent* pEvent = pEventManager->CreateEvent("show_survival_respawn_status", true))
{
pEvent->SetString("loc_token", "<font color='#00ff00'>Hello from a game event</font>");
pEvent->SetInt("duration", 5);
pEvent->SetPlayer("userid", pCaller->GetPlayerSlot());
pCaller->FireEventToClient(pEvent);
pEventManager->FreeEvent(pEvent); // FireEventToClient does not free it
}// To everybody.
IGameEvent event = IGameEventManager2.CreateEvent("round_announce_match_start", true);
if (event != null)
IGameEventManager2.FireEvent(event); // freed by FireEvent
// To one player (caller, a CCSPlayerController): HUD text through show_survival_respawn_status.
event = IGameEventManager2.CreateEvent("show_survival_respawn_status", true);
if (event != null)
{
event.SetString("loc_token", "<font color='#00ff00'>Hello from a game event</font>");
event.SetInt("duration", 5);
event.SetPlayer("userid", caller.GetPlayerSlot());
caller.FireEventToClient(event);
IGameEventManager2.FreeEvent(event); // FireEventToClient does not free it
}- The second argument of
CreateEventisbForce: create the event even if nothing on the server listens for it. Passtruefor events you send to clients. CreateEventreturnsnullptr(nullin a script) for a name the game does not know.- The schema headers also declare a free function
GetGameEventManager(), returning the same manager.
FireEventToClient is on
CBasePlayerController;
CCSPlayerController::PrintToCenterHtml is built on exactly this event.
See also
- Event reference — fields of the common events.
- Listeners — map start/end and plugin lifecycle.
ISource2ModEventsreference
Events
The kinds of events a plugin can react to — game events, Source2Mod listener callbacks, engine hooks, entity outputs and net messages — and where each one is documented.
Listeners
The ISource2ModListener callbacks — plugin lifecycle, map start and end, interface queries — what each one reports and exactly when it fires.