してログ

ROM チェックエラー
ROM チェックエラー

今まで ROM チェックを無視して結果判定のところだけ NG に分岐しないよう改造してたんですけど、それをちゃんとチェックが通るようにしよう、ということで重い腰を上げてみた。

ROM データの最終バイトにチェックサム調整の数値が書き込まれているのは気づいていたので、恐らく総和の下位8ビットがゼロになるよう調整してあるんだろうと察しはついていました。実際に計算してみるとその通りだったので、チェックサムを計算してこの調整バイトに書き込んで終わり、と思ったのになぜかチェックが通らない。調べてみると、チェックサムを計算した計算結果が並ぶメモリがあって、そこの恐らく XVI6 に当たる部分がゼロになっていませんでした。でも、こちらのエクセルの計算結果はゼロになります。

プログラムを追ってようやく分かったのは、なんとサブ CPU にマップされる ROM はチェックサムを計算していない、ということです。データの中でたまたまゼロになっている1バイトのアドレスを、ただ計算結果のメモリにコピーしてるだけ! そんなの有りですか? 時間を返して欲しいです。

だから、プログラム内のたまたまゼロ値であるアドレスは変わりようがないので、XVI5 についてはチェックサムは関係なくてもパスします。しかし、出現テーブルの XVI6 は改変したら大きくずれて参照先アドレスがゼロ値でなくなり、チェックサムが合っていても ROM エラーとなります。しかも、ROM 5 という表示でいかにも XVI5 がエラーというふうなのですが、XVI6 がエラーというトラップもありました。

散々苦労しましたが、最初の ROM チェックを無視するというコードが正解でした。同時にプログラムの ROM を改造する場合は、最終バイトまで心置きなく使っても大丈夫なことが判明しました。たぶん、サブ CPU のほうはメンドクサイからそうしたのか、チェック完了のタイミングが CPU 同士で取るのが大変だから、とかそういう理由でしょうね。

ちなみに、エリア8の中盤で右側道路に3台のドモグラムが下りてくるシーンがあると思いますが、そのうち真ん中の1台のデータがそれです。このドモグラムを弄るか、この前のエリアに何か追加してずれてしまうと ROM エラーになります。

再現できたガル・バキュラ
再現できたガル・バキュラ

デモプレイに画面が切り替わる瞬間にクレジットを投入した際、ソルバルウなどが画面に残った状態で PUSH START BUTTON の画面になることが、このバグが成立した状態になります。ただし、状況によっては目立った変化が無い動画もあります、何れにしてもこれが成立していれば、スタート直後に横一列にバキュラが並んだ状態で出現します。こちらのページにそのリプレイファイルがあったので、今回はこのバグが成立する条件を探ってみました。

まず、大量のバキュラが出る原因はすぐに分かりました。バキュラのキャラクターコードが 0x01 で、これが地上物、空中物、バキュラ、スパリオなどのワークメモリすべて埋め尽くされるために起きています。これは総攻撃などと同じ原理で、どのワークに入れても指定したコードのキャラクタが出現するのと一緒です。そのため、空中物に入れられたバキュラは破壊可能で、得点テーブルも 0x01 で埋められているので対応した得点が加算されます。

問題はなぜこのような現象(0x01 で埋め尽くされる)が起きるかですが、やはりメインとサブの CPU が関係していました。画面モードが切り替わった直後にメイン CPU がワークメモリを 0x00 でクリアしています。原因はそのゼロクリアのし方が下記のようになっているためです。

該当箇所のコード(XVI4 の先頭から)
  1. クリアする先頭アドレスに 0x00 を書き込む
  2. ブロック転送命令で1バイト前のアドレスの値を指定回数コピーする
ld   hl,$7900
ld   de,$7901
ld   bc,$06FF
ld   (hl),$00
ldir

このコードはシングル CPU であれば問題は起こりませんが、このブロック転送の最中にサブ CPU が介入してキャラクタの状態を表すワーク領域に 0x01 を書き込むと、意図しない値がコピーされてしまいます。具体的には、ひとつ前のアドレスをメイン CPU が読み出す直前に、サブ CPU がそのアドレスに 0x01 を書き込んだ場合です。本来は先頭に書き込んだ 0x00 がバケツリレーのようにコピーされるはずが、途中から 0x01 に差し替えられてしまい、以降の領域のすべてが 0x01 で埋め尽くされることによります。なお、画面にスプライトが残る残らないの違いは、どの場所をクリアできてどの場所がクリアできなかったかの違いで起きていると思われます。

