> For the complete documentation index, see [llms.txt](https://docs.allout.game/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.allout.game/all-out-docs/ru/dannye-i-sokhranenie/save.md).

# Система сохранения

Система Save — это простое **хранилище ключ-значение**. Вы даёте ему строковый ключ (например `"xp"`), и оно сохраняет значение, которое останется там и в следующий раз, когда игрок зайдёт.

Есть два вида данных сохранения:

* **Сохранение игрока**: личные данные одного игрока (XP, улучшения, настройки, инвентарь и т. д.)
* **Глобальное сохранение**: общее для всех в игре (мировые рекорды, настройки сервера, глобальные счётчики и т. д.)

{% hint style="warning" %}
Не используйте систему сохранения для хранения покупок, которые игроки сделали за sparks. Используйте [API для покупок/продуктов](/all-out-docs/ru/osnovnye-koncepcii-dvizhka/purchasing-product-apis.md) вместо этого.
{% endhint %}

### Быстрый старт (сохранение + загрузка характеристики)

Самый распространённый шаблон:

* Загружайте сохранённые значения в `ao_start`
* Снова сохраняйте каждый раз, когда значение меняется

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

    ao_start :: method() {
        // По умолчанию используются значения для новых игроков
        xp    = Save.get_int(this, "xp", 0);
        level = Save.get_int(this, "level", 1);
    }
}

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

    // ... ваша логика повышения уровня здесь ...

    // Сохраняйте сразу, когда значение меняется
    Save.set_int(player, "xp", player.xp);
    Save.set_int(player, "level", player.level);
}
```

### Сохранение игрока (данные для каждого игрока)

Сохранение игрока привязано к одному игроку. У каждого игрока есть свои изолированные данные ключ/значение.

Поддерживаемые типы:

* **Строка**: `Save.set_string` / `Save.get_string`
* **Целое число (`s64`)**: `Save.set_int` / `Save.get_int` (примечание: сейчас значения внутренне обрезаются до 32 бит)
* **Число с плавающей точкой (`f64`)**: `Save.set_f64` / `Save.get_f64`
* **JSON (для продвинутых)**: `Save.set_json` / `Save.try_get_json`

```go
// Предпочтения
Save.set_string(player, "selected_skin", "knight");
music_volume := Save.get_f64(player, "music_volume", 0.8);

// Удаление ключа (полезно при миграции/удалении старых данных)
Save.delete_key(player, "old_key_name");
```

{% hint style="info" %}
Всегда задавайте разумное значение по умолчанию при чтении. У новых игроков ключей ещё не будет, и `get_*` вернёт значение по умолчанию.
{% endhint %}

### Сохранение «больших» данных (JSON)

Если у вас есть небольшой набор полей (например, прогресс + разблокированные вещи), часто удобнее сохранить это как один JSON-blob.

Сохраняются только поля, помеченные `@ao_serialize` .

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

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

load_progress :: proc(player: Player) -> Player_Progress {
    progress := new(Player_Progress);
    if !Save.try_get_json(player, "progress", ref progress) {
        // Отсутствующий ключ (или сбой разбора) уже имеет значения класса по умолчанию.
    }
    return progress;
}
```

{% hint style="info" %}
`Save.try_get_json` возвращает `false` если ключ не существует или строка JSON повреждена. Выделяйте данные класса перед загрузкой, потому что ветка false их не выделяет. Отсутствующие поля сохраняют значения класса по умолчанию, а неизвестные поля игнорируются.
{% endhint %}

### Версионирование сохранений (безопасная миграция старых данных)

Если вы когда-нибудь измените формат сохранения, храните ключ `version` и переносите старые сохранения вперёд.

```go
Player :: class : Player_Base {
    hp: f64;

    ao_start :: method() {
        save_version := Save.get_int(this, "version", 0);

        if save_version < 6 {
            // Пример миграции: раньше hp было целым числом, теперь это число с плавающей точкой
            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);

        // Загрузить текущий формат
        hp = Save.get_f64(this, "hp", 100);
    }
}
```

### Глобальное сохранение (общее для всех)

Глобальное сохранение общее для всей игры, а не для каждого игрока отдельно.

```go
// Обладатель мирового рекорда
Save.set_game_string("world_record_holder", player.get_username());
holder := Save.get_game_string("world_record_holder", "nobody yet");

// Глобальные счётчики (атомарное увеличение, безопасно, когда многие игроки обновляют его)
Save.increment_game_int("total_games_played", 1);
total := Save.get_game_int("total_games_played", 0);
```

{% hint style="info" %}
Используйте `Save.increment_game_int` для счётчиков, которые несколько игроков могут обновлять одновременно (убийства, входы, сыгранные раунды и т. д.). Его `optimistic_update` параметр по умолчанию равен `true`, поэтому локальное кэшированное значение меняется сразу.
{% endhint %}

### Распространённые шаблоны

#### Булевы значения

Сохраняйте булевы значения как `0/1`:

```go
// Сохранить
Save.set_int(player, "tutorial_complete", tutorial_complete ? 1 : 0);

// Загрузить
tutorial_complete = Save.get_int(player, "tutorial_complete", 0) != 0;
```

#### Именование ключей

Ключи — это просто строки, поэтому выбирайте имена, которые позже не будут конфликтовать:

* `"xp"`, `"level"`, `"selected_skin"`
* `"tycoon.cash"`, `"tycoon.upgrades.mouth_level"`

Если у вас несколько игр, связанных через иерархию игр (хаб + мини-игры), см. [Продукты/данные между играми](/all-out-docs/ru/dannye-i-sokhranenie/cross-game-products-data.md) о том, как можно делиться данными сохранения.

### Дополнительные API

```go
Save :: struct {
    // Удаление ключей
    delete_key      :: proc(player: Player, key: string);
    delete_all_keys :: proc(player: Player);

    // Перечисление ключей
    get_all_keys :: proc(player: Player) -> []string;

    // Глобальное перечисление
    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_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));
}
```

`ordered_get` и `ordered_get_all` асинхронны и выполняются на сервере. Вызывайте их из обычного общего игрового пути; не добавляйте `Game.is_server()` проверку.

Массивы callback действуют только во время callback, поэтому копируйте все записи, которые нужно сохранить:

```go
Ranking_State :: class : Component {
    entries: [..]Ordered_Save_Entry;

    refresh :: method() {
        Save.ordered_get_all(
            "weekly_score",
            0,
            100,
            this,
            proc(results: []Ordered_Save_Entry, userdata: Object) {
                state := userdata.(Ranking_State);
                state.entries.clear();
                for result: results {
                    state.entries.append(result);
                }
            }
        );
    }
}
```

Каждый завершённый `ordered_get` запрос вызывает свой callback один раз. Отсутствующая запись или сбой запроса возвращает переданное значение по умолчанию. Каждый завершённый `ordered_get_all` запрос также вызывает свой callback один раз; пустой результат или сбой запроса возвращают пустой массив. Сбои запросов записываются в журнал.
