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 info 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, ISource2ModHooks (hook points, and Function points on gamedata entries), schema fields by nameupdate the core; the plugin keeps running
Rawits own KHook hooks (KHOOK_MEMBER, KHOOK_VIRTUAL, KHOOK_FUNCTION, KHook objects), 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 info <id> says which is which, together with the plugin API it was built against, its KHook commit, how many hooks of its own it declared and the interfaces it asked for:

+---------------------------------------------------------------------+
|                              Plugin 2                               |
+-----------------+---------------------------------------------------+
|            Name | Sample Plugin                                     |
|         Version | 1.0.0                                             |
|          Author | Slynx                                             |
|     Description | Everything the SDK can do, one section each       |
|            Type | Native                                            |
|            Path | addons/source2mod/plugins/cpp_sample.smx          |
|      Plugin API | 5                                                 |
|           KHook | 2a8953533da2                                      |
| Own KHook hooks | 2 (raw tier: rebuild on a KHook or engine change) |
|      Interfaces | ISource2ModCommands003                            |
|                 | ISource2ModHooks001                               |
|                 | ISource2ModMenus007                               |
+-----------------+---------------------------------------------------+

A plugin on the stable tier reads KHook (none of its own) and Own KHook hooks 0 (stable tier). s2m list shows only id, name, version, author and whether the plugin is native or SourcePawn.

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 hook points' prototypes — is the core's alone.

The last such change (plugin API 5) replaced ISource2ModGameHooks with ISource2ModHooks001 and bumped ISource2ModEntities003: the entity and output listeners take a designer name and one entity, and IEntityOutputListener replaces the IEntityIOListener classes. ISource2ModFunctions002 describes functions that can be called and hooked.

The one before it (core v1.0.37) bumped ISource2ModCommands003: commands take an optional permission, the context says where a command was typed, and ReplyToCommand() answers there.

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 (since replaced), 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 6 but this core speaks 5 (update the core), an older one with plugin API version 4 but this core needs 5 -- update the plugin or rebuild it against the current SDK.

VersionCoreWhat came with it
5ISource2ModHooks (hook point + target) replaces ISource2ModGameHooks and the engine callbacks of ISource2ModListener, which are hook points now (HookPoint::GameFrame, ClientConnect, ClientDisconnect, …); the matching Source2Mod_On… script forwards are gone; ISource2ModEntities003 listeners with filters (IEntityOutputListener replaces IEntityIOListener); the KHook wrappers moved to ISource2ModKHook.h; entity-virtual points (Spawn, Use, StartTouch, EndTouch, Blocked, Think, Killed, WeaponPickup, WeaponSwitch); the SDK's schema helpers call through the gamedata functions (SOURCE2MOD_CALL_FUNCTION) and their code is the core's (ISource2ModSchemaMethods: a plugin carries generated forwarders, so a helper fix needs only a core update); g_Source2ModAPI / g_PluginAPI renamed g_pSource2ModAPI / g_pPluginAPI; g_pSource2ModNatives filled by SOURCE2MOD_SAVEVARS; ISource2ModEntities004 (FindAllEntitiesByDesignerName). The listener's slots moved: a plugin built against 4 or older is refused.
4OnClientConnect (could refuse the connection), OnClientConnected and OnClientDisconnectPost on ISource2ModListener; OnClientDisconnect called pre. A plugin built against 3 or older is refused.
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 through ISource2ModHooks: a hook point where the function or engine callback has one (HookPoint::TakeDamage, HookPoint::GameFrame, HookPoint::ClientDisconnect, …), a Function point otherwise — no KHOOK_* of your own.
  • Describe the functions you hook or call in gamedata (a "function" object, or FromGameConfig) rather than with a pattern compiled in, so they follow a gamedata update.
  • 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, 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 hooks come out synchronously when it unloads — the core drops its ISource2ModHooks hooks, and a raw plugin's own come out 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