-
-
Save anpage/4834433944a2875ee6d4cbb5786c6bf7 to your computer and use it in GitHub Desktop.
#!/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')) |
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.
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
.
Yep, that was the issue. The script is fixed now, at least according to Mortal Kombat 2.
I cleaned up the script a little and added support for the DSP-1 games that pcm720 listed. I only tested with Pilotwings though.
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
I wonder if any non-DSP-1 games use those ROM types also.
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.
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 :(
i have figured out how to use this to convert sfc to sfroms but how do you convert smc to sfrom whats the command?