Skip to content
RustPluginGenerator

How to make a Rust VIP kit plugin (tiered permissions)

Adds tiered VIP kits (vip1/vip2/vip3) where every tier has its own permission, item list and cooldown, and /vipkit grants the highest tier the player is allowed to claim. Uses OnServerInitialized to register tier permissions and persists claim times through the Oxide data file.

Most kit plugins treat every kit the same, so VIP tiers end up as either three unrelated kits you have to police or one kit with a hand-maintained permission check inside the command. Both break the moment you sell a new tier or change a cooldown. The approach here is data-driven: the config holds an array of tiers, each with a permission, a cooldown in seconds and a list of item entries (shortname, amount, skin, condition). The plugin registers every tier permission on startup and resolves item definitions once, so adding a tier is a config edit plus a permission grant, no code change.

/vipkit walks the tiers from highest priority to lowest, stops at the first one the player holds the permission for and whose cooldown has expired, then gives that tier's items through GiveItem and stores the claim time per UserIDString. If the best tier is still cooling down the player is told the remaining seconds instead of silently dropping to a lower tier. Cooldowns survive restarts in oxide/data/VipKits.json, and /vipkits lists the tier table for admins and players alike.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("VipKits", "RustPluginGenerator", "1.0.0")]
    [Description("Tiered VIP kits where each tier has its own permission, item list and cooldown.")]
    public class VipKits : RustPlugin
    {
        private const string PermUse = "vipkits.use";

        private PluginConfig config;
        private StoredData data;
        private readonly Dictionary<string, ItemDefinition> definitions = new Dictionary<string, ItemDefinition>();

        private class KitItem
        {
            [JsonProperty("Shortname")]
            public string Shortname { get; set; } = "scrap";

            [JsonProperty("Amount")]
            public int Amount { get; set; } = 1;

            [JsonProperty("Skin")]
            public ulong Skin { get; set; } = 0;

            [JsonProperty("Condition")]
            public float Condition { get; set; } = 0f;
        }

        private class Tier
        {
            [JsonProperty("Name")]
            public string Name { get; set; } = "VIP";

            [JsonProperty("Permission")]
            public string Permission { get; set; } = "vipkits.vip1";

            [JsonProperty("Cooldown seconds")]
            public int CooldownSeconds { get; set; } = 86400;

            [JsonProperty("Items")]
            public List<KitItem> Items { get; set; } = new List<KitItem>();
        }

        private class PluginConfig
        {
            [JsonProperty("Tiers (highest first)")]
            public List<Tier> Tiers { get; set; } = new List<Tier>
            {
                new Tier
                {
                    Name = "VIP 3",
                    Permission = "vipkits.vip3",
                    CooldownSeconds = 43200,
                    Items = new List<KitItem>
                    {
                        new KitItem { Shortname = "scrap", Amount = 1000 },
                        new KitItem { Shortname = "rifle.ak", Amount = 1 },
                        new KitItem { Shortname = "ammo.rifle", Amount = 120 },
                        new KitItem { Shortname = "supply.signal", Amount = 1 }
                    }
                },
                new Tier
                {
                    Name = "VIP 2",
                    Permission = "vipkits.vip2",
                    CooldownSeconds = 86400,
                    Items = new List<KitItem>
                    {
                        new KitItem { Shortname = "scrap", Amount = 500 },
                        new KitItem { Shortname = "smg.2", Amount = 1 },
                        new KitItem { Shortname = "ammo.pistol", Amount = 96 },
                        new KitItem { Shortname = "syringe.medical", Amount = 5 }
                    }
                },
                new Tier
                {
                    Name = "VIP 1",
                    Permission = "vipkits.vip1",
                    CooldownSeconds = 86400,
                    Items = new List<KitItem>
                    {
                        new KitItem { Shortname = "scrap", Amount = 200 },
                        new KitItem { Shortname = "bow.hunting", Amount = 1 },
                        new KitItem { Shortname = "arrow.wooden", Amount = 32 },
                        new KitItem { Shortname = "largemedkit", Amount = 2 }
                    }
                }
            };
        }

        private class StoredData
        {
            [JsonProperty("Last claim unix time per user id")]
            public Dictionary<string, long> ClaimTimes { get; set; } = new Dictionary<string, long>();
        }

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

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                config = Config.ReadObject<PluginConfig>();
                if (config == null || config.Tiers == 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>
            {
                ["NoPermission"] = "You do not have permission to use VIP kits.",
                ["NoTier"] = "You do not have any VIP kit permission. Ask an admin about VIP tiers.",
                ["Given"] = "You claimed <color=#8fd18f>{0}</color>: {1}",
                ["Cooldown"] = "{0} is on cooldown. Try again in <color=#e0b040>{1}</color>.",
                ["ListHeader"] = "Available VIP kits (highest first):",
                ["ListLine"] = "{0} - permission <color=#9dc4e8>{1}</color>, cooldown {2}, items: {3}"
            }, 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(PermUse, this);
        }

        private void OnServerInitialized()
        {
            foreach (var tier in config.Tiers)
            {
                if (!string.IsNullOrEmpty(tier.Permission))
                    permission.RegisterPermission(tier.Permission, this);

                if (tier.Items == null) continue;
                foreach (var entry in tier.Items)
                {
                    if (string.IsNullOrEmpty(entry.Shortname)) continue;
                    var def = ItemManager.FindItemDefinition(entry.Shortname);
                    if (def == null)
                        PrintWarning($"Unknown item shortname '{entry.Shortname}' in tier '{tier.Name}', entry skipped");
                    else
                        definitions[entry.Shortname] = def;
                }
            }

            LoadData();
        }

        private void Unload()
        {
            SaveData();
        }

        [ChatCommand("vipkit")]
        private void CmdVipKit(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }

            var tier = ResolveTier(player);
            if (tier == null)
            {
                player.ChatMessage(Msg("NoTier", player.UserIDString));
                return;
            }

            long last;
            data.ClaimTimes.TryGetValue(player.UserIDString, out last);
            var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            var ready = last + Math.Max(1, tier.CooldownSeconds);
            if (now < ready)
            {
                player.ChatMessage(Msg("Cooldown", player.UserIDString, tier.Name, FormatTime(ready - now)));
                return;
            }

            var given = GiveTier(player, tier);
            data.ClaimTimes[player.UserIDString] = now;
            SaveData();
            player.ChatMessage(Msg("Given", player.UserIDString, tier.Name, given));
        }

        [ChatCommand("vipkits")]
        private void CmdVipKits(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }

            player.ChatMessage(Msg("ListHeader", player.UserIDString));
            foreach (var tier in config.Tiers)
            {
                var items = new List<string>();
                if (tier.Items != null)
                {
                    foreach (var entry in tier.Items)
                        items.Add($"{entry.Amount}x {entry.Shortname}");
                }
                player.ChatMessage(Msg("ListLine", player.UserIDString, tier.Name, tier.Permission,
                    FormatTime(tier.CooldownSeconds), string.Join(", ", items)));
            }
        }

        private Tier ResolveTier(BasePlayer player)
        {
            foreach (var tier in config.Tiers)
            {
                if (!string.IsNullOrEmpty(tier.Permission) &&
                    permission.UserHasPermission(player.UserIDString, tier.Permission))
                    return tier;
            }
            return null;
        }

        private string GiveTier(BasePlayer player, Tier tier)
        {
            var parts = new List<string>();
            if (tier.Items == null) return "nothing";

            foreach (var entry in tier.Items)
            {
                ItemDefinition def;
                if (string.IsNullOrEmpty(entry.Shortname) || !definitions.TryGetValue(entry.Shortname, out def))
                    continue;

                var amount = Mathf.Clamp(entry.Amount, 1, Math.Max(1, def.stackable));
                var item = ItemManager.CreateByItemID(def.itemid, amount, entry.Skin);
                if (item == null) continue;

                if (entry.Condition > 0f && item.hasCondition)
                {
                    item.condition = Mathf.Min(entry.Condition, item.maxCondition);
                }

                player.GiveItem(item);
                parts.Add($"{amount}x {def.displayName.english}");
            }

            return parts.Count > 0 ? string.Join(", ", parts) : "nothing";
        }

        private static string FormatTime(long seconds)
        {
            if (seconds < 0) seconds = 0;
            var span = TimeSpan.FromSeconds(seconds);
            if (span.TotalHours >= 1)
                return $"{(int)span.TotalHours}h {span.Minutes:00}m {span.Seconds:00}s";
            if (span.TotalMinutes >= 1)
                return $"{span.Minutes}m {span.Seconds:00}s";
            return $"{span.Seconds}s";
        }

        private void LoadData()
        {
            try
            {
                data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name);
            }
            catch
            {
                PrintWarning("Data file is invalid, starting with an empty claim table");
                data = null;
            }

            if (data == null)
                data = new StoredData();
            if (data.ClaimTimes == null)
                data.ClaimTimes = new Dictionary<string, long>();
        }

        private void SaveData()
        {
            if (data != null)
                Interface.Oxide.DataFileSystem.WriteObject(Name, data);
        }
    }
}

