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
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.
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.
| Fine | Not fine |
|---|---|
| Comparing an int you already have | Resources.FindObjectsOfTypeAll<T>() |
TechTreeApi.GetLevel(...) (dictionary lookup) | FindObjectsOfType<T>() |
| Reading a singleton field | Reading a file, or anything allocating per call |
| A cached bool your own code updates on an event | LINQ 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:
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.
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
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.
IsUnlocked = () => TechTreeApi.GetLevel("more_customers") >= 3,
LockedReason = () => "Needs More Customers III",Depend on the store
IsUnlocked = () => DayCycleManager.HasInstance && DayCycleManager.Instance.CurrentDay >= 10,
LockedReason = () => "Available from day 10",Depend on another mod, safely
// 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 false | ComingSoon true | |
|---|---|---|
| Means | You can have this, once you meet the condition | This does not exist yet |
| Card shows | Your LockedReason | "In development" |
| Can become buyable in-session | Yes | No, it needs a mod update |