# 華和梨 ヒント集

2004/02/01
Phase 8.2.0

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

## Index

← back

> 辞書
> 擬似kawari.ini
> ストーリートーク
> エントリの重複回避呼び出し
> エントリの順序呼び出し
> コミュニケート
> コミュニケートコマンドの改善
> その他
> 簡易文字種判別
> SHIORI/3.0 NOTIFY処理
> デバッグ支援スクリプト

## 辞書

### 疑似kawari.ini

ゴースト定義辞書ファイルを読むのに、 いちいちloadコマンドを並べるのは癪だという方は、 kawarirc.kisに以下の記述を行うことで、 従来のkawari.iniと同じようにdictに辞書ファイル名を書けます。

# kawarirc.kis内サンプル
=kis
load kawari.ini;
foreach @f dict $( load ${@f} );
=end
# kawari.iniサンプル
dict : dict-word.txt
dict : dict-game.txt

### ストーリートーク

エントリ配列呼び出しの導入で、 自発トークでストーリーのような、 連続したトークを分かりやすく書けるようになりました。 また、連続したトークをランダムトークとして呼ぶことも容易です。

# story1エントリに、ストーリーを書く
story1 : (
\0\s[0]昔々ある所に、おじいさんとおばあさんが住んでいました。
\1\s[10]…。\e
)
story1 : (
\0\s[0]おばあさんが川で洗濯していたところ、
大きな桃がどんぶらこ、どんぶらこと流れてきました。
\1\s[10]ま、お約束だな。\e
)
story1 : (
\0\s[0]喜んだおばあさんは桃を持ち帰り、
おじいさんと桃を二つに割りました。
\1\s[10]桃太郎は無事なんだろうか。\e
)
story1 : (
\0\s[0]すると桃の中から、\s[4]丸々と太った立派なうにゅうが…。
\1\s[10]ちょっと待て。\e
)
story1 : (
\0\s[0]…おじいさんとおばあさんは、急いで桃を封印しました。
\1\s[10]おい！
\0\s[0]そして${ヤバイ組織}に高値で売り飛ばし、
\s[5]余生を面白おかしく過ごしましたとさ。おしまい。
\1\s[11]それが落ちか！\e
)
story1 : (
\0\s[5]いいお話だったねえ、うにゅう。心が和むよ。
\1\s[11]どこが！お前とは一度、拳で語り合う必要があるな。
\0\s[0]つまり、私の${必殺技}がフルコースで欲しいと？\s[6]
\1…\w9…\w9…\w9\s[10]今夜は${ご馳走}でございます、お嬢様。
\0\s[5]分かればよろしい。\e
)
# どこまで読んだか記録するカウンタ
story1.pos : 0
# カウンタを一つ増やす。全エントリを読んだらカウンタを0に戻す
story1.inc : $(setstr story1.pos $[ (${story1.pos} + 1) % $(size story1) ])
# トークエントリにストーリーを登録する。1回呼ばれたらカウンタを1増やす
sentence : $story1[${story1.pos}]${story1.inc}
# ランダムトークとしてストーリーを登録する
sentence : ${story1}

### エントリの重複回避呼び出し

通常のエントリ呼び出し(${hoge})は、 単語をランダムに選んで返します。 しかし、同じ文章で同じエントリ呼び出しを複数使った場合、 それぞれが異なることを保証しません。 これが気になる場合、まず次のようなスクリプトを用意します。

# エントリからランダムに単語を重複せずに呼び出す
# 第1引数 : エントリ名
# 戻り値 : エントリ内の単語
# 備考 : 「エントリ名.buffer」というエントリを占有する
=kis
function GetWordRandom $(
if $[ $(size @arg) != 2] $(return);
if $[ $(size $@arg[1].buffer) == 0 ] $(copy $@arg[1] $@arg[1].buffer);
setstr @pos $(rand $(size $@arg[1].buffer));
get $@arg[1].buffer[${@pos}];
clear $@arg[1].buffer[${@pos}];
);
=end
# 指定エントリを単語重複せず呼び出すエントリ化する
# 第1引数 : エントリ名
# 戻り値 : なし
# 備考 : 「エントリ名.backup」、「エントリ名.backup.buffer」という
# エントリを占有する
=kis
function Nonoverlap $(
if $[ $(size $@arg[1].backup) == 0 ] $(copy $@arg[1] $@arg[1].backup);
set $@arg[1] "$(GetWordRandom "$@arg[1]".backup)";
writeprotect $@arg[1];
writeprotect $@arg[1].backup;
);
=end

