@pasta_search
名前の頭さえ分かれば、シーンも単語も見つけ出せますの。それが @pasta_search ですわ。
もっとも、直接お呼びする機会は少ないと伺っております。
そうですわね……DSL の @単語 や >シーン の裏で、黙々と働いている縁の下の力持ちですもの。けれど仕組みを知っておけば、迷子にはなりませんわ。
@pasta_search は、シーンと単語を前方一致で検索するモジュールである。ランタイムが持つ検索コンテキスト(シーン表と単語表)をユーザーデータとして公開し、メソッドを : で呼ぶ。
local SEARCH = require "@pasta_search"
DSL の単語参照(act:word(name))、Call(>)のシーン解決、ランダムトーク(OnTalk)のシーン選択などは、内部でこのモジュールを使う。シーン関数の中では、これらを経由するのが普通で、@pasta_search を直接呼ぶのは、検索結果を Lua で調べたい場合とテストで選択を固定したい場合である。
利用できる時期
検索対象は、シーン辞書の読み込みの最後に require("pasta").finalize_scene() が実行された時点で確定する。finalize_scene() はランタイムが自動で呼ぶため、シーン関数・イベントハンドラの実行時には検索できる。
finalize_scene() より前(main.lua や、シーン辞書の読み込み中に実行される Lua ブロックのトップレベル)でも require と呼び出しはエラーにならないが、検索対象が確定していないため、結果は使えない。
検索の範囲
どのメソッドも、第 2 引数 global_scene_name の有無で検索の範囲が決まる。
| 第 2 引数 | 検索する範囲 |
|---|---|
省略(nil) | グローバルシーン(search_scene)・グローバル単語(search_word)だけ |
| グローバルシーン名を指定 | 指定したグローバルシーンのローカルシーン・ローカル単語だけ |
第 2 引数を指定した検索は、ローカルに候補が無くてもグローバルへフォールバックしない(nil を返す)。DSL の単語参照や Call で見られる「ローカルを探し、無ければグローバルを探す」順序は、このモジュールではなく act 側の検索手順が、2 回の呼び出し(第 2 引数ありと、なし)で実現している(単語のスコープと優先順位、Call のスコープ解決アルゴリズム)。
候補の選び方
前方一致した候補が複数あるとき、DSL の単語参照と同じシャッフル&順次消費で 1 つを選ぶ(シャッフル&順次消費)。
- 候補を一巡するまで、同じ候補は選ばれない。一巡すると並べ直して最初から使う。
- 並び順の記録は、検索の範囲(第 2 引数)と検索に使った名前の組ごとに持つ。照合用の名前に揃える引数(
search_sceneの第 1 引数、search_wordの第 2 引数)は、揃えた後の名前で組を作る。グローバル単語に解決される DSL の@挨拶(Lua のact:word("挨拶"))とSEARCH:search_word("挨拶")は、同じ記録を進める。 - 記録はゴーストの実行中、イベントをまたいで保たれ、辞書の再読込で消える。
前方一致で集まった候補を次の順に並べたものを、候補の並びと呼ぶ。set_scene_selector・set_word_selector に渡す整数は、候補の並びの位置を指す(set_scene_selector(…) / set_word_selector(…))。
- 候補の検索キーの文字コード順(バイト順)である。検索キーは、単語では単語キー、シーンでは照合の相手(通し番号を除いた名前。search_scene)である。定義を読み込んだ順ではない。たとえば
@挨拶朝:朝1、朝2を@挨拶昼:昼1より先に定義しても、search_word("挨拶")の候補の並びは昼1・朝1・朝2になる(「昼」の文字コードが「朝」より小さい)。 - 同じ単語キーの値どうしは、定義した順に並ぶ。
- 検索キーが同じシーンどうし(同名シーン)は、通し番号の小さい順に並ぶ。
メイン_2はメイン_10より先になる。
メソッド
search_scene(name, global_scene_name?)
シーンを前方一致で検索する。
SEARCH:search_scene(name, global_scene_name?) -> global_name, local_name | nil
| パラメータ | 型 | 必須 | 説明 |
|---|---|---|---|
name | string | ✅ | 検索するシーン名(照合用の名前に揃えてから前方一致) |
global_scene_name | string | ❌ | 指定すると、そのグローバルシーンのローカルシーンだけを検索する |
name は、DSL の Call と同じく、照合用の名前(名前の中の文字と数字と _ 以外の文字を _ に置き換えた名前。シーン名の照合)に揃えてから、シーン名の照合用の名前と前方一致させる。"挨拶・朝" も "挨拶_朝" も、照合用の名前 挨拶_朝 で探す。照合用の名前が同じになるグローバルシーンは区別されず、同じ名前の候補になる。第 2 引数 global_scene_name には global_name(登録名)を渡す。第 2 引数は照合用の名前に揃えずに使う。
第 2 引数を省略したとき(グローバルシーンの検索)は、照合用の名前に揃える前に、name を pasta.toml の別名表で置き換える(シーン名の別名・pasta.toml の [scene])。name が別名と完全に一致するときだけ置き換え先の名前にし、その名前を照合用の名前に揃えて前方一致させる。既定の別名表(OnTalk = ["会話"] を含む)のもとでは、search_scene("会話") は search_scene("OnTalk") と同じ候補から選び、同じ順次消費の記録を進める。"会話・朝" は別名と完全一致しないため置き換えない。第 2 引数を指定したとき(ローカルシーンの検索)は、name を置き換えない。search_word は別名表を使わない。
照合の相手は、シーン名の照合用の名前である。登録名(後述の global_name・local_name)の末尾の _ と通し番号は照合に使わない。name は作者が書くシーン名として扱うため、登録名を渡しても、その登録名のシーンは指さない。たとえば "メイン_1" は、1 つ目の *メイン ではなく、照合用の名前が メイン_1 で始まるシーン(*メイン・1 など)を探す。第 2 引数を指定したときの第 1 引数(ローカルシーン名)も同じである。
Lua ブロックで定義したシーン関数(function SCENE.名前(act))も、そのグローバルシーンのローカルシーンとして検索される。照合の相手は関数名の全体である(SCENE.加算ループ なら 加算ループ)。ただし、関数名が _ と半角数字(0〜9)で終わるときは、生成されたローカルシーンの登録名と見分けず、最後の _ と数字を除いた部分が照合の相手になる(SCENE.step_2 なら step)。全角数字で終わる名前は、関数名の全体が照合の相手である。
戻り値:
- 見つかったとき:
global_name, local_nameの 2 値。global_name: グローバルシーンの登録名。シーン名の照合用の名前の後ろに_と、照合用の名前が同じグローバルシーンの通し番号(1 から)を付けた名前である(例: 1 つ目の*メインは"メイン_1"、2 つ目は"メイン_2"。1 つ目の*挨拶・朝は"挨拶_朝_1")。別名で宣言したシーンの登録名は、置き換え後の名前から作られる。既定の別名表のもとで*OnTalkの無いゴーストの 1 つ目の*会話は"OnTalk_1"になり、*OnTalkと*会話を両方宣言すると、通し番号は両方を通して振られる。第 2 引数を指定したときは、第 2 引数と同じ名前が返る。local_name: グローバルシーンの本体なら"__start__"。ローカルシーンなら、ローカルシーン名の照合用の名前の後ろに_と、同じグローバルシーン内の同名ローカルシーンの通し番号(1 から)を付けた名前(例:"選択肢_1"。・選択・Aなら"選択_A_1")。
- 見つからないとき: 何も返さない(受け取った変数は
nilになる)。
name に空文字列を渡すと、Lua のエラー(Scene search error: Invalid scene name: '')になる。
local SEARCH = require "@pasta_search"
-- グローバルシーンを検索する
local global, local_name = SEARCH:search_scene("メイン")
if global then
print(global, local_name) -- "メイン_1", "__start__"
end
-- メイン_1 のローカルシーンだけを検索する
local g, l = SEARCH:search_scene("選択肢", "メイン_1")
if g then
print(g, l) -- "メイン_1", "選択肢_1"
end
-- 見つからない場合
local result = SEARCH:search_scene("存在しないシーン")
if not result then
print("Scene not found")
end
search_scene_exact(name, global_scene_name?)
シーンを完全一致で検索する。
SEARCH:search_scene_exact(name, global_scene_name?) -> global_name, local_name | nil
引数、照合用の名前への揃え方、別名表での置き換え、戻り値は search_scene と同じである。違うのは候補だけで、照合用の名前が name(揃えた後)と完全に等しいシーンだけが候補になる。search_scene_exact("つっつき反応女") は *つっつき反応女の子Head を選ばない。候補が複数あれば search_scene と同じくシャッフル&順次消費で 1 つを選ぶ。並び順の記録は search_scene とは別に持つ。
つつき・撫での既定ハンドラは、この関数でショートカットのシーンを探す(つつき・撫でのショートカット)。
local SEARCH = require "@pasta_search"
local g, l = SEARCH:search_scene_exact("つっつき反応Head")
if g then
print(g, l) -- "つっつき反応Head_1", "__start__"
end
search_local_scene_exact(global_name, local_name)
グローバルシーンの名前とローカルシーンの名前の組で、ローカルシーンを完全一致で検索する。
SEARCH:search_local_scene_exact(global_name, local_name) -> global_name, local_name | nil, miss
| パラメータ | 型 | 必須 | 説明 |
|---|---|---|---|
global_name | string | ✅ | グローバルシーンの名前(作者が書く名前。別名表で置き換え、照合用の名前に揃えてから完全一致) |
local_name | string | ✅ | ローカルシーンの名前(作者が書く名前。照合用の名前に揃えてから完全一致) |
2 つの引数は、どちらも作者が書くシーン名として扱う。search_scene_exact の第 2 引数と違い、global_name に登録名("会話_1")を渡しても、その登録名のグローバルシーンは指さない。照合用の名前への揃え方は search_scene と同じである。別名表での置き換えは global_name にだけ行い、local_name には行わない。
候補は次の 2 段で選ぶ。
- 照合用の名前が
global_nameと完全に等しいグローバルシーンのうち、照合用の名前がlocal_nameと完全に等しいローカルシーンを持つものだけを候補にし、シャッフル&順次消費で 1 つを選ぶ。同じ名前のグローバルシーンのうち片方だけがローカルシーンを持つときは、持つ方が毎回選ばれる。並び順の記録は、search_scene_exact(global_name)ともsearch_sceneとも別に持つ。 - 選んだグローバルシーンの中で、
local_nameのローカルシーンをsearch_scene_exactと同じ方法で選ぶ(同じ名前のローカルシーンが複数あれば、シャッフル&順次消費)。
戻り値:
- 見つかったとき:
global_name, local_nameの 2 値。どちらも登録名である(search_scene の戻り値と同じ形。例:"会話_1", "挨拶_1")。 - 見つからないとき:
nilと、見つからなかった側を表す文字列の 2 値。"global":global_nameのグローバルシーンが無い。別名で置き換えた後の名前で見つからなかったときは、search_sceneと同じく、書いた名前と置き換えた後の名前の両方を記した警告ログ(Scene not found (alias applied))を出す。"local":global_nameのグローバルシーンはあるが、どれもlocal_nameのローカルシーンを持たない。
global_name か local_name に空文字列を渡すと、Lua のエラー(Scene search error: Invalid scene name: '')になる。
デバッグ用のイベント OnPastaScene の既定ハンドラは、Reference1 を渡されたとき、この関数でローカルシーンを探す(OnPastaScene)。
local SEARCH = require "@pasta_search"
local g, l = SEARCH:search_local_scene_exact("会話", "挨拶")
if g then
print(g, l) -- "会話_1", "挨拶_1"
else
print("not found:", l) -- "global" か "local"
end
search_word(name, global_scene_name?)
単語を前方一致で検索し、候補の値を 1 つ返す。
SEARCH:search_word(name, global_scene_name?) -> string | nil
| パラメータ | 型 | 必須 | 説明 |
|---|---|---|---|
name | string | ✅ | 検索する単語キー(前方一致) |
global_scene_name | string | ❌ | 指定すると、そのグローバルシーンのローカル単語だけを検索する(照合用の名前に揃えてから照合する) |
戻り値:
- 見つかったとき: 単語の値(文字列)。
- 見つからないとき: 何も返さない(受け取った変数は
nilになる)。
第 2 引数には、search_scene が返す global_name("メイン_1" の形の登録名)を渡す。DSL に書いたシーン名("メイン")では一致しない。登録名は照合用の名前を元にした名前である(1 つ目の *挨拶・朝 なら "挨拶_朝_1")。
第 2 引数(検索の範囲)は、照合用の名前に揃えてから照合する。第 1 引数の単語キーは揃えずに、そのまま前方一致させる。
local SEARCH = require "@pasta_search"
-- グローバル単語を検索する
local word = SEARCH:search_word("挨拶")
if word then print(word) end -- 例: "こんにちは"
-- メイン_1 のローカル単語だけを検索する
local g = SEARCH:search_scene("メイン")
local reply = SEARCH:search_word("返事", g)
scene_aliases()
実際に使っている別名表(既定の表と pasta.toml の [scene.alias] に書いた行を合わせた結果)を返す(pasta.toml の [scene])。
SEARCH:scene_aliases() -> { [置き換え先] = { 別名, ... } }
戻り値: キーが置き換え先の名前、値が別名の文字列の配列である表。別名の配列は辞書順(文字コード順)に並ぶ。別名表が空なら空の表を返す。
local aliases = SEARCH:scene_aliases()
-- 既定の表のもとでは
-- aliases["OnTalk"] → { "会話" }
-- aliases["sakura.recommendsites"] → { "おすすめメニュー" }
-- aliases["sakura.portalbuttoncaption"] → { "ポータルボタン名" }
- 呼ぶたびに新しい表を返す。返った表を書き換えても、別名表と検索は変わらない。別名表は pasta.toml でだけ変えられる。
set_scene_selector(…) / set_word_selector(…)
テストのために、候補の選び方を決定論的にする。set_scene_selector はシーンの検索、set_word_selector は単語の検索に効く。
SEARCH:set_scene_selector(n1, n2, ...) -- 決定論的な選択にする
SEARCH:set_scene_selector() -- 既定(シャッフル)に戻す
SEARCH:set_word_selector(n1, n2, ...) -- 決定論的な選択にする
SEARCH:set_word_selector() -- 既定(シャッフル)に戻す
| パラメータ | 型 | 説明 |
|---|---|---|
n1, n2, ... | integer | 0 以上の整数(候補の並びの位置。0 始まり) |
- 整数を 1 個以上渡すと、シャッフルをやめ、巡(候補を重複なく返し切るまでのひとまわり)の中で候補を返す順番を、渡した整数の列で指定する。整数は候補の並びの 0 始まりの位置で、
0は先頭の候補、1は 2 番目の候補を指す。 - 巡の最初の検索は位置
n1の候補を、次の検索は位置n2の候補を返す。整数を使い切ったら、その巡でまだ返していない候補を、候補の並びの順に返す。 - 一巡すると、次の巡も整数の列の先頭から当てはめる。整数の列は 1 本で、すべての巡に同じ順を当てはめる。巡ごとに違う順は指定できない。別の順にするには、
set_scene_selector・set_word_selectorを呼び直す。 - 整数の列は、検索(検索に使った名前と検索の範囲の組)ごとに独立して当てはまる。ある検索の巡は、ほかの検索を何回挟んでも変わらない。
- 候補の数以上の整数と、その巡ですでに返した位置の整数は、その検索では無視する(エラーにならない)。
- 引数なしで呼ぶと、既定のシャッフルに戻す。
- どちらの呼び出しも、それまでの並び順の記録を消す。呼んだ後の最初の検索は、新しい巡の最初の検索になる。
- 負の整数を渡すと Lua のエラー(
expected non-negative integer argument)、整数でない引数(文字列・小数など)を渡すと Lua のエラー(expected integer argument)になる。エラーになった呼び出しは、設定も並び順の記録も変えない。 - 設定はランタイム全体の検索に効く。DSL の単語参照・Call・ランダムトークのシーン選択も同じ選び方になるため、テストの後は引数なしで呼んで既定に戻す。
SEARCH:set_scene_selector(0) -- 同名シーンを候補の並びの順(通し番号順)に選ぶ(メイン_1, メイン_2, ...)
SEARCH:set_word_selector(0) -- @挨拶:こんにちは、やあ → "こんにちは", "やあ", "こんにちは", ...
-- @天気:晴れ、曇り、雨(候補の並びは 晴れ, 曇り, 雨)
SEARCH:set_word_selector(1) -- 2 番目を最初に選ぶ → "曇り", "晴れ", "雨", "曇り", "晴れ", "雨", ...
SEARCH:set_word_selector(2, 0) -- 3 番目、1 番目の順 → "雨", "晴れ", "曇り", "雨", "晴れ", "曇り", ...
-- テストの後は既定に戻す
SEARCH:set_scene_selector()
SEARCH:set_word_selector()
前方一致にシャッフル、そして決定論的な選択――これで検索の手綱は、あなたのものですわ!
とはいえ、テストの後で既定へ戻し忘れることもございましょう。
そのときは、わたくしがきっちり叱って差し上げますわ。引数なしで呼び直せば、元どおりですもの。