初代ポケモンで任意コードを常に実行する方法

pokemon jump ゲーム

初代ポケモンにバイナリエディタを導入したので、Bボタンでジャンプする機能を実装してみました。
アセンブリ言語はすぐ忘れそうなので書き残しておきます。

何らかの手段でバイナリエディタを導入している前提です。
赤の後期ROMで検証しています。

Bボタンでジャンプするコード

アドレスD6B5に40をセットすると、段差を飛び越えるモーションが出ます。

ということは、

  1. Bボタンが押されたらD6B5に40をセットするコードを空き領域に書き込む
  2. どうにかして毎フレーム実行させる

この2点をクリアできれば、目標を達成できそうです。

D6B5の挙動についてはこちらのサイトを参考にしました。

最初に、Bボタンが押されたらD6B5に40をセットするコードを考えました。

ld  a  ,(FFB1) ; F0 B1
ld  b  , 02    ; 06 02
and b          ; A0
jr  z  , +06   ; 28 06
ld  a  , 40    ; 3E 40
ld  bc , D6B5  ; 01 B5 D6
ld (bc), a     ; 02
ret            ; C9

FFB1には前フレームで入力されたキーの情報が入っています。
FFB2からFFB4もキー入力に関するアドレスのようです。

下記の情報は海外版のものですが、日本版でも同一と思われます。

FFB1 – Joypad input during previous frame
FFB2 – Released buttons on this frame
FFB3 – Pressed buttons on this frame
FFB4 – Held buttons on this frame

引用元:Pokémon Red/Blue:RAM map

キーとビットは [↓, ↑, ←, →, Start, Select, B, A] のように対応しているため、
Bボタンが押されているかはFFB1の値を02とand演算してゼロフラグを使うことで分岐できますね。

押されていた場合のみ、D6B5に40をセットする命令が書けました。

毎フレーム?任意コード実行(簡易版)

次に、任意コードを毎フレーム?実行させる方法を考えました。

厳密には毎フレームなのかわかりませんが、細かいことはハイドロポンプで流しました。

マップを切り替えると解除されてもよいなら、D2EDとD2EEに書き込んだコードの先頭アドレスを書き込むことで簡単に実現可能です。

バイナリエディタを導入している前提なので直接書き換えればいいのですが、コードにするとこうなります。

アドレスxxyyを毎フレーム実行するコード

ld  a  , yy    ; 3E yy
ld  bc , D2ED  ; 01 ED 02
ld (bc), a     ; 02
int bc         ; 03
ld  a  , xx    ; 3E xx
ld (bc), a     ; 02
ret            ; C9

しかし、この方法にはいくつかデメリットがあります。
詳しくはこちらのサイトを参照してほしいのですが、簡単にまとめると

  • 特定の座標に立つと発動する系のイベントが全滅する
  • マップを切り替えるとD2EDとD2EEの値が上書きされる
    (任意コードの毎フレーム実行が解除される)

こんな遊び方をしている時点でストーリーを楽しんでいる人はいないでしょうから、イベントが作動しないことは大したデメリットではないでしょう。

しかし、マップが切り替わる度に値をセットし直さないとジャンプできないのは不便です。

そこで、既存の処理を置き換えるのではなく、途中にコードを差し込む方法を検討しました。

毎フレーム任意コード実行(リセットするまで有効)

FF80からFFFEまでのHRAMと呼ばれる領域は、何をしているのか私にはまだ理解できていません。
しかし、FF80には毎フレーム飛んでくるらしいことはわかりました。

そこで、FF80にjp命令を上書きして任意コードの実行を行い、上書きしてしまった本来のコードも実行してから戻ってこれないか試しました。

jp命令には3バイト必要です。
FF80とFF82にはそれぞれ2バイトの命令が書き込まれているので、
jp命令とnopで4バイトを置き換えました。

ジャンプ先の任意コードは以下のようにしました。

  1. Bボタンが押されたらジャンプする命令
  2. FF80~FF83の本来の命令
  3. FF84へのjp命令

しかし、FF80~FF83の本来の命令が実行されるとほとんどの領域がFFで埋められるため、FF84へジャンプして戻ってくることができないことがわかりました。

FF80~FF83の本来の命令は、HRAMで行う必要がありそうです。
幸いなことに、HRAMにはnopで埋まっていて書き換えても問題なさそうな領域があるのでそこを活用することにしました。

改善した流れは

  1. FF80~FF83の値をFFF8~FFFBにコピー
  2. FFFC~FFFEにFF84へのjp命令をセット
  3. FF80~FF82に任意アドレスへのjp命令をセット
  4. FF83にnopをセット

です。

ジャンプ先の任意コードはFFF8へ戻ってくるようにします。

HRAMはゲームをリセットすると消去されるため、バイナリエディタで直接書き換えるのではなく、書き換えるためのコードをDA00~にでも書きこみましょう。

HRAMの命令はプログラムの1ループの中で一気に書き換えないとフリーズするという問題もあります。

機械語で書いていたのでニモニックはないですが、ほとんど同じ命令を繰り返しています。
GBZ80はブロック転送が使えないっぽいのが地味に面倒です。

// FFF8~FFFBに3E C3 E0 46を書き込む
3E 3E
01 F8 FF
02
03
3E C3
02
03
3E E0
02
03
3E 46
02
// FFFC~FFFEにFF84へのjp命令C3 84 FFを書き込む
03
3E C3
02
03
3E 84
02
03
3E FF
02
// FF80~FF82にxxyyへのjp命令を書き込む
3E C3
01 80 FF
02
03
3E yy
02
03
3E xx
02
// FF83にnop命令を書き込む
03
3E 00
02
C9

HRAM領域にコードの本体を書き込んでしまえば、実行後はボックスを切り替えても問題ないプログラムを書けるかもしれません。