496の落書き帳

496がなんか思ったことを書いたり書かなかったりする場所です。

ゲームのバグの話がしたかったんです。(後編)

はじめに(はじめではないが)

ISer Advent Calendar 2020 - Adventarの12日目の記事です。 前日の記事 の続編。
前編を書いている間は「長くなっちゃったし、2日分にしてもええか」とか思っていたんですが、読み返すと短い気がしてきてかなしい。とにかく最後まで書きます。

追記.ごめんなさいちょっと遅刻しました・・・。

後編もくじ

前編でしゃべったこと。

  • 風来のシレンのセーブ機能(中断・再開)はリプレイ機能(回想)を回すことで実装されている。
  • わりと簡単な条件下でリプレイにおける乱数がズレることが長い研究の末に分かった。
  • 乱数がズレると位置ズレ、所持アイテムズレが起き、結構ヤバイことが起きそうな気がする。

破ー綻び (つづきから)

用語定義(笑)

ここからリプレイバグ、通称回想バグのお話をしますが、ちょっとだけ。

ここまで参考文献*1の話を自分なりに書き直してきたんですが、風来のシレン学会では以下のような定義が一般に通っています。折角なのでこの記事でも使っていきましょう(使いたいだけ)。

  • 中断バグ:元プレイと回想で何らかの食い違いが起きること。
  • 回想バグ:元プレイと回想で行動データの解釈を変えることで、通常実行できないはずの処理を(シミュレータに)実行させること。
  • α世界線:回想バグの実験・分析において、元プレイのこと。
  • β世界線:回想バグの実験・分析において、回想のこと。

用例.

  • プレイ中がα世界線であり、β世界線は直接観測できないことに留意しましょう
  • 中断バグを起こしてβ世界線を分岐させる
  • α世界線で〜することで、β世界線で異常処理を起こす
  • 中断・再開してβ世界線に移行する

錯誤ー型エラーの誘発

さて、前編ラストの問いかけに戻って

α世界線では持ち物内に存在して、β世界線では存在しないアイテムを消費するとどうなるか?

リプレイ用の行動データ記録では、00~FFまでのコードまでのうちどこかの20個*2くらいに(持ち物欄のn番目アイテムを使用)という割り当てがあります。α世界線で使ったアイテムがそのとき持ち物欄10番目にあったとして、β世界線ではそれが拾われていなかったならば、β世界線ではそのとき持ち物欄10番目に何か別のものが入っているはずです(あるいは何も入っていないかもしれない*3が)。で、β世界線では別のアイテムが使われるということになります。「アイテムを使用」の解釈は適当に行われ、アイテムの種別を示すデータを読むことで、同じ行動コードでも草や肉ならば「食べる」、巻物ならば「読む」、剣ならば「装備する」などが実行されます。型チェックみたいな、というか、型オーバーロード演算子みたいな話ですね。

これだけだと、αとβで色々異なる挙動をするにはしますが、そこまで大変なことにはならなさそうです。が、風来のシレンには「」というものすごく特殊なアイテム種があります。壺には壺以外の*4アイテムを入れたり出したりすることができます。アイテム欄を1つしか消費せずに複数アイテムを持ち運べるので攻略上は非常に有用ですが、「所持アイテムの中から何かを入れる」「中身のどれかを出す」といった複数の特殊処理が必要なので、前述の「使用」とは別に*5行動コードが割り振られています。具体的には(持ち物欄n番目から取り出す)というコードがあり、それに続けて壺内の何番目のアイテムを取り出すかを示す値を書くという文法で処理されます。入れる方もおおよそ同様です。

さて、α世界線で持ち物欄10番目の壺から何かを取り出して、β世界線ではそのとき持ち物欄10番目に肉があったとしたら・・・まずそうですね。「入れる」「取り出す」は壺専用の行動コードなので、「使用」とは逆にアイテム種類が壺であることのチェックはありません。型チェックしろ〜〜〜と言いたくなりますが、既に想定外の事態が起きているので・・・。シミュレータが壺以外のアイテムを壺と解釈して各種のパラメータを処理することになります。具体的に一番やばい処理としては、モンスターの肉を壺と解釈してアイテムを取り出す処理をすると、肉の種類を示す値(本来は壺の空き容量を示す値)が1増えて別の肉になってしまうそうです。前編で触れた怪現象:見たこともない謎のモンスターの肉を所持はこれによることが知られています。食べると謎のモンスターに変身したりして大変なことになります。この時点で開発側が触れられることを想定していない(いわゆる没)データに侵入できます。やべ〜〜〜。

亀裂ー文脈破壊

