# プログラマブル準AIモジュール "華和梨" ユーザーズマニュアル

2004/02/01
Phase 8.2.0

華和梨開発チーム :
NAKAUE.T (Meister), 偽Meister (夢乃), さとー, 酔狂, さくらのにえ

## Index

← back

> 1. はじめに
> 2. 辞書ファイル
> 2.1. コメント
> 2.2. セキュリティレベル設定
> 2.3. 暗号化辞書
> 3. エントリ定義
> 3.1. 文と文字列
> 3.2. クォート文字列
> 3.3. ブロック
> 3.4. 複数行対応
> 4. 置換とエントリ呼び出しと実行
> 4.1. エントリ呼び出し
> 4.2. 実行
> 4.3. 置換の入れ子
> 5. エントリ呼び出しのバリエーション
> 5.1. 集合演算式
> 5.2. エントリ配列呼び出し
> 5.3. 履歴参照(中級)
> 5.4. 一時エントリ(上級)
> 6. 演算式
> 6.1. 比較演算
> 6.2. 論理演算
> 6.3. ビット演算
> 6.4. 数値と文字列の扱い
> 7. インラインスクリプト(中級)
> 7.1. 構文コマンドと関数コマンド
> 7.2. if
> 7.3. 組み込みコマンドとユーザ定義コマンド
> 8. SHIORI/SAORIインターフェース
> 8.1. コールバック
> 8.2. その他のシステムエントリ
> 8.3. 栞としての華和梨
> 8.4. SAORIモジュールとしての華和梨
> 8.5. SAORIモジュールの使用宣言
> 8.6 SAORIモジュール呼び出し
> 9. 応用編
> 9.1. イベント反応
> 9.2. コミュニケート
> 9.3. 自発トーク

## 1. はじめに

ユーザーズマニュアルでは、華和梨のほぼ全ての文法と機能を紹介します。 これとKISリファレンスで、 一通りの機能が使えるようになります。

初めて華和梨を使う方は、 ユーザーズマニュアルより先にGetting Started を読み、イメージを掴んでください。

何もないところからゴーストを作るのは大変手間がかかります。 簡単に華和梨の動作を確認していくためには、 コンソールUI版アプリケーション『幸水』 を使ってください。幸水なしの華和梨修得は、まず考えられません。

## 2. 辞書ファイル

> 辞書ファイルとは、 華和梨に対して、文章や語彙や動作パターンを指示するためのファイルです。 辞書ファイルが、あなたのゴーストを記述する全てです。

華和梨が最初に読む辞書ファイルは"kawarirc.kis"です。

辞書ファイルは、'='で始まる行によって、 辞書定義ゾーンと、スクリプト記述ゾーンの二つのゾーンに分かれます。

辞書定義ゾーンは辞書定義を書く領域であり、 スクリプト記述ゾーンはKISを直接書き、その場で実行する領域です。

初期状態(辞書ファイルを読み始めた状態)では辞書定義ゾーンで、 =kisで始まる行から =endで始まる行までがスクリプト記述ゾーンです。

# ここは辞書記述ゾーン
sentence : \0\s[7]${地名}よ、私は帰ってきた！\1\s[10]……。\e
sentence : \0\s[7]${坊や}は死んだ！　何故か！？\e
=kis
# ここはスクリプト記述ゾーン
load dict-gundam.txt;
logprint ガンダム辞書のろーど。;
=end
# 再び辞書記述ゾーン
地名 : サンシャイン60,晴海,幕張,有明

スクリプト記述ゾーンにおける文法は、 '$(' 〜 ')' の間の文法と完全に同一ですので、ここでは省略します。 スクリプト記述ゾーンで書いたスクリプトは、 そのスクリプトが読み込まれた瞬間に実行される ということだけ注意してください。

辞書定義ゾーンにおける文法は3章以下で詳しく述べます。

### 2.1. コメント

行頭、空白記号以外の最初の文字がシャープ'#' だった場合、その行全体はコメントと見なされ、無視されます。

また、':rem'のみの行から ':endrem'のみの行までの領域全体もコメントと見なされ、 無視されます。

両者とも、後述の全ての文法に優先して処理されます。 複数行に分けた記述の場合、行の先頭の文字に'#' が来ないように注意してください。 その場合はクォート文字列にするなどして逃げることができます。

:rem
=======================================================
何行もコメントを書くときは
こんな風にすることもできますが
むしろ、あるエリアを一括してコメントアウトしたいときの方が
便利でしょう
=======================================================
:endrem
function checker $(
if $[$@arg[0]=~"OK"] $(
# OKの場合
echo オッケー;
) else $(
# 失敗(?)の場合
echo シテオク;
)
)

### 2.2. セキュリティレベルの指定

「何か。」から送られてくるイベントの中には、 ローカルマシン(ゴーストの動作しているコンピュータ)以外の場所から SSTPなどを通じて送られてくるものがあります。 このようなイベントを排除するために 以下の記述によってセキュリティレベルを設定することが出来ます。

何も書かなかった場合、自動的に安全な設定(high)になるので、 普通は記述する必要はありません。

記述する際には必ず初期化時に読み込まれるファイルに書いて下さい。 kawarirc.kisに書くのが確実です。 ゴーストが動作し始めてからはセキュリティレベルを変更することはできません。

securitylevel level ;
  セキュリティレベルをlevelに設定する。 levelに書けるのは以下。

  0 / low | 全てのイベントを許可する。
  1 / middle | 外部からやってきたイベントを禁止。
  2 / high | 当面は1(middle)と同じ。
  3 / ultrahigh | 明確にローカルマシン発行と記されたイベントのみ許可。

# 指定する場合は、kawarirc.kisのできるだけ
# 先頭に近い部分を強く推奨します。
=kis
securitylevel ultrahigh;
=end

なお、華和梨の動作中、 セキュリティレベルは数値表現(0〜3)で "System.SecurityLevel"に格納されています。 書き換えることは出来ません。

### 2.3. 暗号化辞書

ネタをよりばれにくくするために、辞書ファイルに簡単な暗号化を施すことができます。 暗号化には付属のkawari_encode2.exeを用います。 使用方法は、DOSプロンプトで以下のように入力します。

kawari_encode2 辞書ファイル名1 辞書ファイル名2 ...

そうすると、キーワードを聞かれるので、適当なキーワードを入れてください。 暗号化されたファイルが作成されます。

C:\home\suikyo\project\kawari\kiu\current> kawari_encode2 dict-sample.txt
Input Keyword : this is test

辞書ファイルのファイル名の拡張子を「.kaw」に変えたファイルが、 暗号化された辞書ファイルです。 辞書ファイル中、 ":crypt"と書かれた行から、 ":endcrypt"と書かれた行までが暗号化の対象となります。 それ以外の行は元のまま残されます。

例 dict-sample.txt
#############################
# 著作権表示など
#############################
# ここから暗号化
:crypt
# 会話データ
sentence : \h昨日${npw}を${npp}で偶然見かけたよ。\e
sentence : \h${npp}良いとこ一度はおいで、はぁーじょいなじょいな。\e
# 名詞-固有名詞-人名 ( npw )
npw : 鈴木一郎 , 山本太郎
# 名詞-固有名詞-地名 ( npp )
npp : 首相官邸 , ホワイトハウス
npp : ${npw}の家
:endcrypt
# ここまで暗号化

このファイルを暗号化すると、次のようなファイルが生成されます。 (見やすさのために一部行を折り返してあります)

例 dict-sample.kaw
#############################
# 著作権表示など
#############################
# ここから暗号化
!KAWA0001uJuYMVcg2jveOeM75g==
!KAWA0001uMvd1szd1tvdmIKY5NA1SCtCnMPWyM/FOkicw9bIyMU6f
TNMKeo0EToROhc6BTpeOfrk3Q==
!KAWA0001uMvd1szd1tvdmIKY5NCcw9bIyMUvfzoaOn46CTBSK8A6d
ToQOho6fTn5OnU6JznjOg46XToaOnA6DjpdOho6cDn65N0=
!KAWA0001uJuYLgQ2NJU0fS/0LgQ2NJUo1C4EmJCY1sjPmJE=
!KAWA0001uNbIz5iCmC9RLmAwUiDhmJSYNuouwykGIOE=
!KAWA0001uJuYLgQ2NJU0fS/0LgQ2NJUq1i4EmJCY1sjIm JE=
!KAWA0001uNbIyJiCmDZJKTIyFyv4mJSYO8I7Nzv7O9871 jv9O+A=
!KAWA0001uNbIyJiCmJzD1sjPxTp0MX4=
# ここまで暗号化

生成された暗号化辞書ファイル「dict-*.kaw」を、kawarirc.kisで指定して下さい。 暗号化ファイルの拡張子は、.kaw以外に変更しても大丈夫です。

