Skip to content
RustPluginGenerator

How to make a Rust kits plugin with cooldowns (Oxide/Carbon)

Adds permission-aware kits with per-kit cooldowns and per-wipe use limits. Uses OnNewSave to reset stored cooldowns and uses when a new save is created.

Most kit plugins either ignore cooldowns or lose track of them when the map wipes, which lets players claim the same starter kit twice per wipe. This plugin stores cooldown timestamps and use counters per player and per kit in a single data file under oxide/data, and clears the player dictionary inside OnNewSave, so a wipe gives everyone a clean slate automatically. Each kit is defined in the config with an item list, a cooldown in seconds, an optional per-wipe use cap and an optional permission. /kit lists the kits a player may claim, /kit <name> gives the items and starts the cooldown, and admins can clear stored data with a console command. Item delivery uses ItemManager.CreateByName and player.GiveItem, so items are dropped at the player's feet if the inventory is full. The code is a single file with no external dependencies.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("Kit Cooldown Manager", "RustPluginGenerator", "1.0.0")]
    [Description("Gives players configurable kits with cooldowns and per-wipe use limits.")]
    public class KitCooldownManager : RustPlugin
    {
        private const string PermAdmin = "kitcooldownmanager.admin";

        private PluginConfig config;
        private StoredData storedData;

        private class KitItem
        {
            [JsonProperty("Shortname")] public string Shortname { get; set; } = "wood";
            [JsonProperty("Amount")] public int Amount { get; set; } = 1000;
            [JsonProperty("Skin")] public ulong Skin { get; set; } = 0;
        }

        private class KitConfig
        {
            [JsonProperty("Display Name")] public string DisplayName { get; set; } = "Kit";
            [JsonProperty("Cooldown Seconds")] public int CooldownSeconds { get; set; } = 3600;
            [JsonProperty("Max Uses (0 = unlimited)")] public int MaxUses { get; set; } = 0;
            [JsonProperty("Permission (empty = everyone)")] public string Permission { get; set; } = "";
            [JsonProperty("Items")] public List<KitItem> Items { get; set; } = new List<KitItem>();
        }

        private class PluginConfig
        {
            [JsonProperty("Kits")]
            public Dictionary<string, KitConfig> Kits { get; set; } = new Dictionary<string, KitConfig>();
        }

        private class KitState
        {
            [JsonProperty("Last Claim")] public DateTime LastClaim { get; set; } = DateTime.MinValue;
            [JsonProperty("Uses")] public int Uses { get; set; } = 0;
        }

        private class PlayerData
        {
            [JsonProperty("Kits")] public Dictionary<string, KitState> Kits { get; set; } = new Dictionary<string, KitState>();
        }

        private class StoredData
        {
            [JsonProperty("Players")] public Dictionary<string, PlayerData> Players { get; set; } = new Dictionary<string, PlayerData>();
        }

        protected override void LoadDefaultConfig()
        {
            config = new PluginConfig
            {
                Kits = new Dictionary<string, KitConfig>
                {
                    ["starter"] = new KitConfig
                    {
                        DisplayName = "Starter Kit",
                        CooldownSeconds = 3600,
                        MaxUses = 3,
                        Permission = "",
                        Items = new List<KitItem>
                        {
                            new KitItem { Shortname = "wood", Amount = 1000 },
                            new KitItem { Shortname = "stone", Amount = 500 },
                            new KitItem { Shortname = "bandage", Amount = 5 }
                        }
                    },
                    ["vip"] = new KitConfig
                    {
                        DisplayName = "VIP Kit",
                        CooldownSeconds = 7200,
                        MaxUses = 0,
                        Permission = "kitcooldownmanager.vip",
                        Items = new List<KitItem>
                        {
                            new KitItem { Shortname = "rifle.ak", Amount = 1 },
                            new KitItem { Shortname = "ammo.rifle", Amount = 120 },
                            new KitItem { Shortname = "syringe.medical", Amount = 5 }
                        }
                    }
                }
            };
        }

        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>
            {
                ["KitListHeader"] = "Available kits:",
                ["KitListEntry"] = "<color=#ce422b>{0}</color> - {1}",
                ["KitReady"] = "ready",
                ["KitCooldown"] = "cooldown {0}",
                ["KitUsesLeft"] = "{0} use(s) left this wipe",
                ["KitUnknown"] = "No kit named '{0}'. Use /kit to list kits.",
                ["KitNoPermission"] = "You do not have permission to claim this kit.",
                ["KitOnCooldown"] = "This kit is on cooldown for another {0}.",
                ["KitUsesExhausted"] = "You have used this kit the maximum number of times this wipe.",
                ["KitClaimed"] = "You claimed the {0}.",
                ["KitBadItems"] = "These items were skipped (unknown shortname): {0}",
                ["KitNoKits"] = "No kits are configured.",
                ["DataWiped"] = "New save detected, all kit cooldowns and uses were reset.",
                ["ResetUsage"] = "Usage: kitcooldowns.reset <steamid> [kit]",
                ["ResetDone"] = "Reset kit data for {0}."
            }, 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(PermAdmin, this);
            LoadData();
        }

        private void OnServerInitialized()
        {
            foreach (var kit in config.Kits)
            {
                if (!string.IsNullOrEmpty(kit.Value.Permission))
                    permission.RegisterPermission(kit.Value.Permission, this);
            }
        }

        private void Unload()
        {
            SaveData();
        }

        private void LoadData()
        {
            storedData = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name) ?? new StoredData();
            if (storedData.Players == null) storedData.Players = new Dictionary<string, PlayerData>();
        }

        private void SaveData()
        {
            Interface.Oxide.DataFileSystem.WriteObject(Name, storedData);
        }

        private void OnNewSave(string strFilename)
        {
            storedData = new StoredData();
            SaveData();
            Puts(Msg("DataWiped"));
        }

        private PlayerData GetPlayerData(string userId)
        {
            PlayerData data;
            if (!storedData.Players.TryGetValue(userId, out data))
            {
                data = new PlayerData();
                storedData.Players[userId] = data;
            }
            return data;
        }

        private KitConfig FindKit(string name)
        {
            if (string.IsNullOrEmpty(name)) return null;
            foreach (var entry in config.Kits)
            {
                if (string.Equals(entry.Key, name, StringComparison.OrdinalIgnoreCase))
                    return entry.Value;
            }
            return null;
        }

        private string KitKey(KitConfig kit)
        {
            foreach (var entry in config.Kits)
            {
                if (entry.Value == kit) return entry.Key;
            }
            return null;
        }

        private string Humanize(TimeSpan span)
        {
            if (span.TotalHours >= 1) return string.Format("{0}h {1}m", (int)span.TotalHours, span.Minutes);
            if (span.TotalMinutes >= 1) return string.Format("{0}m {1}s", span.Minutes, span.Seconds);
            return string.Format("{0}s", (int)span.TotalSeconds);
        }

        private string CanClaim(BasePlayer player, string key, KitConfig kit)
        {
            if (!string.IsNullOrEmpty(kit.Permission) && !permission.UserHasPermission(player.UserIDString, kit.Permission))
                return "permission";

            var pdata = GetPlayerData(player.UserIDString);
            KitState state;
            if (!pdata.Kits.TryGetValue(key, out state)) state = new KitState();

            if (kit.MaxUses > 0 && state.Uses >= kit.MaxUses)
                return "uses";

            var nextClaim = state.LastClaim + TimeSpan.FromSeconds(Math.Max(0, kit.CooldownSeconds));
            if (nextClaim > DateTime.UtcNow)
                return "cooldown";

            return null;
        }

        private void GiveKitItems(BasePlayer player, KitConfig kit)
        {
            var skipped = new List<string>();
            foreach (var entry in kit.Items)
            {
                if (string.IsNullOrEmpty(entry.Shortname) || entry.Amount <= 0) continue;
                var item = ItemManager.CreateByName(entry.Shortname, entry.Amount, entry.Skin);
                if (item == null)
                {
                    skipped.Add(entry.Shortname);
                    continue;
                }
                player.GiveItem(item);
            }
            if (skipped.Count > 0)
                player.ChatMessage(Msg("KitBadItems", player.UserIDString, string.Join(", ", skipped.ToArray())));
        }

        [ChatCommand("kit")]
        private void CmdKit(BasePlayer player, string command, string[] args)
        {
            if (player == null || config.Kits == null || config.Kits.Count == 0)
            {
                if (player != null) player.ChatMessage(Msg("KitNoKits", player.UserIDString));
                return;
            }

            if (args == null || args.Length == 0)
            {
                ListKits(player);
                return;
            }

            var kit = FindKit(args[0]);
            if (kit == null)
            {
                player.ChatMessage(Msg("KitUnknown", player.UserIDString, args[0]));
                return;
            }

            var key = KitKey(kit);
            var reason = CanClaim(player, key, kit);
            if (reason == "permission")
            {
                player.ChatMessage(Msg("KitNoPermission", player.UserIDString));
                return;
            }
            if (reason == "uses")
            {
                player.ChatMessage(Msg("KitUsesExhausted", player.UserIDString));
                return;
            }

            var pdata = GetPlayerData(player.UserIDString);
            KitState state;
            if (!pdata.Kits.TryGetValue(key, out state))
            {
                state = new KitState();
                pdata.Kits[key] = state;
            }

            if (reason == "cooldown")
            {
                var remaining = (state.LastClaim + TimeSpan.FromSeconds(Math.Max(0, kit.CooldownSeconds))) - DateTime.UtcNow;
                player.ChatMessage(Msg("KitOnCooldown", player.UserIDString, Humanize(remaining)));
                return;
            }

            GiveKitItems(player, kit);
            state.LastClaim = DateTime.UtcNow;
            state.Uses++;
            SaveData();
            player.ChatMessage(Msg("KitClaimed", player.UserIDString, string.IsNullOrEmpty(kit.DisplayName) ? key : kit.DisplayName));
        }

        private void ListKits(BasePlayer player)
        {
            player.ChatMessage(Msg("KitListHeader", player.UserIDString));
            foreach (var entry in config.Kits)
            {
                var kit = entry.Value;
                if (!string.IsNullOrEmpty(kit.Permission) && !permission.UserHasPermission(player.UserIDString, kit.Permission))
                    continue;

                var pdata = GetPlayerData(player.UserIDString);
                KitState state;
                if (!pdata.Kits.TryGetValue(entry.Key, out state)) state = new KitState();

                string status;
                var nextClaim = state.LastClaim + TimeSpan.FromSeconds(Math.Max(0, kit.CooldownSeconds));
                if (nextClaim > DateTime.UtcNow)
                    status = Msg("KitCooldown", player.UserIDString, Humanize(nextClaim - DateTime.UtcNow));
                else
                    status = Msg("KitReady", player.UserIDString);

                if (kit.MaxUses > 0)
                    status += ", " + Msg("KitUsesLeft", player.UserIDString, Math.Max(0, kit.MaxUses - state.Uses));

                player.ChatMessage(Msg("KitListEntry", player.UserIDString, entry.Key, status));
            }
        }

        [ConsoleCommand("kitcooldowns.reset")]
        private void CcmdReset(ConsoleSystem.Arg arg)
        {
            if (arg == null) return;
            if (!arg.IsAdmin && (arg.Player() == null || !permission.UserHasPermission(arg.Player().UserIDString, PermAdmin)))
            {
                arg.ReplyWith("You do not have permission to use this command.");
                return;
            }

            var userId = arg.GetString(0, null);
            if (string.IsNullOrEmpty(userId))
            {
                arg.ReplyWith(Msg("ResetUsage"));
                return;
            }

            var kitName = arg.GetString(1, null);
            PlayerData pdata;
            if (!storedData.Players.TryGetValue(userId, out pdata))
            {
                arg.ReplyWith(Msg("ResetDone", null, userId));
                return;
            }

            if (string.IsNullOrEmpty(kitName))
            {
                storedData.Players.Remove(userId);
            }
            else
            {
                foreach (var entry in config.Kits)
                {
                    if (string.Equals(entry.Key, kitName, StringComparison.OrdinalIgnoreCase))
                    {
                        pdata.Kits.Remove(entry.Key);
                        break;
                    }
                }
            }

            SaveData();
            arg.ReplyWith(Msg("ResetDone", null, userId));
        }
    }
}

