-
-
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')) |
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
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.
Well, they don't really need to detect anything since they know the ROM type beforehand. So why bother?
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?
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?
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)
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.
i have figured out how to use this to convert sfc to sfroms but how do you convert smc to sfrom whats the command?
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 :(
For sfc2sfrom, the
presetid
probably needs changing depending on the ROM. Super Mario World's0x1011
works for the couple of ROMs I tried, while Donkey Kong Country's0x1022
did not.