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.

Prerequisites
- Understanding of
IRocketCommandfrom Defining Commands with IRocketCommand. - A working RocketMod plugin project.
- Familiarity with C# attributes and reflection.
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
| Parameter | Required | Type | Purpose |
|---|---|---|---|
| First positional | Yes | string | Command name (maps to Name in IRocketCommand) |
| Second positional | Yes | string | Help text (maps to Help) |
Syntax | No | string | Usage syntax (maps to Syntax) |
AllowedCaller | No | AllowedCaller | Defaults to Both if not specified |
Positional vs named parameters
The [RocketCommand] attribute constructor requires two positional arguments:
- The command name — what players type after
/ - The help text — displayed in
/helplistings
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
| Parameter | Type | Purpose |
|---|---|---|
| First positional | string | The 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
| Parameter | Type | Purpose |
|---|---|---|
| First positional | string | The 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:
- The attribute provides
Name,Help,Syntax, andAllowedCaller. These override the interface properties if both are present. - Aliases from
[RocketCommandAlias]attributes are merged with aliases from theAliasesproperty. - Permissions from
[RocketCommandPermission]attributes are merged with permissions from thePermissionsproperty.
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
| Rule | Invalid example | Behavior |
|---|---|---|
| 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 commands | Two 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:
- Add
[RocketCommand("name", "help", Syntax = "...", AllowedCaller = ...)]to the class. - Replace
Aliasesproperty with[RocketCommandAlias("alias")]attributes. - Replace
Permissionsproperty with[RocketCommandPermission("perm")]attributes. - Keep the
Name,Help,Syntax,Aliases,Permissions, andAllowedCallerproperties only if they have dynamic logic. Otherwise, remove them — the attribute covers them. Executestays 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 classes | Scan time (typical) | Impact |
|---|---|---|
| 1 — 10 | < 2ms | Negligible |
| 10 — 50 | 2ms — 10ms | Negligible |
| 50 — 100 | 10ms — 30ms | Acceptable |
| 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
| Version | Date | Author | Notes |
|---|---|---|---|
| 1.0 | 2026-07-27 | 57 Studios | Initial publication. Full attribute command reference. |
