Skip to content
RustPluginGenerator

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

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

SleeperProtection.json
{
  "Protection seconds": 300,
  "Block looting of protected sleepers": true,
  "Block explosion damage": true,
  "Admins bypass loot block": true
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/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
Permissions
PermissionGrants
sleeperprotection.adminGrants access to the /sleepers status and /sleepers clear commands.

How the code works

  1. Start the timer on disconnect

    The OnPlayerDisconnected hook receives the leaving player and the disconnect reason. The plugin ignores NPCs and players that are not connected, then stores the current Unix time from Facepunch.Math.Epoch.Current keyed by the player's UserIDString. The protection window ends at now + config.ProtectionSeconds. Nothing is written back to the player entity, so the body stays a normal sleeper.

  2. Cancel protection on reconnect

    OnPlayerSleepEnded fires 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.

  3. Block damage to protected sleepers

    OnEntityTakeDamage is declared with the BaseCombatEntity parameter type so it fires for players and all other combat entities. The handler returns early when the entity is not a BasePlayer, 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 if BlockExplosions is false.

  4. Deny looting when configured

    CanLootPlayer is called with the looter and the target player. When BlockLooting is on and the target still has a valid protection entry, the hook sends a chat message to the looter through the language file and returns false to abort the loot. Returning null or true keeps vanilla behavior. Admins with the sleeperprotection.admin permission bypass the loot block so they can inspect bodies during a ticket.

  5. Handle permission and data lifecycle

    Init registers sleeperprotection.admin. OnServerInitialized loads oxide/data/SleeperProtection.json with Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, int>>, dropping entries whose timestamps have already expired. Unload writes the dictionary back and logs the remaining count, so a restart during an active protection window does not turn a sleeper into fresh meat.

  6. Admin commands

    The /sleepers chat command is split into status and clear. status converts the stored Unix time into seconds remaining and replies with Msg("Status", ...). clear removes the entry and saves immediately. The handler resolves the argument with BasePlayer.FindAwakeOrSleeping so admins can use a display name or a Steam ID, and both branches check the permission before touching data.

Install it on your server

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

    Give it to every player, or swap default for a group such as vip: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 version
  • Broadcast protection end

    Notify nearby players when a sleeper's protection expires so hunting feels fair.

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

Prompt: Write an Oxide plugin called SleeperProtection that makes sleeping players immune to player damage for a configurable number of seconds after disconnect, optionally blocks looting, persists timestamps in oxide/data, and adds an admin /sleepers status and clear 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 sleeper protection

Customize