DOCUMENTATION / sdk/save.md
Save slot version 1
Each cartridge has one persistent slot containing 1–65536 bytes (64 KiB). Luau uses
byt.save; Odin imports save "sdk:save". Games own their record format,
versioning, checksums and migrations. The host stores opaque bytes.
The interface remains version 1 after expanding its bound from 4096 bytes. Existing records within the former bound remain readable without migration. Older players still enforce their smaller bound and cannot load larger records.
| Operation | Luau | Odin |
|---|---|---|
| Committed length, zero when absent | byt.save.size() |
save.size() |
| Copy complete committed record | byt.save.read(destinationBuffer) |
save.read(destinationSlice) |
| Submit replacement snapshot | byt.save.write(sourceBuffer) |
save.write(sourceSlice) |
| Most recent storage operation status | byt.save.status() |
save.status() |
| Maximum bytes | byt.save.max_bytes |
save.MAX_BYTES |
| Interface version | byt.save.version |
save.VERSION |
read returns the actual record length. It returns zero for an absent record,
an insufficient destination, or a destination larger than 65536 bytes. Rejected
reads leave the whole destination unchanged. Reads see the last committed
snapshot, including while a newer write is pending or after a failed write.
write returns one of the following statuses; status queries the latest
accepted storage operation. Invalid arguments, another pending write, or a
second write in the same simulation tick return Error without changing the
existing operation status. Initialization permits one write, then each update
permits one more. Additional draw calls do not replenish this allowance.
| Value | Luau constant | Odin enum | Meaning |
|---|---|---|---|
| 0 | empty |
.Empty |
No committed record exists |
| 1 | ready |
.Ready |
A record was loaded or a write committed successfully |
| 2 | pending |
.Pending |
Browser write accepted; transaction is still pending |
| 3 | error |
.Error |
Storage unavailable, record invalid, or operation failed |
The native host finishes each accepted write synchronously. It writes a complete
temporary file before atomically replacing the prior save. Failed replacement
preserves the prior file. The browser copies source bytes immediately and writes
them in an IndexedDB transaction. After Pending, poll status() in later updates
until Ready or Error before showing a successful-save message or exiting. Closing
the browser or terminating its worker while Pending may lose that write. Ready
confirms successful filesystem replacement or IndexedDB transaction completion;
it does not promise survival of storage-device failure.
The game may reuse its staging buffer immediately after write returns. The
host rejects oversized input before copying and cannot expose arbitrary files.
There is no filename argument, directory access, or placeholder success result.
Declare a stable identity in console.json to retain saves across compatible
rebuilds, cover/assets/code changes and file renames in either SDK:
"save": {"id": "org.example.game.compat-v1", "legacy": []}
IDs contain 1–128 lowercase ASCII letters/digits, with single dot or hyphen
separators. Choose a distinct ID for unrelated games or incompatible save formats.
The slot key is SHA-256 of UTF-8 bytorio-save-v1\0 followed by the ID (the
separator is one NUL byte). Titles and file paths never determine sharing.
Without a declaration, the key remains SHA-256 of the complete PNG. Unpackaged
development games retain their raw module/source hash identity.
The optional legacy array lists up to 16 unique lowercase 64-digit SHA-256
keys of older complete cartridges, in priority order. Only when the stable slot
is absent does the host adopt the first existing legacy record. It preserves the
old copy and atomically creates the canonical slot; a concurrent canonical writer
wins. A corrupt canonical or first existing legacy record reports Error without
fallback or overwrite. Failed adoption leaves the legacy data intact and reports
Error. This migrates storage keys only: the game owns record-format compatibility.
The SDK stores this declaration in one optional private ancillary PNG saVe
chunk after caRt. Its four-byte header is version (1), ASCII ID byte count,
legacy digest count and reserved zero; ID bytes precede ordered 32-byte raw SHA-256
digests. Duplicate, misplaced, malformed or unsupported metadata rejects loading.
Both hosts use the same bounded Odin parser and borrow metadata from the retained
PNG; browser callers copy metadata before growing runtime memory. Older players
ignore this optional chunk and continue using the whole-PNG key. The cartridge
profile, guest ABI and save interface version are unchanged.
Native saves live in the operating
system's local application-data directory under Bytorio/saves. Host tooling may
override that directory with BYTORIO_SAVE_DIRECTORY. Browser saves belong to
the player origin and are independent from native saves. Storage is local to the
device; private browsing, cleared site data or browser quota can prevent or erase
browser saves.
Run mise run validate test:save for maximum-size snapshots, independent byte
expectations, bounds, restart, separate cartridge identities, native atomic
replacement failure, and browser transaction rollback checks through both SDKs.
Get started