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
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.
{
"Protection Hours": 12,
"Protect Deployables": true,
"Tick Seconds": 60,
"Save Interval": 120,
"Wipe Seed": "",
"Playtime Minutes": {},
"Protection Broken": []
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/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.status | Prints how many players are currently protected and the configured threshold. | newplayerprotection.admin |
| Permission | Grants |
|---|---|
newplayerprotection.admin | Grants access to /npp check, /npp end and npp.status. |
How the code works
Track playtime with a repeating timer
A
Timercreated inOnServerInitializedfires everyTick Secondsand loops overBasePlayer.activePlayerList. For each connected, non-NPC player it addsTick Seconds / 60.0minutes todata.Playtime[player.UserIDString]. Sleeping players do not accumulate time. The timer reference is kept in a field and destroyed inUnload, and the dictionary is written tooxide/data/NewPlayerProtectionwithInterface.Oxide.DataFileSystem.WriteObject. Config keySave Intervalcontrols a second timer that flushes the data, so writes happen every couple of minutes instead of on every tick.Check the owner in OnEntityTakeDamage
object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo)receives every damage event. The plugin first rejects events with a nullentity,hitInfoor missingInitiatorPlayer, then readsentity.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 callshitInfo.damageTypes.ScaleAll(0f)and returns true.Only protect building blocks and deployables
Shielding every entity would block damage to a protected player's loot bags, vehicles and player body.
IsShieldedEntityreturns true forBuildingBlock, and whenProtect Deployablesis on it also coversDoor,StorageContainer,CodeLockandBuildingPrivlidge. Everything else is ignored. BecauseOwnerIDis 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.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 todata.Broken.IsProtectedreturns false for any ID in that set, andCmdNppwithenduses 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.Reset on wipe and expose player state
void OnNewSave(string strFilename)replacesdatawith a freshProtectionDatawhoseWipeSeedholds the filename, clears the dictionaries and flushes. Admins use/npp check <name or steamid>to resolve a player withBasePlayer.FindAwakeOrSleeping, print stored minutes and whether the entry is indata.Brokenor under threshold./npp endadds the ID todata.Broken, which removes protection without editing playtime. All replies are formatted through the language file with theMsghelper.Read and write the data safely
ProtectionDataholdsPlaytimeas aDictionary<string, double>andBrokenas aHashSet<string>, plus aWipeSeedstring. It is loaded inOnServerInitializedwithReadObject<ProtectionData>and created when the file is missing or unreadable. The plugin never writes to disk insideOnEntityTakeDamage; it only setsdirty = trueand lets the save timer callFlush. The config class uses[JsonProperty]attributes so the generated config file stays readable.
Install it on your server
Save NewPlayerProtection.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/NewPlayerProtection.json(Carbon:carbon/configs/NewPlayerProtection.json). After editing, reload it:oxide.reload NewPlayerProtection c.reload NewPlayerProtection # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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 versionWarn the attacker
Send a chat message to the raider explaining why the hit did nothing.
Generate this versionPermission-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.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More raiding plugins
- Explosive damage multiplierScales explosion damage against building blocks, deployables and other structures using per-weapon multipliers keyed by the attacker's weapon prefabโฆOnEntityTakeDamage
- Offline raid protectionScales damage to building blocks and deployables when every player authorized on the covering tool cupboard has been offline for a configurable delay.OnServerInitializedOnCupboardAuthorizeOnCupboardDeauthorize
- Raid alertsChat-alerts online owners and tool cupboard authed players when explosives damage their building, with the attacker name and map grid, throttled toโฆOnServerInitializedOnEntityTakeDamage
Last updated Oct 26, 2026.
Customize new player protection
Customize