# セーブ、再生、決定性

Kagari のセーブはメモリのダンプではありません。物語の開始からのプレイヤーの選択の記録であり、
ロードはそれを再生します。この章では、セーブに何が含まれるか、既存のセーブを壊さずにパッチで
何を変えてよいか、そしてなぜ再生が正確なのかを説明します。

## セーブは到達可能性の証拠 {#a-save-is-a-witness-of-reachability}

物語は [プッシュダウン機械](./story.md) で、入力はプレイヤーの選択と乱数生成器の抽選だけです。
エントリーシーン、その引数、乱数生成器のシード、選択の列が与えられれば、物語の位置は一意に
決まります。セーブが記録するのはまさにその列、**トレース** で、ロードはエントリーシーンから
物語をもう一度実行し、記録された選択を与えながら、セーブした位置に着くまで進めます。

したがってセーブは状態ではなく、*その位置が到達可能であることの証拠* です。つまり開始からの
経路です。到達不能な位置のセーブは作れず、ロードによって、物語自身では作れない物語変数の組み合わせが
生まれることもありません。そのため、コンパイラが物語に対して行うすべての解析は、ロードした
ゲームについても成り立ちます。

### 安定した選択肢 ID {#stable-choice-ids}

トレースは行番号やアームの位置を記録しません。各選択を、三つの部分からなる **選択肢 ID** として
記録します。

| 部分 | 例 | 由来 |
| ---- | -- | ---- |
| `choice!` を含む関数のパス | `crate::story::Prologue` | モジュールツリーと関数名 |
| 選択肢の `id` | `confess` | `choice!` の `id = "…"` |
| 選ばれたアーム | `crate::story::Confess::Yes` | アームの値。列挙型のバリアントならそのパス、そうでなければ `#n`（アームの位置） |

例えば `crate::story::Prologue/confess/crate::story::Confess::Yes` です。呼び出しの文脈は再生が
補うので、関数が多くの場所から呼ばれたりループの中で実行されたりしても、選択肢の `id` はその関数の
中で一意であれば十分です。アームの値は自分で宣言した列挙型のバリアントにしてください。位置で
記録されたアームは、アームの並べ替えで壊れます。

## セーブの内容 {#what-a-save-contains}

| 部分 | 内容 |
| ---- | ---- |
| トレース | エントリーシーンのパス、エントリー引数、PRNG のシード、すべての選択の選択肢 ID、最後の選択の後のステップ数 |
| 終了ハンドラ | `nav::start` に `on_exit` として渡した関数のパス |
| スクリプトハッシュ | セーブを作ったときの `.kagari` ファイルの SHA-256 |
| スロット情報 | ローカル時刻、最後の行の話者とテキスト、画面の 320×180 のサムネイル |
| フォーマットのバージョン | セーブのレイアウトのバージョン |

