Skip to content

Instantly share code, notes, and snippets.

@sethmlarson
Last active August 6, 2025 03:56
Show Gist options
  • Save sethmlarson/1fa8c95b9f7afbdb85252e4d321b1d5b to your computer and use it in GitHub Desktop.
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
# 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