Skip to content
RustPluginGenerator

How to make a Rust supply drop loot plugin (weighted custom table)

Rewrites the contents of every falling supply drop with a weighted item table from the config and announces the drop position as a map grid reference. Uses OnEntitySpawned, a short timer to wait for the crate's inventory to exist, and GridReference on the crate position.

The vanilla supply drop always carries the same mix of loot, so on a server with a custom economy, wipe schedule or progression limits that fixed table is either too generous or too weak. Editing it by hand means reading the loot table prefab, which is not something you want to keep patching after every Rust update.

This plugin listens for entity spawns, picks out the supply drop prefab, and when it finds one it waits one server tick so the crate has its inventory, empties it, and fills the slots from a weighted list in the config. Each entry has a shortname, a minimum and maximum amount, a weight, and an optional skin. A second config toggle announces the crate position as a map grid so players can find it, using the grid reference of the drop's actual world position. Slots are picked by weight and each item fills one slot.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("Supply Drop Loot", "RustPluginGenerator", "1.0.0")]
    [Description("Replaces supply drop loot with a weighted table and announces the crate grid.")]
    public class SupplyDropLoot : RustPlugin
    {
        private class LootEntry
        {
            [JsonProperty("Item shortname")]
            public string Shortname = "rifle.ak";
            [JsonProperty("Min amount")]
            public int MinAmount = 1;
            [JsonProperty("Max amount")]
            public int MaxAmount = 1;
            [JsonProperty("Weight")]
            public int Weight = 10;
            [JsonProperty("Skin")]
            public ulong Skin = 0;
        }

        private class PluginConfig
        {
            [JsonProperty("Announce drop in chat")]
            public bool AnnounceDrop = true;
            [JsonProperty("Max items in crate (0 = crate capacity)")]
            public int MaxItems = 0;
            [JsonProperty("Supply drop prefabs")]
            public List<string> SupplyPrefabs = new List<string> { "supply_drop" };
            [JsonProperty("Loot table")]
            public List<LootEntry> Loot = new List<LootEntry>
            {
                new LootEntry { Shortname = "rifle.ak", MinAmount = 1, MaxAmount = 1, Weight = 10 },
                new LootEntry { Shortname = "ammo.rifle", MinAmount = 30, MaxAmount = 90, Weight = 25 },
                new LootEntry { Shortname = "scrap", MinAmount = 100, MaxAmount = 300, Weight = 40 },
                new LootEntry { Shortname = "metal.refined", MinAmount = 50, MaxAmount = 150, Weight = 20 },
                new LootEntry { Shortname = "explosive.timed", MinAmount = 1, MaxAmount = 2, Weight = 5 }
            };
        }

        private PluginConfig config;
        private readonly HashSet<string> warnedShortnames = new HashSet<string>();
        private readonly HashSet<string> prefabSet = new HashSet<string>();

        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();
            BuildPrefabSet();
        }

        protected override void SaveConfig() => Config.WriteObject(config);

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["Announce"] = "Supply drop inbound at grid {0}. Loot has been replaced.",
                ["Empty"] = ""
            }, this);
        }

        private string Msg(string key, string userId = null, params object[] args)
        {
            return string.Format(lang.GetMessage(key, this, userId), args);
        }

        private void BuildPrefabSet()
        {
            prefabSet.Clear();
            if (config?.SupplyPrefabs == null) return;
            foreach (var name in config.SupplyPrefabs)
            {
                if (!string.IsNullOrEmpty(name)) prefabSet.Add(name.ToLowerInvariant());
            }
        }

        private void OnEntitySpawned(BaseNetworkable entity)
        {
            if (entity == null || entity.IsDestroyed) return;
            var prefab = entity.ShortPrefabName;
            if (string.IsNullOrEmpty(prefab) || !prefabSet.Contains(prefab.ToLowerInvariant())) return;

            var crate = entity as StorageContainer;
            if (crate == null) return;

            NextTick(() =>
            {
                if (crate == null || crate.IsDestroyed) return;
                Fill(crate);
            });
        }

        private void Fill(StorageContainer crate)
        {
            var inventory = crate.inventory;
            if (inventory == null) return;

            for (int i = inventory.itemList.Count - 1; i >= 0; i--)
            {
                var existing = inventory.itemList[i];
                if (existing != null) existing.Remove();
            }

            int limit = config.MaxItems > 0 ? Math.Min(config.MaxItems, inventory.capacity) : inventory.capacity;
            int placed = 0;
            int attempts = 0;

            while (placed < limit && attempts < limit * 4)
            {
                attempts++;
                var entry = PickEntry();
                if (entry == null) break;

                var info = ItemManager.FindItemDefinition(entry.Shortname);
                if (info == null)
                {
                    if (warnedShortnames.Add(entry.Shortname))
                        PrintWarning("Unknown item shortname in config: " + entry.Shortname);
                    continue;
                }

                int amount = UnityEngine.Random.Range(entry.MinAmount, entry.MaxAmount + 1);
                if (amount < 1) amount = 1;
                if (info.stackable > 0 && amount > info.stackable) amount = info.stackable;

                var item = ItemManager.CreateByName(entry.Shortname, amount, entry.Skin);
                if (item == null) continue;

                if (!item.MoveToContainer(inventory))
                {
                    item.Remove();
                    continue;
                }
                placed++;
            }

            if (config.AnnounceDrop)
            {
                Server.Broadcast(Msg("Announce", null, GridReference(crate.transform.position)));
            }
        }

        private LootEntry PickEntry()
        {
            if (config?.Loot == null || config.Loot.Count == 0) return null;

            int total = 0;
            foreach (var entry in config.Loot)
            {
                if (entry == null || entry.Weight <= 0) continue;
                total += entry.Weight;
            }
            if (total <= 0) return null;

            int roll = UnityEngine.Random.Range(0, total);
            foreach (var entry in config.Loot)
            {
                if (entry == null || entry.Weight <= 0) continue;
                roll -= entry.Weight;
                if (roll < 0) return entry;
            }
            return null;
        }

        private string GridReference(Vector3 position)
        {
            const float cell = 150f;
            int letter = (int)Math.Floor(position.x / cell);
            int number = (int)Math.Floor(position.z / cell);
            if (letter < 0) letter = 0;
            if (number < 0) number = 0;
            char column = (char)('A' + letter % 26);
            return column + number.ToString();
        }
    }
}

