Most save code is three lines of FileAccess.store_var() — until a player's PC dies
mid-save, or you ship an update and every old save breaks. SaveVault is the drop-in that
handles the parts you only find out about after a bad review.
Godot 4.x · GDScript · no dependencies · ~150 lines you can read in one sitting.
- Atomic writes. Every save is written to a temp file and renamed into place, so a
process crash mid-write can never leave a half-written, unloadable save. (Godot has no
fsync, so a power-loss tear is still possible at the OS level — that's what the corruption recovery below is for.) - Versioned migrations. Each save carries a schema version. When you change your save format, register a migration — old saves upgrade themselves on load instead of breaking.
- Corruption recovery. The last good save is kept as a
.bak. If the main file is ever torn or garbage,load()silently falls back to the backup — and a save written over a corrupt main never overwrites that backup with the garbage.
- Copy
addons/save_vault/into your project. - Project → Project Settings → Plugins → enable SaveVault
(or add
save_vault.gdas an autoload namedSaveVaultyourself).
# Save anywhere
SaveVault.save("slot1", {"hp": 80, "level": 3, "items": ["sword"]})
# Load anywhere — returns {} if there's no save yet
var data := SaveVault.load("slot1")
func _ready() -> void:
if SaveVault.has_save("slot1"):
load_game(SaveVault.load("slot1"))Bump the version and register a migration for the old one. Players' existing saves upgrade on the next load — no lost progress, no special-casing.
func _ready() -> void:
SaveVault.CURRENT_VERSION = 2
# v1 stored "health"; v2 renames it to "hp"
SaveVault.register_migration(1, _migrate_v1_to_v2)
func _migrate_v1_to_v2(d: Dictionary) -> Dictionary:
d["hp"] = d.get("health", 100)
d.erase("health")
return dMigrations chain automatically: a v1 save loaded under CURRENT_VERSION = 3 runs the v1→v2
then v2→v3 migrations in order.
| Call | Does |
|---|---|
save(slot, data) -> Error |
Write data (a Dictionary) atomically. Returns OK or an error code. |
load(slot) -> Dictionary |
Load + migrate to the current version. {} if no readable save. |
has_save(slot) -> bool |
Is there a main or backup save for this slot? |
erase(slot) -> void |
Delete the slot, its backup, and any stray temp file. |
register_migration(from_version, Callable) |
Register an upgrade from from_version to the next. |
CURRENT_VERSION |
Set this to your current schema version. |
save_dir |
Where saves live (default user://saves). |
Data is stored as plain JSON, so saves are inspectable and portable. Use any Dictionary of JSON-safe values (numbers, strings, bools, arrays, nested dictionaries).
tests/test_save_vault.gd is a headless test suite covering round-trip, the migration
chain, atomic-write safety, corruption→backup recovery, and erase. Run it:
godot --headless --path . -s tests/test_save_vault.gd
MIT. Drop it in a commercial game, no attribution required.