OnExperimentEnded
OnExperimentEnded is called on the server right after a Workbench finishes an experiment cycle and unlocks its inventory. It fires once per completed cycle, regardless of whether the experiment succeeded or failed.
void OnExperimentEnded(Workbench workbench)Parameters
| Parameter | Type | Description |
|---|---|---|
workbench | Workbench | The Workbench entity that finished the experiment cycle (Tier 1, Tier 2 or Tier 3). Still spawned and usable; its inventory is unlocked again by the time the hook fires. |
Return value
The return value is ignored. The injection site calls Interface.CallHook("OnExperimentEnded", this) as a plain statement with no branch or assignment, so returning null, true or anything else has no effect on the game's completion logic.
How it works
The hook fires inside Workbench.ExperimentComplete, after the workbench clears its On flag, nulls pendingBlueprint, sets inventory.SetLocked(false) and sends a network update. In practice that means it runs every time a research cycle finishes: the client-side progress timer ran out, the workbench IsOn() state went false, and the container is openable and movable again. The single parameter is the Workbench entity itself, so you can read its transform.position, OwnerID, net.ID.Value, inventory, or check the grade via the concrete type.
Declaring Workbench (as given) is the only correct type; narrowing to a subclass is impossible because Tier 1/2/3 workbenches all use the same class, so you must inspect ShortPrefabName or net.ID.Value if you need per-tier behavior. The hook does not tell you whether the roll succeeded or lost, and it gives no player reference, so resolve the owner through workbench.OwnerID (or the parent entity if the workbench is deployed). Because it fires only on completed cycles and not on cancel, it is a reliable marker for "a research attempt finished on this bench".
Example plugin: Chat feedback and statistics when a bench finishes
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Experiment Finished Notice", "RustPluginGenerator", "1.0.0")]
[Description("Notifies bench owners when a Workbench experiment cycle finishes and tracks per-player totals.")]
public class ExperimentFinishedNotice : RustPlugin
{
private const string PermStats = "experimentfinishednotice.stats";
private PluginConfig config;
private readonly Dictionary<ulong, int> totals = new Dictionary<ulong, int>();
private class PluginConfig
{
[JsonProperty("Send chat notice to owner")]
public bool SendNotice = true;
[JsonProperty("Include bench tier in notice")]
public bool IncludeTier = 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>
{
["Notice"] = "Experiment cycle finished on your {0} bench.",
["Bench"] = "Workbench",
["Stats"] = "You have completed {0} experiment cycles since the plugin loaded.",
["NoPerm"] = "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(PermStats, this);
}
private void OnExperimentEnded(Workbench workbench)
{
if (workbench == null || workbench.IsDestroyed) return;
ulong ownerId = workbench.OwnerID;
if (ownerId == 0) return;
int count;
totals.TryGetValue(ownerId, out count);
totals[ownerId] = count + 1;
if (!config.SendNotice) return;
BasePlayer owner = BasePlayer.FindByID(ownerId);
if (owner == null || !owner.IsConnected) return;
string tier = config.IncludeTier ? GetTierName(workbench) : Msg("Bench", owner.UserIDString);
owner.ChatMessage(Msg("Notice", owner.UserIDString, tier));
}
private string GetTierName(Workbench bench)
{
string prefab = bench.ShortPrefabName ?? string.Empty;
if (prefab.Contains("tier3")) return "Tier 3";
if (prefab.Contains("tier2")) return "Tier 2";
return "Tier 1";
}
[ChatCommand("bench")]
private void CmdBench(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermStats))
{
player.ChatMessage(Msg("NoPerm", player.UserIDString));
return;
}
int count;
totals.TryGetValue(player.userID.Get(), out count);
player.ChatMessage(Msg("Stats", player.UserIDString, count));
}
}
}The plugin registers a chat command /bench that prints the calling player's completed experiment count. OnExperimentEnded records the owner's user ID in a small in-memory dictionary and increments their total; it then messages the owner if connected and the config toggle is on. No log spam is used inside the hook, only a chat message to the owner. The bench tier is derived from ShortPrefabName so the notice can name the tier. A permission gates the /bench command, and the config allows disabling the chat notice while keeping counters running.
Common use cases
Post-experiment feedback message
Send the bench owner a short chat message telling them the cycle finished, e.g. with an optional hint about their next research attempt.
Track research activity per player
Increment a per-user counter in oxide/data when
workbench.OwnerIDmatches a player, then expose it through a command for server reporting.Trigger bench-linked effects
Run a particle effect or sound at
workbench.transform.positiononly when a bench actually completes a cycle, e.g. for a themed server hub area.
Pitfalls
No success/failure information
The hook does not tell you whether the roll succeeded. If you need the outcome, you would need to inspect player inventory or use
OnExperimentEnd(before completion) plus your own timing logic.Cancel is not covered
Picking up or destroying the bench during a cycle typically does not call this hook, because
ExperimentCompletenever runs. Clear any pending state you set insideOnExperimentStarton entity kill as well.OwnerID may be 0 or not the user
workbench.OwnerIDis the deployer, which may be 0 for benches placed by server tooling. Guard against 0 before callingBasePlayer.FindByIDand always null-check the result.
Build a plugin with OnExperimentEnded
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 OnExperimentEnded
Generate