How to make a Rust double gather event plugin (scheduled multiplier)
Runs scheduled or admin-triggered double gather windows that multiply dispenser and collectible yields for a fixed duration, with start and end announcements. It uses OnDispenserGather and OnCollectiblePickup to scale item amounts.
Most wipe days funnel everyone into the same trees and nodes at the same time. A timed gather boost gives players a reason to log on at set hours and keeps resource flow predictable for admins testing economy balance. The plugin below runs as a pure event: either it fires automatically at configured server times, or an admin starts it manually with a chat or console command.
During an active window it multiplies the amount of items from resource dispensers (trees, nodes, hemp) and from collectibles (berries, mushrooms, corn) by a configurable factor. Both hooks return the item untouched outside the window, so normal gather rates are preserved. Start and end are announced in chat, and a countdown warning plays before the end. Only one window runs at a time; starting again while active reports that the event is already running.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Double Gather Event", "RustPluginGenerator", "1.0.0")]
[Description("Runs scheduled or admin-triggered gather multiplier windows with announcements.")]
public class DoubleGatherEvent : RustPlugin
{
private const string PermAdmin = "doublegatherevent.admin";
private PluginConfig config;
private DateTime eventEndsAt = DateTime.MinValue;
private Timer endTimer;
private Timer warnTimer;
private Timer scheduledTimer;
private class PluginConfig
{
[JsonProperty("Use schedule")]
public bool UseSchedule { get; set; } = true;
[JsonProperty("Schedule times (HH:mm, server local time)")]
public List<string> ScheduleTimes { get; set; } = new List<string> { "19:00", "22:00" };
[JsonProperty("Gather multiplier")]
public float GatherMultiplier { get; set; } = 2.0f;
[JsonProperty("Duration minutes")]
public int DurationMinutes { get; set; } = 30;
[JsonProperty("Warn seconds before end")]
public int WarnSeconds { get; set; } = 120;
}
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>
{
["Started"] = "<color=#ffd479>Gather event started!</color> All gathering is multiplied by {0} for {1} minutes.",
["Ended"] = "<color=#ffd479>Gather event ended.</color> Normal gather rates restored.",
["EndingSoon"] = "<color=#ffd479>Gather event ends in {0} seconds.</color>",
["AlreadyRunning"] = "The gather event is already running.",
["NotRunning"] = "No gather event is running.",
["StartedByAdmin"] = "{0} started a gather event: x{1} for {2} minutes.",
["CommandStart"] = "Gather event started for {0} minutes.",
["CommandStop"] = "Gather event stopped.",
["CommandStatus"] = "Gather event running with {0} seconds left.",
["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()
{
if (config.UseSchedule) ScheduleNextRun();
}
private void Unload()
{
DestroyTimer(ref endTimer);
DestroyTimer(ref warnTimer);
DestroyTimer(ref scheduledTimer);
}
private static void DestroyTimer(ref Timer timer)
{
if (timer != null)
{
timer.Destroy();
timer = null;
}
}
private bool IsEventActive() => DateTime.UtcNow < eventEndsAt;
private object OnDispenserGather(ResourceDispenser resourceDispenser, BasePlayer entity, Item item)
{
if (!IsEventActive() || item == null) return null;
var original = item.amount;
var scaled = Mathf.Max(original, Mathf.RoundToInt(original * config.GatherMultiplier));
if (scaled != original)
{
item.amount = scaled;
item.MarkDirty();
}
return null;
}
private object OnCollectiblePickup(CollectibleEntity collectibleEntity, BasePlayer reciever, bool eat)
{
if (!IsEventActive() || collectibleEntity?.itemList == null) return null;
foreach (var entry in collectibleEntity.itemList)
{
if (entry == null || entry.itemDef == null) continue;
var original = (int)entry.amount;
var scaled = Mathf.Max(original, Mathf.RoundToInt(original * config.GatherMultiplier));
entry.amount = scaled;
}
return null;
}
private void ScheduleNextRun()
{
DestroyTimer(ref scheduledTimer);
DateTime now = DateTime.Now;
DateTime? next = null;
foreach (var raw in config.ScheduleTimes)
{
if (string.IsNullOrEmpty(raw)) continue;
if (!TimeSpan.TryParse(raw.Trim(), out var span)) continue;
var candidate = now.Date + span;
if (candidate <= now) candidate = candidate.AddDays(1);
if (next == null || candidate < next.Value) next = candidate;
}
if (next == null) return;
double seconds = Math.Max(1.0, (next.Value - now).TotalSeconds);
scheduledTimer = timer.Once((float)seconds, () =>
{
scheduledTimer = null;
StartEvent(config.DurationMinutes);
});
}
private bool StartEvent(int minutes)
{
if (IsEventActive()) return false;
if (minutes <= 0) minutes = config.DurationMinutes;
eventEndsAt = DateTime.UtcNow.AddMinutes(minutes);
Server.Broadcast(Msg("Started", null, config.GatherMultiplier, minutes));
DestroyTimer(ref endTimer);
endTimer = timer.Once(minutes * 60f, () =>
{
endTimer = null;
EndEvent();
});
DestroyTimer(ref warnTimer);
if (config.WarnSeconds > 0 && config.WarnSeconds < minutes * 60)
{
warnTimer = timer.Once(minutes * 60f - config.WarnSeconds, () =>
{
warnTimer = null;
Server.Broadcast(Msg("EndingSoon", null, config.WarnSeconds));
});
}
return true;
}
private bool EndEvent()
{
if (eventEndsAt == DateTime.MinValue) return false;
eventEndsAt = DateTime.MinValue;
DestroyTimer(ref endTimer);
DestroyTimer(ref warnTimer);
Server.Broadcast(Msg("Ended"));
if (config.UseSchedule) ScheduleNextRun();
return true;
}
[ChatCommand("dge")]
private void CmdDge(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
string sub = args != null && args.Length > 0 ? args[0].ToLowerInvariant() : "status";
switch (sub)
{
case "start":
{
if (IsEventActive())
{
SendReply(player, Msg("AlreadyRunning", player.UserIDString));
return;
}
int minutes = config.DurationMinutes;
if (args.Length > 1 && int.TryParse(args[1], out var parsed) && parsed > 0) minutes = parsed;
StartEvent(minutes);
Server.Broadcast(Msg("StartedByAdmin", null, player.displayName, config.GatherMultiplier, minutes));
SendReply(player, Msg("CommandStart", player.UserIDString, minutes));
break;
}
case "stop":
{
if (!IsEventActive())
{
SendReply(player, Msg("NotRunning", player.UserIDString));
return;
}
EndEvent();
SendReply(player, Msg("CommandStop", player.UserIDString));
break;
}
default:
{
if (!IsEventActive())
{
SendReply(player, Msg("NotRunning", player.UserIDString));
return;
}
var left = (int)Math.Max(0, (eventEndsAt - DateTime.UtcNow).TotalSeconds);
SendReply(player, Msg("CommandStatus", player.UserIDString, left));
break;
}
}
}
[ConsoleCommand("dge.start")]
private void CcmdStart(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 (!arg.IsAdmin && player == null)
{
arg.ReplyWith(Msg("NoPermission"));
return;
}
if (IsEventActive())
{
arg.ReplyWith(Msg("AlreadyRunning"));
return;
}
int minutes = config.DurationMinutes;
int requested = arg.GetInt(0, 0);
if (requested > 0) minutes = requested;
StartEvent(minutes);
arg.ReplyWith(Msg("CommandStart", null, minutes));
}
[ConsoleCommand("dge.stop")]
private void CcmdStop(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 (!arg.IsAdmin && player == null)
{
arg.ReplyWith(Msg("NoPermission"));
return;
}
if (!EndEvent())
{
arg.ReplyWith(Msg("NotRunning"));
return;
}
arg.ReplyWith(Msg("CommandStop"));
}
}
}Hooks used: OnServerInitialized, OnDispenserGather, OnCollectiblePickup
Config file
Created automatically on first load at oxide/config/DoubleGatherEvent.json.
{
"Use schedule": true,
"Schedule times (HH:mm, server local time)": [
"19:00",
"22:00"
],
"Gather multiplier": 2,
"Duration minutes": 30,
"Warn seconds before end": 120
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/dge start [minutes] | Starts the gather event; uses the configured duration when no minutes are given | doublegatherevent.admin |
/dge stop | Ends a running event and announces the end | doublegatherevent.admin |
/dge status | Shows whether the event is running and how many seconds are left | doublegatherevent.admin |
dge.start | Console/RCON command that starts the event with an optional minutes argument | doublegatherevent.admin |
dge.stop | Console/RCON command that ends a running event | doublegatherevent.admin |
| Permission | Grants |
|---|---|
doublegatherevent.admin | Grants access to the /dge commands and the dge.start / dge.stop console commands |
How the code works
Scaling dispenser yields
object OnDispenserGather(ResourceDispenser resourceDispenser, BasePlayer entity, Item item)fires after the game has resolved how much a player gets from a node or tree. The plugin checksIsEventActive()first and returnsnullwhen no window is open, leaving vanilla behavior untouched. When active it multipliesitem.amountbyconfig.GatherMultiplier, clamps the result to at least the original amount, and callsitem.MarkDirty()so the change reaches the client. BothitemandresourceDispenserare null-checked because the hook can fire with unusual callers.Scaling collectible pickups
object OnCollectiblePickup(CollectibleEntity collectibleEntity, BasePlayer reciever, bool eat)runs when a player picks berries, mushrooms or other collectibles. The sameIsEventActive()gate applies. The plugin multipliesitem.amountfor every entry incollectibleEntity.itemList, skipping null entries, then marks each item dirty. The parameter names from the hook are kept exactly, including the game's ownrecieverspelling, so Oxide dispatch matches. The hook returnsnullso the vanilla pickup logic continues.Scheduling with the configured times
config.ScheduleTimesis a list ofHH:mmstrings in server local time. OnOnServerInitializedthe plugin callsScheduleNextRun(), which converts each entry to aDateTimefor today or tomorrow, picks the nearest future one, and sets atimer.Oncefor the difference. When the timer fires it callsStartEvent(config.DurationMinutes). SetUse scheduletofalseto disable automatic windows entirely and rely on admin commands. Keep the timer in thescheduledTimerfield soUnloadcan destroy it.Start, end and announcements
StartEvent(int minutes)refuses to stack: ifeventEndsAtis in the future it replies that the event is running. Otherwise it storeseventEndsAt = DateTime.UtcNow.AddMinutes(minutes), broadcasts the localized start message throughServer.Broadcast, and sets an end timer plus a warning timerWarn seconds before endbefore the end.EndEvent()clears the timers, resetseventEndsAt, broadcasts the end message, and schedules the next window. Every string goes throughlang, so admins can translate announcements inoxide/lang.Commands and permissions
The
[ChatCommand("dge")]handler parses a subcommand fromargs[0](start, stop, status) and checkspermission.UserHasPermission(player.UserIDString, PermAdmin)before doing anything. Two[ConsoleCommand]handlers,dge.startanddge.stop, allow RCON automation; they usearg.Player()which is null from the server console, so replies go througharg.ReplyWithrather thanSendReply.PermAdminisdoublegatherevent.admin, registered inInit().The multiplier and duration in config
Gather multiplieraccepts values like2.0for a true double gather hour or1.5for a softer boost.Duration minutessets the default window length used by the schedule and by/dge startwithout an argument.Warn seconds before endcontrols the lead time of the ending-soon message, andSchedule timesholds theHH:mmentries. The whole config is rewritten withSaveConfigafterLoadConfig, so a corrupt file falls back to defaults with a warning instead of failing to load.
Install it on your server
Save DoubleGatherEvent.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/DoubleGatherEvent.json(Carbon:carbon/configs/DoubleGatherEvent.json). After editing, reload it:oxide.reload DoubleGatherEvent c.reload DoubleGatherEvent # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default doublegatherevent.admin c.grant group default doublegatherevent.admin # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Weighted multipliers
Scale different resources at different rates, e.g. wood x2 but ore x1.5.
Generate this versionVote-triggered event
Let players vote to start a gather boost instead of waiting for the schedule.
Generate this versionEconomy bonus during the event
Pay out a scrap bonus per gathered item while the window is open.
Generate this version
FAQ
Does the multiplier apply to quarry or pumpjack output?
No. Quarries, pumpjacks and excavators use other paths (OnQuarryGather, OnExcavatorGather) that this plugin does not hook. Only hand-gathered nodes, trees, hemp and collectibles are affected. Add those hooks if you want automated extraction included.
How are the schedule times interpreted?
Each Schedule times entry is compared as local server time using DateTime.Now. If a time has already passed today, the plugin schedules it for the following day. Make sure the server machine timezone matches what you expect, otherwise edit the list to compensate.
Can I run two gather events at once?
No. StartEvent checks whether eventEndsAt is still in the future and refuses to start a second window, telling the caller it is already running. Use /dge stop first, then start again with the new duration.
How do I install it on Oxide or Carbon?
Drop the compiled DoubleGatherEvent.cs into oxide/plugins, restart or reload the plugin, and grant doublegatherevent.admin to your staff group. Carbon loads the same file from its plugin folder. Edit oxide/config/DoubleGatherEvent.json and oxide/lang/en/DoubleGatherEvent.json for rates and wording.
Why do some nodes give more than a multiple of the normal amount?
Vanilla already applies a bonus based on tool tier and player modifiers before the hook runs. The plugin multiplies whatever the game resolved, so the visible amount is your multiplier times the boosted yield, not the base node value.
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 events plugins
- Bradley and heli kill announcerTracks damage dealt to the Bradley APC and patrol helicopter, then announces in chat which player did the most damage and the map grid where theโฆOnEntityTakeDamageOnEntityDeath
- First bloodAnnounces the first player kill after a restart as First Blood and pays the killer a configured reward.OnServerInitializedOnNewSaveOnEntityDeath
- Locked crate drop eventSpawns a hackable locked crate at a random configured monument on a repeating timer, announces the monument in chat, and removes the previous eventโฆOnServerInitializedOnCrateHackOnEntityKill
Last updated Oct 26, 2026.
Customize double gather hour
Customize