そして、次のように書きます。

# 「人名」エントリを重複回避して呼ぶよう指定する
=kis
Nonoverlap 人名;
=end

すると、単に${人名}と呼ぶだけで、 エントリ内の単語をすべて使うまで、同じ単語を呼ばなくなります。 単語を呼ぶ順序は、通常通りランダムです。

この記述をすると、指定したエントリに単語の追加が出来なくなります。 指定する際は、 辞書の読み込みがすべて完了した時点で、 Nonoverlapを実行すると良いでしょう。

### エントリの順序呼び出し

エントリ呼び出しの際、 呼び出すたびにエントリ内の単語の順序に従って呼び出すことが出来ます。 主にストーリー等を記述する際に便利でしょう。 次のようなスクリプトを用意してください。

# エントリから単語を添え字順に呼び出す
# 第1引数 : エントリ名
# 戻り値 : エントリ内の単語
# 備考 : 「エントリ名.pos」というエントリを占有する
=kis
function GetWordSequential $(
if $[ $(size @arg) != 2] $(return);
if $[ $(size $@arg[1].pos) == 0 ] $(setstr $@arg[1].pos 0);
get $@arg[1][${$@arg[1].pos}];
setstr $@arg[1].pos $[ (${$@arg[1].pos}+1) % $(size $@arg[1]) ];
);
=end
# 指定エントリを添え字順に呼び出すエントリ化する
# 第1引数 : エントリ名
# 戻り値 : なし
# 備考 : 「エントリ名.backup」、「エントリ名.backup.pos」という
# エントリを占有する
=kis
function Sequential $(
if $[ $(size $@arg[1].backup) == 0 ] $(copy $@arg[1] $@arg[1].backup);
set $@arg[1] "$(GetWordSequential "$@arg[1]".backup)";
writeprotect $@arg[1];
writeprotect $@arg[1].backup;
);
=end

そして、次のように書きます。

# 「カード」エントリを添え字順に呼ぶよう指定する
=kis
Sequential カード;
=end

すると、単に${カード}と呼ぶだけで、 エントリ内の単語を添え字順に呼びます。 一番最後の単語を呼び出すと、先頭に戻ります。

この記述をすると、指定したエントリに単語の追加が出来なくなります。 指定する際は、 辞書の読み込みがすべて完了した時点で、 Sequentialを実行すると良いでしょう。

## コミュニケート

### コミュニケートコマンドの改善

華和梨Phase 8のコミュニケートコマンド群は、 原始的コマンドばかりで、Phase 7.3.1より使いにくい部分があります。 キーワードを動的に拡張できる反面、エントリ名を大量に消費します。 そこで、あえてキーワードを従来同様に固定にすることで、 記述を容易にする方法を考えました。

