記事一覧

バグの原因の見つけ方 with SSP

ドウメキ不具合調査中の思考経路垂れ流しまとめ

昨日うにゅう@もどき板ゴースト製作上の質問スレでドウメキの不具合相談がありまして、問題は解決したのですが、特定の現象の解決法そのものよりも、その解決に至るプロセスの方が多くの人にとって価値があると考えたため、その過程を思い出しながらまとめてみました。

不具合の原因の特定に至る時間の節約に役立てれば幸いです。

以下、その時の脳内ダダ漏れ

※SSP固有の機能を利用した部分に【!】マークを付けています。

報告内容

これを起動して開発パレットのスクリプト入力から、

\h\i[79]\e

というスクリプトをおくると40秒ほどでアニメーションが止まってしまいます。

ドウメキ起動
  • 【!】Ctrl+S
  • "\h\i[79]\e"と打つ
  • 数十秒待つ
  • 再現した。(通常のバグ報告の場合ここまで来れば90%解決したも同然です)

ファイル 112-1.png

  • surfaces.txtをさらっと眺める
  • 数十秒でアニメーションがとまるような記述は見当たらない

※SSPはSHIORIイベントを投げて返ってきたSakuraScriptを再生するだけの装置である
※SSPが勝手に判断してゴーストを制御することは(原則として)ない

【!】SSPのログ起動

機能>スクリプトログ

  • もう一度再現手順を繰り返す
  • アニメーション停止と同時に"\0\s[0]\1\s[10]\e"再生キタ!(ここで99%解決したも同然です)
  • こいつが原因だ。SHIORIが"\0\s[0]\1\s[10]\e"を投げ返すきっかけとなったイベントは何だ?

ファイル 112-2.png

【!】SSP SHIORIログをとる

機能>開発用パレット>「SHORIログを取る」にチェック

  • もう一度再現手順を繰り返す
  • ゴーストディレクトリ内のssp_shiori_log.txtを開いて"\h\i[79]\e"で検索
  • その次に来る"\0\s[0]\1\s[10]\e"を検索
  • イベント名確認
  • OnSurfaceRestoreと判明(ここで99.9%解決したも同然です)
=====send=====
GET SHIORI/3.0
ID: OnTranslate
Charset: Shift_JIS
Sender: SSP
SecurityLevel: local
Reference0: \h\i[79]\e\e


=====response=====
SHIORI/3.0 204 No Content
Charset: Shift_JIS

...(略)

=====send=====
GET SHIORI/3.0
Charset: Shift_JIS
Sender: SSP
SecurityLevel: local
ID: OnSurfaceRestore
Reference0: 0
Reference1: 10


=====response=====
SHIORI/3.0 200 OK
Value: \0\s[0]\1\s[10]\e
Charset: Shift_JIS
脳内中間報告

里々がOnSurfaceRestoreで"\0\s[0]\1\s[10]\e"を返していることが原因だった

OnSurfaceRestoreで何も返さない(204 No Contentを返す)ようにすれば直るはず

解決策の模索
  • 「何も返さない」ことを明記して基底の処理をオーバーライドする必要がある(どうやるんだっけ?)
  • 里々Wikiで"204"で検索
  • $今回は喋らない【タブ】有効
  • 直った!!!!(100%解決)
*OnSurfaceRestore
$今回は喋らない【タブ】有効

おしまい

【!】でマークした部分を駆使すれば、今まで闇雲にバグ潰しにかけていた時間がかなり節約できるかもしれません。

何よりも今回は90%解決済の状態で報告が上がっていたので解決は容易でした(要点を得ない、再現しないものは解決不能)。再現のため単純化したNARを添付するのは再現性を確実にする上、原因の絞り込みを容易にするための模範的なレポートだと思います(自分一人でデバッグする際も大事なことです)。ということで勝手にご紹介させて頂きました。

DAU-Crawler, NAR-Station 公開

SiReFaSoSoSiReMiのコードを一般化

ゴースト更新捕捉システムと、ネットワーク更新に対応したアップローダーのソースコードを公開しました。

