Home/GANDER docs/GANDER saves

GANDER saves

Save slots, autosave, Continue, and which game state to write.

saves.mdllms-full.txt

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

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.

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.

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:

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.

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 (GANDER 0.1 API reference, 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.