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

plugin.cpp
#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;
}

The API:

ISource2ModEvents.h
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 — false for a pre hook, true for 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 named public function; it returns 0 when 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);

Full reference: ISource2ModEvents.

Pre and post

Pre (post == false)Post (post == true)
WhenThe event is filled in, nobody has seen it yet.The engine has processed and sent it.
The eventThe real event.A copy taken after the pre hooks ran.
Change fieldsYes — the engine and clients see your values.Pointless — it is a copy.
dontBroadcastYes, see below.No effect.
Action::SupersedeCancels 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:

GetterFor 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();

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

plugin.cpp
// 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);

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:

plugin.cpp
// 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);

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

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

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. FireEvent takes 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.
plugin.cpp
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
}
  • The second argument of CreateEvent is bForce: create the event even if nothing on the server listens for it. Pass true for events you send to clients.
  • CreateEvent returns nullptr (null in 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

On this page