Skip to content
RustPluginGenerator

How to make a Rust backpack plugin (Oxide/Carbon personal storage)

Adds a /backpack command that opens a per-player StorageContainer sized by permission, with contents persisted to oxide/data and optionally dropped on death. Contents are saved in OnPlayerLootEnd and restored on open.

Rust has no built-in personal storage for players, so admins who want a backpack usually need either a wipe-proof data file or a custom container that survives disconnects. The tricky parts are tracking which player has which container open, restoring item data correctly (shortname, amount, skin, condition) and deciding what happens when the owner dies while the contents are loaded. This plugin keeps item data in an Oxide data file and rebuilds the inventory every time the player runs /backpack. Slot counts come from the config, with a separate VIP value, so VIPs can have more slots than regular players. OnPlayerLootEnd writes the live container back to disk when the player closes it, and a config flag controls whether the contents spill onto the ground on death. Everything goes through the standard StorageContainer type, so the UI is the familiar Rust loot panel with no CUI to maintain.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("Backpack", "RustPluginGenerator", "1.0.0")]
    [Description("Adds a permission-sized personal storage container opened with /backpack.")]
    public class Backpack : RustPlugin
    {
        private const string PermUse = "backpack.use";
        private const string PermVip = "backpack.vip";
        private const string PermAdmin = "backpack.admin";
        private const string BoxPrefab = "assets/prefabs/deployable/large wood storage/box.wooden.large.prefab";
        private const int MaxSlots = 42;

        private PluginConfig config;
        private Dictionary<string, PlayerBackpack> stored = new Dictionary<string, PlayerBackpack>();
        private Dictionary<ulong, StorageContainer> openContainers = new Dictionary<ulong, StorageContainer>();

        public class PluginConfig
        {
            [JsonProperty("Default slots")]
            public int DefaultSlots = 12;

            [JsonProperty("VIP slots")]
            public int VipSlots = 24;

            [JsonProperty("Drop contents on death")]
            public bool DropOnDeath = false;

            [JsonProperty("Drop visual box on death")]
            public bool DropVisualBox = false;
        }

        private class StoredItem
        {
            [JsonProperty("shortname")]
            public string Shortname;

            [JsonProperty("amount")]
            public int Amount;

            [JsonProperty("skin")]
            public ulong Skin;

            [JsonProperty("condition")]
            public float Condition;
        }

        private class PlayerBackpack
        {
            [JsonProperty("items")]
            public List<StoredItem> Items = new List<StoredItem>();

            [JsonProperty("slot override")]
            public int SlotOverride = 0;
        }

        protected override void LoadDefaultConfig() => config = new PluginConfig();

        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>
            {
                ["NoPermission"] = "You do not have permission to use a backpack.",
                ["Opened"] = "Backpack opened ({0} slots).",
                ["Saved"] = "Backpack saved.",
                ["Wiped"] = "Backpack cleared.",
                ["WipedAll"] = "All backpacks cleared.",
                ["SizeSet"] = "Slot override set for {0} to {1}.",
                ["SizeCleared"] = "Slot override cleared for {0}.",
                ["InvalidTarget"] = "Invalid target. Use a Steam ID or 'all'.",
                ["ServerOnly"] = "This command can only be run from the server console or RCON."
            }, 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);
            permission.RegisterPermission(PermVip, this);
            permission.RegisterPermission(PermAdmin, this);
            LoadData();
        }

        private void Unload()
        {
            foreach (var kvp in openContainers)
            {
                var player = BasePlayer.FindByID(kvp.Key);
                if (player != null && player.IsConnected)
                    SavePlayer(player, kvp.Value);
                if (kvp.Value != null && !kvp.Value.IsDestroyed)
                    kvp.Value.Kill();
            }
            openContainers.Clear();
            SaveData();
        }

        private void LoadData()
        {
            stored = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, PlayerBackpack>>(Name)
                ?? new Dictionary<string, PlayerBackpack>();
        }

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

        private PlayerBackpack GetBackpack(string userId)
        {
            if (stored.TryGetValue(userId, out var bp)) return bp;
            bp = new PlayerBackpack();
            stored[userId] = bp;
            return bp;
        }

        private int GetSlotCount(BasePlayer player)
        {
            var bp = GetBackpack(player.UserIDString);
            if (bp.SlotOverride > 0) return Mathf.Min(bp.SlotOverride, MaxSlots);
            if (permission.UserHasPermission(player.UserIDString, PermVip)) return Mathf.Min(config.VipSlots, MaxSlots);
            if (permission.UserHasPermission(player.UserIDString, PermUse)) return Mathf.Min(config.DefaultSlots, MaxSlots);
            return 0;
        }

        [ChatCommand("backpack")]
        private void CmdBackpack(BasePlayer player, string command, string[] args)
        {
            if (player == null || !player.IsConnected) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse)
                && !permission.UserHasPermission(player.UserIDString, PermVip))
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }

            if (openContainers.ContainsKey(player.userID.Get()))
            {
                player.inventory.loot.Clear();
                return;
            }

            var slots = GetSlotCount(player);
            if (slots <= 0)
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }

            var entity = GameManager.server.CreateEntity(BoxPrefab, player.transform.position);
            if (entity == null) return;
            var container = entity as StorageContainer;
            if (container == null)
            {
                entity.Kill();
                return;
            }

            container.OwnerID = player.userID.Get();
            container.inventory.capacity = slots;
            container.Spawn();
            container.enableSaving = false;

            RestoreItems(player, container);
            openContainers[player.userID.Get()] = container;

            player.inventory.loot.Clear();
            player.inventory.loot.AddContainer(container.inventory);
            player.inventory.loot.SendImmediate();
            player.SendConsoleCommand("gametip.hide");
            player.inventory.loot.SendImmediate();
            player.ChatMessage(Msg("Opened", player.UserIDString, slots));
        }

        private void RestoreItems(BasePlayer player, StorageContainer container)
        {
            var bp = GetBackpack(player.UserIDString);
            foreach (var record in bp.Items)
            {
                var def = ItemManager.FindItemDefinition(record.Shortname);
                if (def == null) continue;
                var amount = Mathf.Clamp(record.Amount, 1, def.stackable);
                var item = ItemManager.Create(def, amount, record.Skin);
                if (item == null) continue;
                if (record.Condition > 0f && item.hasCondition)
                {
                    item.condition = record.Condition;
                    item.maxCondition = item.info.condition.max;
                }
                if (!item.MoveToContainer(container.inventory))
                    item.Remove();
            }
        }

        private void SavePlayer(BasePlayer player, StorageContainer container)
        {
            if (player == null) return;
            var bp = GetBackpack(player.UserIDString);
            bp.Items.Clear();

            if (container != null && !container.IsDestroyed && container.inventory != null)
            {
                foreach (var item in container.inventory.itemList)
                {
                    if (item == null || item.info == null) continue;
                    bp.Items.Add(new StoredItem
                    {
                        Shortname = item.info.shortname,
                        Amount = item.amount,
                        Skin = item.skin,
                        Condition = item.hasCondition ? item.condition : 0f
                    });
                }
            }

            SaveData();
        }

        private void OnPlayerLootEnd(PlayerLoot playerLoot)
        {
            if (playerLoot == null) return;
            var player = playerLoot.GetComponent<BasePlayer>();
            if (player == null) return;

            if (!openContainers.TryGetValue(player.userID.Get(), out var container)) return;
            openContainers.Remove(player.userID.Get());

            SavePlayer(player, container);
            if (container != null && !container.IsDestroyed)
                container.Kill();
        }

        private void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            if (player == null) return;
            if (!openContainers.TryGetValue(player.userID.Get(), out var container)) return;
            openContainers.Remove(player.userID.Get());
            SavePlayer(player, container);
            if (container != null && !container.IsDestroyed)
                container.Kill();
        }

        private void OnPlayerDeath(BasePlayer player, HitInfo info)
        {
            if (player == null || player.IsNpc) return;
            var userId = player.userID.Get();

            if (openContainers.TryGetValue(userId, out var open))
            {
                SavePlayer(player, open);
                openContainers.Remove(userId);
                if (open != null && !open.IsDestroyed)
                    open.Kill();
            }

            if (!config.DropOnDeath) return;

            var bp = GetBackpack(player.UserIDString);
            if (bp.Items.Count == 0) return;

            var pos = player.transform.position;
            foreach (var record in bp.Items)
            {
                var def = ItemManager.FindItemDefinition(record.Shortname);
                if (def == null) continue;
                var item = ItemManager.Create(def, record.Amount, record.Skin);
                if (item == null) continue;
                if (record.Condition > 0f && item.hasCondition)
                {
                    item.condition = record.Condition;
                    item.maxCondition = item.info.condition.max;
                }
                item.Drop(pos + UnityEngine.Random.insideUnitSphere * 0.5f, Vector3.up);
            }
            bp.Items.Clear();
            SaveData();

            if (config.DropVisualBox)
            {
                var box = GameManager.server.CreateEntity(BoxPrefab, pos);
                if (box != null)
                {
                    box.Spawn();
                    timer.Once(300f, () => { if (box != null && !box.IsDestroyed) box.Kill(); });
                }
            }
        }

        [ConsoleCommand("backpack.wipe")]
        private void CcmdWipe(ConsoleSystem.Arg arg)
        {
            if (!CanAdmin(arg)) return;
            var target = arg.GetString(0, "").Trim();
            if (string.IsNullOrEmpty(target))
            {
                arg.ReplyWith(Msg("InvalidTarget"));
                return;
            }
            if (target.Equals("all", StringComparison.OrdinalIgnoreCase))
            {
                stored.Clear();
                SaveData();
                arg.ReplyWith(Msg("WipedAll"));
                return;
            }
            var bp = GetBackpack(target);
            bp.Items.Clear();
            SaveData();
            arg.ReplyWith(Msg("Wiped"));
        }

        [ConsoleCommand("backpack.size")]
        private void CcmdSize(ConsoleSystem.Arg arg)
        {
            if (!CanAdmin(arg)) return;
            var target = arg.GetString(0, "").Trim();
            var size = arg.GetInt(1, -1);
            if (string.IsNullOrEmpty(target) || size < 0)
            {
                arg.ReplyWith("Usage: backpack.size <steamid> <slots|0>");
                return;
            }
            var bp = GetBackpack(target);
            bp.SlotOverride = Mathf.Clamp(size, 0, MaxSlots);
            SaveData();
            arg.ReplyWith(bp.SlotOverride == 0 ? Msg("SizeCleared", null, target) : Msg("SizeSet", null, target, bp.SlotOverride));
        }

        private bool CanAdmin(ConsoleSystem.Arg arg)
        {
            if (arg == null) return false;
            var player = arg.Player();
            if (player == null || arg.IsAdmin) return true;
            return permission.UserHasPermission(player.UserIDString, PermAdmin);
        }
    }
}

