How to make a Rust sleeper protection plugin (timed spawn protection)
Gives sleeping players a configurable grace period after disconnect during which other players cannot damage them, and optionally blocks looting their body. It uses OnPlayerDisconnected to start the timer, OnEntityTakeDamage to block hits, and CanLootPlayer to deny looting.
Offline raiding is the most common complaint on PvP servers: a player disconnects, their body drops to the ground, and anyone can walk up and kill or loot it without a fight. A hard toggle (full immunity or none) does not fit most servers, so this plugin uses a time window instead.
On disconnect the plugin records the disconnect time in oxide/data and immediately expires the protection when the player reconnects or wakes. While a sleeper is protected, incoming damage from other players is cancelled and, if enabled, looting their body is denied. The plugin answers OnEntityTakeDamage, OnPlayerDisconnected and CanLootPlayer, stores everything in a JSON data file, and exposes a /sleepers command so admins can inspect or clear a single sleeper's status.
The plugin code
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Sleeper Protection", "RustPluginGenerator", "1.0.0")]
[Description("Gives sleeping players a timed protection window against damage and looting after they disconnect.")]
public class SleeperProtection : RustPlugin
{
private const string PermAdmin = "sleeperprotection.admin";
private const string DataFile = "SleeperProtection";
private PluginConfig config;
private Dictionary<string, int> protection = new Dictionary<string, int>();
private class PluginConfig
{
[JsonProperty("Protection seconds")]
public int ProtectionSeconds = 300;
[JsonProperty("Block looting of protected sleepers")]
public bool BlockLooting = true;
[JsonProperty("Block explosion damage")]
public bool BlockExplosions = true;
[JsonProperty("Admins bypass loot block")]
public bool AdminBypassLoot = 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>
{
["Protected"] = "This sleeper is protected and cannot be looted right now.",
["Status"] = "{0} is protected for another {1} seconds.",
["StatusExpired"] = "{0} has no active protection.",
["Cleared"] = "Protection cleared for {0}.",
["NotFound"] = "No player found matching '{0}'.",
["Usage"] = "Usage: /sleepers <status|clear> <name or steamid>"
}, 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(PermAdmin, this);
}
private void OnServerInitialized()
{
var loaded = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, int>>(DataFile);
if (loaded != null) protection = loaded;
int now = Facepunch.Math.Epoch.Current;
var expired = new List<string>();
foreach (var kvp in protection)
{
if (kvp.Value <= now) expired.Add(kvp.Key);
}
foreach (var id in expired) protection.Remove(id);
SaveData();
}
private void Unload()
{
SaveData();
}
private void SaveData()
{
Interface.Oxide.DataFileSystem.WriteObject(DataFile, protection);
}
private bool IsProtected(BasePlayer sleeper)
{
if (sleeper == null) return false;
int expires;
if (!protection.TryGetValue(sleeper.UserIDString, out expires)) return false;
if (Facepunch.Math.Epoch.Current >= expires)
{
protection.Remove(sleeper.UserIDString);
return false;
}
return true;
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (player == null || player.IsNpc || !player.IsConnected) return;
if (config.ProtectionSeconds <= 0) return;
protection[player.UserIDString] = Facepunch.Math.Epoch.Current + config.ProtectionSeconds;
SaveData();
}
private void OnPlayerSleepEnded(BasePlayer player)
{
if (player == null) return;
if (protection.Remove(player.UserIDString)) SaveData();
}
private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo)
{
if (entity == null || hitInfo == null) return null;
var sleeper = entity as BasePlayer;
if (sleeper == null) return null;
if (!IsProtected(sleeper)) return null;
if (hitInfo.Initiator == null) return null;
if (hitInfo.Initiator == entity) return null;
if (hitInfo.damageTypes != null && hitInfo.damageTypes.GetMajorityDamageType() == Rust.DamageType.Explosion && !config.BlockExplosions)
return null;
var attacker = hitInfo.InitiatorPlayer;
if (attacker == null)
{
if (!(hitInfo.Initiator is BasePlayer)) return null;
attacker = hitInfo.Initiator as BasePlayer;
}
if (attacker == null || attacker == sleeper) return null;
return true;
}
private object CanLootPlayer(BasePlayer player, BasePlayer target)
{
if (player == null || target == null) return null;
if (!config.BlockLooting) return null;
if (!IsProtected(target)) return null;
if (config.AdminBypassLoot && permission.UserHasPermission(player.UserIDString, PermAdmin))
return null;
player.ChatMessage(Msg("Protected", player.UserIDString));
return false;
}
[ChatCommand("sleepers")]
private void CmdSleepers(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
{
player.ChatMessage(Msg("Usage", player.UserIDString));
return;
}
if (args == null || args.Length < 2)
{
player.ChatMessage(Msg("Usage", player.UserIDString));
return;
}
string sub = args[0].ToLowerInvariant();
string query = string.Join(" ", args, 1, args.Length - 1);
BasePlayer target = BasePlayer.FindAwakeOrSleeping(query);
if (target == null)
{
player.ChatMessage(Msg("NotFound", player.UserIDString, query));
return;
}
if (sub == "status")
{
int expires;
if (!IsProtected(target))
{
player.ChatMessage(Msg("StatusExpired", player.UserIDString, target.displayName));
return;
}
expires = protection[target.UserIDString];
int remaining = expires - Facepunch.Math.Epoch.Current;
player.ChatMessage(Msg("Status", player.UserIDString, target.displayName, remaining));
}
else if (sub == "clear")
{
protection.Remove(target.UserIDString);
SaveData();
player.ChatMessage(Msg("Cleared", player.UserIDString, target.displayName));
}
else
{
player.ChatMessage(Msg("Usage", player.UserIDString));
}
}
}
}Hooks used: OnServerInitialized, OnPlayerDisconnected, OnPlayerSleepEnded, OnEntityTakeDamage, CanLootPlayer
Config file
Created automatically on first load at oxide/config/SleeperProtection.json.
{
"Protection seconds": 300,
"Block looting of protected sleepers": true,
"Block explosion damage": true,
"Admins bypass loot block": true
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/sleepers status <name or steamid> | Shows whether the named sleeper is still protected and how many seconds remain. | sleeperprotection.admin |
/sleepers clear <name or steamid> | Removes the protection from a sleeper immediately. | sleeperprotection.admin |
| Permission | Grants |
|---|---|
sleeperprotection.admin | Grants access to the /sleepers status and /sleepers clear commands. |
How the code works
Start the timer on disconnect
The
OnPlayerDisconnectedhook receives the leaving player and the disconnect reason. The plugin ignores NPCs and players that are not connected, then stores the current Unix time fromFacepunch.Math.Epoch.Currentkeyed by the player'sUserIDString. The protection window ends atnow + config.ProtectionSeconds. Nothing is written back to the player entity, so the body stays a normal sleeper.Cancel protection on reconnect
OnPlayerSleepEndedfires when the player wakes up. Because a reconnect also wakes the body, the same handler that clears protection covers both cases. The entry is removed from the data dictionary and saved, so a player who just logs back in cannot be attacked through a stale record. Remove the dictionary entry rather than storing a zero timestamp so the data file stays small.Block damage to protected sleepers
OnEntityTakeDamageis declared with theBaseCombatEntityparameter type so it fires for players and all other combat entities. The handler returns early when the entity is not aBasePlayer, when the attacker is null, when the sleeper has no record, or when the timer already elapsed. Two exclusions keep the server playable: self-damage from the sleeper's own entity is allowed, and damage from explosives still ticks ifBlockExplosionsis false.Deny looting when configured
CanLootPlayeris called with the looter and the target player. WhenBlockLootingis on and the target still has a valid protection entry, the hook sends a chat message to the looter through the language file and returnsfalseto abort the loot. Returningnullortruekeeps vanilla behavior. Admins with thesleeperprotection.adminpermission bypass the loot block so they can inspect bodies during a ticket.Handle permission and data lifecycle
Initregisterssleeperprotection.admin.OnServerInitializedloadsoxide/data/SleeperProtection.jsonwithInterface.Oxide.DataFileSystem.ReadObject<Dictionary<string, int>>, dropping entries whose timestamps have already expired.Unloadwrites the dictionary back and logs the remaining count, so a restart during an active protection window does not turn a sleeper into fresh meat.Admin commands
The
/sleeperschat command is split intostatusandclear.statusconverts the stored Unix time into seconds remaining and replies withMsg("Status", ...).clearremoves the entry and saves immediately. The handler resolves the argument withBasePlayer.FindAwakeOrSleepingso admins can use a display name or a Steam ID, and both branches check the permission before touching data.
Install it on your server
Save SleeperProtection.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/SleeperProtection.json(Carbon:carbon/configs/SleeperProtection.json). After editing, reload it:oxide.reload SleeperProtection c.reload SleeperProtection # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default sleeperprotection.admin c.grant group default sleeperprotection.admin # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Shrink the window for geared players
Scale the protection duration down when the disconnected player carried expensive gear, so naked sleepers keep full coverage.
Generate this versionBroadcast protection end
Notify nearby players when a sleeper's protection expires so hunting feels fair.
Generate this versionTeam-wide protection
Extend the timer whenever one member of a team disconnects, protecting the whole group.
Generate this version
FAQ
Does the sleeper become immune to NPC and environmental damage too?
No. The handler only cancels damage when the attacker resolves to a real player through hitInfo.InitiatorPlayer. Scientist NPCs, animals, fire, fall damage and decay still apply, which keeps sleeping players in a normal state and prevents the protection from turning into a free safe zone.
What happens to the timer if the server restarts?
Disconnect times are stored as Unix timestamps in oxide/data, and the plugin compares them against Facepunch.Math.Epoch.Current on load. A restart therefore preserves the remaining window, and any entry whose timestamp already passed is dropped during OnServerInitialized.
Can players lose protection by logging back in?
Yes, by design. OnPlayerSleepEnded clears the record, so a player who wakes up or reconnects is treated as a normal active target. This also stops repeated login checks from keeping a stale protection entry alive.
Does this work on Carbon?
Yes. Carbon loads standard Oxide plugins, including this file, without changes. The permission registered in Init and the data file path both behave the same on both frameworks.
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 pvp plugins
- Combat blockTracks combat with the OnEntityTakeDamage hook and blocks a configurable list of chat commands while a player is in combat, using OnPlayerCommand toโฆOnEntityTakeDamageOnPlayerCommand
- DuelsAdds a player-vs-player duel system where /duel <player> sends a challenge, /duel accept starts it, and damage outside the duel pair is blocked.OnEntityTakeDamageOnEntityDeathOnPlayerDisconnected
- Friendly fire protectionBlocks damage between members of the same Rust team using OnEntityTakeDamage, with a permission whitelist and a /ff toggle for team leaders thatโฆOnEntityTakeDamageOnPlayerDisconnected
Last updated Oct 26, 2026.
Customize sleeper protection
Customize