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
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.
{
"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
| Command | What it does | Permission |
|---|---|---|
/vipkit | Claims the highest VIP tier the player has permission for and whose cooldown has expired | vipkits.use |
/vipkits | Lists every configured tier, its permission and its cooldown | vipkits.use |
| Permission | Grants |
|---|---|
vipkits.use | Allows the player to run /vipkit and /vipkits |
vipkits.vip1 | Grants access to the first VIP tier (default permission name of tier 1) |
vipkits.vip2 | Grants access to the second VIP tier (default permission name of tier 2) |
vipkits.vip3 | Grants access to the third VIP tier (default permission name of tier 3) |
How the code works
Model the tiers in the config
The nested
PluginConfigclass holdsTiers, aList<Tier>where eachTierhasName,Permission,CooldownSecondsandItems. EachKitItemcarriesShortname,Amount,SkinandCondition, all with[JsonProperty]names so the config file stays readable. Tiers are ordered highest-first:/vipkittakes the first match, so the array order is the priority order.LoadConfigchecks thatTiersis not null and falls back toLoadDefaultConfigwith a warning on a hand-edited file, then callsSaveConfig.Register permissions and resolve item definitions once
Initregisters onlyvipkits.use.OnServerInitializediteratesconfig.Tiers, callspermission.RegisterPermission(tier.Permission, this)for each tier and resolves every entry withItemManager.FindItemDefinition(entry.Shortname). A typo in a shortname is reported once at startup withPrintWarningand that entry is skipped, rather than failing silently on every claim. Resolved definitions are cached in thedefinitionsdictionary keyed by shortname, so a claim never touches the item manager lookup path while a player is waiting.Pick the best tier the player can claim
ResolveTierwalks the list in order and returns the first tier wherepermission.UserHasPermission(player.UserIDString, tier.Permission)is true. That single pass gives you highest-tier-wins behavior for free. Claiming checks the cooldown afterwards:CmdVipKitreads the stored timestamp, computesready = last + CooldownSecondsand, if the current unix time fromDateTimeOffset.UtcNow.ToUnixTimeSeconds()is below that, reports the remaining time viaFormatTimeand returns without granting anything, so avip3player never receives avip1kit as a fallback.Give the items and store the claim time
For each entry
GiveTierlooks the definition up indefinitionsand callsItemManager.CreateByItemID(def.itemid, amount, entry.Skin). The amount is clamped withMathf.Clamp(entry.Amount, 1, def.stackable), andConditionis applied when it is above zero on an item whosehasConditionis 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'sdisplayName.englishis collected for the chat confirmation.Cooldown bookkeeping per tier
data.ClaimTimesstores one unix timestamp per player id: the last successful claim. The next allowed claim is that value plus theCooldownSecondsof the tier the player resolved to. Because the timestamp belongs to the last claim regardless of tier, a player who drops fromvip3tovip1still waits out the lower-tier cooldown instead of claiming twice in a minute.FormatTimerenders the remaining seconds as1h 04m 12s, minutes as5m 10sand anything shorter as plain seconds.Commands and language strings
Both commands are attribute-declared:
[ChatCommand("vipkit")]and[ChatCommand("vipkits")], each guarded bypermission.UserHasPermission(player.UserIDString, PermUse). Every response goes through theMsghelper andlang.GetMessage, including the tier list, which is assembled shortname by shortname. Both handlers null-checkplayerbefore use./vipkitsprints the header and one line per tier with its name, permission, cooldown and item summary.Saving and unloading cleanly
LoadDatareads aStoredDatawrapper throughInterface.Oxide.DataFileSystem.ReadObject<StoredData>(Name), falling back to a fresh object with a warning if the file is missing or corrupt, and creates an emptyClaimTimesdictionary when the field is null.SaveDatawrites it back withWriteObject, producingoxide/data/VipKits.json.UnloadcallsSaveDataand destroys nothing else, since the plugin holds no timers or UI.
Install it on your server
Save VipKits.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/VipKits.json(Carbon:carbon/configs/VipKits.json). After editing, reload it:oxide.reload VipKits c.reload VipKits # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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 versionDaily 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 versionEconomics 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.
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
- Kits with cooldownsAdds permission-aware kits with per-kit cooldowns and per-wipe use limits.OnServerInitializedOnNewSave
Last updated Oct 26, 2026.
Customize vip kit tiers
Customize