今後、SiReFaSoはDAU-Crawlerを利用したサイトの一つ、SoSiReMiはNAR-Stationを利用したサイトの一つ、という位置づけになります。

個人的なバックアップが主目的ですが、アップローダーの方は個人利用の需要があるかも知れないので、詳細な設置方法など暇を見て駄デベWikiなどに書いていこうと考えています。

インストールされていない追加シェルを紹介する

type,shell_notinstalled

非公式シェル仕様書を眺めていたのですが、一番下の方に shell_notinstalled なんていう知らない項目を見つけました。(みんな知ってた?)

type,shell_notinstalled
name,せりこver.To
craftmanurl,http://www.abc.or.jp/sakura/index.html
craftman,Tomohiko
craftmanw,ともひこ

こんな風に書いたdescript.txtを該当ディレクトリに入れておくとシェルが"見える"ようになります。切り替えようとするとダイアログが開いて配布ページに誘導されます。

ウチのゴーストの場合

ファイル 103-1.png

ファイル 103-2.png

注意点

NARを作成するときに含めます。ネットワーク更新に含めてはいけません(既にインストール済の人でも上書きされてまだインストールされていない状態として認識されてしまうため)。

ネットワーク更新に含められないのが残念ですが、追加シェルの存在するゴーストのNARを作成するときは含めてみるとよいかもしれませんね。

追加シェルとバルーンのネットワーク更新時にもトークさせるYAYA版

伺かアドベントカレンダー2010/追加シェルとバルーンのネットワーク更新時にもトークさせるお話

知らなかった。ショック。

YAYAの場合

はろーYAYAわーるど」ならこんな感じですね。OnUpdateOtherBeginとOnUpdateOtherCompleteだけshellとballoonに対応してみた版。他はghostの更新と共通にしてあります。

// This document is in the public domain.

OnUpdateOtherBegin
{
  if ISFUNC('OnUpdateOtherBegin_' + reference[3]) {
    EVAL('OnUpdateOtherBegin_' + reference[3])
  }
  else {
    "\0\s[5]%(reference[3])に更新はあるかなー?\w8\1\s[10]どうだろな。\e"
  }
}
OnUpdateOtherBegin_shell
{
  "\0\s[5]流行のファッションをチェック!\w8\1\s[10]無駄遣いはするなよ。\e"
  "\0\s[0]クリーニング屋さんに行ってくる。\w8\1\s[10]何か預けてあったか?\e"
}
OnUpdateOtherBegin_balloon
{
  "\0\s[5]丸文字フォントのバルーンになーれ!\w8\1\s[11]やめろ。\e"
  "\0\s[0]Apric○tっぽいバルーンをお願いします。\w8\1\s[10]諦めろ。\e"
}
OnUpdateOtherComplete
{
  if ISFUNC('OnUpdateOtherComplete_' + reference[3]) {
    EVAL('OnUpdateOtherComplete_' + reference[3])
  }
  else {
    OnUpdateComplete()
  }
}
OnUpdateOtherComplete_shell
{
  if reference[0] == "none" {
    OnUpdateComplete()
  }
  else {
    "\0\s[5]わーい!\w8ちょっと新しくなったよ!\w8\1\s[10]どれどれ。\e"
    "\0\s[0]どう?\w8ナウい?\w8\1\s[11]ナウいってお前‥\w5‥\w5。\e"
  }
}
OnUpdateOtherComplete_balloon
{
  if reference[0] == "none" {
    OnUpdateComplete()
  }
  else {
    "\0\s[2]びっくりしてフォントがでっかくなっちゃった!\w8\1\s[10]んなこたーない。\e"
    "\0\s[0]音声読み上げに対応しました。\w8\1\s[10]バルーンだけじゃ無理だろ。\e"
  }
}
OnUpdateOtherReady                 { OnUpdateReady() }
OnUpdateOtherFailure               { OnUpdateFailure() }
OnUpdateOther.OnDownloadBegin      { OnUpdate.OnDownloadBegin() }
OnUpdateOther.OnMD5CompareBegin    { OnUpdate.OnMD5CompareBegin() }
OnUpdateOther.OnMD5CompareComplete { OnUpdate.OnMD5CompareComplete() }
OnUpdateOther.OnMD5CompareFailure  { OnUpdate.OnMD5CompareFailure() }

