Difference between revisions of "Commands (SourceMod Scripting)"

From AlliedModders Wiki
Jump to: navigation, search
m (Update highlighting)
m (Update formatting for readability and consistency)
Line 25: Line 25:
 
public void OnPluginStart()
 
public void OnPluginStart()
 
{
 
{
  RegServerCmd("test_command", Command_Test);
+
    RegServerCmd("test_command", Command_Test);
 
}
 
}
  
 
public Action Command_Test(int args)
 
public Action Command_Test(int args)
 
{
 
{
  char arg[128];
+
    char arg[128];
  char full[256];
+
    char full[256];
  
  GetCmdArgString(full, sizeof(full));
+
    GetCmdArgString(full, sizeof(full));
  
  PrintToServer("Argument string: %s", full);
+
    PrintToServer("Argument string: %s", full);
  PrintToServer("Argument count: %d", args);
+
    PrintToServer("Argument count: %d", args);
  for (int i=1; i<=args; i++)
+
 
  {
+
    for (int i = 1; i <= args; i++)
    GetCmdArg(i, arg, sizeof(arg));
+
    {
    PrintToServer("Argument %d: %s", i, arg);
+
        GetCmdArg(i, arg, sizeof(arg));
  }
+
        PrintToServer("Argument %d: %s", i, arg);
  return Plugin_Handled;
+
    }
 +
 
 +
    return Plugin_Handled;
 
}
 
}
 
</sourcepawn>
 
</sourcepawn>
Line 51: Line 53:
 
public void OnPluginStart()
 
public void OnPluginStart()
 
{
 
{
  RegServerCmd("kickid", Command_KickId);
+
    RegServerCmd("kickid", Command_KickId);
 
}
 
}
  
 
public Action Command_Kickid(int args)
 
public Action Command_Kickid(int args)
 
{
 
{
  return Plugin_Handled;
+
    return Plugin_Handled;
 
}
 
}
 
</sourcepawn>
 
</sourcepawn>
Line 79: Line 81:
 
<sourcepawn>public void OnPluginStart()
 
<sourcepawn>public void OnPluginStart()
 
{
 
{
  RegConsoleCmd("test_command", Command_Test);
+
    RegConsoleCmd("test_command", Command_Test);
 
}
 
}
  
 
public Action Command_Test(int client, int args)
 
public Action Command_Test(int client, int args)
 
{
 
{
  char arg[128];
+
    char arg[128];
  char full[256];
+
    char full[256];
 +
 
 +
    GetCmdArgString(full, sizeof(full));
 +
 
 +
    if (client)
 +
    {
 +
        PrintToServer("Command from client: %N", name);
 +
    }
 +
    else
 +
    {
 +
        PrintToServer("Command from server.");
 +
    }
  
  GetCmdArgString(full, sizeof(full));
+
    PrintToServer("Argument string: %s", full);
 +
    PrintToServer("Argument count: %d", args);
  
  if (client)
+
    for (int i = 1; i <= args; i++)
  {
+
    {
    PrintToServer("Command from client: %N", name);
+
        GetCmdArg(i, arg, sizeof(arg));
  } else {
+
        PrintToServer("Argument %d: %s", i, arg);
    PrintToServer("Command from server.");
+
    }
  }
 
  
  PrintToServer("Argument string: %s", full);
+
     return Plugin_Handled;
  PrintToServer("Argument count: %d", args);
 
  for (int i=1; i<=args; i++)
 
  {
 
     GetCmdArg(i, arg, sizeof(arg));
 
    PrintToServer("Argument %d: %s", i, arg);
 
  }
 
  return Plugin_Handled;
 
 
}</sourcepawn>
 
}</sourcepawn>
  
Line 121: Line 127:
 
*Argument #3: yams
 
*Argument #3: yams
  
<sourcepawn>ConVar ff = null;
+
<sourcepawn>ConVar g_cvarFF = null;
  
 
public void OnPluginStart()
 
public void OnPluginStart()
 
{
 
{
  ff = FindConVar("mp_friendlyfire");
+
    g_cvarFF = FindConVar("mp_friendlyfire");
 
}
 
}
  
 
public Action OnClientSayCommand(int client, const char[] command, const char[] sArgs)
 
public Action OnClientSayCommand(int client, const char[] command, const char[] sArgs)
 
{
 
{
  if (strcmp(sArgs, "ff", false) == 0)
+
    if (strcmp(sArgs, "ff", false) == 0)
  {
 
    if (ff != null)
 
 
     {
 
     {
      if (ff.BoolValue)
+
        if (g_cvarFF != null)
      {
+
        {
        PrintToChat(client, "Friendly fire is enabled.");
+
            if (g_cvarFF.BoolValue)
      }  
+
            {
                        else  
+
                PrintToChat(client, "Friendly fire is enabled.");
                        {
+
            }  
        PrintToChat(client, "Friendly fire is disabled.");
+
            else  
      }
+
            {
      /* Block the client's messsage from broadcasting */
+
                PrintToChat(client, "Friendly fire is disabled.");
      return Plugin_Handled;
+
            }
 +
 
 +
            /* Block the client's messsage from broadcasting */
 +
            return Plugin_Handled;
 +
        }
 
     }
 
     }
  }
 
  
  /* Let say continue normally */
+
    /* Let say continue normally */
  return Plugin_Continue;
+
    return Plugin_Continue;
 
}</sourcepawn>
 
}</sourcepawn>
  
Line 158: Line 165:
 
<sourcepawn>public void OnPluginStart()
 
<sourcepawn>public void OnPluginStart()
 
{
 
{
  RegAdminCmd("admin_kick", Command_Kick, ADMFLAG_KICK, "Kicks a player by name");
+
    RegAdminCmd("admin_kick", Command_Kick, ADMFLAG_KICK, "Kicks a player by name");
 
}
 
}
  
 
public Action Command_Kick(int client, int args)
 
public Action Command_Kick(int client, int args)
 
{
 
{
  if (args < 1)
+
    if (args < 1)
  {
+
    {
    PrintToConsole(client, "Usage: admin_kick <name>");
+
        PrintToConsole(client, "Usage: admin_kick <name>");
    return Plugin_Handled;
+
        return Plugin_Handled;
  }
+
    }
  
  char name[32];
+
    char name[32];
        int target = -1;
+
    int target = -1;
  GetCmdArg(1, name, sizeof(name));
+
    GetCmdArg(1, name, sizeof(name));
  
  for (int i=1; i<=MaxClients; i++)
+
    for (int i = 1; i <= MaxClients; i++)
  {
 
    if (!IsClientConnected(i))
 
 
     {
 
     {
      continue;
+
        if (!IsClientConnected(i))
 +
        {
 +
            continue;
 +
        }
 +
 
 +
        char other[32];
 +
        GetClientName(i, other, sizeof(other));
 +
 
 +
        if (StrEqual(name, other))
 +
        {
 +
            target = i;
 +
        }
 
     }
 
     }
    char other[32];
+
 
    GetClientName(i, other, sizeof(other));
+
     if (target == -1)
     if (StrEqual(name, other))
 
 
     {
 
     {
      target = i;
+
        PrintToConsole(client, "Could not find any player with the name: \"%s\"", name);
 +
        return Plugin_Handled;
 
     }
 
     }
  }
 
  
  if (target == -1)
+
    KickClient(target);
  {
+
 
    PrintToConsole(client, "Could not find any player with the name: \"%s\"", name);
 
 
     return Plugin_Handled;
 
     return Plugin_Handled;
  }
 
 
  KickClient(target);
 
 
  return Plugin_Handled;
 
 
}</sourcepawn>
 
}</sourcepawn>
  
Line 219: Line 228:
 
<sourcepawn>public Action Command_Kick(int client, int args)
 
<sourcepawn>public Action Command_Kick(int client, int args)
 
{
 
{
  if (args < 1)
+
    if (args < 1)
  {
+
    {
    PrintToConsole(client, "Usage: admin_kick <name>");
+
        PrintToConsole(client, "Usage: admin_kick <name>");
    return Plugin_Handled;
+
        return Plugin_Handled;
  }
+
    }
 +
 
 +
    char name[32];
 +
    int target = -1;
 +
    GetCmdArg(1, name, sizeof(name));
 +
 
 +
    for (int i = 1; i <= MaxClients; i++)
 +
    {
 +
        if (!IsClientConnected(i))
 +
        {
 +
            continue;
 +
        }
  
  char name[32];
+
        char other[32];
         int target = -1;
+
         GetClientName(i, other, sizeof(other));
  GetCmdArg(1, name, sizeof(name));
+
       
 +
        if (StrEqual(name, other))
 +
        {
 +
            target = i;
 +
        }
 +
    }
  
  for (int i=1; i<=MaxClients; i++)
+
    if (target == -1)
  {
 
    if (!IsClientConnected(i))
 
 
     {
 
     {
      continue;
+
        PrintToConsole(client, "Could not find any player with the name: \"%s\"", name);
 +
        return Plugin_Handled;
 
     }
 
     }
    char other[32];
+
 
    GetClientName(i, other, sizeof(other));
+
     if (!CanUserTarget(client, target))
     if (StrEqual(name, other))
 
 
     {
 
     {
      target = i;
+
        PrintToConsole(client, "You cannot target this client.");
 +
        return Plugin_Handled;
 
     }
 
     }
  }
 
  
  if (target == -1)
+
    KickClient(target);
  {
 
    PrintToConsole(client, "Could not find any player with the name: \"%s\"", name);
 
    return Plugin_Handled;
 
  }
 
  
  if (!CanUserTarget(client, target))
 
  {
 
    PrintToConsole(client, "You cannot target this client.");
 
 
     return Plugin_Handled;
 
     return Plugin_Handled;
  }
 
 
  KickClient(target);
 
 
  return Plugin_Handled;
 
 
}</sourcepawn>
 
}</sourcepawn>
  
Line 265: Line 276:
 
<sourcepawn>public Action OnClientCommand(int client, int args)
 
<sourcepawn>public Action OnClientCommand(int client, int args)
 
{
 
{
  char cmd[16];
+
    char cmd[16];
  GetCmdArg(0, cmd, sizeof(cmd)); /* Get command name */
+
    GetCmdArg(0, cmd, sizeof(cmd)); /* Get command name */
  
  if (StrEqual(cmd, "test_command"))
+
    if (StrEqual(cmd, "test_command"))
  {
+
    {
    /* Got the client command! Block it... */
+
        /* Got the client command! Block it... */
    return Plugin_Handled;
+
        return Plugin_Handled;
  }
+
    }
  
  return Plugin_Continue;
+
    return Plugin_Continue;
 
}</sourcepawn>
 
}</sourcepawn>
  

Revision as of 20:21, 29 March 2020

SourceMod allows you to create console commands similar to AMX Mod X. There are two main types of console commands:

  • Server Commands - Fired from one of the following input methods:
    • the server console itself
    • the remote console (RCON)
    • the ServerCommand() function, either from SourceMod or the Half-Life 2 engine
  • Console Commands - Fired from one of the following input methods:
    • a client's console
    • any of the server command input methods

For server commands, there is no client index. For console/client commands, there is a client index, but it may be 0 to indicate that the command is from the server.

Note that button-bound "commands," such as +attack and +duck, are not actually console commands. They are a separate command stream sent over the network, as they need to be tied to a specific frame.

Server Commands

As noted above, server commands are fired through the server console, whether remote, local, or through the Source engine. There is no client index associated with a server command.

Server commands are registered through the RegServerCmd() function defined in console.inc. When registering a server command, you may be hooking an already existing command, and thus the return value is important.

  • Plugin_Continue - The original server command will be processed, if there was one. If the server command was created by a plugin, this has no effect.
  • Plugin_Handled - The original server command will not be processed, if there was one. If the server command was created by a plugin, this has no effect.
  • Plugin_Stop - The original server command will not be processed, if there was one. Additionally, no further hooks will be called for this command until it is fired again.

Adding Server Commands

Let's say we want to add a test command to show how Half-Life 2 breaks down command arguments. A possible implementation might be

public void OnPluginStart()
{
    RegServerCmd("test_command", Command_Test);
}
 
public Action Command_Test(int args)
{
    char arg[128];
    char full[256];
 
    GetCmdArgString(full, sizeof(full));
 
    PrintToServer("Argument string: %s", full);
    PrintToServer("Argument count: %d", args);
 
    for (int i = 1; i <= args; i++)
    {
        GetCmdArg(i, arg, sizeof(arg));
        PrintToServer("Argument %d: %s", i, arg);
    }
 
    return Plugin_Handled;
}

Blocking Server Commands

