Listeners
The ISource2ModListener callbacks — plugin lifecycle, map start and end, Steam authorisation, permissions, interface queries — what each one reports and exactly when it fires.
ISource2ModListener is how Source2Mod tells your plugin about the plugin system,
the map lifecycle, Steam authorisation and permissions. The engine's own
callbacks — the frame, clients connecting and leaving — are
hook points instead. 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.
#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
};// No class and no registration: a public function named after a
// source2mod/listener.inc forward is called.
public void Source2Mod_OnAllSource2ModPluginsLoaded()
{
}
public void Source2Mod_OnLevelInit(const char[] mapName, const char[] mapEntities, const char[] oldLevel,
const char[] landmarkName, bool loadGame, bool background)
{
}
public void Source2Mod_OnLevelShutdown()
{
}bool MyPlugin::Load(PluginId id, ISource2ModAPI* api, char* error, size_t maxlen, bool late)
{
SOURCE2MOD_SAVEVARS();
api->AddListener(this, this);
return true;
}Registering
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, henceAddListener(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
| Callback | Fires |
|---|---|
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.
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");
}public void Source2Mod_OnAllSource2ModPluginsLoaded()
{
// "other_plugin": the library name the other plugin registers.
if (!Source2ModNatives.LibraryExists("other_plugin"))
Source2ModApi.Log("Other plugin not found, running without it.");
}Map lifecycle
| Callback | Fires |
|---|---|
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.
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.
}public void Source2Mod_OnLevelInit(const char[] mapName, const char[] mapEntities, const char[] oldLevel,
const char[] landmarkName, bool loadGame, bool background)
{
Source2ModApi.Log("OnLevelInit(%s)", mapName);
}
public void Source2Mod_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
The engine calls a plugin used to override here — OnGameFrame,
OnStartupServer, OnClientConnect, OnClientConnected,
OnClientPutInServer, OnClientDisconnect / OnClientDisconnectPost,
OnClientVoice, OnClientSettingsChanged, the Steam API pair and
OnLoadEventsFromFile — are no longer on the listener. Since plugin API 5
they are hook points of
ISource2ModHooks, hooked with HookTarget::All(), pre or post, with a
context instead of a parameter list. In SourcePawn the matching
Source2Mod_On… forwards are gone the same way.
| Was | Now |
|---|---|
OnGameFrame(simulating, firstTick, lastTick) | HookPoint::GameFrame, post |
OnStartupServer(config, session, mapName) | HookPoint::StartupServer |
OnClientConnect(...) returning false | HookPoint::ClientConnect, pre: { Action::Supersede, false } |
OnClientConnected, OnClientPutInServer | HookPoint::ClientConnected, HookPoint::ClientPutInServer |
OnClientDisconnect / OnClientDisconnectPost | HookPoint::ClientDisconnect, pre / post |
OnClientVoice, OnClientSettingsChanged | HookPoint::ClientVoice, HookPoint::ClientSettingsChanged |
OnGameServerSteamAPIActivated / Deactivated | HookPoint::GameServerSteamAPIActivated / Deactivated |
OnLoadEventsFromFile(manager, filename, searchAll) | HookPoint::LoadEventsFromFile |
g_pSource2ModHooks->Hook(HookPoint::ClientPutInServer, HookTarget::All(), SOURCE2MOD_MEMBER(this, &MyPlugin::OnClientPutInServer), true);
HookReturn<void> MyPlugin::OnClientPutInServer(ClientPutInServerContext& ctx, bool post)
{
SOURCE2MOD_LOG(this, "%s is in (slot %d)
", ctx.name, ctx.slot.Get());
return Action::Ignore;
}Source2ModHooks.Hook(HookPoint_ClientPutInServer, OnClientPutInServer, true);
public Source2ModAction OnClientPutInServer(ClientPutInServerContext ctx, bool post)
{
char name[64];
ctx.GetName(name, sizeof(name));
Source2ModApi.Log("%s is in (slot %d)", name, ctx.slot.Get());
return Action_Ignore;
}Clients and permissions
| Callback | Fires |
|---|---|
OnClientAuthorized(CPlayerSlot slot, uint64 steamId) | Steam validated the player in this slot: from now on they are checked under steamId. Not called for bots. |
OnClientAuthorizeFailed(CPlayerSlot slot, uint64 steamId) | Steam refused the ticket; steamId is the one they claimed. |
OnPermissionsChanged(uint64 steamId) | What this SteamID64 holds may have changed — a grant, a group, a reload. 0 when a change can touch anybody. |
Scripts get them as Source2Mod_OnClientAuthorized,
Source2Mod_OnClientAuthorizeFailed and Source2Mod_OnPermissionsChanged,
with the SteamID as decimal text.
Interface queries
These let your plugin expose interfaces to other plugins:
| Callback | Called when |
|---|---|
void* OnSource2ModQuery(const char* iface, int* ret) | Another s2m plugin asks for an interface (g_pSource2ModAPI->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.
// 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:
| Event | Where |
|---|---|
| The frame, clients connecting, put in server, disconnecting | Hook points (HookPoint::GameFrame, ClientConnect, ClientPutInServer, ClientDisconnect, …) |
ClientActive, ClientCommand and other engine calls | A Function hook, or the player_connect_full game event |
| Damage, movement, item pickup, entity inputs | Hook points |
| Entity created / spawned / deleted | IEntityListener via AddEntityListener — see Entity listeners |
| Entity outputs | IEntityOutputListener via AddEntityOutputListener — see Entity listeners |
| Round start, deaths, bomb, … | Game events |
The full interface is documented in the
ISource2ModPlugin reference; AddListener in
ISource2ModApi.