Listeners

The ISource2ModListener callbacks — plugin lifecycle, map start and end, interface queries — what each one reports and exactly when it fires.

ISource2ModListener is how Source2Mod tells your plugin about the plugin system and the map lifecycle. Every method has an empty default, so you override only what you need, and you register the listener once in Load(). A script gets the same callbacks as forwards from source2mod/listener.inc, named Source2Mod_On…: no class and no registration, it just defines the public functions it wants.

plugin.h
#include "source2mod/ISource2ModPlugin.h"

class MyPlugin final : public ISource2ModPlugin, public ISource2ModListener
{
public:
    bool Load(PluginId id, ISource2ModAPI* api, char* error, size_t maxlen, bool late) override;
    bool Unload(char* error, size_t maxlen) override;

    // ISource2ModListener
    void OnAllSource2ModPluginsLoaded() override;
    void OnLevelInit(const char* mapName, const char* mapEntities, const char* oldLevel,
                     const char* landmarkName, bool loadGame, bool background) override;
    void OnLevelShutdown() override;

    // GetAuthor(), GetName(), ... as usual
};
plugin.cpp
bool MyPlugin::Load(PluginId id, ISource2ModAPI* api, char* error, size_t maxlen, bool late)
{
    SOURCE2MOD_SAVEVARS();

    api->AddListener(this, this);
    return true;
}

Registering

ISource2ModApi.h
virtual void AddListener(ISource2ModPlugin* plugin, ISource2ModListener* pListener) = 0;
  • plugin — your plugin object; the listener is filed under it.
  • pListener — the object that implements the callbacks. Usually the plugin class itself, hence AddListener(this, this), but it can be any object that lives as long as the plugin. A plugin can add several.

Call it from Load(). There is no RemoveListener: Source2Mod forgets a plugin's listeners when the plugin unloads (or when its Load() fails).

C++ only — a script has nothing to register; its Source2Mod_On* functions are called as long as it is loaded.

The callbacks

Plugin lifecycle

CallbackFires
OnPluginLoad(PluginId id, const char* name)After a s2m plugin (.smx) loaded successfully, on every plugin's listeners — including the new plugin's own, so you see your own g_PluginID once.
OnPluginUnload(PluginId id, const char* name)When a Source2Mod plugin is about to unload — before its Unload() runs — on every plugin's listeners, including the one going away.
OnAllSource2ModPluginsLoaded()After Source2Mod loaded the plugins folder at startup, and again after s2m refresh loaded at least one new plugin.
OnMetamodPluginLoad(PluginId id)A Metamod:Source plugin was loaded.
OnMetamodPluginUnload(PluginId id)A Metamod:Source plugin was unloaded.
OnAllMetamodPluginsLoaded()Metamod:Source finished loading all of its plugins.

The ids in OnPluginLoad/OnPluginUnload are Source2Mod plugin ids, the ones s2m list shows. The ids in the two OnMetamod… callbacks are Metamod's own, from a different numbering — which is why they are separate callbacks.

OnAllSource2ModPluginsLoaded is the place for anything that depends on other plugins: fetching another plugin's interface with GET_SOURCE2MOD_IFACE, for example. Note what does not trigger it: loading one plugin by hand with s2m load, or a hot reload. A plugin that may be loaded that way should also try its setup from Load() or from OnPluginLoad. A script has no interfaces to fetch; it checks for the other plugin's library with Source2ModNatives.LibraryExists and then calls its natives.

plugin.cpp
void MyPlugin::OnAllSource2ModPluginsLoaded()
{
    // IOtherPluginApi / OTHER_PLUGIN_API_NAME: the header the other plugin ships.
    int ret = SOURCE2MOD_IFACE_FAILED;
    GET_SOURCE2MOD_IFACE(m_pOtherApi, IOtherPluginApi, OTHER_PLUGIN_API_NAME, ret);

    if (ret != SOURCE2MOD_IFACE_OK)
        SOURCE2MOD_LOG(this, "Other plugin not found, running without it.\n");
}

Map lifecycle

CallbackFires
OnLevelInit(mapName, mapEntities, oldLevel, landmarkName, loadGame, background)A map is initialising — Metamod's OnLevelInit, passed through. mapName is the map being loaded.
OnLevelShutdown()The current map is shutting down.

Map start is the place to reset per-map state; map shutdown is the last moment to let go of entity pointers, handles to per-map objects, and timers that must not survive the map.

