Development

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

TierWhat the plugin usesAfter a game or core update
Stablethe ISource2Mod* interfaces, ISource2ModListener callbacks, game hooks, schema fields by name, KHOOK_VIRTUAL with a gamedata nameupdate the core; the plugin keeps running
Rawits own KHook hooks (KHOOK_MEMBER, KHOOK_VIRTUAL with &Class::Method), s2sdk classes and their layouts, protobuf messages by generated type, SOURCE2MOD_ORIGINALrebuild 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 forThe core
the current revisionserves it
an older onerefuses it: X was built against ISource2ModMenus003, this core serves ISource2ModMenus004: rebuild the plugin against the current SDK
a newer onerefuses 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.

VersionCoreWhat came with it
3v1.0.37OnClientAuthorized, OnClientAuthorizeFailed, OnPermissionsChanged; the client callbacks of ISource2ModListener in the order they happen; g_pSource2ModPermissions
2v1.0.26the 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 ISource2ModGameHooks where the function is in its set.
  • Take engine callbacks from ISource2ModListener (OnGameFrame, OnStartupServer, OnClientPutInServer, OnClientDisconnect, …) instead of a KHOOK_VIRTUAL of 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 it

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

On this page