Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

@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
パラメータ型必須説明
namestring✅検索するシーン名(照合用の名前に揃えてから前方一致)
global_scene_namestring❌指定すると、そのグローバルシーンのローカルシーンだけを検索する

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_namestring✅グローバルシーンの名前(作者が書く名前。別名表で置き換え、照合用の名前に揃えてから完全一致)
local_namestring✅ローカルシーンの名前(作者が書く名前。照合用の名前に揃えてから完全一致)

2 つの引数は、どちらも作者が書くシーン名として扱う。search_scene_exact の第 2 引数と違い、global_name に登録名("会話_1")を渡しても、その登録名のグローバルシーンは指さない。照合用の名前への揃え方は search_scene と同じである。別名表での置き換えは global_name にだけ行い、local_name には行わない。

候補は次の 2 段で選ぶ。

  1. 照合用の名前が global_name と完全に等しいグローバルシーンのうち、照合用の名前が local_name と完全に等しいローカルシーンを持つものだけを候補にし、シャッフル&順次消費で 1 つを選ぶ。同じ名前のグローバルシーンのうち片方だけがローカルシーンを持つときは、持つ方が毎回選ばれる。並び順の記録は、search_scene_exact(global_name) とも search_scene とも別に持つ。
  2. 選んだグローバルシーンの中で、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
パラメータ型必須説明
namestring✅検索する単語キー(前方一致)
global_scene_namestring❌指定すると、そのグローバルシーンのローカル単語だけを検索する(照合用の名前に揃えてから照合する)

戻り値:

  • 見つかったとき: 単語の値(文字列)。
  • 見つからないとき: 何も返さない(受け取った変数は 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, ...integer0 以上の整数(候補の並びの位置。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()

クローディア(高笑い)

前方一致にシャッフル、そして決定論的な選択――これで検索の手綱は、あなたのものですわ!

アンソニー(素)

とはいえ、テストの後で既定へ戻し忘れることもございましょう。

クローディア(したり顔)

そのときは、わたくしがきっちり叱って差し上げますわ。引数なしで呼び直せば、元どおりですもの。