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.
SaveState
Section titled “SaveState”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.
SaveState.new()
Section titled “SaveState.new()”Returns: SaveState — A new, empty store.
import "engine" for SaveState
var s = SaveState.new()Setters
Section titled “Setters”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.
| Method | Value (Num/Bool/String) | Description |
|---|---|---|
setBool(key, value) | Bool | Store a boolean. |
setInt(key, value) | Num | Store an integer (the fractional part is dropped). |
setNumber(key, value) | Num | Store a floating-point number. |
setString(key, value) | String | Store a string. |
var s = SaveState.new()s.setInt("level", 3)s.setString("player", "Ada")s.setBool("tutorialDone", true)s.setNumber("volume", 0.8)Getters
Section titled “Getters”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.
| Method | Returns | Description |
|---|---|---|
getBool(key) / getBool(key, fallback) | Bool | The stored boolean, or fallback if the key is absent. |
getInt(key) / getInt(key, fallback) | Num | The stored integer, or fallback if the key is absent. |
getNumber(key) / getNumber(key, fallback) | Num | The stored number, or fallback if the key is absent. |
getString(key) / getString(key, fallback) | String | The 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 setvar name = s.getString("player", "P1")has(key)
Section titled “has(key)”Returns: Bool — true if a value is stored under key.
Parameters:
key(String) — The key to test.
remove(key)
Section titled “remove(key)”Returns: Bool — true if a value was present and removed; false if the key was
absent.
Parameters:
key(String) — The key to remove.
clear()
Section titled “clear()”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.
Save.write(name, state)
Section titled “Save.write(name, state)”Returns: Bool — true 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")}Save.read(name)
Section titled “Save.read(name)”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.}Save.exists(name)
Section titled “Save.exists(name)”Returns: Bool — true if a file exists at name in the write directory.
Parameters:
name(String) — The file path to test, relative to the write directory.
Enumerating save slots
Section titled “Enumerating save slots”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", ...}Versioning and where saves live
Section titled “Versioning and where saves live”- Versioned format. Each file carries a
[meta].format_versionalongside 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
nullfromSave.read— it is rejected, not corrupted or mis-parsed. Always guard a read againstnull. - 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.saveuses. 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 like0.8may serialize in full-precision form (0.80000000000000004) in the TOML.
Worked example — a persisted counter
Section titled “Worked example — a persisted counter”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).