10. 文字コード・改行・エスケープ

この節は、「エラーは出ないのに、文が消える・改行が変わる・文字が化ける」ときの原因を集めたものです。特に改行のルールは、里々と湊で大きく違います。

文字コード

対象里々湊
台本dic*.txt。既定は Shift_JIS(satori_bootconf.txt の is_utf8_dic で UTF-8 も可)talks/main.mnt(とその include)。UTF-8(BOMなし)だけ
設定ファイルsatori_conf.txtconfig.toml。UTF-8(BOMがあっても読める)
セーブデータsatori_savedata.txt。既定は Shift_JISsave.json。UTF-8(BOMなし)
SSPとの通信里々が処理する常に Shift_JIS(応答に Charset: Shift_JIS を付ける)
ファイル操作—file_read / file_write は既定 UTF-8。第2・3引数に "sjis" を渡すと Shift_JIS
SAORIとの通信—UTF-8(8. SAORI呼び出し)

台本の文字コードを変換する

里々の辞書は書き直すことになるので、多くの場合、エディタで新しい .mnt を作ることになります。既存のテキストをShift_JISからUTF-8(BOMなし)に変換するには、PowerShell で、変換するファイルのあるフォルダに移動して次のようにします。

$enc = New-Object Text.UTF8Encoding($false)
[IO.File]::WriteAllText("$PWD\out.txt", [IO.File]::ReadAllText("$PWD\in.txt", [Text.Encoding]::GetEncoding(932)), $enc)

Set-Content -Encoding UTF8 は、Windows PowerShell 5.1 ではBOM付きで書き出すので、使わないでください。

BOM付きUTF-8は使わない

BOM付きの main.mnt は、エラーを出さずに、ファイルの最初のトークだけを壊します。 先頭の OnBoot が、BOMを含んだ別の名前として登録され、呼ばれなくなります。ファイルの最初のトークだけが動かないので、原因が分かりにくい不具合です。エディタの設定で「UTF-8(BOMなし)」にしてください。

config.toml のBOMは問題ありません。save.json は、BOMがあると壊れたものとして扱われます(9. セーブデータ)。

出力できない文字

湊は、SSPに返すスクリプトを Shift_JIS(Windows-31J)に変換します。Shift_JIS に無い文字は、&#数値; の形(数値文字参照)に置き換わり、そのまま画面に出ます。

OnBoot => {
    湊: ①丸数字と😀と𠮷と髙と−。
}
\0①丸数字と😀と𠮷と髙と-。\e
  • ①(丸数字)、髙、﨑 などは、Windows-31J にあるので出ます。
  • 絵文字(😀)や 𠮷 などは、😀 のように化けます。
  • −(U+2212、数学のマイナス)は、エラーにならず全角ハイフン - に置き換わります。

里々の辞書はもともと Shift_JIS で書かれているので、里々から移した文章は影響を受けません。新しく UTF-8 で書く文章に、絵文字などを入れたときだけ問題になります。

改行のルール

湊が改行として出力するのは、\n(さくらスクリプトの改行)だけで、それが入る条件は決まっています。 里々では、* の中の通常の改行はすべて \n に置き換えられました。湊では、セリフの書き方によって、改行が入る場合と入らない場合があります。

1. 連続したセリフ行には \n が入る

同じ話者の行が続くと、その間に \n が入ります。コロンなしの続き行も同じです。

OnBoot => {
    湊: 一行目
    二行目(続き行)
    湊: 三行目
}
\0一行目\n二行目(続き行)\n三行目\e

2. コード行を挟むと \n が入らない

let や if、foreach など、セリフではない行を挟むと、そこで文が区切られ、改行は入りません。

OnBoot => {
    湊: 一行目
    let x = 1
    湊: 二行目
}
\0一行目二行目\e

for や foreach の中でセリフを出すときも同じです。1回ごとに改行したいなら、セリフの末尾に \n を書きます(5. 配列変数)。

3. 空行も、セリフをつなぐ改行を止める

空行は「文の区切り」として働きます。空行の前後のセリフの間には、改行が入りません。

OnBoot => {
    湊: 一行目
    湊: 二行目

    湊: 空行の後
    湊: 行末セミコロン;
    湊: その次
}
\0一行目\n二行目空行の後\n行末セミコロン\nその次\e
  • 「二行目」と「空行の後」が、つながっています。
  • 行末の ; は取り除かれるだけです。改行の有無には影響しません(「行末セミコロン」の後に \n が入っています)。

4. ; だけの行は「区切り」

; だけを書いた行は、そこまでのセリフを確定させます。改行は入りません。

OnBoot => {
    湊: 一行目
    ;
    湊: 二行目
}
\0一行目二行目\e

5. セリフの直後の行は「続き行」になる

セリフの直後に書いた行は、let / global / call / if などのキーワードで始まっていない限り、セリフの続き行として読まれます。 代入文(x = 5)や関数呼び出し(hello())は、コードではなくセリフになります。

OnBoot => {
    湊: 一行目
    counter = 5
    湊: 二行目
}
\0一行目\ncounter = 5\n二行目\e

counter = 5 が、代入されずにセリフとして出力されています。エラーも出ません。

