Kagari
キーワードを入力してドキュメントを検索します。

中核概念

セーブ、再生、決定性

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

セーブは到達可能性の証拠

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

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

安定した選択肢 ID

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

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

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

セーブの内容

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

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

ロードと食い違い

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

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

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

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

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

パッチで変えてよいもの

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

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

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

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

スナップショットとトレース

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

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

巻き戻し

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

セーブスロット

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

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

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

決定性

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

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

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

プレビュー

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

最終更新日 2026年10月29日