OnGrowableStateChange
OnGrowableStateChange is called when a GrowableEntity is about to move to a new plant growth state, such as Seed to Sprout or Mature to Fruiting. Returning a non-null value cancels the transition.
object OnGrowableStateChange(GrowableEntity growableEntity, PlantProperties.State state)Parameters
| Parameter | Type | Description |
|---|---|---|
growableEntity | GrowableEntity | The plant or growable whose state is about to change. Use it to read position, OwnerID, prefab and find the planting entity or player that owns the crop. |
state | PlantProperties.State | The PlantProperties.State the growable is transitioning to (for example Seed, Sprout, Sapling, Mature, Fruiting, Dying, Dead). Compare it to growableEntity.State to detect real growth events. |
Return value
Returning null lets the state change proceed normally. Returning any non-null value aborts the transition: the method returns before State is updated, so the growable keeps its current state, age is not reset and no growth effects or network updates are applied.
How it works
The hook fires inside GrowableEntity.ChangeState, before the growable's State field is assigned and before any server side effects (age reset, network update, effects) run. It fires whenever the game advances a plant's growth stage: a seed genome maturing on a planter, a crop reaching fruiting, or a plant dying from neglect. It is not tied to player interaction, so one hook call per state transition per growable.
The state argument is the destination state, not the current one. Compare it against growableEntity.State to know what the plant is moving from. Useful fields on growableEntity are transform.position, OwnerID, ShortPrefabName and the parent planter entity, which let you attribute the change to a player or zone.
Because every argument is typed GrowableEntity, declaring a more specific subclass parameter would filter out plain GrowableEntity instances; keep the declared type as given unless you know you only care about a custom prefab class. The hook also fires while loading a saved plant, so avoid heavy work or player notifications for transitions that arrive during a load.
Example plugin: Freeze crop growth for a permission
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("GrowableGrowthFreezer", "RustPluginGenerator", "1.0.0")]
[Description("Blocks plant growth state transitions for players with a permission")]
public class GrowableGrowthFreezer : RustPlugin
{
private const string PermUse = "growablegrowthfreezer.freeze";
private PluginConfig config;
private readonly Dictionary<ulong, ulong> frozenPlants = new Dictionary<ulong, ulong>();
private class PluginConfig
{
[JsonProperty("Only block after server is ready")]
public bool OnlyAfterReady { get; set; } = true;
}
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>
{
["FreezeOn"] = "Crop growth freezing is now enabled for you ({0} tracked plants).",
["FreezeOff"] = "Crop growth freezing is now disabled for you.",
["NoPermission"] = "You do not have permission to use this command.",
["Loaded"] = "GrowableGrowthFreezer loaded. {0} plants were restored."
}, 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);
}
private void OnServerInitialized()
{
Puts(Msg("Loaded", null, frozenPlants.Count));
}
[ConsoleCommand("growable.freeze")]
private void CmdFreeze(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player == null) return;
if (!player.IsAdmin && !permission.UserHasPermission(player.UserIDString, PermUse))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
bool enable = arg.GetBool(0, true);
if (enable)
{
if (!permission.UserHasPermission(player.UserIDString, PermUse))
permission.GrantUserPermission(player.UserIDString, PermUse, this);
SendReply(player, Msg("FreezeOn", player.UserIDString, CountOwned(player.userID.Get())));
}
else
{
permission.RevokeUserPermission(player.UserIDString, PermUse);
SendReply(player, Msg("FreezeOff", player.UserIDString));
}
}
private int CountOwned(ulong ownerId)
{
int count = 0;
foreach (var kvp in frozenPlants)
{
if (kvp.Value == ownerId) count++;
}
return count;
}
private object OnGrowableStateChange(GrowableEntity growableEntity, PlantProperties.State state)
{
if (growableEntity == null) return null;
if (state == PlantProperties.State.Dying) return null;
if (config.OnlyAfterReady && !ServerMgr.Instance) return null;
ulong ownerId = growableEntity.OwnerID;
if (ownerId == 0) return null;
if (!permission.UserHasPermission(ownerId.ToString(), PermUse)) return null;
if (growableEntity.net == null) return null;
frozenPlants[growableEntity.net.ID.Value] = ownerId;
return false;
}
private void Unload()
{
frozenPlants.Clear();
}
}
}The plugin registers the growablegrowthfreezer.freeze permission and tracks frozen crops in an in-memory dictionary keyed by the growable's network id. In OnGrowableStateChange, it ignores dying plants, returns null before the server is ready when the config option Only block after server is ready is enabled, and otherwise blocks the transition with false only when the growable has a non-zero OwnerID whose user has the permission. The growable.freeze console command takes an optional boolean (default true); admins and players who already hold the permission can use it to grant or revoke the permission for themselves, and enabling reports how many already-tracked plants that player owns. The tracked set is not persisted: it is cleared on unload.
Common use cases
Pause growth for donators
Return a non-null value for plants owned by players with a permission so their crops stay at the current stage while they are away.
Announce rare mutations
When the destination state is Mature, check the plant's genome or owner team and send a one-time chat message to the planting player.
Prevent dying stage on owned farms
Block the transition to Dying for plants inside a protected zone so crops cannot wither during water or light shortages.
Pitfalls
Fires during load
ChangeState is also used when restoring saved plants, so blocking every transition can leave loaded plants stuck or inconsistent. Check whether the server has finished initializing before enforcing rules.
Returning true is not a value
This hook uses the return only as a cancel signal. Returning
falseortrueboth abort the change; onlynulllets it continue.No player context
There is no BasePlayer parameter. Resolve ownership via
growableEntity.OwnerIDor the parent planter, and treat OwnerID 0 as unowned ground plants.
Build a plugin with OnGrowableStateChange
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 OnGrowableStateChange
Generate