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)
Do not use the save system to store purchases players made with sparks. Use the Purchasing/Product APIs instead.
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 playersxp=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 changesSave.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.
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 saved JSON no longer matches your structure. Treat that as “start fresh with defaults”.
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).
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.
Player :: class : Player_Base {
hp: f64;
ao_start :: method() {
save_version := Save.get_int(this, "version", 0);
if save_version < 6 {
// Example migration: hp used to be an int, now it's a float
save_version = 6;
old_hp := Save.get_int(this, "hp", 100);
Save.delete_key(this, "hp");
Save.set_f64(this, "hp", old_hp.(f64));
}
Save.set_int(this, "version", save_version);
// Load current format
hp = Save.get_f64(this, "hp", 100);
}
}
// Global record holder
Save.set_game_string("world_record_holder", player->get_username());
holder := Save.get_game_string("world_record_holder", "nobody yet");
// Global counters (atomic increment, safe when many players update it)
Save.increment_game_int("total_games_played", 1);
total := Save.get_game_int("total_games_played", 0);