Skip to content
RustPluginGenerator

How to make a Rust PvE raid hours plugin (weekly raid windows)

Blocks player-versus-player damage and player damage to other players' buildings outside configured weekly raid windows, using OnEntityTakeDamage plus a /raidtime command that reports the next opening.

Most PvE servers are peaceful by default but still want a scheduled window where raiding and player combat are allowed. Writing that by hand means deciding, on every damage event, whether the attacker is a player, whether the victim belongs to someone else, and whether the current server time falls inside any configured window.

This plugin does that in one place. OnEntityTakeDamage returns a non-null value to cancel damage when the attacker is a player and either the victim is another player or the victim is a building owned by a different player. Windows are defined in the config as a day of week plus a start and end hour in server time, so they recur every week without any manual toggling. /raidtime prints the next window and its distance from now, and the console command does the same for RCON.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("PveRaidHours", "RustPluginGenerator", "1.0.0")]
    [Description("Blocks player and building damage outside configured weekly raid windows.")]
    public class PveRaidHours : RustPlugin
    {
        private const string PermAdmin = "pveraidhours.admin";

        private PluginConfig config;

        private class Window
        {
            [JsonProperty("Day")]
            public string Day = "Saturday";
            [JsonProperty("StartHour")]
            public int StartHour = 18;
            [JsonProperty("EndHour")]
            public int EndHour = 22;
        }

        private class PluginConfig
        {
            [JsonProperty("BlockPlayerDamage")]
            public bool BlockPlayerDamage = true;
            [JsonProperty("BlockBuildingDamage")]
            public bool BlockBuildingDamage = true;
            [JsonProperty("IgnoreSameTeam")]
            public bool IgnoreSameTeam = true;
            [JsonProperty("RaidWindows")]
            public List<Window> RaidWindows = new List<Window>
            {
                new Window { Day = "Saturday", StartHour = 18, EndHour = 22 },
                new Window { Day = "Sunday", StartHour = 14, EndHour = 18 }
            };
        }

        private static readonly string[] DayNames =
        {
            "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
        };

        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>
            {
                ["RaidOpen"] = "Raiding is currently <color=#7ec850>allowed</color>. This window ends at {0}:00.",
                ["RaidClosed"] = "Raiding is currently <color=#d9534f>blocked</color>. Next window: {0} {1}:00 - {2}:00 (in {3}).",
                ["NoWindows"] = "Raiding is <color=#d9534f>blocked</color>. No raid windows are configured.",
                ["InMinutes"] = "{0} minutes",
                ["InHours"] = "{0} hours {1} minutes",
                ["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(PermAdmin, this);
        }

        private void OnServerInitialized()
        {
            Puts("PveRaidHours active with " + (config.RaidWindows?.Count ?? 0) + " raid window(s).");
        }

        private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo)
        {
            if (entity == null || hitInfo == null) return null;
            if (config.RaidWindows == null || config.RaidWindows.Count == 0) return null;

            var attacker = hitInfo.InitiatorPlayer;
            if (attacker == null || attacker.IsNpc) return null;

            if (entity is BasePlayer victimPlayer && config.BlockPlayerDamage)
            {
                if (victimPlayer == attacker || victimPlayer.IsNpc) return null;
                if (config.IgnoreSameTeam && attacker.currentTeam != 0 && attacker.currentTeam == victimPlayer.currentTeam) return null;
                if (!IsRaidTime()) return true;
                return null;
            }

            var building = entity as BuildingBlock;
            if (building != null && config.BlockBuildingDamage)
            {
                if (building.OwnerID == 0 || building.OwnerID == attacker.userID.Get()) return null;
                if (!IsRaidTime()) return true;
            }

            return null;
        }

        private int DayIndexOf(string name)
        {
            if (string.IsNullOrEmpty(name)) return -1;
            return Array.FindIndex(DayNames, d => d.Equals(name, StringComparison.OrdinalIgnoreCase));
        }

        private bool IsRaidTime()
        {
            var now = DateTime.Now;
            int dayIndex = (int)now.DayOfWeek;
            double hour = now.Hour + now.Minute / 60.0;

            foreach (var window in config.RaidWindows)
            {
                if (window == null) continue;
                int windowDay = DayIndexOf(window.Day);
                if (windowDay < 0) continue;

                int start = Mathf.Clamp(window.StartHour, 0, 24);
                int end = Mathf.Clamp(window.EndHour, 0, 24);

                if (start < end)
                {
                    if (windowDay == dayIndex && hour >= start && hour < end) return true;
                }
                else if (start > end)
                {
                    // Window crosses midnight: evening part on windowDay, morning part on the following day.
                    int nextDay = (windowDay + 1) % 7;
                    if (windowDay == dayIndex && hour >= start) return true;
                    if (nextDay == dayIndex && hour < end) return true;
                }
            }
            return false;
        }

        private bool TryGetActiveWindow(out Window window, out DateTime end)
        {
            window = null;
            end = DateTime.Now;
            var now = DateTime.Now;
            int dayIndex = (int)now.DayOfWeek;
            double hour = now.Hour + now.Minute / 60.0;

            foreach (var w in config.RaidWindows)
            {
                if (w == null) continue;
                int windowDay = DayIndexOf(w.Day);
                if (windowDay < 0) continue;

                int start = Mathf.Clamp(w.StartHour, 0, 24);
                int endHour = Mathf.Clamp(w.EndHour, 0, 24);

                if (start < endHour)
                {
                    if (windowDay == dayIndex && hour >= start && hour < endHour)
                    {
                        window = w;
                        end = now.Date.AddHours(endHour);
                        return true;
                    }
                }
                else if (start > endHour)
                {
                    int nextDay = (windowDay + 1) % 7;
                    if (windowDay == dayIndex && hour >= start)
                    {
                        window = w;
                        end = now.Date.AddDays(1).AddHours(endHour);
                        return true;
                    }
                    if (nextDay == dayIndex && hour < endHour)
                    {
                        window = w;
                        end = now.Date.AddHours(endHour);
                        return true;
                    }
                }
            }
            return false;
        }

        private Window FindNextWindow(out DateTime startTime)
        {
            startTime = DateTime.MinValue;
            if (config.RaidWindows == null) return null;

            var now = DateTime.Now;
            Window best = null;
            DateTime bestStart = DateTime.MaxValue;

            foreach (var w in config.RaidWindows)
            {
                if (w == null) continue;
                int windowDay = DayIndexOf(w.Day);
                if (windowDay < 0) continue;

                int start = Mathf.Clamp(w.StartHour, 0, 24);
                int diff = (windowDay - (int)now.DayOfWeek + 7) % 7;
                var candidate = now.Date.AddDays(diff).AddHours(start);
                if (candidate <= now) candidate = candidate.AddDays(7);
                if (candidate < bestStart)
                {
                    bestStart = candidate;
                    best = w;
                }
            }

            startTime = bestStart;
            return best;
        }

        private string DescribeStatus(string userId)
        {
            Window active;
            DateTime end;
            if (TryGetActiveWindow(out active, out end))
                return Msg("RaidOpen", userId, end.Hour.ToString("00"));

            DateTime start;
            var next = FindNextWindow(out start);
            if (next == null)
                return Msg("NoWindows", userId);

            var span = start - DateTime.Now;
            string inText = span.TotalHours >= 1
                ? Msg("InHours", userId, (int)span.TotalHours, span.Minutes)
                : Msg("InMinutes", userId, Math.Max(1, span.Minutes));

            return Msg("RaidClosed", userId, next.Day, Mathf.Clamp(next.StartHour, 0, 24).ToString("00"),
                Mathf.Clamp(next.EndHour, 0, 24).ToString("00"), inText);
        }

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

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

Hooks used: OnServerInitialized, OnEntityTakeDamage

Config file

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

PveRaidHours.json
{
  "BlockPlayerDamage": true,
  "BlockBuildingDamage": true,
  "IgnoreSameTeam": true,
  "RaidWindows": [
    {
      "Day": "Saturday",
      "StartHour": 18,
      "EndHour": 22
    },
    {
      "Day": "Sunday",
      "StartHour": 14,
      "EndHour": 18
    }
  ]
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/raidtimeShows whether raiding is currently allowed and when the next raid window starts.Everyone
pveraidhours.statusConsole/RCON version of /raidtime; prints the current raid state and next window.Everyone
Permissions
PermissionGrants
pveraidhours.adminGrants /raidtime and the pveraidhours.status console command.

How the code works

  1. Model the raid windows in config

    The RaidWindows list holds objects with Day (a day name), StartHour and EndHour in 24-hour server time. Hours may cross midnight, so StartHour 22 with EndHour 2 covers the same night across two calendar days. BlockBuildingDamage and BlockPlayerDamage let an admin turn off one half of the protection while keeping the other, which is useful on servers that allow PvP but not raiding.

  2. Decide who is attacking whom

    OnEntityTakeDamage is called for every damage event, so it must be cheap. The plugin first checks whether the hit has an initiator player (hitInfo.InitiatorPlayer) which is null for most NPC, decay and environmental damage. It then compares the attacker with the victim using OwnerID on buildings and userID on players, so a player never blocks their own damage and same-team hits are ignored when IgnoreSameTeam is enabled.

  3. Check the clock against the windows

    IsRaidTime() converts DateTime.Now into a day index and a fractional hour, then walks every configured window. For a window that crosses midnight it splits the check into an evening part and a morning part. The method returns true as soon as one window matches; the result is not cached because the server clock keeps moving while the plugin is running.

  4. Cancel the damage

    When the hit is player-to-player or player-to-building and IsRaidTime() is false, the hook returns a non-null value. Oxide treats a non-null return from this hook as a cancel, so the target keeps its health. Returning null in every other case leaves Rust's normal damage pipeline untouched, which matters for explosives, fire and NPC damage that should always apply.

  5. Report the next window with /raidtime

    The chat command calls TryGetActiveWindow for the current state and FindNextWindow when nothing is open. FindNextWindow looks at each configured day, projects it to the next occurrence and picks the earliest start, so the message can name the day and hours through the language file. The same routine backs the console command so RCON tools can poll the state.

  6. Keep the plugin quiet in hot paths

    OnEntityTakeDamage fires hundreds of times per second on a busy server, so nothing in it writes to the console. Warnings are limited to load time when the config is missing or malformed, and OnServerInitialized logs a single line that summarizes the number of configured windows.

Install it on your server

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

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

Variations

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

  • Broadcast window announcements

    Warn players that raiding is about to start and end without them having to type /raidtime.

    Generate this version
  • Protect only buildings, allow PvP

    Useful for servers that want constant PvP but raiding only during scheduled hours.

    Generate this version
  • Exempt authorized players

    Let a player damage buildings their own team is authorized on even outside raid hours.

    Generate this version

FAQ

Why is damage still blocked in a raid window?

Check the server's local time zone against DateTime.Now; windows are read in the machine's local time. If you want UTC, change the clock call in IsRaidTime and FindNextWindow. Also confirm BlockBuildingDamage and BlockPlayerDamage are true in oxide/config/PveRaidHours.json.

Does this stop explosives from damaging buildings?

Yes for player-thrown explosives. hitInfo.InitiatorPlayer is set for C4 and rockets thrown by a player, so the hook cancels the damage outside raid hours. Damage with no player initiator, such as decay or NPC explosions, is left alone.

Will it stop teammates from damaging each other?

With IgnoreSameTeam set to true, damage between players on the same currentTeam is skipped before the raid check, so teammates can practice on each other. Setting it to false applies the normal raid rule to teammates as well.

How do I install it on Oxide or Carbon?

Drop the compiled .cs file into oxide/plugins, reload the server or run oxide.reload PveRaidHours. Carbon loads the same file from carbon/plugins. Edit oxide/config/PveRaidHours.json to add windows and run oxide.reload PveRaidHours after saving.

Can I have more than one window per day?

Yes. Add one entry per window to the RaidWindows list. The check returns true on the first match, so an unlimited number of windows per week is fine.

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 PvE raid hours plugin with weekly windows defined by day, start hour and end hour, blocking player damage and building damage outside them, with a /raidtime command showing the next window.
Customize for my server

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

Last updated Oct 26, 2026.

Customize pve with raid hours

Customize