ところで、「取り出す」行動のコーディングは(持ち物欄n番目の壺から取り出す)(壺内のm番目のアイテムを)という2byteの列になるので、1行動が1byteにならない場合があるわけです。そして行動ごとのコードは固定長になっていません。ちょっと嫌な予感がしてきますね。

さらに、(話しかける/攻撃)*6という行動コードがあります。人に話しかけると会話があり、会話があれば選択肢が出たりすることもあります。選択肢を選んだ際はそれが何番目の選択肢だったかを行動データの次のbyteに書くことになっています。結果的に、(話しかける/攻撃)の行動の結果の記録byte列の長さは実行時の状況によって可変になるということです。これが致命的な事態へと繋がります。

位置ズレによって、αでは目の前に誰もいないがβでは目の前に人がいる状態になったとしましょう。ここで攻撃(素振り)をすると、αでは何も起きず、βでは会話が始まります。さらにβの会話では選択肢が1つあったとすると、αでの次の行動コード…(X)がβでの選択肢として解釈されます。で、βで会話が終わると(X)の次の行動コード…(Y)からシミュレートが再開します。なんか良からぬズレが起きてますね。αでの行動が(X)の1byteで済むものならばβではそれがスキップされるような感じになりますが、2byte以上の場合はさらにややこしい展開が発生します。例えば以下のように。

(わかりにくい図解)
α世界線の解釈…(攻撃)(XYからなる行動)…
行動コード列 … (話しかける/攻撃)(X)(Y)…
β世界線の解釈…(会話)(選択肢X)(Y…からなる行動)…

何がヤバイのかというと、行動の内容・コードの長さはおおよそ先頭のbyteによって決まるのに、(XY)が表現する行動の2番目のbyteである(Y)が1行動のコードの先頭byteとして読まれてしまうという事態です。これで「このbyte列はどこで区切られるべきで何を示しているのか」という解釈(文脈)が思いっきり破壊されてしまいます。

ただ、α世界線から入力できるbyte列が限られているので、β世界線で意図した処理を引き起こすのは中々難しいだろう…と思われたのですが…
「正体不明のアイテムに名前をつける」*7という機能があって、好きな文字6文字を入力できます。これはおおよそ任意の6byte列を行動コード中に埋め込むことができることを示唆します。もうプログラミングするしかねえ*8

挙句、回想バグで(本来ありえない)とある行動コードを入力すると(開発中に使われていたであろう)デバッグモードが呼び出せることが発見され、(しかもその方法が確立され、)ほぼ任意のアイテムを召喚できるようになってしまいました。デバッグ環境が整ってしまった・・・・

急ーそして崩壊へ

そういうわけで完全崩壊一歩手前まで来てしまった感じがする風来のシレンですが、ここにもう一つ凶悪なバグが追加されることでまだまだ壊れます。正直話したいことはだいたい話したんですが崩壊っぷりが面白いので、ザクザク紹介します。もうやめて…

破滅ー階段取得

フロア状態の各マスデータ列に

  • 足元には何が落ちているか

という項がありました。実はこれ、アイテムと次フロアへの階段が1byteのメモリを共有していて、アイテムIDか、階段を示すデータが入っています*9。足元にアイテムが落ちていれば当然「拾う」コマンドが実行できるわけですが、階段では「進む」しか実行できません。

α世界線ならば

もうお分かりですね。α世界線でアイテムの上に立ち、β世界線で階段の上に立つように調整して、α世界線で足元のアイテムを拾うと、β世界線で階段を拾うことができます。階段(のID)はアイテムとして処理させるとバグりまくることが容易に想像できますが、うまく扱えば足元に置くと階段として機能する上、持ち物から消えないというあまりにも便利な性質を持つことが分かってしまいました。つまり、階段を持っているプレイヤーは

階段を足元において次のフロアに進む

を繰り返すだけでダンジョンを踏破できます。実用性高杉・・・

壊滅ーメモリの海へ

ところでこのゲームにはダンジョンだけでなくちゃんと町というものがあります。町には何個か建物があって、それぞれに入ると建物内マップに移動するといった処理が行われますが、実は町もダンジョンフロアの一種で、建物の出入り口も階段の一種らしいということが近年分かってきた*10そうです。ダンジョンにおける階段は「次のフロアに進む」だけで済みますが、町での出入り口の複雑な処理はどうやってできているのかというと、「ここはここに繋がっている」という階段の座標と行き先の対応表を見ているっぽいです(至極真っ当)。というかむしろ

