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
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.
{
"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
| Command | What it does | Permission |
|---|---|---|
/kit | Lists 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 |
| Permission | Grants |
|---|---|
kitcooldownmanager.admin | Allows using the kitcooldowns.reset console command. |
How the code works
Define kits in the config
The
KitConfigclass holdsDisplay Name,Cooldown Seconds,Max Uses (0 = unlimited),Permission (empty = everyone)andItems, and the plugin ships with astarterand avipkit. EachKitItementry carriesShortname,AmountandSkin, so you can hand out skinned items.LoadConfigis wrapped in a try/catch that prints a warning and falls back toLoadDefaultConfigwhen the JSON is broken, then writes the file back so admins always see every supported key.Normalise kit lookups
Chat commands are typed by hand, so
FindKitcompares the requested name against each dictionary key withstring.Equals(..., StringComparison.OrdinalIgnoreCase). The dictionary key is the kit name players type after/kit;Display Nameis only used in chat output.KitKeywalks the same dictionary to recover the key from aKitConfiginstance, and every stored cooldown entry is indexed by that key. Keeping the key stable means you can renameDisplay Namewithout invalidating stored entries.Check permission, uses and cooldown in order
CanClaimreturns a short reason string used by the chat reply. It first checksPermission(when set) withpermission.UserHasPermission, thenMax Usesagainststate.Uses, then the cooldown by comparingstate.LastClaimplus the kit'sCooldown SecondsagainstDateTime.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.Deliver items and persist state
GiveKitItemsbuilds each item withItemManager.CreateByName(entry.Shortname, entry.Amount, entry.Skin)and callsplayer.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 deliveryCmdKitsetsstate.LastClaimand incrementsstate.Uses, thenSaveDatawrites the wholeStoredDataobject throughInterface.Oxide.DataFileSystem.WriteObject.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 replacesstoredDatawith a freshStoredDatainstance, which emptiesPlayers, saves the file and prints one line to console. Because cooldowns and use counters live only in that file, the next/kitafter 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.Admin reset command
kitcooldowns.resetreads a Steam ID and an optional kit name viaarg.GetString(0, null)andarg.GetString(1, null). It allows the call whenarg.IsAdminis true or the calling player holdskitcooldownmanager.admin. With no kit name the wholePlayerDatarecord is removed fromPlayers; with a kit name only that entry is removed from the record'sKitsdictionary.SaveDataruns at the end either way andarg.ReplyWithconfirms the reset.
Install it on your server
Save KitCooldownManager.cs
Keep the file name exactly as the class name. Oxide matches them.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.Edit the config if you want
On first load the default config is written tooxide/config/KitCooldownManager.json(Carbon:carbon/configs/KitCooldownManager.json). After editing, reload it:oxide.reload KitCooldownManager c.reload KitCooldownManager # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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.
Auto-kit on first join
Gives new players a starter kit once, without them typing a command.
Generate this versionShared team cooldowns
Applies the cooldown to the whole team instead of each player.
Generate this versionKit preview UI
Shows kit contents in a CUI panel before claiming.
Generate this version
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.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More kits plugins
- Give kit commandThis plugin adds an admin /givekit command plus a matching console command that hands a config-defined kit to any online or sleeping player,โฆOnServerInitialized
- Group respawn kitsGives each player a respawn loadout based on the highest-priority Oxide group they belong to.OnPlayerRespawned
- One per wipe kitsGives players configurable kits that can be claimed once per wipe, tracked in a data file cleared when the server starts a new map save, using theโฆOnNewSave
Last updated Oct 26, 2026.
Customize kits with cooldowns
Customize