OnTechTreeNodeUnlock
OnTechTreeNodeUnlock is called when a player requests a tech tree node unlock at a Workbench, before the server checks and spends scrap. Returning a non-null value cancels the vanilla unlock path.
object OnTechTreeNodeUnlock(Workbench workbench, TechTreeData.NodeInstance byID, BasePlayer player)Parameters
| Parameter | Type | Description |
|---|---|---|
workbench | Workbench | The Workbench entity the player used to open the tech tree and request the unlock. Use it for position checks, ownership or building privilege lookups. |
byID | TechTreeData.NodeInstance | The TechTreeData.NodeInstance being unlocked. Its node data identifies the blueprint/item; treat it as read-only and null-check it before reading members. |
player | BasePlayer | The BasePlayer who sent the unlock request over RPC. Always null-check it; it is the player whose scrap the game will spend next. |
Return value
Return null to let the vanilla unlock proceed: the game then checks the player's scrap, deducts it and grants the blueprint. Return any non-null object to stop the original code path, and the unlock is silently cancelled with no scrap spent and no blueprint granted.
How it works
This hook fires inside Workbench.RPC_TechTreeUnlock, the RPC a client sends when the player clicks a node in the tech tree UI. It runs after the workbench has located the requested node from the client-supplied id and before the server reads the player's scrap amount, so it is the earliest point where you can accept or refuse the request.
The three arguments map directly to the request: workbench is the entity that routes the RPC, byID is the resolved node instance, and player is the requesting player. There is no separate blueprint argument; if you need the item, read it from the node data rather than from the player's inventory.
The injected call treats any non-null return as a veto: the method returns immediately and the vanilla scrap check, blueprint grant and item grant never run. Because of that, cancelling this hook never charges the player. You cannot use the return value to substitute a different node or a different cost.
Declaring player as a more specific type does not help here (the argument is already most specific), but it is still worth checking player for null and for NPC flags, since the RPC is client-driven. Fires once per unlock request, not repeatedly; it is safe to run heavier logic than in damage or input hooks.
Example plugin: Restrict tech tree unlocks to a permission and log them
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
namespace Oxide.Plugins
{
[Info("Tech Tree Unlock Guard", "RustPluginGenerator", "1.0.0")]
[Description("Gates workbench tech tree unlocks behind a permission and logs accepted unlocks")]
public class TechTreeUnlockGuard : RustPlugin
{
private const string PermUse = "techtreeunlockguard.use";
private PluginConfig config;
private UnlockLog log = new UnlockLog();
private class PluginConfig
{
[JsonProperty("Require permission to unlock nodes")]
public bool RequirePermission { get; set; } = true;
[JsonProperty("Maximum log entries kept")]
public int MaxLogEntries { get; set; } = 500;
}
private class UnlockEntry
{
[JsonProperty("Player name")]
public string PlayerName { get; set; }
[JsonProperty("Player id")]
public string PlayerId { get; set; }
[JsonProperty("Node id")]
public int NodeId { get; set; }
[JsonProperty("Workbench position")]
public string Position { get; set; }
[JsonProperty("Time (UTC)")]
public string Time { get; set; }
}
private class UnlockLog
{
[JsonProperty("Unlocks")]
public List<UnlockEntry> Unlocks { get; set; } = new List<UnlockEntry>();
}
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>
{
["NoPermission"] = "You do not have permission to unlock tech tree nodes."
}, 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);
log = Interface.Oxide.DataFileSystem.ReadObject<UnlockLog>(Name) ?? new UnlockLog();
}
private object OnTechTreeNodeUnlock(Workbench workbench, TechTreeData.NodeInstance byID, BasePlayer player)
{
if (player == null || player.IsNpc) return null;
if (byID == null) return null;
if (config.RequirePermission && !permission.UserHasPermission(player.UserIDString, PermUse))
{
player.ChatMessage(Msg("NoPermission", player.UserIDString));
return false;
}
log.Unlocks.Add(new UnlockEntry
{
PlayerName = player.displayName,
PlayerId = player.UserIDString,
NodeId = 0,
Position = workbench != null ? workbench.transform.position.ToString() : "unknown",
Time = DateTime.UtcNow.ToString("u")
});
int overflow = log.Unlocks.Count - Math.Max(1, config.MaxLogEntries);
if (overflow > 0) log.Unlocks.RemoveRange(0, overflow);
return null;
}
private void Unload()
{
Interface.Oxide.DataFileSystem.WriteObject(Name, log);
}
}
}The plugin gates tech tree unlocks behind a permission and records each accepted unlock. OnTechTreeNodeUnlock receives the workbench, node and requesting player. If the config option Require permission to unlock nodes is enabled (default true) and the player lacks techtreeunlockguard.use, it sends the localized NoPermission message and returns false to cancel the unlock. Accepted unlocks are appended to the data file with the player name, id, workbench position and UTC time, then trimmed to the configured maximum (Maximum log entries, default 500) by removing the oldest entries. Init loads the existing log, and Unload writes it back. The plugin does not touch inventory, so vanilla scrap handling still applies to everyone who is allowed.
Common use cases
Permission-gated tech tree
Only players with a VIP or admin permission may unlock nodes, blocking everyone else before any scrap is spent.
Unlock auditing
Record the player, node id, workbench position and timestamp of every unlock into a data file for later review or wipe reports.
Scrap economy events
Combine the hook with a reward plugin to mark a player's first unlock of the day or fire an event when a rare node is unlocked.
Pitfalls
Any non-null value blocks
Returning
false,0or an empty string still cancels the unlock. Only returningnullkeeps vanilla behavior, so do not return booleans.No refund path here
The hook fires before scrap is read, so cancelling it charges nothing. If you want to charge your own currency, do it inside the hook after your checks and handle failure by returning non-null.
Client-driven RPC
The request arrives from the client, so validate the player, the workbench distance and permission state; never trust that the node is reachable or affordable without your own checks.
Build a plugin with OnTechTreeNodeUnlock
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 OnTechTreeNodeUnlock
Generate