本作は 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) として扱われる:
| x0 | x1 | x2 | x3 | x4 | x5 | x6 | x7 | x8 | x9 | xA | xB | xC | xD | xE | xF | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 6x | が | ぎ | ぐ | げ | ご | ざ | じ | ず | ぜ | ぞ | だ | ぢ | づ | で | ど | |
| 7x | ば | び | ぶ | べ | ぼ | ぱ | ぴ | ぷ | ぺ | ぽ | ||||||
| 8x | ||||||||||||||||
| 9x | ||||||||||||||||
| Ax | を | ゃ | ゅ | ょ | っ | |||||||||||
| Bx | SP | あ | い | う | え | お | か | き | く | け | こ | さ | し | す | せ | そ |
| Cx | た | ち | つ | て | と | な | に | ぬ | ね | の | は | ひ | ふ | へ | ほ | ま |
| Dx | み | む | め | も | や | ゆ | よ | ら | り | る | れ | ろ | わ | ん |
パスワードに記録すべき情報を格納したバイト列を「セーブデータ」と呼ぶ。
ゲーム状態は以下のステップを経てパスワードに変換される:
$07A5-$07B3 に置かれる。$07B4-$07C7 に置かれる。8-bit 形式セーブデータを格納する長さ 15 のバイト列を save8 とおく。
まず save8 にゲーム状態、チェックサム check、暗号鍵 keyを以下のように格納する:
| オフセット | 内容 |
|---|---|
| 0 | score |
| 3 | hand_ball |
| 6 | stage_win |
| 7 | bit0-3:life, bit4-7:stage |
| 8 | event_flags |
| 13 | check |
| 14 | key |
ここで、check は save8[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;
}
}
次に、save8 を key を用いて暗号化し、その結果が 8-bit 形式セーブデータとなる。暗号化処理は単に save8[0..=13] 全体をビット列とみなして key 回右ローテートするだけである (save8 のバイト内のビットは MSB-first, 以下同様)。
6-bit 形式セーブデータを格納する長さ 20 のバイト列を save6 とおく。
save8 全体をビット列とみなし、先頭から 6 ビット単位で 20 回取り出し、それらを save6 内の各バイトに格納する。この結果が 6-bit 形式セーブデータとなる。
6-bit 形式セーブデータ内の各バイト (値は 0..=0x3F) を以下の規則で正規文字に変換したものがパスワードとなる:
| x0 | x1 | x2 | x3 | x4 | x5 | x6 | x7 | x8 | x9 | xA | xB | xC | xD | xE | xF | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0x | ず | ぜ | ぞ | だ | で | ど | ば | び | ぶ | べ | ぼ | ぱ | ぴ | ぷ | ぺ | ぽ |
| 1x | や | ゆ | よ | ら | り | る | れ | ろ | わ | が | ぎ | ぐ | げ | ご | ざ | じ |
| 2x | つ | て | と | な | に | の | は | ひ | ふ | へ | ほ | ま | み | む | め | も |
| 3x | あ | い | う | え | か | き | く | け | こ | さ | し | す | せ | そ | た | ち |
なお、ここまでの処理から推測されるように、暗号鍵が同じならば似た状態に対しては似たパスワードが生成される (ただし暗号鍵は 256 通りあるので、通常プレイでこれが観察されることは少ないと思われる)。
ロード処理は基本的にはセーブ処理の逆を行うだけである。つまり、パスワードは以下のステップを経てゲーム状態に変換される:
まず save6 にパスワードの文字コードが格納された状態から始める。エンコードの逆変換を行うことで save6 の先頭から順に文字をバイトに変換していくが、途中で非正規文字が現れた場合、その文字以降については変換を行わず文字コードをそのままバイトとして扱う。この結果が 6-bit 形式セーブデータとなる (各バイトの上位 2 ビットは無視される)。
セーブ時の処理の逆を行うだけである。
基本的にはセーブ時の処理の逆を行うだけである。チェックサムが正しくない場合、不正なパスワードとみなされてロードは失敗する。
なお、ゲーム状態に関する値域チェックは行われないので、不正な値をロードさせることもできる。たとえば、面を 1〜10 の範囲外にすると画面がバグる。
ロード可能なパスワードは最短で 2 文字 (3 文字目以降は空白) であり、全部で 20 種存在する。一覧を以下に示す (イベントフラグ以外のゲーム状態は全て同じである)。なお、暗号鍵は全て 0x30 である (デコード処理を参照):
| パスワード | stage |
life |
score |
hand_ball |
stage_win |
event_flags |
|---|---|---|---|---|---|---|
| げが | 7 | 12 | 0x37B75A | 0x527EE3 | 70 | [0x2A, 0xFD, 0xA9, 0x78, 0x0B] |
| ずぎ | 7 | 12 | 0x37B75A | 0x527EE3 | 70 | [0x5A, 0xCD, 0xA9, 0x78, 0x0B] |
| ぢゆ | 7 | 12 | 0x37B75A | 0x527EE3 | 70 | [0xEA, 0x3D, 0xA9, 0x78, 0x0B] |
| でぐ | 7 | 12 | 0x37B75A | 0x527EE3 | 70 | [0x4A, 0xDD, 0xA9, 0x78, 0x0B] |
| ぶげ | 7 | 12 | 0x37B75A | 0x527EE3 | 70 | [0x7A, 0xAD, 0xA9, 0x78, 0x0B] |
| ぴご | 7 | 12 | 0x37B75A | 0x527EE3 | 70 | [0x6A, 0xBD, 0xA9, 0x78, 0x0B] |
| ぴん | 7 | 12 | 0x37B75A | 0x527EE3 | 70 | [0x6A, 0xBD, 0xA9, 0x78, 0x0B] |
| ゃゆ | 7 | 12 | 0x37B75A | 0x527EE3 | 70 | [0xEA, 0x3D, 0xA9, 0x78, 0x0B] |
| あざ | 7 | 12 | 0x37B75A | 0x527EE3 | 70 | [0x9A, 0x8D, 0xA9, 0x78, 0x0B] |
| かじ | 7 | 12 | 0x37B75A | 0x527EE3 | 70 | [0x8A, 0x9D, 0xA9, 0x78, 0x0B] |
| こや | 7 | 12 | 0x37B75A | 0x527EE3 | 70 | [0xBA, 0x6D, 0xA9, 0x78, 0x0B] |
| せゆ | 7 | 12 | 0x37B75A | 0x527EE3 | 70 | [0xAA, 0x7D, 0xA9, 0x78, 0x0B] |
| つよ | 7 | 12 | 0x37B75A | 0x527EE3 | 70 | [0xDA, 0x4D, 0xA9, 0x78, 0x0B] |
| にら | 7 | 12 | 0x37B75A | 0x527EE3 | 70 | [0xCA, 0x5D, 0xA9, 0x78, 0x0B] |
| ねわ | 7 | 12 | 0x37B75A | 0x527EE3 | 70 | [0x7A, 0xAD, 0xA9, 0x78, 0x0B] |
| ふり | 7 | 12 | 0x37B75A | 0x527EE3 | 70 | [0xFA, 0x2D, 0xA9, 0x78, 0x0B] |
| みる | 7 | 12 | 0x37B75A | 0x527EE3 | 70 | [0xEA, 0x3D, 0xA9, 0x78, 0x0B] |
| やれ | 7 | 12 | 0x37B75A | 0x527EE3 | 70 | [0x1A, 0x0D, 0xA9, 0x78, 0x0B] |
| りろ | 7 | 12 | 0x37B75A | 0x527EE3 | 70 | [0x0A, 0x1D, 0xA9, 0x78, 0x0B] |
| わわ | 7 | 12 | 0x37B75A | 0x527EE3 | 70 | [0x3A, 0xED, 0xA9, 0x78, 0x0B] |