How to make a Rust corpse loot protection plugin (solo and team lock)
Locks a dead player's corpse and dropped backpack to the owner and their current team for a configurable number of minutes. It watches spawned containers with OnEntitySpawned and blocks other players in CanLootEntity, with a permission-based admin bypass.
On a PvE or semi-PvE server the biggest complaint is corpse theft: someone dies, a stranger sprints in and takes everything before the owner respawns and walks back. Rust does not ship a way to lock a body or a backpack, and the corpse is a normal LootableCorpse while the backpack is a DroppedItemContainer, so you have to treat both.
This plugin records the owner of each new corpse and dropped backpack through OnEntitySpawned, stores the owner's team at spawn time, and starts a countdown from your config. While the lock is active, CanLootEntity returns false for anyone who is not the owner, not on that team, and not holding the bypass permission. After the timer expires the container becomes public again. Owners and teammates always get in, so nobody is ever locked out of their own loot.
The plugin code
using System.Linq;
using Oxide.Core;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Corpse Loot Protection", "RustPluginGenerator", "1.0.0")]
[Description("Locks player corpses and dropped backpacks to the owner and their team for a configurable time.")]
public class CorpseLootProtection : RustPlugin
{
private const string PermAdmin = "corpselootprotection.admin";
private const string PermUse = "corpselootprotection.use";
private class LockEntry
{
public ulong OwnerId;
public ulong TeamId;
public int ExpiresAt;
}
private class PluginConfig
{
[JsonProperty("Lock duration in seconds")]
public int LockSeconds = 300;
[JsonProperty("Share lock with owner's team")]
public bool TeamShared = true;
[JsonProperty("Lock player corpses")]
public bool LockPlayerCorpses = true;
[JsonProperty("Lock dropped backpacks")]
public bool LockDroppedBackpacks = true;
[JsonProperty("Show remaining time in refusal message")]
public bool ShowRemainingTime = true;
[JsonProperty("Refusal message cooldown in seconds")]
public float MessageCooldown = 5f;
}
private PluginConfig config;
private readonly Dictionary<ulong, LockEntry> locks = new Dictionary<ulong, LockEntry>();
private readonly Dictionary<ulong, float> lastMessage = new Dictionary<ulong, float>();
private Timer cleanupTimer;
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 another player for {0} more seconds.",
["LockedShort"] = "This container is locked to another player.",
["NotYours"] = "This is not your container.",
["Status"] = "This container is locked to its owner for {0} more seconds.",
["NoLock"] = "This container is not locked.",
["UnlockedAll"] = "All corpse locks were cleared ({0} entries removed).",
["NoPermission"] = "You do not have permission to use this command."
}, this);
}
private string Msg(string key, string userId = null, params object[] args)
{
return string.Format(lang.GetMessage(key, this, userId), args);
}
private void Init()
{
permission.RegisterPermission(PermAdmin, this);
permission.RegisterPermission(PermUse, this);
}
private void OnServerInitialized()
{
LoadLocks();
cleanupTimer = timer.Every(30f, Cleanup);
}
private void Unload()
{
cleanupTimer?.Destroy();
SaveLocks();
}
private void LoadLocks()
{
try
{
var stored = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<ulong, LockEntry>>(Name);
if (stored == null) return;
var now = Facepunch.Math.Epoch.Current;
foreach (var pair in stored)
{
if (pair.Value == null || pair.Value.ExpiresAt <= now) continue;
if (!(BaseNetworkable.serverEntities.Find(new NetworkableId(pair.Key)) is BaseEntity)) continue;
locks[pair.Key] = pair.Value;
}
}
catch (Exception ex)
{
PrintWarning("Could not load stored locks: " + ex.Message);
}
}
private void SaveLocks()
{
Interface.Oxide.DataFileSystem.WriteObject(Name, locks);
}
private void OnEntitySpawned(BaseNetworkable entity)
{
if (entity == null || entity.IsDestroyed) return;
var corpse = entity as LootableCorpse;
if (corpse != null)
{
if (!config.LockPlayerCorpses) return;
if (corpse.playerSteamID == 0) return;
CreateLock(entity, corpse.playerSteamID);
return;
}
var dropped = entity as DroppedItemContainer;
if (dropped != null)
{
if (!config.LockDroppedBackpacks) return;
if (dropped.playerSteamID == 0) return;
CreateLock(entity, dropped.playerSteamID);
}
}
private void CreateLock(BaseNetworkable entity, ulong ownerId)
{
if (config.LockSeconds <= 0) return;
var id = entity.net.ID.Value;
if (locks.ContainsKey(id)) return;
ulong teamId = 0;
var owner = BasePlayer.FindByID(ownerId);
if (owner != null && owner.currentTeam != 0) teamId = owner.currentTeam;
locks[id] = new LockEntry
{
OwnerId = ownerId,
TeamId = teamId,
ExpiresAt = Facepunch.Math.Epoch.Current + config.LockSeconds
};
}
private void Cleanup()
{
if (locks.Count == 0) return;
var now = Facepunch.Math.Epoch.Current;
var remove = new List<ulong>();
foreach (var pair in locks)
{
if (pair.Value == null || pair.Value.ExpiresAt <= now) remove.Add(pair.Key);
}
foreach (var id in remove) locks.Remove(id);
}
private object CanLootEntity(BasePlayer player, LootableCorpse corpse)
{
if (player == null || corpse == null) return null;
return CheckAccess(player, corpse.net.ID.Value);
}
private object CanLootEntity(BasePlayer player, DroppedItemContainer container)
{
if (player == null || container == null) return null;
return CheckAccess(player, container.net.ID.Value);
}
private object CanLootEntity(BasePlayer player, StorageContainer container)
{
if (player == null || container == null) return null;
return CheckAccess(player, container.net.ID.Value);
}
private object CheckAccess(BasePlayer player, ulong entityId)
{
LockEntry entry;
if (!locks.TryGetValue(entityId, out entry) || entry == null) return null;
if (entry.ExpiresAt <= Facepunch.Math.Epoch.Current)
{
locks.Remove(entityId);
return null;
}
if (permission.UserHasPermission(player.UserIDString, PermAdmin)) return null;
if (player.userID.Get() == entry.OwnerId) return null;
if (config.TeamShared && entry.TeamId != 0 && player.currentTeam == entry.TeamId) return null;
NotifyLocked(player, entry);
return false;
}
private void NotifyLocked(BasePlayer player, LockEntry entry)
{
float now = UnityEngine.Time.realtimeSinceStartup;
float last;
if (lastMessage.TryGetValue(player.userID.Get(), out last) && now - last < config.MessageCooldown) return;
lastMessage[player.userID.Get()] = now;
int remaining = Mathf.Max(0, entry.ExpiresAt - Facepunch.Math.Epoch.Current);
if (config.ShowRemainingTime)
player.ChatMessage(Msg("Locked", player.UserIDString, remaining));
else
player.ChatMessage(Msg("LockedShort", player.UserIDString));
}
[ChatCommand("corpselock")]
private void CmdCorpseLock(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
player.ChatMessage(Msg("NoPermission", player.UserIDString));
return;
}
BaseEntity found = FindLookingAt(player);
if (found == null)
{
player.ChatMessage(Msg("NotYours", player.UserIDString));
return;
}
LockEntry entry;
if (!locks.TryGetValue(found.net.ID.Value, out entry) || entry == null || entry.ExpiresAt <= Facepunch.Math.Epoch.Current)
{
player.ChatMessage(Msg("NoLock", player.UserIDString));
return;
}
if (player.userID.Get() != entry.OwnerId &&
!(config.TeamShared && entry.TeamId != 0 && player.currentTeam == entry.TeamId) &&
!permission.UserHasPermission(player.UserIDString, PermAdmin))
{
player.ChatMessage(Msg("NotYours", player.UserIDString));
return;
}
int remaining = Mathf.Max(0, entry.ExpiresAt - Facepunch.Math.Epoch.Current);
player.ChatMessage(Msg("Status", player.UserIDString, remaining));
}
private BaseEntity FindLookingAt(BasePlayer player)
{
RaycastHit hit;
if (!Physics.Raycast(player.eyes.HeadRay(), out hit, 5f, Physics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore))
return null;
var entity = hit.GetEntity();
if (entity is LootableCorpse || entity is DroppedItemContainer || entity is StorageContainer)
return entity;
return null;
}
[ConsoleCommand("corpselootprotection.unlock")]
private void CcmdUnlockAll(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player != null && !permission.UserHasPermission(player.UserIDString, PermAdmin))
{
arg.ReplyWith(Msg("NoPermission", player.UserIDString));
return;
}
int count = locks.Count;
locks.Clear();
SaveLocks();
arg.ReplyWith(Msg("UnlockedAll", null, count));
}
}
}Hooks used: OnServerInitialized, OnEntitySpawned, CanLootEntity
Config file
Created automatically on first load at oxide/config/CorpseLootProtection.json.
{
"Lock duration in seconds": 300,
"Share lock with owner's team": true,
"Lock player corpses": true,
"Lock dropped backpacks": true,
"Show remaining time in refusal message": true,
"Refusal message cooldown in seconds": 5
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/corpselock | Shows the remaining lock time on the corpse, backpack or container you are looking at. | corpselootprotection.use |
corpselootprotection.unlock | Console command that clears every active corpse lock immediately. | corpselootprotection.admin |
| Permission | Grants |
|---|---|
corpselootprotection.admin | Bypass every corpse lock and use the unlock console command. |
corpselootprotection.use | Allows the /corpselock chat command that shows remaining lock time. |
How the code works
Record the owner when a corpse or backpack spawns
OnEntitySpawnedfires for every networked entity, so the first thing the hook does is test the type. A player death produces aLootableCorpsewhoseplayerSteamIDis the dead player's id, and a dropped backpack is aDroppedItemContainerwhoseplayerSteamIDis the owner. Both get aLockEntrykeyed bynet.ID.Value, the network id, which is stable for the lifetime of the entity. Entities with a zero owner id (NPC corpses, loot bags spawned by plugins) are skipped.Capture the team at spawn time
When the lock is created the plugin reads
BasePlayer.FindByID(ownerId)and storesowner.currentTeamin the entry. Storing the team id once means a teammate who joins later cannot loot an old corpse, and a teammate who was removed keeps access only if they were on the team at death. IfcurrentTeamis 0 the storedTeamIdstays 0 and the lock is solo-only. The expiry isFacepunch.Math.Epoch.Current + config.LockSeconds, written to the entry'sExpiresAtfield.Block looting in CanLootEntity
Three overloads matter here:
CanLootEntity(BasePlayer, LootableCorpse),CanLootEntity(BasePlayer, DroppedItemContainer)and the genericCanLootEntity(BasePlayer, StorageContainer), which covers chests someone may stuff a body into. Each one callsCheckAccess, which looks up the entry by network id, checksExpiresAt, then allows the owner (player.userIDequalsOwnerId), any player whosecurrentTeammatchesTeamIdandTeamSharedis true, and anyone withcorpselootprotection.admin. Returningfalsecancels the loot. Everything else returnsnullso default behavior continues.Clean up expired records
A repeating timer runs every 30 seconds and drops entries whose
ExpiresAtis in the past, so the dictionary does not grow forever on a busy server. Removing an entry does not change gameplay: once the lock is gone the container is a normal corpse and anyone can loot it. The same expiry check also runs insideCheckAccess, so a refused or allowed lookup on a stale entry removes it immediately. The timer reference is kept incleanupTimerand destroyed inUnload.Persist locks across restarts
Corpses do not survive a restart in practice, but reloading the plugin during a fight should not hand bodies to strangers.
LoadLocksreads aDictionary<ulong, LockEntry>fromoxide/data/CorpseLootProtection.jsonthroughInterface.Oxide.DataFileSystem.ReadObject, keeps entries whoseExpiresAtis still in the future and whose entity id still resolves inBaseNetworkable.serverEntities.SaveLockswrites the whole dictionary back, andUnloadsaves on the way out.Feedback, bypass and the chat command
When a loot attempt is refused,
NotifyLockedsends a language message with the remaining seconds ifShowRemainingTimeis true, otherwise a shorter one. It is rate-limited per player with theMessageCooldownconfig value (default 5 seconds) usingUnityEngine.Time.realtimeSinceStartup, so interaction spam cannot flood chat./corpselockraycasts 5 meters fromplayer.eyes.HeadRay()for a corpse, backpack or container and reports the remaining lock time to the owner or a teammate.Configuration and tuning
Lock duration in secondssets the lock length (default 300).Share lock with owner's teamswitches team access on or off.Lock player corpsesandLock dropped backpackslet admins protect the body and the backpack separately, so a server can keep backpacks private while leaving bodies public.Show remaining time in refusal messageandRefusal message cooldown in secondscontrol what a blocked player sees and how often. A missing or broken config file falls back to defaults and is rewritten.
Install it on your server
Save CorpseLootProtection.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/CorpseLootProtection.json(Carbon:carbon/configs/CorpseLootProtection.json). After editing, reload it:oxide.reload CorpseLootProtection c.reload CorpseLootProtection # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default corpselootprotection.admin c.grant group default corpselootprotection.admin # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Lock only the backpack, release the corpse immediately
On some servers the corpse should be public but the backpack kept private so the owner can retrieve the loot.
Generate this versionExtend the lock every time the owner is hit
Punishes griefers by refreshing the timer when someone attacks the owner nearby.
Generate this versionBroadcast a warning when someone tries to loot a blocked corpse
Makes it obvious why a player cannot open the container.
Generate this version
FAQ
Does this work with Carbon?
Yes. Carbon loads the same Oxide plugin file, so the layout and the hooks are unchanged. Drop it in oxide/plugins and it will be loaded on the next reload or restart.
What happens to a corpse if the owner never comes back?
The lock expires after the configured duration and the corpse becomes public like any other. The entity itself despawns when Rust decides it has rotted, which the plugin does not touch.
Can I lock only my own corpse and not my team's?
Set Share lock with owner's team to false in the config. The plugin still stores the team id at spawn time but ignores it in the access check, so only the owner is allowed to loot.
How does the plugin know a container is a corpse?
It matches on the LootableCorpse and DroppedItemContainer types in OnEntitySpawned. Chests, lockers and other storage are not locked on spawn, though the generic CanLootEntity(StorageContainer) overload still checks the lock table in case a corpse id collides in a moved container.
Will a player with the bypass permission still get the refusal message?
No. The bypass check runs before the message is sent, so admins and moderators can loot freely without spam. Everything still goes through the language file if you want to change the wording.
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 pve plugins
- Building damage protectionBlocks and deployables only take damage from their owner or players authorized on the covering tool cupboard.OnEntityTakeDamage
- Heli and Bradley loot lockTracks damage dealt to the Patrol Helicopter and Bradley APC per player and locks the crates they drop for a configurable time, allowing only theโฆOnServerInitializedOnServerSaveOnEntityTakeDamage
- NPC corpse loot lockLocks NPC corpses to the killer and their team for a configurable number of seconds after death, then opens them to everyone.OnServerInitializedOnEntityDeathCanLootEntity
Last updated Oct 26, 2026.
Customize player corpse loot protection
Customize