この条件が成立するのはかなりシビアであり、狙って出すのはかなり難しいと考えます。それはデモ画面に切り替わった直後『サブ CPU がキャラクタの状態のワーク領域に 0x01 を書き込む直前』にクレジットを投入する、というタイミングと思われますが、クロック単位のシビアなタイミングが予想されます。

状況の再現

ロジックが分かったので再現して確かめてみました。前述のコードの先頭に書き込む 0x00 を 0x01 に変更しただけで再現できました。ただし、強制的にやっている関係でミスしても元に戻りません。

ld   hl,$7900
ld   de,$7901
ld   bc,$06FF
ld   (hl),$01
ldir

ゼビウスの他のバグと根っこは同じ

ゼビウスってこれ系のバグが多いですよね。エリア繰り上げワープ然り、森の地上物さん然り、CPU 間でメモリを共有しているにも関わらず、タイミングが取れてないから意図しないデータ参照や書き込みが起きてしまう。もしかしたらまだまだレアな怪奇現象が起こるかも知れません。

古い MAME をビルドする必要があったので調べた結果を共有しておきます。最近のバージョンはデバッガが使える状態になっていますが、古いバージョンはデバッガをオンにしてビルドしないと使えないためその必要がありました。ただ、あまりに古いのでビルドツールを見つけるのに苦労して探し回った結果、下記の掲示板にまとめがありましたのでリンクを張っておきます。

今回は 0.93 をビルドしたいので "MinGW-3.2.0-rc-3.exe + mingw-over-092.zip (0.92 - 0.104u1)" のリンクより2つダウンロードしました。作業用の WindowsXP にそれらをコピーしインストールします(Windows10 でビルドできるかは試していません)。

ビルド手順

※バックスラッシュは小文字の¥マークです

  1. MinGW-3.2.0-rc-3.exe を実行し解凍先を指定します(今回は C:\mame\MinGW にしました)
  2. mingw-over-092.zip を解凍して C:\mame\MinGW に上書きコピーします(ディレクトリ構成そのままコピーすればOK)
  3. mame のソースをダウンロードして C:\mame\mame093s に解凍しておきます
  4. ファイル名を指定して実行で cmd を入力し OK ボタン(コマンドプロンプトが開きます)
  5. カレントディレクトリを変更します
    cd \mame\mame093s
  6. ビルドツールへのパスを通します(何度も使う場合はバッチファイルにします)
    set PATH=C:\mame\MinGW\bin;C:\mame\MinGW\mingw32\bin;%PATH%
  7. ビルドを実行します
    make

デバッガをオンにしてビルドする

