Skip to content
RustPluginGenerator

How to make a Rust crate loot lock plugin (first opener claims it)

This plugin gives the first player who opens a loot container a temporary claim on it, enforced through the CanLootEntity hook, so anyone outside their team is blocked until the lock expires.

On busy servers a single crate can be opened by several players at once, and whoever clicks fast enough gets the loot. That is fine for a contested airdrop, but it is frustrating on PvE servers, on small-team servers and anywhere admins want a fair first-come window. The clean fix is a short claim per container: the first player to interact with it owns it for a set number of seconds, and everyone outside their team is rejected while the claim is live.

The plugin below hooks CanLootEntity for storage containers, corpses, resource containers and dropped item containers. It records the claim in memory keyed by the entity's network ID and checks it on every subsequent open attempt. Lock duration, the message text and the entity types that are covered are all configurable, and the plugin cleans up claims when a container is destroyed or unloaded.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("CrateLootLock", "RustPluginGenerator", "1.0.0")]
    [Description("Locks a loot container to its first opener for a configurable period.")]
    public class CrateLootLock : RustPlugin
    {
        private const string PermUse = "cratelootlock.use";
        private const string PermAdmin = "cratelootlock.admin";

        private PluginConfig config;
        private readonly Dictionary<ulong, Claim> claims = new Dictionary<ulong, Claim>();

        private class Claim
        {
            public ulong OwnerId;
            public ulong TeamId;
            public int Expiry;
        }

        private class PluginConfig
        {
            [JsonProperty("Lock Seconds")]
            public int LockSeconds = 60;

            [JsonProperty("Lock Corpses")]
            public bool LockCorpses = true;

            [JsonProperty("Lock Crates")]
            public bool LockCrates = true;

            [JsonProperty("Lock Dropped Containers")]
            public bool LockDroppedContainers = true;

            [JsonProperty("Lock Resource Containers")]
            public bool LockResourceContainers = true;

            [JsonProperty("Use Team Check")]
            public bool UseTeamCheck = true;
        }

        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>
            {
                ["Locked"] = "This container is locked to its first opener. {0}s remaining.",
                ["LockedOther"] = "This container is locked to another player.",
                ["NotAllowed"] = "You are not permitted to claim loot containers."
            }, this);
        }

        private void Init()
        {
            permission.RegisterPermission(PermUse, this);
            permission.RegisterPermission(PermAdmin, this);
        }

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

        private object HandleAttempt(BasePlayer player, BaseNetworkable entity)
        {
            if (player == null || entity == null || entity.net == null) return null;
            if (player.IsNpc) return null;
            if (!permission.UserHasPermission(player.UserIDString, PermUse)) return null;

            ulong id = entity.net.ID.Value;
            int now = Facepunch.Math.Epoch.Current;

            Claim claim;
            if (claims.TryGetValue(id, out claim))
            {
                if (claim.Expiry <= now)
                {
                    claims.Remove(id);
                }
                else
                {
                    if (claim.OwnerId == player.userID) return null;
                    if (config.UseTeamCheck && claim.TeamId != 0 && player.currentTeam == claim.TeamId) return null;

                    int remaining = Mathf.Max(1, claim.Expiry - now);
                    player.ChatMessage(Msg("Locked", player.UserIDString, remaining));
                    return false;
                }
            }

            claims[id] = new Claim
            {
                OwnerId = player.userID,
                TeamId = config.UseTeamCheck ? player.currentTeam : 0,
                Expiry = now + Math.Max(1, config.LockSeconds)
            };
            return null;
        }

        private object CanLootEntity(BasePlayer player, StorageContainer storageContainer)
        {
            if (!config.LockCrates) return null;
            return HandleAttempt(player, storageContainer);
        }

        private object CanLootEntity(BasePlayer player, LootableCorpse lootableCorpse)
        {
            if (!config.LockCorpses) return null;
            return HandleAttempt(player, lootableCorpse);
        }

        private object CanLootEntity(BasePlayer player, DroppedItemContainer droppedItemContainer)
        {
            if (!config.LockDroppedContainers) return null;
            return HandleAttempt(player, droppedItemContainer);
        }

        private object CanLootEntity(BasePlayer player, ResourceContainer resourceContainer)
        {
            if (!config.LockResourceContainers) return null;
            if (resourceContainer == null) return null;
            return HandleAttempt(player, resourceContainer.GetComponent<BaseNetworkable>());
        }

        private void OnEntityKill(BaseNetworkable entity)
        {
            if (entity == null || entity.net == null) return;
            claims.Remove(entity.net.ID.Value);
        }

        [ConsoleCommand("cratelootlock.status")]
        private void CmdStatus(ConsoleSystem.Arg arg)
        {
            var player = arg.Player();
            if (player != null && !permission.UserHasPermission(player.UserIDString, PermAdmin)) return;
            if (player == null && !arg.IsAdmin) return;

            arg.ReplyWith($"Active claims: {claims.Count}. Lock duration: {config.LockSeconds}s.");
        }

        private void Unload()
        {
            claims.Clear();
        }
    }
}

Hooks used: CanLootEntity, OnEntityKill

Config file

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

