# gander saves: slots, autosave, Continue, and writing game state Save games for anything a player comes back to: levels, unlocks, a run, a story. A save is a record of named fields in a slot. Slot 0 is the autosave, 1..31 the player's slots. The engine keeps the slots in the browser's storage on the web and in the per-user data folder natively; nothing goes to a server, so a save stays on the device and browser it was made on. Nothing is saved for you. The engine does not snapshot the scene, physics, animation or audio: the game writes the state it needs to rebuild them. ## Writing and reading a save ```goose fn write_save(slot: i32) { gander::save_begin(); gander::save_put("v", 1); // your save format version gander::save_put("level", level); // i32, i64, f32, f64, bool gander::save_put("pos", hero_pos); // float2, float3, float4 gander::save_put("yaw", hero_yaw); gander::save_put("hp", hp); gander::save_put("name", hero_name); // text let ok = gander::save_end(slot, str("Level ", level, ", ", zone_name)); // label on the slot screen } fn read_save(slot: i32) -> bool { if !gander::save_open(slot) { return false; } // empty slot level = gander::save_get("level", 1); // the default is used when the field is missing hero_pos = gander::save_get("pos", level_start(level)); hero_yaw = gander::save_get("yaw", 0.0 as f32); hp = gander::save_get("hp", hp); // current value as the default keeps the type var nm: u8[>..] = []; if gander::save_get("name", nm) { hero_name = nm; } true } ``` | Call | Does | | --- | --- | | `save_begin()` | starts a record | | `save_put(key, value)` | one field; keys are text without `=` or line breaks, not starting with `@` | | `save_end(slot, label) -> bool` | writes it; `false` when the storage refused it (full, private window) | | `save_open(slot) -> bool` | opens a slot for reading; `false` when empty | | `save_get(key, default)` | the field as the default's type (i32, f32, f64, bool, float2/3/4), or the default when missing or unreadable. An integer literal default reads an i32; an i64 field: `save_get_i(key, def)` | | `save_get(key, out: u8[>..]&) -> bool` | text, appended to `out` | | `save_has(key) -> bool` | the open save has the field | | `save_exists(slot)`, `save_delete(slot)`, `save_latest() -> i32` (newest slot, -1 none) | | | `save_label(slot, out&)`, `save_ago(slot, out&)` ("12 min ago"), `save_time(slot) -> i64` (unix seconds) | for your own slot UI | | `unix_time() -> i64` | wall clock (daily rewards, timers across sessions) | Defaults are typed: float literals are f64, so `save_get("hp", 0.0)` returns an f64. Pass the current value (`save_get("hp", hp)`) or cast (`0.0 as f32`). ## What to save Save what you cannot rebuild, and rebuild the rest from it on load: * **Progress**: level or chapter, checkpoint id, unlocked items, collected pickups, quest steps, money, score, time played. * **The player**: position and yaw (at a checkpoint is fine), health, ammo, inventory, equipped item. * **The world**, only what changed: opened doors, killed bosses, picked up items. Loading a level and then applying the changes beats saving every entity. Do not save handles (`Model`, `Texture`, `Sound`, `Body`...: they are only valid in this run), physics velocities, animation times, particles or camera smoothing. On load, reset the round first (as for a new game), then apply the save, then put physics bodies where the save says (`body_set_pos`). Lists: save the count, then one field per item with the index in the key. ```goose gander::save_put("items", inventory.len as i32); for it, i in inventory { gander::save_put(str("item", i, ".kind"), it.kind); gander::save_put(str("item", i, ".count"), it.count); } // reading let n = gander::save_get("items", 0); for i: i32 in 0..n { inventory.push(Item { kind: gander::save_get(str("item", i, ".kind"), 0), count: gander::save_get(str("item", i, ".count"), 1) }); } ``` Booleans per thing (doors, collectibles) can share one text field: a string of `0` and `1`, or a comma list of ids. **Versions**: the game will change after players have saves. Never reuse a key for a different meaning; add new keys (old saves read their default). Save `"v"` and branch on it when a change needs converting. ## The slot screen `ui_save_slots(mode, count) -> i32` is the whole screen: one card per slot with its label and "12 min ago", Delete on each, a confirm before overwriting or deleting, Back. It works with mouse, touch, keys and pads. * `gander::SLOTS_SAVE` lists slots 1..count (save into one), `gander::SLOTS_LOAD` the autosave too, when there is one. * Returns the slot picked this frame, `SLOTS_OPEN` (-1) while open, `SLOTS_BACK` (-2) when closed. It only picks: you save or load. ## With Flow (the usual way) `Flow { saves: 3, .. }` adds the save menus to the ready-made game flow: the main menu gets Continue (the newest save, autosave included), New game, Load game and Settings once a save exists; the pause menu gets Save game and Load game. Flow sets `save_to` or `load_from` to the slot on the frame the player picks it (-1 otherwise), and toasts "Game saved" the frame after, or "Could not save the game" when nothing was written. On desktop and Android the pause menu also has Quit: on that frame `save_to` is 0 (the autosave) and `flow.quitting` is true, and the game ends after the frame. ```goose var flow = gander::Flow { saves: 3, .. }; fn main() { gander::config("Iron Harbor", 1280, 720, 0); gander::flow_init(flow); } export "gander_frame" fn frame(dt: f32) { let playing = gander::flow_update(flow, "IRON HARBOR", "Hold the docks until dawn"); if flow.started { reset_round(); } // New game, Continue, Load, Play again if flow.load_from >= 0 { let ok = read_save(flow.load_from); } if flow.save_to >= 0 { write_save(flow.save_to); } if playing { simulate(dt); } draw_world(); } ``` Without Flow, open the screen from your own menus: ```goose if screen == SCREEN_SAVE { let k = gander::ui_save_slots(gander::SLOTS_SAVE, 3); if k >= 0 { write_save(k); gander::ui_toast("Game saved", 2.0); screen = SCREEN_PAUSED; } if k == gander::SLOTS_BACK { screen = SCREEN_PAUSED; } } ``` ## Autosave Write slot 0 at natural moments: a checkpoint, a level cleared, a shop closed, every few minutes of play. Never every frame: each save is a synchronous storage write. ```goose fn checkpoint() { write_save(0); gander::ui_toast("Checkpoint", 1.5); } ``` Short arcade games with no progress beyond a high score do not need slots: use the high score boards (https://vbgnt.com/docs/gander/api.md, Analytics and high scores), and `save(key, value)` / `load(key, out)` for a single setting or best time. ## Testing `gander test` starts with empty storage every run (`gander test --native` keeps its saves in the per-user folder between runs). Save and load in the same run: drive the menus with `--input`, or call `write_save(1)` and `read_save(1)` at fixed frames, and probe the values that should come back.