Compatibility
What a core update means for a plugin — the stable and raw tiers, interface revisions and the compat adapters, the plugin API version, the KHook check, and what s2m list shows.
A game update changes the server binaries; the s2m core follows with new gamedata and a release. The question for every plugin on the server is whether it has to be rebuilt too. The answer depends on what the plugin uses, and the Source2Mod is built so that the answer is no for as much as possible.
Two tiers
| Tier | What the plugin uses | After a game or core update |
|---|---|---|
| Stable | the ISource2Mod* interfaces, ISource2ModListener callbacks, game hooks, schema fields by name, KHOOK_VIRTUAL with a gamedata name | update the core; the plugin keeps running |
| Raw | its own KHook hooks (KHOOK_MEMBER, KHOOK_VIRTUAL with &Class::Method), s2sdk classes and their layouts, protobuf messages by generated type, SOURCE2MOD_ORIGINAL | rebuild when the thing it compiled in moved |
The line runs through the plugin: everything resolved at run time from the
core's gamedata or the schema system is the core's to keep working, everything
compiled in from a header is the plugin's. A plugin with no raw hooks is on the
stable tier; s2m list says which is which:
[2] Sample Plugin (1.0.0) by Slynx
API 2, KHook 2a8953533da2, raw tier (own KHook hooks: rebuild on a KHook or engine change)s2m info <id> adds the interfaces the plugin asked for, its KHook commit
and how many hooks of its own it declared.
Interface revisions
Every interface name carries a revision — ISource2ModMenus003,
ISource2ModEntities002. The C++ name never changes; the number does whenever
the interface does, a changed slot or an appended one alike. The factory
compares what the plugin asks for with what the core has:
| The plugin asks for | The core |
|---|---|
| the current revision | serves it |
| an older one | refuses it: X was built against ISource2ModMenus003, this core serves ISource2ModMenus004: rebuild the plugin against the current SDK |
| a newer one | refuses it: … newer than this core's …: update the core |
An interface changes rarely, and when it does the log says exactly which plugin and which interface. Everything else a core update touches — gamedata, schema, the game hooks' prototypes — is the core's alone.
The last such change (core v1.0.37) bumped ISource2ModCommands003: commands
take an optional permission, the context says
where a command was typed, and ReplyToCommand() answers there.
The one before it (core v1.0.35) took the PluginId out of every call that takes a
handler (the core now reads the owner off the handler) and made every
Register* / Hook* return a Source2ModHookId. It bumped ISource2ModCommands002,
ISource2ModConVars002, ISource2ModCustomHud002, ISource2ModEntities002,
ISource2ModEvents002, ISource2ModGameHooks003, ISource2ModHTTP002,
ISource2ModNetworkMessages002, ISource2ModScheduler002, ISource2ModScripts002,
ISource2ModSounds002 and ISource2ModTransmit002; a plugin built against the
revisions before those has to be rebuilt against the current SDK.
The plugin API version
SOURCE2MOD_PLAPI_VERSION numbers the plugin-side interface: ISource2ModPlugin,
ISource2ModListener and the globals. The core loads only plugins built
against its own version: a newer one is refused with plugin API version 4 but this core speaks 3 (update the core), an older one with plugin API version 2 but this core needs 3 -- update the plugin or rebuild it against the current SDK.
| Version | Core | What came with it |
|---|---|---|
| 3 | v1.0.37 | OnClientAuthorized, OnClientAuthorizeFailed, OnPermissionsChanged; the client callbacks of ISource2ModListener in the order they happen; g_pSource2ModPermissions |
| 2 | v1.0.26 | the engine callbacks (OnGameFrame, OnClientPutInServer, …) and ISource2ModAPI::GameHooks() |
The KHook check
Plugins share Metamod's detour engine with the core, so a plugin that hooks
through KHook has to be built against the same KHook (the templates are the
engine's layout). The SDK stamps its vendor/khook commit into the binary and
KHOOK_INIT() compares it with the core's: a mismatch returns false out of
Load() with KHook mismatch: … in error.
A plugin that never calls KHOOK_INIT() — no hooks of its own — is not
checked; it does not hook, so it does not care which KHook the core runs. A
plugin using raw KHOOK_NEW objects without any KHOOK_* macro calls
KHOOK_CHECK() itself before the first one. See
SDK dependencies for keeping
vendor/khook at the commit Metamod pins.
Keeping a plugin on the stable tier
- Hook game functions through
ISource2ModGameHookswhere the function is in its set. - Take engine callbacks from
ISource2ModListener(OnGameFrame,OnStartupServer,OnClientPutInServer,OnClientDisconnect, …) instead of aKHOOK_VIRTUALof your own. - Where a virtual hook of your own is unavoidable, give it a gamedata name —
KHOOK_GAMEDATA(&Class::Method, "Class::Method")or the name alone — so the slot follows the core's gamedata. - Read schema fields through the SDK's classes: a field the running game does not have does nothing instead of writing at offset 0, and the error is logged once.
- Take command arguments as
Source2ModCommandContext/Source2ModCommandArgs, not the engine's types.
When the server stalls
Everything a plugin does runs inside a hook of the core, so the engine's
frame-time report blames Source2Mod for a stall wherever it really is.
The core times every plugin callback it dispatches — command and chat
handlers, game event handlers, menu options, timers and next-frame tasks,
game hook handlers, listener callbacks, HTTP callbacks — and logs the ones
that take longer than SlowCallbackWarnMs (core.json, default 100):
slow callback: command handler 'admin' of Core took 4530 ms -- the whole server waited for itSet it to 0 to turn the check off. The cost when nothing is slow is two clock reads per callback.
What stays raw by nature: protobuf messages by generated type, s2sdk classes
used directly, SOURCE2MOD_ORIGINAL, and any hook on a function the core does not
know. Those are rebuilt when the thing they compiled in moves, and Source2Mod list shows the server owner which plugins that concerns.
Unloading, and why it happens between frames
A plugin's own hooks come out synchronously in KHOOK_DESTRUCT(), in
Unload(). That is only safe because the core never unloads a plugin from
inside a hooked function: a detour's dispatch holds the function's lock for
the whole pre → original → post run, and removing a hook on that function from
inside it waits forever. GameFrame is the function most plugins hook, so no
unload runs from a game callback — a s2m unload from the console runs
right away (the command buffer runs between frames), a hot reload and an
unload requested from code are queued into the same command buffer. The one
function a plugin cannot be unloaded from inside is
ICvar::DispatchConCommand itself, which is how the console command arrives.