セーブにはシステムの状態は含まれません。モードスタックも、設定も、メタもありません
（[入れ子になった二つの機械](./system.md#two-nested-machines) を参照）。スロット情報はセーブ
ページで `saves::info(slot)` と `ui::thumbnail(slot)` が表示するもので、ロードには使われません。

## ロードと食い違い {#loading-and-divergence}

`nav::load(slot)` はスロットを読み、そのトレースを実行中のプログラムで再生します。再生は `s!`
ステップを自動で進め、各 `choice!` では記録された選択肢 ID に出会い、記録されたアームを選ばなければ
なりません。トレースを使い切ると、最後の選択の後に記録されたステップ数だけ進んで止まります。
復元されたゲームはモードスタック全体を置き換えます。

再生は静かに行われます。既読フラグや既見集合を設定せず、メタへの書き込みを繰り返さず、音を
鳴らさず、オートセーブもしません。復元されたステップはステップへの入場から始め直します。
タイムラインが最初から始まるので、そのアニメーションはもう一度再生され、ボイスも再び流れます。

セーブがもうプログラムに合わない場合、ロードは **食い違い**（`Divergence`）で止まります。
`nav::load` は `Err(divergence)` を返してスタックを変えず、`divergence.describe()` は
プレイヤー向けのメッセージを返します。ロードが食い違うのは次の場合です。

| 原因 | メッセージが伝えること |
| ---- | ---------------------- |
| スロットが空、または範囲外 | スロットが空であること |
| ファイルが読めない、またはフォーマットのバージョンが未知 | セーブが読めないこと |
| エントリーシーンがもう存在しない | 見つからない物語 |
| エントリー引数がエントリーシーンのパラメータ型にもう合わない | 開始時の値が合わないこと |
| 物語が記録とは別の選択肢に到達する | 期待した選択肢と実際の選択肢 |
| 記録されたアームがない、またはそのガードが偽になった | 選択肢となくなったアーム |
| 記録された選択を使い切る前に物語が終わる | 残った選択の数 |
| 最後の選択の後に続くステップが記録より少ない | 期待したステップ数と到達したステップ数 |
| 再生中に物語が実行時エラーで止まる | そのエラー |
| 1,000,000 ステップ以内に位置に到達しない | ステップの上限 |

クイックロードのバインディングは食い違いをログにだけ出します。ロードページでは `describe()` で
表示します。

### パッチで変えてよいもの {#what-a-patch-may-change}

トレースが持つのは選択肢 ID と最後のステップ数だけなので、セーブはほとんどの編集を乗り越えます。
編集箇所より *後ろ* の位置のセーブについては次の通りです。

| 変更 | セーブへの影響 |
| ---- | -------------- |
| テキストの言い回し、話者、ボイス、テキスト効果の変更 | なし |
| レンダー、アセット、画面、アニメーション、オーディオの編集 | なし。画面は再生で作り直される |
| 二つの選択肢の間の `s!` ステップの追加・削除・並べ替え | なし |
| アームの値が列挙型のバリアントである選択肢のアームの並べ替え | なし |
| 選択肢へのアームの追加 | なし |
| ゲームシステムの変更：UI、モード、設定、バインディング | なし |
| セーブ位置の手前、最後の選択の後にある `s!` ステップの追加・削除 | 別の行に着地する。残りのステップが足りなければ食い違う |
| 選択肢を含む関数、その `id`、アームの列挙型やバリアントの改名・移動 | その選択肢で食い違う |
| アームの削除、または記録されたアームを隠すガード | その選択肢で食い違う |
| 物語のロジックを変えて別の選択肢に先に着く、または物語が早く終わる | 食い違う |
| エントリーシーンの改名・移動、またはパラメータ型の変更 | 食い違う |
| 位置で記録されたアーム（`#n`）の並べ替え | セーブは黙って別のアームを選ぶ |

物語変数、その初期値、条件の変更は、記録された選択が引き続き同じ選択肢を辿る限り安全です。
辿らなくなった場合、再生は最初にそうなった場所を報告します。

既読フラグ（既読スキップとバックログでの既読表示に使われます）はセーブの外に保存され、関数と、
その関数の中でのステップのコード順の位置をキーにしています。したがって関数の中でステップ（`s!`
または `choice!`）を追加・削除すると、同じ関数の中でそれより後ろにあるすべてのステップの既読
フラグがずれます。そうしたパッチの後では、プレイヤーが読んだ行の一部が未読として扱われ、その逆も
起こります。他の関数のステップには影響しません。

## スナップショットとトレース {#snapshots-and-traces}

トレースは **互換性の契約** です。スクリプトのバージョンをまたいで意味を持つのはセーブのこの部分
です。**スナップショット**（ある位置での物語の状態と論理画面）は、その位置に安く到達するための
キャッシュにすぎません。正確に同じプログラムに依存するので、スクリプトハッシュが一致するときにだけ
信頼されます。それ以外のスクリプトでは、エンジンはトレースを再生します。

*暫定:* 現在のエンジンはセーブにスナップショットを書きません。すべてのロードと巻き戻しは、物語の
先頭からトレースを再生します。スクリプトハッシュはすでに記録されており、現在は一つの細部を
決めています：クロージャとして書いた終了ハンドラは同じスクリプトでだけ復元され、名前の付いた
関数はパスで探されます。

## 巻き戻し {#rewind}

`game.rewind(i)` は実行中のゲームをバックログのエントリー `i` に戻します。現在のトレースをその
エントリーで切り、ロードと同じように再生します。メタは決して変えません。既読フラグと既見集合は
設定されたままです。

## セーブスロット {#save-slots}

ゲームには三種類のスロット `SaveSlot::Manual(k)`、`SaveSlot::Quick`、`SaveSlot::Auto(k)` が
あります。

- **手動** スロット `0..save_slots` は、セーブページから `game.save(slot)` で書き込みます。
- **クイック** スロットは、[バインディング](./system.md#bindings) の `QuickSave` と `QuickLoad`
  アクションが書き込み、読み込みます。
- **オート** スロット `0..auto_slots` は、すべての選択肢ステップに入るときと終了時に、エンジンが
  スロットを順に巡りながら書き込みます。

設定された数を超えるスロット番号は無視されます。セーブは何もせず、スロットは空として読まれます。
API の仕様は [`std::system::saves`](../../std/system-saves.md) にあります。

## 決定性 {#determinism}

再生が正確なのは、選択と選択の間に物語が行うことがすべて決定的だからです。

- **乱数はシードされ、記録されます。** シーンは `rng_choose::<T>()` で乱数を引きます。
  `nav::start` のたびに物語の PRNG が新しくシードされ、シードはトレースの一部、生成器の状態は
  物語の状態の一部です。したがって再生はすべての抽選を再現し、ロードによって抽選をやり直すことは
  できません。
- **浮動小数点数はありません。** 言語にあるのは有界整数、Rust の整数、厳密な型 `Rat` と
  `Decimal` です（[数値](../syntax/numbers.md) を参照）。どの計算もすべてのプラットフォームで同じ
  結果になります。
- **レンダーは純粋です。** レンダーは直前の画面の `const fn` なので、選択を再生すれば同じ論理画面が
  作り直されます（[レンダーは物語を見ない](./presentation.md#renders-are-blind-to-the-story) を
  参照）。
- **システムは物語に届きません。** 設定、メタ、シグナル、実時間は物語に流れ込みません。メタが物語に
  届くのはエントリー引数を通してだけで、エントリー引数はトレースに記録されます。

同じモデルが物語を解析可能にもしています。コンパイラにとって乱数の抽選は、プレイヤーの選択と
同じく、`T` のすべての値にわたる自由な選択です。

## プレビュー {#preview}

*予定:* 同じ仕組みが制作用のプレビューにも使えます。エディタでステップにジャンプすることは、
そこまでトレースを再生することです。そのコストはキーフレームで抑えられます。物語は先頭から
（スクリプトが変わっていなければスナップショットから）再生されますが、レンダーは経路上の最後の
[キーフレーム](./presentation.md#keyframes) からだけ、アニメーションなしで実行されます。
レンダーを編集しても、無効になるのは現在の経路でそのレンダーが最初に使われた後の画面だけです。
プレビューのツールはまだ提供されていません。
