OnGrowableGather
OnGrowableGather is called when a player tries to pick a growable plant, right before the game validates eligibility and gives the fruit. Returning a non-null value cancels the pick.
object OnGrowableGather(GrowableEntity growableEntity, BasePlayer player, bool eat)Parameters
| Parameter | Type | Description |
|---|---|---|
growableEntity | GrowableEntity | The plant being picked (GrowableEntity). Use it to read the current growth state, its position and net ID, or to identify the planted seed through the entity's prefab name. |
player | BasePlayer | The BasePlayer performing the pick. It can be null when the action is not driven by a connected player, so always null-check before reading inventory, permissions or sending chat. |
eat | bool | True when the pick was requested through the eat path (the player is consuming the fruit directly), false for a normal harvest into the inventory. |
Return value
Returning null lets the game continue with its own eligibility check and the fruit grant. Returning any non-null value (even false or 0) makes PickFruit return immediately, so the player gets nothing and the harvest counter is not increased.
How it works
The hook fires inside GrowableEntity.PickFruit, which the game calls when a player interacts with a ripe plant such as a hemp, berry or pumpkin bush to take its fruit. It runs before the harvest counter is incremented and before fruit is created, so at this point nothing has been removed from the plant and no items exist yet. The eat flag tells you whether the player asked to consume the fruit on the spot rather than stash it. Declaring growableEntity as GrowableEntity already narrows the hook to plants; you can additionally check the prefab shortname to only act on specific crops. Because the return value is checked, a non-null result stops the whole pick, including the harvest counter and the fruit grant. This is a per-pick call on player input, so keep the body cheap: no Puts or PrintWarning, no data writes without a guard. It fires on both ripe and unripe attempts, since the game's CanPick check happens after the hook.
Example plugin: Enforce a per-player hourly berry harvest limit
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Growable Harvest Limit", "RustPluginGenerator", "1.0.0")]
[Description("Caps how many fruit a player can pick per hour from configured plants")]
public class GrowableHarvestLimit : RustPlugin
{
private class PluginConfig
{
[JsonProperty("Hourly pick limit")]
public int HourlyLimit = 150;
[JsonProperty("Restricted plant shortnames")]
public List<string> Plants = new List<string> { "hemp", "berry", "pumpkin", "corn" };
[JsonProperty("Notify player when limit reached")]
public bool Notify = true;
}
private PluginConfig config;
// userId -> (unix hour -> picks)
private readonly Dictionary<string, int> counts = new Dictionary<string, int>();
private readonly Dictionary<string, int> window = new Dictionary<string, int>();
private readonly HashSet<string> restricted = new HashSet<string>();
protected override void LoadDefaultConfig() => config = new PluginConfig();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<PluginConfig>();
if (config == null || config.Plants == 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>
{
["LimitReached"] = "You have reached your hourly harvest limit of {0} fruit.",
["LimitCleared"] = "Your hourly harvest limit has reset."
}, this);
}
private string Msg(string key, string userId = null, params object[] args) => string.Format(lang.GetMessage(key, this, userId), args);
private void OnServerInitialized()
{
restricted.Clear();
foreach (var name in config.Plants)
{
if (!string.IsNullOrEmpty(name)) restricted.Add(name.ToLowerInvariant());
}
}
private object OnGrowableGather(GrowableEntity growableEntity, BasePlayer player, bool eat)
{
if (growableEntity == null || player == null || player.IsNpc) return null;
var shortName = growableEntity.ShortPrefabName ?? string.Empty;
var kind = shortName.Replace(".entity", string.Empty).ToLowerInvariant();
if (!restricted.Contains(kind)) return null;
var userId = player.UserIDString;
var hour = (int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalHours;
int currentWindow;
if (!window.TryGetValue(userId, out currentWindow) || currentWindow != hour)
{
window[userId] = hour;
counts[userId] = 0;
}
int used;
counts.TryGetValue(userId, out used);
if (used >= config.HourlyLimit)
{
if (config.Notify)
{
SendReply(player, Msg("LimitReached", userId, config.HourlyLimit));
config.Notify = false;
timer.Once(3600f, () => config.Notify = true);
}
return true;
}
counts[userId] = used + 1;
return null;
}
private void Unload()
{
counts.Clear();
window.Clear();
}
}
}The plugin defines a configurable hourly cap on how many fruit a player may pick, plus a list of plant shortnames it applies to. On OnGrowableGather it looks up the plant's short prefab name; if the plant is not in the restricted list it returns null and the pick proceeds normally. For restricted plants it counts the player's picks in the current hour window, resets the counter when the window expires, and cancels the pick with a non-null return once the cap is reached, sending a reply at most once per window. Counts live in a nested dictionary and are cleared in Unload.
Common use cases
Rate-limit automated harvesting
Cap how many fruit a single player can pick per minute to blunt macro or script-assisted farming without blocking normal play.
Restrict picking to permission holders
Cancel the pick when the player lacks a grower permission or is not authorised in the nearby building privilege.
Reward special harvests
Track picks of specific crops per player and hand out a bonus item through a timer while still letting the vanilla harvest continue.
Pitfalls
Returning false cancels the pick
The check is for any non-null value, so
return false;blocks the harvest just likereturn true;. Only return null when you want the vanilla pick to go through.The hook runs before eligibility
OnGrowableGatherfires even whenCanPickwould reject the attempt, so your counters can grow from unripe picks. Verify ripeness on the plant if that matters.Null players and hot path
playercan be null and this hook fires on every pick, so null-check first and never call logging or data writes inside it unguarded.
Build a plugin with OnGrowableGather
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 OnGrowableGather
Generate