# Saving progress

Source: https://zafnok.github.io/shuyi-engine/learn/tutorial/07-save/

The player shouldn't have to start again each time. In this step, the game remembers the furthest level
reached, and the levels past it can't be chosen yet:

![The level menu with only Level 1 bright: the levels not reached yet are dim.](https://zafnok.github.io/shuyi-engine/tutorial-tries/locked/before.png)

Levels not reached are dim. Win the first, and the next lights up:

![After winning level 1, the menu is on Level 2, and Level 2 is now bright.](https://zafnok.github.io/shuyi-engine/tutorial-tries/reached-two/before.png)

| File | What changes |
| ---- | ------------ |
| `crates/game/src/progress.rs` | New: what is kept, and how |
| `crates/game/src/levels.rs` | The menu reads it and writes it |
| `crates/game/src/stages.rs` | `LevelMenu::new` now needs `ctx` |
| `crates/game/src/lib.rs` | `pub mod progress;` |
| `Cargo.toml`, `crates/game/Cargo.toml` | Adds `ron` |
| `crates/game/tests/board.rs` | Tests of saving |

Add `ron` as you added `serde`. In the top `Cargo.toml`, under `[workspace.dependencies]`, add the line
`ron = "0.12"`, and in `crates/game/Cargo.toml`, under `[dependencies]`, add `ron.workspace = true`.

## Where saves go

> **Shuyi:** a game never opens a file itself. It goes through **storage**, which the platform hands it:
> a file on a computer, the browser's storage on the web, and only memory in a test. So the same code saves
> on every machine the game runs on, and a test needs no disk. It's the same idea as actions, and
> as `ctx` itself: the game asks for a thing, and doesn't care how it's done.

Storage keeps small named texts, and that's all. `ctx.storage` has `read` and `write`, and a key names
each text, just as a key names each line of your words.

## What is kept

> **Game idea:** always write the **version** of your save's format into the save. Some day you'll change
> what a save holds, and a player will still have an old one. A later build has to be able to tell what it's
> reading, or it will read it wrong.

Make `crates/game/src/progress.rs`:

```rust
use serde::{Deserialize, Serialize};
use shuyi::core::{SaveFormat, SaveHeader};
use shuyi::ui::storage::{Storage, StorageError};
```

```rust
/// What is kept: the version of the format, and the furthest level.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Progress {
    /// The version of the format that wrote it.
    pub version: u32,
    /// The furthest level reached, counted from 0.
    pub furthest: usize,
}

impl SaveFormat for Progress {
    const NAME: &'static str = "Progress";
    /// Raised whenever this type changes shape or meaning.
    const VERSION: u32 = 1;
}

/// Where it is kept, in the player's profile.
// check-text: not player text, a storage key
pub const KEY: &str = "progress";

/// The furthest level reached: the first, if nothing readable is kept.
pub fn furthest(storage: &dyn Storage) -> usize {
    let Ok(Some(text)) = storage.read(KEY) else {
        return 0;
    };
    match ron::from_str::<SaveHeader<Progress>>(&text) {
        Ok(header) if header.is_current() => {
            ron::from_str::<Progress>(&text).map_or(0, |progress| progress.furthest)
        }
        _ => 0,
    }
}

/// Keeps `level` as the furthest reached, if it is further.
pub fn reached(storage: &mut dyn Storage, level: usize) -> Result<(), StorageError> {
    if level <= furthest(storage) {
        return Ok(());
    }
    let progress = Progress {
        version: Progress::VERSION,
        furthest: level,
    };
    let text = ron::to_string(&progress).map_err(|e| StorageError::Backend(e.to_string()))?;
    storage.write(KEY, &text)
}
```

Take this piece by piece.

`Progress` is what's kept. It derives `Serialize` and `Deserialize`, so serde can write it as text and read
it back. `SaveFormat` gives it a name and a version number, which you raise whenever the type changes.

`furthest` reads it. Its job is to never fail: if anything's wrong, the answer is level 0.

> **Rust:** `storage: &dyn Storage` means "a borrow of anything that implements the `Storage` trait". This
> function doesn't care whether that's a file or the browser. It's the same `dyn` as `Box<dyn Screen>`.
>
> `let Ok(Some(text)) = storage.read(KEY) else { return 0; };` is **let-else**: "this must match, or else do
> this". `read` returns a `Result` holding an `Option`: it could have failed, and if not, there might still
> be nothing saved. This handles both, in one line, and from here on `text` is the saved text.
>
> `header.is_current()` is checked in a match guard, like you saw in the last step, and `_ => 0` catches
> everything else: a version it doesn't know, or text it can't read. `map_or(0, ...)` says "use 0 if this
> failed, and otherwise get this from it".

> **Shuyi:** a `SaveHeader` reads only the version of a save, even one whose other fields can't be read any
> more. So a game can tell "this is from another version" apart from "this is damaged".

`reached` writes it, only if it's further than what's there. It returns a `Result`, as writing can fail,
for instance on a full disk. `map_err` turns one kind of error into another, so `ron`'s error can be
returned as a `StorageError`.

## Using it

The menu now has more to remember: how many levels are open. In `levels.rs`, add a field to the struct,
`pub open: usize,`, and start it from what's saved. `new` needs the context for that:

```rust
    /// The menu, on the furthest level the player has reached.
    pub fn new(ctx: &Ctx<Data>) -> Self {
        let last = ctx.game.own.levels.len().saturating_sub(1);
        let furthest = progress::furthest(&ctx.storage).min(last);
        Self {
            focus: furthest,
            open: furthest + 1,
        }
    }
```

That's why `stages.rs` changes `LevelMenu::new()` to `LevelMenu::new(ctx)`, in both `first` and `screens`.
Note how the menu is careful with what it reads: `.min(last)` means an old save can't point past the last
level, if a later version has fewer.

Levels past it can't be chosen. It's one more call in the chain:

```rust
    /// The menu's items: a text key and a value each.
    pub fn menu(&self, ctx: &Ctx<Data>) -> Menu<Choice> {
        let levels = ctx.game.own.levels.len();
        let items = (0..levels).map(|n| {
            MenuItem::keyed("levels.level")
                .with_arg("n", n + 1)
                .with_enabled(n < self.open)
                .with_value(Choice::Level(n))
        });
        Menu::new(items.collect()).focused(self.focus)
    }
```

The engine's `Menu` does the rest: it draws them dim, and skips them when moving the focus. Prove it to
yourself by opening every level:

In `crates/game/src/levels.rs`:

```diff
- .with_enabled(n < self.open)
+ .with_enabled(true)
```

![Only Level 1 is bright.](https://zafnok.github.io/shuyi-engine/tutorial-tries/all-open/before.png)

![Every level is bright, and can be chosen.](https://zafnok.github.io/shuyi-engine/tutorial-tries/all-open/after.png)

Then, when a level is won, keep the next one. In `uncovered`, after `self.focus` is set:

```rust
            self.open = self.open.max(self.focus + 1);
            // A storage that fails loses only the player's progress.
            let _ = progress::reached(&mut ctx.storage, self.focus);
```

`let _ =` is a decision here, not laziness, as the comment says: if saving fails, the player loses
only their progress, and that's no reason to stop their game. `&mut ctx.storage` is the changing kind of
borrow, as writing changes it.

## The test

The Harness has a storage of its own, in memory, which starts empty for every test:

```rust
#[test]
fn the_furthest_level_is_kept_and_opens_the_menu_there() {
    let mut h = on_board();
    assert_eq!(game::progress::furthest(&h.ctx().storage), 0);
    h.keys("Right Right Enter");
    assert_eq!(game::progress::furthest(&h.ctx().storage), 1);
    // The menu made next, as on the next run, opens there.
    let menu = LevelMenu::new(h.ctx());
    assert_eq!((menu.focus, menu.open), (1, 2));
}
```

It makes a brand new menu to stand in for the next time the game starts, without any disk at all. And since
bad saves are the sort of thing players really do have, it's worth testing that those count as nothing:

```rust
#[test]
fn a_kept_progress_of_another_version_or_damaged_counts_as_none() {
    let mut h = game::game().harness();
    let storage = &mut h.ctx_mut().storage;
    storage
        .write(game::progress::KEY, "(version: 9, furthest: 3)")
        .unwrap();
    assert_eq!(game::progress::furthest(storage), 0);
    storage.write(game::progress::KEY, "(version").unwrap();
    assert_eq!(game::progress::furthest(storage), 0);
}
```

This needs `use shuyi::ui::storage::Storage;` at the top, as you have to name a trait to call its functions.

Now play this step in your browser from the button at the top of this page. Win a level, reload the page, and
the menu remembers. That save is in the browser's storage, and your code didn't need to know.

## What you learned

- **Rust:** `let ... else`, `&dyn`, `map_err` and `map_or`, and that a trait must be named to use it.
- **Game idea:** versions in saves, and treating a bad save as no save.
- **Shuyi:** `Storage`, `SaveFormat` and `SaveHeader`.

[Chapter 7](https://zafnok.github.io/shuyi-engine/learn/book/saves-and-settings/index.md) has more on saves. The game is now whole. Next: the things that
make it pleasant.
