Skip to content
RustPluginGenerator

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

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

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

Chat and console commands
CommandWhat it doesPermission
/dge start [minutes]Starts the gather event; uses the configured duration when no minutes are givendoublegatherevent.admin
/dge stopEnds a running event and announces the enddoublegatherevent.admin
/dge statusShows whether the event is running and how many seconds are leftdoublegatherevent.admin
dge.startConsole/RCON command that starts the event with an optional minutes argumentdoublegatherevent.admin
dge.stopConsole/RCON command that ends a running eventdoublegatherevent.admin
Permissions
PermissionGrants
doublegatherevent.adminGrants access to the /dge commands and the dge.start / dge.stop console commands

How the code works

  1. 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 checks IsEventActive() first and returns null when no window is open, leaving vanilla behavior untouched. When active it multiplies item.amount by config.GatherMultiplier, clamps the result to at least the original amount, and calls item.MarkDirty() so the change reaches the client. Both item and resourceDispenser are null-checked because the hook can fire with unusual callers.

  2. Scaling collectible pickups

    object OnCollectiblePickup(CollectibleEntity collectibleEntity, BasePlayer reciever, bool eat) runs when a player picks berries, mushrooms or other collectibles. The same IsEventActive() gate applies. The plugin multiplies item.amount for every entry in collectibleEntity.itemList, skipping null entries, then marks each item dirty. The parameter names from the hook are kept exactly, including the game's own reciever spelling, so Oxide dispatch matches. The hook returns null so the vanilla pickup logic continues.

  3. Scheduling with the configured times

    config.ScheduleTimes is a list of HH:mm strings in server local time. On OnServerInitialized the plugin calls ScheduleNextRun(), which converts each entry to a DateTime for today or tomorrow, picks the nearest future one, and sets a timer.Once for the difference. When the timer fires it calls StartEvent(config.DurationMinutes). Set Use schedule to false to disable automatic windows entirely and rely on admin commands. Keep the timer in the scheduledTimer field so Unload can destroy it.

  4. Start, end and announcements

    StartEvent(int minutes) refuses to stack: if eventEndsAt is in the future it replies that the event is running. Otherwise it stores eventEndsAt = DateTime.UtcNow.AddMinutes(minutes), broadcasts the localized start message through Server.Broadcast, and sets an end timer plus a warning timer Warn seconds before end before the end. EndEvent() clears the timers, resets eventEndsAt, broadcasts the end message, and schedules the next window. Every string goes through lang, so admins can translate announcements in oxide/lang.

  5. Commands and permissions

    The [ChatCommand("dge")] handler parses a subcommand from args[0] (start, stop, status) and checks permission.UserHasPermission(player.UserIDString, PermAdmin) before doing anything. Two [ConsoleCommand] handlers, dge.start and dge.stop, allow RCON automation; they use arg.Player() which is null from the server console, so replies go through arg.ReplyWith rather than SendReply. PermAdmin is doublegatherevent.admin, registered in Init().

  6. The multiplier and duration in config

    Gather multiplier accepts values like 2.0 for a true double gather hour or 1.5 for a softer boost. Duration minutes sets the default window length used by the schedule and by /dge start without an argument. Warn seconds before end controls the lead time of the ending-soon message, and Schedule times holds the HH:mm entries. The whole config is rewritten with SaveConfig after LoadConfig, so a corrupt file falls back to defaults with a warning instead of failing to load.

Install it on your server

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

    Give it to every player, or swap default for a group such as vip: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 version
  • Vote-triggered event

    Let players vote to start a gather boost instead of waiting for the schedule.

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

Prompt: Create a Rust Oxide plugin that runs a scheduled or admin-started double gather event: multiplier 2.0, 30 minute duration, times in config, start and end chat announcements, /dge start|stop|status and console commands.
Customize for my server

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

Last updated Oct 26, 2026.

Customize double gather hour

Customize