Hooks used: OnServerInitialized

Config file

Created automatically on first load at oxide/config/VipKits.json.

VipKits.json
{
  "Tiers (highest first)": [
    {
      "Name": "VIP 3",
      "Permission": "vipkits.vip3",
      "Cooldown seconds": 43200,
      "Items": [
        {
          "Shortname": "scrap",
          "Amount": 1000,
          "Skin": 0,
          "Condition": 0
        },
        {
          "Shortname": "rifle.ak",
          "Amount": 1,
          "Skin": 0,
          "Condition": 0
        },
        {
          "Shortname": "ammo.rifle",
          "Amount": 120,
          "Skin": 0,
          "Condition": 0
        },
        {
          "Shortname": "supply.signal",
          "Amount": 1,
          "Skin": 0,
          "Condition": 0
        }
      ]
    },
    {
      "Name": "VIP 2",
      "Permission": "vipkits.vip2",
      "Cooldown seconds": 86400,
      "Items": [
        {
          "Shortname": "scrap",
          "Amount": 500,
          "Skin": 0,
          "Condition": 0
        },
        {
          "Shortname": "smg.2",
          "Amount": 1,
          "Skin": 0,
          "Condition": 0
        },
        {
          "Shortname": "ammo.pistol",
          "Amount": 96,
          "Skin": 0,
          "Condition": 0
        },
        {
          "Shortname": "syringe.medical",
          "Amount": 5,
          "Skin": 0,
          "Condition": 0
        }
      ]
    },
    {
      "Name": "VIP 1",
      "Permission": "vipkits.vip1",
      "Cooldown seconds": 86400,
      "Items": [
        {
          "Shortname": "scrap",
          "Amount": 200,
          "Skin": 0,
          "Condition": 0
        },
        {
          "Shortname": "bow.hunting",
          "Amount": 1,
          "Skin": 0,
          "Condition": 0
        },
        {
          "Shortname": "arrow.wooden",
          "Amount": 32,
          "Skin": 0,
          "Condition": 0
        },
        {
          "Shortname": "largemedkit",
          "Amount": 2,
          "Skin": 0,
          "Condition": 0
        }
      ]
    }
  ],
  "Last claim unix time per user id": {}
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/vipkitClaims the highest VIP tier the player has permission for and whose cooldown has expiredvipkits.use
/vipkitsLists every configured tier, its permission and its cooldownvipkits.use
Permissions
PermissionGrants
vipkits.useAllows the player to run /vipkit and /vipkits
vipkits.vip1Grants access to the first VIP tier (default permission name of tier 1)
vipkits.vip2Grants access to the second VIP tier (default permission name of tier 2)
vipkits.vip3Grants access to the third VIP tier (default permission name of tier 3)

How the code works

  1. Model the tiers in the config

    The nested PluginConfig class holds Tiers, a List<Tier> where each Tier has Name, Permission, CooldownSeconds and Items. Each KitItem carries Shortname, Amount, Skin and Condition, all with [JsonProperty] names so the config file stays readable. Tiers are ordered highest-first: /vipkit takes the first match, so the array order is the priority order. LoadConfig checks that Tiers is not null and falls back to LoadDefaultConfig with a warning on a hand-edited file, then calls SaveConfig.

  2. Register permissions and resolve item definitions once

    Init registers only vipkits.use. OnServerInitialized iterates config.Tiers, calls permission.RegisterPermission(tier.Permission, this) for each tier and resolves every entry with ItemManager.FindItemDefinition(entry.Shortname). A typo in a shortname is reported once at startup with PrintWarning and that entry is skipped, rather than failing silently on every claim. Resolved definitions are cached in the definitions dictionary keyed by shortname, so a claim never touches the item manager lookup path while a player is waiting.

  3. Pick the best tier the player can claim

    ResolveTier walks the list in order and returns the first tier where permission.UserHasPermission(player.UserIDString, tier.Permission) is true. That single pass gives you highest-tier-wins behavior for free. Claiming checks the cooldown afterwards: CmdVipKit reads the stored timestamp, computes ready = last + CooldownSeconds and, if the current unix time from DateTimeOffset.UtcNow.ToUnixTimeSeconds() is below that, reports the remaining time via FormatTime and returns without granting anything, so a vip3 player never receives a vip1 kit as a fallback.

  4. Give the items and store the claim time

    For each entry GiveTier looks the definition up in definitions and calls ItemManager.CreateByItemID(def.itemid, amount, entry.Skin). The amount is clamped with Mathf.Clamp(entry.Amount, 1, def.stackable), and Condition is applied when it is above zero on an item whose hasCondition is true. player.GiveItem(item) puts the item in the inventory or drops it at the player's feet when the inventory is full, and the item's displayName.english is collected for the chat confirmation.

  5. Cooldown bookkeeping per tier

    data.ClaimTimes stores one unix timestamp per player id: the last successful claim. The next allowed claim is that value plus the CooldownSeconds of the tier the player resolved to. Because the timestamp belongs to the last claim regardless of tier, a player who drops from vip3 to vip1 still waits out the lower-tier cooldown instead of claiming twice in a minute. FormatTime renders the remaining seconds as 1h 04m 12s, minutes as 5m 10s and anything shorter as plain seconds.

  6. Commands and language strings

    Both commands are attribute-declared: [ChatCommand("vipkit")] and [ChatCommand("vipkits")], each guarded by permission.UserHasPermission(player.UserIDString, PermUse). Every response goes through the Msg helper and lang.GetMessage, including the tier list, which is assembled shortname by shortname. Both handlers null-check player before use. /vipkits prints the header and one line per tier with its name, permission, cooldown and item summary.

  7. Saving and unloading cleanly

    LoadData reads a StoredData wrapper through Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name), falling back to a fresh object with a warning if the file is missing or corrupt, and creates an empty ClaimTimes dictionary when the field is null. SaveData writes it back with WriteObject, producing oxide/data/VipKits.json. Unload calls SaveData and destroys nothing else, since the plugin holds no timers or UI.

