-
-
Save DNA64/5e79c6449785949f86744fa7dcb50ad7 to your computer and use it in GitHub Desktop.
Scripts to convert SNES ROMs to SNES Classic (.sfrom) format and to read .sfrom headers
This file contains 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
#!/usr/bin/env python | |
# Permission to use, copy, modify, and/or distribute this software for any | |
# purpose with or without fee is hereby granted. | |
# | |
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH | |
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY | |
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, | |
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM | |
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR | |
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR | |
# PERFORMANCE OF THIS SOFTWARE. | |
import argparse | |
from struct import pack | |
def get_header_page(rom_format): | |
"""Returns the page containing the SNES header based on the ROM format""" | |
is_hirom = rom_format - 0x14 | |
return 0x7000 + is_hirom * 0x8000 | |
def get_format(data): | |
"""Returns 0x14 for LoROM and 0x15 for HiROM""" | |
# Kind of lazy way of detecting this | |
if all(31 < char < 127 for char in data[0x7FC0:0x7FC0 + 21]): | |
return 0x14 | |
if all(31 < char < 127 for char in data[0xFFC0:0xFFC0 + 21]): | |
return 0x15 | |
return None | |
def get_preset_id(data, header_page): | |
"""Returns a preset ID for some known games, otherwise 0x0""" | |
addr = header_page + 0x0FC0 | |
name = data[addr:addr + 21].decode('ascii').strip() | |
if data[header_page + 0x0FD6] == 0x03: | |
return 0x10BD # DSP-1 Games | |
elif name == "MEGAMAN X2": | |
return 0x1117 | |
elif name == "MEGAMAN X3": | |
return 0x113D | |
elif name == "PILOTWINGS": | |
return 0x10BD | |
elif name == "BREATH OF FIRE": | |
return 0x1144 | |
elif name == "BREATH OF FIRE 2": | |
return 0x1068 | |
elif name == "CASTLEVANIA DRACULA": | |
return 0x1131 | |
elif name == "CONTRA3 THE ALIEN WA": | |
return 0x1036 | |
elif name == "DONKEY KONG COUNTRY": | |
return 0x1077 | |
elif name == "DIDDY'S KONG QUEST": | |
return 0x105D | |
elif name == "EARTH BOUND": | |
return 0x1070 | |
elif name == "F-ZERO": | |
return 0x1018 | |
elif name == "FINAL FIGHT": | |
return 0x100E | |
elif name == "FINAL FIGHT 2": | |
return 0x10E1 | |
elif name == "FINAL FIGHT 3": | |
return 0x10E3 | |
elif name == "FINAL FANTASY 3": | |
return 0x10DC | |
elif name == "GENGHIS KHAN 2": | |
return 0x10C4 | |
elif name == "Kirby's Dream Course": | |
return 0x1058 | |
elif name == "KIRBY SUPER DELUXE": | |
return 0x109F | |
elif name == "KIRBY'S DREAM LAND 3": | |
return 0x10A2 | |
elif name == "MEGAMAN X": | |
return 0x1109 | |
elif name == "MEGAMAN 7": | |
return 0x113A | |
elif name == "SUPER MARIOWORLD": | |
return 0x1011 | |
elif name == "SUPER MARIO KART": | |
return 0x10BD | |
elif name == "Super Metroid": | |
return 0x1040 | |
elif name == "Super Punch-Out!!": | |
return 0x10A9 | |
elif name == "SUPER GHOULS'N GHOST": | |
return 0x1003 | |
elif name == "Street Fighter2 Turb": | |
return 0x1065 | |
elif name == "SUPER MARIO RPG": | |
return 0x109E | |
elif name == "Secret of MANA": | |
return 0x10B0 | |
elif name == "SUPER CASTLEVANIA 4": | |
return 0x1030 | |
elif name == "STAR FOX": | |
return 0x123B | |
elif name == "STARFOX2": | |
return 0x1245 | |
elif name == "STREET FIGHTER ALPHA": | |
return 0x10DF | |
elif name == "THE LEGEND OF ZELDA": | |
return 0x101D | |
elif name == "YOSHI'S ISLAND": | |
return 0x123D | |
elif name == "SHVC FIREEMBLEM": | |
return 0x102B | |
elif name == "YOSSY'S ISLAND": | |
return 0x1243 | |
elif name == "SUPER DONKEY KONG": | |
return 0x1023 | |
elif name == "SUPER FORMATION SOCC": | |
return 0x1240 | |
elif name == "Super Street Fighter": | |
return 0x1056 | |
elif name == "ROCKMAN X": | |
return 0x110A | |
elif name == "CHOHMAKAIMURA": | |
return 0x1004 | |
elif name == "SeikenDensetsu 2": | |
return 0x10B2 | |
elif name == "FINAL FANTASY 6": | |
return 0x10DD | |
elif name == "CONTRA SPIRITS": | |
return 0x1037 | |
elif name == "ganbare goemon": | |
return 0x1048 | |
elif name == "ZELDANODENSETSU": | |
return 0x101F | |
# Nothing special | |
return 0x0 | |
def get_super_fx(data, header_page): | |
"""Returns 0x0C if the ROM uses a Super-FX chip, otherwise 0x0""" | |
# Games that use Super-FX chips have these ROM types | |
SFX_TYPES = [0x13, 0x14, 0x15, 0x1a] | |
addr = header_page + 0x0FD6 | |
if (data[addr] in SFX_TYPES): | |
return 0x0C | |
else: | |
return 0x0 | |
if __name__ == "__main__": | |
parser = argparse.ArgumentParser(description="""Converts SNES ROMs to .sfrom for | |
the SNES Classic""") | |
parser.add_argument('input', type=argparse.FileType('rb')) | |
parser.add_argument('output', type=argparse.FileType('wb')) | |
args = parser.parse_args() | |
# Read in entire file because who needs RAM | |
rom = args.input.read() | |
args.input.close() | |
# Strip out potential header | |
rom = rom[len(rom) % 0x400:] | |
# Simple header format | |
# Start Size Type Contents | |
# 0x00 0x04 I 0x100 | |
# 0x04 0x04 I File size | |
# 0x08 0x04 I 0x50 | |
# 0x0C 0x08 8x 0x0 | |
# 0x14 0x04 I 0x30 | |
# 0x18 0x19 25x 0x0 | |
# 0x31 0x04 I ROM Size | |
# 0x35 0x08 8x 0x0 | |
# 0x3D 0x02 H Game preset ID | |
# 0x3F 0x01 B 0x02 | |
# 0x40 0x01 x 0x0 | |
# 0x41 0x01 B ROM format (hi/lo) | |
# 0x42 0x01 B Super-FX ? 0x0C : 0x0 | |
# 0x43 0x0D 13x 0x0 | |
rom_format = get_format(rom) | |
if not rom_format: | |
print("Can't get ROM format. Corrupted?") | |
args.output.close() | |
quit(-1) | |
header_page = get_header_page(rom_format) | |
header = pack('<3I8xI25xI8xHBx2B13x', | |
0x100, | |
len(rom) + 0x50, | |
0x50, | |
0x30, | |
len(rom), | |
get_preset_id(rom, header_page), | |
0x02, | |
rom_format, | |
get_super_fx(rom, header_page)) | |
args.output.write(header + rom) | |
args.output.close() |
This file contains 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
#!/usr/bin/env python | |
from struct import unpack | |
from collections import namedtuple | |
import argparse | |
parser = argparse.ArgumentParser() | |
parser.add_argument('filename', type=str) | |
args = parser.parse_args() | |
filename = args.filename | |
f = open(filename, "rb") | |
header = f.read(0x30) | |
SNES_header = namedtuple('Header', 'const1 filesize const2 endofrom footeraddr footer eof unknown gid') | |
unpacked = SNES_header._make(unpack('<7I4xI8s4x', header)) | |
print('File: ' + filename) | |
print('\nHeader: ') | |
for k, v in unpacked._asdict().iteritems(): | |
if type(v) is str: | |
print(k + ": \t " + v) | |
else: | |
print(k + ": \t0x" + format(v, '08X')) | |
f.read(unpacked.footer - 0x30) | |
footer = f.read(0x22) | |
SNES_footer = namedtuple('Footer', 'emuspeed romsize pcmsize footersize presetid unknown1 vol romtype unknown2 unknown3') | |
footunpacked = SNES_footer._make(unpack('<B3IH3B8x2I', footer)) | |
print('\nFooter: ') | |
for k, v in footunpacked._asdict().iteritems(): | |
if type(v) is str: | |
print(k + ": \t " + v) | |
else: | |
print(k + ": \t0x" + format(v, '08X')) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment