Skip to content

Skinflap/SaveVault

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SaveVault — a save system for Godot 4 that doesn't lose data

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.

What it does that naive save code doesn't

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

Install

  1. Copy addons/save_vault/ into your project.
  2. Project → Project Settings → Plugins → enable SaveVault (or add save_vault.gd as an autoload named SaveVault yourself).

Use

# 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"))

Changing your save format later

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 d

Migrations chain automatically: a v1 save loaded under CURRENT_VERSION = 3 runs the v1→v2 then v2→v3 migrations in order.

API

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

Tested

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

License

MIT. Drop it in a commercial game, no attribution required.

About

No description, website, or topics provided.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors