Skip to content
RustPluginGenerator

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

LockedCrateEvent.cs
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.

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

Chat and console commands
CommandWhat it doesPermission
/lockedcrate startSpawn the event crate immediately, ignoring the timer.lockedcrateevent.manage
lockedcrateevent.statusPrint the current event crate position and seconds remaining until the next drop.lockedcrateevent.manage
Permissions
PermissionGrants
lockedcrateevent.manageGrants the /lockedcrate start chat command and the lockedcrateevent.status console command.

How the code works

  1. Model the config

    The nested PluginConfig class uses [JsonProperty] keys: IntervalMinutes, CratePrefab, HeightAboveMonument, Monuments (a list of MonumentPoint with Name, X, Y, Z), AnnounceMessage, and KillPreviousOnStart. Defaults include a few common monument coordinates so the plugin works on the first load. LoadDefaultConfig assigns defaults and LoadConfig falls back to them if the file cannot be read.

  2. Build a spawn list once the server is ready

    OnServerInitialized converts every configured monument with a non-zero position into a Vector3 and stops if none are valid. It registers the permission, starts the repeating timer, and calls SpawnEventCrate once so the first drop is not delayed by a full interval. NextDropAt is stored as a Unix time using Facepunch.Math.Epoch.Current so the status command can report remaining seconds.

  3. Spawn and track the crate

    SpawnEventCrate picks a random entry, calls GameManager.server.CreateEntity(config.CratePrefab, position, Quaternion.identity) and Spawn() if the result is not null. The prefab defaults to assets/prefabs/misc/hackablelockedcrate/codelockedhackablecrate.prefab. The returned BaseEntity is saved to currentCrate and its net.ID.Value is also stored so the plugin can check the crate later. If KillPreviousOnStart is true, DespawnCurrent runs before any new crate is created.

  4. Announce the location

    After a successful spawn, the plugin calls Msg("Announce", null, monumentName, minutes) and broadcasts the result with Server.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 through lang, so a player using a different locale receives their own translated string.

  5. Watch the hack and kill events

    OnCrateHack(HackableLockedCrate crate) fires when someone begins the hack and the plugin sends a short Msg("Hacking", null, crate.net.ID.Value) broadcast only if the crate matches currentCrate. OnEntityKill(BaseNetworkable entity) clears currentCrate if the crate was destroyed by players or the next spawn. OnServerShutdown calls DespawnCurrent so saves are not left with a stale crate.

  6. Destroy timers and UI-safe cleanup

    Unload destroys the repeating timer, calls DespawnCurrent so the crate does not survive a plugin reload, and clears the tracking fields. Because the plugin broadcasts through Server.Broadcast and does not use CUI, there is nothing else to clean up. DespawnCurrent checks IsDestroyed before calling Kill() to avoid errors on already-dead entities.

Install it on your server

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

    Give it to every player, or swap default for a group such as vip: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 version
  • Team-wide crate rewards

    Awards scrap to every member of the team that hacks the crate.

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

Prompt: Create an Oxide Rust plugin named LockedCrateEvent that on a configurable interval (default 60 minutes) kills its previous hackable locked crate and spawns a new one at a random configured monument, announcing the monument name in chat.
Customize for my server

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

Last updated Oct 26, 2026.

Customize locked crate drop event

Customize