=kis
# 第1引数: 登録する対象のエントリ
# 第2引数: 登録するデータを記録したエントリ
function regist_communicate $(
# 引数が2以外の場合終了
if $[ $(size @arg) != 3 ] $(return);
# 登録すべきデータがなければ終了
if $[ $(size $@arg[2]) == 0 ] $(return);
# 登録対象にデータ登録数を設定
if $[ $(size $@arg[1]) == 0 ] $(setstr $@arg[1].size 0);
setstr @pos 0;
setstr @segment 0;
# メインルーチン
while $[ ${@segment} < $(size $@arg[2]) ] $(
# キーワード、応答分部分の特定
setstr @segment $(find $@arg[2] "//" ${@pos});
if $[ ${@segment} == -1 ] $(return);
setstr @keypos $(find $@arg[2] "-" ${@pos});
if $[ ${@keypos} == -1 ] $(return);
if $[ ${@keypos} > ${@segment} ] $(return);
if $[ ${@keypos}+1 >= ${@segment} ] $(return);
# communicate用エントリに登録
# キーワード
setstr @i ${@pos};
while $[ ${@i} < ${@keypos} ] $(
push $@arg[1].${$@arg[1].size}.keyword $(getcode $@arg[2][${@i}]);
inc @i;
);
# 応答文
setstr @i $[ ${@keypos}+1 ];
while $[ ${@i} < ${@segment} ] $(
push $@arg[1].${$@arg[1].size}.answer $(getcode $@arg[2][${@i}]);
inc @i;
);
# communicateエントリに登録
push $@arg[1] (
"$(if $(xargs "
$@arg[1]
"."
${$@arg[1].size}
".keyword matchall ${System.Request.Reference1}) "
$@arg[1]
"."
${$@arg[1].size}".answer)"
);
# インクリメント
inc $@arg[1].size;
setstr @pos $[ ${@segment}+1 ];
);
);
# 第1引数: 登録する対象のエントリ
# 第2引数: 登録するデータを記録したエントリ
function regist_communicate_to $(
# 引数が2以外の場合終了
if $[ $(size @arg) != 3 ] $(return);
# 登録すべきデータがなければ終了
if $[ $(size $@arg[2]) == 0 ] $(return);
# 登録対象にデータ登録数を設定
if $[ $(size $@arg[1]) == 0 ] $(setstr $@arg[1].size 0);
setstr @pos 0;
setstr @segment 0;
# メインルーチン
while $[ ${@segment} < $(size $@arg[2]) ] $(
# ゴースト名部分の特定
setstr @segment $(find $@arg[2] "//" ${@pos});
if $[ ${@segment} == -1 ] $(return);
setstr @ghostpos $(find $@arg[2] "-" ${@pos});
if $[ ${@ghostpos} == -1 ] $(return);
if $[ ${@ghostpos} > ${@segment} ] $(return);
if $[ ${@ghostpos}+1 >= ${@segment} ] $(return);
# communicate用エントリに登録
# ゴースト名
setstr @i ${@pos};
while $[ ${@i} < ${@ghostpos} ] $(
push $@arg[1].${$@arg[1].size}.ghost $(getcode $@arg[2][${@i}]);
inc @i;
);
# キーワード、応答分部分の特定
setstr @pos $[ ${@ghostpos}+1 ];
setstr @keypos $(find $@arg[2] "-" ${@pos});
if $[ ${@keypos} == -1 ] $(return);
if $[ ${@keypos} > ${@segment} ] $(return);
if $[ ${@keypos}+1 >= ${@segment} ] $(return);
# communicate用エントリに登録
# キーワード
setstr @i ${@pos};
while $[ ${@i} < ${@keypos} ] $(
push $@arg[1].${$@arg[1].size}.keyword $(getcode $@arg[2][${@i}]);
inc @i;
);
# 応答文
setstr @i $[ ${@keypos}+1 ];
while $[ ${@i} < ${@segment} ] $(
push $@arg[1].${$@arg[1].size}.answer $(getcode $@arg[2][${@i}]);
inc @i;
);
# communicateエントリに登録
push $@arg[1] (
"$(if $[ $(find "
$@arg[1]
"."
${$@arg[1].size}
".ghost ${System.Request.Reference0}) >= 0 && $(xargs "
$@arg[1]
"."
${$@arg[1].size}
".keyword matchall ${System.Request.Reference1}) ] "
$@arg[1]
"."
${$@arg[1].size}".answer)"
);
# インクリメント
inc $@arg[1].size;
setstr @pos $[ ${@segment}+1 ];
);
);
=end

以上で定義したregist_communicateは、 次のように使います。 まず、あるエントリ(仮にdata1とする)に、 次のような単語を登録します。

