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.
#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;
}public void OnPluginStart()
{
Source2ModCommands.RegisterConCommand("hello", Cmd_Hello);
}
public void Cmd_Hello(Source2ModCommandContext ctx, Source2ModCommandArgs args, bool post)
{
char text[256];
args.ArgS(text, sizeof(text));
Source2ModApi.ConPrintf("hello from slot %d: %s", ctx.GetPlayerSlot().Get(), text);
}hello now works from the server console, from a client's console, and from
chat as !hello or /hello.
The API
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) {}); // lambdaSOURCE2MOD_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:
| Macro | Calls |
|---|---|
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:
- Creates a real engine
ConCommandcalledname, so the console knows it (autocomplete,find, no "Unknown command"). - Registers your handler for
nameand for the chat forms, so!nameand/namein 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:
| Call | Returns |
|---|---|
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.
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);
});// in OnPluginStart()
Source2ModCommands.RegisterConCommand("sample_echo", Cmd_Echo);
public void Cmd_Echo(Source2ModCommandContext context, Source2ModCommandArgs args, bool post)
{
if (args.ArgC() < 2)
{
Reply(context, "Usage: sample_echo <text>");
return;
}
char text[256], name[64] = "Console";
args.ArgS(text, sizeof(text));
CCSPlayerController caller = CallerOf(context);
if (caller != NULL_ENTITY)
caller.GetPlayerName(name, sizeof(name));
Reply(context, "%s said: %s (%d arguments)", name, text, 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.
// 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;
}// A controller for a player, NULL_ENTITY for the server console.
CCSPlayerController CallerOf(Source2ModCommandContext context)
{
CPlayerSlot slot = context.GetPlayerSlot();
return slot.IsValid() ? CCSPlayerController.FromSlot(slot) : NULL_ENTITY;
}GetSource() says where it was typed:
Source2ModCommandSource | Typed |
|---|---|
Console | in a console — the server's, RCON, or the player's own |
Chat | in chat with a public trigger (!kick); the message shows |
SilentChat | in 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());
});// in OnPluginStart()
Source2ModCommands.RegisterConCommand("hp", Cmd_Hp);
public void Cmd_Hp(Source2ModCommandContext ctx, Source2ModCommandArgs args, bool post)
{
CCSPlayerController player = CallerOf(ctx);
CCSPlayerPawn pawn = player != NULL_ENTITY ? player.GetPlayerPawn() : NULL_ENTITY;
if (pawn == NULL_ENTITY)
{
Source2ModCommands.ReplyToCommand(ctx, "Players only.");
return;
}
Source2ModCommands.ReplyToCommand(ctx, "You have %d HP.", pawn.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");Source2ModCommands.RegisterConCommand("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;
// ...
});public void Cmd_Reload(Source2ModCommandContext ctx, 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:
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");
});int g_NoShakeMask[2]; // one bit per player slot
// in OnPluginStart()
Source2ModCommands.RegisterChatListener("noshake", Chat_NoShake);
public void Chat_NoShake(Source2ModCommandContext context, Source2ModCommandArgs args, bool post)
{
CPlayerSlot slot = context.GetPlayerSlot();
if (!slot.IsValid())
return;
if (PlayerMask_Has(g_NoShakeMask, slot.Get()))
PlayerMask_Remove(g_NoShakeMask, slot.Get());
else
PlayerMask_Add(g_NoShakeMask, slot.Get());
bool off = PlayerMask_Has(g_NoShakeMask, slot.Get());
Reply(context, "Screen shakes are now %s for you.", off ? "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));// By the id the registration returned -- the only way in a script.
int helloId = Source2ModCommands.RegisterConCommand("hello", Cmd_Hello);
Source2ModCommands.UnregisterConCommand(helloId); // the handler and its chat forms
int noshakeId = Source2ModCommands.RegisterChatListener("noshake", OnNoShake);
Source2ModCommands.UnregisterChatListener(noshakeId);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
- Command listeners — hooking commands you do not own.
ISource2ModCommandsreferenceISource2ModTypes— theActionenum.