Commands

Registering console and chat commands, reading their arguments and caller, and taking them down again.

A plugin command is one call in Load() (in a script, OnPluginStart()). Source2Mod creates the console command, routes the chat aliases to it, and removes it again when your plugin unloads.

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

bool MyPlugin::Load(PluginId id, ISource2ModAPI* api, char* error, size_t maxlen, bool late)
{
    SOURCE2MOD_SAVEVARS();

    REGISTER_CON_COMMAND("hello", [](const Source2ModCommandContext& ctx, const Source2ModCommandArgs& args, bool post)
    {
        SOURCE2MOD_CONPRINTF("hello from slot %d: %s\n", ctx.GetPlayerSlot().Get(), args.ArgS());
    });

    return true;
}

hello now works from the server console, from a client's console, and from chat as !hello or /hello.

The API

ISource2ModCommands.h
using ChatHandler    = Source2ModCallback<void(const Source2ModCommandContext&, const Source2ModCommandArgs&, bool post)>;
using CommandHandler = Source2ModCallback<Action(const Source2ModCommandContext&, const Source2ModCommandArgs&, bool post)>;

virtual Source2ModHookId RegisterChatListener(const char* pchName, ChatHandler handler, const char* pchPermission = nullptr) = 0;
virtual bool UnregisterChatListener(const char* pchName, const ChatHandler& handler) = 0;
virtual bool UnregisterChatListener(Source2ModHookId id) = 0;

virtual Source2ModHookId RegisterConCommand(const char* pchName, ChatHandler handler, const char* pchPermission = nullptr) = 0;
virtual bool UnregisterConCommand(const char* pchName, const ChatHandler& handler) = 0;
virtual bool UnregisterConCommand(Source2ModHookId id) = 0;

virtual Source2ModHookId RegisterConListener(const char* pchName, CommandHandler handler, bool post) = 0;
virtual bool UnregisterConListener(const char* pchName, const CommandHandler& handler, bool post) = 0;
virtual bool UnregisterConListener(Source2ModHookId id) = 0;

virtual void ReplyToCommand(const Source2ModCommandContext& ctx, const char* pszMessage) = 0;

Scripts call the same methods on Source2ModCommands (source2mod/commands.inc); there the ids are plain ints and unregistering goes by id only.

There is no plugin id argument. A handler is a Source2ModCallback: a lambda, a free or static function, or an object and a method, and the core reads which plugin it belongs to off the handler itself:

REGISTER_CON_COMMAND("hello", OnHello);                                 // function
REGISTER_CON_COMMAND("hello", &Commands::OnHello);                      // static method
REGISTER_CON_COMMAND("hello", SOURCE2MOD_MEMBER(this, &MyPlugin::OnHello)); // object and method
REGISTER_CON_COMMAND("hello", [](auto& ctx, auto& args, bool post) {}); // lambda

SOURCE2MOD_MEMBER(inst, func) (or { this, &MyPlugin::OnHello }) and SOURCE2MOD_STATIC(func) are SourceHook's SH_MEMBER / SH_STATIC spelling of the same thing. Inside a macro argument use SOURCE2MOD_MEMBER: the comma in a bare { this, &MyPlugin::OnHello } would split it into two arguments.

C++ only — a script's handler is always a named public function: Source2ModCommands.RegisterConCommand("hello", Cmd_Hello);.

The macros just forward:

MacroCalls
REGISTER_CON_COMMAND(name, handler)RegisterConCommand(name, handler)
REGISTER_CHAT_LISTENER(name, handler)RegisterChatListener(name, handler)
REGISTER_CON_LISTENER(name, handler, post)RegisterConListener(name, handler, post)

There are no unregister macros; call Unregister* on g_pSource2ModCommands (in a script, on Source2ModCommands) (see Unregistering).

Commands and chat listeners take a ChatHandler, which returns nothing. A handler that needs to block something is a listener, which returns an Action — see Command listeners. Full reference: ISource2ModCommands.

Console commands

REGISTER_CON_COMMAND(name, handler) does two things:

  1. Creates a real engine ConCommand called name, so the console knows it (autocomplete, find, no "Unknown command").
  2. Registers your handler for name and for the chat forms, so !name and /name in chat reach the same code.

If the name already exists in the engine — the game's own command or another plugin's — Source2Mod cannot take it over. It logs a warning and registers only the handler, so your code still runs whenever that command is dispatched. To replace someone else's command, use a pre listener that returns Action::Supersede instead.

Register names in lower case. The dispatcher lower-cases the typed command before looking up handlers, so a handler registered as "MyCmd" is never found — while !mycmd and !MyCmd in chat both reach one registered as "mycmd".

Arguments

Source2ModCommandArgs is the tokenized command line — Source2Mod's own type, built at the dispatch boundary, so neither the engine's CCommand nor CCommandContext is part of the plugin API (their layout can change without a rebuild). The calls are the ones the engine type has:

CallReturns
args.ArgC()number of tokens, including the command itself
args.Arg(0)the command name
args.Arg(i)token i (1-based for real arguments)
args.ArgS()everything after the command name, as one string
args.GetCommandString()the whole line

From chat the trigger is already stripped: !hello a b gives Arg(0) == "hello", Arg(1) == "a", ArgC() == 3. Arg(i) past the end is "", never null; the strings belong to the dispatch and are gone when the handler returns — copy what has to outlive it. In a script, Arg, ArgS and GetCommandString copy into a buffer you pass.

