Skip to content
RustPluginGenerator

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.

Signature
void OnDispenserGathered(ResourceDispenser resourceDispenser, BasePlayer player, Item item)

Parameters

Hook parameters
ParameterTypeDescription
resourceDispenserResourceDispenserThe 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.
playerBasePlayerThe BasePlayer who harvested the node. This is the gatherer whose gather call ran.
itemItemThe 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

DispenserGatherTracker.cs
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.amount for 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.amount upward 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), player may be null or an NPC. Null-check before touching UserIDString or permissions.

Plugin recipes that use OnDispenserGathered

Build a plugin with OnDispenserGathered

Describe what should happen and get a compiled plugin that uses this hook.

Prompt: Economy-based resource rewards using the OnDispenserGathered hook, with a config file and a permission
Generate this plugin

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