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
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.
{
"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
| Command | What it does | Permission |
|---|---|---|
/gatherrate | Print the player's current global multiplier and VIP status | gatherratemultiplier.use |
/gatherrate set <shortname|default> <multiplier> | Change a per-resource or default multiplier and save the config | gatherratemultiplier.admin |
/gatherrate reload | Reload the config from disk and report the active default multiplier | gatherratemultiplier.admin |
| Permission | Grants |
|---|---|
gatherratemultiplier.use | Allows the player to check their own gather multiplier with /gatherrate |
gatherratemultiplier.vip | Applies the VIP bonus multiplier on top of the per-resource multiplier |
gatherratemultiplier.admin | Allows changing multipliers and reloading config with /gatherrate set and /gatherrate reload |
How the code works
Scaling hits on nodes, trees and corpses
OnDispenserGatherfires after the dispenser has chosen an amount for one hit. The plugin looks upitem.info.shortnamein the config map, falls back toDefaultMultiplier, and multiplies byVipMultiplierwhenpermission.UserHasPermission(player.UserIDString, PermVip)returns true.item.amountis set to the rounded result, so rounding is done once here rather than per item. Returningnullkeeps default behavior; returning an object would replace Rock's own gather result, which is rarely what you want.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 foritem.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.World pickups
OnCollectiblePickupfires when a player collects hemp, pumpkins, corn, mushrooms, stones, metal ore, wood, berries and similarCollectibleEntityitems. The hook does not expose the produced item, only the entity and the receiver, so the plugin walkscollectibleEntity.itemListand scales each entry's amount before the pickup is handed to the player. Theeatflag is true when corn or pumpkins are consumed instantly; scaling still applies because it happens on the source amounts.Config layout and fallbacks
PluginConfigholds aMultipliersdictionary keyed by shortname plusDefaultMultiplier,VipMultiplier,MinAmountandMaxMultiplier.LoadConfigcatches parse failures, warns once at startup and rewrites defaults. On multipliers above 1 the rounded result can drop to 0 for low rolls, soMinAmount(default 1) is applied after scaling to avoid a hit that produces nothing.MaxMultipliercaps both config values and admin input.Admin commands
/gatherrateprints the caller's default multiplier and VIP status./gatherrate set <shortname|default> <multiplier>validates the argument againstItemManager.FindItemDefinitionso typos do not silently create dead config keys, clamps the value toMaxMultiplier, writes it intoconfig.Multipliers, callsSaveConfig()and confirms with the new value throughMsg./gatherrate reloadre-runsLoadConfigand reports the active default.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, soUnloadonly needs to be present; the plugin keeps no CUI or per-player state.
Install it on your server
Save GatherRateMultiplier.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/GatherRateMultiplier.json(Carbon:carbon/configs/GatherRateMultiplier.json). After editing, reload it:oxide.reload GatherRateMultiplier c.reload GatherRateMultiplier # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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 versionZone-based multipliers
Apply a higher multiplier inside monument or custom radii.
Generate this versionTimed 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.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More gathering plugins
- Auto smelt on gatherConverts gathered metal ore into metal fragments, sulfur ore into sulfur and optionally wood into charcoal for players with a permission, usingโฆOnDispenserGatherOnDispenserBonus
- Farming yield multiplierMultiplies the amount of crops a player receives when harvesting planter crops or wild plants, with a separate multiplier per item shortname.OnServerInitializedOnGrowableGathered
- Fishing catch bonusMultiplies the amount of each caught fish and rolls a configurable chance for bonus items after every successful catch, using the OnFishCaught hook.OnFishCaught
Last updated Oct 26, 2026.
Customize gather rate multiplier
Customize