Table of Contents

Command System

The SampSharp command system provides a declarative way to handle player and console commands through attributes. Commands are discovered automatically from ISystem implementations and can include complex features like overloading, aliasing, command groups, and permission checking.

Setup

To enable the command system, call UseCommands() in your IEcsStartup.Initialize() method:

public class Startup : IEcsStartup
{
    public void Initialize(IStartupContext context)
    {
        context.UseEntities()
            .UseCommands();
    }
}

Core Concepts

Player Commands vs Console Commands

Player Commands are invoked when a player types a message starting with / in the game chat:

  • Always require a Player parameter as the first argument
  • Automatically receive the player who executed the command
  • Intended for in-game gameplay commands

Console Commands (also known as RCON commands) are executed from the server console:

  • Execute without automatic player context (typically server-side)
  • Optional ConsoleCommandDispatchContext parameter can provide player context if the command was executed by a player
  • Do not have automatic permission checking
  • Useful for server administration and debugging

Registering Command Handlers

Command methods must be part of an ISystem implementation. The command system automatically discovers and registers these methods:

public class MyCommandsSystem(IEntityManager entityManager) : ISystem
{
    [PlayerCommand(Name = "hello")]
    public void HelloCommand(Player player)
    {
        player.SendClientMessage("Hello!");
    }

    [ConsoleCommand(Name = "server_status")]
    public void ServerStatus()
    {
        Console.WriteLine("Server is running.");
    }
}

Command Naming

Command names come from the Name property of the attribute. If Name is not specified, the command system infers it from the method name:

  • Method name is converted to lowercase
  • If the method name ends with "Command", that suffix is removed

Examples:

  • Method KillCommand() → command name kill
  • Method ServerStatus() → command name serverstatus
  • Method StatusCommand() → command name status
  • Method Help() → command name help

By default, command names are case-insensitive, so /Kill, /KILL, and /kill all invoke the same command. If you need exact case matching, configure case sensitivity in your startup:

public class Startup : IEcsStartup
{
    public void Initialize(IStartupContext context)
    {
        context.UseEntities()
            .UsePlayerCommands(cfg => cfg.StringComparison = StringComparison.Ordinal)
            .UseConsoleCommands(cfg => cfg.StringComparison = StringComparison.Ordinal);
    }
}

With StringComparison.Ordinal, /kill and /Kill are now treated as different commands.

Player Commands

PlayerCommandAttribute

The [PlayerCommand] attribute marks a method as a player command:

[PlayerCommand(Name = "kill")]
public void KillCommand(Player player)
{
    player.Health = 0;
}

Capabilities:

  • Automatically detects the first parameter type to determine who can execute the command
  • Supports Player or custom Component types as the first parameter

Parameter Resolution: The first parameter determines command execution context:

  • If Player: the command can be executed by any player
  • If custom Component (like a VIPPlayer component): the command only works for players with that component
  • Subsequent parameters are parsed from user input

Example:

[PlayerCommand(Name = "spawn")]
public void SpawnCommand(Player player, VehicleModelType model, IWorldService worldService)
{
    player.SendClientMessage($"Spawned a {model}!");
    var vehicle = worldService.CreateVehicle(model, player.Position, player.Angle, -1, -1);
    player.PutInVehicle(vehicle);
}

Console Commands

ConsoleCommandAttribute

The [ConsoleCommand] attribute marks a method as a console command:

Capabilities:

  • Optional ConsoleCommandDispatchContext parameter (if present, must be the first parameter) for command metadata
  • No permission checking (intended for server-side use)

See the Registering Command Handlers section above for a console command example.

Options

Command Aliasing

Use [Alias] to provide shorthand names for commands. Multiple aliases are supported:

[CommandGroup("economy")]
[PlayerCommand(Name = "money")]
[Alias("$", "cash")]
public void MoneyCommand(Player player)
{
    player.SendClientMessage($"Money: ${player.Money}");
}

Now /economy money, /$, and /cash all work. Note that aliases bypass the command group—they're global shortcuts regardless of the group hierarchy. Aliases work for both player and console commands.

Command Groups

Command groups organize commands into hierarchies. Apply groups to the entire system class or to individual methods using [CommandGroup]:

[CommandGroup("admin")]
public class AdminCommandsSystem : ISystem
{
    [PlayerCommand(Name = "kick")]
    public void KickCommand(Player player, Player target) { }

    [PlayerCommand(Name = "ban")]
    public void BanCommand(Player player, Player target) { }
}

This creates /admin kick and /admin ban commands. Groups can be stacked for deeper hierarchies by applying multiple [CommandGroup] attributes.

Command Overloading

Multiple command handlers can have the same name with different parameter signatures. This is useful for commands that support different use cases:

[CommandGroup("teleport")]
[PlayerCommand(Name = "player")]
public void TeleportCommand(Player player, Player target)
{
    // /teleport player <target_name>
    player.Position = target.Position;
    player.SendClientMessage($"Teleported to {target.Name}");
}

[CommandGroup("teleport")]
[PlayerCommand(Name = "player")]
public void TeleportCommand(Player player, float x, float y, float z)
{
    // /teleport player <x> <y> <z>
    player.Position = new Vector3(x, y, z);
    player.SendClientMessage($"Teleported to ({x}, {y}, {z})");
}

[CommandGroup("teleport")]
[PlayerCommand(Name = "player")]
[Alias("tp")]
public void TeleportCommand(Player player, Player target, float x, float y, float z)
{
    // /teleport player <target_name> <x> <y> <z> OR /tp <target_name> <x> <y> <z>
    // Admin command to teleport another player
    target.Position = new Vector3(x, y, z);
    player.SendClientMessage($"Teleported {target.Name} to ({x}, {y}, {z})");
}

The command system automatically selects the correct overload based on parameter types and count:

  • /teleport player Johnny calls the first overload (Player parameter)
  • /teleport player 100 200 50 calls the second overload (three float parameters)
  • /teleport player Johnny 100 200 50 or /tp Johnny 100 200 50 calls the third overload (Player + three floats)

Command overloading works for both player and console commands.

Optional Parameters

Commands support optional parameters to make command syntax more flexible. Optional parameters can have default values:

[PlayerCommand(Name = "money")]
public void MoneyCommand(Player player, int? amount = null)
{
    if (amount.HasValue)
    {
        player.Money = amount.Value;
        player.SendClientMessage($"Money set to ${amount.Value}");
    }
    else
    {
        player.SendClientMessage($"Current money: ${player.Money}");
    }
}

Now /money displays the current money, while /money 5000 sets it to 5000.

Dependency Injection

Commands can receive dependencies from your service collection. Any registered service can be injected as a parameter. Dependency injection parameters can appear anywhere in the method signature, even before optional parameters:

[PlayerCommand(Name = "money")]
public void MoneyCommand(Player player, IWorldService worldService, int? amount = null)
{
    if (amount.HasValue)
    {
        player.Money = amount.Value;
        player.SendClientMessage($"Money set to ${amount.Value}");
    }
    else
    {
        player.SendClientMessage($"Current money: ${player.Money}");
    }
}

The injected IWorldService is provided automatically, while amount remains optional from user input.

Command Introspection

The IPlayerCommandService provides access to all registered commands:

[PlayerCommand(Name = "help")]
public void HelpCommand(Player player, IPlayerCommandService commands)
{
    player.SendClientMessage("--- Available Commands ---");
    
    var commandList = commands.Registry.GetAll()
        .OrderBy(c => c.Name)
        .ToList();

    foreach (var cmd in commandList)
    {
        var aliases = cmd.Aliases.Count > 0 
            ? $" ({string.Join(", ", cmd.Aliases.Select(a => $"/{a.Name}"))})" 
            : "";
        player.SendClientMessage($"/{cmd.Name}{aliases}");
    }
}

Player Component Checking

Instead of Player (or EntityId), a custom Component can be used as the first parameter of a player command. In this case, the command is only available to players who have that component attached.

public class CivilianComponent : Component { }

[PlayerCommand(Name = "civonly")]
public void CivOnlyCommand(CivilianComponent civilian)
{
    // Only players with the CivilianComponent can use this command
    civilian.Player.SendClientMessage("You are a civilian!");
}

This allows for role-based or feature-based command access control using your own Component types.

Command Tags

Use [CommandTag] to attach custom metadata to commands as key-value pairs. Tags can be used for permission checking, categorization, or any other custom logic.

[PlayerCommand(Name = "kick")]
[CommandTag("permission", "admin")]
[CommandTag("category", "moderation")]
public void KickCommand(Player player, Player target)
{
    target.Kick();
}

Multiple tags can be applied to the same command. Tags are typically used by custom IPermissionChecker implementations to enforce access control:

public class MyPermissionChecker : IPermissionChecker
{
    public bool HasPermission(Player player, CommandDefinition command)
    {
        var permission = command.GetTag("permission");
        if (permission == null)
            return true; // No permission requirement
        
        return player.HasPermission(permission);
    }
}

Return Values

Command methods can return values to indicate success or failure:

  • void - Standard synchronous command execution (always treated as success)
  • bool - Returns true to indicate success, or false to indicate the command was not recognized (proceeds as if command was not found)
  • Task - Asynchronous command execution (always treated as success)
  • Task<bool> - For synchronous completion, the bool indicates success/failure. For async completion, the task completion is assumed to be success regardless of the return value

Parameter Types

The command system automatically parses the following parameter types from user input:

  • int, float, double - Numeric types
  • bool - Boolean values (true/false)
  • string - Text input (greedily consumes remaining input for the last parameter, or a single word for non-last parameters)
  • Player or EntityId - Player matched by player ID or player name
  • Enum types - Parsed by name (case-insensitive)

Any other parameter type not in this list is treated as a dependency injection parameter and resolved from the service collection.

Customization

The command system provides extension points for customization: permission checking, text formatting, and custom parameter type parsing.

Permission Checking

Implement IPermissionChecker to define custom permission logic for player commands. The default implementation allows all commands.

public class MyPermissionChecker : IPermissionChecker
{
    public bool HasPermission(Player player, CommandDefinition command)
    {
        // Check if the command has a "permission" tag
        var permission = command.GetTag("permission");
        if (permission == null)
            return true; // No permission requirement

        // Check your permission system
        return player.IsAdmin || HasPlayerPermission(player, permission);
    }

    private bool HasPlayerPermission(Player player, string permission)
    {
        // Implement your game's permission system here
        return false;
    }
}

Register your custom permission checker in your startup class to replace the default:

services.RemoveAll(typeof(IPermissionChecker));
services.AddSingleton<IPermissionChecker, MyPermissionChecker>();

Custom Text Formatting

Implement ICommandTextFormatter to customize how command usage, not found, and other error messages are formatted. This is useful for localization or custom message styles.

public class MyCommandTextFormatter : ICommandTextFormatter
{
    public string FormatCommandUsage(string commandName, string? group, CommandParameterInfo[] parameters, bool includeSlash = true)
    {
        var prefix = includeSlash ? "/" : "";
        var groupPrefix = group != null ? $"{group} " : "";
        var paramStr = string.Join(" ", parameters.Select(p => 
            p.IsOptional ? $"[{p.Name}]" : $"<{p.Name}>"
        ));
        
        return $"{prefix}{groupPrefix}{commandName} {paramStr}".Trim();
    }
}

Register your custom formatter in your startup class to replace the default:

services.RemoveAll(typeof(ICommandTextFormatter));
services.AddSingleton<ICommandTextFormatter, MyCommandTextFormatter>();

Custom Parameter Type Parsing

To support parsing of custom parameter types, implement ICommandParameterParserFactory. The recommended approach is to extend DefaultCommandParameterParserFactory and override the CreateParser method to handle your custom types:

public class MyCommandParameterParserFactory : DefaultCommandParameterParserFactory
{
    public override ICommandParameterParser? CreateParser(ParameterInfo[] parameters, int index)
    {
        var param = parameters[index];
        var paramType = param.ParameterType;

        // Handle custom type
        if (paramType == typeof(Vector3))
        {
            return new Vector3Parser();
        }

        // Fall back to default types
        return base.CreateParser(parameters, index);
    }
}

Register your custom parser factory in your startup class:

services.RemoveAll(typeof(ICommandParameterParserFactory));
services.AddSingleton<ICommandParameterParserFactory, MyCommandParameterParserFactory>();

Your custom parser receives the raw input string and should return a parsed value.