Skip to content

Instantly share code, notes, and snippets.

@DNA64
Forked from anpage/sfc2sfrom.py
Last active March 23, 2018 12:06
Show Gist options
  • Save DNA64/5e79c6449785949f86744fa7dcb50ad7 to your computer and use it in GitHub Desktop.
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
#!/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()
#!/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