Skip to content
RustPluginGenerator

How to make a Rust new player protection plugin (raid protection)

Blocks damage to buildings owned by players under a configurable playtime threshold, tracks playtime per player in oxide/data, and removes protection early when the protected player damages another player's base. Uses OnEntityTakeDamage and OnNewSave.

Freshly wiped servers put new players next to groups that already have rockets. A playtime-based raid shield gives newcomers a few hours to build before their base can be touched, without handing veterans the same grace period.

This plugin accumulates online time per player in a data file and, in OnEntityTakeDamage, checks the owner of the building being hit. If that owner is still under Protection Hours, damage from anyone else is cancelled. Protection is dropped immediately when the attacker (a shielded player) deals damage to another player's building, so a shielded player cannot raid while staying immune. OnNewSave wipes the stored playtime so the clock starts over with the map. Ownership comes from the entity's OwnerID, which is the player who placed the block, so only the builder's own structures are shielded.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("New Player Protection", "RustPluginGenerator", "1.0.0")]
    [Description("Blocks damage to buildings owned by players under a playtime threshold this wipe.")]
    public class NewPlayerProtection : RustPlugin
    {
        private const string PermAdmin = "newplayerprotection.admin";

        private PluginConfig config;
        private ProtectionData data;
        private Timer tickTimer;
        private Timer saveTimer;
        private bool dirty;

        private class PluginConfig
        {
            [JsonProperty("Protection Hours")]
            public double ProtectionHours { get; set; } = 12.0;

            [JsonProperty("Protect Deployables")]
            public bool ProtectDeployables { get; set; } = true;

            [JsonProperty("Tick Seconds")]
            public float TickSeconds { get; set; } = 60f;

            [JsonProperty("Save Interval")]
            public float SaveInterval { get; set; } = 120f;

            [JsonProperty("Wipe Seed")]
            public string WipeSeed { get; set; } = string.Empty;

            [JsonProperty("Playtime Minutes")]
            public Dictionary<string, double> Playtime { get; set; } = new Dictionary<string, double>();

            [JsonProperty("Protection Broken")]
            public HashSet<string> Broken { get; set; } = new HashSet<string>();
        }

        private class ProtectionData
        {
            public string WipeSeed { get; set; } = string.Empty;
            public Dictionary<string, double> Playtime { get; set; } = new Dictionary<string, double>();
            public HashSet<string> Broken { get; set; } = new HashSet<string>();

            public ProtectionData() { }

            public ProtectionData(string wipeSeed, Dictionary<string, double> playtime, HashSet<string> broken)
            {
                WipeSeed = wipeSeed ?? string.Empty;
                Playtime = playtime ?? new Dictionary<string, double>();
                Broken = broken ?? new HashSet<string>();
            }
        }

        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>
            {
                ["Check"] = "{0} has {1} minutes tracked and is {2}.",
                ["Protected"] = "protected",
                ["Unprotected"] = "not protected",
                ["End"] = "Protection removed for {0}.",
                ["NotFound"] = "No player found with that name or ID.",
                ["Status"] = "{0} of {1} known players are currently protected. Threshold: {2} hours.",
                ["NoPermission"] = "You do not have permission to use this command.",
                ["Usage"] = "Usage: /npp check <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()
        {
            LoadData();
            tickTimer = timer.Every(config.TickSeconds, TickPlaytime);
            saveTimer = timer.Every(config.SaveInterval, Flush);
        }

        private void Unload()
        {
            tickTimer?.Destroy();
            saveTimer?.Destroy();
            Flush();
        }

        private void LoadData()
        {
            try
            {
                data = Interface.Oxide.DataFileSystem.ReadObject<ProtectionData>(Name);
            }
            catch
            {
                data = null;
            }

            if (data == null)
            {
                data = new ProtectionData(config.WipeSeed, config.Playtime, config.Broken);
            }

            if (data.Playtime == null) data.Playtime = new Dictionary<string, double>();
            if (data.Broken == null) data.Broken = new HashSet<string>();
            config.WipeSeed = data.WipeSeed;
            config.Playtime = data.Playtime;
            config.Broken = data.Broken;
        }

        private void Flush()
        {
            if (!dirty || data == null) return;
            config.WipeSeed = data.WipeSeed;
            config.Playtime = data.Playtime;
            config.Broken = data.Broken;
            Interface.Oxide.DataFileSystem.WriteObject(Name, data);
            SaveConfig();
            dirty = false;
        }

        private void TickPlaytime()
        {
            double minutes = config.TickSeconds / 60.0;
            foreach (var player in BasePlayer.activePlayerList)
            {
                if (player == null || player.IsNpc || !player.IsConnected || player.IsSleeping()) continue;
                string id = player.UserIDString;
                data.Playtime.TryGetValue(id, out double current);
                data.Playtime[id] = current + minutes;
            }
            dirty = true;
        }

        private void OnNewSave(string strFilename)
        {
            data = new ProtectionData { WipeSeed = strFilename ?? string.Empty };
            dirty = true;
            Flush();
        }

        private bool IsTracked(string userId) => data.Playtime.TryGetValue(userId, out double minutes) && minutes < config.ProtectionHours * 60.0;

        private bool IsProtected(BasePlayer player)
        {
            if (player == null || player.IsNpc) return false;
            string id = player.UserIDString;
            if (data.Broken.Contains(id)) return false;
            return IsTracked(id);
        }

        private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo)
        {
            if (entity == null || hitInfo == null) return null;
            var attacker = hitInfo.InitiatorPlayer;
            if (attacker == null || attacker.IsNpc) return null;

            ulong ownerId = entity.OwnerID;
            if (ownerId == 0) return null;

            // Same player or teammate: no interference with self-damage or team repairs.
            if (ownerId == attacker.userID.Get()) return null;
            if (attacker.currentTeam != 0 && data.Playtime.ContainsKey(ownerId.ToString()))
            {
                var team = RelationshipManager.ServerInstance.FindTeam(attacker.currentTeam);
                if (team != null && team.members.Contains(ownerId)) return null;
            }

            var owner = BasePlayer.FindByID(ownerId);
            string ownerKey = ownerId.ToString();

            // If the victim is a protected builder, cancel the hit.
            if (IsShieldedEntity(entity) && !data.Broken.Contains(ownerKey) && IsTracked(ownerKey))
            {
                if (IsProtected(owner) || owner == null)
                {
                    hitInfo.damageTypes.ScaleAll(0f);
                    return true;
                }
            }

            // If the attacker is protected and hits someone else's structure, drop their shield.
            if (IsShieldedEntity(entity) && !data.Broken.Contains(attacker.UserIDString) && IsTracked(attacker.UserIDString))
            {
                if (owner != null && owner.UserIDString != attacker.UserIDString)
                {
                    data.Broken.Add(attacker.UserIDString);
                    dirty = true;
                }
            }

            return null;
        }

        private bool IsShieldedEntity(BaseCombatEntity entity)
        {
            if (entity is BuildingBlock) return true;
            if (!config.ProtectDeployables) return false;
            if (entity is Door) return true;
            if (entity is StorageContainer) return true;
            if (entity is CodeLock) return true;
            if (entity is BuildingPrivlidge) return true;
            return false;
        }

        [ChatCommand("npp")]
        private void CmdNpp(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
            {
                SendReply(player, Msg("NoPermission", player.UserIDString));
                return;
            }

            if (args == null || args.Length < 2)
            {
                SendReply(player, Msg("Usage", player.UserIDString));
                return;
            }

            string sub = args[0].ToLower();
            string target = args[1];
            var found = BasePlayer.FindAwakeOrSleeping(target);
            if (found == null)
            {
                SendReply(player, Msg("NotFound", player.UserIDString));
                return;
            }

            string id = found.UserIDString;
            if (sub == "check")
            {
                data.Playtime.TryGetValue(id, out double minutes);
                bool protectedNow = !data.Broken.Contains(id) && minutes < config.ProtectionHours * 60.0;
                SendReply(player, Msg("Check", player.UserIDString, found.displayName, Math.Round(minutes, 1), Msg(protectedNow ? "Protected" : "Unprotected", player.UserIDString)));
            }
            else if (sub == "end")
            {
                data.Broken.Add(id);
                dirty = true;
                Flush();
                SendReply(player, Msg("End", player.UserIDString, found.displayName));
            }
            else
            {
                SendReply(player, Msg("Usage", player.UserIDString));
            }
        }

        [ConsoleCommand("npp.status")]
        private void CcmdStatus(ConsoleSystem.Arg arg)
        {
            if (arg == null) return;
            var caller = arg.Player();
            if (caller != null && !permission.UserHasPermission(caller.UserIDString, PermAdmin))
            {
                arg.ReplyWith(Msg("NoPermission", caller.UserIDString));
                return;
            }

            int protectedCount = 0;
            foreach (var kvp in data.Playtime)
            {
                if (!data.Broken.Contains(kvp.Key) && kvp.Value < config.ProtectionHours * 60.0) protectedCount++;
            }

            arg.ReplyWith(Msg("Status", caller?.UserIDString, protectedCount, data.Playtime.Count, config.ProtectionHours));
        }
    }
}

