Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
#!/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)
@20Phoenix

This comment has been minimized.

Copy link

@20Phoenix 20Phoenix commented Aug 8, 2019

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 😃 👯‍♀️

@TBirdSoars

This comment has been minimized.

Copy link

@TBirdSoars TBirdSoars commented Oct 4, 2020

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?

@einstein95

This comment has been minimized.

Copy link
Owner Author

@einstein95 einstein95 commented Oct 5, 2020

@TBirdSoars

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

@TBirdSoars

This comment has been minimized.

Copy link

@TBirdSoars TBirdSoars commented Oct 5, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment