Last active
August 6, 2025 03:56
-
-
Save sethmlarson/1fa8c95b9f7afbdb85252e4d321b1d5b to your computer and use it in GitHub Desktop.
Extract NES, Famicom, and Multiboot ROMs from Animal Crossing: https://sethmlarson.dev/extracting-nes-and-famicom-roms-from-animal-crossing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# License: MIT | |
import sys | |
import mmap | |
import hashlib | |
import struct | |
# MD5 hashes from https://datomatic.no-intro.org | |
# Headerless, as header is changed from non-AC releases. | |
known_roms = { | |
"0033972cb952bbbc4f04217decdaf3a7": "Mahjong (Japan) (Rev 2) (Animal Crossing).nes", | |
"0dd95c3047bb0336823c39fefb7639c3": "Donkey Kong (World) (Rev 1) (Animal Crossing).nes", | |
"1ca706896a8d4f2a2b5480d941130a4a": "Donkey Kong Jr. Math (USA, Europe).nes", | |
"1de41e13a2e691a8cc13b757a46ae3b8": "Clu Clu Land (World) (Animal Crossing).nes", | |
"27b4479df4228d48986698ffb94e9f6b": "Punch-Out!! (USA).nes", | |
"28c4a5b81feb4033acee9d67852d8ffc": "Gomoku Narabe Renju (Japan) (Animal Crossing).nes", | |
"2bf3976d15ec25a756846465a16b064c": "Excitebike (Japan, USA) (En) (Animal Crossing).nes", | |
"44d401f92e1da528ca4a9d7083acc9d2": "Clu Clu Land (Japan) (En) (GameCube, Virtual Console).qd", | |
"5f37d85ba0f296bd471cd674d63cb640": "Legend of Zelda, The (USA) (Rev 1) (Animal Crossing).nes", | |
"8e3630186e35d477231bf8fd50e54cdd": "Super Mario Bros. (World).nes", | |
"70c309cb6b9ead20c06d554cf48b3993": "Balloon Fight (USA).nes", | |
"108fea367dc5ba9a691b3500fc1464b4": "Baseball (USA, Europe) (Animal Crossing).nes", | |
"6631ceac1aaef8efb063a34da86bacb1": "Donkey Kong Jr. (World) (Rev 1) (Animal Crossing).nes", | |
"a2b5bddb4c7a5a39c8fac13e64494c9a": "Donkey Kong 3 (World).nes", | |
"bec7fa447c1c8e13a87bd4a5685ce563": "Wario's Woods (USA).nes", | |
"bfab5f738adb919f1ba389f5c38deb67": "Pinball (Japan, USA) (En) (Animal Crossing).nes", | |
"c9c94df2ebb19bd6d717b2cfbf977574": "Ice Climber (USA, Europe, Asia) (En).nes", | |
"c432b613606c41bafa4a09470d75e75f": "Soccer (Japan, USA) (En).nes", | |
"cbb2c477a37b28517e330d1c562049f8": "Tennis (Japan, USA) (En) (Animal Crossing).nes", | |
"d67ee6a0a7af959c417ce894470a49cb": "Mario Bros. (World) (Animal Crossing).nes", | |
"f0d94f25db202c935cd8f1cdde10a0aa": "Golf (USA, Asia) (En).nes", | |
# Multiboot ROMs | |
"594b8e60e9406a570c9990e9bbc4340f": "Clu Clu Land (USA, Europe) (Animal Crossing).mb", | |
"aa6bdfc4fce58b19d1a8a9f2f11042d9": "Donkey Kong (USA, Europe) (Animal Crossing).mb", | |
"ee8a23328607687b50a6706c7fdfc2e1": "Donkey Kong Jr. (USA, Europe) (Animal Crossing).mb", | |
"d3d227a1ca88b0629ef333d544686c41": "Excitebike (USA, Europe) (Animal Crossing).mb", | |
"684e46cb0c672e06664ae742825ae89c": "Mario Bros. (USA, Europe) (Animal Crossing).mb", | |
"c1c6eb0d42591c46f2e4dc68145e4c81": "Pinball (USA, Europe) (Animal Crossing).mb", | |
"34217e69f45e52d1550a8b241ce27404": "Super Mario Bros. (USA, Europe) (Animal Crossing).mb", | |
"a81a5d9b9268da64ea8426bdc6a987ba": "Soccer (USA, Europe) (Animal Crossing).mb", | |
"146bdf7f70335a2ad67b59ef9e07bfaf": "Tennis (USA, Europe) (Animal Crossing).mb", | |
"62c26ddf7579b5179b2a67073bc7e4a4": "Balloon Fight (USA, Europe) (Animal Crossing).mb", | |
"b0c8f4dfe47c3649760748ad5c96a649": "Baseball (USA, Europe) (Animal Crossing).mb", | |
"a7b95f64a01e7cc18968b1c501741414": "Donkey Kong 3 (USA, Europe) (Animal Crossing).mb", | |
"f92aeb4bc274cb08c2eabe9dd3aadcb4": "Golf (USA, Europe) (Animal Crossing).mb", | |
"f7adee0901bb73b6f1c1fbeb36b4ab4c": "Ice Climber (USA, Europe) (Animal Crossing).mb", | |
"1c8dcf20e4ce979cb9962c835c39a5c9": "Donkey Kong Jr. Math (USA, Europe) (Animal Crossing).mb", | |
} | |
def main(): | |
animal_crossing_iso = sys.argv[1] | |
with open(animal_crossing_iso, mode="r+b") as f, mmap.mmap(f.fileno(), 0) as fm: | |
offset = -1 | |
# Find all 'Yaz0' headers. | |
while -1 != (offset := fm.find(b"Yaz0", offset + 1)): | |
try: | |
data = yaz0(fm[offset : offset + 0x80000]) | |
except Exception: | |
continue | |
# NES ROM | |
if data.startswith(b"NES\x1a"): | |
# Calculate the MD5 without the NES header. | |
rom_md5 = hashlib.md5(data[16:]).hexdigest() | |
# Famicom Disk System ROM | |
elif data.startswith(b"\x01*NINTENDO-HVC*\x01"): | |
rom_md5 = hashlib.md5(data).hexdigest() | |
# GBA Joyboot ROM | |
elif data[0xAC:0xB3] == b"AGBJ01\x96": | |
rom_md5 = hashlib.md5(data).hexdigest() | |
else: | |
continue | |
if rom_md5 not in known_roms: | |
print(f"Unknown ROM MD5: {rom_md5}") | |
continue | |
filename = known_roms[rom_md5] | |
print(f"Extracted ROM: {filename}") | |
with open(filename, mode="wb") as rom_fp: | |
rom_fp.truncate() | |
rom_fp.write(data) | |
def yaz0(data: bytes) -> bytes: | |
# Implementation of Yaz0 following | |
# this reference: http://www.amnoid.de/gc/yaz0.txt | |
(buf_length,) = struct.unpack(">xxxxLxxxxxxxx", data[:16]) | |
data = data[16:] | |
src = dst = 0 | |
buf = bytearray(buf_length) | |
while dst < buf_length and src < len(data): | |
bit_header = data[src] | |
src += 1 | |
for _ in range(8): | |
if not (dst < buf_length and src < len(data)): | |
break | |
if bit_header & 0x80: | |
buf[dst] = data[src] | |
dst += 1 | |
src += 1 | |
else: | |
byte1, byte2 = struct.unpack(">BB", data[src : src + 2]) | |
assert byte1 >= 0 and byte2 >= 0 | |
src += 2 | |
copy_src = dst - ((byte1 & 0x0F) << 8 | byte2) - 1 | |
num_bytes = byte1 >> 4 | |
if num_bytes == 0: | |
num_bytes = data[src] + 0x12 | |
src += 1 | |
else: | |
num_bytes += 2 | |
for i in range(num_bytes): | |
buf[dst + i] = buf[copy_src + i] | |
dst += num_bytes | |
bit_header <<= 1 | |
return bytes(buf) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment