How to make a Rust raid schedule plugin (raid hours)
Blocks damage from players to player-built structures and deployables outside configured daily raid windows, using the OnEntityTakeDamage hook and a /raid status command.
Most servers that want raid hours end up with either no enforcement or a PvE plugin that blocks all damage all day. This recipe takes a narrower approach: only damage that comes from a player, against entities owned by another player, is gated. A hit is allowed when the entity is a BuildingBlock, or when it carries a non-zero OwnerID and was built or deployed by a player.
The plugin keeps a list of daily windows in its config (start and end in HH:mm, plus an optional list of day names). On every damage event it converts the current server time to minutes and checks whether any window is open. If none is open, the hit is cancelled and the attacker gets a rate-limited chat notice. Outside hits from NPCs, the environment, decay or the server itself pass through untouched.
/raid prints the current state and the next opening time. raid.schedule lets an admin turn gating off, reload the config or check from the console.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Raid Schedule", "RustPluginGenerator", "1.0.0")]
[Description("Blocks player damage to player buildings and deployables outside configured daily raid windows.")]
public class RaidSchedule : RustPlugin
{
private const string PermUse = "raidschedule.use";
private const string PermAdmin = "raidschedule.admin";
private PluginConfig config;
private readonly Dictionary<ulong, float> lastNotice = new Dictionary<ulong, float>();
private class RaidWindow
{
[JsonProperty("Start (HH:mm)")] public string Start = "18:00";
[JsonProperty("End (HH:mm)")] public string End = "22:00";
[JsonProperty("Days (empty = every day)")] public string[] Days = new string[0];
}
private class PluginConfig
{
[JsonProperty("Enabled")] public bool Enabled = true;
[JsonProperty("Block deployables (doors, boxes, turrets)")] public bool BlockDeployables = true;
[JsonProperty("Notify the attacker")] public bool NotifyAttacker = true;
[JsonProperty("Notice cooldown (seconds)")] public float NotifyCooldownSeconds = 15f;
[JsonProperty("Raid windows")] public List<RaidWindow> Windows = new List<RaidWindow>();
}
private class ParsedWindow
{
public int Start;
public int End;
public bool Overnight;
public HashSet<DayOfWeek> Days; // null = every day
}
private readonly List<ParsedWindow> windows = new List<ParsedWindow>();
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();
ParseWindows();
}
protected override void SaveConfig() => Config.WriteObject(config);
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["ClosedChat"] = "Raiding is closed right now. Damage to player buildings is blocked. Next opening: {0}",
["OpenChat"] = "Raiding is open right now. Raid windows end at the configured time.",
["Disabled"] = "Raiding is currently disabled server-wide by an admin.",
["AttackerBlocked"] = "Raiding is closed. You cannot damage player structures right now.",
["Toggled"] = "Raid schedule gating is now {0}.",
["Reloaded"] = "Config reloaded. {0} raid window(s) active.",
["StatusConsole"] = "Raid schedule: {0} | Windows: {1}",
["NoPermission"] = "You do not have permission to use this command."
}, this);
}
private string Msg(string key, string userId = null, params object[] args)
{
return string.Format(lang.GetMessage(key, this, userId), args);
}
private void Init()
{
permission.RegisterPermission(PermUse, this);
permission.RegisterPermission(PermAdmin, this);
}
private void ParseWindows()
{
windows.Clear();
if (config.Windows == null) return;
foreach (var w in config.Windows)
{
int start, end;
if (!TryParseMinutes(w.Start, out start) || !TryParseMinutes(w.End, out end))
{
PrintWarning($"Skipping invalid raid window '{w.Start}' - '{w.End}'");
continue;
}
HashSet<DayOfWeek> days = null;
if (w.Days != null && w.Days.Length > 0)
{
days = new HashSet<DayOfWeek>();
foreach (var d in w.Days)
{
DayOfWeek parsed;
if (Enum.TryParse(d, true, out parsed)) days.Add(parsed);
}
if (days.Count == 0)
{
PrintWarning($"Raid window '{w.Start}' - '{w.End}' has no valid day names, treating as every day");
days = null;
}
}
windows.Add(new ParsedWindow
{
Start = start,
End = end,
Overnight = end < start,
Days = days
});
}
}
private static bool TryParseMinutes(string value, out int minutes)
{
minutes = 0;
if (string.IsNullOrEmpty(value)) return false;
var parts = value.Split(':');
if (parts.Length != 2) return false;
int h, m;
if (!int.TryParse(parts[0], out h) || !int.TryParse(parts[1], out m)) return false;
if (h < 0 || h > 23 || m < 0 || m > 59) return false;
minutes = h * 60 + m;
return true;
}
private bool IsRaidOpen()
{
return IsOpenAt(DateTime.Now);
}
private bool IsOpenAt(DateTime now)
{
int minute = now.Hour * 60 + now.Minute;
var today = now.DayOfWeek;
var yesterday = now.AddDays(-1).DayOfWeek;
foreach (var w in windows)
{
if (w.Overnight)
{
if (minute >= w.Start && DayMatches(w, today)) return true;
if (minute < w.End && DayMatches(w, yesterday)) return true;
}
else
{
if (minute >= w.Start && minute < w.End && DayMatches(w, today)) return true;
}
}
return false;
}
private static bool DayMatches(ParsedWindow w, DayOfWeek day)
{
return w.Days == null || w.Days.Contains(day);
}
private string NextOpening()
{
var now = DateTime.Now;
for (int i = 0; i <= 10080; i++)
{
var probe = now.AddMinutes(i);
if (IsOpenAt(probe)) return probe.ToString("ddd HH:mm");
}
return "-";
}
private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo)
{
if (!config.Enabled || entity == null || hitInfo == null) return null;
if (hitInfo.damageTypes == null) return null;
var attacker = hitInfo.InitiatorPlayer ?? hitInfo.Initiator as BasePlayer;
if (attacker == null) return null;
if (!IsRaidTarget(entity)) return null;
var ownerId = entity.OwnerID;
if (ownerId == attacker.userID.Get()) return null;
if (IsRaidOpen()) return null;
if (config.NotifyAttacker)
{
float now = Time.realtimeSinceStartup;
float last;
if (!lastNotice.TryGetValue(attacker.userID, out last) || now - last >= config.NotifyCooldownSeconds)
{
lastNotice[attacker.userID] = now;
attacker.ChatMessage(Msg("AttackerBlocked", attacker.UserIDString));
}
}
return true;
}
private bool IsRaidTarget(BaseCombatEntity entity)
{
if (entity is BuildingBlock) return true;
if (config.BlockDeployables && entity.OwnerID != 0) return true;
return false;
}
[ChatCommand("raid")]
private void CmdRaid(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
player.ChatMessage(Msg("NoPermission", player.UserIDString));
return;
}
if (!config.Enabled)
{
player.ChatMessage(Msg("Disabled", player.UserIDString));
return;
}
if (IsRaidOpen())
{
player.ChatMessage(Msg("OpenChat", player.UserIDString));
return;
}
player.ChatMessage(Msg("ClosedChat", player.UserIDString, NextOpening()));
}
[ConsoleCommand("raid.schedule")]
private void CcmdSchedule(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player != null && !permission.UserHasPermission(player.UserIDString, PermAdmin))
{
arg.ReplyWith(Msg("NoPermission"));
return;
}
var action = arg.GetString(0, "status").ToLowerInvariant();
switch (action)
{
case "toggle":
config.Enabled = !config.Enabled;
SaveConfig();
arg.ReplyWith(Msg("Toggled", null, config.Enabled ? "enabled" : "disabled"));
break;
case "reload":
LoadConfig();
arg.ReplyWith(Msg("Reloaded", null, windows.Count));
break;
default:
arg.ReplyWith(Msg("StatusConsole", null, config.Enabled ? (IsRaidOpen() ? "open" : "closed") : "disabled", windows.Count));
break;
}
}
}
}Hooks used: OnEntityTakeDamage
Config file
Created automatically on first load at oxide/config/RaidSchedule.json.
{
"Enabled": true,
"Block deployables (doors, boxes, turrets)": true,
"Notify the attacker": true,
"Notice cooldown (seconds)": 15,
"Raid windows": [
{
"Start (HH:mm)": "18:00",
"End (HH:mm)": "22:00",
"Days (empty = every day)": []
}
]
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/raid | Shows whether raiding is open right now, and the next open window if it is closed. | raidschedule.use |
raid.schedule [status|toggle|reload] | Console command: report state, flip the Enabled flag (saved to config) or reload the config and re-parse windows. | raidschedule.admin |
| Permission | Grants |
|---|---|
raidschedule.use | Grants access to the /raid status command. |
raidschedule.admin | Grants access to the raid.schedule console command (toggle, reload). |
How the code works
Config shape
The config class holds
Enabled,BlockDeployables,NotifyAttacker,NotifyCooldownSecondsand aWindowslist. Each window is aRaidWindowwithStart,Endand aDaysstring array; an empty array means every day. Overnight windows such as22:00to02:00are supported: if the parsed end minute is smaller than the start minute, the window is treated as crossing midnight and the day check applies to the start day.Parsing HH:mm strings
TryParseMinutes(string value, out int minutes)splits on:and usesint.TryParseon both halves, rejecting anything outside 0-23 for hours and 0-59 for minutes.ParseWindows()runs after the config loads, builds aParsedWindowfor each entry and skips invalid ones with a singlePrintWarning, so a typo in the config cannot spam the console during a raid. Day names are matched withEnum.TryParse(d, true, out parsed), which accepts any casing and both full and abbreviatedDayOfWeeknames.Is raiding open right now
IsRaidOpen()callsIsOpenAt(DateTime.Now), which uses server local time, the same clock admins expect fromHH:mm. It computesnow.Hour * 60 + now.Minuteand walks the parsed windows. For a normal window it checksminute >= Start && minute < EndandDayMatches. For an overnight window it checks the after-midnight part againsttoday.AddDays(-1)so the start day is still honoured.Deciding what counts as raid damage
OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo)returnsnullwhen gating is disabled, raiding is open, the attacker is not a player, or the entity belongs to the attacker. The attacker comes fromhitInfo.InitiatorPlayerwithhitInfo.Initiator as BasePlayeras fallback. Damage without a player initiator (decay, fire, fall, NPC fire) is never blocked.IsRaidTargetmatches everyBuildingBlock, and whenBlockDeployablesis on, any entity whoseOwnerIDis non-zero.Blocking the hit and telling the attacker
Returning a non-null value from the hook cancels the damage; the plugin returns
trueso secondary effects such as condition loss are not applied either. Before that, it looks up the attacker'suserIDin thelastNoticedictionary ofTime.realtimeSinceStartupvalues and sends theAttackerBlockedmessage only whenNotifyCooldownSecondshas elapsed, keeping chat clean during sustained fire.The /raid status command
[ChatCommand("raid")]checksraidschedule.use, then reports one of three states through the language file: disabled server-wide, open, or closed with the next opening.NextOpening()probesIsOpenAtminute by minute for up to 10080 minutes (seven days) and returns the first matching moment formatted asddd HH:mm, or-if nothing is configured.Admin console control
[ConsoleCommand("raid.schedule")]acceptsstatus(default),toggleandreload. When the argument comes from a player,raidschedule.adminis required; RCON and the server console always pass becausearg.Player()is null. Toggle flipsEnabledand callsSaveConfig()so it survives restarts; reload callsLoadConfig(), which re-parses the windows and reports how many are active.
Install it on your server
Save RaidSchedule.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/RaidSchedule.json(Carbon:carbon/configs/RaidSchedule.json). After editing, reload it:oxide.reload RaidSchedule c.reload RaidSchedule # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default raidschedule.use c.grant group default raidschedule.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Broadcast on open and close
Announce in global chat when a raid window starts and ends.
Generate this versionPer-grade damage multiplier
Allow partial building damage outside hours instead of a hard block.
Generate this versionDiscord webhook log
Report blocked raid attempts with attacker, owner and position.
Generate this version
FAQ
Does this stop decay, fire or NPC damage outside raid hours?
No. The hook only blocks damage whose initiator is a player. Decay, campfire and other environmental damage use non-player initiators, and NPC damage has InitiatorPlayer == null, so those paths return early and behave as vanilla.
Which time zone are the HH:mm windows in?
Server local time, read through DateTime.Now. Set the server's time zone at the OS level if you want the windows to match a region, or change the call to DateTime.UtcNow if you prefer UTC windows across a fleet.
Why does the plugin register two permissions?
raidschedule.use gates the /raid chat command so normal players can check status. raidschedule.admin gates raid.schedule toggling and reloading from the F1 console or RCON, so a regular player cannot disable raid hours.
Will it block damage on other players' own buildings (self-raid)?
No. The hook compares entity.OwnerID with the attacker's userID and returns early when they match, so a player can always damage their own build. Deployables with OwnerID == 0 (map objects) are never treated as raid targets.
How do I install it on Oxide or Carbon?
Drop RaidSchedule.cs into oxide/plugins/, wait for it to compile, then edit oxide/config/RaidSchedule.json to set your windows. Reload with oxide.reload RaidSchedule. Carbon loads the same file from its plugins folder without changes.
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
- 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
Last updated Oct 26, 2026.
Customize raid schedule
Customize