MAME のソースコードのルートにある makefile をテキストエディタで開き、DEBUG=1 のコメントアウトを取って(行頭の # を取る)ビルドするだけです。ビルドが完了すると mame.exe と別に mamed.exe ができ、こちらがデバッガ内臓 MAME になっています。デバッガをオンにして起動するには、下記のようにオプションを指定します。

mamed.exe -window -debug [ROMNAME]

試したバージョン(0.93)ではエラーで落ちる

試したバージョン MAME0.93 ではエラーで落ちる事象が見られました。エミュレーション動作中にデバッガのコマンドを実行すると ACCESS VIOLATION で落ちます。このバージョンだけなのか、その他何が原因なのかは分かりません。

ただし、起動直後の停止状態のときであれば、デバッガのコマンド設定が通るのでそこでブレークポイント等を設定してなんとか使えています。最新の MAME で適当なブレークポイントを探しておき、0.93 の開始直後に設定して止まったところで、ステップ実行その他のコマンドを実行するという、非常に面倒な状況で使うしかありません。一応止まった状態からは、trace、dasm など使っても落ちません。

OpenLayers2 と OpenLayers3 以降とではだいぶ構成が違うので慣れるまでが大変です。

イベントハンドラの代替案2つ

Feature にはクリックイベントが無いようです。似たような動作をしたい場合は、マップオブジェクトのクリックイベントで行えます。以下の例では、クリックした座標から forEachFeatureAtPixels を使ってヒットした Feature をすべてコンソールに出力しています。

map.on('click',function(e){
	map.forEachFeatureAtPixel(e.pixel,function(feature,layer){
		console.log(feature);
	})
});

セレクトツール(インタラクションというらしいが)で良ければ、下記のようなコードでも実現できます。こちらの場合、選択時のスタイルも設定しておくか、選択されたら即解除するような処理を入れておかないとおかしな動作に見えると思います。

var selectSingleClick = new ol.interaction.Select();
map.addInteraction(selectSingleClick);
selectSingleClick.on('select',function(e) {
	console.log(e.target.getFeatures().getLength());
});

ポップアップ(吹き出し)の作り方

OpenLayers2 のときあった OpenLayers.Popup.FramedCloud も無いみたいです。CSS で吹き出し形状にするのって、after などの疑似要素やボーダーの変態活用が絡むのであまりやりたくない。標準のポップアップくらい用意しておいてくれよと思います。

枠だけで良いなら下記のように jQuery でスタイルも埋め込んでやれば CSS 要りません。ただし地図においては、三角形がないとどこを示しているポップアップなのか分からなくなるので、素直に CSS に定義したほうが良さそうです。

popup_container =
	$('<div>')
	.css({
		'position':'absolute',
		'background-color':'white',
		'box-shadow':'0 1px 4px rgba(0,0,0,0.2)',
		'padding':'15px',
		'border-radius':'10px',
		'border':'1px solid #cccccc',
		'bottom':'12px',
		'left':'-50px',
		'min-width':'280px'
	})
	.append($('<div>',{class:'pp_content'}));

popup_overlay = new ol.Overlay({
	element: popup_container[0],
	autoPan: true,
	autoPanAnimation: {
		duration: 250
	}
});

マップのクリックイベントハンドラを下記のように書けばポップアップを表示できます。

map.on('click',function(e){
	map.forEachFeatureAtPixel(e.pixel,function(feature,layer){
		var geom = feature.getGeometry();
		var coord = geom.getCoordinates();
		popup_overlay.setPosition(coord);
		var obj = $(popup_overlay.element);
		obj.find('.pp_content').text(feature.getProperties()...);
	})
});

ポップアップを閉じるには下記のようにします。

popup_overlay.setPosition(undefined);

[ゼビウス] AC版ゼビウスに総攻撃がプログラムされていた (4) からのつづき)

今回はロマン溢れる記事から一転、現実解的な内容になりますので、よろしくお願いします。インパクトある物量攻撃が起こる原因について少し見て行きたいと思います。

複合攻撃の仕組み

空中物が出現するとワークメモリに、出現機数とキャラクタ出現テーブルの位置がロードされます。例えば弾なしトーロイドを3機出現させる場合「0x03 0x01」になります。同時出現は6機までなので、1バイト目は 0x01~0x06 の範囲、2バイト目は 0x01~0x73 くらいの値を取ります。

キャラクタ出現テーブルは表のように定義されており、先の例だと1番目から6機分を出現させる指定になります。複合攻撃にしたい場合は、キャラクタの切り替わる境界を使うことで実現しています。例えば、弾なしトーロイド×3+弾ありトーロイド×3を出したい場合「0x06 0x04」と指定すれば良いことになります。

おなじみの、ジアラ×2+タルケン×3~4の組み合わせもジアラとタルケンが切り替わる境界のところで、「0x05 0x17」か「0x06 0x17」と指定します。機数の違いのみでタルケンを3機にしたり4機にしたりできる、とても巧妙な作りです。

+0+1+2+3+4+5
0x01トーロイド(弾なし)トーロイド(弾なし)トーロイド(弾なし)トーロイド(弾なし)トーロイド(弾なし)トーロイド(弾なし)
0x07トーロイド(弾あり)トーロイド(弾あり)トーロイド(弾あり)トーロイド(弾あり)トーロイド(弾あり)トーロイド(弾あり)
0x0Dジアラ(弾あり)ジアラ(弾あり)ジアラ(弾あり)ジアラ(弾あり)ジアラ(弾あり)ジアラ(弾あり)
0x13ジアラ(弾なし)ジアラ(弾なし)ジアラ(弾なし)ジアラ(弾なし)ジアラ(弾なし)ジアラ(弾なし)
0x19タルケンタルケンタルケンタルケンタルケンタルケン
出現テーブルの冒頭部分

総攻撃はバッファオーバーランか

