Skip to content

Instantly share code, notes, and snippets.

@anpage
Last active April 27, 2023 06:58
Show Gist options
  • Save anpage/4834433944a2875ee6d4cbb5786c6bf7 to your computer and use it in GitHub Desktop.
Save anpage/4834433944a2875ee6d4cbb5786c6bf7 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
# 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'))
@nnotfilc
Copy link

nnotfilc commented Oct 5, 2017

i have figured out how to use this to convert sfc to sfroms but how do you convert smc to sfrom whats the command?

@markmono
Copy link

markmono commented Oct 5, 2017

A common pattern i'm seeing using the newer revisions of the script break the 2nd controller input.
Using your 2nd revision which allows lorom/hirom choice is the last revision which 2nd controller input still works.

@anpage
Copy link
Author

anpage commented Oct 5, 2017

Interesting! I bet one of the values I'm omitting from the header is related to that somehow. I wonder which one. I'd probably guess footer offset 0x0F since that's usually 0x02.

@anpage
Copy link
Author

anpage commented Oct 5, 2017

Yep, that was the issue. The script is fixed now, at least according to Mortal Kombat 2.

@anpage
Copy link
Author

anpage commented Oct 5, 2017

I cleaned up the script a little and added support for the DSP-1 games that pcm720 listed. I only tested with Pilotwings though.

@pcm720
Copy link

pcm720 commented Oct 5, 2017

I looked at more DSP-1 games just to be sure. Turns out some games use 0x05 at offset 0x?FD6:

0x03:
Michael Andretti's Indy Car Challenge
Pilotwings
Lock On
Super Air Diver 2 (Japan-exclusive sequel to Lock On)
Suzuka 8 Hours (U)

0x05:
Super Mario Kart
Battle Racers
Final Stretch
Super 3D Baseball (Super Bases Loaded 2)
Suzuka 8 Hours (J)
Ballz 3D

@anpage
Copy link
Author

anpage commented Oct 5, 2017

I wonder if any non-DSP-1 games use those ROM types also.

@pcm720
Copy link

pcm720 commented Oct 5, 2017

This page has a lot of useful info about special chip types:
https://everything2.com/user/malcster/writeups/Super+Nintendo+Entertainment+System

To detect whether a ROM uses the DSP-1, then open it in a hex editor such as frhed. If it is a LoRom then check the location $7FD5 - a value of 0x20, 0x21, 0x30 or 0x31 indicates some kind of a DSP-1. If you need confirmation, also check location $7FD6: A value of 0x3 or 0x5 indicates a DSP-1 of some kind. If you are working with a HiRom, then do the same as above, except use the locations $FFD5 and $FFD6.

There's also information on Cx4:

To detect a C4 chip, look for 0x20 in $XFD5 and 0xF3 in $XFD6.

@Darky2k1
Copy link

Darky2k1 commented Oct 8, 2017

Could you add instructions on how to use this? I drag my rom on the .py and a black screen comes up but nothing happens :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment