Skip to content

Attribute-Based Commands

RocketMod supports two approaches for defining commands: the traditional interface-based approach using IRocketCommand and an alternative attribute-based approach using RocketCommandAttribute, RocketCommandAliasAttribute, and RocketCommandPermissionAttribute. The attribute approach allows you to define command metadata inline on the class without implementing the full interface, which can lead to more concise code for simple commands.

This article covers the attribute-based command system, explains how it differs from the interface approach, when each approach is appropriate, and how to combine them in a single plugin. The 57 Studios™ team uses both approaches depending on the command's complexity, and this reference reflects that practical experience.

Visual Studio showing attribute-based command class with RocketCommand attribute

Prerequisites

The RocketCommand attribute

The [RocketCommand] attribute is the primary attribute for declaring a command. It replaces the Name, Help, Syntax, and AllowedCaller properties from the IRocketCommand interface.

csharp
using Rocket.API;
using Rocket.API.Commands;

[RocketCommand("heal", "Heals a player to full health.",
    Syntax = "/heal <player>",
    AllowedCaller = AllowedCaller.Player)]
public class HealCommand : IRocketCommand
{
    public void Execute(IRocketPlayer caller, string[] command)
    {
        // Command logic here
    }
}

Attribute parameters

ParameterRequiredTypePurpose
First positionalYesstringCommand name (maps to Name in IRocketCommand)
Second positionalYesstringHelp text (maps to Help)
SyntaxNostringUsage syntax (maps to Syntax)
AllowedCallerNoAllowedCallerDefaults to Both if not specified

Positional vs named parameters

The [RocketCommand] attribute constructor requires two positional arguments:

  1. The command name — what players type after /
  2. The help text — displayed in /help listings

Named parameters (Syntax, AllowedCaller) are optional. If omitted, Syntax defaults to an empty string, and AllowedCaller defaults to AllowedCaller.Both.

csharp
// Minimal declaration — only name and help
[RocketCommand("ping", "Check if the server is responsive.")]
public class PingCommand : IRocketCommand { ... }

// Full declaration with all parameters
[RocketCommand("kick", "Kicks a player from the server.",
    Syntax = "/kick <player> [reason]",
    AllowedCaller = AllowedCaller.Console)]
public class KickCommand : IRocketCommand { ... }

The RocketCommandAliasAttribute

The [RocketCommandAlias] attribute adds alternative names for the command. Unlike IRocketCommand where aliases are a List<string> property, the attribute-based approach uses a separate attribute that can be stacked.

csharp
[RocketCommand("teleport", "Teleport to a player or location.",
    Syntax = "/teleport <player>")]
[RocketCommandAlias("tp")]
[RocketCommandAlias("tpo")]
[RocketCommandAlias("goto")]
public class TeleportCommand : IRocketCommand
{
    public void Execute(IRocketPlayer caller, string[] command)
    {
        // Command logic
    }
}

Each [RocketCommandAlias] attribute adds one alias. You can stack as many as needed. Aliases behave identically to those defined through the Aliases property — they trigger the same Execute method and share the same permissions.

Alias attribute parameters

ParameterTypePurpose
First positionalstringThe alias keyword

The attribute takes a single positional parameter that is the alias string. The number of alias attributes is unlimited, but as a practical matter, more than five aliases suggests the command is trying to cover too many use cases.

The RocketCommandPermissionAttribute

The [RocketCommandPermission] attribute adds permission requirements for the command. Like aliases, permissions are defined as separate attributes.

csharp
[RocketCommand("heal", "Heals a player to full health.")]
[RocketCommandPermission("myplugin.heal")]
[RocketCommandPermission("myplugin.admin")]
public class HealCommand : IRocketCommand
{
    public void Execute(IRocketPlayer caller, string[] command)
    {
        // Command logic
    }
}

If the caller has ANY of the specified permissions, the command executes. If the caller has none of them, RocketMod blocks execution with a "You do not have permission to use this command" message.

Permission attribute parameters

ParameterTypePurpose
First positionalstringThe permission string

Multiple [RocketCommandPermission] attributes create an OR relationship — any one permission grants access. If you need an AND relationship (multiple permissions required simultaneously), you must check the second permission manually inside Execute using R.Permissions.HasPermission().

csharp
[RocketCommand("superadmin-action", "An action requiring two permissions.")]
[RocketCommandPermission("myplugin.superadmin")]
[RocketCommandPermission("myplugin.senior")]
public class SuperAdminActionCommand : IRocketCommand
{
    public void Execute(IRocketPlayer caller, string[] command)
    {
        // The attribute checks only OR — one permission is enough to reach here.
        // If AND is required, check the second permission manually.
        if (!R.Permissions.HasPermission(caller, "myplugin.senior"))
        {
            UnturnedChat.Say(caller,
                "You need both superadmin and senior permissions.", Color.red);
            return;
        }

        // Both permissions verified — execute the action
    }
}

Combining attributes with interface properties

When you use [RocketCommand], RocketMod reads the command metadata from the attribute at runtime. However, you can still implement IRocketCommand properties alongside the attribute. RocketMod gives priority to the attribute values when both are present.

csharp
[RocketCommand("heal", "Heals a player to full health.",
    Syntax = "/heal <player>",
    AllowedCaller = AllowedCaller.Player)]
public class HealCommand : IRocketCommand
{
    // These properties are still required by the interface
    // but RocketMod uses the attribute values instead.
    public string Name => "heal-override";           // Ignored — attribute wins
    public string Help => "This is ignored.";        // Ignored — attribute wins
    public string Syntax => "/heal <player> [amount]"; // Ignored — attribute wins
    public List<string> Aliases { get; } = new List<string>(); // Used for aliases not in attributes
    public string[] Permissions { get; } = new string[] { };    // Merged with attribute permissions
    public AllowedCaller AllowedCaller => AllowedCaller.Both;   // Ignored — attribute wins

    public void Execute(IRocketPlayer caller, string[] command) { }
}

The resolution order is:

  1. The attribute provides Name, Help, Syntax, and AllowedCaller. These override the interface properties if both are present.
  2. Aliases from [RocketCommandAlias] attributes are merged with aliases from the Aliases property.
  3. Permissions from [RocketCommandPermission] attributes are merged with permissions from the Permissions property.

This merge behavior means you can define the core metadata in attributes for readability and still use the interface properties for programmatic alias or permission generation.

When to use attributes vs interface

Use attributes when

  • The command is simple (fewer than 50 lines of logic).
  • The metadata is static and does not change between environments.
  • You want the metadata visible at the top of the class for quick scanning.
  • The command has a small, fixed set of aliases and permissions.

Use the interface when

  • The command metadata is dynamic (e.g., permissions are loaded from configuration or generated based on server state).
  • The command has complex alias logic (e.g., aliases that must be computed at runtime).
  • You are already using the interface pattern across your plugin and want consistency.
  • The command requires advanced features like custom cooldown behavior.

Both in the same plugin

A single plugin can mix attribute-based and interface-based commands. RocketMod discovers both through the same command scanning mechanism. The choice is per-class and has no effect on other classes in the same assembly.

csharp
// Attribute-based
[RocketCommand("ping", "Ping the server.")]
[RocketCommandAlias("p")]
public class PingCommand : IRocketCommand
{
    public void Execute(IRocketPlayer caller, string[] command)
    {
        UnturnedChat.Say(caller, "Pong!", Color.green);
    }
}

// Interface-based alongside attribute-based
public class HealCommand : IRocketCommand
{
    public string Name => "heal";
    public string Help => "Heal yourself or another player.";
    public string Syntax => "/heal [player]";
    public List<string> Aliases => new List<string> { "hp" };
    public string[] Permissions => new string[] { "myplugin.heal" };
    public AllowedCaller AllowedCaller => AllowedCaller.Player;

    public void Execute(IRocketPlayer caller, string[] command) { }
}

Both commands are registered and work identically from the player's perspective.

Attribute validation at build time

RocketMod validates command attributes at runtime when the plugin loads. If the attributes are invalid, RocketMod logs a warning but does not crash the plugin — the command simply does not register.

Validation rules

RuleInvalid exampleBehavior
Command name must not be empty[RocketCommand("", "Help")]Warning logged, command not registered
Command name must not contain spaces[RocketCommand("my heal", "Help")]Warning logged, command not registered
Help text must not be empty[RocketCommand("heal", "")]Warning logged, command registered with empty help
AllowedCaller must be a valid enum value[RocketCommand("heal", "Help", AllowedCaller = (AllowedCaller)99)]Warning logged, defaults to Both
Duplicate alias across commandsTwo commands with [RocketCommandAlias("tp")]Last-loaded plugin wins (same as name collision)

