Command Permissions and Cooldowns
Intermediate20-30 minutesWindowsVisual StudioNotepad++
RocketMod's permission system controls who can run which commands and how often they can run them. Permissions are defined in XML files on the server, checked at runtime through the R.Permissions manager, and can optionally include cooldowns that prevent rapid re-use. Cooldowns are measured in minutes, measured from the moment of last use.
This article covers the permission and cooldown system end to end: the XML file format, the runtime API for checking permissions, the cooldown configuration through the Permission.Cooldown attribute, programmatic cooldown management through RocketCommandManager, and production patterns that the 57 Studios™ team uses across our plugin suite.

Prerequisites
- A working RocketMod plugin with at least one command. See Creating Your First RocketMod Plugin.
- Understanding of the
Permissionsproperty onIRocketCommand. See Defining Commands with IRocketCommand. - Access to the server's
Rocket/Permissions/directory.
The permission system overview
RocketMod permissions are a three-layer system:
- Permissions — individual permission strings that map to features or commands (e.g.,
myplugin.heal). - Groups — named collections of permissions (e.g.,
admin,moderator,vip). - Players — individual Steam users who are assigned to groups or given direct permissions.
When a player runs a command, RocketMod checks whether the player has the required permission string through any of these paths:
- Direct permission assigned to the player.
- Permission inherited from a group the player belongs to.
- No permission requirement (empty
Permissionsarray on the command).
Permission storage
All permission data lives in two XML files in Rocket/Permissions/:
permissions.xml— defines permission strings and their cooldowns.groups.xml— defines permission groups and their member permissions.
RocketMod reloads these files at server start and on /rocket reload Permissions. Changes made while the server is running do not take effect until the files are reloaded.
Permissions XML format (permissions.xml)
The permissions.xml file defines each permission string and its optional cooldown.
xml
<?xml version="1.0" encoding="utf-8"?>
<Permissions>
<Permission Cooldown="0">myplugin.heal</Permission>
<Permission Cooldown="5">myplugin.tp</Permission>
<Permission Cooldown="60">myplugin.broadcast</Permission>
<Permission Cooldown="0">myplugin.kick</Permission>
</Permissions>The Permission element
Each <Permission> element has:
| Attribute | Required | Type | Default | Description |
|---|---|---|---|---|
Cooldown | No | double | 0 | Cooldown in minutes between uses |
The text content of the <Permission> element is the permission string. Permission strings are case-insensitive in RocketMod but should be lowercase by convention to avoid confusion.
Cooldown behavior
When a permission has a Cooldown value greater than zero, the cooldown applies to every command that requires that permission. The timer starts when the player successfully executes the command. While the cooldown is active, any attempt to run the same command again is blocked, and the player sees a message indicating how much time remains.
Cooldown values are specified in minutes. A value of 5 means the player must wait five minutes between uses. A value of 0 means no cooldown — the command can be used freely.
Permission.Cooldown="5" → 5 minutes between uses
Permission.Cooldown="1" → 1 minute between uses
Permission.Cooldown="0" → No cooldownFractional cooldowns
Cooldowns support fractional values for shorter intervals:
xml
<Permission Cooldown="0.5">myplugin.special</Permission>0.5 minutes translates to 30 seconds. This is useful for commands that should be rate-limited but not blocked for a full minute. Values below 0.016 (approximately 1 second) effectively disable the cooldown because the timer expires almost instantly.
Multiple permissions on one command
When a command lists multiple permissions in its Permissions array, the player needs only ONE of them to execute it. The cooldown applied is the cooldown of the permission that granted access.
csharp
// In the command class
public string[] Permissions => new string[]
{
"myplugin.heal", // Cooldown 0 minutes (no cooldown)
"myplugin.vipheal" // Cooldown 10 minutes
};If the player has myplugin.heal, the cooldown is 0 and they can use it freely. If the player only has myplugin.vipheal, the cooldown is 10 minutes. The player is not subject to both cooldowns — only the one that granted access applies.
Groups XML format (groups.xml)
Groups are defined in groups.xml. Each group has an ID, a display name, a parent (optional), and a list of permissions.
xml
<?xml version="1.0" encoding="utf-8"?>
<Groups>
<Group>
<Id>default</Id>
<DisplayName>Default</DisplayName>
<Permissions>
<Permission Cooldown="0">myplugin.ping</Permission>
<Permission Cooldown="0">myplugin.help</Permission>
</Permissions>
</Group>
<Group>
<Id>vip</Id>
<DisplayName>VIP</DisplayName>
<Parent>default</Parent>
<Permissions>
<Permission Cooldown="5">myplugin.heal</Permission>
<Permission Cooldown="5">myplugin.tp</Permission>
</Permissions>
</Group>
<Group>
<Id>moderator</Id>
<DisplayName>Moderator</DisplayName>
<Parent>vip</Parent>
<Permissions>
<Permission Cooldown="0">myplugin.kick</Permission>
<Permission Cooldown="0">myplugin.freeze</Permission>
</Permissions>
</Group>
<Group>
<Id>admin</Id>
<DisplayName>Admin</DisplayName>
<Parent>moderator</Parent>
<Permissions>
<Permission Cooldown="0">*</Permission>
</Permissions>
</Group>
</Groups>Group inheritance
Groups support parent-child inheritance through the <Parent> element. A child group inherits ALL permissions from its parent group, including their cooldowns. The inheritance chain in the example above:
default (ping, help)
└─ vip (heal, tp) — inherits ping, help
└─ moderator (kick, freeze) — inherits ping, help, heal, tp
└─ admin (*) — inherits everythingThe wildcard permission
The * permission grants access to every command on the server, regardless of the command's required permission strings. It should be reserved for the admin group. Commands with empty Permissions arrays are always accessible to everyone and are not affected by the wildcard.
Assigning players to groups
Players are assigned to groups through the in-game command:
/rocket player <playerName> <groupId>This writes to Rocket/Permissions/players.xml:
xml
<?xml version="1.0" encoding="utf-8"?>
<Players>
<Player>
<Id>76561197960265728</Id>
<DisplayName>Notch</DisplayName>
<Groups>
<Group>admin</Group>
</Groups>
</Player>
<Player>
<Id>76561197960265729</Id>
<DisplayName>Player2</DisplayName>
<Groups>
<Group>vip</Group>
</Groups>
</Player>
</Players>Players can be in multiple groups. They inherit the union of all permissions from all their groups.
Checking permissions at runtime
In the command class (automatic)
If the command's Permissions array is non-empty, RocketMod checks the permission automatically before Execute is called. The player sees the default "You do not have permission" message if the check fails.
csharp
public string[] Permissions => new string[] { "myplugin.heal" };Manual permission checking
If you need to check a permission that is not in the command's primary Permissions array (e.g., a sub-permission for a specific operation within the command), use R.Permissions.HasPermission():
csharp
public void Execute(IRocketPlayer caller, string[] command)
{
// Primary permission is checked automatically by RocketMod.
// Check a secondary permission for a sub-operation.
if (command.Length > 1 && command[1].Equals("all", StringComparison.OrdinalIgnoreCase))
{
if (!R.Permissions.HasPermission(caller, "myplugin.heal.all"))
{
UnturnedChat.Say(caller,
"You do not have permission to heal all players.", Color.red);
return;
}
}
// Execute the heal
}Checking group membership
To check if a player belongs to a specific group:
csharp
bool isAdmin = R.Permissions.HasGroup(player, "admin");
bool isVip = R.Permissions.HasGroup(player, "vip");Checking without a player entity
If you need to check a permission for a Steam ID that is not currently connected, there is no direct built-in method. The permission system operates on cached group membership for connected players. Permission checks for disconnected players require parsing the XML files manually.
Programmatic cooldowns with RocketCommandManager
In addition to the XML-based cooldown system, RocketMod provides a programmatic cooldown API through R.Commands. This allows plugins to set dynamic cooldowns that are not tied to a specific permission string.
Checking an existing cooldown
csharp
double remainingCooldown = R.Commands.GetCooldown(player, "heal");The returned value is the remaining time in seconds. If the value is 0 or negative, the cooldown has expired.
Setting a cooldown
csharp
// Set a 30-second cooldown for the heal command
R.Commands.SetCooldown(player, "heal", 30);The cooldown parameter is in seconds. A value of 30 means the player cannot use the heal command again for 30 seconds.
Clearing a cooldown
csharp
// Clear a cooldown (set remaining time to 0)
R.Commands.SetCooldown(player, "heal", 0);Cooldown key conventions
The command name used as the key in SetCooldown and GetCooldown should match the command's Name property. Using the alias name as the key creates a separate cooldown tracker, which means a player could bypass the cooldown by using an alias.
csharp
// BAD — alias creates separate cooldown
R.Commands.SetCooldown(player, "tp", 30); // cooldown on alias "tp"
// Player uses /teleport (the primary name) — different cooldown bucket
// GOOD — use the primary command name
R.Commands.SetCooldown(player, "teleport", 30); // cooldown on primary name
// Player uses /tp — same cooldown bucket because RocketMod maps aliasesRocketMod's cooldown system maps aliases to the primary command name internally when checking from the command execution path. However, when you call SetCooldown and GetCooldown directly, you are working with the raw cooldown key, and the alias mapping does not apply. Always use the primary Name value as the key.
Conditional cooldowns
You can apply cooldowns conditionally based on the command's outcome:
csharp
public void Execute(IRocketPlayer caller, string[] command)
{
if (command.Length < 1)
{
UnturnedChat.Say(caller, "Usage: /heal <player>", Color.red);
return; // No cooldown set — invalid usage does not trigger cooldown
}
UnturnedPlayer target = U.Instance.Players.FindPlayer(command[0]);
if (target == null)
{
UnturnedChat.Say(caller, "Player not found.", Color.red);
return; // No cooldown — target not found is not a valid use
}
target.Heal(100);
UnturnedChat.Say(caller, $"Healed {target.DisplayName}.", Color.green);
// Set cooldown only on successful execution
R.Commands.SetCooldown(caller, "heal", 30);
}This pattern prevents players from being penalized with a cooldown when the command failed due to invalid arguments or missing targets.
Per-player cooldowns
Cooldowns are tracked per-player, per-command. Player A using /heal does not affect Player B's cooldown for /heal. The cooldown state is stored in memory and is reset on server restart.
Cooldown message customization
When RocketMod blocks a command due to a cooldown, it sends a built-in message to the player:
You must wait X seconds before using this command again.To customize this message, check the cooldown manually in Execute and return your own message:
csharp
public void Execute(IRocketPlayer caller, string[] command)
{
double remaining = R.Commands.GetCooldown(caller, "heal");
if (remaining > 0)
{
UnturnedChat.Say(caller,
$"Heal is on cooldown. {remaining:F0} seconds remaining.", Color.yellow);
return;
}
// Execute heal
// ...
R.Commands.SetCooldown(caller, "heal", 30);
}This pattern bypasses RocketMod's automatic cooldown check (which you cannot easily customize) and replaces it with your own messaging.
Integrating cooldowns with permissions.xml
The XML-based cooldown system and the programmatic cooldown system are independent. A command can have both:
- A cooldown defined through
Permission.Cooldowninpermissions.xml. - A programmatic cooldown set through
R.Commands.SetCooldown.
When both are present, RocketMod checks the XML cooldown first (from the permission that granted access). If the XML cooldown has expired, the programmatic cooldown is checked next. Both must be expired for the command to execute.
xml
<Permission Cooldown="5">myplugin.heal</Permission>csharp
// In Execute()
R.Commands.SetCooldown(caller, "heal", 10);In this configuration, the player must wait 5 minutes (from the XML) AND 10 seconds (from the programmatic check). The effective cooldown is the longer of the two, but note that they are measured in different units — minutes in XML, seconds in code. A 5-minute XML cooldown (300 seconds) dwarfs a 10-second programmatic cooldown in practice.
Permission auditing
Listing permissions for a player
To audit a player's effective permissions:
csharp
var permissions = R.Permissions.GetPermissions(player);
foreach (var permission in permissions)
{
Rocket.Core.Logging.Logger.Log(
$"Permission: {permission.Name}, Cooldown: {permission.CooldownMinutes}");
}The PermissionEntry object returned by GetPermissions includes:
| Property | Type | Description |
|---|---|---|
Name | string | The permission string |
CooldownMinutes | double | Cooldown in minutes from the permissions.xml definition |
Listing groups for a player
csharp
var groups = R.Permissions.GetGroups(player);
foreach (var group in groups)
{
Rocket.Core.Logging.Logger.Log($"Group: {group.Id}");
}Common mistakes
Cooldown in minutes vs seconds confusion
The Permission.Cooldown attribute in permissions.xml is measured in minutes. Setting Cooldown="1" means one minute, which is 60 seconds. If you are transitioning from a programmatic cooldown (which uses seconds) and move the value to XML, remember to divide by 60. Setting a cooldown of 60 in XML when you meant 60 seconds results in a 60-minute cooldown.
Empty Permissions array on administrative commands
An empty Permissions array means NO permission is required. If an administrative command like /ban or /shutdown has an empty permissions array, any player can use it. Always set at least one permission on commands that have side effects.
Forgetting to reload permissions
Editing permissions.xml or groups.xml on a running server does not take effect until /rocket reload Permissions is run or the server restarts. The changes are not hot-reloaded automatically.
Wildcard permission in non-admin groups
Assigning * to a non-admin group grants that group unrestricted access to every command. Verify that only the admin group has the wildcard permission. If a test group or default group has *, every player on the server has full access.
Complete example
The following combines permissions, XML cooldowns, and programmatic cooldowns in a single command:
csharp
using Rocket.API;
using Rocket.Unturned.Chat;
using Rocket.Unturned.Player;
using System.Collections.Generic;
using UnityEngine;
namespace MyPlugin.Commands
{
public class HealCommand : IRocketCommand
{
public string Name => "heal";
public string Help => "Heals a player to full health.";
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)
{
if (command.Length < 1)
{
UnturnedChat.Say(caller,
$"Usage: {Syntax}", Color.red);
return;
}
// Check programmatic cooldown for customized message
double remaining = R.Commands.GetCooldown(caller, "heal");
if (remaining > 0)
{
UnturnedChat.Say(caller,
$"Please wait {remaining:F0} seconds before healing again.",
Color.yellow);
return;
}
UnturnedPlayer target = U.Instance.Players.FindPlayer(command[0]);
if (target == null)
{
UnturnedChat.Say(caller,
$"Player '{command[0]}' not found.", Color.red);
return;
}
target.Heal(100);
UnturnedChat.Say(caller,
$"Healed {target.DisplayName}.", Color.green);
// Set programmatic cooldown
R.Commands.SetCooldown(caller, "heal", 30);
Rocket.Core.Logging.Logger.Log(
$"[MyPlugin] {caller.DisplayName} healed {target.DisplayName}.");
}
}
}With the corresponding permission entry in permissions.xml:
xml
<Permission Cooldown="5">myplugin.heal</Permission>The player experiences two cooldowns:
- A 5-minute XML cooldown from the permission.
- A 30-second programmatic cooldown from
SetCooldown.
Since the 5-minute (300-second) XML cooldown is longer, it dominates in practice.
Persistent cooldown storage
RocketMod's in-memory cooldown system is reset on every server restart. For cooldowns that must survive restarts (e.g., daily reward claims, weekly event participation), implement persistent storage using a file or database.
File-based persistent cooldown
csharp
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
public class PersistentCooldownStore
{
private readonly string _filePath;
private Dictionary<string, DateTime> _cooldowns;
public PersistentCooldownStore(string pluginDirectory)
{
_filePath = Path.Combine(pluginDirectory, "cooldowns.dat");
_cooldowns = Load();
}
public bool IsOnCooldown(string playerId, string cooldownKey,
double cooldownMinutes)
{
string key = $"{playerId}:{cooldownKey}";
if (_cooldowns.TryGetValue(key, out DateTime expires))
{
if (DateTime.UtcNow < expires)
{
return true;
}
_cooldowns.Remove(key);
}
return false;
}
public void SetCooldown(string playerId, string cooldownKey,
double cooldownMinutes)
{
string key = $"{playerId}:{cooldownKey}";
_cooldowns[key] = DateTime.UtcNow.AddMinutes(cooldownMinutes);
Save();
}
private Dictionary<string, DateTime> Load()
{
if (!File.Exists(_filePath))
return new Dictionary<string, DateTime>();
using (var stream = File.OpenRead(_filePath))
{
var formatter = new BinaryFormatter();
return (Dictionary<string, DateTime>)formatter.Deserialize(stream);
}
}
private void Save()
{
using (var stream = File.OpenWrite(_filePath))
{
var formatter = new BinaryFormatter();
formatter.Serialize(stream, _cooldowns);
}
}
}Usage in a command:
csharp
private PersistentCooldownStore _cooldownStore;
protected override void Load()
{
_cooldownStore = new PersistentCooldownStore(
Directory.GetCurrentDirectory());
}
public void Execute(IRocketPlayer caller, string[] command)
{
string playerId = caller.Id;
if (_cooldownStore.IsOnCooldown(playerId, "daily_reward", 1440))
{
UnturnedChat.Say(caller,
"You have already claimed your daily reward today.", Color.yellow);
return;
}
// Grant reward
_cooldownStore.SetCooldown(playerId, "daily_reward", 1440);
UnturnedChat.Say(caller, "Daily reward claimed!", Color.green);
}Cooldown cleanup on server start
Persistent cooldown files grow over time as expired entries accumulate. Add a cleanup pass during Load() that removes expired entries:
csharp
protected override void Load()
{
_cooldownStore = new PersistentCooldownStore(
Directory.GetCurrentDirectory());
_cooldownStore.CleanupExpired();
}
// In PersistentCooldownStore
public void CleanupExpired()
{
var expired = _cooldowns
.Where(kvp => DateTime.UtcNow >= kvp.Value)
.Select(kvp => kvp.Key)
.ToList();
foreach (var key in expired)
{
_cooldowns.Remove(key);
}
if (expired.Count > 0)
{
Save();
Rocket.Core.Logging.Logger.Log(
$"[CooldownStore] Cleaned up {expired.Count} expired entries.");
}
}Cooldown bypass patterns
Some users should bypass cooldowns entirely — administrators, trusted players, or donors with special privileges.
Admin bypass
csharp
public void Execute(IRocketPlayer caller, string[] command)
{
// Check if the caller has admin (bypass all cooldowns)
if (R.Permissions.HasGroup(caller, "admin"))
{
// Execute immediately without cooldown check
ExecuteHeal(caller, command);
return;
}
// Normal cooldown check for non-admin users
double remaining = R.Commands.GetCooldown(caller, "heal");
if (remaining > 0)
{
UnturnedChat.Say(caller,
$"Please wait {remaining:F0} seconds.", Color.yellow);
return;
}
ExecuteHeal(caller, command);
R.Commands.SetCooldown(caller, "heal", 30);
}Permission-based cooldown tiers
Define different cooldown lengths based on the player's permission level:
csharp
private double GetCooldownForPlayer(IRocketPlayer player)
{
if (R.Permissions.HasPermission(player, "myplugin.heal.instant"))
return 0;
if (R.Permissions.HasPermission(player, "myplugin.heal.fast"))
return 10;
if (R.Permissions.HasPermission(player, "myplugin.heal.normal"))
return 30;
return 60; // Default: no special permission
}The corresponding permissions.xml:
xml
<Permission Cooldown="0">myplugin.heal.instant</Permission>
<Permission Cooldown="0">myplugin.heal.fast</Permission>
<Permission Cooldown="0">myplugin.heal.normal</Permission>Note that the XML Cooldown values are set to 0 for these permissions because the cooldown is managed programmatically, not through the XML system. The permission strings are only used as markers to identify which tier the player belongs to.
Multi-server permission synchronization
On server networks where players move between servers, permissions and cooldowns must be synchronized. RocketMod's file-based permission system does not support this natively. Server networks use one of these approaches:
Database-backed permissions
Replace the file-based permission storage with a database that all servers read from. This requires a custom IRocketPermissionsProvider implementation:
csharp
public class DatabasePermissionsProvider : IRocketPermissionsProvider
{
private readonly string _connectionString;
public DatabasePermissionsProvider(string connectionString)
{
_connectionString = connectionString;
}
public bool HasPermission(IRocketPlayer player, string permission)
{
// Query the database for the player's permissions
// Cache results locally with a short TTL
}
// Implement all other IRocketPermissionsProvider methods
}Register the custom provider during plugin Load:
csharp
protected override void Load()
{
var provider = new DatabasePermissionsProvider(
Configuration.Instance.DatabaseConnectionString);
R.Permissions.SetProvider(provider);
}This is an advanced pattern. Most server networks run RocketMod without cross-server permission synchronization and rely on consistent permissions.xml files deployed to each server.
Shared cooldown database
For cooldowns that must persist across servers (e.g., a daily reward that should only be claimable once across all servers), use the same persistent cooldown store pattern but write to a shared database instead of a local file.
Frequently asked questions
Are cooldowns reset on server restart?
Yes. All programmatic cooldowns are stored in memory and reset when the server restarts. XML-based cooldowns are not stored per-player in a persistent database — they are enforced by checking the time elapsed since the player's last use, and that tracking data is also in-memory and lost on restart.
Can a player bypass a cooldown by disconnecting and reconnecting?
Yes, if the cooldown is purely in-memory (set through SetCooldown). The cooldown state is not persisted to disk. When the player disconnects and reconnects, the cooldown timer is lost. If this is a concern for your use case, implement a persistent cooldown using a file or database.
Can I set different cooldowns for different groups?
Yes. Define separate permissions for each cooldown tier, assign the appropriate permission to each group, and set the Cooldown attribute differently on each permission.
xml
<Permission Cooldown="0">myplugin.heal.vip</Permission>
<Permission Cooldown="10">myplugin.heal.default</Permission>Then in the command, list both permissions. VIP players use the 0-cooldown permission; default players use the 10-minute cooldown.
Does the cooldown apply if the command fails?
RocketMod's automatic cooldown check happens before Execute is called. If the permission has a cooldown and it has not expired, the command never reaches Execute. If the cooldown has expired, Execute runs, and the cooldown timer resets after the command completes, regardless of whether Execute threw an exception or returned early.
What happens if I set Cooldown to 0?
No cooldown is enforced. The player can use the command repeatedly without waiting.
Document history
| Version | Date | Author | Notes |
|---|---|---|---|
| 1.0 | 2026-07-27 | 57 Studios | Initial publication. Full permissions and cooldowns reference. |