出現テーブルに従って機数分の空中物ワークへキャラクタが転送されると、キャラクタに対応するプログラムが紐づいて攻撃が始まります。また同様に、地上物は地上物用ワーク、バキュラはバキュラ用ワークを持っていて、出現テーブルに従ってセットされたりクリアされたりしながらエリアが進行していきます。

これらのワークはメモリ上で隣接しているため、例えばボザログラム(ワークを5個使用する)を11番目以降(地上物のワークは14個まで)に設定してしまうとバキュラ用ワークにはみ出します。それぞれのワーク領域では、自機に当たる・当たらない、ザッパーに当たる・当たらない、ブラスターに当たる・当たらない、の属性になっているので、このボザログラムはバキュラバリアを装備し、かつ自機との当たり判定も持つことになります。

ここで面白いのは、本来とは異なるワーク領域へキャラクタをロードさせてもそのキャラクタのプログラム(動き)はそのままな点です。左記のボザログラムはスクロールに同期して移動し、見た目上は地上物としてふるまいます。

総攻撃に話を戻しましょう。今までの前置きを踏まえると「空中物のワーク領域からオーバーランさせて、地上物やバキュラのワーク領域にロードさせた状態なのでは無いのか」という話が見えてきます。

実際に検証してみます

1番目のトーロイドを起点に実際にオーバーランさせて検証してみたいと思います。敵出現テーブルのメモリにブレークポイント入れて、読みだされた後の動きを追ってみると、XVI5 の 0x04D8~0x054B あたりまでが空中物の組み合わせを定義しているテーブルと分かりました(複合攻撃のところで書いた2バイト単位が羅列されています)。

このテーブルの機数のところを書き換えてオーバーランを起こさせます。しかし、適当に数を増やしてみても6機以上は出て来ず、極端に増やすとリセットが掛かりました。見当が外れたのかと思いましたが、71機以上設定すると地上物のワークを使い始めると判明。トーロイド起点だとカピ(71機目)からブラグザカート(87機目)までが地上物扱いになりました。

これ以降109機くらいまではバキュラのワークに配置され、バキュラバリアが付いた状態になります。

更に増やすとスパリオのワークに行くらしく、幻影弾が出るようになります。これ以上増やすとキャラクタのテーブルも終わるため、リセットや halt してしまいます。

0xA2、0xB2、0xA3 では何が起きているか

理由は分かりませんが上のキャラテーブルの続きではなく、XVI5 の 0x03D8~0x04D7 を参照していました。実は 0x80 以降の攻撃パターンは、0xE1 のジアラ+ギドスパリオぐらいしか使用されておらず、なぜこうなっているのか良くわかっていません。キャラテーブルに空きもあるし入れられると思うですけどね、謎です。

それぞれの参照先で何が定義されているか調べてみると下記の通りでした。

攻撃コード参照先(XVI5)定義(機数・起点キャラ)
0xA20x041C0xA7 0xED
0xB20x043C0xC1 0xC9
0xA30x041E0x52 0x19

うーむ。上2つは想像してたのとぜんぜん違いますね。これだとリセットコースなんですが、さっき書き換えてテストしていた部分に入れてみると A2, B2 と同じ攻撃が出て来ました。この A2 の定義だと範囲外のキャラを167機出現させるという指定であり、リセットが掛かってもおかしくありません。これでよく動いてるな、というのが心証です。

追記:0x80~0xFF までの参照で実際有効なのは 0xE0~0xFF(XVI5 の 0x0498~0x04D7)です。 残念ながらプログラムコードの一部を拾ってしてしまい、バグって総攻撃に至っているというのが事の真相のようです。

残りの A3 については、25番目(0x19)のタルケンを指定して82機出現させると言った意味になります。関連するキャラテーブルを書き出すと下記の通り。