Missing attribute

If a command class implements IRocketCommand but has no [RocketCommand] attribute, RocketMod falls back to reading the metadata from the interface properties. The attribute is purely additive — it overrides the interface properties but is not required for command registration.

Complete attribute-based command example

The following example shows a moderation kit using attribute-based commands with proper structure:

csharp
using Rocket.API;
using Rocket.API.Commands;
using Rocket.Unturned.Chat;
using Rocket.Unturned.Player;
using System.Collections.Generic;
using UnityEngine;

namespace MyModerationPlugin.Commands
{
    [RocketCommand("kick", "Kicks a player from the server.",
        Syntax = "/kick <player> [reason...]",
        AllowedCaller = AllowedCaller.Console)]
    [RocketCommandAlias("k")]
    [RocketCommandAlias("boot")]
    [RocketCommandPermission("moderation.kick")]
    [RocketCommandPermission("moderation.admin")]
    public class KickCommand : IRocketCommand
    {
        public void Execute(IRocketPlayer caller, string[] command)
        {
            if (command.Length < 1)
            {
                UnturnedChat.Say(caller, "Usage: /kick <player> [reason]", Color.red);
                return;
            }

            UnturnedPlayer target = U.Instance.Players.FindPlayer(command[0]);
            if (target == null)
            {
                UnturnedChat.Say(caller, "Player not found.", Color.red);
                return;
            }

            string reason = command.Length > 1
                ? string.Join(" ", command, 1, command.Length - 1)
                : "No reason specified.";

            target.Kick(reason);
            Rocket.Core.Logging.Logger.Log(
                $"[Moderation] {caller.DisplayName} kicked {target.DisplayName}. Reason: {reason}");
            UnturnedChat.Say(caller,
                $"Kicked {target.DisplayName}. Reason: {reason}", Color.green);
        }
    }

    [RocketCommand("freeze", "Freezes a player in place.",
        Syntax = "/freeze <player>",
        AllowedCaller = AllowedCaller.Console)]
    [RocketCommandAlias("fz")]
    [RocketCommandPermission("moderation.freeze")]
    public class FreezeCommand : IRocketCommand
    {
        public void Execute(IRocketPlayer caller, string[] command)
        {
            if (command.Length < 1)
            {
                UnturnedChat.Say(caller, "Usage: /freeze <player>", Color.red);
                return;
            }

            UnturnedPlayer target = U.Instance.Players.FindPlayer(command[0]);
            if (target == null)
            {
                UnturnedChat.Say(caller, "Player not found.", Color.red);
                return;
            }

            target.Freeze();
            UnturnedChat.Say(caller,
                $"Frozen {target.DisplayName}.", Color.green);
        }
    }

    [RocketCommand("unfreeze", "Unfreezes a frozen player.",
        Syntax = "/unfreeze <player>",
        AllowedCaller = AllowedCaller.Console)]
    [RocketCommandAlias("ufz")]
    [RocketCommandPermission("moderation.freeze")]
    public class UnfreezeCommand : IRocketCommand
    {
        public void Execute(IRocketPlayer caller, string[] command)
        {
            if (command.Length < 1)
            {
                UnturnedChat.Say(caller, "Usage: /unfreeze <player>", Color.red);
                return;
            }

            UnturnedPlayer target = U.Instance.Players.FindPlayer(command[0]);
            if (target == null)
            {
                UnturnedChat.Say(caller, "Player not found.", Color.red);
                return;
            }

            target.Unfreeze();
            UnturnedChat.Say(caller,
                $"Unfrozen {target.DisplayName}.", Color.green);
        }
    }
}

Performance considerations

Attribute metadata is resolved once during plugin load (when RocketMod scans the assembly for command classes) and then cached in memory for the lifetime of the plugin. There is no per-execution reflection cost. The attribute approach and the interface approach have identical runtime performance.

The only practical difference is the code structure at development time. Attributes make the command metadata visible at the top of the class file; interface properties require scrolling to the property definitions. Choose based on readability for your team.

Migration from interface to attributes

If you have existing interface-based commands and want to migrate them to attributes, the process is mechanical:

  1. Add [RocketCommand("name", "help", Syntax = "...", AllowedCaller = ...)] to the class.
  2. Replace Aliases property with [RocketCommandAlias("alias")] attributes.
  3. Replace Permissions property with [RocketCommandPermission("perm")] attributes.
  4. Keep the Name, Help, Syntax, Aliases, Permissions, and AllowedCaller properties only if they have dynamic logic. Otherwise, remove them — the attribute covers them.
  5. Execute stays unchanged.

