# ヘッドレスモード

ヘッドレスモードは、ウィンドウ、オーディオデバイス、ゲームパッドなしでゲームを実行します。スクリプトが入力と時計を動かし、スクリーンショットを書き出します。CI でのスモークテスト、ルートがエンディングに到達するかの確認、ビルド間のスクリーンショット比較などの自動テストに使います。

## ヘッドレスでの実行 {#running-headless}

```text
kagari-desktop-runtime [GAME_DIR] --headless --script <FILE> --out <DIR> [--seed <N>] [--data-dir <DIR>]
```

| オプション | 意味 |
| ---------- | ---- |
| `--headless` | ウィンドウなしで実行します。`--script` と `--out` が必要です。 |
| `--script <FILE>` | 入力スクリプト（後述）。 |
| `--out <DIR>` | 出力ディレクトリ。なければ作成します。キャプチャはここに書き出されます。 |
| `--seed <N>` | 新しい物語すべてに使う乱数シード。既定は `1`。 |
| `--data-dir <DIR>` | 設定とセーブのディレクトリ。既定は `<out>/data`。 |

`--script`、`--out`、`--seed` を `--headless` なしで指定すると使い方の誤り（ステータス 2）です。`GAME_DIR` は[ランタイム](./runtime.md#command-line)と同じです。

ヘッドレスモードでも GPU で描画するため、GPU ドライバは必要ですが、ディスプレイサーバーは不要です。ハードウェアの GPU がない環境（多くの CI マシン）では、Mesa の llvmpipe のようなソフトウェアアダプタで動作します。

## 決定性 {#determinism}

ヘッドレスでの実行は再現可能です。同じゲーム、スクリプト、シードからは同じキャプチャが得られます。

| 項目 | ウィンドウあり | ヘッドレス |
| ---- | -------------- | ---------- |
| 時計 | 実時間 | 0 ms から始まり、`wait` でのみ進む |
| セーブ時刻（`local_time`） | ローカル時刻 | 常に 2026-01-01 00:00:00 |
| 新しい物語のシード | システム時計とプロセス ID から | `--seed`、既定は `1` |
| 設定とセーブ | プラットフォームのデータディレクトリ | `--data-dir`、既定は `<out>/data` |
| オーディオ | 再生する | 処理するが再生しない |
| ゲームパッド | 読み取る | なし |

設定とセーブは実行をまたいでデータディレクトリに残ります。毎回まっさらな状態から始める必要がある場合は、それを削除するか、新しい `--out` を使ってください。

## スクリプトの形式 {#script-format}

スクリプトは 1 行に 1 コマンドを書いたテキストファイルです。引数は空白で区切ります。空行と、最初の空白でない文字が `#` の行は読み飛ばされます。コマンドは順に実行されます。

| コマンド | 動作 |
| -------- | ---- |
| `wait MS` | 時計を `MS` ミリ秒進めます。最大 16 ms 刻みで進め、刻みごとにゲームを更新します。 |
| `key KEY` | キーを押して離します。 |
| `down KEY` | キーを押したままにします。 |
| `up KEY` | キーを離します。 |
| `click X Y` | マウスをデザインピクセル（`X`, `Y`）に動かし、左ボタンを押して離します。 |
| `move X Y` | マウスをデザインピクセル（`X`, `Y`）に動かします。 |
| `wheel DY` | `DY` ノッチだけスクロールします。`DY > 0` は上方向です。 |
| `capture NAME` | 現在のフレームを `<out>/NAME.png` に書き出し、`wrote <path>` を表示します。`NAME` は `A-Z a-z 0-9 _ -` からなる 1〜64 文字です。 |
| `expect-crash` | この時点でゲームがエラーで停止している必要があります。 |
| `assert-no-crash` | ゲームがまだ動作している必要があります。 |

- `KEY` は `std::system::Key` のバリアント名です。`Enter`、`Escape`、`Space`、`Backspace`、`Tab`、`Up`、`Down`、`Left`、`Right`、`PageUp`、`PageDown`、`Home`、`End`、`Shift`、`Control`、`Alt`、`F1`〜`F12`、`A`〜`Z`、`Digit0`〜`Digit9`。大文字と小文字は区別されます。
- 座標とホイール量は 10 進数（`285`、`12.5`）です。座標はゲームのデザイン解像度で表し、ウィンドウの大きさには依存しません。
- 入力コマンド（`key`、`down`、`up`、`click`、`move`、`wheel`）は時計を進めません。ゲームは現在の時刻でそれを処理します。アニメーションやテキストを進めるには `wait` を加えてください。
- キャプチャはデザイン解像度の PNG ファイルです。

スクリプト全体はゲームの開始前にパースされます。構文の誤りがあると、`script:<line>: <message>` というメッセージ（例: `` script:3: unknown key `space` ``）を出してステータス 5 で止まります。

### エラーとクラッシュ {#errors-and-crashes}

ゲームがエラーで停止したとき（[ゲームがエラーで停止したとき](./runtime.md#when-the-game-stops-with-an-error)を参照）:

- `expect-crash` がなければ、次の `capture` 以外のコマンドでステータス 3 で終了します。最後のコマンドの実行中にエラーが起きた場合もステータス 3 で終了します。
- `expect-crash` はエラーを受け入れます。以降の `capture` 以外のコマンドはすべて無視され、実行はステータス 0 で終わります。`capture` はエラー画面を書き出します。
- ゲームがまだ動作しているときの `expect-crash` はスクリプトのエラー（ステータス 5）です。
- エラー後の `assert-no-crash` はステータス 3 で終了します。

ゲームが自ら終了した場合（たとえば「終了」ボタンで）、残りのコマンドは飛ばされ、ステータス 0 で終わります。スクリプトの最後でゲームがまだ動作していれば、プレイヤーがウィンドウを閉じたときと同じようにゲームを終了させます。

## 終了ステータス {#exit-statuses}

| ステータス | 意味 |
| ---------: | ---- |
| 0 | スクリプトが最後まで実行された（またはゲームが終了した）。 |
| 2 | 使い方の誤り、`game.kagari` がない、またはゲームを開始できない（不正な `.kagari` ファイル、このランタイムにないビルトインのインポート、フォントの読み込み失敗。[ランタイム](./runtime.md#when-the-game-stops-with-an-error)を参照）。 |
| 3 | スクリプトが想定していないエラーでゲームが停止した、または `assert-no-crash` が失敗した。 |
| 4 | GPU の障害。使えるアダプタがない、メモリ不足、デバイスの喪失。 |
| 5 | スクリプトのエラー。スクリプトを読めないかパースできない、`expect-crash` の時点でゲームが動作中、`--out` を作成できない、キャプチャを書き出せない。 |

## 例 {#example}

`example/test_game1` のスモークテストです。タイトル画面を待ってキャプチャし、タイトルの Start ボタンをクリックして、最初の台詞をキャプチャします。

```sh
cat > smoke.txt <<'EOF'
wait 2000
capture title
# the title's Start button
click 285 265
wait 1000
capture first-line
assert-no-crash
EOF
kagari-desktop-runtime example/test_game1/target/v0 --headless --script smoke.txt --out shots --seed 1
```

`shots/title.png` と `shots/first-line.png` を書き出し、設定とセーブは `shots/data` に保存されます。最後にゲームがまだ動作していれば、終了ステータスは 0 です。
