wrote a GDScript save file migrator so old saves don't break between builds

420 views 0 replies

Been shipping updates to a Godot 4 project with persistent saves. Every time I restructure the save format, rename a field, change how inventory is stored, or add a new required key, old saves either crash on load or silently produce garbage state. Got tired of the "delete your save and start over" workaround and spent a morning writing a small migration chain class.

The concept is straightforward: each save file has a version integer. You register a callable for each version step. At load time, migrate() runs the chain from the current version up to the latest automatically.

class_name SaveMigrator
extends RefCounted

var _migrations: Dictionary = {}

func register(from_version: int, migration: Callable) -> void:
    _migrations[from_version] = migration

func migrate(data: Dictionary) -> Dictionary:
    var version: int = data.get("version", 0)
    while _migrations.has(version):
        data = _migrations[version].call(data)
        version += 1
    data["version"] = version
    return data

Usage looks like this:

var migrator := SaveMigrator.new()

migrator.register(0, func(data: Dictionary) -> Dictionary:
    # v0 to v1: "hp" renamed to "health"
    data["health"] = data.get("hp", 100)
    data.erase("hp")
    return data
)

migrator.register(1, func(data: Dictionary) -> Dictionary:
    # v1 to v2: inventory changed from flat array to dict keyed by item_id
    var old_inv: Array = data.get("inventory", [])
    var new_inv: Dictionary = {}
    for item in old_inv:
        new_inv[item["id"]] = item.get("count", 1)
    data["inventory"] = new_inv
    return data
)

var raw := load_raw_save()
var migrated := migrator.migrate(raw)

Each migration is self-contained and only knows about its own transformation. The system handles chaining automatically. Missing version key defaults to 0, which covers saves from before I added versioning at all.

The part I'm still not happy with is error handling mid-chain. Right now I wrap the whole call in a deep copy and restore on failure, but it feels clunky. Also not sure what the right behavior is when someone loads a save from a newer version than the current build. Right now it just passes through unchanged, which might be fine or might silently break things depending on what changed.

Anyone else versioning their save files? Curious if there's a cleaner approach to error recovery, or if people just let corrupted saves fail gracefully and offer a reset.

Moonjump
Forum Search Shader Sandbox
Sign In Register