Skip to content
RustPluginGenerator

How to make a Rust gather rate plugin (per-resource multipliers)

Scales resources from trees, ore nodes, corpses and world pickups with per-resource multipliers and a VIP bonus. Uses OnDispenserGather, OnDispenserBonus and OnCollectiblePickup.

Rust's ResourceDispenser already rolls a random amount per hit, and world pickups like hemp or mushrooms give a fixed amount. Admins who want a faster economy usually need to touch each of these paths separately, and per-resource control (more stone, less sulfur) is not exposed in vanilla. This plugin reads the amount in item.amount at the moment Oxide fires OnDispenserGather for a hit on a node, tree or corpse, and writes back a scaled amount. OnCollectiblePickup handles ground pickups and only adjusts the main item, since the hook fires once per pickup rather than once per item. OnDispenserBonus scales the bonus resource a dispenser awards when the entity is depleted. Multipliers come from a config section keyed by item shortname, so unknown resources fall back to a global default, and players with the VIP permission get an extra multiplier on top.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("Gather Rate Multiplier", "RustPluginGenerator", "1.0.0")]
    [Description("Scales resources from dispensers, depleted bonuses and world pickups with per-resource multipliers.")]
    public class GatherRateMultiplier : RustPlugin
    {
        private const string PermUse = "gatherratemultiplier.use";
        private const string PermVip = "gatherratemultiplier.vip";
        private const string PermAdmin = "gatherratemultiplier.admin";

        private PluginConfig config;

        private class PluginConfig
        {
            [JsonProperty("Default multiplier")]
            public float DefaultMultiplier = 2f;

            [JsonProperty("VIP multiplier")]
            public float VipMultiplier = 1.5f;

            [JsonProperty("Minimum amount")]
            public int MinAmount = 1;

            [JsonProperty("Maximum multiplier")]
            public float MaxMultiplier = 50f;

            [JsonProperty("Per-resource multipliers")]
            public Dictionary<string, float> Multipliers = new Dictionary<string, float>
            {
                ["wood"] = 2f,
                ["stones"] = 2f,
                ["metal.ore"] = 2f,
                ["hq.metal.ore"] = 1.5f,
                ["sulfur.ore"] = 1f,
                ["cloth"] = 2f,
                ["bone.fragments"] = 2f,
                ["fat.animal"] = 1.5f
            };
        }

        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>
            {
                ["YourRate"] = "Your gather multiplier: {0}x{1}",
                ["VipActive"] = " (VIP bonus included)",
                ["NoPermission"] = "You do not have permission to use this command",
                ["SetOk"] = "{0} multiplier set to {1}x",
                ["SetDefaultOk"] = "Default multiplier set to {0}x",
                ["Reloaded"] = "Config reloaded. Default multiplier: {0}x",
                ["UnknownItem"] = "'{0}' is not a valid item shortname",
                ["UsageSet"] = "Usage: /gatherrate set <shortname|default> <multiplier>",
                ["BadNumber"] = "'{0}' is not a valid number"
            }, 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);
            permission.RegisterPermission(PermVip, this);
            permission.RegisterPermission(PermAdmin, this);
        }

        private object OnDispenserGather(ResourceDispenser resourceDispenser, BasePlayer entity, Item item)
        {
            if (entity == null || item == null || item.info == null) return null;
            ScaleItem(entity, item);
            return null;
        }

        private object OnDispenserBonus(ResourceDispenser resourceDispenser, BasePlayer player, Item item)
        {
            if (player == null || item == null || item.info == null) return null;
            ScaleItem(player, item);
            return null;
        }

        private object OnCollectiblePickup(CollectibleEntity collectibleEntity, BasePlayer reciever, bool eat)
        {
            if (reciever == null || collectibleEntity == null) return null;
            if (collectibleEntity.itemList == null) return null;

            // Hook does not expose the produced item, so scale the source entries
            foreach (var entry in collectibleEntity.itemList)
            {
                if (entry == null || entry.itemDef == null) continue;
                var mult = MultiplierFor(entry.itemDef.shortname, reciever);
                if (Mathf.Approximately(mult, 1f)) continue;

                int scaled = Mathf.RoundToInt(entry.amount * mult);
                if (scaled < config.MinAmount) scaled = config.MinAmount;
                entry.amount = scaled;
            }

            return null;
        }

        private void ScaleItem(BasePlayer player, Item item)
        {
            var mult = MultiplierFor(item.info.shortname, player);
            if (Mathf.Approximately(mult, 1f)) return;

            int scaled = Mathf.RoundToInt(item.amount * mult);
            if (scaled < config.MinAmount) scaled = config.MinAmount;
            item.amount = scaled;
        }

        private float MultiplierFor(string shortname, BasePlayer player)
        {
            float mult = config.DefaultMultiplier;
            if (!string.IsNullOrEmpty(shortname) && config.Multipliers != null && config.Multipliers.TryGetValue(shortname, out float configured))
            {
                mult = configured;
            }

            if (player != null && permission.UserHasPermission(player.UserIDString, PermVip))
            {
                mult *= config.VipMultiplier;
            }

            if (mult < 0f) mult = 0f;
            if (mult > config.MaxMultiplier) mult = config.MaxMultiplier;
            return mult;
        }

        [ChatCommand("gatherrate")]
        private void CmdGatherRate(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;

            if (args == null || args.Length == 0)
            {
                if (!permission.UserHasPermission(player.UserIDString, PermUse))
                {
                    player.ChatMessage(Msg("NoPermission", player.UserIDString));
                    return;
                }

                float baseMult = config.DefaultMultiplier;
                bool vip = permission.UserHasPermission(player.UserIDString, PermVip);
                string vipText = vip ? Msg("VipActive", player.UserIDString) : string.Empty;
                player.ChatMessage(Msg("YourRate", player.UserIDString, baseMult.ToString("0.##"), vipText));
                return;
            }

            if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }

            if (args[0].Equals("reload", StringComparison.OrdinalIgnoreCase))
            {
                LoadConfig();
                player.ChatMessage(Msg("Reloaded", player.UserIDString, config.DefaultMultiplier.ToString("0.##")));
                return;
            }

            if (!args[0].Equals("set", StringComparison.OrdinalIgnoreCase) || args.Length < 3)
            {
                player.ChatMessage(Msg("UsageSet", player.UserIDString));
                return;
            }

            string target = args[1].ToLowerInvariant();
            if (!float.TryParse(args[2], out float value))
            {
                player.ChatMessage(Msg("BadNumber", player.UserIDString, args[2]));
                return;
            }

            if (value < 0f) value = 0f;
            if (value > config.MaxMultiplier) value = config.MaxMultiplier;

            if (target == "default")
            {
                config.DefaultMultiplier = value;
                SaveConfig();
                player.ChatMessage(Msg("SetDefaultOk", player.UserIDString, value.ToString("0.##")));
                return;
            }

            var def = ItemManager.FindItemDefinition(target);
            if (def == null)
            {
                player.ChatMessage(Msg("UnknownItem", player.UserIDString, target));
                return;
            }

            if (config.Multipliers == null) config.Multipliers = new Dictionary<string, float>();
            config.Multipliers[def.shortname] = value;
            SaveConfig();
            player.ChatMessage(Msg("SetOk", player.UserIDString, def.shortname, value.ToString("0.##")));
        }
    }
}

