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
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.
{
"Default slots": 12,
"VIP slots": 24,
"Drop contents on death": false,
"Drop visual box on death": false
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/backpack | Opens 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 |
| Permission | Grants |
|---|---|
backpack.use | Grants access to /backpack with the default slot count. |
backpack.vip | Grants access to /backpack with the VIP slot count. |
backpack.admin | Grants the backpack.wipe and backpack.size console commands. |
How the code works
Config: slot sizes and death behavior
The nested
PluginConfigclass definesDefault slots,VIP slots,Drop contents on deathandDrop visual box on death. When/backpackruns,GetSlotCountreads any per-player override first, then checkspermission.UserHasPermissionforbackpack.vipandbackpack.usein that order, and clamps the result to 42. IfDrop contents on deathis true,OnPlayerDeathrecreates every saved item and drops it at the death position. Update the values inoxide/config/Backpack.jsonand reload the plugin.Data model per player
Each player's backpack is a
List<StoredItem>(shortname, amount, skin, condition) inside aPlayerBackpack, held in aDictionary<string, PlayerBackpack>keyed by UserIDString. The dictionary is read withInterface.Oxide.DataFileSystem.ReadObjecton load and written withWriteObjecttooxide/data/Backpack. Saving shortnames instead of item IDs means the file stays readable, andRestoreItemsskips records whose shortname no longer resolves throughItemManager.FindItemDefinition, so a removed item cannot break the next open.Open flow with a temporary StorageContainer
/backpackcreates an entity fromBoxPrefabwithGameManager.server.CreateEntity, casts it toStorageContainer, setsinventory.capacityto the player's slot count, callsSpawn()and setsenableSaving = falseso it is never written to the world save.RestoreItemsrebuilds each record withItemManager.CreateandMoveToContainer, clamping the amount todef.stackable. The player then enters the loot panel throughplayer.inventory.loot.AddContainerandSendImmediate, and the container is recorded inopenContainerskeyed by userID.Saving on OnPlayerLootEnd
When the player closes the loot panel,
OnPlayerLootEndreceivesPlayerLoot, gets theBasePlayerwithGetComponent<BasePlayer>()and looks up the container inopenContainers.SavePlayerreadsinventory.itemList, stores shortname, amount, skin and condition into thePlayerBackpack, callsSaveData()and thenKill()s the container.OnPlayerDisconnectedruns the same routine so items are not lost when a player drops while looting.Death handling
OnPlayerDeathsaves and kills any open backpack first, then, ifDrop contents on deathis true, walks the stored list and callsitem.Dropwith a small random offset for each entry before clearing the list. IfDrop visual box on deathis also true, a second emptyBoxPrefabentity is spawned as a visual marker and killed after 300 seconds bytimer.Once. Both flags are checked only at death, so changing them does not affect items already saved.Permissions and admin commands
backpack.use,backpack.vipandbackpack.adminare registered inInit.backpack.wipeacceptsallor a Steam ID and clears the stored list (or the whole dictionary).backpack.sizewritesSlotOverrideinto the same data file so admins can adjust a player without touching config. Both console commands callCanAdmin, which allows the server console, RCON and players withbackpack.adminor who are flaggedIsAdminon the command, then replies througharg.ReplyWithwith translated messages.
Install it on your server
Save Backpack.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/Backpack.json(Carbon:carbon/configs/Backpack.json). After editing, reload it:oxide.reload Backpack c.reload Backpack # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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 versionDrop a visual backpack entity
Spawn a small lootable container on death instead of scattering items.
Generate this versionTeam-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.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More quality of life plugins
- Crafting speedA plugin that speeds up crafting by scaling the duration of ItemCraftTask instances when a craft starts, using a global multiplier plus optionalโฆOnItemCraft
- Day and night controlSkips or shortens night by advancing the in-game clock to a configurable morning time, and adds /day and /night admin commands.OnServerInitialized
- Default item skinsLets each player store a default skin ID per item shortname with /skin and applies it to every item the player crafts through theโฆOnServerInitializedOnItemCraftFinished
Last updated Oct 26, 2026.
Customize backpacks
Customize