サムネは魔除けです。
この記事はPokémon RNG Advent Calendar 2020 6日目の記事です。
エメラルドID調整の手順そのものについては数ヶ月前に上げているので、調査の詳細に興味のある方向けの記事となります。
まあ調査記録といっても当時の記憶の書き起こしだしそこまで厳密には纏めてないけどね。
めんどくさいので画像も特にないです。
すごく読みづらいと思いますが何卒ご容赦下さい。
それと調査にあたってROMHackやマジコンを利用している為、苦手な方はブラウザバック推奨です。
また、筆者は乱数調整にあまり詳しい人間ではないので細かい所で「ん?」と思う所があるかと思いますがどうか生温かい目で見てあげてください。
文句言われてもスルーするので言うだけ無駄です。
概要
ポケットモンスターエメラルド自体は16年程前に発売されたそれなりに古いゲームで、殆どの要素において乱数調整方法が確立されていたが、トレーナーIDの決定に関しては1年くらい前まで調整不可能とされていた。
この記事では、不可能とされていた理由やその後調整できるようになった経緯等について適当に解説する。(正しく解説できる自信はない)
解説
まずはエメラルドのトレーナーID決定処理について。
// 名前入力開始 static void NameInStart(u8 type, u8* buff, u16 work0, u16 work1, u32 work2, pFunc rec_proc) { Namein = (NAME_IN*)AllocMemory(sizeof(NAME_IN)); if (Namein == NULL){ MainProcChange(rec_proc); return; } Namein->name_type = type; Namein->arg_work0 = work0; Namein->arg_work1 = work1; Namein->arg_work2 = work2; Namein->set_name_buf = buff; Namein->rec_proc = rec_proc; // 主人公名の決定時間を乱数のタネとする if(type == NAMEIN_HERO) { RandomTimerStart(); } MainProcChange(InitNameInTask); } // 名前入力終了 static u8 NameInEnd(void) { if (FadeData.fade_sw == 0){ // 時間計測終了 if (Namein->name_type == NAMEIN_HERO) { RandomTimerEnd(); } MainProcChange(Namein->rec_proc); DelTask(CheckTaskNo(NameInMainTask)); BMPWIN_SysExit(); MEM_RELEASE(Namein); } return 0; }
名前入力画面への遷移時にタイマーを開始し、名前決定後に経過時間をそのままトレーナーIDとして使用するという非常に単純な処理である。
ちなみにこの処理はFRLGでも使用されている。
「入力に掛かった時間がTIDになるならその入力時間を調整すれば任意のトレーナーIDが引けるのでは?」と思うかもしれないがこの「入力時間の調整」がとても難しい。
エメラルドの実行環境であるゲームボーイアドバンスはマルチスレッドに対応していない為、裏で何かしらのタスクを処理したい場合は割り込み処理を利用して実装することが多い。
割り込み処理の実行中はメインタスクの処理が停止するが、その前にセットされたハードウェアタイマーはそのまま動き続ける。
これが「入力時間の調整」を難しくしている原因となる。
この時裏で割り込み処理によって動いている処理とは何か?
BGMの再生処理である。
ゲームボーイアドバンスには先代のゲームボーイが持っていたチャンネルに加えて8bitのPCMが追加されており、サンプリングされた音源をそのまま再生できるようになっている。
これによってソフトウェアによって生成された合成音声を再生し、擬似的に同時発音数を増やすことに成功している。
ちなみにポケットモンスターエメラルドにおいて使用されているサウンドドライバは任天堂純正のMusicPlayer2000である。(Sappyとも呼ばれる。こちらの方が親しみのある名前だろうか?)
この「ソフトウェアによる音声合成」が中々に曲者で、BGMの再生位置によって合成するデータ量が大きく変動する為、処理に要するCPUサイクル数にムラが生じやすい。
ハードウェアタイマーのスタート/ストップの起点となるVBLANK割り込みから当該処理までのステップ数は一定だが、途中で割り込んでくる音声合成処理によって経過時間が変動してしまうのである。
しかし、逆に言えばBGMの再生位置と経過時間の管理によってトレーナーIDの調整が可能である、ということになる。
理論的な説明は以上の通り。後は全て力技で解決します。
ここまで分かったら「BGMの再生位置(OP開始からの経過時間)」と「名前入力画面内での経過時間」を渡してIDを概算できるようにしたい。
今回はハードウェアタイマーのスタート/ストップ処理のそれぞれに対応した「BGMの再生位置」と「処理実行までのクロック数」を紐付けたテーブルを作成する。
まずはタイマーストップ処理用のテーブル作成手順を紹介する。
BGMの再生処理を潰したROMを用意し、1フレームずつ名前入力の経過時間をズラしながら出現するトレーナーIDを確認する。
すると、1フレームにつき18752値が増えていた。
16777216(GBAのCPUクロック数) / 59.7275(リフレッシュレート) = 280896.002....
280896 % 65536 = 18752
なので、計算上でもこの結果はまあまあ正しそう。
後は未改造ROMで取得できたトレーナーIDと比較して音声合成処理によって生じたサイクル数のズレを記録していく。
この時、名前入力画面へ遷移する際のBGMの再生位置は固定するようにする。
次はタイマースタート処理用のテーブル作成手順。
名前入力画面遷移後にBGM再生処理が停止するようHackを施したROMを用意し、名前入力画面へ遷移する際のBGM再生位置を1フレームずつズラしながら出現するトレーナーIDを確認する。
この時、名前入力画面へ遷移した後の経過時間は固定するようにする。
その後、BGMの再生処理を潰したROMで同じ操作を行い、トレーナーIDの差分を取る。
この差分がタイマースタート処理実行までに発生した音声合成処理によるズレとなる。
後は記録した差分を2つ合わせた値と実際の値のズレを基準値とし、[基準値 + タイマースタートのズレ + タイマーストップのズレ = ID]が成り立つようにする。
かなりの荒技になってしまうが、一応これでエメラルドのIDの予測ができるようになった。
最後に
以上がポケットモンスターエメラルドのID調整を行うにあたっての調査内容です。
少し頭のいい方なら「もう少しスマートなやり方あるだろ!アレをソレしてこうして...」とツッコミたくなってしまうと思いますが、突っ込まれた所で基本スルーするつもりなので心の内にしまっておいて頂けると幸いです。
明日はpo氏が何か書くそうです。
当記事執筆時点では「何も考えていません。」とのことですが何が上がってくるんでしょう?
楽しみにしていますね。