暗号化したファイルを元に戻したい場合は、 kawari_decode2.exeを使います。

kawari_encode2 辞書ファイル名1 辞書ファイル名2 ...

やはりキーワードを聞かれるので、 暗号化の時に入力したキーワードを入れてください。

C:\home\suikyo\project\kawari\kiu\current> kawari_decode2 dict-sample.kaw
Input Keyword : this is test

すると、拡張子が「.txt」の、復号化された辞書ファイルが作成されます。

注意：すでに同名のファイルがあっても上書きされてしまいます！

## 3. エントリ定義

華和梨は、全てのデータを「エントリ」に分類して保持しています。 辞書定義とは、エントリ定義行を沢山並べたものです。 エントリ定義は以下の書式('['〜']'は、無くても良い部分)で書いてください。

エントリ名 [ , エントリ名, エントリ名 ... ] : 文 , 文 , 文 ... <改行>
エントリ名 [ , エントリ名, エントリ名 ... ] ( 文 , 文 , 文 ... ,<改行>
文 , 文 , 文 ... <改行>
... )

> Phase 8では、以前「単語」と呼んでいたものを、 「文」と「単語」に区別していますが、 主に細かい文法定義上の問題ですので気にしなくて結構です。

コロン":"やカンマ","の 前後には自由に空白を入れられます。

エントリを複数並べる形式は、同じ文を複数のエントリに登録する場合に使って下さい。 '('〜')'で囲う後者の形式は、 文を何行にも分けて並べたい場合に使ってください。 '('〜')'の間は自由に改行できます。 これはその中に幾つもの文を並べられる点で、 後述の「ブロック」とは異なるので注意してください。 分かりづらければ使わなくても構いません。

# 単語の登録
お昼の挨拶 : こんちは。, ちーっす。, にょース。,やっ, ゴルァ
萌え, コスプレ : メイド, 眼鏡っ娘, チャイナ服
# 文章の登録も同じです
ランダム会話.夜更かし : \0\s[0]やっぱり緑茶でしょ。\1\s[10]…\w8…\w8は？\w8\0\s[3]コーヒーより緑茶の方がカフェインは多いよね。\1\s[10]そやな。\w8\0今夜も仕事は続きそうなので、緑茶を飲もうかと――――\1\s[11]スコッチ\0\s[2]……\w8\w8え？\1\s[11]スコッチ\e
# どこいつ的文章の例
ランダム会話.電波系 : 私たちは、たぶん、${地名}と${地名}にひきさかれる${人間関係}の、最初の世代だ。
# イベント処理スクリプトなど(後述)も同じ
イベント.OnCommunicate : $(if $[$(Reference 0)=="まゆら"] ……これは夢？　それともまぼろし？)

エントリ名に使える文字は以下です。 強調してあるものは、新たに使えるようになったものです。

英数字(A〜Za〜z0〜9) / アットマーク('@') / クエスチョンマーク('?') / ピリオド('.') / アンダーバー('_') / いわゆる全角文字

ただし、アットマーク「@」 は一時エントリに用いるので、普段は使わないでください。 さらに、エントリ名の先頭に「.」は使えません。 また、エントリ名中に「.」が連続した場合、 一つにまとめられます(npw.....special → npw.special)。 'System.'で始まるエントリは、 華和梨が特別に使うエントリ名ですので勝手に使ってしまわないように注意してください。

以上の決まり事を守る限り、エントリ名は自由に決められます。 自分にとって分かりやすい名前にしてください。 いわゆる「全角文字」が自由に使えるので、積極的に使うと良いでしょう (タイプは少し面倒かもしれませんが……)。

### 3.1. 文と文字列

文は、「文字列」「置換」「ブロック」を好きなだけ並べたものです。 置換とブロックは後述します。

文字列は、例えば以下のようなものです。

\0\s[3]死んだゴーストにしてやれることなんて無いさ。\1\s[10]……心にもないことを\e

ほとんどの文章は、このように地の文としてそのまま書くことができます。 しかし、ごく少数(以前よりも減っています)ながら、使えない文字があります。

まず、以下の記号は、必ず使えません。 後述のブロックの中では、これ以外の制限はありません。

