> 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`
* **Число с плавающей точкой (`f64`)**: `Save.set_f64` / `Save.get_f64`
  * Для обычных значений CSL `float` (`f32`) `Save.set_float` / `Save.get_float` — это удобные псевдонимы, которые используют то же хранилище f64 и приводят тип на границе API.
* **Ссылка на предмет инвентаря**: `Save.save_item_reference` / `Save.get_item_reference`
* **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" %}
Всегда задавайте разумное значение по умолчанию для скалярных геттеров. У новых игроков ключей ещё не будет; вызовы scalar `get_*` возвращают это значение по умолчанию, тогда как `get_item_reference` возвращает `null`.
{% endhint %}

### Сохранение экипированного предмета

Используйте ссылку на предмет, чтобы запомнить выбор из любого из постоянных инвентарей игрока, не копируя владение предметом или его свойства в Save:

```go
Save.save_item_reference(player, "equipped_weapon", weapon);

weapon := Save.get_item_reference(player, "equipped_weapon");
if weapon != null {
    equip_weapon(player, weapon);
}

Save.save_item_reference(player, "equipped_weapon", null); // Очистить выбор
```

`Save.save_item_reference` принимает предметы в автоматически сохраняемом игроком `default_inventory` или в любом инвентаре, созданном с помощью `Items.create_player_inventory`. Именованные инвентари подходят даже тогда, когда авто-сохранение инвентаря по умолчанию отключено. Ссылки сохраняются при перестановке слотов внутри этого инвентаря и при повторном входе. Удаление, выбрасывание, уничтожение или перенос предмета в любой другой инвентарь делает его старые ссылки недействительными, даже для того же игрока или если предмет позже вернётся. Сохраните его снова, чтобы выбрать его в новом инвентаре. Стек, полностью поглощённый при объединении, также становится `null`; ссылки на оставшийся стек остаются действительными.

Создавайте все именованные инвентари в `Player.ao_start` до разрешения сохранённых ссылок или записей JSON. Инвентарь, который ещё не был создан, не может пока разрешить свои предметы; геттеры возвращают `null` без его создания. Существующие сохранённые ссылки остаются доступными для чтения.

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

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

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

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

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 %}

`Item_Instance` поля (включая производные типы предметов) используют те же ссылки на инвентарь, что и `Save.save_item_reference`. Любой ненулевой предмет должен принадлежать одному из постоянных инвентарей этого игрока. Перенос его в любой другой инвентарь делает ссылку недействительной, даже для того же игрока. Отсутствующий или недействительный предмет, либо предмет, который больше не соответствует производному типу поля, становится `null`. Создавайте именованные инвентари до загрузки записи. Инвентарь остаётся авторитетным источником данных о владении и предмете.

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

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

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

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

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

        // Загрузить текущий формат
        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);

// Удалить любой из двух видов общих для игры значений
Save.delete_game_key("retired_event");
```

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

`Save.delete_game_key` ставит удаления в очередь и дедуплицирует их вместе с обычной очисткой game-save. Последующий `set_game_string` для того же ключа отменяет поставленное в очередь удаление. Последующий `increment_game_int` сохраняет удаление, сбрасывает сохранённый счётчик до нуля, а затем применяет увеличение.
{% 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 {
    // Постоянные выборы в рамках постоянных инвентарей игрока
    save_item_reference :: proc(player: Player, key: string, item: Item_Instance);
    get_item_reference :: proc(player: Player, key: string) -> Item_Instance;

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

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

    // Для всей игры
    delete_game_key      :: proc(key: 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 один раз; пустой результат или ошибка запроса возвращают пустой массив. Ошибки запросов записываются в журнал.