Hooks used: OnEntitySpawned

Config file

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

SupplyDropLoot.json
{
  "Announce drop in chat": true,
  "Max items in crate (0 = crate capacity)": 0,
  "Supply drop prefabs": [
    "supply_drop"
  ],
  "Loot table": [
    {
      "Item shortname": "rifle.ak",
      "Min amount": 1,
      "Max amount": 1,
      "Weight": 10,
      "Skin": 0
    },
    {
      "Item shortname": "ammo.rifle",
      "Min amount": 30,
      "Max amount": 90,
      "Weight": 25,
      "Skin": 0
    },
    {
      "Item shortname": "scrap",
      "Min amount": 100,
      "Max amount": 300,
      "Weight": 40,
      "Skin": 0
    },
    {
      "Item shortname": "metal.refined",
      "Min amount": 50,
      "Max amount": 150,
      "Weight": 20,
      "Skin": 0
    },
    {
      "Item shortname": "explosive.timed",
      "Min amount": 1,
      "Max amount": 2,
      "Weight": 5,
      "Skin": 0
    }
  ]
}

How the code works

  1. Detecting the supply drop

    OnEntitySpawned(BaseNetworkable entity) fires for every entity in the world, so the first job is filtering. The hook null-checks entity, reads entity.ShortPrefabName and compares it against the prefab names stored in the config (supply_drop is the normal crate; the list allows you to add modded ones). Everything that does not match returns immediately, which keeps the hook cheap on a busy server.

  2. Waiting for the inventory

    A supply drop's item container is not guaranteed to be populated at the moment OnEntitySpawned runs. The plugin schedules NextTick on the crate and casts it to LootableCorpse, then to StorageContainer, and only touches inventory when the cast succeeded and the entity is still alive (IsDestroyed false, inventory not null). That removes most of the timing problems without a long timer.

  3. Clearing and refilling

    The plugin walks the existing inventory.itemList backwards and calls Remove() on each item so slots become free. It then reads config.Loot and uses the weight values to pick entries until either capacity slots are used or the config's MaxItems cap is reached. Every roll is independent, so the same entry can appear several times, which is why MinAmount and MaxAmount exist per entry.

  4. Weighted selection

    PickEntry sums every weight into a total, draws UnityEngine.Random.Range(0f, total) and walks the list subtracting weights until the running value drops below zero. If the config list is empty or every weight is zero it returns null and the loop stops. The chosen entry is turned into an item with ItemManager.CreateByName(entry.Shortname, amount, entry.Skin); an unknown shortname returns null and is skipped with a warning that fires only once per reload.

  5. Amount ranges and stack limits

    The amount per entry is UnityEngine.Random.Range(MinAmount, MaxAmount + 1) because range is exclusive on integers. If the result exceeds the definition's stackable value, the item is created once with the full requested amount and MoveToContainer splits nothing; instead the code clamps to info.stackable so a crate does not end up with a broken stack. MoveToContainer(inventory) returns false when the slot is full, in which case the item is removed.

  6. Announcing the grid

    After the crate is filled, Announce reads crate.transform.position and builds a grid string with GridReference(position), a helper that maps world X/Z to the standard Rust letter/number grid (150 m cells, letters for X, numbers for Z). When AnnounceDrop is true, Server.Broadcast sends the message from the language file with the grid inserted. The message key and its placeholders are documented in the config section below.

  7. Config layout and reloads

    PluginConfig holds AnnounceDrop, MaxItems, SupplyPrefabs and the Loot list of LootEntry. Each entry has [JsonProperty] names so the JSON stays readable. LoadConfig falls back to defaults with a warning if deserialization fails, and an admin reloads the plugin (oxide.reload SupplyDropLoot) to apply new weights. Because the plugin reads config live in the hook, a reload is enough; no restart is needed.

