# Save System

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)

{% hint style="warning" %}
Do not use the save system to store purchases players made with sparks. Use the [Purchasing/Product APIs](/core-engine-concepts/purchasing-product-apis.md) instead.
{% endhint %}

### Quick start (save + load a stat)

The most common pattern is:

* Load saved values in `ao_start`
* Save again whenever the value changes

```go
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`

```go
// Preferences
Save.set_string(player, "selected_skin", "knight");
music_volume := Save.get_f64(player, "music_volume", 0.8);

// Deleting a key (useful when migrating/removing old data)
Save.delete_key(player, "old_key_name");
```

{% hint style="info" %}
Always provide a sensible default when reading. New players won't have keys yet, and `get_*` will return your default.
{% endhint %}

### 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.

```go
Player_Progress :: class {
    version: s64 @ao_serialize;
    max_health: s64 @ao_serialize;
    unlocked_skins: [..]string @ao_serialize;
}

save_progress :: proc(player: Player, progress: ref Player_Progress) {
    Save.set_json(player, "progress", ref progress);
}

load_progress :: proc(player: Player, out: ref Player_Progress) {
    if !Save.try_get_json(player, "progress", out) {
        // Missing key (or parse failed) → fill in defaults
        out.version = 1;
        out.max_health = 100;
        out.unlocked_skins = .{};
    }
}
```

{% hint style="info" %}
`Save.try_get_json` returns `false` if the key doesn't exist or the JSON string is malformed. If the key exists but the structure has changed (new fields added, old fields removed), it returns `true` with missing fields zero-filled. Treat the `false` case as “start fresh with defaults”.
{% endhint %}

### Save versioning (migrating old data safely)

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

```go
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);
    }
}
```

### Game-wide save (shared by everyone)

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

```go
// 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);
```

{% hint style="info" %}
Use `Save.increment_game_int` for counters that multiple players might update at the same time (kills, joins, rounds played, etc).
{% endhint %}

### Common patterns

#### Booleans

Save booleans as `0/1`:

```go
// Save
Save.set_int(player, "tutorial_complete", tutorial_complete ? 1 : 0);

// Load
tutorial_complete = Save.get_int(player, "tutorial_complete", 0) != 0;
```

#### 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](/data-and-persistence/cross-game-products-data.md) for how save data can be shared.

### Additional APIs

```go
Save :: struct {
    // Delete keys
    delete_key      :: proc(player: Player, key: string);
    delete_all_keys :: proc(player: Player);

    // Enumerate keys
    get_all_keys :: proc(player: Player) -> []string;

    // Game-wide enumeration
    get_all_game_strings :: proc() -> []Save_Game_String;
    get_all_game_ints    :: proc() -> []Save_Game_Int;
    get_all_game_keys    :: proc() -> []Save_Game_Key;

    // Ordered/ranked data (leaderboards)
    ordered_set     :: proc(document: string, key: string, value: f64);
    ordered_get     :: proc(document: string, key: string, default: f64,
                            userdata: Object,
                            callback: proc(entry: Ordered_Save_Entry, userdata: Object));
    ordered_get_all :: proc(document: string, offset: s64, limit: s64,
                            userdata: Object,
                            callback: proc(entries: []Ordered_Save_Entry, userdata: Object));
}
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.allout.game/data-and-persistence/save.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
