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
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.
{
"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
Detecting the supply drop
OnEntitySpawned(BaseNetworkable entity)fires for every entity in the world, so the first job is filtering. The hook null-checksentity, readsentity.ShortPrefabNameand compares it against the prefab names stored in the config (supply_dropis 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.Waiting for the inventory
A supply drop's item container is not guaranteed to be populated at the moment
OnEntitySpawnedruns. The plugin schedulesNextTickon the crate and casts it toLootableCorpse, then toStorageContainer, and only touchesinventorywhen the cast succeeded and the entity is still alive (IsDestroyedfalse,inventorynot null). That removes most of the timing problems without a long timer.Clearing and refilling
The plugin walks the existing
inventory.itemListbackwards and callsRemove()on each item so slots become free. It then readsconfig.Lootand uses the weight values to pick entries until eithercapacityslots are used or the config'sMaxItemscap is reached. Every roll is independent, so the same entry can appear several times, which is whyMinAmountandMaxAmountexist per entry.Weighted selection
PickEntrysums every weight into a total, drawsUnityEngine.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 withItemManager.CreateByName(entry.Shortname, amount, entry.Skin); an unknown shortname returns null and is skipped with a warning that fires only once per reload.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'sstackablevalue, the item is created once with the full requested amount andMoveToContainersplits nothing; instead the code clamps toinfo.stackableso 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.Announcing the grid
After the crate is filled,
Announcereadscrate.transform.positionand builds a grid string withGridReference(position), a helper that maps world X/Z to the standard Rust letter/number grid (150 m cells, letters for X, numbers for Z). WhenAnnounceDropis true,Server.Broadcastsends the message from the language file with the grid inserted. The message key and its placeholders are documented in the config section below.Config layout and reloads
PluginConfigholdsAnnounceDrop,MaxItems,SupplyPrefabsand theLootlist ofLootEntry. Each entry has[JsonProperty]names so the JSON stays readable.LoadConfigfalls 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
Save SupplyDropLoot.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/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 versionEvent-only airdrops
Only rewrite crates spawned by a specific event keyword, leaving vanilla drops untouched.
Generate this versionScrap-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.
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
- Custom loot tablesReplaces the contents of configured LootContainer crates, matched by short prefab name, with weighted items that have per-item chances and min/maxโฆOnLootSpawn
Last updated Oct 26, 2026.
Customize custom airdrop loot
Customize