Skip to content
RustPluginGenerator

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

CustomLootTables.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("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.

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

Chat and console commands
CommandWhat it doesPermission
/loottablePrints the configured crate prefabs and their item entries to chat.customloottables.admin
customloottables.rerollServer console: empties and refills the LootContainer the executing player is looking at using the configured table.customloottables.admin
Permissions
PermissionGrants
customloottables.adminGrants /loottable and customloottables.reroll.

How the code works

  1. 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 reads lootContainer.ShortPrefabName, lowercases it and looks it up in the Loot Tables dictionary. If the name is not a key in the config, the method returns null, which lets the game run its normal loot generation. The default config ships two keys, crate_elite and crate_normal; you add other prefab names by editing the dictionary.

  2. Block the vanilla fill and write your own

    For a matched crate the method clears whatever is already in lootContainer.inventory by walking itemList backwards and calling Remove() on each item. It then rolls the entries in Items: UnityEngine.Random.Range(0f, 100f) against Chance (0-100), and UnityEngine.Random.Range(min, max + 1) for the stack size, where max is clamped to at least the min. Each successful stack is created with ItemManager.CreateByName and moved with MoveToContainer. The hook always returns null.

  3. 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, if container.itemList.Count is zero, the plugin adds the Fallback Item with the Fallback Amount. The per-table Max Stacks value 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.

  4. Shape the config around prefab names

    The config class holds Dictionary<string, LootTable> Tables under the JSON key Loot Tables. Each LootTable has Max Stacks and a List<LootEntry> Items. LootEntry carries Item shortname, Chance (0-100), Min Amount and Max Amount. LoadConfig falls back to defaults and warns if the file cannot be read or deserializes to null. If ItemManager.CreateByName returns null for an unknown shortname the entry is skipped, so a typo removes that item instead of crashing.

  5. Preview and reroll while tuning

    /loottable prints 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 command customloottables.reroll uses arg.Player() to find the caller, casts a ray from eyes.HeadRay() up to 8 metres, and if the hit entity is a LootContainer with a configured table, calls the same fill method the hook uses.

  6. Permissions and message handling

    The single permission customloottables.admin is registered in Init() and checked in both commands with permission.UserHasPermission. Every reply comes from LoadDefaultMessages through the Msg helper, so you can edit oxide/lang/en/CustomLootTables.json without recompiling. The plugin keeps no timers and stores no data, so there is no Unload() method; nothing has to be torn down when you reload it.

Install it on your server

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

    Give it to every player, or swap default for a group such as vip: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 version
  • Guaranteed rare drop per crate

    Make sure every crate of a chosen prefab contains one rare item.

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

Prompt: Create a Rust Oxide plugin that replaces loot in configured crates by short prefab name using OnLootSpawn, with item shortnames, chances, min and max amounts, a max stack count and a fallback item, plus /loottable and customloottables.reroll admin commands.
Customize for my server

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

Last updated Oct 26, 2026.

Customize custom loot tables

Customize