OnDispenserGathered
OnDispenserGathered is called after a player successfully harvests a resource node and the resulting item has been created but before it is given to the player's inventory. It fires from ResourceDispenser.GiveResourceFromItem, so it covers trees, ore, hemp and other dispensers that hand out items.
void OnDispenserGathered(ResourceDispenser resourceDispenser, BasePlayer player, Item item)Parameters
| Parameter | Type | Description |
|---|---|---|
resourceDispenser | ResourceDispenser | The ResourceDispenser entity that produced the resource, typically a tree, ore node, hemp bush or corpse. Use its baseEntity position and ShortPrefabName to tell resource types apart. |
player | BasePlayer | The BasePlayer who harvested the node. This is the gatherer whose gather call ran. |
item | Item | The Item that was just created and about to be handed to the player. Its amount, info.shortname and skin describe the yield; this is the item before stack overflow splits happen. |
Return value
Return values are ignored. The patched code calls OnDispenserGathered and then continues into the stack-splitting loop regardless of what your hook returns. Nothing you return cancels the harvest or changes the item.
How it works
The hook fires inside ResourceDispenser.GiveResourceFromItem, after the gather analytics call and before the yield loop that splits oversized stacks into the player's inventory. At that point the item exists with its final amount (base yield plus any bonus applied earlier in the same method) and the player is still the intended receiver.
The resourceDispenser argument is the node; resourceDispenser.baseEntity gives you the world object (position, prefab name, owner). player is the gatherer. item is the produced Item — read item.info.shortname for the resource type and item.amount for the size of the yield.
Declaring player as a more specific type does not help here because it is always a BasePlayer; the useful narrowing is on the node side, but the parameter is typed ResourceDispenser, so you cannot swap it for TreeEntity and still receive calls. Cast resourceDispenser.baseEntity instead when you need subtype fields.
Returning anything does not stop the gather. The hook is observational: modifying item.amount or item.Remove() inside the hook affects the item before the split loop, but the default path still runs.
Example plugin: Per-player gather tracker with permission bonus
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Dispenser Gather Tracker", "RustPluginGenerator", "1.0.0")]
[Description("Tracks resources gathered per player and grants a permission-based yield bonus.")]
public class DispenserGatherTracker : RustPlugin
{
private const string PermBonus = "dispensergathertracker.bonus";
private StoredData data = new StoredData();
private PluginConfig config;
private class PluginConfig
{
[JsonProperty("Bonus multiplier per shortname (e.g. wood -> 0.25 adds 25%)")]
public Dictionary<string, double> BonusByShortname = new Dictionary<string, double>
{
["wood"] = 0.25,
["stones"] = 0.2,
["sulfur.ore"] = 0.15
};
}
private class StoredData
{
public Dictionary<string, Dictionary<string, long>> ByPlayer = new Dictionary<string, Dictionary<string, long>>();
}
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>
{
["Stats"] = "Your top gathered resources: {0}",
["NoStats"] = "You have not gathered any tracked resources yet.",
["OthersStats"] = "{0}'s top resources: {1}"
}, 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(PermBonus, this);
data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name) ?? new StoredData();
}
private void Unload()
{
Interface.Oxide.DataFileSystem.WriteObject(Name, data);
}
private void OnDispenserGathered(ResourceDispenser resourceDispenser, BasePlayer player, Item item)
{
if (player == null || item == null || item.info == null) return;
string shortname = item.info.shortname;
if (permission.UserHasPermission(player.UserIDString, PermBonus)
&& config.BonusByShortname.TryGetValue(shortname, out double mult))
{
int extra = Mathf.FloorToInt(item.amount * (float)mult);
if (extra > 0)
{
item.amount += extra;
}
}
Dictionary<string, long> totals;
if (!data.ByPlayer.TryGetValue(player.UserIDString, out totals))
{
totals = new Dictionary<string, long>();
data.ByPlayer[player.UserIDString] = totals;
}
long current;
totals.TryGetValue(shortname, out current);
totals[shortname] = current + item.amount;
}
[ChatCommand("gatherstats")]
private void CmdGatherStats(BasePlayer player, string command, string[] args)
{
if (player == null) return;
Dictionary<string, long> totals;
if (!data.ByPlayer.TryGetValue(player.UserIDString, out totals) || totals.Count == 0)
{
SendReply(player, Msg("NoStats", player.UserIDString));
return;
}
SendReply(player, Msg("Stats", player.UserIDString, Summarize(totals)));
}
[ConsoleCommand("dispensergathertracker.stats")]
private void CcmdStats(ConsoleSystem.Arg arg)
{
if (!arg.IsAdmin) { arg.ReplyWith("Admin only."); return; }
string target = arg.GetString(0, "");
if (string.IsNullOrEmpty(target)) { arg.ReplyWith("Usage: dispensergathertracker.stats <steamid>"); return; }
Dictionary<string, long> totals;
if (!data.ByPlayer.TryGetValue(target, out totals)) { arg.ReplyWith("No data for that player."); return; }
arg.ReplyWith(Msg("OthersStats", null, target, Summarize(totals)));
}
private string Summarize(Dictionary<string, long> totals)
{
var parts = new List<string>();
foreach (var kv in totals)
{
parts.Add(kv.Key + ": " + kv.Value);
}
return string.Join(", ", parts);
}
}
}The plugin keeps a running total of resources harvested per player and per shortname, persisted to oxide/data so totals survive restarts. On OnDispenserGathered it looks up the shortname in the configured bonus table and, if the player holds the dispensergathertracker.bonus permission, adds a proportional extra amount to the already-created item before the game gives it to the inventory. Totals are incremented by the real item amount after any bonus. A chat command prints the caller's top resources, and an admin console command prints another player's totals. Unload writes the data file; there is no timer to destroy because nothing is scheduled.
Common use cases
Economy-based resource rewards
Hook the gathered item and credit an Economics balance per shortname (stone, sulfur, wood) to reward active farmers or drive a server shop.
Per-player gather statistics
Accumulate counts keyed by UserIDString and shortname to build leaderboards, weekly quotas or wipe tracking without polling inventories.
VIP yield multiplier
Add extra amount to
item.amountfor players with a permission, so the bonus feels like part of the harvest rather than a separate giveaway.
Pitfalls
Item amount is not final for the inventory
The game splits oversized stacks after this hook. Modifying
item.amountupward is fine, but expect the split loop to hand out several smaller items rather than one huge stack.Hook fires per yield item, not per swing
A single hit can produce multiple items (multi-resource nodes, corpse gather). Aggregate when you need per-gather totals, and do not assume one call equals one swing.
No player context when gathering via bots or server-driven dispensers
If the dispenser is triggered without a real player (server spawns, NPC logic),
playermay be null or an NPC. Null-check before touchingUserIDStringor permissions.
Plugin recipes that use OnDispenserGathered
Build a plugin with OnDispenserGathered
Describe what should happen and get a compiled plugin that uses this hook.
Edit the prompt on the next screen. 2 free plugins a month, no card.
Last updated Oct 26, 2026. Found a mistake? Email support@rustplugingenerator.com.
Build a plugin with OnDispenserGathered
Generate