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.
#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
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:
| Callback | Fires |
|---|---|
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.
void MyPlugin::OnClientPutInServer(CPlayerSlot slot, const char* name, int type, uint64 xuid)
{
SOURCE2MOD_LOG(this, "%s is in (slot %d)\n", name, slot.Get());
}public void Source2Mod_OnClientPutInServer(CPlayerSlot slot, const char[] name, int type, const char[] xuid)
{
Source2ModApi.Log("%s is in (slot %d)", name, slot.Get());
}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_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.
// 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 |
|---|---|
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 inputs | Game hooks |
| Entity created / spawned / deleted | IEntityListener via ADD_ENTITY_LISTENER — see Entities |
| Entity outputs | IEntityIOListener 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.