Before (interface-only)

csharp
public class HealCommand : IRocketCommand
{
    public string Name => "heal";
    public string Help => "Heals a player.";
    public string Syntax => "/heal [player]";
    public List<string> Aliases => new List<string> { "hp" };
    public string[] Permissions => new string[] { "myplugin.heal" };
    public AllowedCaller AllowedCaller => AllowedCaller.Player;

    public void Execute(IRocketPlayer caller, string[] command) { }
}

After (attribute-based)

csharp
[RocketCommand("heal", "Heals a player.",
    Syntax = "/heal [player]",
    AllowedCaller = AllowedCaller.Player)]
[RocketCommandAlias("hp")]
[RocketCommandPermission("myplugin.heal")]
public class HealCommand : IRocketCommand
{
    public void Execute(IRocketPlayer caller, string[] command) { }
}

The compiled result is identical. RocketMod's command manager treats both forms the same way.

Custom command attribute patterns

While RocketMod provides the three standard command attributes, you can create your own helper patterns that use these attributes under the hood.

Grouped permission attribute

If your command requires multiple permissions and you want to keep the class declaration clean, create a helper attribute that expands into multiple [RocketCommandPermission] entries at compile time. Since C# attributes are static metadata, you cannot generate attributes from other attributes directly, but you can encapsulate the permission logic in a base class or factory.

Standardized command template

For teams writing many commands with consistent metadata patterns, create a base class that provides default attribute values:

csharp
// Base class for all admin commands
[RocketCommand("template", "Override in subclass.",
    AllowedCaller = AllowedCaller.Console)]
[RocketCommandPermission("myplugin.admin")]
public abstract class AdminCommandBase : IRocketCommand
{
    public abstract string Name { get; }
    public abstract string Help { get; }
    public abstract string Syntax { get; }
    public List<string> Aliases => new List<string>();
    public string[] Permissions => new string[] { };
    public AllowedCaller AllowedCaller => AllowedCaller.Console;

    public abstract void Execute(IRocketPlayer caller, string[] command);
}

Note that attribute inheritance has limitations in C#. The [RocketCommand] attribute on the base class is NOT inherited by the subclass in RocketMod's scanning logic. Each concrete command class must have its own attribute. The base class pattern works for shared behavior and properties but not for attribute inheritance.

Attribute scanning and documentation generation

If your plugin suite has many attribute-based commands, you can write a tool that scans the compiled assembly and generates documentation from the attribute metadata:

csharp
public static void GenerateCommandDocs(string assemblyPath)
{
    Assembly asm = Assembly.LoadFrom(assemblyPath);
    var commandTypes = asm.GetTypes()
        .Where(t => typeof(IRocketCommand).IsAssignableFrom(t)
                    && !t.IsAbstract);

    foreach (var type in commandTypes)
    {
        var cmdAttr = type.GetCustomAttribute<RocketCommandAttribute>();
        var permAttrs = type.GetCustomAttributes<RocketCommandPermissionAttribute>();
        var aliasAttrs = type.GetCustomAttributes<RocketCommandAliasAttribute>();

        if (cmdAttr != null)
        {
            Console.WriteLine($"## {cmdAttr.Name}");
            Console.WriteLine($"Help: {cmdAttr.Help}");
            Console.WriteLine($"Syntax: {cmdAttr.Syntax}");
            Console.WriteLine($"AllowedCaller: {cmdAttr.AllowedCaller}");
            Console.WriteLine($"Permissions: {string.Join(", ",
                permAttrs.Select(p => p.Permission))}");
            Console.WriteLine($"Aliases: {string.Join(", ",
                aliasAttrs.Select(a => a.Alias))}");
            Console.WriteLine();
        }
    }
}

Hybrid patterns for complex commands

Some commands benefit from a hybrid approach that uses attributes for static metadata and interface properties for dynamic behavior.

Dynamic permissions based on configuration

csharp
[RocketCommand("heal", "Heals a player.",
    Syntax = "/heal <player>",
    AllowedCaller = AllowedCaller.Player)]