Let's say we wanted to disable the "kickid" command on the server. There's no real good reason to do this, but for example's sake:

public void OnPluginStart()
{
    RegServerCmd("kickid", Command_KickId);
}
 
public Action Command_Kickid(int args)
{
    return Plugin_Handled;
}

Console Commands

Unlike server commands, console commands can be triggered by either the server or the client, so the callback function receives a client index as well as the argument count. If the server fired the command, the client index will be 0.

When returning the Action from the callback, the following effects will happen:

  • Plugin_Continue: The original functionality of the command (if any) will still be processed. If there was no original functionality, the client will receive "Unknown command" in their console.
  • Plugin_Handled: The original functionality of the command (if any) will be blocked. If there was no functionality originally, this prevents clients from seeing "Unknown command" in their console.
  • Plugin_Stop: Same as Plugin_Handled, except that this will be the last hook called.
  • Plugin_Changed: Inputs or outputs have been overridden with new values.

Note that, unlike AMX Mod X, SourceMod does not allow you to register command filters. I.e., there is no equivalent to this notation:

register_clcmd("say /ff", "Command_SayFF");

This notation was removed to make our internal code simpler and faster. Writing the same functionality is easy, and demonstrated below.

Adding Commands

Adding client commands is very simple. Let's port our earlier testing command to display information about the client as well.

public void OnPluginStart()
{
    RegConsoleCmd("test_command", Command_Test);
}
 
public Action Command_Test(int client, int args)
{
    char arg[128];
    char full[256];
 
    GetCmdArgString(full, sizeof(full));
 
    if (client)
    {
        PrintToServer("Command from client: %N", name);
    }
    else
    {
        PrintToServer("Command from server.");
    }
 
    PrintToServer("Argument string: %s", full);
    PrintToServer("Argument count: %d", args);
 
    for (int i = 1; i <= args; i++)
    {
        GetCmdArg(i, arg, sizeof(arg));
        PrintToServer("Argument %d: %s", i, arg);
    }
 
    return Plugin_Handled;
}

Hooking Commands

A common example is hooking the say command. Let's say we want to tell players whether FF is enabled when they say '/ff' in game.

Before we implement this, a common point of confusion with the 'say' command is that Half-Life 2 (and Half-Life 1), by default, send it with the text in one big quoted string. This means if you say "I like hot dogs," your command will be broken down as such:

  • Argument string: "I like hot dogs"
  • Argument count: 1
  • Argument #1: I like hot dogs

However, if a player types this in their console: say I like yams, it will be broken up as:

  • Argument string: I like yams
  • Argument count: 3
  • Argument #1: I
  • Argument #2: like
  • Argument #3: yams
ConVar g_cvarFF = null;
 
public void OnPluginStart()
{
    g_cvarFF = FindConVar("mp_friendlyfire");
}
 
public Action OnClientSayCommand(int client, const char[] command, const char[] sArgs)
{
    if (strcmp(sArgs, "ff", false) == 0)
    {
        if (g_cvarFF != null)
        {
            if (g_cvarFF.BoolValue)
            {
                PrintToChat(client, "Friendly fire is enabled.");
            } 
            else 
            {
                PrintToChat(client, "Friendly fire is disabled.");
            }
 
            /* Block the client's messsage from broadcasting */
            return Plugin_Handled;
        }
    }
 
    /* Let say continue normally */
    return Plugin_Continue;
}

Here we use the OnClientSayCommand forward where as command contains the type (example: say or say_team) and sArgs containing the users input

Creating Admin Commands

Let's create a simple admin command which kicks another player by their full name.

public void OnPluginStart()
{
    RegAdminCmd("admin_kick", Command_Kick, ADMFLAG_KICK, "Kicks a player by name");
}
 
public Action Command_Kick(int client, int args)
{
    if (args < 1)
    {
        PrintToConsole(client, "Usage: admin_kick <name>");
        return Plugin_Handled;
    }
 
    char name[32];
    int target = -1;
    GetCmdArg(1, name, sizeof(name));
 
    for (int i = 1; i <= MaxClients; i++)
    {
        if (!IsClientConnected(i))
        {
            continue;
        }
 
        char other[32];
        GetClientName(i, other, sizeof(other));
 
        if (StrEqual(name, other))
        {
            target = i;
        }
    }
 
    if (target == -1)
    {
        PrintToConsole(client, "Could not find any player with the name: \"%s\"", name);
        return Plugin_Handled;
    }
 
    KickClient(target);
 
    return Plugin_Handled;
}