階段処理:
| if (現在フロア,現在座標)がテーブルに登録されている then
| | テーブルに書かれた移動先の(フロア,座標)に移動
| else
| | 現在フロアから算出される「次のフロア」に移動
| (if 行き先がダンジョン then ランダムフロア生成・初期座標決定)

こんな感じの処理になっているらしいです。で、何が起きるのかというと、

町中に階段を設置すると行き先が猛烈にバグる*11

「猛烈に」というのは、まあ画面が完全に崩壊してしまうんですが、これは本来到達されないはずのフロアに行くどころか、初期座標が(0,0)になってしまうためです。座標(0,0)の意味するところは恐ろしく強烈です。ゲーム画面を映すために近くのマスたちの状況を参照することになりますが、すぐ隣のマスの座標が(オーバーフローして)(255,255)とかになっています。このゲームのマップは64×64マスだということをかなり前に言いましたが、マップデータの配列の(255,255)番目を参照することはいわゆるindex out of range*12、すなわち(愚直にメモリ番地の計算をすれば)本来マップデータでもなんでもない領域をマップデータとして読み込むということです。あかーーーん。

既に大惨事です*13が、このマップを歩き回り異常座標のデータ(落ちているアイテムなど)を変えることで、対応する(マップに関係ない)メモリの値を改変できたという恐ろしい報告が上がりました*14。ついにシレンメモリの海を泳ぐ変態に仲間入りしてしまった瞬間です。あーあ、壊れちゃった・・・

現在の風来のシレン学会では、

  • 階段移動処理の詳細の調査
  • 異常フロアへの到達法の模索
  • 異常座標を用いて改変できる各メモリ番地の用法の調査

などがアツいようです。皆さんもぜひ学会の研究を追いかけてみてください。

最後に まとめ って 言うほどでもないけど。

長々とバグのお話をしてきました。正直疲れました。自分もちゃんと理解してないところあるしなぁ。

ここまで分析・応用を深めてきた研究者の皆さんの執念には脱帽ですね。デバッガーやってほしい。実は本職デバッガーなのかもしれないけど。

回想バグ、乱数のズレという一つの綻びからここまでの展開が起きるのは見ていて楽しいですね。回想バグ許すまじ(建前)いいぞもっとやれ(本音)

最後に、自分が大好きな攻略動画(TAS動画)を紹介して終わろうと思います。α世界線で適当な挙動を繰り返すと、β世界線上で必要なイベント(会話とか、時間がかかる)がこなされて超高速クリアできるというやつです。動画時間(α世界線)に対してやってること(β世界線)の密度が高いのが芸術点高い。まとめとしてぜひご覧ください。

www.nicovideo.jp

*1:参考文献:主にニコニコ動画のタグ「風来のシレン学会」がついた動画群。

*2:持ち物が20個までしか持てないという仕様でバリエーションを抑えています。

*3:β世界線のアイテム欄でアイテムが存在しない位置を指定して、「無を使用」するというのは面白そうな実験ですが、今のところそれを利用する系の報告がないので、おそらくフリーズするとか面白い結果が得られないものと思われます

*4:人肉を扱うことが想定の範囲内に入っている風来のシレンでも、壺に壺を入れることは流石にできないみたいです。

*5:壺の「使用」は「取り出す」とも「入れる」とも違う処理になります。例えば、出し入れや持ち運びとは全く用途が違う「背中の壺」というアイテムがあって、使用する(「押す」というコマンドが出る)と体力が回復したり・・・なんやそれ。

*6:キャラクターに人間・モンスター判定があって「話しかける」「攻撃」が切り替わります。

*7:名前を付ける機能の存在価値については結構ややこしいので割愛します・・・

*8:某研究者が「風来のシレンチューリング完全か」という問題を提起したとかしないとか

*9:正確にはアイテム・階段の他に罠が入ることもあります。本題に関係ないので割愛。罠を拾うとどうなるかは知りませんが・・・

*10:話の順序はむしろこの記事とは逆で、異常座標に飛ぶバグが見つかったので移動処理を解析していたら詳細なことが分かってきたんですが。

*11:異常座標に飛ぶバグはガイバラバグと呼ばれていて、元々はガイバラ先生というキャラクターに関わるイベント内で初めて観測された(これには階段取得は必要なかった)のですが、現在では階段取得によりガイバラ先生に関係なく頻繁に起こされます

*12:index out of rangeした時点で止まればよかったのに・・・そんな機能はついていませんでした。

*13:この(0,0)周辺のバグ空間で行動する実験はフリーズの嵐に見舞われたそうです

*14:異常座標メモリ改ざんは2018/12に発表され、フラグマイニングという名称がつけられました