Skip to content
RustPluginGenerator

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

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

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

Chat and console commands
CommandWhat it doesPermission
/raidShows 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
Permissions
PermissionGrants
raidschedule.useGrants access to the /raid status command.
raidschedule.adminGrants access to the raid.schedule console command (toggle, reload).

How the code works

  1. Config shape

    The config class holds Enabled, BlockDeployables, NotifyAttacker, NotifyCooldownSeconds and a Windows list. Each window is a RaidWindow with Start, End and a Days string array; an empty array means every day. Overnight windows such as 22:00 to 02:00 are 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.

  2. Parsing HH:mm strings

    TryParseMinutes(string value, out int minutes) splits on : and uses int.TryParse on both halves, rejecting anything outside 0-23 for hours and 0-59 for minutes. ParseWindows() runs after the config loads, builds a ParsedWindow for each entry and skips invalid ones with a single PrintWarning, so a typo in the config cannot spam the console during a raid. Day names are matched with Enum.TryParse(d, true, out parsed), which accepts any casing and both full and abbreviated DayOfWeek names.

  3. Is raiding open right now

    IsRaidOpen() calls IsOpenAt(DateTime.Now), which uses server local time, the same clock admins expect from HH:mm. It computes now.Hour * 60 + now.Minute and walks the parsed windows. For a normal window it checks minute >= Start && minute < End and DayMatches. For an overnight window it checks the after-midnight part against today.AddDays(-1) so the start day is still honoured.

  4. Deciding what counts as raid damage

    OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo) returns null when gating is disabled, raiding is open, the attacker is not a player, or the entity belongs to the attacker. The attacker comes from hitInfo.InitiatorPlayer with hitInfo.Initiator as BasePlayer as fallback. Damage without a player initiator (decay, fire, fall, NPC fire) is never blocked. IsRaidTarget matches every BuildingBlock, and when BlockDeployables is on, any entity whose OwnerID is non-zero.

  5. Blocking the hit and telling the attacker

    Returning a non-null value from the hook cancels the damage; the plugin returns true so secondary effects such as condition loss are not applied either. Before that, it looks up the attacker's userID in the lastNotice dictionary of Time.realtimeSinceStartup values and sends the AttackerBlocked message only when NotifyCooldownSeconds has elapsed, keeping chat clean during sustained fire.

  6. The /raid status command

    [ChatCommand("raid")] checks raidschedule.use, then reports one of three states through the language file: disabled server-wide, open, or closed with the next opening. NextOpening() probes IsOpenAt minute by minute for up to 10080 minutes (seven days) and returns the first matching moment formatted as ddd HH:mm, or - if nothing is configured.

  7. Admin console control

    [ConsoleCommand("raid.schedule")] accepts status (default), toggle and reload. When the argument comes from a player, raidschedule.admin is required; RCON and the server console always pass because arg.Player() is null. Toggle flips Enabled and calls SaveConfig() so it survives restarts; reload calls LoadConfig(), which re-parses the windows and reports how many are active.

Install it on your server

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

    Give it to every player, or swap default for a group such as vip: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 version
  • Per-grade damage multiplier

    Allow partial building damage outside hours instead of a hard block.

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

Prompt: Create an Oxide plugin called RaidSchedule that blocks player damage to player-built structures and deployables outside daily HH:mm raid windows from config, with a /raid status command and raid.schedule admin console 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 raid schedule

Customize