How to make a Rust custom loot table plugin (crates by prefab)
Replaces the contents of configured LootContainer crates, matched by short prefab name, with weighted items that have per-item chances and min/max amounts. Uses OnLootSpawn(LootContainer).
Vanilla crate contents are fixed per prefab, so admins who want a custom economy have to choose between spawning extra crates or editing loot tables that change with every game update. This plugin intercepts the loot spawn step instead: when a LootContainer fills its inventory, the plugin checks its short prefab name against a config list and swaps the generated contents for your own table.
Each entry in the table has a shortname, a chance from 0 to 100, and a min/max amount. On every spawn the plugin rolls each entry once; successful rolls create a stack with a random amount in range and try to place it in the container. If no entry rolled, a configurable fallback item is added so the crate never looks empty. A chat command previews the table and a console command rerolls a crate you are looking at, which makes tuning fast without restarting the server.
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("Custom Loot Tables", "RustPluginGenerator", "1.0.0")]
[Description("Replaces the contents of configured crates with custom loot tables")]
public class CustomLootTables : RustPlugin
{
private const string PermAdmin = "customloottables.admin";
private PluginConfig config;
private class LootEntry
{
[JsonProperty("Item shortname")]
public string Shortname = "scrap";
[JsonProperty("Chance (0-100)")]
public float Chance = 100f;
[JsonProperty("Min Amount")]
public int MinAmount = 1;
[JsonProperty("Max Amount")]
public int MaxAmount = 1;
}
private class LootTable
{
[JsonProperty("Max Stacks")]
public int MaxStacks = 6;
[JsonProperty("Items")]
public List<LootEntry> Items = new List<LootEntry>();
}
private class PluginConfig
{
[JsonProperty("Fill Empty Slots")]
public bool FillEmptySlots = true;
[JsonProperty("Fallback Item")]
public string FallbackItem = "scrap";
[JsonProperty("Fallback Amount")]
public int FallbackAmount = 10;
[JsonProperty("Loot Tables")]
public Dictionary<string, LootTable> Tables = new Dictionary<string, LootTable>
{
["crate_elite"] = new LootTable
{
MaxStacks = 5,
Items = new List<LootEntry>
{
new LootEntry { Shortname = "rifle.ak", Chance = 20f, MinAmount = 1, MaxAmount = 1 },
new LootEntry { Shortname = "ammo.rifle", Chance = 80f, MinAmount = 20, MaxAmount = 60 },
new LootEntry { Shortname = "scrap", Chance = 100f, MinAmount = 40, MaxAmount = 120 },
new LootEntry { Shortname = "components.riflebody", Chance = 40f, MinAmount = 1, MaxAmount = 2 }
}
},
["crate_normal"] = new LootTable
{
MaxStacks = 4,
Items = new List<LootEntry>
{
new LootEntry { Shortname = "scrap", Chance = 100f, MinAmount = 5, MaxAmount = 25 },
new LootEntry { Shortname = "metal.fragments", Chance = 60f, MinAmount = 50, MaxAmount = 200 }
}
}
};
}
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 this command.",
["Table"] = "<color=#ce422b>{0}</color> ({1} entries, max {2} stacks): {3}",
["NoTables"] = "No loot tables are configured.",
["Preview"] = "Custom loot tables: {0} crate types configured.",
["CodeOnly"] = "This command must be run by a player."
}, 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(PermAdmin, this);
}
private object OnLootSpawn(LootContainer lootContainer)
{
if (lootContainer == null || lootContainer.ShortPrefabName == null || lootContainer.inventory == null) return null;
LootTable table;
if (!config.Tables.TryGetValue(lootContainer.ShortPrefabName.ToLower(), out table) || table == null) return null;
FillContainer(lootContainer, table);
return null;
}
private void FillContainer(LootContainer lootContainer, LootTable table)
{
ItemContainer container = lootContainer.inventory;
for (int i = container.itemList.Count - 1; i >= 0; i--)
{
Item existing = container.itemList[i];
if (existing != null) existing.Remove();
}
int stacks = 0;
int maxStacks = Mathf.Max(1, table.MaxStacks);
if (table.Items != null)
{
foreach (LootEntry entry in table.Items)
{
if (stacks >= maxStacks) break;
if (entry == null || string.IsNullOrEmpty(entry.Shortname)) continue;
if (UnityEngine.Random.Range(0f, 100f) > entry.Chance) continue;
int min = Mathf.Max(1, entry.MinAmount);
int max = Mathf.Max(min, entry.MaxAmount);
int amount = UnityEngine.Random.Range(min, max + 1);
Item item = ItemManager.CreateByName(entry.Shortname, amount);
if (item == null) continue;
if (!item.MoveToContainer(container)) { item.Remove(); continue; }
stacks++;
}
}
if (config.FillEmptySlots && container.itemList.Count == 0)
{
Item fallback = ItemManager.CreateByName(config.FallbackItem, Mathf.Max(1, config.FallbackAmount));
if (fallback != null && !fallback.MoveToContainer(container)) fallback.Remove();
}
container.MarkDirty();
}
[ChatCommand("loottable")]
private void CmdLootTable(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
{
player.ChatMessage(Msg("NoPermission", player.UserIDString));
return;
}
if (config.Tables == null || config.Tables.Count == 0)
{
player.ChatMessage(Msg("NoTables", player.UserIDString));
return;
}
player.ChatMessage(Msg("Preview", player.UserIDString, config.Tables.Count));
foreach (KeyValuePair<string, LootTable> pair in config.Tables)
{
if (pair.Value == null) continue;
string names = "";
if (pair.Value.Items != null)
{
for (int i = 0; i < pair.Value.Items.Count && i < 5; i++)
{
LootEntry entry = pair.Value.Items[i];
if (entry == null) continue;
names += (names.Length > 0 ? ", " : "") + entry.Shortname + " " + entry.Chance + "%";
}
}
player.ChatMessage(Msg("Table", player.UserIDString, pair.Key, pair.Value.Items != null ? pair.Value.Items.Count : 0, pair.Value.MaxStacks, names));
}
}
[ConsoleCommand("customloottables.reroll")]
private void CcmdReroll(ConsoleSystem.Arg arg)
{
BasePlayer player = arg.Player();
if (player == null)
{
arg.ReplyWith(Msg("CodeOnly"));
return;
}
if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
{
arg.ReplyWith(Msg("NoPermission", player.UserIDString));
return;
}
RaycastHit hit;
if (!Physics.Raycast(player.eyes.HeadRay(), out hit, 8f)) return;
LootContainer lootContainer = hit.GetEntity() as LootContainer;
if (lootContainer == null) return;
LootTable table;
if (!config.Tables.TryGetValue(lootContainer.ShortPrefabName == null ? "" : lootContainer.ShortPrefabName.ToLower(), out table) || table == null)
{
arg.ReplyWith("This crate has no custom loot table.");
return;
}
FillContainer(lootContainer, table);
arg.ReplyWith("Table applied to " + lootContainer.ShortPrefabName + ".");
}
}
}Hooks used: OnLootSpawn
Config file
Created automatically on first load at oxide/config/CustomLootTables.json.
{
"Fill Empty Slots": true,
"Fallback Item": "scrap",
"Fallback Amount": 10,
"Loot Tables": {
"crate_elite": {
"Max Stacks": 5,
"Items": [
{
"Item shortname": "rifle.ak",
"Chance (0-100)": 20,
"Min Amount": 1,
"Max Amount": 1
},
{
"Item shortname": "ammo.rifle",
"Chance (0-100)": 80,
"Min Amount": 20,
"Max Amount": 60
},
{
"Item shortname": "scrap",
"Chance (0-100)": 100,
"Min Amount": 40,
"Max Amount": 120
},
{
"Item shortname": "components.riflebody",
"Chance (0-100)": 40,
"Min Amount": 1,
"Max Amount": 2
}
]
},
"crate_normal": {
"Max Stacks": 4,
"Items": [
{
"Item shortname": "scrap",
"Chance (0-100)": 100,
"Min Amount": 5,
"Max Amount": 25
},
{
"Item shortname": "metal.fragments",
"Chance (0-100)": 60,
"Min Amount": 50,
"Max Amount": 200
}
]
}
}
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/loottable | Prints the configured crate prefabs and their item entries to chat. | customloottables.admin |
customloottables.reroll | Server console: empties and refills the LootContainer the executing player is looking at using the configured table. | customloottables.admin |
| Permission | Grants |
|---|---|
customloottables.admin | Grants /loottable and customloottables.reroll. |
How the code works
Match the crate by prefab name
The hook
object OnLootSpawn(LootContainer lootContainer)fires when the game is about to fill a lootable container. The plugin readslootContainer.ShortPrefabName, lowercases it and looks it up in theLoot Tablesdictionary. If the name is not a key in the config, the method returnsnull, which lets the game run its normal loot generation. The default config ships two keys,crate_eliteandcrate_normal; you add other prefab names by editing the dictionary.Block the vanilla fill and write your own
For a matched crate the method clears whatever is already in
lootContainer.inventoryby walkingitemListbackwards and callingRemove()on each item. It then rolls the entries inItems:UnityEngine.Random.Range(0f, 100f)againstChance (0-100), andUnityEngine.Random.Range(min, max + 1)for the stack size, where max is clamped to at least the min. Each successful stack is created withItemManager.CreateByNameand moved withMoveToContainer. The hook always returnsnull.Never let a crate spawn empty
Because every entry is rolled independently, a table with low chances can produce zero items. The config key
Fill Empty Slots(default true) handles that: after the rolls, ifcontainer.itemList.Countis zero, the plugin adds theFallback Itemwith theFallback Amount. The per-tableMax Stacksvalue caps how many successful rolls are placed; once that count is reached the loop stops, so a generous table cannot fill every slot with a separate stack.Shape the config around prefab names
The config class holds
Dictionary<string, LootTable> Tablesunder the JSON keyLoot Tables. EachLootTablehasMax Stacksand aList<LootEntry> Items.LootEntrycarriesItem shortname,Chance (0-100),Min AmountandMax Amount.LoadConfigfalls back to defaults and warns if the file cannot be read or deserializes to null. IfItemManager.CreateByNamereturns null for an unknown shortname the entry is skipped, so a typo removes that item instead of crashing.Preview and reroll while tuning
/loottableprints how many crate types are configured, then one line per table showing its key, entry count, max stacks and the first five item shortnames with their chances, all routed through the lang file. The console commandcustomloottables.rerollusesarg.Player()to find the caller, casts a ray fromeyes.HeadRay()up to 8 metres, and if the hit entity is aLootContainerwith a configured table, calls the same fill method the hook uses.Permissions and message handling
The single permission
customloottables.adminis registered inInit()and checked in both commands withpermission.UserHasPermission. Every reply comes fromLoadDefaultMessagesthrough theMsghelper, so you can editoxide/lang/en/CustomLootTables.jsonwithout recompiling. The plugin keeps no timers and stores no data, so there is noUnload()method; nothing has to be torn down when you reload it.
Install it on your server
Save CustomLootTables.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/CustomLootTables.json(Carbon:carbon/configs/CustomLootTables.json). After editing, reload it:oxide.reload CustomLootTables c.reload CustomLootTables # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default customloottables.admin c.grant group default customloottables.admin # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Per-crate chance multiplier for events
Boost all chances during a timed event without editing each crate.
Generate this versionGuaranteed rare drop per crate
Make sure every crate of a chosen prefab contains one rare item.
Generate this versionLoot table per monument
Different tables for crates in different monuments.
Generate this version
FAQ
Does this work for crates that are already on the map?
Yes if the crate has not generated its loot yet. OnLootSpawn runs when the container first fills, which for map crates is usually shortly after the entity spawns. Crates that were already looted and refilled earlier in the wipe keep their old contents until they respawn.
Which short prefab names can I use?
The names LootContainer uses in the world, for example crate_normal, crate_elite, bradley_crate, heli_crate and supply_drop. Any prefab name that is not a key in Loot Tables keeps vanilla loot, so adding a name has no effect unless that prefab spawns a LootContainer.
Do probabilities add up to 100?
No. Each entry is rolled independently, so two items at 50 percent can both drop or neither can. If you want guaranteed contents, set a few entries to 100 and use the table's Max Stacks to limit how many of the successful rolls are actually placed in the crate.
How do I install it on Oxide or Carbon?
Drop CustomLootTables.cs into oxide/plugins, let the server compile it once, then edit oxide/config/CustomLootTables.json and run oxide.reload CustomLootTables. Carbon loads the same file from its plugin folder. No other plugins are needed.
What happens if I mistype an item shortname?
ItemManager.CreateByName returns null for unknown shortnames and the plugin skips that entry, so the crate simply contains one fewer item. No error is logged for a single bad shortname, which is why testing with customloottables.reroll on a real crate is worth doing after a config change.
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 loot plugins
- Barrel scrap bonusAwards bonus scrap when a player destroys a loot barrel, with a configurable chance and a VIP multiplier permission.OnEntityDeath
- First opener crate lockThis plugin gives the first player who opens a loot container a temporary claim on it, enforced through the CanLootEntity hook, so anyone outsideโฆCanLootEntityOnEntityKill
- Dropped item despawn timeThis plugin changes how long dropped item containers stay on the ground before Rust despawns them, using a global multiplier plus per-shortnameโฆOnItemDropped
Last updated Oct 26, 2026.
Customize custom loot tables
Customize