対策は、次のどれかです。

  • let や global を付ける(global work.counter = 5)。
  • ; だけの行を挟む。
  • call トーク名 の形で呼ぶ(call はキーワードなので、続き行になりません)。

行頭でコードと判断される語は、if else for(foreach を含む)while match func break continue let global return call、{ }、// /*、\、および => を含む行です。これらの語で始まる名前の関数(format(…)、letter = 1 など)も、コードと判断されます。 逆に、それ以外で始まる代入や関数呼び出しは、セリフの直後ではセリフの続き行になります。

6. 半角コロンのある続き行は、話者の指定になる

セリフの行に、半角コロンが含まれ、その前が空白や記号のない文字だけの場合、その行は「話者の指定」と読まれます。

OnBoot => {
    湊: 一行目
    注意:これは続き行のつもり
}
\0一行目これは続き行のつもり\e

注意: が話者名として扱われ(登録されていないので \0 のまま)、「注意:」が消え、改行も入りません。

  • 全角のコロン : は、話者の指定になりません。文中のコロンは全角にしてください。
  • コロンの前に空白があれば(次の行は 例:これ)、話者の指定にはなりません。

7. \ で始まる行は、直前のセリフに \n 付きでつながる

行頭が \ の行は、\-、\e、\![ で始まるものを除き、直前のセリフに \n を挟んでつながります。\q[…](選択肢)が縦に並ぶのはこのためです(7. 選択肢)。

OnBoot => {
    湊: 一行目
    \n[half]
    助手: 二行目
}
\0一行目\n\n[half]\1二行目\e

\n[half] の前にも改行 \n が入るので、改行が2つになっています。間隔だけを入れたいときは、セリフの行末に書きます(湊: 一行目\n[half])。

8. 話者に戻ると \n が入る

一度話した話者が、他の話者のあとにもう一度話すとき、話者のタグの直後に \n が自動で入ります(config.toml の [settings] で auto_newline = false にすると、止められます)。

9. 行頭の空白は削られる

続き行の行頭の空白(全角スペースを含む)は削られます。里々で全角スペースを字下げに使っていた場合、続き行では字下げが消えます。話者の指定のあとの空白は、そのまま出力されます。

OnBoot => {
    湊: 一行目
  二行目(全角スペースで始まる続き行)
    湊: 「全角スペースの後」
}
\0一行目\n二行目(全角スペースで始まる続き行)\n 「全角スペースの後」\e

10. 1行のブロックにセリフは書けない

OnBoot => { 湊: こんにちは } のように、{ と } の間にセリフを書いて1行にまとめることはできません。セリフが行末まで続くため、} がセリフの一部になり、閉じ括弧が見つからなくなります。

OnBoot => { 湊: こんにちは }
\b[2]\0パースエラー:\nmain.mntの1行目: トーク定義は「}」で閉じてください(ファイル末尾付近)\e

if、for、match の中でも同じです。ブロックは、複数行に分けて書いてください。

改行コード

台本のファイルの改行は、CRLFでもLFでも読めます。

$ と \ の扱い

$

${ は、変数・式の展開の開始です。{ が続かない $ は、セリフでも "…" の中でも、ただの文字として出ます。

OnBoot => {
    湊: 100$です。${'${'}も出せます
}
\0100$です。${も出せます\e
  • ${ という並びを文字として出したいときは、${'${'} と書きます(シングルクォートの中は展開されません)。
  • 湊 0.1.3 までは、{ が続かない $ も構文エラーになり、${'$'} と書く必要がありました。この書き方は今も使えます。
  • 正規表現の $(行末)は、'…' の中に書きます('^[0-9]+$')。

\(バックスラッシュ)

セリフでも文字列でも、\ はさくらスクリプトのタグとしてそのまま出力されます。エスケープ処理はありません。\\ は2文字のままで、さくらスクリプトとしては「\ という文字を表示する」の意味になります(7. ウェイト)。

正規表現の \d のような書き方は、\ を1つだけ書きます。

OnBoot => {
    let ok = regex_captures("2026年1月2日", "(\d+)年(\d+)月(\d+)日")
    let ng = regex_captures("2026年1月2日", "(\\d+)年(\\d+)月(\\d+)日")
    湊: ${len(ok)}|${len(ng)}
}
\04|0\e

\\d と書くと、正規表現としては「バックスラッシュ + d」になり、一致しません。\d と書いてください。

里々の記号

* @ $ > _ と全角の () は、湊ではただの文字です。展開されず、そのまま表示されます。

OnBoot => {
    湊: (今日の運勢)は大吉
}
\0(今日の運勢)は大吉\e

里々の辞書から文章をコピーして使うときは、(…) の展開を ${…} に書き換え忘れると、括弧ごと表示されます。φ(括弧をエスケープする記号)も、湊では意味を持たず、文字としてそのまま出力されますので、削除してください。

#(里々のコメント)は、湊では // に、複数行のコメントは /* … */ にします。

湊の書式の細かい規則

  • 行頭の空白は自由です(インデントは何個でも構いません)。
  • 全角の演算子(+ == など)は使えません。半角で書きます。
  • 文字列は "…"(${} の展開あり)と '…'(展開なし)の2種類です。
  • 予約語(if let global など)は、識別子の一部(iffy など)には影響しません。

次は11. 里々/YAYAにあるが湊にない機能に進んでください。