Skip to content
RustPluginGenerator

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

OneTimeKits.cs
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.

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

Chat and console commands
CommandWhat it doesPermission
/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.listList the configured kits, their permission and item count.Everyone
Permissions
PermissionGrants
onetimekits.useAllows using /claim for kits the player is also permitted for.

How the code works

  1. Config shape

    The nested PluginConfig class holds a Settings block (a boolean Require per-kit permission and a Wipe reset broadcast string) and a Kits dictionary keyed by the kit name used in /claim. Each KitEntry has a Permission string, a Description and an Items list of { Shortname, Amount } objects. LoadConfig reads the file, falls back to LoadDefaultConfig on bad JSON and SaveConfig writes it back. Keys are the [JsonProperty] names so the generated file reads cleanly.

  2. Claim tracking data

    Claims live in oxide/data/OneTimeKits.json through Interface.Oxide.DataFileSystem. The root object is a Data class wrapping a Claims dictionary: outer key is the player's UserIDString, inner list is the kit names already claimed. The file is loaded in Loaded, creating an empty Data if none exists. Writes happen inside SaveData, which is called after every /claim attempt that reaches the give-items stage, and again in Unload. A server crash after items are given but before SaveData runs could lose the claim record, allowing the player to claim again and receive duplicate items.

  3. 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, calls SaveData and, if Settings.Wipe reset broadcast is non-empty, sends that string with Server.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.

  4. The /claim command

    [ChatCommand("claim")] takes BasePlayer player, string command, string[] args. It checks onetimekits.use first, then lower-cases args[0] and looks it up in config.Kits. If Require per-kit permission is set and the kit has a Permission string, permission.UserHasPermission must also pass. It reads the saved list for the player, rejects a repeat claim, builds each item with ItemManager.CreateByName and player.GiveItem, and calls SaveData.

  5. Admin reset and list commands

    [ConsoleCommand("onetimekits.reset")] accepts ConsoleSystem.Arg, requires at least one argument, resolves the target with BasePlayer.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 through arg.ReplyWith. onetimekits.list prints 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.

  6. Language and item delivery

    All player text goes through LoadDefaultMessages and a Msg helper that calls lang.GetMessage. GiveKit creates items with ItemManager.CreateByName(shortname, amount, 0), checks for null in case a shortname is wrong, and hands them over with player.GiveItem. A failed create logs a single PrintWarning at claim time, never inside a hot hook, and the claim is still recorded so a broken kit cannot block the player.

  7. Unload safety

    Unload() calls SaveData one 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

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

    Give it to every player, or swap default for a group such as vip: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 version
  • Team-wide kit sharing

    Only one member of a Rust team may claim a given kit each wipe.

    Generate this version
  • Economy 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.

Prompt: Make an Oxide Rust plugin called OneTimeKits with a config-defined list of kits, a /claim <kit> chat command, and claim records saved to a data file that is cleared in the OnNewSave hook.
Customize for my server

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

Last updated Oct 26, 2026.

Customize one per wipe kits

Customize