Hooks used: OnServerInitialized, OnNewSave, OnEntityTakeDamage

Config file

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

NewPlayerProtection.json
{
  "Protection Hours": 12,
  "Protect Deployables": true,
  "Tick Seconds": 60,
  "Save Interval": 120,
  "Wipe Seed": "",
  "Playtime Minutes": {},
  "Protection Broken": []
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/npp check <name or steamid>Shows a player's tracked playtime and whether their buildings are still protected.newplayerprotection.admin
/npp end <name or steamid>Removes a player's protection immediately without touching their playtime.newplayerprotection.admin
npp.statusPrints how many players are currently protected and the configured threshold.newplayerprotection.admin
Permissions
PermissionGrants
newplayerprotection.adminGrants access to /npp check, /npp end and npp.status.

How the code works

  1. Track playtime with a repeating timer

    A Timer created in OnServerInitialized fires every Tick Seconds and loops over BasePlayer.activePlayerList. For each connected, non-NPC player it adds Tick Seconds / 60.0 minutes to data.Playtime[player.UserIDString]. Sleeping players do not accumulate time. The timer reference is kept in a field and destroyed in Unload, and the dictionary is written to oxide/data/NewPlayerProtection with Interface.Oxide.DataFileSystem.WriteObject. Config key Save Interval controls a second timer that flushes the data, so writes happen every couple of minutes instead of on every tick.

  2. Check the owner in OnEntityTakeDamage

    object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo) receives every damage event. The plugin first rejects events with a null entity, hitInfo or missing InitiatorPlayer, then reads entity.OwnerID. If the attacker is the owner (ownerId == attacker.userID.Get()) or a member of the attacker's team (RelationshipManager.ServerInstance.FindTeam), the hook returns null so self-damage and teammate repairs work normally. When a hit is cancelled the method calls hitInfo.damageTypes.ScaleAll(0f) and returns true.

  3. Only protect building blocks and deployables

    Shielding every entity would block damage to a protected player's loot bags, vehicles and player body. IsShieldedEntity returns true for BuildingBlock, and when Protect Deployables is on it also covers Door, StorageContainer, CodeLock and BuildingPrivlidge. Everything else is ignored. Because OwnerID is inherited from the deployer for most placeables, a door placed by the protected player is covered, while a vehicle they stole keeps the original owner's ID.

  4. Break protection when the player raids

    After the shield branch, the same hook checks the attacker. If the attacker is still tracked, hits a shielded entity owned by someone else (owner != null && owner.UserIDString != attacker.UserIDString), their ID is added to data.Broken. IsProtected returns false for any ID in that set, and CmdNpp with end uses the same set to end protection manually. Only shielded entity types count, so hitting an unowned barrel or a wild animal does not strip the shield.

  5. Reset on wipe and expose player state

    void OnNewSave(string strFilename) replaces data with a fresh ProtectionData whose WipeSeed holds the filename, clears the dictionaries and flushes. Admins use /npp check <name or steamid> to resolve a player with BasePlayer.FindAwakeOrSleeping, print stored minutes and whether the entry is in data.Broken or under threshold. /npp end adds the ID to data.Broken, which removes protection without editing playtime. All replies are formatted through the language file with the Msg helper.

  6. Read and write the data safely

    ProtectionData holds Playtime as a Dictionary<string, double> and Broken as a HashSet<string>, plus a WipeSeed string. It is loaded in OnServerInitialized with ReadObject<ProtectionData> and created when the file is missing or unreadable. The plugin never writes to disk inside OnEntityTakeDamage; it only sets dirty = true and lets the save timer call Flush. The config class uses [JsonProperty] attributes so the generated config file stays readable.