Immunity

In our previous example, we did not take immunity into account. Immunity is a much more complex system in SourceMod than it was in AMX Mod X, and there is no simple flag to denote its permissions. Instead, two functions are provided:

  • CanAdminTarget: Tests raw AdminId values for immunity.
  • CanUserTarget: Tests in-game clients for immunity.

While immunity is generally tested player versus player, it is possible you might want to check for immunity and not have a targetting client. While there is no convenience function for this yet, a good idea might be to check for either default or global immunity on the player's groups (these can be user-defined for non-player targeted scenarios).

When checking for immunity, the following heuristics are performed in this exact order:

  1. If the targeting AdminId is INVALID_ADMIN_ID, targeting fails.
  2. If the targetted AdminId is INVALID_ADMIN_ID, targeting succeeds.
  3. If the targeting admin has Admin_Root (ADMFLAG_ROOT), targeting succeeds.
  4. If the targetted admin has global immunity, targeting fails.
  5. If the targetted admin has default immunity, and the targeting admin belongs to no groups, targeting fails.
  6. If the targetted admin has specific immunity from the targeting admin via group immunities, targeting fails.
  7. If no conclusion is reached via the previous steps, targeting succeeds.

So, how can we adapt our function about to use immunity?

public Action Command_Kick(int client, int args)
{
    if (args < 1)
    {
        PrintToConsole(client, "Usage: admin_kick <name>");
        return Plugin_Handled;
    }
 
    char name[32];
    int target = -1;
    GetCmdArg(1, name, sizeof(name));
 
    for (int i = 1; i <= MaxClients; i++)
    {
        if (!IsClientConnected(i))
        {
            continue;
        }
 
        char other[32];
        GetClientName(i, other, sizeof(other));
 
        if (StrEqual(name, other))
        {
            target = i;
        }
    }
 
    if (target == -1)
    {
        PrintToConsole(client, "Could not find any player with the name: \"%s\"", name);
        return Plugin_Handled;
    }
 
    if (!CanUserTarget(client, target))
    {
        PrintToConsole(client, "You cannot target this client.");
        return Plugin_Handled;
    }
 
    KickClient(target);
 
    return Plugin_Handled;
}

Client-Only Commands

SourceMod exposes a forward that is called whenever a client executes any command string in their console, called OnClientCommand. An example of this looks like:

public Action OnClientCommand(int client, int args)
{
    char cmd[16];
    GetCmdArg(0, cmd, sizeof(cmd)); /* Get command name */
 
    if (StrEqual(cmd, "test_command"))
    {
        /* Got the client command! Block it... */
        return Plugin_Handled;
    }
 
    return Plugin_Continue;
}

It is worth noting that not everything a client sends will be available through this command. Command registered via external sources in C++ may not be available, especially if they are created via CON_COMMAND in the game mod itself. For example, "say" is usually implemented this way, because it can be used by both clients and the server, and thus it does not channel through this forward.


Chat Triggers

SourceMod will automatically create chat triggers for every command you make. For example, if you create a console command called "sm_megaslap", administrators will be able to type any of the following commands in say/say_team message modes:

!sm_megaslap
!megaslap
/sm_megaslap
/megaslap

SourceMod then executes this command and its arguments as if it came from the client console.

  • "!" is the default public trigger (PublicChatTrigger in configs/core.cfg) and your entry will be displayed to all clients as normal.
  • "/" is the default silent trigger (SilentChatTrigger in configs/core.cfg) and your entry will be blocked from being displayed.

SourceMod will only execute commands registered with RegConsoleCmd or RegAdminCmd, and only if those commands are not already provided by Half-Life 2 or the game mod. If the command is prefixed with "sm_" then the "sm_" can be omitted from the chat trigger.

Console commands which wish to support usage as a chat trigger should not use PrintTo* natives. Instead, they should use ReplyToCommand(), which will automatically print your message either as a chat message or to the client's console, depending on the source of the command.

Warning: This template (and by extension, language format) should not be used, any pages using it should be switched to Template:Languages

View this page in:  English  Russian  简体中文(Simplified Chinese)