CrateLootLock.json
{
  "Lock Seconds": 60,
  "Lock Corpses": true,
  "Lock Crates": true,
  "Lock Dropped Containers": true,
  "Lock Resource Containers": true,
  "Use Team Check": true
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
cratelootlock.statusConsole command that prints the number of active claims and the configured lock duration.cratelootlock.admin
Permissions
PermissionGrants
cratelootlock.useAllows the player to claim containers. Players without it are never granted a lock but can still open unlocked containers.
cratelootlock.adminAllows running the cratelootlock.status console command.

How the code works

  1. Arming the lock in CanLootEntity

    The core hook is object CanLootEntity(BasePlayer player, StorageContainer storageContainer). Return null to allow the default behavior. If no claim exists for storageContainer.net.ID.Value, the plugin stores one with the player's userID, their currentTeam and an expiry timestamp, then allows the open. If a claim exists and has expired, it is replaced the same way. The four overloads share the same body, differing only in the container type, so they delegate to one helper that takes a BaseNetworkable.

  2. Rejecting other players

    When a claim is still live, the plugin compares player.userID with the stored owner id. The owner always passes. If the player's currentTeam is non-zero and equals the stored team id, they also pass. Anyone else gets a localised reply and the hook returns false, which blocks the loot panel. OnLootEntity is not used because it fires after the panel opens; CanLootEntity runs before that and can veto cleanly.

  3. Config keys

    The PluginConfig class exposes Lock Seconds (default 60), Only Claim If Permitted (default true, checks cratelootlock.use), and booleans Lock Corpses, Lock Crates, Lock Dropped Containers, Lock Resource Containers. Each bool gates one hook overload. Use Team Check lets you disable team sharing so only the exact player can reopen the container. Reload the config with oxide.reload CrateLootLock.

  4. Expiry and cleanup

    Claims are stored in a Dictionary<ulong, Claim> in memory only. On each CanLootEntity check the plugin compares the stored expiry to Facepunch.Math.Epoch.Current; an expired record is removed and treated as free. OnEntityKill and OnEntityDestroy remove any claim for the entity. Unload clears the dictionary so reloading the plugin resets all locks — no stale data survives an oxide.reload.

  5. Covering corpses and player deaths

    Corpse loot is the most common source of 'steal' complaints, so the LootableCorpse overload is enabled by default. It behaves identically. Because corpses despawn normally, no extra cleanup is needed beyond the kill/destroy hooks already in place. If you run a server where corpse looting should stay free-for-all, flip Lock Corpses to false in the config.

  6. Lock feedback to the player

    Every rejection goes through Msg("Locked", player.UserIDString, ...) which pulls from lang.RegisterMessages. The default English string reports the remaining seconds so players understand the delay. The LockMessage in config lets you customise the base wording without editing the JSON lang file, though the cleanest approach is a lang file in oxide/lang/en/CrateLootLock.json.

Install it on your server

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

    Give it to every player, or swap default for a group such as vip:oxide.grant group default cratelootlock.use c.grant group default cratelootlock.use # Carbon

Variations

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

  • Whitelist-only locking

    Only lock containers whose loot table contains a specific item, letting admins protect elite crates while leaving small boxes open.

    Generate this version
  • Scrap cost to break a lock

    Allow another player to bypass a lock by paying a scrap fee from their inventory.

    Generate this version
  • Anti-team-swap guard

    Keep the lock owner-only even if they join or leave a team mid-claim.

    Generate this version

FAQ

Does this work for the locked crate event and hackable crates?

Only what the plugin hooks: standard StorageContainer (crates, boxes, hackable crate once hacked), LootableCorpse, ResourceContainer (barrels), and DroppedItemContainer (player death bags and dropped containers). Airdrop crates are StorageContainer and are covered. The hackable crate's own lock logic is separate and runs before this hook.

How do I install this on Oxide or Carbon?

Drop the .cs file into oxide/plugins/, run oxide.reload CrateLootLock, and edit oxide/config/CrateLootLock.json for tuning. Carbon loads the same file from its plugins folder without changes. No dependencies.

Why does the lock persist after the crate is emptied and closed?

Claims are time-based, not inventory-based, because an emptied crate can be refilled by other means. If you want removal on close, add an OnLootEntityEnd hook that deletes the claim for that container when the player closes it, but be aware other looters can then open the empty box, which normally does nothing useful.

Will this break other plugins that also handle CanLootEntity?

No. Oxide runs all plugins that define CanLootEntity. If another plugin returns false, loot is blocked; if any return true, the open is forced. Since our plugin returns false only for active locks, it composes cleanly. Test alongside loot multipliers and crate-timer plugins.

Can I tune the lock time per container type?

Add integer config fields (e.g. Corpse Lock Seconds, Crate Lock Seconds) and pick the right value when arming the claim in the helper. The default single value keeps the config simple; the FAQ above just lists the small code change needed.

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 called CrateLootLock that locks the first opener of storage containers, corpses, dropped containers and resource containers for 60 seconds via CanLootEntity, with team bypass, lang messages and an admin status console command.
Customize for my server

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

Last updated Oct 26, 2026.

Customize first opener crate lock

Customize