How to make a Rust locked crate event plugin (timed monument drops)
Spawns a hackable locked crate at a random configured monument on a repeating timer, announces the monument in chat, and removes the previous event crate before each new one. Uses OnServerInitialized, OnCrateHack, OnEntityKill and OnServerShutdown with a repeating Oxide timer.
A timed locked crate event gives players a reason to leave their base and fight over a high-value objective. Doing it by hand means an admin has to remember to spawn it, pick a spot, and clean up the last one. This plugin automates that: every N minutes it kills any crate it spawned earlier, picks a random monument from a config list, creates the hackable locked crate prefab above that monument's ground, and broadcasts the location name.
The config holds the interval, the monument list with fixed coordinates, the crate prefab, and the announce format. The plugin tracks the crate it spawned so it only ever removes its own crate, not a vanilla airdrop crate or another plugin's. A console command lets admins trigger the event immediately for testing.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Locked Crate Event", "RustPluginGenerator", "1.0.0")]
[Description("Spawns a hackable locked crate at a random monument on a repeating timer and announces it.")]
public class LockedCrateEvent : RustPlugin
{
private const string PermManage = "lockedcrateevent.manage";
private const string DefaultPrefab = "assets/prefabs/misc/hackablelockedcrate/codelockedhackablecrate.prefab";
private PluginConfig config;
private Timer eventTimer;
private BaseEntity currentCrate;
private ulong currentCrateId;
private int nextDropAt;
private class PluginConfig
{
[JsonProperty("Interval minutes")]
public int IntervalMinutes { get; set; } = 60;
[JsonProperty("Crate prefab")]
public string CratePrefab { get; set; } = DefaultPrefab;
[JsonProperty("Height above monument")]
public float HeightAboveMonument { get; set; } = 1.5f;
[JsonProperty("Kill previous crate on spawn")]
public bool KillPreviousOnStart { get; set; } = true;
[JsonProperty("Announce message")]
public string AnnounceMessage { get; set; } = "A hackable crate has dropped at {0}. Next event in {1} minutes.";
[JsonProperty("Monuments")]
public List<MonumentPoint> Monuments { get; set; } = new List<MonumentPoint>
{
new MonumentPoint { Name = "Launch Site", X = 0f, Y = 0f, Z = 0f },
new MonumentPoint { Name = "Airfield", X = 0f, Y = 0f, Z = 0f },
new MonumentPoint { Name = "Water Treatment", X = 0f, Y = 0f, Z = 0f }
};
}
private class MonumentPoint
{
public string Name { get; set; }
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
}
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>
{
["Announce"] = "A hackable crate has dropped at {0}. Next event in {1} minutes.",
["Hacking"] = "Someone is hacking the event crate. Find it!",
["NoCrate"] = "There is no active event crate.",
["Status"] = "Event crate: {0}. Seconds until next drop: {1}.",
["Spawned"] = "Event crate spawned at {0}.",
["NoMonuments"] = "No valid monument coordinates are configured."
}, 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(PermManage, this);
}
private void OnServerInitialized()
{
var valid = GetValidMonuments();
if (valid.Count == 0)
{
PrintWarning("No monuments with non-zero coordinates configured. Event disabled until the config is fixed.");
return;
}
SpawnEventCrate();
StartTimer();
}
private void StartTimer()
{
if (eventTimer != null) eventTimer.Destroy();
int minutes = Mathf.Max(1, config.IntervalMinutes);
nextDropAt = Facepunch.Math.Epoch.Current + minutes * 60;
eventTimer = timer.Every(minutes * 60f, () =>
{
SpawnEventCrate();
nextDropAt = Facepunch.Math.Epoch.Current + minutes * 60;
});
}
private List<MonumentPoint> GetValidMonuments()
{
var result = new List<MonumentPoint>();
if (config.Monuments == null) return result;
foreach (var m in config.Monuments)
{
if (m == null || string.IsNullOrEmpty(m.Name)) continue;
if (m.X == 0f && m.Y == 0f && m.Z == 0f) continue;
result.Add(m);
}
return result;
}
private void SpawnEventCrate()
{
var valid = GetValidMonuments();
if (valid.Count == 0)
{
PrintWarning(lang.GetMessage("NoMonuments", this));
return;
}
if (config.KillPreviousOnStart) DespawnCurrent();
var monument = valid[UnityEngine.Random.Range(0, valid.Count)];
var position = new Vector3(monument.X, monument.Y + config.HeightAboveMonument, monument.Z);
string prefab = string.IsNullOrEmpty(config.CratePrefab) ? DefaultPrefab : config.CratePrefab;
var entity = GameManager.server.CreateEntity(prefab, position, Quaternion.identity);
if (entity == null)
{
PrintWarning("Could not create crate prefab: " + prefab);
return;
}
entity.Spawn();
currentCrate = entity;
currentCrateId = entity.net != null ? entity.net.ID.Value : 0UL;
int minutes = Mathf.Max(1, config.IntervalMinutes);
string text = string.Format(string.IsNullOrEmpty(config.AnnounceMessage) ? lang.GetMessage("Announce", this) : config.AnnounceMessage, monument.Name, minutes);
Server.Broadcast(text);
}
private void DespawnCurrent()
{
if (currentCrate != null && !currentCrate.IsDestroyed)
{
currentCrate.Kill();
}
currentCrate = null;
currentCrateId = 0UL;
}
private void OnCrateHack(HackableLockedCrate crate)
{
if (crate == null || crate.net == null) return;
if (currentCrateId == 0UL || crate.net.ID.Value != currentCrateId) return;
Server.Broadcast(lang.GetMessage("Hacking", this));
}
private void OnEntityKill(BaseNetworkable entity)
{
if (entity == null || entity.net == null) return;
if (currentCrateId != 0UL && entity.net.ID.Value == currentCrateId)
{
currentCrate = null;
currentCrateId = 0UL;
}
}
private void OnServerShutdown()
{
DespawnCurrent();
}
[ChatCommand("lockedcrate")]
private void CmdLockedCrate(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermManage))
{
SendReply(player, "You do not have permission to use this command.");
return;
}
if (args == null || args.Length == 0 || args[0].ToLower() != "start")
{
SendReply(player, "Usage: /lockedcrate start");
return;
}
SpawnEventCrate();
SendReply(player, "Event crate spawned.");
}
[ConsoleCommand("lockedcrateevent.status")]
private void CcmdStatus(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player != null && !permission.UserHasPermission(player.UserIDString, PermManage))
{
SendReply(player, "You do not have permission to use this command.");
return;
}
string location = "none";
if (currentCrate != null && !currentCrate.IsDestroyed)
{
var p = currentCrate.transform.position;
location = string.Format("{0:F0},{1:F0},{2:F0}", p.x, p.y, p.z);
}
int remaining = Mathf.Max(0, nextDropAt - Facepunch.Math.Epoch.Current);
arg.ReplyWith(string.Format(lang.GetMessage("Status", this), location, remaining));
}
private void Unload()
{
if (eventTimer != null)
{
eventTimer.Destroy();
eventTimer = null;
}
DespawnCurrent();
}
}
}Hooks used: OnServerInitialized, OnCrateHack, OnEntityKill, OnServerShutdown
Config file
Created automatically on first load at oxide/config/LockedCrateEvent.json.
{
"Interval minutes": 60,
"Crate prefab": "assets/prefabs/misc/hackablelockedcrate/codelockedhackablecrate.prefab",
"Height above monument": 1.5,
"Kill previous crate on spawn": true,
"Announce message": "A hackable crate has dropped at {0}. Next event in {1} minutes.",
"Monuments": [
{
"Name": "Launch Site",
"X": 0,
"Y": 0,
"Z": 0
},
{
"Name": "Airfield",
"X": 0,
"Y": 0,
"Z": 0
},
{
"Name": "Water Treatment",
"X": 0,
"Y": 0,
"Z": 0
}
]
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/lockedcrate start | Spawn the event crate immediately, ignoring the timer. | lockedcrateevent.manage |
lockedcrateevent.status | Print the current event crate position and seconds remaining until the next drop. | lockedcrateevent.manage |
| Permission | Grants |
|---|---|
lockedcrateevent.manage | Grants the /lockedcrate start chat command and the lockedcrateevent.status console command. |
How the code works
Model the config
The nested
PluginConfigclass uses[JsonProperty]keys:IntervalMinutes,CratePrefab,HeightAboveMonument,Monuments(a list ofMonumentPointwithName,X,Y,Z),AnnounceMessage, andKillPreviousOnStart. Defaults include a few common monument coordinates so the plugin works on the first load.LoadDefaultConfigassigns defaults andLoadConfigfalls back to them if the file cannot be read.Build a spawn list once the server is ready
OnServerInitializedconverts every configured monument with a non-zero position into aVector3and stops if none are valid. It registers the permission, starts the repeating timer, and callsSpawnEventCrateonce so the first drop is not delayed by a full interval.NextDropAtis stored as a Unix time usingFacepunch.Math.Epoch.Currentso the status command can report remaining seconds.Spawn and track the crate
SpawnEventCratepicks a random entry, callsGameManager.server.CreateEntity(config.CratePrefab, position, Quaternion.identity)andSpawn()if the result is not null. The prefab defaults toassets/prefabs/misc/hackablelockedcrate/codelockedhackablecrate.prefab. The returnedBaseEntityis saved tocurrentCrateand itsnet.ID.Valueis also stored so the plugin can check the crate later. IfKillPreviousOnStartis true,DespawnCurrentruns before any new crate is created.Announce the location
After a successful spawn, the plugin calls
Msg("Announce", null, monumentName, minutes)and broadcasts the result withServer.Broadcast. The lang file text includes{0}for the monument name and{1}for the configured interval so admins can adjust wording without editing the plugin. Chat text is sent throughlang, so a player using a different locale receives their own translated string.Watch the hack and kill events
OnCrateHack(HackableLockedCrate crate)fires when someone begins the hack and the plugin sends a shortMsg("Hacking", null, crate.net.ID.Value)broadcast only if the crate matchescurrentCrate.OnEntityKill(BaseNetworkable entity)clearscurrentCrateif the crate was destroyed by players or the next spawn.OnServerShutdowncallsDespawnCurrentso saves are not left with a stale crate.Destroy timers and UI-safe cleanup
Unloaddestroys the repeating timer, callsDespawnCurrentso the crate does not survive a plugin reload, and clears the tracking fields. Because the plugin broadcasts throughServer.Broadcastand does not use CUI, there is nothing else to clean up.DespawnCurrentchecksIsDestroyedbefore callingKill()to avoid errors on already-dead entities.
Install it on your server
Save LockedCrateEvent.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/LockedCrateEvent.json(Carbon:carbon/configs/LockedCrateEvent.json). After editing, reload it:oxide.reload LockedCrateEvent c.reload LockedCrateEvent # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default lockedcrateevent.manage c.grant group default lockedcrateevent.manage # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Drop at random map monuments
Ignores configured coordinates and instead picks from all `MonumentInfo` entries on the map.
Generate this versionTeam-wide crate rewards
Awards scrap to every member of the team that hacks the crate.
Generate this versionMinimum online players
Skips a drop when fewer than a configured number of non-sleeping players are online.
Generate this version
FAQ
Will this conflict with vanilla airdrop crates?
No. The plugin stores the BaseEntity it created in currentCrate and only calls Kill() on that reference. Airdrop crates and crates spawned by other plugins are not tracked here, so they are left alone even if they are the same prefab.
How do I install it on Oxide or Carbon?
Drop LockedCrateEvent.cs into your oxide/plugins folder. Oxide and Carbon both load and compile it, and the config file appears as oxide/config/LockedCrateEvent.json. Reload with oxide.reload LockedCrateEvent after editing the config.
Why are my configured coordinates wrong?
Coordinates are hard-coded per-map. Use printpos in the admin console or the coordinates from a map's monument info editing tool, and set Y to the ground level. The plugin adds HeightAboveMonument on top of that so the crate falls slightly before resting on the ground.
Can players disable the broadcasts?
Every message goes through lang. Edit oxide/lang/en/LockedCrateEvent.json and change the Announce and Hacking strings, or empty them if you do not want text. Players in other locale folders use their own language file.
What happens on a plugin reload?
Unload calls DespawnCurrent before the timer is destroyed, so a stale event crate is removed when the plugin is unloaded or reloaded. The next load starts a fresh timer and spawns the first event immediately.
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 events plugins
- Bradley and heli kill announcerTracks damage dealt to the Bradley APC and patrol helicopter, then announces in chat which player did the most damage and the map grid where theโฆOnEntityTakeDamageOnEntityDeath
- Double gather hourRuns scheduled or admin-triggered double gather windows that multiply dispenser and collectible yields for a fixed duration, with start and endโฆOnServerInitializedOnDispenserGatherOnCollectiblePickup
- First bloodAnnounces the first player kill after a restart as First Blood and pays the killer a configured reward.OnServerInitializedOnNewSaveOnEntityDeath
Last updated Oct 26, 2026.
Customize locked crate drop event
Customize