[RocketCommandAlias("hp")]
public class HealCommand : IRocketCommand
{
    // Static permissions from attribute are merged with dynamic ones
    public string[] Permissions
    {
        get
        {
            // If the server config enables self-heal, add that permission
            var perms = new List<string> { "myplugin.heal" };
            if (MyPlugin.Instance.Configuration.Instance.AllowSelfHeal)
            {
                perms.Add("myplugin.heal.self");
            }
            return perms.ToArray();
        }
    }

    public void Execute(IRocketPlayer caller, string[] command)
    {
        // Command logic
    }
}

Environment-aware aliases

csharp
[RocketCommand("greet", "Greets a player.")]
public class GreetCommand : IRocketCommand
{
    public List<string> Aliases
    {
        get
        {
            // Add server-specific aliases based on branding
            var aliases = new List<string>();
            if (MyPlugin.Instance.Configuration.Instance.ServerBrand == "Horizon")
            {
                aliases.Add("horizongreet");
            }
            else
            {
                aliases.Add("defaultgreet");
            }
            return aliases;
        }
    }

    public void Execute(IRocketPlayer caller, string[] command) { }
}

When the hybrid approach makes sense

Use the hybrid approach when:

  • Permission requirements change based on server configuration.
  • Aliases are generated from data (e.g., player names, item names).
  • The command is part of a template system where metadata is computed at startup.
  • You are migrating an existing interface-based command and want to move metadata to attributes incrementally — start with [RocketCommand] for Name and Help, keep dynamic permissions as interface properties, migrate them to attributes later.

Attribute performance at scale

The attribute scanning RocketMod performs at plugin load time is O(n) where n is the number of types in your assembly. For a plugin with 50 command classes, the scan completes in under 10 milliseconds. Performance is not a concern for normal plugin sizes.

Assembly scan overhead

Command classesScan time (typical)Impact
1 — 10< 2msNegligible
10 — 502ms — 10msNegligible
50 — 10010ms — 30msAcceptable
100+30ms+Consider splitting into multiple plugins

If your plugin has more than 100 command classes, consider whether they should be separate plugins. RocketMod's load sequence is synchronous, and the cumulative scan time across all plugins adds up during server startup.

Frequently asked questions

Can I use both attributes and interface properties on the same class?

Yes. Attribute values take priority for Name, Help, Syntax, and AllowedCaller. Aliases and permissions from both sources are merged. This allows you to put static metadata in attributes and dynamic aliases/permissions in property getters.

Do I still need to implement IRocketCommand when using attributes?

Yes. The [RocketCommand] attribute alone does not make a class a command. The class must still implement IRocketCommand, which requires the Execute method. The attribute only overrides the metadata properties; the command execution mechanism remains the same.

Can I define AllowedCaller in the attribute and also in the interface?

Yes. The attribute value takes priority. The interface property value is ignored for AllowedCaller. This is the same resolution rule that applies to Name, Help, and Syntax.

Are attribute commands case-sensitive?

No. The command name in the [RocketCommand] attribute is treated case-insensitively by RocketMod's command matching. The spelling in the attribute serves as the canonical name shown in /help listings.

Can I stack unlimited RocketCommandPermission attributes?

There is no hard limit, but each additional attribute adds a permission check at runtime. In practice, ten to fifteen permissions is a reasonable upper bound. If you need more, consider using groups or wildcard permissions instead.

Do attribute aliases work with command mapping?

Yes. Command aliases defined through [RocketCommandAlias] participate in the same command-mapping system as interface-defined aliases. The CommandMapping settings in config.xml can remap both the primary name and any alias. Priority resolution works the same way.

Combining attribute commands with RocketCommandManager

Attribute-based commands register through the same RocketCommandManager as interface-based commands. This means all R.Commands operations work identically regardless of how the command was declared.

Listing attribute-based commands

csharp
var allCommands = R.Commands.GetCommands();
foreach (var cmd in allCommands)
{
    // cmd is IRocketCommand — no way to distinguish attribute vs interface
    // at runtime, because both produce the same command registration
    Logger.Log($"Command /{cmd.Name} from {cmd.Plugin.Name}");
}

Coexisting registration methods

A single plugin can register some commands via attributes and others via the interface. RocketMod's scanner finds both. There is no performance or behavioral difference between the two approaches after registration. The distinction is purely a development-time code organization choice.

Document history

VersionDateAuthorNotes
1.02026-07-2757 StudiosInitial publication. Full attribute command reference.