今回はドラクエ1・2リメイククリア記念として
ドラクエ4のコインバグについてやっていきたいと思います。
解説動画はこちら
ドラクエ4とは
ファミコンの当時のハードウェア情報
スプライト制限・PPUなど
制約だらけの開発環境だったようです
ファミコン的な多バイト計算のしくみ
「足し算」は常に低位バイト→高位バイトの順に行い
各段で キャリー(繰り上がり) を次の段へ渡す:
その上位のバイトが必要になるが、実装次第で捨てられる
乗算」は 8bit CPU で直接1回の命令で出来ないため
一般に次のどちらかで実装:
オーバーフローが起きる理由
838,861 × 20 がなぜ 4 になるか
= 786432 + 52224 + 205 = 838861
2.各バイトの部分積を求める(コイン価格 = 20)
初期
Pythonで再現する「疑似アセンブリ」計算コード
当時のファミコンで行われていたであろう計算を
Pythonで模したコードが以下です。
簡単に計算できるようにしたコードだとこうなります
当時のFCDQ4のコイン価格を計算してみると
プログラミング的には相当大変な計算をしていたので
ドラクエ4のコインバグについてやっていきたいと思います。
解説動画はこちら
ドラクエ4とは
ドラゴンクエストIV 導かれし者たち
1990年2月11日にエニックス(合併前)から発売された
ファミリーコンピュータ用ロールプレイングゲーム
ファミリーコンピュータ用ロールプレイングゲーム
『ドラゴンクエストシリーズ』の第4作目
5つの章に分かれたシナリオ、AIによる戦闘システムや
5人以上の仲間キャラクターと同時に
冒険できる馬車システムが導入された作品
冒険できる馬車システムが導入された作品
ゲームの2,3,5章にはミニゲームとしてカジノがあり
そこではコインバグが使える部分がありました
ドラクエ4のカジノシステムと「コインバグ」
ゲーム内の2,3,5章でいける大都市エンドール
この都市にはカジノがあり、コインが買える
1枚の価格
2章 : 10ゴールド
3章 : 200ゴールド
5章 : 20ゴールド
第3章ではコイン83,887枚を184ゴールドで
第5章では838,861枚を4ゴールドで購入できる
これはワークエリアの算術オーバーフローに起因する判定処理のバグが原因
購入金額の計算を3バイトで行っているため、カジノでコインを購入する際
計算金額が16,777,215ゴールドを超えるとオーバーフローが発生
(2 ** 24 = 16777216)
本来のコインの金額と16777216との差が
購入金額となっていたようです
購入金額となっていたようです
CPU:Ricoh 2A03(6502の派生)
メモリ構造:8bit CPU・少ないRAM
参考 : PS5のCPU:64ビット、スーパーファミコンのCPU:16ビット
ソフト開発はアセンブリ言語を使用
数値の表現方法(8bit × n バイトで多バイト値を管理)
多バイト演算が必要(24bit/32bitの値を手動で扱う)
スプライト制限・PPUなど
制約だらけの開発環境だったようです
ファミコン的な多バイト計算のしくみ
当時の 8bit CPU は一度に扱えるのは 0..255(1バイト)だけ
大きな数は複数バイトに分割して管理する
例:24ビット値は
LO(下位8ビット)
MID(中位8ビット)
HI(上位8ビット)という3バイトで保持
LO(下位8ビット)
MID(中位8ビット)
HI(上位8ビット)という3バイトで保持
「足し算」は常に低位バイト→高位バイトの順に行い
各段で キャリー(繰り上がり) を次の段へ渡す:
LO = LO1 + LO2 → もし >255 なら carry=1 で LO &= 0xFF
MID = MID1 + MID2 + carry → 同様に繰り上げ
HI = HI1 + HI2 + carry → 最終的にさらに繰り上がれば(HI が 8bit を越える)
その上位のバイトが必要になるが、実装次第で捨てられる
乗算」は 8bit CPU で直接1回の命令で出来ないため
一般に次のどちらかで実装:
1.部分積の加算(schoolbook):
各バイトを8bit×8bitで掛けて(最大16bitの部分積)
適切に桁(バイト)シフトして加算する
適切に桁(バイト)シフトして加算する
2.繰り返し加算(加算を price 回繰り返す):
result = 0;
for i in range(count):
result += price のようにして加える
(実際はより効率的なシフト+加算で実装されることが多い)
オーバーフローが起きる理由
ゲーム側が「結果を格納する領域」を
3 バイト(24ビット)だけ確保していると
数学的に 24 ビットを超える値は
3 バイト(24ビット)だけ確保していると
数学的に 24 ビットを超える値は
自動的に上位ビットが切り捨てられる
(つまり結果は result mod 2^24)
これがバグの本質
(つまり結果は result mod 2^24)
これがバグの本質
838,861 × 20 がなぜ 4 になるか
1.コイン枚数(24bit)をバイトに分解:
コイン枚数 = 838,861
LO = 205 = 0xCD
MID = 204 = 0xCC
HI = 12 = 0x0C
検算: 12 * 65536 + 204 * 256 + 205 = 786432 + 52224 + 205 = 838861
2.各バイトの部分積を求める(コイン価格 = 20)
p0 = LO * 20 = 205 * 20 = 4,100 → hex 0x1004
→bytes = [0x04, 0x10, 0x00, 0x00]
→bytes = [0x04, 0x10, 0x00, 0x00]
p1 = MID * 20 = 204 * 20 = 4,080 → hex 0x0FF0 → shift 1 byte
→ bytes = [0x00, 0xF0, 0x0F, 0x00]
→ bytes = [0x00, 0xF0, 0x0F, 0x00]
p2 = HI * 20 = 12 * 20 = 240 → hex 0x00F0 → shift 2 bytes
→ bytes = [0x00, 0x00, 0xF0, 0x00]
→ bytes = [0x00, 0x00, 0xF0, 0x00]
3.部分積を下位バイトから順に加算(変数 acc は 4 バイトで保持):
初期
acc = [0x00, 0x00, 0x00, 0x00]
加算 p0
acc = [0x04, 0x10, 0x00, 0x00]
加算 p1:
acc[0] += 0x00 → 0x04
acc[1] += 0xF0 → 0x10 + 0xF0 = 0x100 -> acc[1]=0x00, carry=1
acc[2] += 0x0F + carry -> 0x00 + 0x0F + 1 = 0x10
acc[3] += 0x00 -> 0x00
→ acc = [0x04, 0x00, 0x10, 0x00]
加算 p2:
acc[0] += 0x00 -> 0x04
acc[1] += 0x00 -> 0x00
acc[2] += 0xF0 -> 0x10 + 0xF0 = 0x100 -> acc[2]=0x00, carry=1
acc[3] += 0x00 + carry -> 0x00 + 1 = 0x01
→ acc = [0x04, 0x00, 0x00, 0x01]
4.acc を 32bit で読むと 0x01000004
( 0x01000004 = 16,777,220)
ゲームはここで下位3バイトのみ(0x000004)を保存
→ 0x000004 = 4 が実際にストアされる
ゲームはここで下位3バイトのみ(0x000004)を保存
→ 0x000004 = 4 が実際にストアされる
数学的には 838,861 * 20 = 16,777,220 だが
16,777,220 mod 2^24 = 4 となり
格納領域(24bit)に収めると 4 になる
格納領域(24bit)に収めると 4 になる
Pythonで再現する「疑似アセンブリ」計算コード
当時のファミコンで行われていたであろう計算を
Pythonで模したコードが以下です。
# --- ユーティリティ(24bit <-> 3バイト変換) ---
def to_bytes24(n: int):
"""整数 -> (lo, mid, hi) の3バイト"""
mask24 = (1 << 24) - 1
n &= mask24 # 24ビットに制限(実機での格納に相当)
lo = n & 0xFF
mid = (n >> 8) & 0xFF
hi = (n >> 16) & 0xFF
return lo, mid, hi
def from_bytes24(b):
lo, mid, hi = b
return lo | (mid << 8) | (hi << 16)
# --- 8bit 加算(キャリーを明示) ---
def add_with_carry(a: int, b: int, carry_in: int = 0):
"""8bitの加算 -> (結果8bit, carry_out)"""
s = a + b + carry_in
result8 = s & 0xFF
carry_out = 1 if s > 0xFF else 0
return result8, carry_out
# --- 24bit のバイト単位加算(LO->MID->HI の順でキャリーを伝播) ---
def add24(a_bytes, b_bytes):
al, am, ah = a_bytes
bl, bm, bh = b_bytes
rl, c = add_with_carry(al, bl, 0) # LO を足す(6502での ADC 命令相当)
rm, c = add_with_carry(am, bm, c) # MID に carry を足す
rh, c = add_with_carry(ah, bh, c) # HI に carry を足す
return (rl, rm, rh)
# --- 部分積を使った 24bit x 8bit の乗算(8bit CPU の実装を模倣) ---
def mul24_by_8_partial(count_bytes, price8):
"""
count_bytes は (lo, mid, hi) の3バイト。
price8 は 0..255 の 8bit 単価。
実行は:
1) 各バイト * price を計算(部分積) -> 最大16bit
2) それらを適切にシフト(バイト単位)して 4 バイト長の accumulator に足す
3) 最終的に下位3バイトだけを保存
"""
lo, mid, hi = count_bytes
# 各バイトの部分積(16bitまで)
p0 = lo * price8 # 部分積0, shift 0 bytes
p1 = mid * price8 # 部分積1, shift 1 byte (<<8)
p2 = hi * price8 # 部分積2, shift 2 bytes (<<16)
# 各部分積をバイト配列(4バイト:下位から)に展開して加算
def to_4bytes_le(x):
return (x & 0xFF, (x >> 8) & 0xFF, (x >> 16) & 0xFF, (x >> 24) & 0xFF)
# shift としてバイト単位で位置をずらす(p1 は 1 バイトシフト、p2 は 2 バイト)
b0 = to_4bytes_le(p0) # p0 << 0
b1 = to_4bytes_le(p1 << 8) # p1 << 8 -> 実際は p1 の下位を一つ上のバイトにずらす
b2 = to_4bytes_le(p2 << 16) # p2 << 16
# 4バイト幅で逐次加算(8bit CPU の ADC の連鎖を模倣)
acc = (0,0,0,0)
for part in (b0, b1, b2):
acc = add_4bytes_le(acc, part)
# 実機が保存するのは下位3バイトのみ(= acc の 0..2 バイト)
stored24 = (acc[0], acc[1], acc[2])
return stored24
# 補助:4バイト加算(下位からキャリーを伝える)
def add_4bytes_le(a4, b4):
r0, c = add_with_carry(a4[0], b4[0], 0)
r1, c = add_with_carry(a4[1], b4[1], c)
r2, c = add_with_carry(a4[2], b4[2], c)
r3, c = add_with_carry(a4[3], b4[3], c)
return (r0, r1, r2, r3)
簡単に計算できるようにしたコードだとこうなります
当時のFCDQ4のコイン価格を計算してみると
# コイン価格のシミュレーションコード
def simulate_24bit_mul(count, price):
# count を 3 バイトに分解
lo = count & 0xFF
mid = (count >> 8) & 0xFF
hi = (count >> 16) & 0xFF
# 部分積
p0 = lo * price # fits<=0xFFFF
p1 = mid * price
p2 = hi * price
# 32bit accumulator as int
full = p0 + (p1 << 8) + (p2 << 16)
# store as 24bit(下位24bitを取り出す)
stored24 = full & ((1 << 24) - 1)
return full, stored24
print(simulate_24bit_mul(100, 20))
print(simulate_24bit_mul(838_860, 20))
print(simulate_24bit_mul(838_861, 20))
print(simulate_24bit_mul(838_862, 20))
print(simulate_24bit_mul(999_999, 20))(2000, 2000)
(16777200, 16777200)
(16777220, 4)
(16777240, 24)
(19999980, 3222764)
まとめ
ドラクエ4当時の性能では仕方ないバグだったろうと思います。
プログラミング的には相当大変な計算をしていたので
ここまでの確認が出来たかったんだろうと思います
(デバッガーや統合開発環境なさそうだし)
(デバッガーや統合開発環境なさそうだし)
今は実機の性能が上がりすぎて、こういうのは無くなってきてるので
おもしろいバグとかは少なくなってますかね。
おもしろいバグとかは少なくなってますかね。
なお1990年当時
バグの噂は聞いていて知っていたので、めちゃくちゃ助かったです。
ありがとうエニックスの人!!
バグの噂は聞いていて知っていたので、めちゃくちゃ助かったです。
ありがとうエニックスの人!!
PS
ドラクエ1・2リメイク最高でした
ドラクエ3・1・2の流れを埋めるサブストーリー
うるさ過ぎるサマルトリア兄弟
クソ弱いローレシア王子
幼い頃にクリアできなかったので感動しました。
ありがとうスクエアエニックスの人!!
うるさ過ぎるサマルトリア兄弟
クソ弱いローレシア王子
幼い頃にクリアできなかったので感動しました。
ありがとうスクエアエニックスの人!!

コメントする