Install it on your server

  1. Save VipKits.cs

    Keep the file name exactly as the class name. Oxide matches them.
  2. Upload it to the plugins folder

    Oxide: oxide/plugins/. Carbon: carbon/plugins/. The server compiles and loads it within a few seconds, no restart needed.
  3. Edit the config if you want

    On first load the default config is written to oxide/config/VipKits.json (Carbon: carbon/configs/VipKits.json). After editing, reload it:oxide.reload VipKits c.reload VipKits # Carbon
  4. Grant the permission

    Give it to every player, or swap default for a group such as vip:oxide.grant group default vipkits.use c.grant group default vipkits.use # Carbon

Variations

Each one opens the generator with a ready prompt you can tweak.

  • Give every tier at once

    Instead of highest-wins, claim all tiers the player has permission for, each on its own cooldown slot.

    Generate this version
  • Daily reset instead of rolling cooldown

    Reset all claims at a fixed UTC hour so players can claim once per day rather than every N hours.

    Generate this version
  • Economics cost per claim

    Charge a configurable amount of Economics balance per claim to keep VIP kits from being fully free.

    Generate this version

FAQ

How do I add another VIP tier?

Add a new object to Tiers in oxide/config/VipKits.json and put it at the correct position in the array, since order decides priority. Then grant the permission from its Permission field, e.g. oxide.grant group vip4 vipkits.vip4. Reload the plugin or restart the server so the new permission is registered before you grant it.

