# Saves

A game can keep its saves in slots the Hub knows about. The Hub lists each
game's slots and lets the player delete them. It never opens, reads or edits what's
inside a save. Like achievements, saves live in the player's browser, in the
same shared `localStorage` ([why this works](achievements.md#why-this-works-without-a-backend)).

## Slots

A slot is one save, with a stable **slot id**: `a-z`, `0-9` and `-`, at most 40
characters, for example `main`, `slot-1` or `autosave`. A game may have any number
of slots.

Only progress belongs in a slot. Settings such as volume, quality or language are
not saves: keep them out of slots, so that deleting a save never resets them.

With the SDK:

```ts
uniplay.saves.write('slot-1', state, { label: 'Mansion 3', summary: '$182,000 stolen · 4 crew', playtime: 4210 });
const state = uniplay.saves.read('slot-1'); // the data, or null
uniplay.saves.list(); // every slot, newest first
uniplay.saves.remove('slot-1');
```

## Storage: `localStorage`

| Key | Value |
|---|---|
| `uniplay:saves:<game id>` | The index: one entry per slot, read by the Hub. |
| `uniplay:saves:<game id>:<slot id>` | The save data: any JSON value, in a format only the game knows. |

The index:

```json
{
  "v": 1,
  "slots": {
    "slot-1": {
      "label": "Mansion 3",
      "summary": "$182,000 stolen · 4 crew",
      "playtime": 4210,
      "updatedAt": 1790000000000
    }
  }
}
```

| Field | Required | Meaning |
|---|---|---|
| `updatedAt` | yes | Last write, in milliseconds since epoch. The Hub lists the newest slot first. |
| `label` | no | Name to show, at most 40 characters. Defaults to the slot id. |
| `summary` | no | One line about where the player is, at most 80 characters. |
| `playtime` | no | Seconds played in this save. |

- To write a slot, write the data key first, then re-read the index, set the slot's
  entry and write the index. If the index can't be written (storage full), put the
  previous data back, so there's never data the Hub can't see.
- A slot exists only when it has both an index entry and a data key. Readers skip
  an entry whose data key is missing; data without an entry isn't listed.
- Readers must ignore unknown fields, malformed entries and invalid slot ids, and
  treat a missing or unparsable index as "no saves". The data is the game's
  business: if its format changes, version it inside the data.
- Storage can be unavailable or full. Games must keep working and simply not save,
  and may tell the player.

Each game writes only its own keys. The Hub only deletes ([deleting](#deleting-from-the-hub)),
and restores slots from the player's cloud saves.

## Deleting from the Hub

To delete a slot, the Hub removes the data key, then re-reads the index and
removes the slot's entry. It never touches achievements: unlocks stay when their
save is gone. With cloud saves on, the slot is deleted from the cloud
too, and the Hub may also write slots: it restores them in the same order a game
writes them (data key first, then the index entry), and never while that game runs.

A game reads its slots when it starts, so a deleted slot simply isn't there next
time. A game open in another tab during a delete may write that slot again; that's
accepted for now.

## Playthroughs

Players can start a new game without losing the one they have. **Start a new game** in Game
details puts every slot of the game aside in one bundle, and the game finds no saves the next
time it starts. **Switch** brings a put-aside playthrough back: its slots are written again
exactly as the game left them (data key first, then the index entry), after the current ones
are put aside in turn. Achievements, time played and settings never move.

The Hub does this only while the game isn't running, so a game needs nothing new: it already
reads its slots when it starts, and treats no saves as a new game. Put-aside playthroughs live
under keys the Hub owns ([storage keys](storage-keys.md#keys-the-hub-owns)) and sync to the
cloud like slots. A game that has its own save menu can keep several slots as before; the
Hub puts all of them aside together.

## Moving existing saves

Games that saved under their own keys before adopting slots move them on start: if
the old key exists and the slot is empty, copy the value into the slot (as JSON
data), write the index entry, then delete the old key. The SDK does this with
`saves.adopt(oldKey, slot)`. Settings keys stay where they are.
