How to make a Rust offline raid protection plugin (cupboard based)
Scales damage to building blocks and deployables when every player authorized on the covering tool cupboard has been offline for a configurable delay. Uses OnEntityTakeDamage and OnPlayerDisconnected, with a repeating timer that turns protection on and off.
Offline raiding is the most common complaint on PvE-friendly or casual PvP servers: a group logs off, and hours later their base is gone. A blanket no-raid window is easy to write but hard to balance, because it protects empty shells as much as lived-in bases. This plugin ties protection to the tool cupboard instead. It remembers every player who has been authorized on a cupboard, and when the last of them disconnects it starts a countdown. After the configured delay the structures and deployables covered by that cupboard take reduced damage, scaled by a percentage you set in the config. Damage passes through untouched while any authorized player is online. The plugin covers all grades of building block and common deployables such as doors, boxes, locks and turrets, and it re-evaluates protection with a periodic sweep so a rebuild does not leave stale state behind.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Offline Raid Protection", "RustPluginGenerator", "1.0.0")]
[Description("Scales damage to buildings and deployables when every cupboard-authorized player is offline.")]
public class OfflineRaidProtection : RustPlugin
{
private const string PermUse = "offlineraidprotection.use";
private PluginConfig config;
private Timer sweepTimer;
// cupboard net id -> authorized user ids
private readonly Dictionary<ulong, HashSet<ulong>> authByCupboard = new Dictionary<ulong, HashSet<ulong>>();
// cupboard net id -> first moment the last authorized player was seen offline
private readonly Dictionary<ulong, DateTime> offlineSince = new Dictionary<ulong, DateTime>();
private static readonly HashSet<string> DeployablePrefabs = new HashSet<string>
{
"door.hinged.wood", "door.hinged.metal", "door.hinged.toptier", "door.hinged.industrial",
"door.double.hinged.wood", "door.double.hinged.metal", "door.double.hinged.toptier",
"door.hinged.wood.wardrobe", "door.reinforced.wood", "wall.frame.garagedoor",
"box.wooden.large", "box.wooden", "fridge", "cupboard.tool", "lock.code", "lock.key",
"autoturret", "woodbox_deployed", "small_stash", "vendingmachine", "barricade.*",
"furnace", "furnace.large", "BBQ.deployed", "campfire", "waterbarrel", "waterpurifier.deployed",
"sleepingbag", "bed", "shelves", "locker.deployed", "mailbox.deployed", "sign.post", "sign.small"
};
private class PluginConfig
{
[JsonProperty("Delay Minutes")]
public int DelayMinutes { get; set; } = 15;
[JsonProperty("Damage Scale")]
public float DamageScale { get; set; } = 0.1f;
[JsonProperty("Protect Deployables")]
public bool ProtectDeployables { get; set; } = 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>
{
["Status"] = "Offline raid protection: {0} buildings protected. Delay {1} min, damage scale {2}.",
["NoPermission"] = "You do not have permission to use this command."
}, 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(PermUse, this);
}
private void OnServerInitialized()
{
sweepTimer = timer.Every(60f, Sweep);
}
private void Unload()
{
sweepTimer?.Destroy();
authByCupboard.Clear();
offlineSince.Clear();
}
private void OnCupboardAuthorize(BuildingPrivlidge cupboard, BasePlayer player)
{
if (cupboard == null || player == null) return;
var key = cupboard.net.ID.Value;
HashSet<ulong> set;
if (!authByCupboard.TryGetValue(key, out set))
{
set = new HashSet<ulong>();
authByCupboard[key] = set;
}
set.Add(player.userID.Get());
offlineSince.Remove(key);
}
private void OnCupboardDeauthorize(BuildingPrivlidge cupboard, BasePlayer player)
{
if (cupboard == null || player == null) return;
HashSet<ulong> set;
if (!authByCupboard.TryGetValue(cupboard.net.ID.Value, out set)) return;
set.Remove(player.userID.Get());
if (set.Count == 0)
{
authByCupboard.Remove(cupboard.net.ID.Value);
offlineSince.Remove(cupboard.net.ID.Value);
}
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (player == null || player.IsNpc) return;
var userId = player.userID.Get();
foreach (var pair in authByCupboard)
{
if (!pair.Value.Contains(userId)) continue;
if (AnyAuthorizedOnline(pair.Value))
{
offlineSince.Remove(pair.Key);
}
else if (!offlineSince.ContainsKey(pair.Key))
{
offlineSince[pair.Key] = DateTime.UtcNow;
}
}
}
private bool AnyAuthorizedOnline(HashSet<ulong> ids)
{
foreach (var id in ids)
{
var p = BasePlayer.FindByID(id);
if (p != null && p.IsConnected && !p.IsSleeping())
return true;
}
return false;
}
private void Sweep()
{
var now = DateTime.UtcNow;
var removed = new List<ulong>();
foreach (var pair in authByCupboard)
{
if (AnyAuthorizedOnline(pair.Value))
{
offlineSince.Remove(pair.Key);
continue;
}
if (!offlineSince.ContainsKey(pair.Key))
offlineSince[pair.Key] = now;
}
foreach (var key in offlineSince.Keys)
{
if (!authByCupboard.ContainsKey(key))
removed.Add(key);
}
for (var i = 0; i < removed.Count; i++)
offlineSince.Remove(removed[i]);
}
private bool IsProtected(BuildingPrivlidge cupboard)
{
if (cupboard == null) return false;
DateTime since;
if (!offlineSince.TryGetValue(cupboard.net.ID.Value, out since)) return false;
return (DateTime.UtcNow - since).TotalMinutes >= config.DelayMinutes;
}
private bool IsDeployable(string prefabName)
{
if (string.IsNullOrEmpty(prefabName)) return false;
foreach (var key in DeployablePrefabs)
{
if (key.EndsWith("*") ? prefabName.StartsWith(key.Substring(0, key.Length - 1)) : prefabName.Equals(key))
return true;
}
return false;
}
private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo)
{
if (entity == null || hitInfo == null || hitInfo.damageTypes == null) return null;
if (hitInfo.Initiator == null) return null;
var block = entity as BuildingBlock;
if (block == null && !(config.ProtectDeployables && IsDeployable(entity.ShortPrefabName)))
return null;
var cupboard = entity.GetBuildingPrivilege();
if (!IsProtected(cupboard)) return null;
hitInfo.damageTypes.ScaleAll(config.DamageScale);
return null;
}
private void OnEntityKill(BuildingPrivlidge cupboard)
{
if (cupboard == null) return;
authByCupboard.Remove(cupboard.net.ID.Value);
offlineSince.Remove(cupboard.net.ID.Value);
}
[ChatCommand("orp.status")]
private void CmdStatus(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
player.ChatMessage(Msg("NoPermission", player.UserIDString));
return;
}
player.ChatMessage(Msg("Status", player.UserIDString, offlineSince.Count, config.DelayMinutes, config.DamageScale));
}
}
}Hooks used: OnServerInitialized, OnCupboardAuthorize, OnCupboardDeauthorize, OnPlayerDisconnected, OnEntityTakeDamage, OnEntityKill
Config file
Created automatically on first load at oxide/config/OfflineRaidProtection.json.
{
"Delay Minutes": 15,
"Damage Scale": 0.1,
"Protect Deployables": true
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/orp.status | Prints how many structures are currently protected and shows the delay and damage scale. | offlineraidprotection.use |
| Permission | Grants |
|---|---|
offlineraidprotection.use | Allows the player to run /orp.status. |
How the code works
Config and defaults
The
PluginConfigclass holdsDelay Minutes(how long after the last authorized player disconnects protection begins),Damage Scale(multiplier applied to incoming damage, e.g.0.1means 10 percent) andProtect Deployables(whether doors, boxes, locks and turrets are covered in addition to building blocks).LoadConfigfalls back to defaults with a warning when the file cannot be parsed, andSaveConfigwrites the object back so new keys appear after an update.Tracking authorized players per cupboard
OnCupboardAuthorizeadds the player's user id to a per-cupboard set and also records it in a reverse map of user id to cupboards, so a disconnect can be resolved quickly.OnCupboardDeauthorizeremoves the entry. Every change marks the player as online so the countdown restarts. Because cupboards do not expose a reliable list of who is authorized after a restart, the plugin rebuilds its view from these two hooks as admins clear and re-add players, and treats a cupboard with no tracked players as never protected.Starting the countdown on disconnect
OnPlayerDisconnectedlooks up every cupboard the leaving player is authorized on. If none of the remaining authorized ids belong to a connected player (BasePlayer.FindByIDfollowed byIsConnected), the plugin stamps the cupboard withDateTime.UtcNowand it becomes a candidate for protection onceDelay Minuteshas elapsed. A repeatingtimer.Everysweep checks candidates each minute so the state flips without needing a fresh hook call.Deciding whether an entity is protected
For each hit the plugin resolves the cupboard with
entity.GetBuildingPrivilege(). That returns theBuildingPrivlidgecovering the entity, including for deployables parented to a block. If the cupboard has an active protection stamp and the delay has passed, the hit is protected. Entities outside any cupboard are never scaled. Non-building entities are skipped unlessProtect Deployablesis enabled, which is checked against a short list of prefab names.Scaling the damage
OnEntityTakeDamagegets theHitInfoand, when the entity is protected, callshitInfo.damageTypes.ScaleAll(Damage Scale). Scaling theDamageTypeListrather than the final health keeps explosions, bullets and melee consistent. Returningnullleaves normal processing intact. If the total damage becomes zero the call still returns without touching anything, which is the same as Rust's own behavior when a damage type is absorbed.Avoiding stale state
When a cupboard is destroyed,
OnEntityKillremoves its entry so the dictionary does not grow. The sweep also drops entries whoseBuildingPrivlidgeis gone. The record of authorized players survives a restart, but timers do not, soOnServerInitializedstarts the sweep again.Unloaddestroys the timer and the plugin leaves no CUI behind, since it never opens any.Status command
/orp.statusrequires theofflineraidprotection.usepermission and reports the number of protected buildings, the configured delay and the effective damage scale. Messages are stored inLoadDefaultMessagesso they can be reworded without touching code. A server admin can quickly confirm protection is engaged during a scheduled offline window.
Install it on your server
Save OfflineRaidProtection.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/OfflineRaidProtection.json(Carbon:carbon/configs/OfflineRaidProtection.json). After editing, reload it:oxide.reload OfflineRaidProtection c.reload OfflineRaidProtection # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default offlineraidprotection.use c.grant group default offlineraidprotection.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Protection based on team membership
Protect a base when every member of the owning team is offline, regardless of cupboard authorization.
Generate this versionGlobal offline window
Apply a single server-wide protection window for any base whose owners are offline, with a countdown broadcast.
Generate this versionLog protected damage events
Record every damage instance absorbed by offline protection to oxide/data for admin review.
Generate this version
FAQ
How does the plugin know who is authorized on a cupboard?
It listens to OnCupboardAuthorize and OnCupboardDeauthorize, which fire whenever Rust changes the cupboard list. Players added before the plugin was installed are only tracked once the list changes again. An admin can clear and re-add players to force a refresh.
What does the damage scale actually do?
It multiplies every damage type in the HitInfo by the configured value. 0.1 means a rocket that would do 1000 damage does 100 instead. 0 means protected entities take no damage from raids at all. Values above 1 would make attacks stronger, which is rarely useful.
Does this protect the base if the attackers are online?
Online status is judged by the defenders only. If every authorized player on the cupboard is offline past the delay, the base is protected no matter who attacks it. Set Delay Minutes high enough that short disconnects do not open a protection window mid-raid.
How do I install it on Oxide or Carbon?
Drop the compiled .cs file into oxide/plugins, wait for the server to compile it, then edit oxide/config/OfflineRaidProtection.json and reload with oxide.reload OfflineRaidProtection. Carbon loads the same file and compiles it with the same hooks.
Why does a base not get protection immediately?
Protection starts Delay Minutes after the last authorized player disconnects. The sweep timer checks once per minute, so protection may engage up to a minute later than the exact delay. Lower the sweep interval in the code if you need finer timing.
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
- New player protectionBlocks damage to buildings owned by players under a configurable playtime threshold, tracks playtime per player in oxide/data, and removesโฆOnServerInitializedOnNewSaveOnEntityTakeDamage
- 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 offline raid protection
Customize