Hooks used: OnPlayerLootEnd, OnPlayerDisconnected, OnPlayerDeath

Config file

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

Backpack.json
{
  "Default slots": 12,
  "VIP slots": 24,
  "Drop contents on death": false,
  "Drop visual box on death": false
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/backpackOpens your personal backpack storage.backpack.use
backpack.wipe <steamid|all>Clears one player's saved backpack, or all saved backpacks.backpack.admin
backpack.size <steamid> <slots>Sets a slot override for a player (0 removes the override).backpack.admin
Permissions
PermissionGrants
backpack.useGrants access to /backpack with the default slot count.
backpack.vipGrants access to /backpack with the VIP slot count.
backpack.adminGrants the backpack.wipe and backpack.size console commands.

How the code works

  1. Config: slot sizes and death behavior

    The nested PluginConfig class defines Default slots, VIP slots, Drop contents on death and Drop visual box on death. When /backpack runs, GetSlotCount reads any per-player override first, then checks permission.UserHasPermission for backpack.vip and backpack.use in that order, and clamps the result to 42. If Drop contents on death is true, OnPlayerDeath recreates every saved item and drops it at the death position. Update the values in oxide/config/Backpack.json and reload the plugin.

  2. Data model per player

    Each player's backpack is a List<StoredItem> (shortname, amount, skin, condition) inside a PlayerBackpack, held in a Dictionary<string, PlayerBackpack> keyed by UserIDString. The dictionary is read with Interface.Oxide.DataFileSystem.ReadObject on load and written with WriteObject to oxide/data/Backpack. Saving shortnames instead of item IDs means the file stays readable, and RestoreItems skips records whose shortname no longer resolves through ItemManager.FindItemDefinition, so a removed item cannot break the next open.

  3. Open flow with a temporary StorageContainer

    /backpack creates an entity from BoxPrefab with GameManager.server.CreateEntity, casts it to StorageContainer, sets inventory.capacity to the player's slot count, calls Spawn() and sets enableSaving = false so it is never written to the world save. RestoreItems rebuilds each record with ItemManager.Create and MoveToContainer, clamping the amount to def.stackable. The player then enters the loot panel through player.inventory.loot.AddContainer and SendImmediate, and the container is recorded in openContainers keyed by userID.

  4. Saving on OnPlayerLootEnd

    When the player closes the loot panel, OnPlayerLootEnd receives PlayerLoot, gets the BasePlayer with GetComponent<BasePlayer>() and looks up the container in openContainers. SavePlayer reads inventory.itemList, stores shortname, amount, skin and condition into the PlayerBackpack, calls SaveData() and then Kill()s the container. OnPlayerDisconnected runs the same routine so items are not lost when a player drops while looting.

  5. Death handling

    OnPlayerDeath saves and kills any open backpack first, then, if Drop contents on death is true, walks the stored list and calls item.Drop with a small random offset for each entry before clearing the list. If Drop visual box on death is also true, a second empty BoxPrefab entity is spawned as a visual marker and killed after 300 seconds by timer.Once. Both flags are checked only at death, so changing them does not affect items already saved.

  6. Permissions and admin commands

    backpack.use, backpack.vip and backpack.admin are registered in Init. backpack.wipe accepts all or a Steam ID and clears the stored list (or the whole dictionary). backpack.size writes SlotOverride into the same data file so admins can adjust a player without touching config. Both console commands call CanAdmin, which allows the server console, RCON and players with backpack.admin or who are flagged IsAdmin on the command, then replies through arg.ReplyWith with translated messages.

Install it on your server

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

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

Variations

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

  • Wipe backpacks on server wipe

    Clear all stored backpacks on the next Rust wipe so players start fresh.

    Generate this version
  • Drop a visual backpack entity

    Spawn a small lootable container on death instead of scattering items.

    Generate this version
  • Team-shared backpack

    Give teams a shared container keyed by team ID.

    Generate this version

FAQ

Where are backpacks stored?

In oxide/data/Backpack.json. Each entry is keyed by Steam ID and holds a list of shortname/amount/skin/condition records. The file is rewritten every time a backpack is saved via Interface.Oxide.DataFileSystem.WriteObject, so back up that file before large removals.

Can players exploit the loot panel to keep items?

The container is temporary, marked enableSaving = false and killed after OnPlayerLootEnd saves. If a player disconnects while it is open, OnPlayerDisconnected saves and kills the container.

How do I give VIPs a bigger backpack?

Set VIP slots above Default slots in the config and assign backpack.vip to a group with oxide.grant group vip backpack.vip. GetSlotCount checks VIP permission first, then the default one. Both are clamped to 42 slots by MaxSlots.

How do I install it on Oxide or Carbon?

Drop Backpack.cs into oxide/plugins (Carbon reads the same folder). Reload with oxide.reload Backpack and edit oxide/config/Backpack.json to set slot counts and death behavior. No other plugin is required.

Do items keep condition and skins?

Yes. Each StoredItem stores skin (ulong) and condition (float), applied to the recreated item. RestoreItems also sets maxCondition from item.info.condition.max so damaged tools come back at their saved durability instead of full condition.

Customize it for your server

Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.

Prompt: Generate an Oxide plugin called Backpack that opens a per-player StorageContainer via /backpack, sized by permissions, saved in oxide/data with shortname/amount/skin/condition, dropping contents on death when configured.
Customize for my server

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

Last updated Oct 26, 2026.

Customize backpacks

Customize