Hooks used: OnServerInitialized, OnNewSave

Config file

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

KitCooldownManager.json
{
  "Kits": {
    "starter": {
      "Display Name": "Starter Kit",
      "Cooldown Seconds": 3600,
      "Max Uses (0 = unlimited)": 3,
      "Permission (empty = everyone)": "",
      "Items": [
        {
          "Shortname": "wood",
          "Amount": 1000,
          "Skin": 0
        },
        {
          "Shortname": "stone",
          "Amount": 500,
          "Skin": 0
        },
        {
          "Shortname": "bandage",
          "Amount": 5,
          "Skin": 0
        }
      ]
    },
    "vip": {
      "Display Name": "VIP Kit",
      "Cooldown Seconds": 7200,
      "Max Uses (0 = unlimited)": 0,
      "Permission (empty = everyone)": "kitcooldownmanager.vip",
      "Items": [
        {
          "Shortname": "rifle.ak",
          "Amount": 1,
          "Skin": 0
        },
        {
          "Shortname": "ammo.rifle",
          "Amount": 120,
          "Skin": 0
        },
        {
          "Shortname": "syringe.medical",
          "Amount": 5,
          "Skin": 0
        }
      ]
    }
  },
  "Players": {
    "<steamid>": {
      "Kits": {
        "starter": {
          "Last Claim": "0001-01-01T00:00:00",
          "Uses": 0
        }
      }
    }
  }
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/kitLists every kit the player may claim, with cooldown state and remaining uses.Everyone
/kit <name>Claims the named kit if permission, cooldown and use limit allow it.Everyone
kitcooldowns.reset <steamid> [kit]Clears cooldown and use counters for one player (or one kit), from the server console, RCON or in-game admin.kitcooldownmanager.admin
Permissions
PermissionGrants
kitcooldownmanager.adminAllows using the kitcooldowns.reset console command.

How the code works

  1. Define kits in the config

    The KitConfig class holds Display Name, Cooldown Seconds, Max Uses (0 = unlimited), Permission (empty = everyone) and Items, and the plugin ships with a starter and a vip kit. Each KitItem entry carries Shortname, Amount and Skin, so you can hand out skinned items. LoadConfig is wrapped in a try/catch that prints a warning and falls back to LoadDefaultConfig when the JSON is broken, then writes the file back so admins always see every supported key.

  2. Normalise kit lookups

    Chat commands are typed by hand, so FindKit compares the requested name against each dictionary key with string.Equals(..., StringComparison.OrdinalIgnoreCase). The dictionary key is the kit name players type after /kit; Display Name is only used in chat output. KitKey walks the same dictionary to recover the key from a KitConfig instance, and every stored cooldown entry is indexed by that key. Keeping the key stable means you can rename Display Name without invalidating stored entries.

  3. Check permission, uses and cooldown in order

    CanClaim returns a short reason string used by the chat reply. It first checks Permission (when set) with permission.UserHasPermission, then Max Uses against state.Uses, then the cooldown by comparing state.LastClaim plus the kit's Cooldown Seconds against DateTime.UtcNow. Checking the cheap conditions first means a player without the permission never sees a cooldown message, and a player on cooldown still gets the specific reason instead of a generic failure.

  4. Deliver items and persist state

    GiveKitItems builds each item with ItemManager.CreateByName(entry.Shortname, entry.Amount, entry.Skin) and calls player.GiveItem. Unknown shortnames are collected into a list and sent back to the player in one chat message, not printed to the console, so a typo in config does not spam logs on every claim. After delivery CmdKit sets state.LastClaim and increments state.Uses, then SaveData writes the whole StoredData object through Interface.Oxide.DataFileSystem.WriteObject.

  5. Reset everything on a new save

    OnNewSave(string strFilename) fires when the server generates a new save, which is what a Rust wipe produces. The plugin replaces storedData with a fresh StoredData instance, which empties Players, saves the file and prints one line to console. Because cooldowns and use counters live only in that file, the next /kit after a wipe behaves like a first-time claim. Running a manual save or restarting the server does not call this hook, so nothing is reset accidentally.

  6. Admin reset command

    kitcooldowns.reset reads a Steam ID and an optional kit name via arg.GetString(0, null) and arg.GetString(1, null). It allows the call when arg.IsAdmin is true or the calling player holds kitcooldownmanager.admin. With no kit name the whole PlayerData record is removed from Players; with a kit name only that entry is removed from the record's Kits dictionary. SaveData runs at the end either way and arg.ReplyWith confirms the reset.

Install it on your server

  1. Save KitCooldownManager.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/KitCooldownManager.json (Carbon: carbon/configs/KitCooldownManager.json). After editing, reload it:oxide.reload KitCooldownManager c.reload KitCooldownManager # Carbon
  4. Grant the permission

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

Variations

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

FAQ

Do cooldowns survive a server restart?

Yes. Cooldown expiry is stored as a UTC timestamp in the Last Claim field of oxide/data/KitCooldownManager.json and reloaded by LoadData in Init. A restart alone does not clear it; only OnNewSave or the admin reset command does.

What happens if a kit item shortname is wrong?

ItemManager.CreateByName returns null, the entry is skipped, and the player gets one chat warning listing the bad shortnames. Nothing is written to the server console, so repeated claims stay quiet.

Does Max Uses reset on wipe or on cooldown?

Only on wipe. Uses counts claims since the last OnNewSave, independent of the per-kit cooldown. Set Max Uses (0 = unlimited) to 0 for unlimited claims.

How do I install this on Oxide or Carbon?

Drop the .cs file into oxide/plugins and run oxide.reload KitCooldownManager. The default config is written to oxide/config/KitCooldownManager.json on first load; edit it and reload. Carbon loads the same plugin file unchanged.

Can a kit require a permission and a cooldown?

Yes. Permission is checked first in CanClaim, so a player without the permission gets the permission message and never consumes a use or starts a cooldown.

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 kits plugin with /kit listing and /kit <name> claiming, per-kit item lists, cooldown seconds, max uses per wipe, optional permission, data stored in oxide/data and reset in OnNewSave.
Customize for my server

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

Last updated Oct 26, 2026.

Customize kits with cooldowns

Customize