> 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/docs-es/datos-y-persistencia/save.md).

# Sistema de guardado

El sistema de guardado proporciona un sistema de almacenamiento de datos clave-valor para conservar el progreso y las preferencias de los jugadores entre sesiones.

El sistema Save es un simple **almacén de pares clave-valor**. Le das una clave de cadena (como `"xp"`), y almacena un valor que seguirá allí la próxima vez que el jugador se conecte.

Hay dos tipos de datos de guardado:

* **Guardado del jugador**: datos personales de un jugador (XP, mejoras, ajustes, inventario, etc.)
* **Guardado global**: compartido por todos en el juego (récords del mundo, ajustes del servidor, contadores globales, etc.)

{% hint style="warning" %}
No uses el sistema de guardado para almacenar las compras que los jugadores hicieron con sparks. Usa el [API de compra/producto](/all-out-docs/docs-es/conceptos-centrales-del-motor/purchasing-product-apis.md) en su lugar.
{% endhint %}

### Inicio rápido (guardar + cargar una estadística)

El patrón más común es:

* Carga los valores guardados en `ao_start`
* Guarda de nuevo cada vez que cambie el valor

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

    ao_start :: method() {
        // Los valores predeterminados se usan para jugadores nuevos
        xp    = Save.get_int(this, "xp", 0);
        level = Save.get_int(this, "level", 1);
    }
}

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

    // ... tu lógica de subida de nivel aquí ...

    // Guarda inmediatamente cuando cambie
    Save.set_int(player, "xp", player.xp);
    Save.set_int(player, "level", player.level);
}
```

### Guardado del jugador (datos por jugador)

El guardado del jugador está limitado a un solo jugador. Cada jugador tiene sus propios datos aislados de clave/valor.

Tipos compatibles:

* **Cadena**: `Save.set_string` / `Save.get_string`
* **Entero (`s64`)**: `Save.set_int` / `Save.get_int`
* **Flotante (`f64`)**: `Save.set_f64` / `Save.get_f64`
  * Para CSL ordinario `float` (`f32`) valores, `Save.set_float` / `Save.get_float` son alias de conveniencia que usan el mismo almacenamiento f64 y hacen la conversión en el límite de la API.
* **Referencia de objeto del inventario**: `Save.save_item_reference` / `Save.get_item_reference`
* **JSON (avanzado)**: `Save.set_json` / `Save.try_get_json`

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

// Eliminar una clave (útil al migrar o eliminar datos antiguos)
Save.delete_key(player, "old_key_name");
```

{% hint style="info" %}
Proporciona siempre un valor predeterminado razonable a los getters escalares. Los jugadores nuevos todavía no tendrán claves; las llamadas escalares `get_*` devuelven ese valor predeterminado, mientras que `get_item_reference` devuelve `null`.
{% endhint %}

### Guardar un objeto equipado

Usa una referencia de objeto para recordar una selección de cualquiera de los inventarios persistentes del jugador sin copiar la propiedad ni las propiedades del objeto en 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); // Borra la selección
```

`Save.save_item_reference` acepta objetos en el `default_inventory` o cualquier inventario creado con `Items.create_player_inventory`. Los inventarios con nombre cuentan incluso cuando el guardado automático del inventario predeterminado está desactivado. Las referencias sobreviven a la reorganización de ranuras dentro de ese inventario y al volver a entrar. Quitar, soltar, destruir o transferir un objeto a cualquier otro inventario invalida sus antiguas referencias, incluso para el mismo jugador o si el objeto luego regresa. Vuelve a guardarlo para seleccionarlo en su nuevo inventario. Una pila consumida por completo por una fusión también se vuelve `null`; las referencias a la pila superviviente siguen siendo válidas.

Crea todos los inventarios con nombre en `Player.ao_start` antes de resolver las referencias guardadas o los registros JSON. Un inventario que aún no se ha creado no puede resolver todavía sus objetos; los getters devuelven `null` sin crearlo. Las referencias guardadas existentes siguen siendo legibles.

### Guardar datos “más grandes” (JSON)

Si tienes un pequeño conjunto de campos (como progreso + cosas desbloqueadas), suele ser mejor guardarlo como un solo bloque JSON.

Solo los campos marcados con `@ao_serialize` se guardan.

```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) {
        // La clave faltante (o el análisis falló) ya tiene los valores predeterminados de la clase.
    }
    return progress;
}
```

{% hint style="info" %}
`Save.try_get_json` devuelve `false` si la clave no existe o la cadena JSON está mal formada. Asigna memoria para los datos de la clase antes de cargar porque la ruta falsa no la asigna. Los campos faltantes conservan los valores predeterminados de la clase, y los campos desconocidos se ignoran.
{% endhint %}

`Item_Instance` los campos (incluidos los tipos de objeto derivados) usan las mismas referencias de inventario que `Save.save_item_reference`. Todo objeto no nulo debe pertenecer a uno de los inventarios persistentes de ese jugador. Transferirlo a cualquier otro inventario invalida la referencia, incluso para el mismo jugador. Un objeto ausente o invalidado, o uno que ya no coincida con el tipo derivado del campo, se vuelve `null`. Crea inventarios con nombre antes de cargar el registro. El inventario sigue siendo la autoridad sobre la propiedad y los datos del objeto.

### Versionado de Save (migrar datos antiguos de forma segura)

Si alguna vez cambias tu formato de guardado, conserva una `versión` como clave y migra los guardados antiguos hacia adelante.

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

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

        if save_version < 6 {
            // Ejemplo de migración: hp antes era un int, ahora es un flotante
            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);

        // Cargar el formato actual
        hp = Save.get_f64(this, "hp", 100);
    }
}
```

### Guardado global (compartido por todos)

El guardado global se comparte con todo el juego, no por jugador.

```go
// Poseedor del récord global
Save.set_game_string("world_record_holder", player.get_username());
holder := Save.get_game_string("world_record_holder", "nobody yet");

// Contadores globales (incremento atómico, seguro cuando muchos jugadores lo actualizan)
Save.increment_game_int("total_games_played", 1);
total := Save.get_game_int("total_games_played", 0);

// Eliminar cualquiera de los dos tipos de valor global
Save.delete_game_key("retired_event");
```

{% hint style="info" %}
Usa `Save.increment_game_int` para contadores que varios jugadores podrían actualizar al mismo tiempo (muertes, uniones, rondas jugadas, etc.). Su `optimistic_update` parámetro tiene como valor predeterminado `true`, por lo que el valor local en caché cambia de inmediato.

`Save.delete_game_key` encola y deduplica las eliminaciones con el vaciado normal del guardado global. Una posterior `set_game_string` para la misma clave cancela la eliminación en cola. Una posterior `increment_game_int` mantiene la eliminación, reinicia el contador almacenado a cero y luego aplica el incremento.
{% endhint %}

### Patrones comunes

#### Booleanos

Guarda los booleanos como `0/1`:

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

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

#### Nomenclatura de claves

Las claves son solo cadenas, así que elige nombres que no entren en conflicto más adelante:

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

Si tienes varios juegos conectados mediante parentado de juego (hub + minijuegos), consulta [Productos/datos entre juegos](/all-out-docs/docs-es/datos-y-persistencia/cross-game-products-data.md) para ver cómo se pueden compartir los datos guardados.

### APIs adicionales

```go
Save :: struct {
    // Selecciones persistentes a través de los inventarios persistentes del jugador
    save_item_reference :: proc(player: Player, key: string, item: Item_Instance);
    get_item_reference :: proc(player: Player, key: string) -> Item_Instance;

    // Eliminar claves
    delete_key      :: proc(player: Player, key: string);
    delete_all_keys :: proc(player: Player);

    // Enumerar claves
    get_all_keys :: proc(player: Player) -> []string;

    // Global
    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;

    // Datos ordenados/en ranking (tablas de clasificación)
    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` y `ordered_get_all` son asincrónicos y se ejecutan en el servidor. Llámalos desde la ruta normal de juego compartido; no añadas una `Game.is_server()` comprobación.

Los arrays del callback solo son válidos durante el callback, así que copia cualquier entrada que necesites conservar:

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

Cada solicitud `ordered_get` completada invoca su callback una vez. Una entrada faltante o un fallo de la solicitud devuelve el valor predeterminado proporcionado. Cada solicitud completada `ordered_get_all` también invoca su callback una vez; un resultado vacío o un fallo de la solicitud proporciona un array vacío. Los fallos de solicitud se registran.
