Skip to content
RustPluginGenerator

How to make a Rust wipe countdown plugin (UTC schedule)

WipeCountdown shows players how long is left until the next wipe, either from a fixed date/time in the config or from the first Thursday of the month at a configured UTC hour. It caches the next wipe on server start, answers /wipe, and broadcasts reminders on a timer.

Players constantly ask "when is wipe?" in chat, and admins end up typing the same answer by hand. This plugin stores the wipe schedule in its config and answers the question automatically. It supports two modes: a fixed UTC date and time, or the monthly pattern most Rust servers use, the first Thursday of each month at a set hour. Two chat commands expose the countdown and the schedule, and an optional repeating broadcast posts a reminder before the wipe. The next wipe is computed when the server initializes and refreshed when the plugin loads, and every message is localized. The config lets you pick the mode, set the day and hour, tune the broadcast interval, and change the reminder threshold, so the same file works for weekly, biweekly or monthly wipes.

The plugin code

WipeCountdown.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Oxide.Core;

namespace Oxide.Plugins
{
    [Info("Wipe Countdown", "RustPluginGenerator", "1.0.0")]
    [Description("Shows the time left until the next wipe using a fixed or monthly UTC schedule.")]
    public class WipeCountdown : RustPlugin
    {
        private const string PermUse = "wipecountdown.use";

        private PluginConfig config;
        private DateTime nextWipe = DateTime.MaxValue;
        private Timer announceTimer;

        private class PluginConfig
        {
            [JsonProperty("Mode (Fixed or Monthly)")]
            public string Mode = "Monthly";

            [JsonProperty("Fixed wipe UTC (yyyy-MM-dd HH:mm)")]
            public string FixedWipeUtc = "2025-01-02 19:00";

            [JsonProperty("Monthly day of week")]
            public string MonthlyDay = "Thursday";

            [JsonProperty("Monthly hour UTC (0-23)")]
            public int MonthlyHourUtc = 19;

            [JsonProperty("Announce interval minutes (0 disables)")]
            public int AnnounceIntervalMinutes = 0;

            [JsonProperty("Remind within hours")]
            public int RemindWithinHours = 6;
        }

        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>
            {
                ["TimeLeft"] = "Wipe in {0} days {1} hours {2} minutes.",
                ["TimeLeftNoDays"] = "Wipe in {0} hours {1} minutes.",
                ["WipeUnavailable"] = "No wipe date is currently scheduled.",
                ["WipeReminder"] = "Reminder: {0}",
                ["ScheduleFixed"] = "Wipe schedule: {0} UTC.",
                ["ScheduleMonthly"] = "Wipe schedule: first {0} of each month at {1}:00 UTC.",
                ["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()
        {
            nextWipe = GetNextWipe();
        }

        private void Loaded()
        {
            nextWipe = GetNextWipe();
            if (config.AnnounceIntervalMinutes <= 0) return;

            announceTimer = timer.Every(config.AnnounceIntervalMinutes * 60f, () =>
            {
                var remaining = nextWipe - DateTime.UtcNow;
                if (remaining <= TimeSpan.Zero) return;
                if (remaining.TotalHours > config.RemindWithinHours) return;
                Server.Broadcast(Msg("WipeReminder", null, FormatRemaining(remaining)));
            });
        }

        private void Unload()
        {
            if (announceTimer != null)
            {
                announceTimer.Destroy();
                announceTimer = null;
            }
        }

        private DateTime GetNextWipe()
        {
            if (string.Equals(config.Mode, "Fixed", StringComparison.OrdinalIgnoreCase))
            {
                DateTime fixedDate;
                if (!DateTime.TryParseExact(config.FixedWipeUtc, "yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture,
                        DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out fixedDate))
                    return DateTime.MaxValue;
                return fixedDate > DateTime.UtcNow ? fixedDate : DateTime.MaxValue;
            }

            DayOfWeek targetDay;
            if (!Enum.TryParse(config.MonthlyDay, true, out targetDay))
                targetDay = DayOfWeek.Thursday;

            var now = DateTime.UtcNow;
            var candidate = FirstOfMonth(now.Year, now.Month, targetDay);
            if (candidate <= now)
            {
                var nextMonth = now.AddMonths(1);
                candidate = FirstOfMonth(nextMonth.Year, nextMonth.Month, targetDay);
            }
            return candidate;
        }

        private DateTime FirstOfMonth(int year, int month, DayOfWeek day)
        {
            var first = new DateTime(year, month, 1, config.MonthlyHourUtc, 0, 0, DateTimeKind.Utc);
            int offset = ((int)day - (int)first.DayOfWeek + 7) % 7;
            return first.AddDays(offset);
        }

        private string FormatRemaining(TimeSpan span)
        {
            if (span <= TimeSpan.Zero) return Msg("WipeUnavailable");
            int days = span.Days;
            int hours = span.Hours;
            int minutes = span.Minutes;
            return days > 0 ? Msg("TimeLeft", null, days, hours, minutes) : Msg("TimeLeftNoDays", null, hours, minutes);
        }

        [ChatCommand("wipe")]
        private void CmdWipe(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                SendReply(player, Msg("NoPermission", player.UserIDString));
                return;
            }

            if (nextWipe == DateTime.MaxValue)
            {
                SendReply(player, Msg("WipeUnavailable", player.UserIDString));
                return;
            }

            SendReply(player, FormatRemaining(nextWipe - DateTime.UtcNow));
        }

        [ChatCommand("wipeschedule")]
        private void CmdSchedule(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                SendReply(player, Msg("NoPermission", player.UserIDString));
                return;
            }

            if (string.Equals(config.Mode, "Fixed", StringComparison.OrdinalIgnoreCase))
            {
                SendReply(player, Msg("ScheduleFixed", player.UserIDString, config.FixedWipeUtc));
            }
            else
            {
                SendReply(player, Msg("ScheduleMonthly", player.UserIDString, config.MonthlyDay, config.MonthlyHourUtc));
            }
        }
    }
}

