Skip to content

Instantly share code, notes, and snippets.

@anpage
Last active April 27, 2023 06:58
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • 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'))
@anpage
Copy link
Author

anpage commented Oct 2, 2017

For sfc2sfrom, the presetid probably needs changing depending on the ROM. Super Mario World's 0x1011 works for the couple of ROMs I tried, while Donkey Kong Country's 0x1022 did not.

@pcm720
Copy link

pcm720 commented Oct 2, 2017

Some games (mostly the ones that use SuperFX) won't boot if presetid is not set to 0x0000.
SuperFX games also require footer offset 0x12 to be set to 0x0C, else you'll get C7 error.

Also you can easily detect if ROM is HiROM or LoROM:
https://en.wikibooks.org/wiki/Super_NES_Programming/SNES_memory_map#How_do_I_recognize_the_ROM_type.3F

@anpage
Copy link
Author

anpage commented Oct 2, 2017

Thanks for the link! I'll make sure to change those things when I get the time.

I wonder why Nintendo opted not to detect the ROM type that way.

@pcm720
Copy link

pcm720 commented Oct 2, 2017

Well, they don't really need to detect anything since they know the ROM type beforehand. So why bother?

@anpage
Copy link
Author

anpage commented Oct 3, 2017

Well, "detect" is probably the wrong word. I guess what I meant to ask is why would they put the ROM type in the VC header when it's already in the SNES header?

@pcm720
Copy link

pcm720 commented Oct 3, 2017

Well, why bother implementing the detection thing that runs every time emulator loads the ROM when you can simply put that information at a known offset?

@anpage
Copy link
Author

anpage commented Oct 4, 2017

I updated the sfc2sfrom script to only include important information in a single header rather than a header+footer. It also auto-detects hirom/lorom and whether or not the ROM uses a Super-FX chip, and can set the preset ID per ROM name.

(I'm Valter on GBAtemp)

@pcm720
Copy link

pcm720 commented Oct 4, 2017

Cool!
You can also add preset ID 0x10BD for DSP-1 games.
Lock On, Pilotwings and Ballz 3D have 0x03 at offset 0x?FD6. I didn't notice any glitches using this preset ID with these games.

@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