Upgrading an old save
Migrations for a save written by an older build of your game — rename a key, add a default, convert a value, preview the change, and never half-write it.
The shape of a migration#
A save file records the version of the data inside it. When that version is older than the one your build writes, PersistKit walks it forward — one step at a time, in memory — and a version with no migration covering it is refused rather than guessed at.
class="tok-com">// Once, during startup.
Persist.Migrations.Add(
new SaveMigration(2, 3, class="tok-str">"score became points, and the hero gained a name")
.Rename(class="tok-str">"score", class="tok-str">"points")
.SetDefault(class="tok-str">"heroName", ValueKind.Text, class="tok-str">"unnamed"));
Persist.Migrations.SchemaVersion = 3;That is the whole setup. A save written by version 2 now loads as version 3, and the file is rewritten in the new shape the next time the game saves.
The four changes, and one for anything else#
| Change | What it does |
|---|---|
.Rename("score", "points") | Gives a key a new name. If the new name is already in the save it is refused rather than overwriting one of them |
.SetDefault("heroName", ValueKind.Text, "unnamed") | Writes a value for a key the save does not have. An existing value always wins |
.Remove("oldScore") | Drops a key the game no longer saves |
.Convert("points", ValueKind.Text, "why", …) | Changes a value's kind with your own code |
.Custom("why", entries => …) | Anything else: reordering a list, merging two keys, rebuilding a dictionary |
A conversion returns the new value, or sets a problem and returns null to refuse the whole migration. A half-converted save is worse than one that was left alone.
Preview, then apply#
SaveMigrationReport report = Persist.Migrations.Preview();
Debug.Log(report.ToText());A preview writes nothing, and it is the same report a real migration produces, so the button and the dialog never disagree. To update the file immediately instead of waiting for the next save:
Persist.Migrations.Apply(confirm: true);The order is fixed, and each step is there for a reason:
- The file is read and validated with the same code every load uses.
- A copy is taken and kept, so the original is recoverable even if everything after this fails.
- Every step runs in memory, and the result is checked.
- Only then is the file written, through the same safe-write path as any save.
- If anything failed, the copy is put back and the file is exactly as it was.
In the editor, the same two operations are on the menu: Tools → PersistKit → Migrations → Preview upgrading the current slot and Upgrade the current slot….
When it goes wrong#
| Code | What it means |
|---|---|
PK-MIGRATION-001 | No migration is registered for a step in the walk. Nothing was loaded and nothing was changed |
PK-MIGRATION-002 | A step failed — usually a conversion refusing a value. The save is exactly as it was |
PK-MIGRATION-003 | The migrations ran, but what they produced is not a save this build can read. Nothing was committed |
PK-MIGRATION-004 | The migrated save could not be written, so the previous file was put back |
What is deliberately not done#
- A save from a newer build is still refused before any migration is considered (
PK-VERSION-001). - A key a migration does not mention is left exactly where it was — unknown data is not thrown away.
- A migration never runs against a file that has not passed the format, version and integrity checks first.
Next#
Something wrong on this page? Every page here describes behaviour that is checked in the repository. If a page and the package disagree, the package wins.
Report a documentation problem · Error codes · Platforms and limitations