Hooks used: OnServerInitialized

Config file

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

WipeCountdown.json
{
  "Mode (Fixed or Monthly)": "Monthly",
  "Fixed wipe UTC (yyyy-MM-dd HH:mm)": "2025-01-02 19:00",
  "Monthly day of week": "Thursday",
  "Monthly hour UTC (0-23)": 19,
  "Announce interval minutes (0 disables)": 0,
  "Remind within hours": 6
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/wipeShows the time left until the next wipe as days, hours and minutes.wipecountdown.use
/wipeschedulePrints the configured wipe schedule (fixed date or monthly rule).wipecountdown.use
Permissions
PermissionGrants
wipecountdown.useGrants access to the /wipe and /wipeschedule chat commands.

How the code works

  1. Config shape

    The nested PluginConfig class has a Mode string (Fixed or Monthly), a FixedWipeUtc string in yyyy-MM-dd HH:mm format, MonthlyDay and MonthlyHourUtc for the monthly rule, plus AnnounceIntervalMinutes and RemindWithinHours. LoadConfig reads the file and falls back to LoadDefaultConfig when it cannot deserialize, so a broken edit does not crash the plugin.

  2. Computing the next wipe

    GetNextWipe() returns a DateTime in UTC. In Fixed mode it parses FixedWipeUtc with DateTime.TryParseExact and returns DateTime.MaxValue if the date is unreadable or already passed. Otherwise it resolves MonthlyDay to a DayOfWeek, builds the first matching day of the current month with FirstOfMonth, and if that moment is in the past moves to the next month.

  3. Formatting the countdown

    FormatRemaining(TimeSpan span) builds the localized string from the TimeLeft, TimeLeftNoDays and WipeUnavailable messages, showing minutes when the wipe is under a day away. The Msg helper wraps lang.GetMessage so no English text lives in the command handlers. The result is cached in the nextWipe field, set in Loaded and again in OnServerInitialized.

  4. Chat commands

    [ChatCommand("wipe")] checks wipecountdown.use through permission.UserHasPermission, reads the cached nextWipe, subtracts DateTime.UtcNow and replies with SendReply. [ChatCommand("wipeschedule")] prints either the fixed date or the monthly rule with the day name and hour. Both commands null-check the player and reply with the NoPermission message when the permission is missing.

  5. Reminder broadcasts

    When AnnounceIntervalMinutes is greater than zero, Loaded starts a timer.Every at that interval. If the remaining time is under RemindWithinHours, it broadcasts the WipeReminder message with the formatted countdown via Server.Broadcast; otherwise it stays silent. The timer reference is kept in the announceTimer field and destroyed in Unload so a plugin reload does not leave duplicate timers running.

  6. Lifecycle and cleanup

    Init registers the wipecountdown.use permission, OnServerInitialized recomputes nextWipe once the server is fully up, and Unload destroys announceTimer. There is no data file to save because the schedule lives entirely in config, so no player state persists across restarts.

Install it on your server

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

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

Variations

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

FAQ

How do I install it on Oxide or Carbon?

Drop the compiled .cs file into oxide/plugins, then run oxide.reload WipeCountdown from the server console. Carbon loads the same file from its plugins folder. Edit oxide/config/WipeCountdown.json and reload after changing the schedule.

What is the first-Thursday rule?

It is the convention where the wipe happens on the first Thursday of each month, often at a fixed UTC hour. Set Mode to Monthly, MonthlyDay to Thursday and MonthlyHourUtc to your wipe hour. The plugin always looks at UTC, not server local time.

What happens when the fixed date is already in the past?

GetNextWipe can only return a future point in Fixed mode; once the date has passed, /wipe reports that no wipe is scheduled. Update FixedWipeUtc after each wipe, or switch to Monthly mode so the date rolls forward on its own.

Can I turn off the periodic reminder?

Set AnnounceIntervalMinutes to 0. The timer is not started at all in that case and only the chat commands remain active.

Customize it for your server

Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.

Prompt: Write a Rust Oxide plugin WipeCountdown with /wipe and /wipeschedule, permission wipecountdown.use, config supporting a fixed UTC date and a first-Thursday monthly rule, plus an optional reminder broadcast.
Customize for my server

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

Last updated Oct 26, 2026.

Customize wipe countdown

Customize