plugin.cpp
void MyPlugin::OnLevelInit(const char* pMapName, const char* pMapEntities, const char* pOldLevel,
                           const char* pLandmarkName, bool loadGame, bool background)
{
    SOURCE2MOD_LOG(this, "OnLevelInit(%s)\n", pMapName);
}

void MyPlugin::OnLevelShutdown()
{
    // Anything map-scoped goes here.
}

A plugin loaded while a map is already running has missed that map's OnLevelInit. If you need the current map at load time, read it from the engine rather than waiting for the next map change.

Engine callbacks

Plugin API 2 (core v1.0.26) fans the engine calls most plugins used to hook themselves out of the core's own hooks, so a plugin needs no KHOOK_VIRTUAL for any of these — and stays on the stable tier:

CallbackFires
OnGameFrame(bool simulating, bool firstTick, bool lastTick)Once a frame, after ISource2Server::GameFrame and Source2Mod's own per-frame work (timers, menus).
OnStartupServer(const GameSessionConfiguration_t& config, ISource2WorldSession* session, const char* mapName)After INetworkServerService::StartupServer: a new server session, the new map's entity system is Source2Mod's.
OnClientPutInServer(CPlayerSlot slot, const char* name, int type, uint64 xuid)After IServerGameClients::ClientPutInServer.
OnClientVoice(CPlayerSlot slot)After ClientVoice.
OnClientSettingsChanged(CPlayerSlot slot)After ClientSettingsChanged.
OnClientDisconnect(CPlayerSlot slot, ENetworkDisconnectionReason reason, const char* name, uint64 xuid, const char* networkId)After ClientDisconnect; the controller still exists.
OnGameServerSteamAPIActivated()After the Steam API came up.
OnGameServerSteamAPIDeactivated()Before it goes.
OnLoadEventsFromFile(IGameEventManager2* manager, const char* filename, bool searchAll)After IGameEventManager2::LoadEventsFromFile, with the manager it was called on — the place to keep it, or to register a .gameevents file of your own.

They are appended after the older callbacks, so a plugin built against API 1 keeps loading; the core just never calls them on it.

Scripts get them as forwards of the same names prefixed Source2Mod_. There a SteamID (xuid) is decimal text, the disconnect reason an int, Source2Mod_OnStartupServer gets only the map name and Source2Mod_OnLoadEventsFromFile no manager.

plugin.cpp
void MyPlugin::OnClientPutInServer(CPlayerSlot slot, const char* name, int type, uint64 xuid)
{
    SOURCE2MOD_LOG(this, "%s is in (slot %d)\n", name, slot.Get());
}

Interface queries

These let your plugin expose interfaces to other plugins:

CallbackCalled when
void* OnSource2ModQuery(const char* iface, int* ret)Another s2m plugin asks for an interface (g_Source2ModAPI->Source2ModFactory, GET_SOURCE2MOD_IFACE).
void* OnMetamodQuery(const char* iface, int* ret)A Metamod:Source plugin asks for one through Metamod's factory. By default it answers exactly like OnSource2ModQuery.

Return your interface and set *ret to SOURCE2MOD_IFACE_OK when iface is a name you serve; otherwise return nullptr. The default implementation sets SOURCE2MOD_IFACE_FAILED. Override OnMetamodQuery to answer Metamod plugins differently, or to return nullptr and keep an interface Source2Mod-only.

C++ only — a script offers natives instead: Source2ModNatives.CreateNative and Source2ModNatives.RegisterLibrary in AskPluginLoad2.

plugin.cpp
// MY_PLUGIN_API_NAME and g_MyPluginApi: your own interface name and object.
void* MyPlugin::OnSource2ModQuery(const char* iface, int* ret)
{
    if (strcmp(iface, MY_PLUGIN_API_NAME) == 0)
    {
        if (ret) *ret = SOURCE2MOD_IFACE_OK;
        return &g_MyPluginApi;
    }

    if (ret) *ret = SOURCE2MOD_IFACE_FAILED;
    return nullptr;
}

What is not a listener callback

ISource2ModListener is deliberately small. For everything else:

EventWhere
Client connect (ClientConnect, OnClientConnected, ClientActive, ClientCommand)KHook on ISource2GameClients — see Hooking, or the player_connect_full game event; put in server and disconnect are listener callbacks above
Damage, movement, item pickup, entity inputsGame hooks
Entity created / spawned / deletedIEntityListener via ADD_ENTITY_LISTENER — see Entities
Entity outputsIEntityIOListener via ADD_ENTITY_IO_LISTENER — see Entities
Round start, deaths, bomb, …Game events

The full interface is documented in the ISource2ModPlugin reference; AddListener in ISource2ModApi.

On this page