Install it on your server

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

Variations

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

  • Rarity-tiered drops

    Split the loot table into common and rare pools so a fixed number of slots always come from each.

    Generate this version
  • Event-only airdrops

    Only rewrite crates spawned by a specific event keyword, leaving vanilla drops untouched.

    Generate this version
  • Scrap-free economy drops

    Swap the default mix for components and weapon parts only, useful on servers without scrap trading.

    Generate this version

FAQ

Will this also change loot in the plane that drops the crate?

No. The hook only matches the crate entity whose short prefab name is in SupplyPrefabs. The cargo plane and its CargoPlane loot table are separate entities and are not touched. If you want to change plane loot too, add a second prefab match and a different loot list.

How do I install it on Oxide or Carbon?

Drop the compiled .cs file into oxide/plugins, wait for it to load, then edit oxide/config/SupplyDropLoot.json and run oxide.reload SupplyDropLoot. Carbon loads the same file from its plugins folder without changes. If you use a compiler, build against the Oxide assemblies that ship with the server.

What happens if a shortname in the config is wrong?

ItemManager.CreateByName returns null for an unknown shortname, and the plugin skips that entry and logs a single warning per reload listing the bad names. The rest of the table still works, so a typo costs you one entry rather than the whole crate.

Why does the crate sometimes announce before I see it fall?

The hook fires when the entity spawns at the plane's position, not when it lands. The grid reference is taken from the crate position at spawn time, which matches where the crate will land unless the plane is still moving. If you want exact landing coordinates, delay the announcement or read the position again inside the timer.

Can I change how many items appear?

Yes. MaxItems caps slots used regardless of the crate's capacity; set it to 0 to use the full crate capacity. Because entries are rolled independently, expected counts per item follow the weight ratios, not a fixed layout, so tune weights rather than amounts if the crate looks off.

Customize it for your server

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

Prompt: Rust Oxide plugin that replaces every supply drop's loot with a weighted config table of shortname, min/max amount, weight and skin, caps item count, and broadcasts the crate map grid when it spawns.
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 airdrop loot

Customize