Skip to content

Save / Load

Game-authored, versioned save/load. A SaveState is a typed key/value store the game fills with exactly what it wants to persist — checkpoints, settings, progress; the Save facade serializes it to (and reads it back from) a file under the app’s write directory. This is not an automatic scene dump: the game decides what matters, which keeps the format small, stable, and under your control (engine ADR 0036).

Values are one of four scalar types — bool, int, number (float), or string. The on-disk format is human-readable TOML with a [meta].format_version header and a [data] table, so a save is easy to inspect and diff.

A typed key/value store. Keys are strings; values are bool / int / number / string. Unlike most of the engine API, SaveState is an instance — construct one, fill it, then hand it to Save.write.

Returns: SaveState — A new, empty store.

import "engine" for SaveState
var s = SaveState.new()

Each setter stores value under key, replacing any existing value for that key. The stored type is fixed by which setter you call — read it back with the matching getter.

MethodValue (Num/Bool/String)Description
setBool(key, value)BoolStore a boolean.
setInt(key, value)NumStore an integer (the fractional part is dropped).
setNumber(key, value)NumStore a floating-point number.
setString(key, value)StringStore a string.
var s = SaveState.new()
s.setInt("level", 3)
s.setString("player", "Ada")
s.setBool("tutorialDone", true)
s.setNumber("volume", 0.8)

Every getter comes in two forms: a 1-arg form and a 2-arg form that takes a fallback. The 2-arg form returns fallback when the key is absent, which is the safe way to read a save that may predate a given key.

MethodReturnsDescription
getBool(key) / getBool(key, fallback)BoolThe stored boolean, or fallback if the key is absent.
getInt(key) / getInt(key, fallback)NumThe stored integer, or fallback if the key is absent.
getNumber(key) / getNumber(key, fallback)NumThe stored number, or fallback if the key is absent.
getString(key) / getString(key, fallback)StringThe stored string, or fallback if the key is absent.
// Prefer the 2-arg form so a missing key can't surprise you.
var level = s.getInt("level", 1) // 1 if "level" was never set
var name = s.getString("player", "P1")

Returns: Booltrue if a value is stored under key.

Parameters:

  • key (String) — The key to test.

Returns: Booltrue if a value was present and removed; false if the key was absent.

Parameters:

  • key (String) — The key to remove.

Removes every key/value pair, leaving an empty store.

Returns: Num — The number of keys currently stored.

Returns: List of String — Every key currently stored, sorted.

for (k in s.keys) {
System.print("%(k) is set")
}

Static file I/O for a SaveState, over the app’s write directory. A name like "saves/slot1.sav" resolves under the loaded project’s own directory (the PhysicsFS write dir — the same location Config.save writes to), and any missing parent directories are created for you. Paths are sandboxed to the project.

Returns: Booltrue on success; false if the file could not be written (for example, the app directory is not writable).

Parameters:

  • name (String) — The file path to write, relative to the write directory (e.g. "saves/slot1.sav"). Parent directories are created if needed.
  • state (SaveState) — The store to serialize. It is written as versioned TOML.
import "engine" for Save, SaveState
var s = SaveState.new()
s.setInt("level", 3)
s.setString("player", "Ada")
if (Save.write("saves/slot1.sav", s)) {
System.print("saved")
}

Returns: SaveState or null — The parsed store, or null if the file is absent or its format_version is newer than this engine build understands. A newer-format save is rejected outright — it is never partially or wrongly parsed.

Parameters:

  • name (String) — The file path to read, relative to the write directory.
var r = Save.read("saves/slot1.sav")
if (r != null) {
System.print(r.getInt("level", 0))
} else {
// No save yet, or written by a newer build — start fresh.
}

Returns: Booltrue if a file exists at name in the write directory.

Parameters:

  • name (String) — The file path to test, relative to the write directory.

There is no Save.list. Enumerate slots with the existing Resource.list(dir, ext) — the sorted filenames in a mounted directory, filtered by suffix:

import "engine" for Resource, Save
for (name in Resource.list("saves", ".sav")) {
System.print(name) // "slot1.sav", "slot2.sav", ...
}
  • Versioned format. Each file carries a [meta].format_version alongside the [data] table. This build writes and reads version 1. The version is the migration hook for any future format change.
  • Forward-compat is safe. A save written by a newer engine build reads back as null from Save.read — it is rejected, not corrupted or mis-parsed. Always guard a read against null.
  • Where saves live. Files persist under the app’s own directory (the PhysicsFS write dir set by the host), not a per-user OS location — the same mechanism Config.save uses. A nested path like "saves/slot1.sav" has its parent directory created on write.
  • Scalars only (v1). Values are bool / int / number / string; compose structure by naming keys (e.g. "party.0.name"). Numbers round-trip exactly, though a value like 0.8 may serialize in full-precision form (0.80000000000000004) in the TOML.
import "engine" for Save, SaveState, Logger
class Game {
init() {
var loaded = Save.read("saves/demo.sav")
_launches = (loaded == null) ? 1 : loaded.getInt("launches", 0) + 1
var s = SaveState.new()
s.setInt("launches", _launches)
Save.write("saves/demo.sav", s)
Logger.info("launch #%(_launches)") // climbs by 1 every run
}
update(dt) {}
draw() {}
}

See the Save/Load Demo example (apps/save_load_demo) for the full API driven end to end, and Config for saving hand-formatted TOML text (as opposed to a typed, versioned SaveState).