Skip to content

Permissions System

RocketMod's permission system controls which players and groups can access which commands and features. It is file-based, using two XML files — permissions.xml and groups.xml — stored in the Rocket/Permissions/ directory. The system supports hierarchical groups, wildcard permissions, individual permission negation, and runtime permission checks from plugin code.

This article covers the full permission system from file structure through inheritance resolution to programmatic permission checks. It explains how RocketMod resolves permission requests, how to design an effective group hierarchy for a production server, and how plugins should implement permission checks.

57 Studios operates multiple Unturned servers using tightly controlled permission hierarchies. The patterns documented here are drawn from production experience managing permissions for dozens of plugins across multiple game modes.

Prerequisites

  • A working RocketMod installation. See RocketMod and OpenMod Plugin Basics.
  • Access to the server's Rocket/Permissions/ directory.
  • Notepad++ for editing XML files.
  • Familiarity with basic XML syntax.

What you'll learn

  • The structure and syntax of permissions.xml and groups.xml.
  • How group inheritance works and how to build a permission hierarchy.
  • How wildcard permissions and negation work (and what cannot be negated).
  • How to check permissions from plugin code.
  • How RocketPermissionsHelper resolves permission conflicts.
  • Best practices for managing permissions on a production server.

Permission file structure

permissions.xml

The permissions.xml file defines individual permission entries. Each permission has a Cooldown attribute (in seconds, default 0) and a text content permission string:

xml
<?xml version="1.0" encoding="utf-8"?>
<Permissions>
  <Permission Cooldown="0">heal</Permission>
  <Permission Cooldown="5">vanish</Permission>
  <Permission Cooldown="60">broadcast</Permission>
  <Permission Cooldown="0">teleport</Permission>
  <Permission Cooldown="0">myplugin.mycommand</Permission>
</Permissions>

The Cooldown attribute applies a per-player cooldown when the command associated with this permission is used. A cooldown of 0 means no cooldown.

groups.xml

The groups.xml file defines groups, their parent group (for inheritance), and their assigned permissions:

xml
<?xml version="1.0" encoding="utf-8"?>
<Groups>
  <Group>
    <Id>default</Id>
    <DisplayName>Default</DisplayName>
    <Permissions>
      <Permission Cooldown="0">heal</Permission>
    </Permissions>
  </Group>
  <Group>
    <Id>vip</Id>
    <DisplayName>VIP</DisplayName>
    <Parent>default</Parent>
    <Permissions>
      <Permission Cooldown="0">vanish</Permission>
    </Permissions>
  </Group>
  <Group>
    <Id>moderator</Id>
    <DisplayName>Moderator</DisplayName>
    <Parent>vip</Parent>
    <Permissions>
      <Permission Cooldown="60">broadcast</Permission>
      <Permission Cooldown="0">teleport</Permission>
    </Permissions>
  </Group>
  <Group>
    <Id>admin</Id>
    <DisplayName>Admin</DisplayName>
    <Parent>moderator</Parent>
    <Permissions>
      <Permission Cooldown="0">*</Permission>
    </Permissions>
  </Group>
</Groups>

players.xml

Player-to-group assignments are stored in players.xml:

xml
<?xml version="1.0" encoding="utf-8"?>
<Players>
  <Player>
    <Id>76561198012345678</Id>
    <DisplayName>Alex</DisplayName>
    <Group>admin</Group>
  </Player>
  <Player>
    <Id>76561198087654321</Id>
    <DisplayName>Sam</DisplayName>
    <Group>vip</Group>
  </Player>
</Players>

Group assignments are done via the console command:

/rocket player 76561198012345678 admin

This writes the assignment to players.xml and the change takes effect immediately.

Group inheritance

