Tech Tree API
Persistence
Where levels are stored, and why uninstalling your mod does not destroy the player's progress.
You do not have to do anything
Saving is handled for you. Levels are written the moment something is bought or sold, into a file of Tech Tree's own, per save slot. Your mod never opens a file and never touches the game's save.
BepInEx/config/TechTree/slot0.datWhat the file looks like
# TechTree upgrades - slot 0 - My Store
cashier_speed=3
more_customers=2
player_move=5
# kept for mods that are not currently installed; do not edit
nightowl.late_hours=2
supercart.wheels=1Plain id=level text. The block at the bottom is the interesting
part.
Uninstalling your mod does not delete progress
When Tech Tree reads the file and finds an id it does not recognise - because the mod that registered it is not installed right now - it keeps that line aside and writes it back out untouched on every save.
| Player does | What happens to bigboxes.capacity=3 |
|---|---|
| Removes your dll, keeps playing, buys something else | Still in the file, under the "kept for mods" comment |
| Puts your dll back | Your Register claims it; the node comes back at level 3 |
| Starts a new save | Nothing carried over - it is per slot |
| Uses two saves with different mod sets | Each slot keeps its own kept-lines |
Before 1.3.0 an unknown id was dropped on read, and the next save wrote the file without it - so a player who tried a mod, removed it and came back had silently lost everything they bought from it. Fixing that was a prerequisite for opening the API at all: without it, uninstalling a mod would erase real money the player had spent.
The cap is 4096 kept lines per slot, which no realistic mod set will reach.
Registering late is safe
Registration is not tied to load order. If you register a node after a save has already been
read - mid-game, after a config change, from a coroutine - Tech Tree checks the kept lines for
your id and adopts the level immediately. Your OnLevelChanged does not
fire for it, but GetLevel is correct from the next line onwards.
var handle = tree.Register(definition);
Log.LogInfo($"level restored from save: {handle.Level}");Reading the current level
int lvl = TechTreeApi.GetLevel("bigboxes.capacity"); // 0 if unknown or not ownedThis is a dictionary lookup, so calling it inside a Harmony postfix that runs often is fine - that is the intended usage.
Slots and switching saves
The slot is discovered from the game through every load path there is, including "Continue" from
the menu, which does not go through the normal load call. If the player switches saves without
quitting, Tech Tree notices within about two seconds, reloads the right file and fires
SaveLoaded with the new slot index.
Anything you remembered about the previous save - base values keyed by instance id, "already applied" flags, per-day counters - is wrong the moment another slot loads. Pooled objects reuse instance ids across saves, so a stale base value silently corrupts the new game.
TechTreeApi.SaveLoaded += slot =>
{
Base.Clear();
_appliedToday = false;
};Crash safety
The file now holds data that exists nowhere else - the kept lines of uninstalled mods - so an
interrupted write would be permanent loss. Saving goes through a temporary file and an atomic
replace, keeping the previous version as slot0.dat.bak. If the main
file ever comes back empty, the backup is read instead and a warning goes to the log.
What is not saved
Only the level number, per node id. Not your definition, not your icon, not your translations - those come from your dll every launch. Which is exactly what makes changing a price or an icon in an update harmless: the player's level is what persists.
The id is the key. Renaming
bigboxes.capacity to bigboxes.box_size in
version 1.1 means every player starts that line from zero, and their old level sits in the kept
block forever waiting for an id that will never come back.
If you truly have to rename one, LegacyIds below is the way out.
Renaming an id without losing progress
Sometimes it is unavoidable - you shipped an id that does not fit the rules, or your mod changed shape. Since 1.3.1, list the old names and the level comes with them:
tree.Register(new UpgradeDefinition
{
Id = "pallets.permit",
LegacyIds = new[] { "pallet_permit" }, // what it used to be called
// ...
});On registration, Tech Tree looks for a kept level under each old id and moves the highest one onto the new id. The player never notices.
| Situation | What happens |
|---|---|
| New id at 0, old id has a level | Moves over, and the log says so |
| New id already has a level | Nothing moves. Someone who already played with the new id outranks any old value |
| Several old ids with levels | The highest wins; all of them are cleared from the save |
| An old id that was never used | Silence, no error |
| The current id listed among the old ones | Ignored |
The migration happens when the node registers and
finds a kept level. After that the old id is gone from the save file, so leaving
LegacyIds in your code forever costs nothing - it simply finds nothing
on every later launch.