Tech Tree API

Requirements

Gating a node behind a condition, why the predicate is polled three times a second, and what happens when it throws.

The two fields

csharp
new UpgradeDefinition
{
    Id           = "bigboxes.auto",
    // ...
    IsUnlocked   = () => TechTreeApi.GetLevel("bigboxes.capacity") >= 2,
    LockedReason = () => "Needs Bigger Boxes II",
}

While IsUnlocked returns false the whole line is drawn grey and cannot be bought, and the hover card shows LockedReason instead of the effect text. When it flips to true the node becomes buyable within about a third of a second - the player does not have to reopen anything.

Always give a reason

LockedReason is optional and leaving it out is a mistake. A grey node with a blank card is indistinguishable from a bug, and that is what lands in your comments section.

How often it is called

The tree repaints node states about three times a second, and asks every visible node whether it is unlocked. Tech Tree caches your answer for 0.25 seconds per node, so a predicate is called at most about four times a second no matter how many nodes are on screen.

That still means hundreds of calls a minute while the window is open. Keep it cheap.

FineNot fine
Comparing an int you already haveResources.FindObjectsOfTypeAll<T>()
TechTreeApi.GetLevel(...) (dictionary lookup)FindObjectsOfType<T>()
Reading a singleton fieldReading a file, or anything allocating per call
A cached bool your own code updates on an eventLINQ over a scene-wide collection

If you must look at the scene

Do the expensive part on your own schedule and let the predicate read the answer:

csharp
private static bool _hasFreezer;

// Recomputed every couple of seconds on Tech Tree's own pass, not on every repaint.
OnApply = () =>
{
    _hasFreezer = UnityEngine.Object.FindObjectsOfType<Freezer>().Length > 0;
},

IsUnlocked = () => _hasFreezer,

Failure is open, not closed

If your predicate throws, Tech Tree treats the node as unlocked and logs a warning naming your node and plugin.

Why fail-open

The two failure modes are not symmetric. A node wrongly buyable is a mod bug with an obvious cause. A node locked forever with no explanation is a player staring at a grey circle they can never buy, filing a report against the wrong mod. Between those, the noisy one is better - and a mod poking a hole in its own gate is a problem for the mod author, not for the player.

Repeated identical exceptions stop being logged after five lines, so a predicate that throws every quarter second does not fill the log file.

Common gates

Depend on another of your nodes

csharp
IsUnlocked   = () => TechTreeApi.GetLevel("bigboxes.capacity") >= 2,
LockedReason = () => "Needs Bigger Boxes II",

Depend on a Tech Tree built-in

Built-in ids are stable and have no dot: cashier_speed, restocker_speed, staff_speed, customer_speed, more_customers, smart_self_checkout, player_move, player_sprint, player_reach, quick_hands.

csharp
IsUnlocked   = () => TechTreeApi.GetLevel("more_customers") >= 3,
LockedReason = () => "Needs More Customers III",

Depend on the store

csharp
IsUnlocked   = () => DayCycleManager.HasInstance && DayCycleManager.Instance.CurrentDay >= 10,
LockedReason = () => "Available from day 10",

Depend on another mod, safely

csharp
// Compute it once at load, so the predicate is just a field read and cannot throw.
private static readonly bool HasNightOwl =
    IL2CPPChainloader.Instance.Plugins.ContainsKey("NightOwl");

IsUnlocked   = () => HasNightOwl,
LockedReason = () => "Requires the Night Owl mod",

Locked is not hidden

A locked node is still drawn, still shows its name and its price. That is deliberate: the tree is meant to be read as a plan, and an upgrade the player cannot see yet is an upgrade they will never work towards. If you truly want something invisible until a condition holds, register it late instead of gating it.

Locked versus ComingSoon

IsUnlocked falseComingSoon true
MeansYou can have this, once you meet the conditionThis does not exist yet
Card showsYour LockedReason"In development"
Can become buyable in-sessionYesNo, it needs a mod update