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
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.
{
"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
| Command | What it does | Permission |
|---|---|---|
/wipe | Shows the time left until the next wipe as days, hours and minutes. | wipecountdown.use |
/wipeschedule | Prints the configured wipe schedule (fixed date or monthly rule). | wipecountdown.use |
| Permission | Grants |
|---|---|
wipecountdown.use | Grants access to the /wipe and /wipeschedule chat commands. |
How the code works
Config shape
The nested
PluginConfigclass has aModestring (FixedorMonthly), aFixedWipeUtcstring inyyyy-MM-dd HH:mmformat,MonthlyDayandMonthlyHourUtcfor the monthly rule, plusAnnounceIntervalMinutesandRemindWithinHours.LoadConfigreads the file and falls back toLoadDefaultConfigwhen it cannot deserialize, so a broken edit does not crash the plugin.Computing the next wipe
GetNextWipe()returns aDateTimein UTC. InFixedmode it parsesFixedWipeUtcwithDateTime.TryParseExactand returnsDateTime.MaxValueif the date is unreadable or already passed. Otherwise it resolvesMonthlyDayto aDayOfWeek, builds the first matching day of the current month withFirstOfMonth, and if that moment is in the past moves to the next month.Formatting the countdown
FormatRemaining(TimeSpan span)builds the localized string from theTimeLeft,TimeLeftNoDaysandWipeUnavailablemessages, showing minutes when the wipe is under a day away. TheMsghelper wrapslang.GetMessageso no English text lives in the command handlers. The result is cached in thenextWipefield, set inLoadedand again inOnServerInitialized.Chat commands
[ChatCommand("wipe")]checkswipecountdown.usethroughpermission.UserHasPermission, reads the cachednextWipe, subtractsDateTime.UtcNowand replies withSendReply.[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 theNoPermissionmessage when the permission is missing.Reminder broadcasts
When
AnnounceIntervalMinutesis greater than zero,Loadedstarts atimer.Everyat that interval. If the remaining time is underRemindWithinHours, it broadcasts theWipeRemindermessage with the formatted countdown viaServer.Broadcast; otherwise it stays silent. The timer reference is kept in theannounceTimerfield and destroyed inUnloadso a plugin reload does not leave duplicate timers running.Lifecycle and cleanup
Initregisters thewipecountdown.usepermission,OnServerInitializedrecomputesnextWipeonce the server is fully up, andUnloaddestroysannounceTimer. 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
Save WipeCountdown.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/WipeCountdown.json(Carbon:carbon/configs/WipeCountdown.json). After editing, reload it:oxide.reload WipeCountdown c.reload WipeCountdown # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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.
Wipe-only chat filter
Blocks the string "when wipe" in chat and answers with the countdown automatically.
Generate this versionJoin announcement
Shows the countdown to a player when they connect.
Generate this versionServer console command
Adds a console command for RCON to print the countdown.
Generate this version
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.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More utility plugins
- Command aliasesThis plugin maps short chat commands like /h and /k to longer ones such as /home and /kit, defined entirely in its config file, using theโฆOnServerInitializedOnPlayerCommandOnPlayerDisconnected
- Info commandsA config-driven text command plugin: every entry in the config defines a command name and a list of chat lines, and the plugin registers them allโฆ
- Map grid locationA /grid chat command that converts a player's world position into a Rust map grid cell (like G12) using the server's world size, with exactโฆOnServerInitializedOnPlayerConnected
Last updated Oct 26, 2026.
Customize wipe countdown
Customize