How to make a Rust gather bonus plugin (VIP tiers)
Gives players with tiered permissions extra resources when they gather from nodes with a ResourceDispenser or pick up collectible entities, using OnDispenserGather and OnCollectiblePickup.
Vanilla Rust pays the same amount per swing to everyone, so a VIP gather bonus needs to be applied after the game has already decided the yield. The two hooks that matter are OnDispenserGather, which fires for each item a player pulls from a tree, ore node or other ResourceDispenser, and OnCollectiblePickup, which fires when a player collects a hemp bush, mushroom or other CollectibleEntity. Both receive the finished Item, so raising item.amount before the pickup is finalized is enough. The plugin below reads a per-tier multiplier from its config, picks the best tier the player holds from a configurable permission list, scales the gathered or collected amount, and awards the difference to the player. A global multiplier lets admins run a short double-gather event, and a chat command shows a player their current multiplier.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
namespace Oxide.Plugins
{
[Info("Vip Gather Bonus", "RustPluginGenerator", "1.0.0")]
[Description("Gives permission-tiered players extra resources when gathering and picking up collectibles.")]
public class VipGatherBonus : RustPlugin
{
private const string PermUse = "vipgatherbonus.use";
private PluginConfig config;
private readonly Dictionary<string, float> tierCache = new Dictionary<string, float>();
private Timer refreshTimer;
private class Tier
{
[JsonProperty("Permission suffix")]
public string Permission { get; set; } = "vip1";
[JsonProperty("Multiplier")]
public float Multiplier { get; set; } = 1.25f;
}
private class PluginConfig
{
[JsonProperty("Global bonus multiplier")]
public float GlobalBonus { get; set; } = 1f;
[JsonProperty("Top multiplier only")]
public bool TopMultiplierOnly { get; set; } = true;
[JsonProperty("Tier permissions")]
public List<Tier> Tiers { get; set; } = new List<Tier>
{
new Tier { Permission = "vip1", Multiplier = 1.25f },
new Tier { Permission = "vip2", Multiplier = 1.5f },
new Tier { Permission = "vip3", Multiplier = 2f }
};
}
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>
{
["NoBonus"] = "You have no gather bonus. Ask an admin about VIP tiers.",
["Bonus"] = "Your gather bonus is x{0} (tier: {1}).",
["Global"] = "Your gather bonus is x{0}.",
["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);
if (config.Tiers != null)
{
foreach (var tier in config.Tiers)
{
if (tier == null || string.IsNullOrEmpty(tier.Permission)) continue;
permission.RegisterPermission(Prefix + "." + tier.Permission, this);
}
}
}
private void OnServerInitialized()
{
RefreshCache();
refreshTimer = timer.Every(60f, RefreshCache);
}
private void Unload()
{
refreshTimer?.Destroy();
tierCache.Clear();
}
private string Prefix => Name.ToLower().Replace(" ", "");
private void RefreshCache()
{
tierCache.Clear();
foreach (var player in BasePlayer.activePlayerList)
{
if (player == null || player.IsNpc) continue;
tierCache[player.UserIDString] = ResolveMultiplier(player.UserIDString);
}
}
private float ResolveMultiplier(string userId)
{
float result = 1f;
float top = 1f;
if (config.Tiers != null)
{
foreach (var tier in config.Tiers)
{
if (tier == null || tier.Multiplier <= 1f) continue;
if (!permission.UserHasPermission(userId, Prefix + "." + tier.Permission)) continue;
if (tier.Multiplier > top) top = tier.Multiplier;
if (!config.TopMultiplierOnly) result *= tier.Multiplier;
}
}
if (config.TopMultiplierOnly) result = top;
return result * Math.Max(1f, config.GlobalBonus);
}
private float GetMultiplier(BasePlayer player)
{
if (player == null) return 1f;
float cached;
if (tierCache.TryGetValue(player.UserIDString, out cached)) return cached;
float value = ResolveMultiplier(player.UserIDString);
tierCache[player.UserIDString] = value;
return value;
}
private int ScaleAmount(Item item, float multiplier)
{
if (item == null || item.info == null) return 0;
int scaled = (int)Math.Ceiling(item.amount * multiplier);
if (item.info.stackable > 0 && scaled > item.info.stackable) scaled = item.info.stackable;
return scaled < item.amount ? item.amount : scaled;
}
private object OnDispenserGather(ResourceDispenser resourceDispenser, BasePlayer entity, Item item)
{
if (entity == null || entity.IsNpc || item == null) return null;
float multiplier = GetMultiplier(entity);
if (multiplier <= 1f) return null;
item.amount = ScaleAmount(item, multiplier);
return null;
}
private object OnCollectiblePickup(CollectibleEntity collectibleEntity, BasePlayer reciever, bool eat)
{
if (reciever == null || reciever.IsNpc || eat) return null;
float multiplier = GetMultiplier(reciever);
if (multiplier <= 1f) return null;
var item = reciever.GetActiveItem();
if (item == null) return null;
item.amount = ScaleAmount(item, multiplier);
return null;
}
[ChatCommand("gatherbonus")]
private void CmdGatherBonus(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
player.ChatMessage(Msg("NoPermission", player.UserIDString));
return;
}
float multiplier = GetMultiplier(player);
string tierName = "none";
if (config.Tiers != null)
{
foreach (var tier in config.Tiers)
{
if (tier == null) continue;
if (permission.UserHasPermission(player.UserIDString, Prefix + "." + tier.Permission))
{
tierName = tier.Permission;
if (config.TopMultiplierOnly) break;
}
}
}
if (multiplier <= 1f) player.ChatMessage(Msg("NoBonus", player.UserIDString));
else if (tierName == "none") player.ChatMessage(Msg("Global", player.UserIDString, multiplier));
else player.ChatMessage(Msg("Bonus", player.UserIDString, multiplier, tierName));
}
}
}Hooks used: OnServerInitialized, OnDispenserGather, OnCollectiblePickup
Config file
Created automatically on first load at oxide/config/VipGatherBonus.json.
{
"Global bonus multiplier": 1,
"Top multiplier only": true,
"Tier permissions": [
{
"Permission suffix": "vip1",
"Multiplier": 1.25
},
{
"Permission suffix": "vip2",
"Multiplier": 1.5
},
{
"Permission suffix": "vip3",
"Multiplier": 2
}
]
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/gatherbonus | Show your current gather tier and bonus multiplier. | vipgatherbonus.use |
| Permission | Grants |
|---|---|
vipgatherbonus.use | Allows using the /gatherbonus command. |
vipgatherbonus.vip1 | Tier 1 gather bonus, set by the Tier 1 multiplier in the config. |
vipgatherbonus.vip2 | Tier 2 gather bonus, set by the Tier 2 multiplier in the config. |
vipgatherbonus.vip3 | Tier 3 gather bonus, set by the Tier 3 multiplier in the config. |
How the code works
Model the tiers in the config
The
Tier permissionskey is a list of objects, each with aPermission suffix(vip1,vip2,vip3) and aMultiplier. The plugin builds the full node from its own name:Prefixis the class name lowercased with spaces removed, so the full permission isvipgatherbonus.vip1and so on. List them lowest to highest.Top multiplier onlydecides whether holding several tiers gives the best one (default) or multiplies them together, andGlobal bonus multiplieris applied to the final result so admins can run a temporary double-gather event.Register permissions at startup
Initregistersvipgatherbonus.useand then walksconfig.Tiers, registeringPrefix + "." + tier.Permissionfor each. Registering here means the nodes exist in the permissions UI before anyone is granted them. If you edit the tier list in the config and reload the plugin, the new nodes are registered on the next reload.Pick the best tier for a player
ResolveMultiplier(string userId)starts at 1, walks the tier list withpermission.UserHasPermission, and either keeps the highest multiplier or multiplies them whenTop multiplier onlyis false. The result is multiplied byGlobal bonus multiplier.GetMultiplier(BasePlayer)reads a dictionary keyed onUserIDString; the dictionary is filled byRefreshCache, which runs onOnServerInitializedand then every 60 seconds through atimer.Every, so a revoked permission takes effect within a minute without a permission check on every swing.Scale the swing yield in OnDispenserGather
The hook runs for every item a
ResourceDispenserproduces. The plugin null-checks the dispenser and player, ignores NPC gatherers viaIsNpc, gets the multiplier and, if it is above 1, setsitem.amountthroughScaleAmount. That helper rounds up, caps atitem.info.stackable, and never lowers the amount below what the game produced. Returning null keeps Oxide's normal handling, which grants the item to the player.Scale collectibles in OnCollectiblePickup
CollectibleEntitypickups (hemp, berries, mushrooms) go through a different hook and pass aneatflag. When the player is eating the collectible in the field the item is consumed immediately, so the plugin returns early foreat == trueand only adjustsitem.amountwhen a real item is going into the inventory. It reads that item fromreciever.GetActiveItem()and reusesScaleAmountso both paths round the same way.Report and clean up
/gatherbonusis guarded byvipgatherbonus.use, resolves the multiplier withGetMultiplier, finds the highest tier the player holds, and prints one of three messages throughlang: no bonus, global-only bonus, or a tier bonus with both the multiplier and the tier suffix. OnUnloadthe refresh timer is destroyed and the dictionary cleared. No data is written to disk; tier state always comes from the permission system.
Install it on your server
Save VipGatherBonus.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/VipGatherBonus.json(Carbon:carbon/configs/VipGatherBonus.json). After editing, reload it:oxide.reload VipGatherBonus c.reload VipGatherBonus # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default vipgatherbonus.use c.grant group default vipgatherbonus.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Exclude specific nodes
Skip the bonus for trees or ore you want to keep slow.
Generate this versionAdd a daily cap
Limit bonus resources per player per day.
Generate this versionAnnounce in chat
Tell players which tier they just used.
Generate this version
FAQ
Does the bonus apply to the tool decay or durability?
No. OnDispenserGather only changes the produced Item. Damage to the tool is handled elsewhere, so a tier 3 player's pickaxe wears out at the same rate as everyone else's.
Why do I only see whole numbers?
item.amount is an integer. ScaleAmount uses Math.Ceiling, so a 1.25x multiplier on a 10 yield gives 13. A yield of 1 becomes 2 with any multiplier above 1, which is why Top multiplier only exists for stacked tiers.
Do bonuses stack by default?
No. With Top multiplier only at true the plugin uses the highest tier the player holds. Set it to false to multiply every held tier together, and raise Global bonus multiplier for events instead.
Does it work on Carbon?
Yes. Carbon loads the same Oxide plugin file. Drop VipGatherBonus.cs into oxide/plugins and grant the vipgatherbonus.vip1 style permissions with your permissions plugin or oxide.grant.
Why didn't my permission take effect immediately?
Tier lookups are cached and refreshed every 60 seconds to keep the gather hooks cheap. Reload the plugin (oxide.reload VipGatherBonus) or wait for the refresh after granting a permission.
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 vip gather bonus
Customize