For the complete documentation index, see llms.txt. This page is also available as Markdown.

Save System

The save system provides a key-value data storage system for persisting player progress and preferences across sessions.

The Save system is a simple key-value store. You give it a string key (like "xp"), and it stores a value that will still be there the next time the player joins.

There are two kinds of save data:

  • Player save: one player's personal data (XP, upgrades, settings, inventory, etc)

  • Game-wide save: shared by everyone in the game (world records, server settings, global counters, etc)

Quick start (save + load a stat)

The most common pattern is:

  • Load saved values in ao_start

  • Save again whenever the value changes

Player :: class : Player_Base {
    xp: s64;
    level: s64;

    ao_start :: method() {
        // Defaults are used for brand new players
        xp    = Save.get_int(this, "xp", 0);
        level = Save.get_int(this, "level", 1);
    }
}

add_xp :: proc(player: Player, amount: s64) {
    player.xp += amount;

    // ... your level-up logic here ...

    // Save immediately when it changes
    Save.set_int(player, "xp", player.xp);
    Save.set_int(player, "level", player.level);
}

Player save (per-player data)

Player save is scoped to a single player. Every player has their own isolated key/value data.

Supported types:

  • String: Save.set_string / Save.get_string

  • Integer (s64): Save.set_int / Save.get_int (note: values are currently truncated to 32-bit internally)

  • Float (f64): Save.set_f64 / Save.get_f64

  • JSON (advanced): Save.set_json / Save.try_get_json

Always provide a sensible default when reading. New players won't have keys yet, and get_* will return your default.

Saving “bigger” data (JSON)

If you have a little bundle of fields (like progress + unlocked things), it's often nicer to save it as one JSON blob.

Only fields marked with @ao_serialize are saved.

Save.try_get_json returns false if the key doesn't exist or the JSON string is malformed. Allocate class data before loading because the false path does not allocate it. Missing fields keep their class defaults, and unknown fields are ignored.

Save versioning (migrating old data safely)

If you ever change your save format, keep a version key and migrate older saves forward.

Game-wide save (shared by everyone)

Game-wide save is shared across the whole game, not per-player.

Use Save.increment_game_int for counters that multiple players might update at the same time (kills, joins, rounds played, etc). Its optimistic_update parameter defaults to true, so the local cached value changes immediately.

Common patterns

Booleans

Save booleans as 0/1:

Key naming

Keys are just strings, so pick names that won't collide later:

  • "xp", "level", "selected_skin"

  • "tycoon.cash", "tycoon.upgrades.mouth_level"

If you have multiple games connected via game parenting (hub + minigames), see Cross-Game Products/Data for how save data can be shared.

Additional APIs

ordered_get and ordered_get_all are asynchronous and execute on the server. Call them from the normal shared gameplay path; do not add a Game.is_server() guard.

Callback arrays are valid only during the callback, so copy any entries you need to keep:

Each completed ordered_get request invokes its callback once. A missing entry or request failure returns the supplied default. Each completed ordered_get_all request also invokes its callback once; an empty result or request failure supplies an empty array. Request failures are logged.

Last updated