パチ夫くん2 (FC) 攻略/解析

パスワード

本作は 20 文字のパスワードを用いてゲーム状態のセーブ/ロードを行う。パスワード 1 文字あたりの情報量は 6 ビットで、全体の情報量は 120 ビットとなる。

パスワードは RAM $07B4-$07C7 に置かれる。ゲーム状態とパスワードの相互変換は以下のルーチンで行われる:

アドレス内容
$FBD4ゲーム状態をパスワードにセーブする (パスワードへのエンコード処理は呼び出し元で行う)。
$FBE3ゲーム状態をパスワードからロードする。

筆者作のパスワードライブラリ pachio2_password も適宜参照されたい (パスワードのセーブ/ロードおよび最短パスワードの探索を実装している)。

ゲーム状態

パスワードには以下の情報が記録される ("bits" はパスワード内での情報量):

識別子アドレスbits内容
stage $D0 4 現在の面 (通常は 1〜10)。
life $D1 4 残機 (プレイ中の自機を含まない)。
score $D2-$D4 24 得点 (6 桁 packed BCD, リトルエンディアン)。
hand_ball $D5-$D7 24 持ち玉 (6 桁 packed BCD, リトルエンディアン)。
stage_win $D8 8 現在の面における打ち止め回数。
event_flags $078F-$0793 40 イベントフラグ集合 (bitset, バイト内のビットは LSB-first)。
なお、$0793 は起動時/ニューゲーム開始時に初期化されないため、未初期化値が読まれうる。
check 8 チェックサム。
key 8 暗号鍵。セーブ時は 0〜0xFF の乱数となる。

パスワード文字

パスワードとして入力可能な文字は全部で 76 種あるが、正規のパスワード内に現れる文字は 64 種のみである。これらを「正規文字」、残りの 12 種を「非正規文字」と呼ぶ。

パスワード文字コード表 (RAM $07B4-$07C7 上の値) を以下に示す。非正規文字については斜体で表示している。なお、"SP" は空白 (0x20) を意味するが、パスワード入力画面の仕様上、内部的にはハイフン (0xB0) として扱われる:

x0x1x2x3x4x5x6x7x8x9xAxBxCxDxExF
6x
7x
8x
9x
Ax
BxSP
Cx
Dx

パスワードへのセーブ処理

パスワードに記録すべき情報を格納したバイト列を「セーブデータ」と呼ぶ。

ゲーム状態は以下のステップを経てパスワードに変換される:

ゲーム状態から 8-bit 形式セーブデータへの変換

8-bit 形式セーブデータを格納する長さ 15 のバイト列を save8 とおく。

まず save8 にゲーム状態、チェックサム check、暗号鍵 keyを以下のように格納する:

オフセット内容
0score
3hand_ball
6stage_win
7bit0-3:life, bit4-7:stage
8event_flags
13check
14key

ここで、checksave8[0..=12]u8 総和、key は 0〜0xFF の乱数である。

次に、save8 に対するスクランブル処理を行う。スクランブル処理は save8[0..=13] に対して固定のバイト列を XOR するもので、たとえば Rust では以下のように書ける。なお、これは対称な操作 (2 回行うと元に戻る) なので、逆変換時も全く同じことを行えばよい:

fn scramble(bytes: &mut [u8; 15]) {
    const XOR_ARRAY: [u8; 14] = [
        0x99, 0xBB, 0x07, 0x20, 0x72, 0x62, 0x85, 0x70, 0x5B, 0x61,
        0x99, 0xBB, 0x07, 0x20,
    ];

    for (b, xor) in bytes[..14].iter_mut().zip(XOR_ARRAY) {
        *b ^= xor;
    }
}

次に、save8key を用いて暗号化し、その結果が 8-bit 形式セーブデータとなる。暗号化処理は単に save8[0..=13] 全体をビット列とみなして key 回右ローテートするだけである (save8 のバイト内のビットは MSB-first, 以下同様)。

8-bit 形式セーブデータから 6-bit 形式セーブデータへの変換

6-bit 形式セーブデータを格納する長さ 20 のバイト列を save6 とおく。

save8 全体をビット列とみなし、先頭から 6 ビット単位で 20 回取り出し、それらを save6 内の各バイトに格納する。この結果が 6-bit 形式セーブデータとなる。

