Skip to main content

Save games

Use app.storage for a player's progress. Save slots are separate from project resources, scene files and plugin preferences.

Write, read and delete a slot​

Serialize your game's state to bytes using the format your game owns:

try app.storage.write("slot-1", serialized_save);

const bytes = try app.storage.read(gpa, "slot-1");
defer gpa.free(bytes);
// Decode and validate bytes using your game's serializer.

try app.storage.remove("slot-1");

Reads return an independent allocation owned by the supplied allocator. Empty payloads are valid. Slots contain 1–64 ASCII letters, digits, underscores or hyphens. Names are case-sensitive logical identifiers, not paths; dots, slashes, backslashes, spaces and traversal are rejected with InvalidSlot. Case-distinct slot names remain distinct even on case-insensitive filesystems.

write atomically replaces the previous save. It never truncates a slot in place: desktop/Android write and sync a sibling file before renaming it, and browsers use one atomic localStorage replacement. Interrupted writes may leave an unused sibling file, but not a partially written destination. Do not depend on a successful write surviving sudden power loss, user deletion or browser eviction.

Version your game's data​

The short methods use game schema version 1. When your serialized layout changes, choose an explicit version:

const schema: u32 = 2;
try app.storage.writeVersion("autosave", schema, serialized_save);

const bytes = try app.storage.readVersion(gpa, "autosave", schema);
defer gpa.free(bytes);

An incompatible game schema returns UnsupportedSaveSchema. To migrate a known old schema, deliberately read that version, decode and convert it, then write the new version. Never overwrite an incompatible or corrupt save with defaults just because loading failed.

Eggy wraps the payload in a binary envelope with the EGGYSAVE magic, little-endian engine format version (u32), game schema (u32), payload length (u64) and SHA-256 payload checksum. The current envelope format is 1 and its header is 56 bytes. An unknown engine format returns UnsupportedSaveFormat; damaged, truncated or trailing data returns CorruptSave. Payloads are limited to 64 MiB. The checksum detects accidental damage, not cheating or malicious changes.

Handle failures visibly​

FileNotFound means there is no save in the requested slot, including when deleting a missing slot. SaveTooLarge rejects oversized payloads. AccessDenied, StorageFull, StorageIo and OutOfMemory are failures, not empty successful reads. Report save failures to the player and preserve their previous progress. Your own decoder must still validate the payload's contents.

There is no automatic empty save, preferences fallback, resource fallback or in-memory persistence fallback.

Where saves live​

On desktop and Android, saves use SDL's writable per-user data folder for the organization eggy and application games, then a SHA-256 project namespace and saves/. No project checkout or packaged resources need to be writable. The editor and packaged runtime share saves when they use the same project ID. Renaming that ID starts a different namespace; moving the project does not.

Browser saves use localStorage, scoped to the page's origin and project ID, not Emscripten's transient MEMFS. They survive page reloads when the browser permits storage. Browser quotas are usually much smaller than Eggy's payload limit; private browsing, disabled site storage, clearing site data and eviction can prevent or remove persistence. Quota failures return StorageFull; unavailable or blocked Web Storage returns UnsupportedPlatform. A different origin, including a different development-server port, has different saves. Custom web hosts must permit Emscripten's default JavaScript dynamic execution; the current bridge does not support a Content Security Policy that forbids eval.

Custom hosts​

The engine host must explicitly initialize storage before game/plugin initialization:

app.storage = try eggy.storage.Storage.init(process.gpa, process.io, project.name);

Use the same nonempty stable project identity in editor and runtime, not the window title, executable path or project directory. Setup errors must be handled explicitly. An uninitialized service returns StorageNotInitialized. After plugin and world shutdown, call app.storage.deinit() before shutting down SDL or the process I/O. The owner may move with App; copied service views borrow the heap-stable host provider and must not outlive it. Calls are synchronous: keep them on the host/game thread and save outside hot frame paths.