Groups form a parent-child hierarchy. A group inherits all permissions from its parent group (and from the parent's parent, recursively). The chain terminates when a group has no <Parent> element.

default (no parent)
  └── vip (parent: default)
       └── moderator (parent: vip)
            └── admin (parent: moderator)

In this hierarchy:

  • vip has heal (inherited from default) plus vanish (its own).
  • moderator has heal and vanish (inherited) plus broadcast and teleport (its own).
  • admin has all inherited permissions plus the wildcard *.

Circular inheritance

Circular inheritance (A's parent is B, B's parent is A) causes RocketMod to throw a StackOverflowException during permission resolution. The server operator must ensure the group hierarchy is a directed acyclic graph.

Wildcard permissions

The * wildcard grants all permissions. Any plugin that calls HasPermission() against a player with * receives true for any permission check. This is the standard way to grant admin access.

xml
<Permission Cooldown="0">*</Permission>

Wildcard with namespace prefix

Wildcards can be scoped to a prefix using the * at the end of a dotted path:

xml
<Permission Cooldown="0">myplugin.*</Permission>

This grants all permissions that start with myplugin. — such as myplugin.kick, myplugin.ban, myplugin.warn. The prefix matches against the permission string using StartsWith.

Permission negation

RocketMod supports permission negation using a hyphen (-) prefix. A negated permission overrides a positive grant from inheritance or a wildcard.

xml
<Group>
  <Id>moderator</Id>
  <DisplayName>Moderator</DisplayName>
  <Parent>vip</Parent>
  <Permissions>
    <Permission Cooldown="60">broadcast</Permission>
    <Permission Cooldown="0">teleport</Permission>
    <Permission Cooldown="0">-myplugin.sensitivecommand</Permission>
  </Permissions>
</Group>

In this example, even though moderator inherits myplugin.* from its parent chain (or if myplugin.* is in its own permissions), the specific permission myplugin.sensitivecommand is negated. A moderator cannot use that command.

Negation resolution order

RocketMod resolves negation using the following priority (highest wins):

  1. Direct permission on the player's group (positive or negative).
  2. Inherited permission from parent groups (positive or negative).
  3. Wildcard match from own group or inherited.

A negative permission on the player's own group overrides a positive wildcard on the same group or on a parent group.

What negation does NOT do

Permission negation with the hyphen prefix negates a specific permission string. It does not remove a player from a group or revoke an entire group's permissions.

xml
<!-- THIS IS A COMMON MISCONCEPTION -->
<!-- The following does NOT remove a player from the "default" group -->
<Permission Cooldown="0">-group.default</Permission>

The hyphen prefix operates on permission strings only. Writing -group.default tells RocketMod to deny the permission string literally named group.default. It does not interact with the group system at all. A server operator who tries to use -group.default to prevent a group from inheriting permissions will find that the inheritance still applies.

To control which groups a player belongs to, edit players.xml or use the /rocket player command. To control which permissions a group inherits, edit the <Parent> element in groups.xml. The permission negation system only handles individual permission strings.

Permission checks in plugin code

Using R.Permissions

The simplest way to check a permission is through R.Permissions.HasPermission():

csharp
using Rocket.API;

public class MyCommand : IRocketCommand
{
    public void Execute(IRocketPlayer caller, string[] command)
    {
        if (!R.Permissions.HasPermission(caller, "myplugin.mycommand"))
        {
            caller.SendChat("You do not have permission to use this command.", Color.red);
            return;
        }

        // Command logic here
    }
}

Using IRocketCommand's Permissions list

A cleaner approach is to use the Permissions property on IRocketCommand. RocketMod automatically checks these permissions before the command executes:

csharp
using Rocket.API;
using System.Collections.Generic;

public class SafeCommand : IRocketCommand
{
    public string Name => "safe";
    public List<string> Permissions => new List<string> { "myplugin.safe" };
    public AllowedCaller AllowedCaller => AllowedCaller.Player;

    public void Execute(IRocketPlayer caller, string[] command)
    {
        // RocketMod has already verified the caller has myplugin.safe
        // If the caller lacks the permission, Execute is never called.
    }
}

When using the Permissions list, RocketMod handles the permission check before Execute() is called. If the caller lacks the required permission, RocketMod automatically sends a "You do not have permission to use this command." message. This is the recommended approach because it centralizes the check and reduces boilerplate.

Manual check with RocketPermissionsHelper

For more complex scenarios (checking permissions on objects other than the command caller, checking permissions in event handlers, custom error messages), use RocketPermissionsHelper:

csharp
using Rocket.API;
using Rocket.Core.Permissions;

public bool CheckCustomPermission(IRocketPlayer player, string permission)
{
    RocketPermissionsHelper.CheckPermissions(
        player,
        permission,
        out bool canUse,
        out string deniedReason
    );

    if (!canUse)
    {
        Rocket.Core.Logging.Logger.Log(
            $"{player.DisplayName} denied for {permission}: {deniedReason}"
        );
    }

    return canUse;
}

CheckPermissions is the internal method that RocketMod's command system calls. It resolves the full permission chain including inheritance, wildcards, and negation.

Checking permissions in event handlers

Event handlers do not have automatic permission checks. If an event handler needs to verify that a player has a specific permission, use R.Permissions.HasPermission():

csharp
using Rocket.Unturned.Events;
using Rocket.Unturned.Player;

private void OnPlayerConnected(UnturnedPlayer player)
{
    if (R.Permissions.HasPermission(player, "myplugin.greeting"))
    {
        player.SendChat("You have special greeting permissions!", Color.gold);
    }
}

Designing a permission hierarchy

The four-tier model

For production servers, the following four-tier hierarchy balances control and maintainability:

TierGroupParentUse
1default(none)All players automatically. Basic commands like /help, /rules, /discord.
2vipdefaultDonors or ranked players. Cosmetic commands, priority slots.
3staffvipModerators. Moderation commands, kick, mute, warn.
4adminstaffFull access. All commands, configuration changes, server management.

Per-plugin permission namespaces

Assign permissions by plugin namespace to keep the permission file organized:

xml
<Group>
  <Id>staff</Id>
  <DisplayName>Staff</DisplayName>
  <Parent>vip</Parent>
  <Permissions>
    <!-- Moderation plugin -->
    <Permission Cooldown="0">moderation.kick</Permission>
    <Permission Cooldown="0">moderation.mute</Permission>
    <Permission Cooldown="0">moderation.warn</Permission>
    <Permission Cooldown="0">moderation.report</Permission>
    <Permission Cooldown="0">moderation.*</Permission>

    <!-- Teleportation plugin -->
    <Permission Cooldown="5">teleport.tp</Permission>
    <Permission Cooldown="10">teleport.tphere</Permission>
    <Permission Cooldown="30">teleport.tppos</Permission>

    <!-- Vanish plugin (negated for staff, only admin gets it) -->
    <Permission Cooldown="0">-vanish.*</Permission>
  </Permissions>
</Group>

Using the wildcard sparingly

Granting * to a group gives every plugin permission to every command. This is convenient for admin groups but dangerous for lower tiers. If a new plugin registers commands with permissions like newplugin.delete_all_items, the * wildcard on a group automatically grants access — even if the server operator did not intend to expose that command to that tier.

A safer pattern is to grant wildcards per plugin namespace (moderation.*, teleport.*) rather than a blanket *. The blanket * should be reserved for the highest admin tier only.

Common permission mistakes

Permission name mismatch

The permission string in plugin code must match the permission string in groups.xml exactly:

csharp
// Plugin code checks for:
Permissions = new List<string> { "myplugin.mycommand" };
xml
<!-- Configuration file has: -->
<Permission Cooldown="0">myPlugin.myCommand</Permission>

The mismatch (lowercase vs camelCase) causes the permission check to fail silently. RocketMod does not report a warning. The command simply does not work for players who should have access.

Always use lowercase permission names convention: pluginname.commandname. This convention is case-sensitive — myplugin.mycommand and MyPlugin.MyCommand are different permissions.

Permission exists but group does not inherit it

A group only has permissions from its own <Permissions> block and from its parent chain. If a permission is assigned to vip but a player is in the moderator group, and moderator does not inherit from vip, the permission check fails.

Missing wildcard scope

Writing * grants everything. Writing myplugin.* grants everything under myplugin.. But writing myplugin without the dot-star grants only the literal permission myplugin, not myplugin.command. The dot-star suffix is required for wildcard matching.

Overlooking the Cooldown attribute

If a command's cooldown is set to a high value on a frequently-used command, players get frustrated. The Cooldown attribute is per-permission, per-player. Setting <Permission Cooldown="300">teleport.tp</Permission> prevents a player from teleporting more than once every 5 minutes.

Cooldowns do not stack across groups. A player in two groups with the same permission resolves to whichever group RocketMod checks first (the player's primary group).

Debugging permission issues

The rocket permissions command

Use the console command to inspect a player's effective permissions:

/rocket permissions <playerId or playerName>

This outputs all resolved permissions for the player, including inherited permissions and wildcard expansions.

Permission check logging

Add diagnostic logging to your plugin's permission checks during development:

csharp
bool hasPerm = R.Permissions.HasPermission(caller, "myplugin.debug");
Rocket.Core.Logging.Logger.Log(
    $"Permission check: {caller.DisplayName} / myplugin.debug → {hasPerm}"
);

Common permission problems and solutions

SymptomLikely causeFix
Admin cannot use a commandPermission name mismatchCheck the exact string in code vs file
Players in a group cannot use a command they should haveGroup does not inherit the right parentCheck <Parent> in groups.xml
A permission works for some players but not othersOne player is in a different groupCheck players.xml assignments
Negation on -permission.name does not remove access to the permissionWildcard from parent group overrides?Move negation to the group's own block
A new plugin's commands are denied for everyonePermission not added to any groupAdd the permission to the appropriate group block

Complete example: permission-controlled plugin

The following example demonstrates a plugin with granular permission controls, including namespace-scoped wildcard usage and proper command-level permission enforcement.

PermissionAssignment.xml:

xml
<?xml version="1.0" encoding="utf-8"?>
<Groups>
  <Group>
    <Id>default</Id>
    <DisplayName>Default</DisplayName>
    <Permissions>
      <Permission Cooldown="0">reportplugin.report</Permission>
      <Permission Cooldown="0">reportplugin.check</Permission>
    </Permissions>
  </Group>
  <Group>
    <Id>moderator</Id>
    <DisplayName>Moderator</DisplayName>
    <Parent>default</Parent>
    <Permissions>
      <Permission Cooldown="0">reportplugin.*</Permission>
      <Permission Cooldown="0">modtools.warn</Permission>
      <Permission Cooldown="0">modtools.kick</Permission>
      <Permission Cooldown="0">-modtools.ban</Permission>
    </Permissions>
  </Group>
  <Group>
    <Id>admin</Id>
    <DisplayName>Admin</DisplayName>
    <Parent>moderator</Parent>
    <Permissions>
      <Permission Cooldown="0">modtools.ban</Permission>
      <Permission Cooldown="0">*</Permission>
    </Permissions>
  </Group>
</Groups>

ReportPlugin.cs (partial):

csharp
using Rocket.API;
using Rocket.Core.Plugins;
using Rocket.Unturned.Chat;
using Rocket.Unturned.Events;
using Rocket.Unturned.Player;
using System.Collections.Generic;
using UnityEngine;

namespace ReportPlugin
{
    public class ReportPlugin : RocketPlugin<ReportPluginConfiguration>
    {
        public static ReportPlugin Instance { get; private set; }

        protected override void Load()
        {
            Instance = this;
            UnturnedPlayerEvents.OnPlayerChatted += OnPlayerChatted;
            Rocket.Core.Logging.Logger.Log("ReportPlugin loaded.");
        }

        protected override void Unload()
        {
            UnturnedPlayerEvents.OnPlayerChatted -= OnPlayerChatted;
            Instance = null;
            Rocket.Core.Logging.Logger.Log("ReportPlugin unloaded.");
        }

        private void OnPlayerChatted(UnturnedPlayer player, ref Color color, string message, ref bool cancel)
        {
            if (message.StartsWith("!report ") && R.Permissions.HasPermission(player, "reportplugin.report"))
            {
                string reportText = message.Substring(8);
                string formatted = $"REPORT from {player.DisplayName}: {reportText}";

                // Forward to all online moderators
                foreach (var target in Provider.clients)
                {
                    var untPlayer = UnturnedPlayer.FromSteamPlayer(target);
                    if (R.Permissions.HasPermission(untPlayer, "reportplugin.mod.receive"))
                    {
                        untPlayer.SendChat(formatted, Color.magenta);
                    }
                }

                player.SendChat("Your report has been sent to online staff.", Color.green);
                cancel = true;
            }
        }
    }
}

ProcessReportsCommand.cs:

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

namespace ReportPlugin.Commands
{
    public class ProcessReportsCommand : IRocketCommand
    {
        public string Name => "processreports";
        public string Help => "View and manage pending player reports.";
        public string Syntax => "/processreports";
        public List<string> Aliases => new List<string>();
        public List<string> Permissions => new List<string> { "reportplugin.mod.receive" };
        public AllowedCaller AllowedCaller => AllowedCaller.Player;

        public void Execute(IRocketPlayer caller, string[] command)
        {
            UnturnedChat.Say(caller, "Checking reports... This feature is under development.", Color.yellow);
        }
    }
}

Frequently asked questions

Can I assign permissions to individual players without using groups?

No. RocketMod's permission system requires permissions to be assigned to groups. Individual players are assigned to groups, and they inherit the group's permissions. There is no per-player permission override within the default RocketMod system.

What happens if a player is in multiple groups?

RocketMod assigns each player to one group (the group specified in players.xml). A player cannot be in multiple groups simultaneously. If you need compound permission sets, create a group that inherits from multiple parents — but note that RocketMod supports only single inheritance (one <Parent> element per group).

Does RocketMod support permission cooldowns across server restarts?

Cooldowns are in-memory only. They reset when the server restarts. There is no persistent cooldown storage in the default RocketMod permission system.

Can I reload permissions without restarting the server?

Yes. Editing groups.xml or permissions.xml takes effect the next time a permission check is performed. RocketMod reads the files on each check, not at startup. However, players.xml changes require a server restart or the /rocket reload Permissions command.

How do I give a player all permissions except specific ones?

Assign the player to a group with * wildcard, then add negations for the specific permissions you want to deny. Create a group like this:

xml
<Group>
  <Id>almost_admin</Id>
  <DisplayName>Almost Admin</DisplayName>
  <Parent>staff</Parent>
  <Permissions>
    <Permission Cooldown="0">*</Permission>
    <Permission Cooldown="0">-serverstop</Permission>
    <Permission Cooldown="0">-configplugin.*</Permission>
  </Permissions>
</Group>

A player in this group has all permissions except serverstop and everything under configplugin..

Cross-references

Document history

VersionDateAuthorNotes
1.02026-07-2757 StudiosInitial publication. Coverage of permission files, group inheritance, wildcards, negation, permission checks, best practices.