6-bit 形式セーブデータからパスワードへのエンコード

6-bit 形式セーブデータ内の各バイト (値は 0..=0x3F) を以下の規則で正規文字に変換したものがパスワードとなる:

x0x1x2x3x4x5x6x7x8x9xAxBxCxDxExF
0x
1x
2x
3x

なお、ここまでの処理から推測されるように、暗号鍵が同じならば似た状態に対しては似たパスワードが生成される (ただし暗号鍵は 256 通りあるので、通常プレイでこれが観察されることは少ないと思われる)。

パスワードからのロード処理

ロード処理は基本的にはセーブ処理の逆を行うだけである。つまり、パスワードは以下のステップを経てゲーム状態に変換される:

パスワードから 6-bit 形式セーブデータへのデコード

まず save6 にパスワードの文字コードが格納された状態から始める。エンコードの逆変換を行うことで save6 の先頭から順に文字をバイトに変換していくが、途中で非正規文字が現れた場合、その文字以降については変換を行わず文字コードをそのままバイトとして扱う。この結果が 6-bit 形式セーブデータとなる (各バイトの上位 2 ビットは無視される)。

6-bit 形式セーブデータから 8-bit 形式セーブデータへの変換

セーブ時の処理の逆を行うだけである。

8-bit 形式セーブデータからゲーム状態への変換

基本的にはセーブ時の処理の逆を行うだけである。チェックサムが正しくない場合、不正なパスワードとみなされてロードは失敗する。

なお、ゲーム状態に関する値域チェックは行われないので、不正な値をロードさせることもできる。たとえば、面を 1〜10 の範囲外にすると画面がバグる。

最短パスワード

ロード可能なパスワードは最短で 2 文字 (3 文字目以降は空白) であり、全部で 20 種存在する。一覧を以下に示す (イベントフラグ以外のゲーム状態は全て同じである)。なお、暗号鍵は全て 0x30 である (デコード処理を参照):

パスワード stage life score hand_ball stage_win event_flags
げが7120x37B75A0x527EE370[0x2A, 0xFD, 0xA9, 0x78, 0x0B]
ずぎ7120x37B75A0x527EE370[0x5A, 0xCD, 0xA9, 0x78, 0x0B]
ぢゆ7120x37B75A0x527EE370[0xEA, 0x3D, 0xA9, 0x78, 0x0B]
でぐ7120x37B75A0x527EE370[0x4A, 0xDD, 0xA9, 0x78, 0x0B]
ぶげ7120x37B75A0x527EE370[0x7A, 0xAD, 0xA9, 0x78, 0x0B]
ぴご7120x37B75A0x527EE370[0x6A, 0xBD, 0xA9, 0x78, 0x0B]
ぴん7120x37B75A0x527EE370[0x6A, 0xBD, 0xA9, 0x78, 0x0B]
ゃゆ7120x37B75A0x527EE370[0xEA, 0x3D, 0xA9, 0x78, 0x0B]
あざ7120x37B75A0x527EE370[0x9A, 0x8D, 0xA9, 0x78, 0x0B]
かじ7120x37B75A0x527EE370[0x8A, 0x9D, 0xA9, 0x78, 0x0B]
こや7120x37B75A0x527EE370[0xBA, 0x6D, 0xA9, 0x78, 0x0B]
せゆ7120x37B75A0x527EE370[0xAA, 0x7D, 0xA9, 0x78, 0x0B]
つよ7120x37B75A0x527EE370[0xDA, 0x4D, 0xA9, 0x78, 0x0B]
にら7120x37B75A0x527EE370[0xCA, 0x5D, 0xA9, 0x78, 0x0B]
ねわ7120x37B75A0x527EE370[0x7A, 0xAD, 0xA9, 0x78, 0x0B]
ふり7120x37B75A0x527EE370[0xFA, 0x2D, 0xA9, 0x78, 0x0B]
みる7120x37B75A0x527EE370[0xEA, 0x3D, 0xA9, 0x78, 0x0B]
やれ7120x37B75A0x527EE370[0x1A, 0x0D, 0xA9, 0x78, 0x0B]
りろ7120x37B75A0x527EE370[0x0A, 0x1D, 0xA9, 0x78, 0x0B]
わわ7120x37B75A0x527EE370[0x3A, 0xED, 0xA9, 0x78, 0x0B]