クォート2種 '"' , ''' / ドルマーク '$' / 丸括弧 '(' , ')'

エントリ定義文にベタに書いている場合、 さらにカンマ','が使えません。 また、インラインスクリプト中にベタに書いた場合、 セミコロン';'が使えません。 それぞれ、その場所で文を区切るために使われるため、使えないことは感覚的に分かります。 実質的に気を付けなければならないのは最初に挙げたものだけです。

また、文の前後、及び行の先頭末尾にあるベタの空白文字 (半角スペース、タブ、改行可能な場所では改行)は無視されます。 こうした場所に空白を書くには、次のクォートを使ってください。

テスト文字列 : We are the champions ","my friends
=kis
echo ${テスト文字列}
# 「We are the champions ,my friends」が返る
=end

### 3.2. クォート文字列

前述の使えない文字や文頭文末の空白を文字列として華和梨の文に含めるには、 クォート文字列を使います。

" クォート文字列の中では空白も ${エントリ}呼び出し形式も、カンマ(,)や括弧や、セミコロン(;)も書けます"

上のように、 ダブルクォート(")もしくは、 (シングル)クォート(') で囲まれた文字列を「クォート文字列」と呼びます。 クォート文字列では、ほぼ全ての文字を、「書いたまま」の形で出力できます。

クォート文字列では出力できない文字はありません。 が、記述上、ちょっと変わった書き方をしないといけないことが2つだけあります。

「\"」もしくは「\'」 (クォートに使っている文字の方)
  「"」もしくは「'」が出力されます。 クォート文字列内で、クォートに使っている文字を出力するために使います。
「\\」
  「\」が出力されます。 「\」マークを出力するために必ず「\\」を書かなければならないわけではない ことに注意してください。通常は、そのまま"\c"や"\s"などと書いてください。

さて、では「ダブルクォートを出力する」にはどうすればいいでしょうか。

"\""${偽名}"\""って呼ぶなーっ！

このようにするのが一つの答えです。もう一つは：

'"'${偽名}'"'って呼ぶなーっ！

いずれも、色が変わっている部分がクォート文字列です。

もう一つ。閉じクォート直前に\マークを出力するには：

echo-mode > 華和梨では、"こんな風に→「\\\"」"書くと、クォート直前に\が書けます。
華和梨では、こんな風に→「\"」書くと、クォート直前に\が書けます。
echo-mode > 華和梨では、'こんな風に→「\"」'書くと、クォート直前に\が書けます。
華和梨では、こんな風に→「\"」書くと、クォート直前に\が書けます。

### 3.3. ブロック

文中、'(' 〜 ')'で囲まれた部分を 「ブロック」と呼びます。 '$( ' 〜 ')'は、 後述のインラインスクリプトですので、間違えないでください。

ブロックの効果は2つだけです。

- ブロックの中を強制的に一つの単語要素とみなさせる (そのため、','や';'などの、 文を区切るための文字が単語の一部としてみなされます)
- ブロックの中では改行が許される

空白文字は通常通り無視されます。 置換のルールなども全て他の場所と同じです。 また、ブロック用の括弧'(', ')'は、 もちろん出力には現れません。

ブロックは、主に複数行に分けて文を書きたいときに使われます。

# 従来
sentence.メインメニュー : \t\0\s[0]メニューだよん\n\n\q0[RandomTalk][トーク]\q1[TestCommand][テスト]\q2[Communicate][コミュニケート]\q3[PrefTalk][トーク設定]\q4[Cancel][キャンセル]
# Phase 8
sentence.メインメニュー : (
\t\0\s[0]メニューだよん\n
\n
\q0[RandomTalk][トーク]
\q1[TestCommand][テスト]
\q2[Communicate][コミュニケート]
\q3[PrefTalk][トーク設定]
\q4[Cancel][キャンセル]
)

ブロックを使って複数行記述をしていても、 エントリ定義の最後には必ず改行が必要なことに注意してください。

呪文 : (修行するぞ修行するぞ
修行するぞ修行するぞ
修行するぞ修行するぞ)

上記は、こうも書けます。

呪文 : (
修行するぞ修行するぞ
修行するぞ修行するぞ
修行するぞ修行するぞ
)

しかし、これは間違いです。

呪文 :
(
修行するぞ修行するぞ
修行するぞ修行するぞ
修行するぞ修行するぞ
)

なぜなら、「呪文 : 」のところで、 エントリ定義文が終わってしまっているからです。

### 3.4. 複数行対応

「開き括弧に対応する閉じ括弧が来ない間は改行がいくらあってもよい」 というのが、Phase 8の複数行対応の考え方です。 ブロック、エントリ呼び出し、エントリ配列呼び出しの添え字部、 演算式、インラインスクリプトなど、 括弧に囲まれた場所すべてにおいて、 文法的に空白が許される場所ならばどこにでも改行が入れられるようになりました。 そうした場所では改行は空白文字(スペースやタブと同じ)と見なされます。

また、括弧書きのエントリ定義(「エントリ名 '(' ')'」形式)により、 エントリ定義中に全く自由に複数行記述ができるようになりました。

## 4. 置換とエントリ呼び出しと実行

> 最も重要な章です。 疑問が湧くたびにここに立ち戻り、繰り返し読んで理解してください。

上記までで、エントリが呼び出されたとき、 どんな文字列でも出力できるようになりました。 しかし、華和梨が真に華和梨たる知的動作を行うためには、 次に述べる置換機能を欠かすことができません。

華和梨は4つの置換機能を持っています。 そのいずれもが、'$'で始まり、 括弧記号などで決められる特定の範囲を持っています。

# [1] エントリ呼び出し
${エントリ名}
# [2] エントリ配列呼び出し
$エントリ名[数字]
# [3] 演算式
$[式 (1+2*3とか)]
# [4] インラインスクリプト
$(スクリプト文; スクリプト文; スクリプト文 ... )

これらの詳細については順番に詳しく解説します。 ただ、いずれの機能についても、 置換部('$'で始まる記述)を、その実行結果で置き換える という一点は共通です。 中には実行結果が無いものもあります。 そうしたものは、単に置換記述が見えなくなるだけ(空文字列で置き換えた)となります。

また、置換はその文が実行されるたびに起きます。 ですから、特にランダムな動作をするエントリ呼び出しなどは、 文を実行するたびに違った結果を返します。

### 4.1. エントリ呼び出し

エントリに登録した文を呼び出すことを「エントリ呼び出し」と言います。 例えば、「人名」という名前のエントリを呼び出すには、以下のように書きます。

# 人名エントリに幾つかの人名を登録
人名 : サザエ, カツオ, ワカメ, タラ, フグタ
# 人名エントリを呼び出すときは、このように書く
${人名}

エントリ呼び出しのすることは2つです。

1. エントリに登録されている文の中から一つをランダムで選ぶ
2. 選んだ文を実行する

2番目の「実行する」ことが重要なので、覚えてください。 場合によって「評価する」「評価される」などとも呼ばれます。

次節に続きます。

### 4.2. 実行

華和梨辞書でエントリに登録するときの文は、 決してそのまま「何か。」などに送られる形ではありません。

\0\s[0]私は${なまえ}。${しょくぎょう}やってます。相方は${ともだち}だよ。\e

上の例は、${なまえ}、${しょくぎょう}、${ともだち} のそれぞれが正しい文字列に置き換わることを意図しています。

# エントリ定義
なまえ : さくら
しょくぎょう : ゴースト
ともだち : うにゅう

このような辞書があった場合、先ほどの例を実行すると正しい文字列になるでしょう。

\0\s[0]私はさくら。ゴーストやってます。相方はうにゅうだよ。\e

つまり、

「実行」＝「置換を実際に行うこと」

と覚えてください。 華和梨を使い込んでいくと、 置換が行われるタイミングを知ることがどうしても必要となります。 込み入った文を書いて混乱してしまったとき、 「実行(あるいは評価)」が行われるのがいつなのか、 それに注意するようにしてください。

このルールを知れば、クォート文字列の扱いと置換が異なることも理解できると思います。 クォート文字列における「エスケープ」(\"など)は、 実行されるたびに結果が変わる必要はありません。 そこで、読み込まれた段階で既にエスケープの処理は行われています。

さて、それ自体が置換機能であるエントリ呼び出しが、 選び出した文に対してさらに実行を行うため、 必然的にエントリ呼び出しは何度も行われることになります。

なまえ : さくら , ふたば
しょくぎょう : ゴースト , デスクトップマスコット
ともだち : うにゅう , ただきちさん
批評 : 私から言わせれば${なまえ}って、${しょくぎょう}やってる場合じゃないと思うの。
批評 : 私、${なまえ}は嫌いだけど${ともだち}って好きだな。
# 入り口。sentenceというエントリ名は歴史的理由による。
sentence : ${批評}

ここで、${sentence}を実行します。

1. 登録された唯一の文である「${批評}」が選択される。
2. 実行する。「批評」を指定したエントリ呼び出しが起きる。
   1. (例えば)「私から言わせれば${なまえ}って、${しょくぎょう}やってる場合じゃないと思うの。」が選択される。
   2. これも実行する。まず、「なまえ」のエントリ呼び出し。
      1. 「さくら」が選択される。
      2. 実行する。置換子が無いので、変化無し
      「${なまえ}」が「さくら」と置き換えられる。
      「しょくぎょう」のエントリ呼び出し。
      1. 「デスクトップマスコット」が選択される。
      2. 実行する。置換子が無いので、変化無し
      「${しょくぎょう}」が「デスクトップマスコット」と置き換えられる。
   「${批評}」が「私から言わせればさくらって、デスクトップマスコットやってる場合じゃないと思うの。」と置き換えられる。

このように、エントリ呼び出しは、置換子が無くなるまで全ての置換を実行します。

### 4.3. 置換の入れ子

置換は、お互いに入れ子状にすることができます。 置換の種類は問いません。入れ子の深さにも制限はありません。

$(set あの話題 ${この話題})
$[ ${幅} * ${高さ} ]
${ 駄文のリスト.${カウンタ} }
$駄文のエントリ[ ${カウンタ} ]
今から10年後って言うと、西暦$[ $(date %Y) + 10 ]年だね。
# 分かりにくいですが、これでもちゃんと動きます
$${間接参照}[$[${ベース}+${ポインタ}]]

入れ子になった置換は必ず内側から置換されます。 例えば：

1. 今から10年後って言うと、西暦$[ $(date %Y) + 10 ]年だね。
2. 今から10年後って言うと、西暦$[ 2002 + 10 ]年だね。
3. 今から10年後って言うと、西暦2012年だね。

あるいは

1. $[ ${幅} * ${高さ} ]
2. $[ 1600 * 1200 ]
3. 1920000

という具合です。

Phase 7ユーザは、 以前はできなかった(entry/evalコマンドで実現していた) ${ ${ } } という入れ子ができるようになっていることに注意してください。

# 「何か。」からのGETリクエストに対し、「event.<イベント名>」を呼ぶ
System.Callback.OnGET : ${event.${System.Request.ID}}

なお、以下の部分だけは例外的に置換にできません。注意してください。

- 演算式の演算子
- 構文コマンドのコマンド名 (関数コマンドは可)
- 集合演算式の演算子

例えば以下はエラーです。

# 足したり引いたりしたい
$[ 100 ${plus_or_minus} 10 ]
# 場合によってuntilとwhileを使い分けたい
$(${until_or_while} ${条件} $(実行文 ) )
# 共通単語を呼びたい
${x${and}y}}

## 5. エントリ呼び出しのバリエーション

### 5.1. 集合演算式

エントリ呼び出しの中身('${'〜'}'の間)には、 以下のようなことも書けます。

${ 作家 & 女性 }
${ ゴースト & 植物 }
${ 男性 + 女性 }
${ ゴースト - 友達 }

一行目は「「作家」エントリと「女性」エントリの両方に入っている文の中から一つを選ぶ」 (もちろん選んだ後に実行します)、 二行目は「「ゴースト」エントリと「植物」エントリの両方に入っている文の中から一つを選ぶ」、 同様に、 三行目は「「男性」エントリと「女性」エントリのどちらか」、 四行目は「「ゴースト」エントリに入っていて「友達」エントリに入っていない」 文から一つを選びます。

これらは幾らでも並べられます。

優先度について：

数式では'*'や'/'が、 '+'や'-'よりも 「優先」されます。 例えば

100 - 10 * 2 = 80

など。

同様に華和梨の集合演算式では'&'が、 他の2つの演算子よりも「優先」されます。

${ 男性 + 女性 & 作家 }

と書くと、「『男性』、もしくは『女性かつ作家』」が選ばれます。 これを「男性か女性、かつ作家」に変えるには、数式と同様、こう書きます。

${ (男性 + 女性) & 作家 }

なお、ジャンル分けをやりやすくするために、 ${エントリ名}のみの文は、 その先のエントリの持つ文まで候補に入れます。

作家 : ${SF作家}, ${ファンタジー作家}, ${ミステリ作家}, ${良くわかんない作家}
SF作家 : ティプトリJr.
ファンタジー作家 : トールキン
ミステリ作家 : クリスティ
良くわかんない作家 : キング
男性 : アシモフ, トールキン, ポオ, キング
=kis
echo ${男性 & 作家}
# 「トールキン」もしくは「キング」
=end

### 5.2. エントリ配列呼び出し

エントリに登録された文の内、ある特定の位置の文を選ぶ時に使います。

# サンプル
$sentence[0]
$人名[2]
$演説[${最後に喋った演説}+1]

上記のように「'$' + エントリ名 + '[' + 演算式 + ']'」 という形式になります。 演算式については後述します。

エントリ配列呼び出しを実行すると、 指定エントリの、指定番目(これをインデックスと言います)の文を選択し、実行します。 インデックスは0から数え始めます。 普通「一番目」と呼ぶものは、「0」番目ですので気を付けてください。 また、インデックスに負の値を入れた場合は、後ろから数え始めます。

指定番目の文が存在しなかった場合の結果は空文字列("")です。

以下、幸水の表記で動作を示します。

# 辞書内容
a : 零, 壱, 弐, 参
echo-mode > $a[0]
零
echo-mode > $a[2]
弐
echo-mode > $a[-1]
参

### 5.3. 履歴参照(中級)

「${数値}」 という特殊なエントリ呼び出しを履歴参照と呼びます。 履歴参照は、同じ文脈で行われた置換結果を再度参照するときに使います。

n : 石
food : 梨
sentence : ${n}のような${food}、${1}のような${0}。

上記の例ですと、sentenceの実行結果は「石のような梨、梨のような石。」になります。

この数値もやはり0から数え始めます。 また、負の数値を入れると後ろから数え始めます。

履歴参照は少し特別扱いなので、 集合演算に使う(${0 & entry}など)ことはできません。 ${ ${エントリ } }のような書き方で、 内部のエントリ呼び出し結果が数値でも、履歴参照にはなりません。

ここからはPhase 7.3.1以前との違いです。

まず、全ての置換記述が履歴参照によって参照可能になりました。 つまり、エントリ呼び出し、エントリ配列呼び出し、演算式、インラインスクリプトのことです。 分かりやすく言えば、「全部の'$'を参照可能」です。

では、次の場合はどうでしょうか。

n : 石
food : 梨
sentence : $(echo 「${n}」)のような$(echo 「${food}」)、${1}のような${0}。

${1}は、 $(echo 「${n}」)の中の${n} を参照してしまわないのでしょうか？ もしくは、実行順序としては${n} の方が先のように思えますから、 ${n}が0番目で、 $(echo 「${n}」)が1番目でしょうか。

答えはどちらでもありません。 履歴参照においては、'$'の中は関知しません。 よって、 $(echo 「${n}」)が0番目で、 $(echo 「${food}」)が1番目です。

かと言って、 '$(' 〜 ')'の中(あるいはエントリ集合演算式などの中)では、 履歴参照は使えないという意味ではありません。 もちろん使えます。そして従来通り、括弧の外の以前の置換履歴も参照可能です。

sentence : ${n}のような${food}、$(echo 「${1}」)のような$(echo 「${0}」)。

しかし、スクリプトの外から参照するときは、スクリプト全体しか見えません。

### 5.4. 一時エントリ(上級)

見かけは全く異なりますが、これは履歴参照とほぼ同じ機能です。

一時エントリとは、華和梨が普段持っている辞書とは別に、 あるエントリ中の一つの文を実行している間のみ存在する一時的な辞書 に登録されるエントリです。 エントリ名の先頭にアットマーク'@'が付くのが特徴です。

この辞書は、文の実行(置換作業ですね)が始まると同時に、その文専用に一つ作成され、 終わると同時に削除されます。

一時エントリは辞書定義時には存在しない(何も実行されていないのですから当然です)ため、 通常のエントリ定義で文を登録することができません。 従って、一時エントリは常にスクリプトによって作られることになります。

sentence : $(setstr @名前 ${名前})\0\s[0]名前エントリから${@名前}を選びました。\1\s[10]\w8何で${@名前}\0\s[0]\w2何でって言われても……

上記では、文の実行が始まった瞬間に一時辞書(中身無し)が設定され、 最初のスクリプトで@名前エントリに (通常辞書の)名前エントリを呼び出した結果の文字列が入ります。 それ以降、@名前エントリは、 この文の実行が終了するまで残ります (もちろん、それ以前にスクリプトによって消去することは可能です)。

履歴参照が、自動的に設定される過去の置換履歴を参照するという単機能であるのに比べ、 一時エントリは自由に設定・変更・呼び出しができ、 集合演算にも使えます。

質問 : ${@名前}って何？

他のエントリに対して履歴参照できないのと同様、 他のエントリの一時辞書を参照することはできません。

sentence : $(pushstr @名前 うなぎ)${質問}

まず「@名前」一時エントリに、 「うなぎ」という文字列をセットしてから、 さきほどの「質問」エントリを呼び出していますが、 これも無意味です。 呼び出し元に対して履歴参照できないのと同様、 呼び出し元の一時辞書を参照することはできません。

履歴参照と違う点は、 同じ文の中でありさえすれば、スクリプトや演算式の中だろうと外だろうと、 場所に関係なく同じ一時辞書にアクセスできるところです。 スクリプト中で操作を行った結果をスクリプト外で受け取ることも勿論できます。 実際、上に挙げた例でも既に行っています。

一時エントリがもっとも活用されるのは、 KISのユーザ定義関数においてでしょう。 以前は「関数的機能を持つエントリ」を呼び出す場合、 そのエントリに渡すべき値(引数)を特別に用意した(しかし通常辞書の一部である)エントリにセットしてから呼び出すのが通例でした。 しかし、この形式では問題があります：

- 再入(その関数の実行中に再び同じ関数が呼び出されること)不能
- 関数型エントリ実行終了時に引数エントリをいちいちクリアしない限り、以前の値が残っている可能性がある
- 呼び出す際以外に、偶然から引数エントリの値が書き変わる可能性がある

Phase 8のユーザ定義関数では、 華和梨システムによって、引数は自動的に呼び出された関数側の一時エントリ@argにセットされますので、 安心して引数を使うことができます。 複数のユーザ定義関数が互いを何度も呼び合っても、 その引数のエントリが上書きされたり、過去の引数が残っていたりする危険はありません。

なお、この機能は無理に使わなくても構いません。 全てのエントリ名が互いにぶつからないように自分で管理できていて、 なおかつfunctionによる関数定義を使わない場合は、一時エントリを使う必要はありません。 エントリ呼び出しを関数代わりに使う従来の手法を踏襲する場合などです。

## 6. 演算式

$[ 〜 ]で囲まれた領域を「演算式」と呼びます。 演算式では、整数演算、ビット単位演算、論理演算、整数比較、文字列比較が行えます。

$[ 演算式 ]
# 一年は何分？
$[ 365 * 24 * 60 ]
# 今年は2002年ですか？
$[ $(date %y)==2002 ]
# あなたの名前は「ほげ」ですか？
$[ ${name}=="ほげ" ]
# widthからxを引いて、10で割る
$[(${width}-${x})/10]

使用できる演算子は以下になります。 結合優先度が高いもの順です。 優先度の定義は集合演算の章のものと同じです。

記号 | 数値 | 文字列 | 動作 | 例
** | ○ |  | 累乗 | $[10**2] => 100
- | ○ |  | 単項マイナス | $[-10] => -10
+ | ○ |  | 単項プラス | $[+10] => 10
! | ○ | ○ | (単項)NOT | $[!1] => false, $[!"hoge"] => false, $[!""] => true
~ | ○ |  | (単項)補数 | $[~-10] => 9
* | ○ |  | 乗算 | $[10*"2"] => 20, $["string"*10] => 0
/ | ○ |  | 除算 | $[10/2] => 5, $[10/0] => "" (エラーログ:"devided by 0")
% | ○ |  | 剰余算 | $[10%3] => 1
+ | ○ |  | 加算 | $[-10+2] => -8, $[""+1] => 1
- | ○ |  | 減算 | $[10-3] => 7
& | ○ |  | ビットAND | $[1&2] => 0
| | ○ |  | ビットOR | $[1|2] => 3
^ | ○ |  | ビットXOR | $[1^2] => 3
> | ○ |  | より大きい | $[10>10] => false
>= | ○ |  | 以上 | $[10>=10] => true
< | ○ |  | 未満 | $[10<10] => false
<= | ○ |  | 以下 | $[10<=10] => true
== | ○ | ○ | 等しい | $["string"=="string"] => true, $[10==8] => false
!= | ○ | ○ | 等しくない | $["mac"!="mcdonalds"] => true
=~ |  | ○ | マッチ | $["substring"=~"string"] => true
!~ |  | ○ | 非マッチ | $["substring"!~"string"] => false
&& | ○ | ○ | 論理AND | $["str"&&10] => "str", $["false"&&10] => false, $[0&&10] => false
|| | ○ | ○ | 論理OR | $["str"||0] => "str", $["false"||10] => 10

幾つかの、四則演算以外の演算について、非常にいい加減な説明をします。 ここにある演算形式は全て既存の(プログラミング言語としては)一般的な概念ですので、 正しい説明はその手の教科書をご覧下さい。

### 6.1. 比較演算

比較演算('>', '>=', '<', '<=', '==', '!=', '=~', '!~')は、 「その記述が正しいか否か」を確認するものだと思えばよいでしょう。 結果として、必ず真偽値を返します。 例えば：

$[ 1 == 10 ]

これは明らかに間違っています。 間違っていることを専門用語で「偽」と言います。 反対に正しい状態であることは「真」と言います。 偽の場合は文字列"false"が返ります。 真の場合は文字列"true"が返ります。

$[ "ばよえ〜ん" == "だいあきゅーと" ]
# "false"
$[ (100 / 2) < 100 ]
# "true"

少し先走りますが、 この「正しいか間違っているか」を利用して、 スクリプトで条件分岐することができます。 華和梨の真偽判断の基準は、論理演算子、および、 if, while, untilなどの構文コマンド全てにおいて統一されています。

"", "0", "false"は「偽」、それ以外は全て「真」として扱う

では、ディスプレイの幅(screen.widthエントリに格納されているとします) が1200を越えていたら「広いディスプレイ」 エントリを呼ぶようにしてみます。

$(if $[ ${screen.width} > 1200 ] ${広いディスプレイ})

### 6.2. 論理演算

論理演算('!', '&&', '||')は、 真偽値を使った演算です。

'!'は、「ではない」とでも言えるもので、 右側の値の逆の値を返します。 右側の値が真であれば偽、偽であれば真を返します。

$[ ! "ほげ" ]
# "ほげ"は、上述の偽の条件に当てはまらないから真。
# 従って、その逆である「偽」を返します。
# 偽を返す場合は"false"ですから、"false"が返ります。
$[ ! 0 ]
# 「0」は偽ですから、真が返ります。
# "true"になります。

'&&'は、「且つ」つまり、 「〜〜〜 且つ 〜〜〜」です。 右側の値と左側の値が真の時のみ、真、 そうでなければ偽を返します。 必ず、すべての要素を評価します。 ただし真を返す場合は、"true"ではなく、 並列に並べられた'&&'の、一番左側の値をそのまま返します。

$[ 10 < 100 && "ほげほげ" == "ほげほげ" ]
# 左側、右側の双方が"true"なので、真、つまり"true"を返します。
$[ 10 <= ${数値} && ${数値} <= 100 ]
# ${数値}が、10以上100以下の場合、"true"、そうでなければ"false"を返します。
$[ ${他のゴースト} && $(暇?) ]
# ${他のゴースト}エントリに名前が一つ以上あり、"暇?"コマンドがtrueを返した場合に限り、${他のゴースト}エントリから選ばれたランダムな名前が一つ返ります。

'||'は、「または」つまり、 「〜〜〜 または 〜〜〜」です。 両側の値のどちらかが真の時、真、そうでなければ偽を返します。 最初に真の値が出現した時点で評価を終了します。 ただし真を返す場合は、"true"ではなく、 並列に並べられた'||'の左側から順にテストして、 最初に真となった値をそのまま返します。

$[ 100 < 10 || "ほげほげ" == "ほげほげ" ]
# 後者が正しいので、"true"
$[ ${台詞}=~"胸" || ${台詞}=~"尻" ]
# ${台詞}が、「胸」か「尻」を含んでいれば"true"
$[ ${counter1} || ${counter2} || ${counter3} ]
# counter1, counter2, counter3の内、最初に0以外が返ったところでその値を返す。

### 6.3. ビット演算

ビット演算('&', '|', '^')は、 数値を32bit値として扱う演算です。 通常はまず使わないでしょう。

$[ 100 & 10 ]
# 01100100 & 00001010 -> 00000000 = "0"
$[ 64 & 96 ]
# 01000000 & 01100000 -> 01000000 = "64"
$[ 100 | 10 ]
# 01100100 | 00001010 -> 01101110 = "110"
$[ 64 | 96 ]
# 01000000 | 01100000 -> 01100000 = "96"

### 6.4. 数値と文字列の扱い

演算式では数値として扱えるものは必ず数値として扱う という規則があります。 この結果、次のような事態が起きます。

$[ "001" == "1" ]
# "true"になる

このような現象を避けたい(必ず文字列として比較したい)場合は、 KISのcompareコマンドを用いてください。

$[$(compare "001" "1")==0]
# "false"になる

## 7. インラインスクリプト

エントリ呼び出しとは別に、 '$(' 〜 ')'でいくつかの文を囲った部分を、 「インラインスクリプト」と言います。 また、=kisのみの行と、 =endのみの行で囲った部分も、 同様に「インラインスクリプト」と言います。 例えば、日付情報を返すdateコマンドを使うには、次のように書きます。

$(date %H)

インラインスクリプトは、 エントリ呼び出しの「実行する」機能を、 より強化したものと考えて下さい。 エントリ呼び出しと同様、インラインスクリプトを呼ぶと、 インラインスクリプトは実行結果に置き換わります。

\0\s[0]今日は$(date %n)月$(date %e)日です。\e

エントリ呼び出しと違うのは、 上の例で言うと「date」等のコマンド名の後に、 空白を挟んで「%n」などの文がある点です。 コマンド名はエントリ呼び出しのエントリ名に相当し、 どの機能を呼ぶかを決めます。この機能を「コマンド」と呼びます。

一方、空白以降の文は、 コマンドを呼ぶ際に、補助的情報としてコマンドに渡されます。 この補助的情報を「引数」と言います。 引数はコマンドの許す限り、空白で区切って幾つでも並べることが出来ます。

$(echo 引数を 幾つも 並べることが出来る 例です)
$(matchall ${System.Request.Reference1} 胸 薄い)

### 7.1. 構文コマンドと関数コマンド

引数の中に、エントリ呼び出しやインラインスクリプトがあった場合を考えます。

$(set Today $(date %m%d))

コマンドが実行される時、エントリ呼び出しやインラインスクリプトは、 それぞれの実行結果に置き換わったものが引数となり、コマンドに渡ります。 上の例では、$(date %m%d)はその日の日付、 例えば「0522」に置き換わり、setコマンドは、

$(set Today 0522)

と書いたのと同じ状態で実行されます。 引数の中のインラインスクリプトの引数も、 さらにエントリ呼び出し、インラインスクリプトを含む場合も有り得ます。 この場合、考え方はエントリ呼び出しと同じです。 一番内側の括弧から順番に、置換子がなくなるまで置換を実行します。 そして、その結果が引数としてコマンドに渡ります。

エントリ名 : entry
内容1 : 内容2
内容2 : あ , い , う , え , お
# 模式的な引数の置換の様子
$(set ${エントリ名} $(get ${内容1}))
→$(set entry $(get 内容2))
→$(set entry あいうえお)
→entryエントリに「あいうえお」をセットする

しかし、幾つかのコマンドでは、この引数の置換タイミングが違います。 具体的には、if、while、foreachなど、 プログラムの流れを司るコマンドと、 function、returnコマンド等です。

これらのコマンドは「構文コマンド」又は単に「構文」と呼びます。 構文コマンドは、その引数を使うときにエントリ呼び出し等を置換し、 使わない引数は置換しないという性質があります。 具体的な例を挙げます。

$(if $[ ${a} == "Y" ] $(set answer Yes) else $(set answer No))

この例の場合、$[ ${a} == "Y" ]の結果に応じて、 $(set answer Yes)、 もしくは$(set answer No)のどちらか一方だけ、 実行(=置換)されます。

構文コマンド以外のコマンドは、 「関数コマンド」又は単に「コマンド」と呼びます。

### 7.2. if

構文コマンドのうち、ifはPhase 7.3.1と比べ、 特に文法が変わりました。 elseとelse ifの、 2つのキーワードを新たに導入しました。

従来のifは、 連続した条件分岐で入れ子のifを使う必要がありました。 これは括弧の対応を間違いやすいだけではなく、 間違いを発見しにくいものでした。 多くの場合、 入れ子のifを、 別のエントリに記述する等の対策が必要でした。 ただし、こうした入れ子をエントリに分解する方法は、 条件を追加・削除する際に厄介です。

# 従来のif
$(if <条件> <条件が真の時の文> <条件が偽の時の文>)
$(if <条件1> <条件1が真の時の文>
$(if <条件2> <条件1が偽、条件2が真の時の文>
$(if <条件3> <条件1、2が偽、条件3が真の時の文>
$(…
<すべての条件が偽の時の文> )…<ifの数に対応した小括弧の連続>…)
# あるいは、次のようにエントリに分解する
if1 : $(if <条件1> <条件1が真の時の文> ${if2})
if2 : $(if <条件2> <条件1が偽、条件2が真の時の文> ${if3})
if3 : $(if <条件3> <条件1、2が偽、条件3が真の時の文> ${if4})
…
ifn : $(if <条件n> <条件1…n-1が偽、条件nが真の時の文> <すべての条件が偽の時の文>)

新しいifでは、こうした問題が起きにくくなっています。 複数行記述と併せ、一つの処理は一つのエントリの中で完結します。 メンテナンスが容易になるでしょう。

# 新しいif
$(if <条件> <条件が真の時の文> else <条件が偽の時の文>)
$(if <条件1> <条件1が真の時の文>
else if <条件2> <条件1が偽、条件2が真の時の文>
else if <条件3> <条件1、2が偽、条件3が真の時の文>
…
else <すべての条件が偽の時の文>)

### 7.3. 組み込みコマンドとユーザ定義コマンド

エントリと違い、いくつかのコマンドは、ユーザが定義しなくても実行できます。 このようなコマンドを、「組み込みコマンド」と言います。 一方、functionコマンドを使いユーザが定義したコマンドを、 「ユーザ定義コマンド」と言います。 ユーザ定義コマンドは、一度定義すれば、華和梨が起動している間有効です。 なお、ユーザ定義コマンドは必ず関数コマンドになります。

コマンド定義中では、 @arg一時エントリを引数として使います。 第1引数は$@arg[1]、 第2引数は$@arg[2]、 以降第N引数は$@arg[N]で参照できます。 $@arg[0]は定義中のコマンド名となります。 以下にコマンド定義の例を示します。

# @argエントリに引数が入っているものとして記述する
$(function 改行挿入 $(clear @arg[0] ; foreach i @arg $(echo ${i}\n)))
# 使用例
# 「カステラ1番\n電話は2番\n3時のおやつは文明堂\n」が返る
$(改行挿入 カステラ1番 電話は2番 3時のおやつは文明堂)

次に、既存の組み込みコマンドと同じ名前で、 ユーザ定義コマンドを定義した場合を考えます。

$(function size $(length $(get $@arg[1])))

この例の場合、sizeコマンドは、 ユーザ定義コマンド版sizeに上書きされます。 必ず組み込みコマンドを呼びたい場合、 $(.size entry)のように、 コマンド名の先頭に「.」を付けて呼んで下さい。

また、ユーザ定義コマンドをもう一度定義すると、 後に定義した方が呼ばれます。

$(function default $(echo さくら) ; default)
# 「さくら」が返る
$(function default $(echo まゆら) ; default)
# 「まゆら」が返る
$(function default $(echo 涼璃) ; default)
# 「涼璃」が返る

上の例では、defalutコマンドを3回呼んでいます。 しかし、毎回直前で定義しなおしているため、3回とも違う結果になります。

## 8. SHIORI/SAORIインターフェース

ここまでに、ゴーストの動作記述方法については、ほぼ全て解説しました。 が、肝心の 「ダブルクリックイベントに対応するには？」 「おすすめURLを表示するには？」 などの説明を一切しませんでした。

栞としての機能については、この章でまとめて説明します。 また、華和梨はSAORIとしても機能しますので、それについても併せて説明します。

### 8.1. コールバック

華和梨が情報のやり取りのため、 特別扱いするエントリを「システムエントリ」と呼びます。 このうち、幾つかのエントリは呼び出す際の仕組みが、 他のエントリとまったく違います。 この節では、このシステムエントリの中でも特異な、 「コールバックエントリ」を説明します。

コールバックエントリとは、本体がイベント、NOTIFYを通知してきた際、 最初に評価するエントリです。 通常のエントリを評価する場合、エントリ中の文を一つランダムに選び、 その文の評価結果をエントリの評価結果とします。 一方、コールバックエントリが本体から呼ばれた場合、 コールバックエントリ中のすべての文を添え字順に評価し、 すべての評価結果を結合したものを本体に返します。 仮に、System.Callback.OnGETエントリが、 次のような内容だったとします。

System.Callback.OnGET : \0
System.Callback.OnGET : \s[0]
System.Callback.OnGET : 阿。
System.Callback.OnGET : \1
System.Callback.OnGET : \s[10]
System.Callback.OnGET : 吽。
System.Callback.OnGET : \e

もしこのコールバックエントリが本体から呼ばれたとすると、 本体に返すスクリプトは次のようになります。

\0\s[0]阿。\1\s[10]吽。\e

この結果は、 KISコマンドのgetを使い、 次のように書いた結果と等価です。 コールバックエントリは、 コールバックエントリをgetで評価した結果を本体に返すと考えて下さい。

get System.Callback.OnGET

このコールバックエントリの動作は、 主にミドルウェアの記述を簡素化する際に有効でしょう。 ミドルウェアはOnSecondChangeイベント等で、 多数の独立した機能を動作させることがあります。 従来、この独立動作する機能を追加したい場合、 必然的にミドルウェアを書き換える必要がありました。 しかし、今回からはSystem.Callback.*エントリに、 追加機能を呼ぶ文を追加するだけで大丈夫です。

次に、華和梨の使う全コールバックエントリを示します。

SHIORI/3.0
System.Callback.OnGET | GET
System.Callback.OnNOTIFY | NOTIFY
SHIORI/2.x
System.Callback.OnEvent | SHIORI/2.2 イベント応答、GET Sentenceのみ
System.Callback.OnGetSentence | SHIORI/2.3b コミュニケート
System.Callback.OnGetStatus | ステータス取得
System.Callback.OnResource | SHIORI/2.5リソース取得
SAORI/1.0
System.Callback.OnSaoriExecute | SAORIモジュールとして呼ばれた
共通
System.Callback.OnUnload | 切り離しイベント(華和梨が発行)
System.Callback.OnRequest | その他全てのリクエスト(NOTIFY SHIORI/2.x、TRANSLATE SHIORI/2.x等)

#### System.Callback.OnRequest

このうちSystem.Callback.OnRequestは、 少し変わっているので解説します。 このコールバックエントリは、他のコールバックエントリに該当しなかった、 全ての本体からのコールで呼ばれます。 具体的にはNOTIFY SHIORI/2.x、TRANSLATE SHIORI/2.6、 TEACH SHIORI/2.4等が該当します。 あまり使用しない呼び出しや、将来の本体仕様変更に備えたエントリです。

どのような呼び出しが来たかを知るためには、 System.Requestエントリを参照します。 このエントリに、本体のコール種類を示す文字列である、 「TEACH」や「NOTIFY OtherGhostName」等が格納されます。 また、詳しくは次の節で解説しますが、 本体が渡したヘッダは、 System.Request.*エントリ群に格納されます。 また、本体への応答ヘッダは、 System.Response.*エントリ群に書き込みます。 処理状態を示すステータスコードは、 System.Responseエントリに書き込みます。 どのようなヘッダが来るか、どのようなヘッダを返すか、 どのようなステータスコードを返すかについては、 本体仕様書を参照して下さい。

一例として、TEACH SHIORI/2.4を簡易的に処理するスクリプトを示します。 TEACH SHIORI/2.4は、Wordヘッダに教えた単語が来ます。 応答の際は、Sentenceヘッダに書きます。 処理が成功したら、ステータスコードとして200を発行します。 これをスクリプトにすると、次のようになります。

System.Callback.OnRequest : $(
if $[ ${System.Request} == "TEACH" ] $(
setstr @Word ${System.Request.Word};
pushstr TeachWord ${@Word};
setstr System.Response.Sentence \0\s[0]${@Word}ね。\e;
setstr System.Response 200;
);
)

### 8.2. その他のシステムエントリ

コールバックエントリ以外にも、幾つかシステムエントリが存在します。 次に一覧を示します。

本体からの通知情報
System.Request.* | リクエストヘッダ
本体への応答
System.Response.* | レスポンスヘッダ
System.Response.To | SHIORI/2.3b 話しかけたいゴースト名。"stop"で打ち切り。
System.Response | SHIORI/2.0 ステータスコード
その他(リードオンリー)
System.DataPath | shiori.dllの存在するディレクトリ
System.SecurityLevel | セキュリティレベル

特に重要なのは、 System.Request.*のリクエストヘッダエントリ群です。 このエントリ群は、SHIORI/2.x、SHIORI/3.0、SAORI/1.0において、 本体が送ってきたヘッダに対応します。 具体例で説明すると、 例えば「Reference0: まゆら」というヘッダが来た場合、 System.Request.Reference0エントリに、 「まゆら」という単語をセットすることになります。

これとは反対に、 System.Response.*エントリ群は、 本体に返すレスポンスヘッダに対応します。 具体的には、 例えばSystem.Response.Reference0エントリに、 「さくら」という単語をセットしたと考えます。 すると本体に返すヘッダに、 「Reference0: さくら」というヘッダが追加されます。 また、System.Responseエントリにセットされた単語は、 本体に返すステータスコードになります。

リクエストヘッダエントリ、レスポンスヘッダエントリ群は、 本体から呼ばれてコールバックエントリを評価する直前に、 一度完全に内容を消します。

### 8.3. 栞としての華和梨

栞サブシステムの仕事は一見複雑ですが、要約すれば、 「本体の通知してきたIDから、相応しい応答を割り出して本体に返す」ことです。 華和梨Phase 8はPhase 7までと比べると、 こうした栞の仕事を、使用者により「生のまま」見せています。

代表的な例はイベント応答です。 華和梨Phase 8はイベント応答、リソース文字列の要求、 NOTIFY処理の呼び分けを、KISを使って書く必要があります。 本体が通知してきたID(=イベント名、リソース名)は、 System.Request.IDエントリに入っています。 これを使って呼び分けます。

華和梨Phase 7.3.1と同じ名前でイベントエントリ、 リソース文字列エントリを使いたい場合、 System.Callback.OnGETエントリに次のように書きます。

# GET SHIORI/3.0への応答
# イベント、リソース文字列、コミュニケート、etc…
# イベントは「event.<イベント名>」、
# リソースは「Resourece.<リソース名>」というエントリを呼ぶ
System.Callback.OnGET: $(
if $[ $(match_at ${System.Request.ID} On) ]
${event.${System.Request.ID}}
else $(
${resource.${System.Request.ID}}
)

この例では、例えばマウスのダブルクリックイベントが来た時、 event.OnMouseDoubleClickエントリを呼びます。 また、例えばさくら側の「おすすめURL」の要求があった場合、 ${resource.sakura.recommendsites}の評価結果を返します。

しかし、SHIORI/3.0ではイベントとリソース文字列要求は、 本体からの通知形式に差がありません。 そこで、エントリ名を従来から変更する代わりに、 記述を簡素化することが出来ます。 この場合、次のように書きます。

# GET SHIORI/3.0への応答
# イベント、リソース文字列、コミュニケート、etc…
# 「reply.<要求されている処理名>」というエントリを呼ぶ
System.Callback.OnGET : ${reply.${System.Request.ID}}

この例では、例えばマウスのダブルクリックイベントが来た時、 reply.OnMouseDoubleClickエントリを呼びます。 また、例えばさくら側の「おすすめURL」の要求があった場合、 ${reply.sakura.recommendsites}の評価結果を返します。

最後に、NOTIFYの処理の呼び分け触れます。 NOTIFYの形式はイベント・リソース文字列要求のGETの場合と、 ほとんど差がありません。

# NOTIFY SHIORI/3.0の処理
# HWnd通知、他のゴースト通知、インストール済みゴースト通知、etc…
# 「notify.<通知された情報名>」というエントリを呼ぶ
System.Callback.OnNOTIFY : ${notify.${System.Request.ID}}

例えば他に起動中のゴーストの名前がNOTIFYされた場合、 notify.otherghostnameエントリを呼びます。

ミドルウェアを使わずに華和梨を使う場合、こうした記述が必ず必要です。 しかし、多くのミドルウェアでは、 こうした低レベル(よりプログラムに密着した)の記述が、 既にパッケージ化されています。 何らかの事情が無い限り、こうしたミドルウェアの使用をおすすめします。

### 8.4. SAORIモジュールとしての華和梨

華和梨はSHIORI規格の準AIモジュールですが、 同時にSAORI規格モジュールでもあります。 SAORIモジュールとして使うと、

- 華和梨Phase 7から移行する際、新旧スクリプトを同時に使える
- 他の栞を使用している場合でも、華和梨の欲しい機能だけを使える

といったメリットがあります。

華和梨をSAORIとして使うとき、次の3つのことに注意します。

#### 引数の受け取り

華和梨がSAORIとして呼ばれた時、 System.Callback.OnSaoriExecuteエントリを評価します。 他のコールバックエントリと同様、所属する全ての単語を評価します。 この時、SAORIモジュールに与えられた引数は、 System.Request.*以下、 System.Request.Argument0、 System.Request.Argument1 等のエントリに存在します。

引数が何個あるか等を知りたい場合、listtreeコマンドを使い、

listtree arguments System.Request

として、argumentsエントリを調べると知ることが出来ます。

#### 戻り値の引渡し

戻り値を返したい時はコミュニケートと同様、 System.Response.*エントリ群を使います。 SAORI規格に従って、戻り値は、 System.Response.Resultに書き込みます。 以下、 System.Response.Value0、 System.Response.Value1、 System.Response.Value2 等のエントリに補助情報を書き込みます。

#### ステータス

戻り値を返すには、実はこれだけでは不十分です。 System.Resoposeエントリに、 ステータスを書き込む必要があります。 このステータスコードを見て、 SAORIモジュールを呼び出した側は処理の成功/失敗を判断する為、 必須情報です。 主なステータスコードは次の通りです。

200 | 引数を正しく理解し、戻り値を書き込んだ
204 | 引数は正しく理解したが、戻り値がない
400(省略時デフォルト) | 理解できない引数が来た

なお、 System.Callback.OnSaoriExecuteの評価結果は、 ステータスと無関係です。 この点が他のコールバックエントリと違いますので、注意が必要です

System.Callback.OnSaoriExecute : $(
if $[ ${System.Request.Argument0} == "処理1" ] $(
SAORI処理1;
) else if $[ ${System.Request.Argument0} == "処理2" ] $(
SAORI処理2;
);
# SAORI処理をわざわざ関数にする必然性はないのだが…
# 新機軸に見慣れて貰うために、あえて関数で記述した。
# 普通にエントリとして書いても何の問題もない。
# 引数はSystem.Request.*に存在し、関数の引数として与える必要がないから。
=kis
function SAORI処理1 $(
if $[ ${System.Request.Argument1} == "貢物" ] $(
# Resultのみ返す例
setstr System.Response.Result "うむ、よろしい！";
setstr System.Response 200;
) else $(
# ResultとValue*を併用する例
setstr System.Response.Result "貢物はどうした！";
setstr System.Response.Value0 "文句";
setstr System.Response.Value1 "不満";
setstr System.Response.Value2 $(date %y%m%d);
setstr System.Response 200;
);
);
function SAORI処理2 $(
if $[ ${System.Request.Argument1} == "1" ] $(
# Resultなしだが正常終了の例
setstr System.Response 204;
) else $(
# 異常終了の例
setstr System.Response 400;
);
);
=end

### 8.5. SAORIモジュールの使用宣言

SAORIモジュールを使用する際は、 「これからこのSAORIを使う」という使用宣言の記述が必要です。 この記述を受けて、 華和梨はSAORIモジュールを読み込んだり、読み込む準備をします。

華和梨Phase 8以前では、 このSAORI使用宣言はkawari.iniに記述しました。 しかし、Phase 8からkawari.iniは廃止になり、 使用宣言はKISで記述することになりました。

SAORIモジュール使用宣言は、 saoriregistコマンドを使います。

saoriregist ＜SAORIモジュール名＞ ＜エイリアス＞ [＜オプション＞]

SAORIモジュール名(必須)
  SAORIモジュールのファイル名を記述します。拡張子(「.dll」等)を含めます。 華和梨の存在するフォルダからの相対パスで記述します。
エイリアス(必須)
  実際にSAORIモジュールを使用する際、 使用するモジュールを区別するためのラベルです。 複数のSAORIモジュールを使う場合、モジュールごとに別の名前を付けてください。 また、モジュールを一つだけ使う場合も、必ずエイリアスが必要です。
オプション(省略可能)
  SAORIモジュールを読み込むタイミングを指定します。 「preload」で直ちにに読み込み、 「loadoncall」でモジュールを使用する直前に読み込み、 「noresident」でモジュールを使用する直前に読み込み、 使用後に切り離しです。
  省略した場合、「loadoncall」と等価です。 使用するSAORIモジュールのマニュアルに注意書きがない場合、 通常はオプションを省略して構いません。

こうして使用宣言をすると、華和梨からSAORIモジュールを呼び出すことが出来ます。

### 8.6. SAORIモジュール呼び出し

SAORIモジュールを呼び出す時は、 callsaoriコマンド、 callsaorixコマンドを使います。 callsaoriコマンド、 callsaorixコマンドは、 先に定義した「エイリアス」でSAORIモジュールを呼びます。 仮に、music.dllというSAORIモジュールを、 「音楽」というエイリアスで使用宣言したとします。

saoriregist music.dll 音楽

このSAORIモジュールの機能が指定した音楽ファイルの再生だとしたら、 再生する音楽ファイル名を指定するのが普通でしょう。 この場合、SAORIモジュールに再生するファイル名を伝える必要があります。 話の都合上、music.dllは

- 第1引数が「play」で再生開始、「stop」で再生停止
- 第2引数が再生開始時は再生するファイル名、停止時は無し

という仕様だとします。

このmusic.dllで、 「technopolis.mid」という音楽ファイルを再生したい場合、

callsaori 音楽 play technopolis.mid

と書きます。 callsaoriコマンドのエイリアスより後ろの引数は、 SAORIモジュールに引数として渡します。

callsaorixコマンドは、 SAORIモジュールが戻り値以外に、 様々な情報を送ってくるタイプの場合に使用します。 仮に、先ほどのmusic.dllが「play」を指示した場合、 次のような情報を送ってくるとします。

- Result: 再生に成功したら「OK」
- Value0: 音楽ファイルのタイトル
- Value1: 音楽ファイルのアーティスト
- Value2: 音楽ファイルの演奏時間(mm:ss形式)

では、callsaorixを使って、 音楽ファイル「Truth21c.mid」を再生します。 エイリアスはcallsaoriコマンド、 callsaorixコマンドに共通で使えます。

callsaorix 音楽 music.info play Truth21c.mid

ここで、エイリアスのすぐ後ろの「music.info」は、 SAORIモジュールが送ってきた情報を記録する際の、 基準となるエントリの名前です。このエントリの名前は自由に決められます。 上の例の場合、次のようなエントリに情報を書き込みます。

- music.info.Value0: "TRUTH(21C Remix)"
- music.info.Value1: "T-SQUARE plus"
- music.info.Value2: "05:30"
- music.info.size: "3"
- music.info: "SAORI/1.0 200 OK"

sizeはValueが何個あるかを示します。

callsaori、 callsaorixはともに戻り値を返す関数コマンドです。 戻り値はSAORIモジュールの返したResultヘッダです。

## 9. 応用編

上記を踏まえ、ゴーストの基本動作を実装する際、 華和梨Phase 8ではどのように記述するのか、 幾つか例を交えて紹介します。

### 9.1. イベント

イベント通知の際、本体からのReferenceヘッダを華和梨から参照するには、 ${System.Request.Reference0}など、 非常に長いエントリ名を記述する必要があります。 使ってみると、これは不便です。 そこで、コマンドを作って記述を簡単にする場合を考えます。

コマンドを定義する際の注意ですが、 スクリプト記述ゾーンで定義して下さい。 辞書記述ゾーンのエントリ定義中でコマンドを定義した場合、 コマンドの定義は「そのエントリが呼び出されて」初めて機能します。 多く場合、コマンドは未定義も同然となります。

では、実際に定義してみます。

=kis
# Referenceを返すコマンド "Reference"
function Reference $(
# 引数がなかった場合、何も返さない
if $[ $(size @arg) <= 1 ] $(return);
# System.Request.Reference?エントリの一つ目の単語を得る
get System.Request.Reference$@arg[1][0];
);
=end

この例では、同じReference番号のReferenceヘッダが複数来る場合を想定し、 getで0番目のRefernce?を参照しています。

### 9.2. コミュニケート

他のゴーストに話し掛ける時は、 Reference0に話し掛けたいゴースト名をセットして、 トークを返します。 ReferenceN(Nは0以上の整数)を返すには、 System.Response.ReferenceNエントリに単語をセットします。 なお、System.Responseで始まるエントリは、 コールバックごとに毎回内容が消去されます。

sentence : (
$(setstr System.Response.Reference0 毒子)
\0\s[0]おーい、毒子さん、聞こえるかーい。
\1\s[11]やめとけ、アレは偽毒子だ。変な注射されるぞ。\e
)

こうして話し掛けられたゴーストには、 OnCommunicateがやってきます。 SHIORI/3.0から、コミュニケートはイベントの一種になりました。

# 「会話」エントリの全内容を評価し、戻り値の一つをエントリ名として
# エントリ呼び出し
# 戻り値がなければ、communicate.unknownエントリを呼ぶ
reply.OnCommunicate : $(communicate 会話 ${communicate.unknown})

自分から話し掛けるときと同様、 話し掛けるゴースト名をReference0に設定して下さい。

なお、コミュニケート用コマンドは、 ユーザ定義コマンドで使いやすいようラッピングするのが得策ですが、 ここでは一番素直に書いた場合を例示します。

コミュニケートの基本的書き方は、次のようになります。

1. 反応するキーワードを書いたエントリを作る。 キーワード群1個につきエントリ一個とする。仮にkeyとする。
2. キーワード群に対応する、返事を書いたエントリを作る。 これもキーワード群1個につきエントリ1個とする。仮にanswerとする。
3. communicateコマンドで束ねるエントリを、 仮にcomとする。
4. キーワード、返事をcomエントリに登録する時は、
   com : $(
   if $(xargs key matchall ${System.Request.Reference1})
   answer
   )
   と書く。
5. OnCommunicateイベントで、
   $(communicate com)
   と書く。

以上をまとめた例を挙げます。

# 例えば「あなたの名前は何？」に反応する
com.key.1 : あなた , 名前 , 何？
com.answer.1 : (
\0\s[0]私の名前は${myname}です。\s[5]あなたは？\e
$(setstr System.Response.Reference0 $(Reference 0))
)
com.answer.1 : \0\s[4]うー。\1\s[10]すまん、こいつ今機嫌が悪くてな\e
com.answer.1 : \0\s[8]…。\1\s[10]無視かよ。\e
com : $(
if $(xargs com.key.1 matchall $(Reference 1))
com.answer.1
)
# 「胸が無い」と言われるとキれる
com.key.2 : 胸 , 無い
com.answer.2 : \0\s[25]…。\1\s[11]俺が時間を稼ぐ、早く逃げろ！\e
com : $(
if $(xargs com.key.2 matchall $(Reference 1))
com.answer.2
)
# 複数のゴーストの呼びかけに応じる例
# 「うにゅぼんで一緒に遊ぼう」等に反応する
com.key.3 : うにゅぼん , 一緒 , 遊ぼう
com.answer.3 : (
\0\s[5]うん！じゃ、私は「${installedghost}」使うよ。\e
$(setstr System.Response.Reference0 $(Reference 0))
)
# 反応するゴースト名一覧
com.ghost.3 : 黒姉 , まゆら , さくら , せりこ
com.ghost.3 : 毒子 , あると , 奈留 , 安子
# findを使い、反応するべきゴーストかどうか判定
com : $(
if $[ $(find com.ghost.3 $(Reference 0)) >= 0 &&
$(xargs com.key.3 matchall $(Reference 1)) ]
com.answer.3
)
# あるゴーストに自分の名前を呼ばれたときに反応
com.key.4 : ${myname}
com.answer.4 : \0\s[5]……$(Reference 0)様！\e$(setstr FlagMode happy)
com.ghost.4 : まゆら
com : $(
if $[ $(Reference 0) == ${com.ghost.4} &&
$(xargs com.key.4 matchall $(Reference 1)) ]
com.answer.4
)
# キーワード外の反応文
com.unknown : \0\s[4]ごめん、よく分かんない。\e
com.unknown : \0\s[8]なプー。\1\s[10]…。\e
com.unknown : (
\0\s[0]…それから？\e
$(setstr System.Response.Reference0 $(Reference 0))
)
# コミュニケートイベントでcomエントリを登録
reply.OnCommunicate : $(communicate com ${com.unknown})

### 9.3. 自発トーク

自発的にトークをするには、 OnSecondChangeイベントを使うのが一般的です。 ほぼ毎秒来るこのイベントが発生するたびに内部でカウンタを1ずつ増やし、 カウンタが指定の値になったら、 OnSecondChangeでトークを返す、という方法です。

このとき注意が必要なのは、 OnSecondChangeが最小化しているときも発生する、 という点です。 OnSecondChangeでトークが捨てられるか否かは、 Reference3の1/0で判定できます。

以上を踏まえて、簡単な自発トークをするスクリプトを組んでみます。 伝統的理由から、 トークはsentenceエントリにあるものとします。

System.Callback.OnGET : ${reply.${System.Request.ID}}
# トーク周期(秒)
# このエントリに指定した時間間隔で発話する
interval : 90
# トークカウンタ
# このカウンタがトーク周期と等しくなったら発話する
talkcounter : 0
# OnSecondChangeイベント
# Reference3が1なら、トークを返しても大丈夫
reply.OnSecondChange : $(
if ${System.Request.Reference3} $(
inc talkcounter;
# カウンタがトーク周期以上になっていたら発話し、
# カウンタをリセットする
if $[ ${talkcounter} >= ${interval} ] $(
entry sentence;
setstr talkcounter 0;
);
);
)