Why does /vipkit tell me my tier is cooling down instead of giving a lower kit?

That is intentional. The plugin resolves the best tier first and checks its cooldown, so a player with vipkits.vip3 never receives the vip1 kit while the tier 3 cooldown runs. If you want lower tiers as a backup, change ResolveTier to keep scanning after a failed cooldown check.

Are cooldowns kept when the server restarts?

Yes. Claim times are written to oxide/data/VipKits.json on every successful claim and read back when the plugin initializes. Deleting that file resets everyone's cooldowns, which is useful after a wipe or a config change that shortens them.

Does this work on Carbon?

Yes. Carbon loads Oxide plugins, so you can drop the same .cs file into Carbon's plugin folder. Both frameworks register permissions and language files the same way. Install by placing the file in oxide/plugins or Carbon's plugins directory and letting it compile on the next reload.

What happens if a shortname in the config is wrong?

The plugin logs a warning once at startup, skips that entry, and gives the rest of the tier normally. Nothing crashes mid-claim. Fix the shortname in the config and reload; the resolved item cache is rebuilt on initialization.

Customize it for your server

Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.

Prompt: Create a Rust Oxide plugin called VipKits with three tiered kits vip1/vip2/vip3, each having its own permission, cooldown and item list in the config. /vipkit claims the highest tier the player can claim, persisting cooldowns to oxide/data.
Customize for my server

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

Last updated Oct 26, 2026.

Customize vip kit tiers

Customize