+0+1+2+3+4+5
0x19タルケンタルケンタルケンタルケンタルケンタルケン
0x1Fゾシー(乱)ゾシー(乱)ゾシー(乱)ゾシー(乱)ゾシー(乱)ゾシー(乱)
0x25トーロイド(弾)トーロイド(弾)ギドスパリオギドスパリオギドスパリオギドスパリオ
0x2Bギドスパリオギドスパリオゾシー(追撃)ゾシー(追撃)ゾシー(追撃)ゾシー(追撃)
0x31ゾシー(追撃)バックゾシーバックゾシーバックゾシーバックゾシーザカート(落下)
0x37ザカート(落下)ザカート(落下)ザカート(落下)ザカート(落下)ザカート(落下)ザカート(自狙)
0x3Dザカート(自狙)ザカート(自狙)ザカート(自狙)ザカート(自狙)ザカート(自狙)バックゾシー
0x43バックゾシーバックゾシーカピカピカピカピ
0x49カピカピザカート(自狙)ザカート(自狙)ザカート(自狙)テラジ
0x4Fテラジテラジテラジテラジテラジブラグザカート(自狙)
0x55ブラグザカート(自狙)ブラグザカート(自狙)ブラグザカート(自狙)ブラグザカート(自狙)ブラグザカート(自狙)ブラグザカート(自狙)
0x5Bブラグザカート(自狙)タルケンタルケンギドスパリオギドスパリオギドスパリオ
0x61ギドスパリオジアラ(弾)ジアラ(弾)カピカピギドスパリオ
0x67ギドスパリオギドスパリオギドスパリオテラジテラジザカート(自狙)

青字が通常の空中物として出ているタルケン6機、赤字が地上物扱いで出ているものになります。正直これ以外はメモリのゴミか無関係な参照かも知れませんが、こちらはキャラクタの区切りにきっちり設定してあり、キャラテーブル内の機数で設定され、かつ地上物ワークの範囲内に入れてある、という点を取って意図を感じてしまいます。

追記:地上物ワークまで間が空くのは、<地上物>~<バキュラ>~<スパリオ>~<空中物>~<未使用>の順に配置されているためでした(2バイト単位なので128機(うち有効なのが最大54機)で1周しています)。 細かく書くとバキュラとスパリオの間も、自機や照準など7機分の空きがあるようです。

結びに

果たして、意図したものなのか、単に無意味な参照だったのか、判断はお任せします。個人的にはロマンあるほうを主張しておきたいですね。

最後に、すごくインパクトのある発見で久しぶりにゼビウスで楽しめましたし、ここ最近プレイもして久方ぶりに1000万点を出しました。あと、消失ソルを1バイトで出す方法(今までは2バイトだった)とか、複合攻撃のコントロール方法、その他改造に役立つ知見が得られました。消失ソルの件は時間が出来たら記事にしますので、お楽しみに。

[ゼビウス] AC版ゼビウスに総攻撃がプログラムされていた (6) につづく)

PHP + JavaScript でシステムを組む場合、PHP 側の設定値を JavaScript 側に渡したいときがあります。いくつも方法があり簡単ではありますが、どうもスマートでないやり方になってしまうのでどうにかしたいと思っていました。

代表的な方法は、テンプレートエンジンに埋め込む(JavaScript を埋め込むか、input タグに入れる、class 属性などに突っ込む)、ajax で json を取得させるなどがあります。前者はカッコ悪いの一言。後者は非同期通信になってロード完了するまで、それらの値にアクセスできないなどイマイチです。そこで少し検討してみました。

JavaScript を吐き出す PHP を書いてしまう

JavaScript のコードを吐き出す PHP を書いて、ヘッダーの script タグでロードするという方法です。これならテンプレートが複雑にならないし、ロードのタイミングで悩むこともありません。ただし、PHP 側のコードが汚くなるのが目に見えるので、もう少し検討してみました。

せっかくなので JSON を受け渡しに使いたい

PHP 側で JSON を作り JavaScript 側は ajax で受け取る、この流れ少し変えて JSON データをデコードするだけの JavaScript を書いてロードしてもらう、というふうにします。

  1. PHP で受け渡したいデータを配列などにまとめる
  2. データを JSON にエンコードして base64 エンコードする(エスケープしなくて良くするため)
  3. JavaScript で base64 → JSON の順でデコードするコードに加工する
  4. JavaScript として返却する

このような流れでコードを書けば、PHP、JavaScript でコードが入り乱れることは無くなり非常にすっきりすると思います。簡単なのでコードを例示する必要も無いかと思いますが、一応書いておきます。

PHP 側のコード($data に受け渡すものは準備済みとする)
$json = json_encode($data);
$b64 = base64_encode($json);
$js = "var data=JSON.parse(window.atob('".$b64."'));";
header('Content-Type: text/javascript; charset=UTF-8');
echo $js;
HTML(JavaScript)側のコード
<script type="text/javascript" src="config.js.php"></script>