Hooks used: OnDispenserGather, OnDispenserBonus, OnCollectiblePickup

Config file

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

GatherRateMultiplier.json
{
  "Default multiplier": 2,
  "VIP multiplier": 1.5,
  "Minimum amount": 1,
  "Maximum multiplier": 50,
  "Per-resource multipliers": {
    "wood": 2,
    "stones": 2,
    "metal.ore": 2,
    "hq.metal.ore": 1.5,
    "sulfur.ore": 1,
    "cloth": 2,
    "bone.fragments": 2,
    "fat.animal": 1.5
  }
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/gatherratePrint the player's current global multiplier and VIP statusgatherratemultiplier.use
/gatherrate set <shortname|default> <multiplier>Change a per-resource or default multiplier and save the configgatherratemultiplier.admin
/gatherrate reloadReload the config from disk and report the active default multipliergatherratemultiplier.admin
Permissions
PermissionGrants
gatherratemultiplier.useAllows the player to check their own gather multiplier with /gatherrate
gatherratemultiplier.vipApplies the VIP bonus multiplier on top of the per-resource multiplier
gatherratemultiplier.adminAllows changing multipliers and reloading config with /gatherrate set and /gatherrate reload

How the code works

  1. Scaling hits on nodes, trees and corpses

    OnDispenserGather fires after the dispenser has chosen an amount for one hit. The plugin looks up item.info.shortname in the config map, falls back to DefaultMultiplier, and multiplies by VipMultiplier when permission.UserHasPermission(player.UserIDString, PermVip) returns true. item.amount is set to the rounded result, so rounding is done once here rather than per item. Returning null keeps default behavior; returning an object would replace Rock's own gather result, which is rarely what you want.

  2. Handling the depleted bonus

    When a dispenser runs out of resources it awards one extra item through OnDispenserBonus. Because the signature is the same shape as the gather hook, the same scaling helper is reused. The helper takes the multiplier for item.info.shortname, so a sulfur node bonus scales with the sulfur multiplier, not the default. Keep the VIP check in one place so both hooks stay consistent if you change the formula later.

  3. World pickups

    OnCollectiblePickup fires when a player collects hemp, pumpkins, corn, mushrooms, stones, metal ore, wood, berries and similar CollectibleEntity items. The hook does not expose the produced item, only the entity and the receiver, so the plugin walks collectibleEntity.itemList and scales each entry's amount before the pickup is handed to the player. The eat flag is true when corn or pumpkins are consumed instantly; scaling still applies because it happens on the source amounts.

  4. Config layout and fallbacks

    PluginConfig holds a Multipliers dictionary keyed by shortname plus DefaultMultiplier, VipMultiplier, MinAmount and MaxMultiplier. LoadConfig catches parse failures, warns once at startup and rewrites defaults. On multipliers above 1 the rounded result can drop to 0 for low rolls, so MinAmount (default 1) is applied after scaling to avoid a hit that produces nothing. MaxMultiplier caps both config values and admin input.

  5. Admin commands

    /gatherrate prints the caller's default multiplier and VIP status. /gatherrate set <shortname|default> <multiplier> validates the argument against ItemManager.FindItemDefinition so typos do not silently create dead config keys, clamps the value to MaxMultiplier, writes it into config.Multipliers, calls SaveConfig() and confirms with the new value through Msg. /gatherrate reload re-runs LoadConfig and reports the active default.

  6. Permission setup and unload

    All three permissions are registered in Init. The VIP check happens inside the scaling helper only, so removing the permission from a group takes effect immediately without reloading. There are no timers or stored data, so Unload only needs to be present; the plugin keeps no CUI or per-player state.

Install it on your server

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

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

Variations

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

  • Tool-based multipliers

    Give pickaxes and hatchets their own multiplier so upgrading tools pays off.

    Generate this version
  • Zone-based multipliers

    Apply a higher multiplier inside monument or custom radii.

    Generate this version
  • Timed double-gather event

    Run an admin-triggered weekend event that stacks on the normal multiplier.

    Generate this version

FAQ

Does scaling `item.amount` in `OnDispenserGather` scale the whole hit or each resource?

OnDispenserGather fires once per resource item produced by a hit, so trees that drop both wood and sometimes other resources are handled per resource. Each call carries its own item, and the per-shortname multiplier is applied to that item only.

Why does the plugin return null instead of the item?

The hook's return value replaces the original method result. Returning null tells Oxide there is no override, so Rock's gather logic finishes normally with your modified item.amount. Returning an object would substitute your value for the entire result and can skip internal bookkeeping.

Can players stack the VIP permission bonus with the normal multiplier?

Yes. The VIP bonus is multiplicative: the per-resource multiplier is applied first, then the result is multiplied by VipMultiplier (default 1.5). Set VipMultiplier to 1.0 in the config to disable stacking while keeping the permission registered.

Does this affect quarry, pumpjack or excavator output?

No. Quarries, pumpjacks and the excavator use different hooks (OnQuarryGather and OnExcavatorGather). This plugin only covers ResourceDispenser hits, its depleted bonus, and CollectibleEntity pickups.

How do I install it on Oxide or Carbon?

Place the .cs file in oxide/plugins, or let the server compile it on next load. The default config is written to oxide/config/GatherRateMultiplier.json. Edit it and run oxide.reload GatherRateMultiplier from the console. Carbon loads the same file; grant permissions with oxide.grant or Carbon's equivalent.

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 gather multiplier plugin using OnDispenserGather, OnDispenserBonus and OnCollectiblePickup with per-shortname multipliers in config, a default multiplier, a VIP permission bonus, minimum amount of 1, and admin set/reload chat 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 gather rate multiplier

Customize