Skip to content
RustPluginGenerator

OnTechTreeNodeUnlocked

OnTechTreeNodeUnlocked is called after a player unlocks a node in a Workbench tech tree, once the scrap has been taken from their inventory and the blueprints have been written.

Signature
void OnTechTreeNodeUnlocked(Workbench workbench, TechTreeData.NodeInstance node, BasePlayer player, PooledList<ItemDefinition> unlockedItems)

Parameters

Hook parameters
ParameterTypeDescription
workbenchWorkbenchThe Workbench entity whose tech tree RPC was used. Use it to check position, owner, level or to identify which bench unlocked the node.
nodeTechTreeData.NodeInstanceThe TechTreeData.NodeInstance that was just unlocked. Carries the node's blueprint reference and tree position; treat its blueprint item as the unlocked item.
playerBasePlayerThe BasePlayer who paid the scrap and unlocked the node. Null-check it, though in practice it is always set from the RPC caller.
unlockedItemsPooledList<ItemDefinition>A pooled list of ItemDefinitions that were added to the player's blueprints by this unlock, including any prerequisites granted along with the chosen node.

Return value

The return value is ignored. The original RPC continues normally after the hook runs, so returning null or any value has no effect on the unlock or the scrap cost.

How it works

The hook fires inside Workbench.RPC_TechTreeUnlock, after player.inventory.Take removes the scrap cost and player.blueprints.UnlockList marks the blueprints as known. It runs before the server loops over the unlocked definitions to calculate research scrap and tax, which means the player already owns the blueprints and the payment is final at this point.

Use player to reward or notify the specific player, workbench to check which bench or location triggered the unlock (useful for bench-specific rules like "only T1 benches grant XP"), and unlockedItems to inspect every ItemDefinition granted in this single action, including prerequisite nodes the tree unlocked automatically. node gives you the chosen node itself if you need to distinguish it from prerequisites.

The PooledList<ItemDefinition> is a pooled collection owned by the game; enumerate it and read its items, but do not add, remove or hold references to it beyond the hook body. The hook always fires after the unlock succeeds, so it cannot be used to veto a purchase. For prevention you would need to intercept earlier client input, which this hook does not expose. Declaring BasePlayer here is already the natural type and needs no narrowing; you cannot safely substitute the workbench type beyond Workbench.

Example plugin: Grant a chat notification and bonus scrap refund on tech tree unlocks

TechTreeUnlockReward.cs
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Tech Tree Unlock Reward", "RustPluginGenerator", "1.0.0")]
    [Description("Notifies players and grants a scrap refund when they unlock a workbench tech tree node.")]
    public class TechTreeUnlockReward : RustPlugin
    {
        private const string PermReward = "techtreeunlockreward.reward";

        private PluginConfig config;
        private readonly Dictionary<ulong, float> lastNotify = new Dictionary<ulong, float>();

        private class PluginConfig
        {
            [JsonProperty("Enable scrap refund")]
            public bool EnableRefund = true;

            [JsonProperty("Scrap refund amount")]
            public int RefundAmount = 25;

            [JsonProperty("Require permission for refund")]
            public bool RequirePermission = 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>
            {
                ["Unlocked"] = "You unlocked {0} in the tech tree.",
                ["Refund"] = "Refunded {0} scrap for your tech tree purchase."
            }, this);
        }

        private string Msg(string key, string userId = null, params object[] args)
        {
            return string.Format(lang.GetMessage(key, this, userId), args);
        }

        private void Init()
        {
            permission.RegisterPermission(PermReward, this);
        }

        private void OnTechTreeNodeUnlocked(Workbench workbench, TechTreeData.NodeInstance node, BasePlayer player, PooledList<ItemDefinition> unlockedItems)
        {
            if (player == null || !player.IsConnected)
                return;

            string itemName = "a blueprint";
            if (unlockedItems != null)
            {
                foreach (ItemDefinition def in unlockedItems)
                {
                    if (def == null) continue;
                    itemName = def.displayName != null ? def.displayName.english : def.shortname;
                    break;
                }
            }

            float now = Time.realtimeSinceStartup;
            if (lastNotify.TryGetValue(player.userID, out float previous) && now - previous < 1f)
                return;
            lastNotify[player.userID] = now;

            player.ChatMessage(Msg("Unlocked", player.UserIDString, itemName));

            if (!config.EnableRefund || config.RefundAmount <= 0)
                return;

            if (config.RequirePermission && !permission.UserHasPermission(player.UserIDString, PermReward))
                return;

            Item scrap = ItemManager.CreateByName("scrap", config.RefundAmount);
            if (scrap == null)
                return;

            player.GiveItem(scrap);
            player.ChatMessage(Msg("Refund", player.UserIDString, config.RefundAmount));
        }

        private void Unload()
        {
            lastNotify.Clear();
        }
    }
}

The plugin listens for OnTechTreeNodeUnlocked and, for each unlock, checks that the player and workbench are valid. It reads the first item in the unlocked list as the headline item and sends a localized chat message naming it. If the config enables a refund, it gives back a percentage of a configurable flat scrap value through ItemManager.CreateByName("scrap", ...), so players see a small bonus without needing to track the exact price the game charged. A short timer-based throttle stops repeated notification spam when a single purchase unlocks several prerequisites in a row. The whole flow is guarded by permission so admins can restrict the bonus to VIPs.

Common use cases

  • Reward players for tech tree progress

    Give a small scrap or item bonus when a player unlocks a node, letting you encourage deep tech tree investment on your server.

  • Log unlock activity per bench

    Write which player unlocked which items and at which workbench to a data file so admins can audit progression or detect exploit patterns.

  • Bench-specific bonuses

    Apply different rewards depending on the workbench that triggered the unlock, for example only paying out for unlocks done at a public T2 bench in a safe zone.

Pitfalls

  • The list is pooled and includes prerequisites

    unlockedItems may hold more than the node the player clicked, and the collection is reused by the game. Never store it; copy the definitions you need immediately.

  • Unlock already happened

    Scrap is taken and blueprints are written before the hook fires, so returning a value or throwing does not cancel the purchase. Do prevention elsewhere.

  • Guarding the player and items

    Null-check player, workbench and each ItemDefinition before reading shortname or displayName, since a missing reference can throw inside the RPC.

Build a plugin with OnTechTreeNodeUnlocked

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

Prompt: Reward players for tech tree progress using the OnTechTreeNodeUnlocked 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 OnTechTreeNodeUnlocked

Generate