-
-
Save einstein95/6545066905680466cdf200c4cc8ca4f0 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3 | |
""" | |
Converts between QD and FDS disk images | |
""" | |
import struct | |
def create_fds_header(side_count): | |
return b"FDS\x1A" + bytes([side_count & 0xFF]) + bytes(11) | |
def fds_crc(data): | |
# http://forums.nesdev.com/viewtopic.php?p=194867 | |
# Do not include any existing checksum, not even the blank checksums 00 00 or FF FF. | |
# The formula will automatically count 2 0x00 bytes without the programmer adding them manually. | |
# Also, do not include the gap terminator (0x80) in the data. | |
# If you wish to do so, change sum to 0x0000. | |
s = 0x8000 | |
for byte in data + bytes(2): | |
s |= byte << 16 | |
for _ in range(8): | |
if (s & 1): | |
s ^= 0x8408 << 1 | |
s >>= 1 | |
return s | |
def convert_to_fds(disk, null): | |
""" | |
Convert a .qd to .fds | |
""" | |
del disk[0x38:0x3A] # 01 block chksum | |
# fn = disk[0x39] # get num of files from 02 block | |
pos = 0x3A | |
del disk[pos: pos + 2] # 02 block chksum | |
try: | |
while disk[pos] == 3: # if there's any more files, like in Doki Doki Panic | |
print(f"{pos:05X}\t{disk[pos + 0x3:pos + 0xB].decode('latin_1')}") | |
filesize, = struct.unpack('<H', disk[pos + 0xD: pos + 0xF]) | |
del disk[pos + 0x10: pos + 0x12] # 03 block chksum | |
pos = pos + 0x10 + 1 + filesize | |
del disk[pos: pos + 2] # 04 block chksum | |
except IndexError: | |
pass | |
if len(disk) > 65500: | |
disk = disk[:65500] | |
else: | |
disk = disk.ljust(65500, b"\0") | |
return disk | |
def convert_to_qd(disk, null): | |
""" | |
Convert a .fds to .qd | |
""" | |
def insert_crc(start, end, null): | |
if not null: | |
crc = fds_crc(disk[start:end]) | |
disk.insert(end + 0, crc & 0xFF) | |
disk.insert(end + 1, crc >> 0x8) | |
else: | |
disk.insert(end + 0, 0) | |
disk.insert(end + 1, 0) | |
if disk[0] != 0x01: | |
return bytearray() | |
insert_crc(0, 0x38, null) # 01 block chksum | |
# fn = disk[0x3B] # get num of files from 02 block | |
pos = 0x3A | |
insert_crc(pos, pos + 2, null) # 02 block chksum | |
pos = 0x3E | |
try: | |
while disk[pos] == 3: # if there's any more files, like in Doki Doki Panic | |
print(disk[pos + 0x3: pos + 0xB].decode('latin_1')) | |
filesize, = struct.unpack('<H', disk[pos + 0xD: pos + 0xF]) | |
insert_crc(pos, pos + 0x10, null) # 03 block chksum | |
pos += 0x10 + 2 | |
insert_crc(pos, pos + 1 + filesize, null) # 04 block chksum | |
pos += 1 + filesize + 2 | |
except IndexError: | |
pass | |
if len(disk) > 0x10000: | |
disk = disk[:0x10000] | |
else: | |
disk = disk.ljust(0x10000, b"\0") | |
return disk | |
if __name__ == '__main__': | |
import sys | |
import argparse | |
from pathlib import Path | |
parser = argparse.ArgumentParser(description='Converts between QD and FDS disk images') | |
parser.add_argument('rom', metavar='ROM', help='QD or FDS file') | |
parser.add_argument('-o', '--output') | |
parser.add_argument('-f', '--fds-header', action='store_true', help='add FDS header') | |
parser.add_argument('-n', '--null-checksum', action='store_true', help='use null bytes instead of calculated checksum (like in some VC games)') | |
args = parser.parse_args() | |
path = Path(args.rom) | |
try: | |
size = path.stat().st_size | |
except FileNotFoundError as err: | |
print("File Not Found: {0}".format(err)) | |
sys.exit(1) | |
if not path.is_file(): | |
print("Error: It's not file: '{0}'".format(args.rom)) | |
sys.exit(1) | |
side_size = 0 | |
start_pos = 0 | |
convert_func = None | |
ext = '' | |
if size % 0x10000 == 0: | |
side_size = 0x10000 | |
convert_func = convert_to_fds | |
ext = '.fds' | |
elif size % 65500 == 0 or size % 65500 == 16: | |
side_size = 65500 | |
start_pos = size % 65500 | |
convert_func = convert_to_qd | |
ext = '.qd' | |
if convert_func: | |
in_bytes = bytearray(path.read_bytes()) | |
out_bytes = bytearray() | |
if ext == '.fds' and args.fds_header: | |
out_bytes += create_fds_header(size // side_size) | |
for i in range(start_pos, size, side_size): | |
out_bytes += convert_func(in_bytes[i: i + side_size], null=args.null_checksum) | |
path = path.with_suffix(ext) | |
if args.output: | |
path = Path(args.output) | |
path.write_bytes(out_bytes) |
This is great, but I keep encountering an issue with Super Mario Bros The Lost Levels. I have a QD rom of that game that matches the checksums on no-intro:
https://datomatic.no-intro.org/?page=show_record&s=31&n=0333
But the resulting FDS rom doesn't match the checksums found here:
https://datomatic.no-intro.org/?page=show_record&s=31&n=0297
Something similar is happening to Bubble Bobble, but from FDS to QD.
A while back, I added Famicom Disk System rom extraction to this project:
https://github.com/wheatevo/wiiu-vc-extractor
I realized that the Wii U VC used QD roms, so I converted this script to C# in order for the extracted roms to be playable in most emulators, but I had to add a patch for Lost Levels to match the entry on no-intro.
Do you have any idea why these converted roms don't match the known dumps?
This is great, but I keep encountering an issue with Super Mario Bros The Lost Levels. I have a QD rom of that game that matches the checksums on no-intro:
https://datomatic.no-intro.org/?page=show_record&s=31&n=0333
But the resulting FDS rom doesn't match the checksums found here:
https://datomatic.no-intro.org/?page=show_record&s=31&n=0297
This is because the GC/Wii/U version is a derivative of the non-DV2 version (https://datomatic.no-intro.org/index.php?page=show_record&s=31&n=0120) with 3 single byte patches. The resulting FDS file of this version should have the SHA1 hash b2dbc55efcae77abad6207b802c0a76d7a47ed0d
.
Something similar is happening to Bubble Bobble, but from FDS to QD.
I can only presume this is due to the same thing as the VC versions are prepatched ROMs
Thanks for the quick reply!
Interesting. That SHA1 hash does in fact match the Lost Levels QD rom I have. It would make sense then that my Bubble Bobble QD wouldn't match the Wii U QD dump, since my FDS rom matches this one:
https://datomatic.no-intro.org/?page=show_record&s=31&n=0208
Very nice and usefull tool, thanks a lot.
Sadly it doesn't work if the FDS file has a header.
Can you add the possibility to convert both, with and without header ?
Or adding an error message if there're problems because of the header.
That would be AWESOME 😃 👯♀️