Install it on your server

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

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

Variations

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

  • Raid-only grace period

    Keep explosion damage blocked but allow bullets and melee at all times.

    Generate this version
  • Warn the attacker

    Send a chat message to the raider explaining why the hit did nothing.

    Generate this version
  • Permission-based exemption

    Let a VIP permission bypass protection entirely.

    Generate this version

FAQ

Why does the shield not follow a player who builds with a friend's materials?

Protection is keyed to OwnerID, which is the player who placed the building block. If a second player places the wall, that wall belongs to them and is evaluated against their own playtime. Only structures placed by the protected player are covered.

Does damage from NPCs or the environment get blocked too?

No. The hook returns early when hitInfo.InitiatorPlayer is null, so decay, animals, Bradley and Patrol Helicopter damage pass through unchanged. The shield only stops damage credited to another player.

How is playtime counted for players who log off?

The timer only adds time to players in BasePlayer.activePlayerList that are connected and not asleep. Time spent offline or in the sleeping state does not count, so a player cannot idle their way past protection.

Does the data file grow without limit?

The playtime dictionary keeps one entry per player who joined this wipe. OnNewSave clears it at wipe time. If your file gets large, add a periodic cleanup that drops entries for IDs that no longer exist, but most servers never need it.

How do I install it on Oxide or Carbon?

Place NewPlayerProtection.cs in oxide/plugins, run oxide.reload NewPlayerProtection from the server console, then edit oxide/config/NewPlayerProtection.json and reload again. Carbon loads the same file; drop it in carbon/plugins instead.

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 that blocks damage to buildings owned by players with less than 12 hours online this wipe, tracks playtime in oxide/data, removes protection when the owner damages another player's base, and resets on new save.
Customize for my server

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

Last updated Oct 26, 2026.

Customize new player protection

Customize