Skip to content
RustPluginGenerator

OnGroupRankSet

OnGroupRankSet is called after an Oxide permission group's rank value has been changed and written to the group data, but before SetGroupRank returns its success result.

Signature
void OnGroupRankSet(string name, string title)

Parameters

Hook parameters
ParameterTypeDescription
namestringThe exact name of the Oxide permission group whose rank was just changed, as stored in the group data.
titlestringThe new rank value. Despite the parameter being named title, it holds the integer group rank assigned by SetGroupRank, delivered to the hook as a string.

Return value

The return value is ignored. SetGroupRank calls the hook and then returns true regardless of what your method returns, so you cannot cancel or alter the rank change from OnGroupRankSet.

How it works

Oxide fires this hook from Permission.SetGroupRank(string groupName, int groupRank), which is Oxide's own method rather than a patched game method. The order is: the group must exist, the group data must be retrievable, and if the new rank equals the stored rank the method returns early without calling the hook. Otherwise the rank is written first, then OnGroupRankSet fires with the group name and the new rank, and finally SetGroupRank returns true. Because the rank is already committed when your method runs, there is no way to veto the change from this hook; use it for reaction and mirroring.

The second argument arrives as a string even though SetGroupRank takes an int rank. Treat it as the numeric rank and parse it if you need arithmetic. The hook only fires on an actual change, so it is safe to use for audit trails without deduplicating.

Declaring string for both parameters is the only useful shape. Narrowing to a more specific type is meaningless here since both values are strings, and any narrowing to a non-string type would make Oxide skip your method entirely. Ranks are commonly used by permission plugins to order groups (staff tiers, donator tiers), so a rank change usually matters to anything that builds an ordered group display.

Example plugin: Track and announce permission group rank changes

GroupRankAudit.cs
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Group Rank Audit", "RustPluginGenerator", "1.0.0")]
    [Description("Records permission group rank changes and exposes them to admins")]
    public class GroupRankAudit : RustPlugin
    {
        private const string PermList = "grouprankaudit.list";

        private PluginConfig config;
        private readonly Dictionary<string, int> ranks = new Dictionary<string, int>();
        private Timer flushTimer;

        private class PluginConfig
        {
            [JsonProperty("Announce rank changes in chat")]
            public bool Announce { get; set; } = false;

            [JsonProperty("Chat prefix")]
            public string Prefix { get; set; } = "[Ranks]";
        }

        protected override void LoadDefaultConfig() => config = new PluginConfig();

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                config = Config.ReadObject<PluginConfig>();
                if (config == null) throw new Exception();
            }
            catch
            {
                PrintWarning("Configuration file is invalid, using defaults");
                LoadDefaultConfig();
            }
            SaveConfig();
        }

        protected override void SaveConfig() => Config.WriteObject(config);

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["RankChanged"] = "{0} rank of group '{1}' changed to {2}",
                ["ListHeader"] = "Tracked group ranks:",
                ["ListEntry"] = "  {0} -> {1}",
                ["NoData"] = "No rank changes have been recorded yet."
            }, this);
        }

        private string Msg(string key, string userId = null, params object[] args) =>
            string.Format(lang.GetMessage(key, this, userId), args);

        private void Init()
        {
            permission.RegisterPermission(PermList, this);
        }

        private void OnGroupRankSet(string name, string title)
        {
            int rank;
            if (!int.TryParse(title, out rank)) rank = 0;

            ranks[name] = rank;

            // Defer the broadcast so it does not run inside the permission call stack.
            if (flushTimer != null) flushTimer.Destroy();
            flushTimer = timer.Once(0.5f, () =>
            {
                Server.Broadcast(Msg("RankChanged", null, config.Prefix, name, rank));
            });

            if (config.Announce)
            {
                foreach (var player in BasePlayer.activePlayerList)
                {
                    if (player == null || !permission.UserHasPermission(player.UserIDString, PermList)) continue;
                    player.ChatMessage(Msg("RankChanged", player.UserIDString, config.Prefix, name, rank));
                }
            }
        }

        [ConsoleCommand("grouprankaudit.list")]
        private void CmdList(ConsoleSystem.Arg arg)
        {
            var player = arg.Player();
            if (player != null && !permission.UserHasPermission(player.UserIDString, PermList)) return;

            if (ranks.Count == 0)
            {
                Reply(arg, Msg("NoData", player?.UserIDString));
                return;
            }

            Reply(arg, Msg("ListHeader", player?.UserIDString));
            foreach (var entry in ranks)
            {
                Reply(arg, Msg("ListEntry", player?.UserIDString, entry.Key, entry.Value));
            }
        }

        private void Reply(ConsoleSystem.Arg arg, string text)
        {
            var player = arg.Player();
            if (player != null) player.ChatMessage(text);
            else arg.ReplyWith(text);
        }

        private void OnServerInitialized()
        {
            // Seed from existing Oxide group data so a fresh load is not empty.
            foreach (var group in permission.GetGroups())
            {
                ranks[group] = permission.GetGroupRank(group);
            }
        }

        private void Unload()
        {
            if (flushTimer != null)
            {
                flushTimer.Destroy();
                flushTimer = null;
            }
            ranks.Clear();
        }
    }
}

The plugin listens for rank changes and turns them into an operator-facing record. On each OnGroupRankSet it parses the new rank, updates an in-memory snapshot of group ranks, and schedules a broadcast chat message after a short delay so the write happens outside the permission call stack. The message uses the configured Chat prefix and is sent to all players via Server.Broadcast. When the Announce config flag is on, it also sends a direct chat message to each active player who holds the grouprankaudit.list permission. A console command grouprankaudit.list prints the current tracked ranks, and that command is restricted to players with the grouprankaudit.list permission when run by a player. Config exposes the announce toggle and the chat prefix.

Common use cases

  • Keep an external rank registry in sync

    Store the latest rank per group in a data file so other tooling or later plugin logic can read group ordering without touching Oxide's internals.

  • Notify staff when donator tiers change

    Broadcast or log a message when a rank change happens, so admins know a tier was regraded after a purchase or a demotion.

  • Invalidate cached sorted group lists

    If a plugin caches groups sorted by rank for a UI, mark the cache dirty in this hook and rebuild it on the next request instead of on every frame.

Pitfalls

  • The change is already applied

    The rank is written before the hook fires, so returning a value or printing a warning cannot undo it. To block rank changes you would need to wrap the calling code, not this hook.

  • The hook does not fire on a no-op

    SetGroupRank returns early when the new rank equals the current rank, so plugins that rely on this hook to seed state must also read existing ranks on load.

  • Second argument is a string

    Although SetGroupRank takes an int, the hook receives the rank as a string. Parse it carefully and handle a failed parse instead of assuming the format.

Build a plugin with OnGroupRankSet

Describe what should happen and get a compiled plugin that uses this hook.

Prompt: Keep an external rank registry in sync using the OnGroupRankSet hook, with a config file and a permission
Generate this plugin

Edit the prompt on the next screen. 2 free plugins a month, no card.

Last updated Oct 26, 2026. Found a mistake? Email support@rustplugingenerator.com.

Build a plugin with OnGroupRankSet

Generate