plugin.cpp
REGISTER_CON_COMMAND("sample_echo", [](const Source2ModCommandContext& context, const Source2ModCommandArgs& args, bool post)
{
    if (args.ArgC() < 2)
    {
        Reply(context, "Usage: sample_echo <text>");
        return;
    }

    CCSPlayerController* pCaller = CallerOf(context);
    Reply(context, "%s said: %s (%d arguments)",
          pCaller ? pCaller->GetPlayerName() : "Console", args.ArgS(), args.ArgC() - 1);
});

The caller

Source2ModCommandContext::GetPlayerSlot() tells you who ran the command. A player has a valid slot; the server console (and RCON) has none — the slot is -1 and IsValid() is false.

plugin.cpp
// A controller for a player, nullptr for the server console.
static CCSPlayerController* CallerOf(const Source2ModCommandContext& context)
{
    const CPlayerSlot slot = context.GetPlayerSlot();
    return slot.IsValid() ? CCSPlayerController::FromSlot(slot) : nullptr;
}

GetSource() says where it was typed:

Source2ModCommandSourceTyped
Consolein a console — the server's, RCON, or the player's own
Chatin chat with a public trigger (!kick); the message shows
SilentChatin chat with a silent trigger (/kick); the message is hidden

IsFromChat() is true for both chat kinds.

Replying

ReplyToCommand(ctx, message) answers where the command came from: the player's chat for a chat command, the player's console for one typed there, the server console for the server. REPLY_TO_COMMAND formats first; a script's Source2ModCommands.ReplyToCommand(ctx, format, ...) formats itself:

REGISTER_CON_COMMAND("hp", [](const Source2ModCommandContext& ctx, const Source2ModCommandArgs& args, bool post)
{
    CCSPlayerController* pPlayer = CallerOf(ctx);
    CCSPlayerPawn* pPawn = pPlayer ? pPlayer->GetPlayerPawn() : nullptr;
    if (!pPawn)
    {
        REPLY_TO_COMMAND(ctx, "Players only.");
        return;
    }

    REPLY_TO_COMMAND(ctx, "You have %d HP.", pPawn->m_iHealth());
});

For a specific place regardless of where it was typed, use the controller's PrintToChat, PrintToConsole or PrintToCenter on CBasePlayerController.

The post argument

Command handlers are registered as pre handlers and are called once, with post == false. The parameter is there because the type is shared with listeners; for commands you can ignore it.

Permissions

The ConCommands Source2Mod creates are client-executable, so a command registered without a permission runs for any player. Give it one and the core checks it before your handler runs:

REGISTER_CON_COMMAND("kick", OnKick, "myplugin.admin.kick");

A player without it does not reach the handler; they are told so where they typed the command (core.json's AccessDeniedMessage, or a plugin's own answer). The server console and RCON always may. The server owner can give the command another permission under Overrides in permissions.json. Who holds what — admins, groups, wildcards — is on the Permissions page.

For something only the server console should run, check the slot:

REGISTER_CON_COMMAND("myplugin_reload", [](const Source2ModCommandContext& ctx, const Source2ModCommandArgs& args, bool post)
{
    // Server console and RCON only.
    if (ctx.GetPlayerSlot().IsValid())
        return;

    // ...
});

Chat-only commands

REGISTER_CHAT_LISTENER(name, handler) (Source2ModCommands.RegisterChatListener in a script) registers the same handler without creating a console command. It fires when a player types !name or /name (or any other configured chat trigger) in chat:

plugin.cpp
REGISTER_CHAT_LISTENER("noshake", [this](const Source2ModCommandContext& context, const Source2ModCommandArgs& args, bool post)
{
    const CPlayerSlot slot = context.GetPlayerSlot();
    if (!slot.IsValid())
        return;

    m_NoShakeMask ^= (uint64_t(1) << slot.Get());

    const bool bOff = (m_NoShakeMask >> slot.Get()) & 1;
    Reply(context, "Screen shakes are now %s for you.", bOff ? "off" : "on");
});

A chat listener cannot hide the message: with the public trigger (!) it stays visible, with the silent trigger (/) the core hides it anyway. To hide !name messages too, register a pre listener on name and return Action::Supersede — after the command or chat listener, because a Supersede stops the handlers registered behind it.

Unregistering

You rarely need to — everything is dropped when your plugin unloads. To turn a command off while the plugin keeps running (a script goes by the id the registration returned):

// By the id the registration returned -- the only way for a lambda.
Source2ModHookId m_hHello = REGISTER_CON_COMMAND("hello", [](auto& ctx, auto& args, bool post) { /* ... */ });
g_pSource2ModCommands->UnregisterConCommand(m_hHello);     // the handler and its chat forms

// Or by the same function / object and method again.
REGISTER_CHAT_LISTENER("noshake", SOURCE2MOD_MEMBER(this, &MyPlugin::OnNoShake));
g_pSource2ModCommands->UnregisterChatListener("noshake", SOURCE2MOD_MEMBER(this, &MyPlugin::OnNoShake));

Each returns true when it found the handler. Several handlers may share one command name; each runs, and unregistering removes only the one you name. UnregisterConCommand removes your handler. The engine ConCommand stays until the plugin unloads (it just does nothing in the meantime); registering the same name again reuses it. On unload Source2Mod gives the name back to the engine, so a reloaded plugin — or another one — can register it again.

See also

On this page