data1 (
今日, 天気, 良い,
-,
\0\s[5]そうだね。どこかにお出かけしたいね。\e,
\0\s[0]こんな日は、外に遊びに行きたいな。\e,
\0\s[4]こんな日に限って、仕事が一杯なの…\e,
//
)

その後、実際にcommunicateコマンドに登録するエントリを、 仮にcomとすると、

=kis
regist_communicate com data1;
=end

このように記述します。すると、 com以下の次のようなエントリ群に、 data1エントリの内容が展開されます。

com : $(if $(xargs com.0.keyword matchall ${System.Request.Reference1}) com.0.answer)
com.0.keyword : 今日 , 天気 , 良い
com.0.answer : \0\s[5]そうだね。どこかにお出かけしたいね。\e
com.0.answer : \0\s[0]こんな日は、外に遊びに行きたいな。\e
com.0.answer : \0\s[4]こんな日に限って、仕事が一杯なの…\e
com.size : 1

なお、上記の展開結果を確認したい場合、 regist_communicateを実行後、 次のようなスクリプトを実行してください。 saved.txtに展開結果が記録されています。

=kis
clear temp;
listtree temp com;
xargs temp save saved.txt;
=end

regist_communicateで読み込むエントリの形式ですが、 最初の「-」までの単語は、「キーワード」になります。 Reference1の文章に上記キーワードがすべて存在すると、 「-」以降の文章のうち、一つが応答文として選ばれます。 一つの応答の組み合わせの最後には、「//」を置いて区切りとして下さい。 「//」以降は、また別の応答を書くことが出来ます。

data2 (
キーワード1,
-,
応答文1,
//,
キーワード2,
-,
応答文2,
//
)

これらの応答は、 comエントリを communicateコマンドを通じて、 OnCommunicateイベントに登録登録することで機能します。

reply.OnCommunicate : $(communicate com)

一方、 regist_communicate_toコマンドは、 さらに反応するゴースト名も指定できるよう、機能拡張したものです。 最初の「-」までの単語が反応するゴースト名で、 ここに列挙したゴーストのうち、 いずれかが呼びかけた場合に反応します。 最初の「-」から次の「-」までがキーワード、 2番目の「-」以降は返す文章です。 regist_communicateコマンドと同様、 キーワード・応答文の組み合わせの最後に、「//」を置いて下さい。

data3 (
毒子,サンバーレイン,
-,
メタル,
-,
\0\s[5]今度、いいアルバム紹介してね。\e,
\0\s[0]デスとゴアってどう違うの？\e,
//
)
=kis
regist_communicate_to com data3;
=end

ゴーストの判定はcommunicateを2段重ねにして、 regist_communicateのみを使う方法もあります。 使い勝手の良い方を選んで使ってください。

# com.毒子エントリには、対毒子用文章をregist_communicateで登録
# com.陽子エントリには、対陽子用文章をregist_communicateで登録
com : $(
if $[ ${System.Request.Reference0} == "毒子" ]
com.毒子
);
)
com : $(
if $[ ${System.Request.Reference0} == "陽子" ]
com.陽子
);
)
reply.OnCommunicate : $(communicate com ${com.unknown})

なお、このスクリプトはゴーストに組み込んで使っても構いませんし、 幸水を使って事前に変換する為に利用しても大丈夫です。

## その他

### 簡易文字種判別

関数を作っていると、英大文字、英小文字、数字等、 文字の種類を判別したくなることがよくあります。 精度は今ひとつですが、比較的簡単に判別する関数が作れるので、 次に示します。

# 判別に用いる文字テーブル
asciichar : "ABCDEFGHIJKLMNOPQRSTUVWXYZ_"
asciichar : "abcdefghijklmnopqrstuvwxyz_"
asciichar : "+-0123456789."
asciichar : "-^\!\"#$%&'()=|@[;:],./`{+*}<>?_"
=kis
writeprotect asciichar;
=end
# 文字種判別のテンプレートとなる関数
=kis
function ischaracters $(
setstr @pos 0;
while $[ ${@pos} < $(length $@arg[1]) ] $(
setstr @char $(char_at $@arg[1] ${@pos});
if $[ $@arg[2] !~ ${@char} ] $(
return 0;
);
inc @pos;
);
return 1;
);
# 英大文字だけの文字列ならば真
function isupper $(ischaracters $@arg[1] $asciichar[0]);
# 英小文字だけの文字列ならば真
function islower $(ischaracters $@arg[1] $asciichar[1]);
# 数字だけなら真
function isnumerical $(ischaracters $@arg[1] $asciichar[2]);
# アルファベットだけの文字列ならば真
function isalpha $(ischaracters $@arg[1] $(get asciichar[0..1]));
# アルファベットと数字だけの文字列ならば真
function isalnum $(ischaracters $@arg[1] $(get asciichar[0..2]));
=end

これらの関数は、次のように使います。

string1 : alphabet
string2 : P2P
string3 : 12345
=kis
isupper ${string1};
# 0が返る
islower ${string1};
# 1が返る
isalnum ${string2};
# 1が返る
isnumerilac ${string1}${string3};
# 0が返る
=end

### SHIORI/3.0 NOTIFY処理

従来華和梨が担当していたNOTIFYの処理は、 恐らくどのミドルウェアでも共通して必要だと思います。 そこで、基本的なNOTIFY処理のサンプルを示します。 このサンプルは、華和梨Phase 7.3.1時代のNOTIFY処理を模した動作をします。 また、 Referenceヘッダの参照に、 ユーザーズマニュアルの応用編で定義した Referenceコマンドを使用しています。

# コールバックエントリの設定
System.Callback.OnNOTIFY: ${notify.${System.Request.ID}}
# HWnd通知
# System.Hwnd.shellエントリ…シェル側のHWnd
# System.Hwnd.balloonエントリ…バルーン側のHWnd
notify.hwnd : $(
split System.Hwnd.shell $(Reference 0) $(chr 1);
split System.Hwnd.balloon $(Reference 1) $(chr 1);
)
# インストール済みゴースト通知
# System.InstalledGhostエントリにインストール済みゴースト名一覧
notify.installedghostname : $(
clear System.InstalledGhost;
setstr @count 0;
while $(Reference ${@count}) $(
pushstr System.Installedghost $(Reference ${@count});
inc @count;
);
)
# 起動中の他ゴースト通知
# System.OtherGhostエントリ…起動中ゴースト名一覧
# System.OtherGhostExエントリ…起動中ゴースト名＋サーフィス番号一覧
notify.otherghostname : $(
clear System.OtherGhost;
clear System.OtherGhostEx;
setstr @count 0;
while $(Reference ${@count}) $(
split @ghost $(Reference ${@count}) $(chr 1);
pushstr System.OtherGhost $@ghost[0];
pushstr System.OtherGhostEx $(Reference ${@count});
inc @count;
);
)
# ユニークID通知
# System.UniqueIdエントリ…ユニークID
notify.uniqueid : $(setstr System.UniqueId $(Reference 0))

### デバッグ支援スクリプト

ゴーストを作る際、試行錯誤によるデバッグ作業は避けられません。 しかし、その為にいちいち本体を立ち上げるのは、 あまり効率が良くありません。 その点、幸水を使ったデバッグは効率が良いですが、 イベント処理やNOTIFY処理のデバッグを行おうとすると、 少し工夫が必要です。

そこで、 コールバックエントリの処理動作と、 本体との入出力ヘッダをエミュレートするスクリプトを作りました。 自由に望みのイベント、NOTIFYをエミュレートし、 結果をログに出力できます。 全てKISで記述していますので、 幸水だけでゴーストのデバッグが出来ます。

=kis
# debugOnGET…GETのデバッグ
#
# 第1引数: イベント名、リソース名
# 第2引数以降: Reference[x]
function debugOnGET $(
# IDがなければ終了
if $[ $(size @arg) < 2 ] $(return);
# ヘッダのお掃除
cleartree System.Request;
# ヘッダセット
setstr System.Request GET;
setstr System.Request.ID $@arg[1];
clear @arg[0];
setstr @arg[0] OnGET;
xargs @arg debugCallback;
);
# debugOnNOTIFY…NOTIFYのデバッグ
#
# 第1引数: notifyされた情報名
# 第2引数以降: Reference[x]
function debugOnNOTIFY $(
# IDがなければ終了
if $[ $(size @arg) < 2 ] $(return);
# ヘッダのお掃除
cleartree System.Request;
# ヘッダセット
setstr System.Request NOTIFY;
setstr System.Request.ID $@arg[1];
clear @arg[0];
setstr @arg[0] OnNOTIFY;
xargs @arg debugCallback;
);
# debugCallback…コールバックエントリデバッグ用共通関数
#
# 第1引数: コールバックエントリ(OnGET、OnNOTIFY)
# 第2引数以降: Reference[x]
# 備考: リクエストヘッダはこの関数を呼ぶ前にセットすること
function debugCallback $(
# IDがなければ終了
if $[ $(size @arg) < 2 ] $(return);
# ヘッダのお掃除
cleartree System.Response;
# 共通ヘッダセッド
setstr System.Request.Sender Kosui;
setstr System.Request.SecurityLevel local;
setstr System.Request.Charset Shift_JIS;
setstr @pos 2;
loop $[ $(size @arg)-2 ] $(
setstr System.Request.Reference$[${@pos}-2] $@arg[${@pos}];
inc @pos;
);
# クエリー開始宣言
logprint "[SHIORI/SAORI Emulator] Query sequence begin.";
# リクエストの表示
printRequestHeaders;
# コールバック
if $(size System.Callback.$@arg[1]) $(
setstr @Value $(get System.Callback.$@arg[1]);
if $(length ${@Value}) $(setstr System.Response.Value ${@Value});
);
# 応答の表示
printResponseHeaders;
# クエリー終了宣言
logprint "[SHIORI/SAORI Emulator] Query sequence end.";
#ヘッダの後片付け
cleartree System.Request;
cleartree System.Response;
);
# リクエストヘッダの表示
function printRequestHeaders $(
logprint "---------------------- REQUEST";
logprint ${System.Request} "SHIORI/3.0";
listtree @Headers System.Request;
if $(size @Headers) $(
foreach @h @Headers $(
if $[ ${@h} != "System.Request" ] $(
logprint $(substr ${@h} 15)":" ${${@h}};
);
);
);
logprint;
);
# 応答ヘッダの表示
function printResponseHeaders $(
logprint "---------------------- RESPONSE";
if $(size System.Response.Value) $(
logprint "SHIORI/3.0 200 OK";
) else $(
logprint "SHIORI/3.0 204 No Content";
);
listtree @Headers System.Response;
if $(size @Headers) $(
foreach @h @Headers $(
if $[ ${@h} != "System.Response" ] $(
logprint $(substr ${@h} 16)":" ${${@h}};
);
);
);
logprint;
);
=end

このスクリプトを幸水からロードし、 次のようにコマンドモードで入力すると、 「OnSecondChangeイベント、連続起動15時間目、見切れ・重なり無し、 トークは出力される状態」がエミュレートできます。

debugOnGET OnSecondChange 15 0 0 1

また、次のように入力すると、 「インストールされているゴーストはさくら、まゆら、双葉、毒子」 のNOTIFYをエミュレートします。

debugOnNOTIFY installedghostname さくら まゆら 双葉 毒子

実際は、さらにこのコマンドを使ってスクリプトを組み、 長時間起動時の加速試験等を行うと便利でしょう。 次の例は、1時間分のOnSecondChangeとOnMinuteChangeを送り、 加速耐久試験を行うスクリプトです。 トーク途中で選択肢を出した場合、サーフィス復帰を考慮していませんが、 普通はこれで十分だと思います。

=kis
# Hourエントリに起動何時間目か記憶させる
loop 60 $(
debugOnGET OnMinuteChange ${Hour} 0 0 1;
loop 60 $(
debugOnGET OnSecondChange ${Hour} 0 0 1;
);
);
=end