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.

path
BepInEx/config/TechTree/slot0.dat

What the file looks like

slot0.dat
# 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=1

Plain 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 doesWhat happens to bigboxes.capacity=3
Removes your dll, keeps playing, buys something elseStill in the file, under the "kept for mods" comment
Puts your dll backYour Register claims it; the node comes back at level 3
Starts a new saveNothing carried over - it is per slot
Uses two saves with different mod setsEach slot keeps its own kept-lines
This was a real bug, fixed in 1.3.0

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.

csharp
var handle = tree.Register(definition);
Log.LogInfo($"level restored from save: {handle.Level}");

Reading the current level

csharp
int lvl = TechTreeApi.GetLevel("bigboxes.capacity");   // 0 if unknown or not owned

This 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.

Clear your caches on SaveLoaded

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.

csharp
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.

Never change an id after release

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:

csharp
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.

SituationWhat happens
New id at 0, old id has a levelMoves over, and the log says so
New id already has a levelNothing moves. Someone who already played with the new id outranks any old value
Several old ids with levelsThe highest wins; all of them are cleared from the save
An old id that was never usedSilence, no error
The current id listed among the old onesIgnored
It only runs once

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.