How to make a Rust one-time claim kits plugin (Oxide)
Gives 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 hook.
Admins want starter kits that issue once per wipe per player, so a fresh map means fresh kits but a mid-wipe reconnect does not. The tricky part is knowing when a wipe happened: Rust servers get wiped by hand, and the cleanest signal in Oxide is OnNewSave, which fires when the server generates or switches to a new map save. This plugin stores claim records in oxide/data as a dictionary mapping each player's ID to the list of kit names they have claimed. When OnNewSave fires, that dictionary is replaced with an empty one so every kit becomes claimable again. Each kit is defined in the config with its granted items and an optional per-kit permission. A single /claim <kit> chat command checks the plugin permission, the kit's permission and the saved claim record before handing out items and saving. Two console commands let admins inspect the kit list and reset a player's claims.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
namespace Oxide.Plugins
{
[Info("OneTimeKits", "RustPluginGenerator", "1.0.0")]
[Description("Gives players configurable kits that can be claimed once per wipe.")]
public class OneTimeKits : RustPlugin
{
private const string PermUse = "onetimekits.use";
private const string DataFile = "OneTimeKits";
[PluginReference] private Plugin Economics;
private PluginConfig config;
private Data data;
private class KitItem
{
[JsonProperty("Shortname")] public string Shortname { get; set; } = "scrap";
[JsonProperty("Amount")] public int Amount { get; set; } = 100;
}
private class KitEntry
{
[JsonProperty("Permission")] public string Permission { get; set; } = "";
[JsonProperty("Description")] public string Description { get; set; } = "";
[JsonProperty("Items")] public List<KitItem> Items { get; set; } = new List<KitItem>();
}
private class Settings
{
[JsonProperty("Require per-kit permission")] public bool RequirePerKitPermission { get; set; } = false;
[JsonProperty("Wipe reset broadcast")] public string WipeResetMessage { get; set; } = "A new wipe has started. All kit claims are available again.";
}
private class PluginConfig
{
[JsonProperty("Settings")] public Settings Settings { get; set; } = new Settings();
[JsonProperty("Kits")] public Dictionary<string, KitEntry> Kits { get; set; } = new Dictionary<string, KitEntry>();
}
private class Data
{
[JsonProperty("Claims")] public Dictionary<string, List<string>> Claims { get; set; } = new Dictionary<string, List<string>>();
}
protected override void LoadDefaultConfig()
{
config = new PluginConfig
{
Kits = new Dictionary<string, KitEntry>
{
["starter"] = new KitEntry
{
Description = "Basic starter kit",
Items = new List<KitItem>
{
new KitItem { Shortname = "wood", Amount = 5000 },
new KitItem { Shortname = "stone", Amount = 3000 },
new KitItem { Shortname = "scrap", Amount = 250 }
}
},
["builder"] = new KitEntry
{
Permission = "onetimekits.builder",
Description = "Building materials kit",
Items = new List<KitItem>
{
new KitItem { Shortname = "wood", Amount = 10000 },
new KitItem { Shortname = "stone", Amount = 8000 },
new KitItem { Shortname = "metal.fragments", Amount = 2000 }
}
}
}
};
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<PluginConfig>();
if (config == null) throw new Exception();
if (config.Kits == null) config.Kits = new Dictionary<string, KitEntry>();
if (config.Settings == null) config.Settings = new Settings();
}
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 this command.",
["Usage"] = "Usage: /claim <kit>",
["KitPermission"] = "You do not have permission to claim the {0} kit.",
["KitUnknown"] = "There is no kit called {0}.",
["KitUsed"] = "You have already claimed the {0} kit this wipe.",
["KitGiven"] = "You claimed the {0} kit.",
["KitBroken"] = "Some items in the {0} kit could not be created. Contact an admin.",
["ResetDone"] = "Reset {0} claim(s) for {1}.",
["ResetNone"] = "No matching claims found.",
["ListHeader"] = "Configured kits:"
}, 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 Loaded()
{
data = Interface.Oxide.DataFileSystem.ReadObject<Data>(DataFile);
if (data == null || data.Claims == null)
{
data = new Data();
data.Claims = new Dictionary<string, List<string>>();
}
}
private void Unload()
{
SaveData();
}
private void SaveData()
{
Interface.Oxide.DataFileSystem.WriteObject(DataFile, data);
}
private void OnNewSave(string strFilename)
{
data = new Data();
data.Claims = new Dictionary<string, List<string>>();
SaveData();
if (!string.IsNullOrEmpty(config.Settings.WipeResetMessage))
{
Server.Broadcast(config.Settings.WipeResetMessage);
}
}
[ChatCommand("claim")]
private void CmdClaim(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
if (args == null || args.Length < 1)
{
SendReply(player, Msg("Usage", player.UserIDString));
return;
}
string kitName = args[0].ToLowerInvariant();
KitEntry kit;
if (!config.Kits.TryGetValue(kitName, out kit) || kit == null)
{
SendReply(player, Msg("KitUnknown", player.UserIDString, kitName));
return;
}
if (config.Settings.RequirePerKitPermission && !string.IsNullOrEmpty(kit.Permission)
&& !permission.UserHasPermission(player.UserIDString, kit.Permission))
{
SendReply(player, Msg("KitPermission", player.UserIDString, kitName));
return;
}
List<string> claims;
if (data.Claims.TryGetValue(player.UserIDString, out claims) && claims != null && claims.Contains(kitName))
{
SendReply(player, Msg("KitUsed", player.UserIDString, kitName));
return;
}
if (!GiveKit(player, kitName, kit))
{
SendReply(player, Msg("KitBroken", player.UserIDString, kitName));
}
else
{
SendReply(player, Msg("KitGiven", player.UserIDString, kitName));
}
if (claims == null)
{
claims = new List<string>();
data.Claims[player.UserIDString] = claims;
}
claims.Add(kitName);
SaveData();
}
private bool GiveKit(BasePlayer player, string kitName, KitEntry kit)
{
if (kit.Items == null) return true;
bool allOk = true;
foreach (KitItem entry in kit.Items)
{
if (entry == null || string.IsNullOrEmpty(entry.Shortname)) continue;
Item item = ItemManager.CreateByName(entry.Shortname, Math.Max(1, entry.Amount), 0);
if (item == null)
{
PrintWarning("Kit '" + kitName + "' references an unknown item: " + entry.Shortname);
allOk = false;
continue;
}
player.GiveItem(item);
}
return allOk;
}
[ConsoleCommand("onetimekits.reset")]
private void CcmdReset(ConsoleSystem.Arg arg)
{
if (arg == null || arg.Args == null || arg.Args.Length < 1) return;
string who = arg.GetString(0, "");
string kitArg = arg.Args.Length >= 2 ? arg.GetString(1, "").ToLowerInvariant() : null;
string key = null;
BasePlayer target = BasePlayer.FindAwakeOrSleeping(who);
if (target != null)
{
key = target.UserIDString;
}
else if (who.Length > 0 && data.Claims.ContainsKey(who))
{
key = who;
}
if (key == null)
{
arg.ReplyWith(lang.GetMessage("ResetNone", this));
return;
}
List<string> claims;
if (!data.Claims.TryGetValue(key, out claims) || claims == null)
{
arg.ReplyWith(lang.GetMessage("ResetNone", this));
return;
}
int removed;
if (kitArg == null)
{
removed = claims.Count;
data.Claims.Remove(key);
}
else
{
removed = claims.RemoveAll(c => c == kitArg);
if (claims.Count == 0) data.Claims.Remove(key);
}
SaveData();
arg.ReplyWith(string.Format(Msg("ResetDone", null, removed, key)));
}
[ConsoleCommand("onetimekits.list")]
private void CcmdList(ConsoleSystem.Arg arg)
{
if (arg == null) return;
arg.ReplyWith(Msg("ListHeader", null));
foreach (KeyValuePair<string, KitEntry> kvp in config.Kits)
{
string perm = string.IsNullOrEmpty(kvp.Value.Permission) ? "-" : kvp.Value.Permission;
arg.ReplyWith(" " + kvp.Key + " | permission: " + perm + " | items: " + (kvp.Value.Items != null ? kvp.Value.Items.Count : 0));
}
}
}
}Hooks used: OnNewSave
Config file
Created automatically on first load at oxide/config/OneTimeKits.json.
{
"Settings": {
"Require per-kit permission": false,
"Wipe reset broadcast": "A new wipe has started. All kit claims are available again."
},
"Kits": {
"starter": {
"Permission": "",
"Description": "Basic starter kit",
"Items": [
{
"Shortname": "wood",
"Amount": 5000
},
{
"Shortname": "stone",
"Amount": 3000
},
{
"Shortname": "scrap",
"Amount": 250
}
]
},
"builder": {
"Permission": "onetimekits.builder",
"Description": "Building materials kit",
"Items": [
{
"Shortname": "wood",
"Amount": 10000
},
{
"Shortname": "stone",
"Amount": 8000
},
{
"Shortname": "metal.fragments",
"Amount": 2000
}
]
}
},
"Claims": {}
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/claim <kit> | Claim a configured kit once this wipe. | onetimekits.use |
onetimekits.reset <steamid|name> [kit] | Clear one player's claim records (all kits or a single kit). | Everyone |
onetimekits.list | List the configured kits, their permission and item count. | Everyone |
| Permission | Grants |
|---|---|
onetimekits.use | Allows using /claim for kits the player is also permitted for. |
How the code works
Config shape
The nested
PluginConfigclass holds aSettingsblock (a booleanRequire per-kit permissionand aWipe reset broadcaststring) and aKitsdictionary keyed by the kit name used in/claim. EachKitEntryhas aPermissionstring, aDescriptionand anItemslist of{ Shortname, Amount }objects.LoadConfigreads the file, falls back toLoadDefaultConfigon bad JSON andSaveConfigwrites it back. Keys are the[JsonProperty]names so the generated file reads cleanly.Claim tracking data
Claims live in
oxide/data/OneTimeKits.jsonthroughInterface.Oxide.DataFileSystem. The root object is aDataclass wrapping aClaimsdictionary: outer key is the player'sUserIDString, inner list is the kit names already claimed. The file is loaded inLoaded, creating an emptyDataif none exists. Writes happen insideSaveData, which is called after every/claimattempt that reaches the give-items stage, and again inUnload. A server crash after items are given but beforeSaveDataruns could lose the claim record, allowing the player to claim again and receive duplicate items.Reset on new map save
OnNewSave(string strFilename)fires when the server starts a new map save. The hook replaces the claim dictionary with an empty one, callsSaveDataand, ifSettings.Wipe reset broadcastis non-empty, sends that string withServer.Broadcast. This is the only place claims are reset; nothing else touches wipe timing, so admin wipes behave exactly like automatic ones. No timers are involved.The /claim command
[ChatCommand("claim")]takesBasePlayer player, string command, string[] args. It checksonetimekits.usefirst, then lower-casesargs[0]and looks it up inconfig.Kits. IfRequire per-kit permissionis set and the kit has aPermissionstring,permission.UserHasPermissionmust also pass. It reads the saved list for the player, rejects a repeat claim, builds each item withItemManager.CreateByNameandplayer.GiveItem, and callsSaveData.Admin reset and list commands
[ConsoleCommand("onetimekits.reset")]acceptsConsoleSystem.Arg, requires at least one argument, resolves the target withBasePlayer.FindAwakeOrSleeping(who), and falls back to a raw steam id already present in the data. It removes one kit or the whole entry and replies througharg.ReplyWith.onetimekits.listprints each kit name, its permission column and item count. Both run from the server console or RCON; reset needs a target argument (steamid or name), while list takes none.Language and item delivery
All player text goes through
LoadDefaultMessagesand aMsghelper that callslang.GetMessage.GiveKitcreates items withItemManager.CreateByName(shortname, amount, 0), checks for null in case a shortname is wrong, and hands them over withplayer.GiveItem. A failed create logs a singlePrintWarningat claim time, never inside a hot hook, and the claim is still recorded so a broken kit cannot block the player.Unload safety
Unload()callsSaveDataone last time. The plugin holds no timers and no CUI, so nothing else needs cleanup. If you later add a CUI overlay, destroy it per player before unload, since this plugin currently has no UI to tear down.
Install it on your server
Save OneTimeKits.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/OneTimeKits.json(Carbon:carbon/configs/OneTimeKits.json). After editing, reload it:oxide.reload OneTimeKits c.reload OneTimeKits # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default onetimekits.use c.grant group default onetimekits.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Per-kit cooldown instead of once
Allow the same kit again after a real-time delay rather than once per wipe.
Generate this versionTeam-wide kit sharing
Only one member of a Rust team may claim a given kit each wipe.
Generate this versionEconomy purchase on claim
Charge scrap or an Economics balance before granting the kit.
Generate this version
FAQ
When exactly is a claim reset?
On OnNewSave, which Oxide fires when the server begins a new map save. That happens on a server wipe and when you point the server at a fresh save file, so manual admin wipes behave the same as automatic ones. Nothing resets on a normal restart of the same map.
Can a player claim two different kits?
Yes. Require per-kit permission defaults to false, so any player with onetimekits.use can claim each configured kit once. Set it to true and fill in each kit's Permission field to gate kits individually through a permission plugin.
What happens if a kit shortname is misspelled?
ItemManager.CreateByName returns null. The plugin skips that item, logs one warning and still records the claim, so a player never gets a stuck claim loop. Fix the shortname in oxide/config/OneTimeKits.json and reset the player's entry with onetimekits.reset.
Does it work on Carbon?
Yes. Carbon loads the same Oxide plugin file, so Interface.Oxide.DataFileSystem, lang and the OnNewSave hook all work unchanged. Drop the .cs file into the plugin folder and reload.
How do I wipe claims without wiping the map?
Use onetimekits.reset <steamid|name> for a single player, or delete oxide/data/OneTimeKits.json and reload the plugin to clear everyone. The next /claim will succeed for any player who has not claimed since the reset.
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 one per wipe kits
Customize