Guides

Scripts

Run cs_script (the game's server-side JavaScript) from a plugin — compiled assets, raw source, hot-reloaded files, inputs into a script and messages back out.

CS2 runs map and mode logic in JavaScript through point_script entities (import { Instance } from "cs_script/point_script"). ISource2ModScripts lets a plugin run scripts of its own and talk to them in both directions.

plugin.cpp
#include "source2mod/ISource2ModScripts.h"

Running a script

A script is known by a name you pick — it is also the targetname of its point_script entity. Running a script under a name already in use replaces the old one.

CallRunsNeeds a signature
SCRIPTS_LOAD_ASSET(name, asset, persistent)a compiled .vjs asset, exactly as a map's point_script doesno
SCRIPTS_RUN_SOURCE(name, path, source, persistent)raw JavaScript source you hand overyes
SCRIPTS_RUN_FILE(name, file, persistent)a .js file from the server's disk, relative to game/csgo or absoluteyes

Each returns the point_script entity's CEntityHandle, invalid on failure. In SourcePawn the same calls are Source2ModScripts.LoadAsset(), RunSource() and RunFile(), returning the CCSPointScriptEntity or NULL_ENTITY. Raw source goes through the game's script loader, found through the gamedata signature CSScript::RunScript; g_pSource2ModScripts->CanRunSource() (Source2ModScripts.CanRunSource()) says whether it resolved. Compiled assets need nothing from the gamedata and keep working through game updates.

plugin.cpp
static const char* s_source = R"js(
import { Instance } from "cs_script/point_script";

Instance.OnRoundStart(() => {
    Instance.ServerCommand("source2mod_script myplugin " + encodeURIComponent("round started"));
});

Instance.OnScriptInput("ping", () => {
    Instance.Msg("pong from cs_script");
});
)js";

SCRIPTS_RUN_SOURCE("myplugin", "scripts/myplugin.js", s_source, true);

The path of RunSource only names the module in errors and stack traces. A script that does not compile is reported by the game in the server console ("Exception during CompileModule …"); the entity exists regardless.

Hot reload

RunFile() reads the file again on every run, so after an edit:

g_pSource2ModScripts->Reload("myplugin"); // throws the old entity away and runs the file again

Reload() works for every kind — source and assets simply run again as they were given. A handy dev loop is a console command that calls it.

Persistent scripts

With persistent = true a script comes back by itself: when its entity is removed (round restart, a map's cleanup, ent_remove) it is spawned again on the next simulating frame, and after a map change on the new map's first one. A non-persistent script is done once its entity goes. Remove(name) ends either kind.

Native → script: inputs

SCRIPTS_FIRE_INPUT(name, input) (Source2ModScripts.FireInput()) fires the game's RunScriptInput entity input, which calls the callback the script registered with Instance.OnScriptInput(input, …):

SCRIPTS_FIRE_INPUT("myplugin", "ping");

Only the input name reaches the script. Keep data on the script side, or send it the other way.

Script → native: messages

A script sends a message with Instance.ServerCommand, through the hidden source2mod_script command:

Instance.ServerCommand("source2mod_script <channel> " + encodeURIComponent(JSON.stringify(data)));

and every plugin that hooked <channel> gets everything after the channel name:

plugin.cpp
HOOK_SCRIPT_MESSAGE("myplugin", [](const char* pszChannel, const char* pszPayload)
{
    // pszPayload is still URI-encoded, exactly as the script sent it.
    META_CONPRINTF("[myplugin] script says: %s\n", pszPayload);
});

Several handlers may hook one channel, from one plugin or more; each gets every message. HOOK_SCRIPT_MESSAGE returns a Source2ModHookId. To remove a handler, pass that id to g_pSource2ModScripts->UnhookScriptMessage(id), or pass the same function or object and method again with UNHOOK_SCRIPT_MESSAGE(channel, handler). A lambda has no identity, so it can only be removed by id. In SourcePawn Source2ModScripts.HookScriptMessage() returns the id that Source2ModScripts.UnhookScriptMessage(id) takes. Whatever your plugin still has hooked at unload is dropped for it.

The line goes through the server's command buffer: it arrives on a later tick, a ; or a newline in it would split it into two commands, and it is cut at the buffer's line limit. Encode the payload (as above) and keep it short. Only the server itself can run source2mod_script — a client typing it, or !source2mod_script in chat, is ignored.

Pitfalls

  • The spawn warning. A script run from source spawns its point_script without a compiled asset, and the game prints Failed to load cs_script "" while it spawns. It is harmless: the source goes into the same entity right after.
  • Lifetime. Scripts and message hooks belong to your plugin and are removed when it unloads — nothing to undo in Unload() / OnPluginEnd().
  • Main thread only.

Reference

On this page