Last active
April 27, 2023 06:58
-
-
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
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 | |
# 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
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 :(