ウチのYAYAゴースト(自前のミドルウェア使ってるので少し書式が違いますが)にも搭載してみました。

SSPのエラーログを見よう!

surfaces.txtを正しく書こう

今更surfaces.txtの書き方など、私より絵を描かれるみなさんの方が詳しいと思いますが、時々エラーログを吐くsurfaces.txtに遭遇することがあります。そこで今回は主にsurfaces.txtを正しく書くためのチェック方法について取り上げてみたいと思います。

なぜ正しく書く必要があるのか

間違った書き方をしても、SSPは頑張って描画してくれます。でも、それは本当に自分が意図した通りの動きをしているでしょうか。アニメーションをさせようとして、「なかなか思い通りに動かないなー」って悩んだ経験はありませんか?
それはもしかしたら、記述法が間違っているのかもしれません。また、正しくないsurfaces.txtではSSP以外のベースウェアでは表示すらされないかもしれません。効率的に記述ミスを発見して自分の思い通りの動きをさせるために、異なるベースウェア間でも同じ動作をすることを保証するために、SSPをあまり困らせないように、自作のシェルにエラーが無いかどうか、以下に紹介する方法で一度チェックしてみましょう。

surfaces.txtのデバッグ方法

まず自作のゴーストを立たせます。シェル作者さんは自分のシェルを使ったゴーストを立たせましょう。私は『ポストと狛犬』を説明用に使わせていただきます。

まずオーナードローメニューから、「機能」>「開発用パレット」を選択します。

ファイル 98-1.png

「エラーログを表示」を選択します。

ファイル 98-2.png

NoticeやErrorといったものが表示されました。今表示されているものがすべて消えることを最終目標とします。

まずErrorから。"[SERIKO]Surface=7 画像が存在しません。"とあります。 『ポストと狛犬』のshellフォルダの中にはSurface=7にあたる画像は見当たりません。しかしsurfaces.txtには以下の指定が記述されています。

surface7
{
collision0,10,50,200,90,Head
collision1,69,138,85,163,LeftEye
collision2,116,137,132,162,RightEye
collision3,18,117,187,242,Face

point.kinoko.centerx,145
point.kinoko.centery,30
}

これはSurface=7にあたる画像を入れ忘れた可能性がありますね。このままでは\s[7]としても描画されません。surface0007.pngを作成してshellフォルダに入れる、もしくはGHOST側で\s[7]を使わないのであれば、上記の記述を削除することでこのエラーは消えます。

次にNotice。"[SERIKO]Surface=** どこからも使われていません。"とあります。shellフォルダには確かにsurface00**.pngが入っていますが、surfaces.txtにはそれに関する記述がありません。つまり、このサーフィスは使われていない可能性があります。起動時にSSPが読み込んでしまう分、起動が遅くなってしまうので、このサーフィス画像は削除したほうが賢明です。

「いやいや、そのサーフィス使うんだよ!」っていう場合。surface0011.pngはそもそも狛犬の刮目サーフィスで実際使われていますし、surface0021.pngからsurface0023.pngまでは「らぼ」メニューの「ちんぼつ」で使われています。

*ちんぼつ
:(0)(21)(22)(23):何、してるんだ。
:沈没。
:な……
:(23)(22)(21)(0)どうかな。
:どうかな、じゃなくてだな……。

当たり判定もアニメーションも画像の合成もしていないのでsurfaces.txtには何も書かれていませんが、実際にはSSPが画像のファイル名から当該番号のサーフィス定義を補っていると考えられます。でも、実際に使われていないファイルと区別がつかないのでNoticeが出ています。画像を使用する旨をきちんと定義することでこのNoticeを消すことができます。

surface11
{
}

ただ、これをやると偽林檎で死ぬらしいです。(参考:殊海夕音/やってはいけない - 駄でべろぱの小ネタWiki

ダミーの記述を入れておけば問題ないでしょう。

surface11
{
element0,overlay,surface0011.png,0,0
}

他の3つのNoticeも同様の記述で消すことができます。

まとめ

いかがでしたでしょうか。これでエラーの無い真っ当なシェルをリリースすることができます。中にはSERIKO/1.xとSERIKO/2.0のアニメーションの記述が混在していてエラーが出る場合もあるでしょう。SERIKO/2.0については以下のサイトが参考になります。

非公式 SERIKO 仕様書

surfaces.txt付きのフリーシェルを作成する場合や、他のGHOSTの追加シェルを作る際にもぜひエラーなどがないようチェックしておきましょう。

ここまでくるとsurfacetable.txtの書き方に関するTipsなども欲しくなりますが、今回はここまでとさせていただきます。

宣伝

このエントリは以下の企画用に書き上げたものです。

伺かアドベントカレンダー2010

特に技術的なテーマに限定しませんので、伺かに関して何か書いてみたい方は、お気軽にご応募下さい。

伺かアドベントカレンダー2010

本日12月1日より,プログラマ有志による技術系Advent Calendarが各所ではじまる

最近技術系ブログが賑わっていると思ったらこんな企画があったのですね。

例えばMySQLはこんな感じ → http://mysql-casual.org/2010/12/mysql-advent-calendar-2010.html

JavaScriptとかめっちゃ面白そう。 → http://atnd.org/events/10497

毎日持ち回りで一人ずつTips的な何かを書いていく企画、みたいです。楽しそうですね。

伺かでもやりましょう

既に4日ですが、明日からやりましょう。

伺かアドベントカレンダー2010

突発的な思い付きですが、1人1エントリ書くだけなのでなんとかなるでしょう。各記事へのリンクまとめとかは駄デベWikiとかお借りすればよいかな?Bolgを持ってない人はWikiに書いてもいいよ!

伺かにまつわる記事であればプログラミングに限らず何でもOKです。各種Webサービスのアカウントに加え、OpenIDなどでも登録できます。お気軽にご応募下さい。

創作に役立つ知識がみんなで共有されるとよいですね。

SSPで画像を選択肢に使用する

選択肢のタグは通常 \q[表示名,選択肢ID] を使用しますが、SSPでは \__q[選択肢ID]表示名\__q というタグが利用できます。

さくらスクリプト@wiki - さくらスクリプト/基本コマンド

これを使うと何が良いかというと、"表示名"の部分にタグが利用できます。 \_b タグを使用すれば画像を選択肢にすることも可能です。

\__q[menu]\_b[menu_icon.png,inline]\__q

使用例としては、「れいちぇるのれすとらん~ねこのいるおみせ~」の着せ替えメニューがあります。画像を見ながら着せ替えを指定できるのでわかりやすいです。

拙作の「BalloonSelector」でも利用させて頂いています。

GitHubを伺か更新ファイル置き場にする

GitHubって何よ

Secure source code hosting and collaborative development - GitHub

里々YAYAが置かれてるGoogle Codeってありますよね。あれの遠い親戚です。バージョン管理システムのリポジトリホスティングサービスです。

Gitの使い方を覚えてみたくなったので、勉強がてら先日作ったPLUGINのソースをアップしてみました。

nikolat's daumaker at master - GitHub

ついでに色々実験してみた

GitHubに伺かのコードなんぞアップしてる人なんてあまり見かけないですが、意外と相性がいいのではないかと考えました。

長所
  • httpsで直接ファイルにアクセスできる->SSPはhomeurlにhttpsを指定できる->ネットワーク更新ファイル置き場として機能する
  • ソースコード一式をtar.gzまたはzipでダウンロードできる->SSPに直接インストールできる
  • Gitコマンドにより一括アップロードが可能、除外ファイルも楽々指定
  • readme.txtが表示されている
短所
  • GitはWindowsにやさしくないし設定が大変
  • HTTPリクエストでファイルの更新時刻を返してくれない(これはDropboxとかにも言えることなんだけど)

SoSiReMiとは何だったのか

ここまで色々できてバージョン管理までできるとなるとSoSiReMiは要らない子…いやいや、更新時刻が取得できないのはSiReFaSoとか作ってる身からするとかなりマイナスですね。

ちょっとした作り捨てのPLUGINとか置いておく分には便利そうなのでもうしばらく使い込んでみようと思います。

GHOSTからNARとupdates2.dauを作成

DnDじゃなくて

GHOST自身が作成する方法

NAR

\![execute,compressarchive]を使う

さくらスクリプト@wiki - さくらスクリプト/未分類

updates2.dau

YAYAで頑張って作る

YAYAで頑張って作ってみた

100行プログラミング。 Download => daumaker.nar

  • developer_options.txtを読んで除外ファイルを考慮している。
  • profileフォルダやvarフォルダはデフォルトで除いている。
  • GHOSTのルートディレクトリにupdates2.dauが作成される。
  • updates2.dauの正式な書式の仕様って何処にあるの?
OnMenuExec
{
  "\![get,property,OnGetGhostPathFromPlugin,ghostlist(%(sender)).path]\e"

  res_event = 'OnPluginExec'
  res_reference[0] = "Version=%(version())"
  res_reference[1] = 'From=DauMaker'
}

OnGetGhostPathFromPlugin
{
  _path = reference[0]
  sys.fnc.MakeUpdates2Dau(SUBSTR(_path, 0, STRLEN(_path) - 1))

  res_event = 'OnUpdatedataCreated'
  res_marker = version()
}

sys.fnc.MakeUpdates2Dau
{
  _path = _argv[0]
  _delim = CHR(0x01)
  _ignore = sys.fnc.getIgnoeFiles(_path, _delim)
  _files = SPLIT(sys.fnc.getUpdateFiles(_path, _delim, _ignore), _delim)
  _fname = _path + '\updates2.dau'
  if !FOPEN(_fname, 'w')
    return
  foreach _files; _f {
    _relative_path = REPLACE(REPLACE(_f, _path + '\', ''), '\', '/')
    _hash = TOLOWER(FDIGEST(_f, 'MD5'))
    FWRITE(_fname, _relative_path + CHR(0x01) + _hash + CHR(0x01))
  }
  FCLOSE(_fname)
}

sys.fnc.getUpdateFiles
{
  _path = _argv[0]
  _delim = _argv[1]
  _ignorestr = _argv[2]

  _ignores = SPLIT(_ignorestr, _delim)
  if !_ignorestr; _ignores = IARRAY()
  _ignore_default = '\\profile|\\var|updates2\.dau|update\.txt|ngm\.dat'

  _fenum = FENUM(_path, _delim)
  if !_fenum; return
  _fs = SPLIT(_fenum, _delim)
  _ret = ''
  foreach _fs; _f {
    if RE_MATCH(_f, _ignore_default); continue
    _sub_ret = ''
    if SUBSTR(_f, 0, 1) == '\' {
      if ASEARCH(_path + _f + '\', _ignores) >= 0; continue
      _sub_ret = sys.fnc.getUpdateFiles(_path + _f, _delim, _ignorestr)
      if !_sub_ret; continue
    }
    else {
      if ASEARCH(_path + '\' + _f, _ignores) >= 0; continue
      _sub_ret = _path + '\' + _f
    }
    if _ret != ''; _ret += _delim
    _ret += _sub_ret
  }
  _ret
}

sys.fnc.getIgnoeFiles
{
  _path = _argv[0]
  _delim = _argv[1]

  _fname = _path + '\developer_options.txt'
  if !FOPEN(_fname, 'r')
    return

  _ret = ''
  while 1 {
    if (_line = FREAD(_fname)) == -1; break
    _params = IARRAY()
    foreach _line; _l {
      _params ,= CUTSPACE(_l)
    }
    if ASEARCH('noupdate', _params) >= 1 {
      if _ret != ''; _ret += _delim
      _ret += REPLACE(_path + '\' + _params[0], '/', '\')
    }
  }
  FCLOSE(_fname)

  _ret
}

version
{
  'DauMaker/1.0'
}

共有変数プラグインを試してみた

共有変数プラグイン

SSP/2.2.42より標準添付された、ゴースト間共有変数処理用プラグインです。詳しくはreadme.txtを参照。

使ってみる

書き込み

readme.txtの41行目にOnSharedValueReadって書いてありますけどOnSharedValueWriteの間違いです。

{
  _pluginID = 'ABED14AF-F34B-4ff2-95B7-30ED37D5802D'

  _script = "\![raiseplugin,%(_pluginID),OnSharedValueWrite,きー,ばりゅー]"
  _script += '\0かきこみー\e'

  _script
}

readme.txtにもありますが、「ゴースト名」を指定することはできません。たぶんSenderを読んでるんだと思います(今回は「ごーすとじてん」)。別ゴースト領域への書き込みを防ぐための仕様のようです。

読み込み
{
  _pluginID = 'ABED14AF-F34B-4ff2-95B7-30ED37D5802D'

  _script = "\![raiseplugin,%(_pluginID),OnSharedValueRead,ごーすとじてん,きー,かー]\e"

  _script
}
OnSharedValueRead
{
  _script = '\0\_qごーすとじてんのでーた\n' + sys.fnc.ShowReference() + '\e'

  _script
}
sys.fnc.ShowReference
{
  _s = ''
  for _i = 0; _i < ARRAYSIZE(reference); _i++ {
    _s += "Reference%(_i): %(reference[_i])\n"
  }
  _s
}

//こんなresponseが返ってくる
//Reference0: ごーすとじてん
//Reference1: きー
//Reference2: ばりゅー
//Reference3: かー

先程保存した「きー」に対する「ばりゅー」が返ってきます。「かー」は存在しないので次のReferenceは空欄。これは他のGHOSTからも読めます(ていうかそのためのPLUGIN)。

リスト表示
{
  _pluginID = 'ABED14AF-F34B-4ff2-95B7-30ED37D5802D'

  _script = "\![raiseplugin,%(_pluginID),OnSharedValueGhostList]\e"

  _script
}
OnSharedValueGhostList
{
  _script = '\0\_q' + sys.fnc.ShowReference() + '\e'

  _script
}
sys.fnc.ShowReference
{
  _s = ''
  for _i = 0; _i < ARRAYSIZE(reference); _i++ {
    _s += "Reference%(_i): %(reference[_i])\n"
  }
  _s
}
//こんなresponseが返ってくる
//Reference0: ごーすとじてん

そのPLUGINを利用しているGHOSTのリストが返ってきます。

ちょっと意地悪をしてみる

SSPのタグを使わず直接PLUGINにrequestを投げてみます。

#define C_CRLF CHR(0xd)+CHR(0xa)

foo
{
  _dll_path = '..\..\..\..\plugin\shared_value\shared_value.dll'

  _r_load = LOADLIB(_dll_path)
  if _r_load == 0 {
    '\0失敗\e'
    return
  }
  //ホントはLOAD直後にversionをリクエストするのが流儀
  //だけど今回は省略

  _reqheader = "/
    GET PLUGIN/2.0%(C_CRLF)/
    Charset: UTF-8%(C_CRLF)/
    ID: OnSharedValueWrite%(C_CRLF)/
    Sender: AYA%(C_CRLF)/
    Reference0: hoge%(C_CRLF)/
    Reference1: fuga%(C_CRLF)/
    "
  _reqheader += C_CRLF

  _result = REQUESTLIB(_dll_path, _reqheader)//responseは 204 No Content

  UNLOADLIB(_dll_path)

  '\0書き込み完了\e'
}

YAYAはSAORIに限らず伺かのプロトコルなら何でも投げられます。今回はSenderをAYAとしたのでAYAさんの名前で保存されました。Senderを偽装ずれば他のGHOSTのデータを改竄できそう(やらないけど)。

何に使う?

YAYAから直接利用できたので当然他のPLUGINからも利用出来ます。ウチでも音楽再生PLUGINなどを公開していますが、再生リストを全てのGHOSTと共有可能にしてみるとか。音楽再生機能を持つGHOSTは数あれど、それぞれ登録し直さないといけないのも不便ですし。

…でもこれは全てのGHOSTに書き込み権限が無いと逆に不便ですね。もう少し色々考えてみようと思います。

ページ移動