def _add_info_from_ds_header(rom: FileROM, metadata: 'Metadata', header: bytes): if header[0:4] == b'.\0\0\xea': metadata.specific_info['PassMe?'] = True else: internal_title = header[0:12].decode( 'ascii', errors='backslashreplace').rstrip('\0') if internal_title: metadata.specific_info['Internal Title'] = internal_title try: product_code = convert_alphanumeric(header[12:16]) metadata.product_code = product_code add_info_from_tdb(_tdb, metadata, product_code) _add_cover(metadata, product_code) except NotAlphanumericException: pass try: if not metadata.publisher: licensee_code = convert_alphanumeric(header[16:18]) if licensee_code in _nintendo_licensee_codes: metadata.publisher = _nintendo_licensee_codes[ licensee_code] except NotAlphanumericException: pass is_dsi = False unit_code = header[18] if unit_code == 0: metadata.specific_info['DSi Enhanced?'] = False elif unit_code == 2: is_dsi = True metadata.specific_info['DSi Enhanced?'] = True elif unit_code == 3: is_dsi = True metadata.platform = "DSi" if is_dsi: region_flags = int.from_bytes(header[0x1b0:0x1b4], 'little') if region_flags < 0xffff0000: #If they're set any higher than this, it's region free #GBATEK says region free is 0xffffffff specifically but Pokemon gen 5 is 0xffffffef so who knows #Although either way, it doesn't imply regions is world, it just means it'll work worldwide, so like... ehh... regions is a weird metadata field tbh metadata.regions = _parse_dsi_region_flags(region_flags) parse_ratings(metadata, header[0x2f0:0x300], True, False) else: region = header[29] if region == 0x40: metadata.regions = {regions_by_name['Korea']} elif region == 0x80: metadata.regions = {regions_by_name['China']} metadata.specific_info['Is iQue?'] = True #If 0, could be anywhere else metadata.specific_info['Revision'] = header[30] banner_offset = int.from_bytes(header[0x68:0x6C], 'little') if banner_offset: _parse_banner(rom, metadata, header, is_dsi, banner_offset)
def add_gamecube_wii_disc_metadata(rom: FileROM, metadata: Metadata, header: bytes): internal_title = header[32:128] metadata.specific_info['Internal Title'] = internal_title.decode('ascii', errors='backslashreplace').rstrip('\0 ') if internal_title[:28] == b'GAMECUBE HOMEBREW BOOTLOADER': return product_code = None try: product_code = convert_alphanumeric(header[:4]) except NotAlphanumericException: pass publisher = None licensee_code = None try: licensee_code = convert_alphanumeric(header[4:6]) publisher = nintendo_licensee_codes.get(licensee_code) except NotAlphanumericException: pass if not (product_code == 'RELS' and licensee_code == 'AB'): # This is found on a few prototype discs, it's not valid metadata.product_code = product_code metadata.publisher = publisher if product_code and licensee_code: add_info_from_tdb(_tdb, metadata, product_code + licensee_code) add_cover(metadata, product_code, licensee_code) disc_number = header[6] + 1 if disc_number: metadata.disc_number = disc_number metadata.specific_info['Revision'] = header[7] #Audio streaming: header[8] > 1 #Audio streaming buffer size: header[9] #Unused: 10-24 is_wii = header[0x18:0x1c] == b']\x1c\x9e\xa3' is_gamecube = header[0x1c:0x20] == b'\xc23\x9f=' # Is this ever set to both? In theory no, but... hmm if not is_wii and not is_gamecube: metadata.specific_info['No Disc Magic?'] = True elif main_config.debug: if metadata.platform == 'Wii' and not is_wii: print(rom.path, 'lacks Wii disc magic') if metadata.platform == 'GameCube' and not is_gamecube: print(rom.path, 'lacks GameCube disc magic')
def _parse_satellaview_header(rom: 'FileROM', base_offset: int) -> Mapping[str, Any]: #TODO Use namedtuple/dataclass header = rom.read(seek_to=base_offset, amount=0xe0) metadata: dict[str, Any] = {} try: publisher = convert_alphanumeric(header[0xb0:0xb2]) metadata['Publisher'] = publisher except NotAlphanumericException as nae: raise BadSNESHeaderException("Publisher not alphanumeric") from nae try: title = header[0xc0:0xd0].decode('shift_jis') metadata['Title'] = title except UnicodeDecodeError as ude: raise BadSNESHeaderException( 'Title not ASCII or Shift-JIS: %s' % header[0xc0:0xd0].decode( 'shift_jis', errors='backslashreplace')) from ude month = (header[0xd6] & 0b_1111_0000) >> 4 day = (header[0xd7] & 0b_1111_1000) >> 3 if month == 0 or month > 12: raise BadSNESHeaderException('Month not valid: %d' % month) if day > 31: raise BadSNESHeaderException('Day not valid: %d' % day) metadata['Month'] = calendar.month_name[month] metadata['Day'] = day rom_layout = header[0xd8] if rom_layout not in _rom_layouts: raise BadSNESHeaderException('ROM layout is weird: %d' % rom_layout) metadata['ROM layout'] = _rom_layouts[rom_layout] return metadata
def _parse_gameboy_header(metadata: 'Metadata', header: bytes): nintendo_logo = header[4:0x34] nintendo_logo_valid = crc32(nintendo_logo) == _nintendo_logo_crc32 metadata.specific_info['Nintendo Logo Valid'] = nintendo_logo_valid title = header[0x34:0x44] cgb_flag = title[15] title_length = 16 try: metadata.specific_info['Is Colour?'] = GameBoyColourFlag(cgb_flag) title_length = 15 except ValueError: #On older carts, this would just be the last character of the title, so it would be some random value #Anyway, that would logically mean it is not in colour metadata.specific_info['Is Colour?'] = GameBoyColourFlag.No #On newer games, product code is at title[11:15], the tricky part is what exactly is a newer game and what isn't, because if the product code isn't there then those characters are just the last 4 characters of the title. It seems that it's always there on GBC exclusives, and _maybe_ there on GBC-enhanced games. And that's only for officially licensed stuff of course. #Well, might as well try that. If it's junk, we're looking up the software list later for the proper serial anyway. if cgb_flag == 0xc0: try: metadata.product_code = title[11:15].decode('ascii').rstrip('\0') title_length = 11 except UnicodeDecodeError: pass else: #Things end in null chars, so if there's null chars in the middle, that indicates if nothing else that the title ends there. If there is then 4 chars left over, that would probably be the product code maybe_title_and_serial = re.split(b'\0+', title[:title_length].rstrip(b'\0')) if len(maybe_title_and_serial) == 2: title_length = len(maybe_title_and_serial[0]) if len(maybe_title_and_serial[1]) == 4: metadata.product_code = maybe_title_and_serial[1].decode('ascii').rstrip('\0') #Might as well add that to the info. I thiiink it's just ASCII and not Shift-JIS metadata.specific_info['Internal Title'] = title[:title_length].decode('ascii', errors='backslashreplace').rstrip('\0') metadata.specific_info['SGB Enhanced?'] = header[0x46] == 3 if header[0x47] in game_boy_mappers: mapper = game_boy_mappers[header[0x47]] metadata.specific_info['Mapper'] = mapper.name metadata.save_type = SaveType.Cart if mapper.has_battery else SaveType.Nothing metadata.specific_info['Force Feedback?'] = mapper.has_rumble metadata.specific_info['Has RTC?'] = mapper.has_rtc if mapper.has_accelerometer: metadata.input_info.input_options[0].inputs.append(input_metadata.MotionControls()) metadata.specific_info['Destination Code'] = header[0x4a] #0 means Japan and 1 means not Japan. Not sure how reliable that is. licensee_code_int = header[0x4b] if licensee_code_int == 0x33: try: licensee_code = convert_alphanumeric(header[0x44:0x46]) if licensee_code in _nintendo_licensee_codes: metadata.publisher = _nintendo_licensee_codes[licensee_code] except NotAlphanumericException: pass else: licensee_code = '{:02X}'.format(licensee_code_int) if licensee_code in _nintendo_licensee_codes: metadata.publisher = _nintendo_licensee_codes[licensee_code] metadata.specific_info['Revision'] = header[0x4c]
def add_pokemini_rom_file_info(rom: 'FileROM', metadata: 'Metadata'): header = rom.read(seek_to=0x21ac, amount=16) #https://github.com/pokemon-mini/pm-dev-docs/wiki/PM_Cartridge - we are only bothering to read a small part of the thing, which is really all that's there product_code_bytes = header[0:4] try: product_code = convert_alphanumeric(product_code_bytes) metadata.product_code = product_code except NotAlphanumericException: pass title = header[4:16].decode('shift_jis', errors='backslashreplace').rstrip('\0 ') if title: metadata.specific_info['Internal Title'] = title
def _parse_gba_header(metadata: 'Metadata', header: bytes): #Entry point: 0-4 nintendo_logo = header[4:0xa0] nintendo_logo_valid = crc32(nintendo_logo) == nintendo_gba_logo_crc32 metadata.specific_info['Nintendo Logo Valid?'] = nintendo_logo_valid internal_title = header[0xa0:0xac].decode('ascii', errors='backslashreplace').rstrip('\0') metadata.specific_info['Internal Title'] = internal_title if internal_title == 'mb2gba': return product_code = None try: product_code = convert_alphanumeric(header[0xac:0xb0]) if len(product_code) == 4: game_type = product_code[0] if game_type in {'K', 'R'}: metadata.input_info.input_options[0].inputs.append(input_metadata.MotionControls()) metadata.specific_info['Force Feedback?'] = game_type in {'R', 'V'} metadata.product_code = product_code except NotAlphanumericException: pass licensee_code = None try: licensee_code = convert_alphanumeric(header[0xb0:0xb2]) if licensee_code in nintendo_licensee_codes: metadata.publisher = nintendo_licensee_codes[licensee_code] except NotAlphanumericException: pass #"Fixed value": 0xb2, apparently should be 0x96 #Main unit code: 0xb3, apparently should be 0 #Device type: 0xb4, apparently normally should be 0 #Reserved: 0xb5 - 0xbc metadata.specific_info['Revision'] = header[0xbc]
def add_vectrex_header_info(rom: 'FileROM', metadata: 'Metadata'): try: year = convert_alphanumeric(rom.read(seek_to=6, amount=4)) try: if int( year ) > 1982: #If it's any less than that, we know it was invalid (or maybe it was a prototype, but then I especially don't trust the header) year_date = Date(year, is_guessed=True) if year_date.is_better_than(metadata.release_date): metadata.release_date = metadata.release_date except ValueError: pass except NotAlphanumericException: pass
def _parse_tmd(metadata: 'Metadata', tmd: bytes): #Stuff that I dunno about: 0 - 388 if tmd[387]: metadata.specific_info['Is vWii?'] = True #IOS version: 388-396 #Title ID is just title type + hex product code, so don't worry about that try: metadata.specific_info['Title Type'] = WiiTitleType(int.from_bytes(tmd[396:400], 'big')) except ValueError: pass product_code = None try: product_code = tmd[400:404].decode('ascii') metadata.product_code = product_code if product_code: try: metadata.specific_info['Virtual Console Platform'] = WiiVirtualConsolePlatform(product_code[0]) except ValueError: pass except UnicodeDecodeError: pass #Title flags: 404-408 maker_code = None try: maker_code = convert_alphanumeric(tmd[408:410]) if maker_code in _nintendo_licensee_codes: metadata.publisher = _nintendo_licensee_codes[maker_code] except NotAlphanumericException: pass if product_code: #Inconsistently enough WiiWare doesn't require appending the maker code, apparently add_info_from_tdb(_tdb, metadata, product_code) #Unused: 410-412 region_code = int.from_bytes(tmd[412:414], 'big') try: metadata.specific_info['Region Code'] = NintendoDiscRegion(region_code) except ValueError: pass parse_ratings(metadata, tmd[414:430]) #Reserved: 430-442 #IPC mask: 442-454 (wat?) #Reserved 2: 454-472 #Access rights: 472-476 metadata.specific_info['Revision'] = int.from_bytes(tmd[476:478], 'big')
def add_virtual_boy_rom_info(rom: FileROM, metadata: 'Metadata'): rom_size = rom.size header_start_position = rom_size - 544 #Wait wouldn't that make it a footer sorta header = rom.read(seek_to=header_start_position, amount=32) title = header[0:20].decode('shift_jis', errors='backslashreplace').rstrip('\0 ') if title: metadata.specific_info['Internal Title'] = title try: licensee_code = convert_alphanumeric(header[25:27]) if licensee_code in nintendo_licensee_codes: metadata.publisher = nintendo_licensee_codes[licensee_code] elif licensee_code in unofficial_vb_publishers: metadata.publisher = unofficial_vb_publishers[licensee_code] except NotAlphanumericException: pass try: metadata.product_code = convert_alphanumeric(header[27:31]) except NotAlphanumericException: pass #Can get country from product_code[3] if needed metadata.specific_info['Revision'] = header[31]
def parse_n64_header(metadata: 'Metadata', header: bytes): #Clock rate, apparently? 0:4 #Program counter: 4-8 #Release address: 8-12 #Checksum: 12-16 #Checksum 2: 16-20 #Zero filled: 20-28 internal_title = header[28:52].decode( 'shift_jis', errors='backslashreplace').rstrip('\0') if internal_title: metadata.specific_info['Internal Title'] = internal_title #Unknown: 52-59 try: product_code = convert_alphanumeric(header[59:63]) metadata.product_code = product_code except NotAlphanumericException: pass metadata.specific_info['Revision'] = header[63]
def _parse_snes_header(rom: 'FileROM', base_offset: int) -> Mapping[str, Any]: #TODO: Use namedtuple/dataclass #In order to make things simpler, we'll just ignore any carts that are out of line. You wouldn't be able to get interesting results from homebrew or bootleg games anyway #Hence why we won't add metadata to the game object straight away, we'll store it in a dict first and add it all later, so we add nothing at all from invalid headers metadata: dict[str, Any] = {} #Note that amount goes up to 256 if you include exception vectors, but... nah header = rom.read(seek_to=base_offset, amount=0xe0) title = None try: title = header[0xc0:0xd5].decode('shift_jis') metadata['Title'] = title except UnicodeDecodeError as ude: raise BadSNESHeaderException( 'Title not ASCII or Shift-JIS: %s' % header[0xc0:0xd5].decode( 'shift_jis', errors='backslashreplace')) from ude rom_layout = header[0xd5] if rom_layout in _rom_layouts: metadata['ROM layout'] = _rom_layouts[rom_layout] elif title == "HAL'S HOLE IN ONE GOL": #HAL's Hole in Golf has 70 here, because that's the letter F, and the title immediately preceding this is "HAL HOLE IN ONE GOL". Looks like they overwrote this part of the header with the letter F. Whoops. #Anyway the internet says it's LoROM + SlowROM metadata['ROM layout'] = _rom_layouts[0x20] else: raise BadSNESHeaderException('ROM layout is weird: %s' % hex(rom_layout)) rom_type = header[0xd6] if rom_type in _rom_types: metadata['ROM type'] = _rom_types[rom_type] else: raise BadSNESHeaderException('ROM type is weird: %d' % rom_type) rom_size = header[0xd7] if rom_size not in _ram_rom_sizes: raise BadSNESHeaderException('ROM size is weird: %d' % rom_size) ram_size = header[0xd8] #We'll just use ROM type to detect presence of save data rather than this if ram_size not in _ram_rom_sizes: raise BadSNESHeaderException('RAM size is weird: %d' % ram_size) country = header[0xd9] #Dunno if I want to validate against countries, honestly. Might go wrong if country in _countries: metadata['Country'] = _countries[country] licensee = header[0xda] #Hmm.. not sure if I should validate that, but... it shouldn't be 0x00 or 0xff, maybe? metadata['Revision'] = header[0xdb] inverse_checksum = int.from_bytes(header[0xdc:0xde], 'little') checksum = int.from_bytes(header[0xde:0xe0], 'little') #Can't be arsed calculating the checksum because it's complicated (especially with some weird ROM sizes), but we know they have to add up to 0xffff if (checksum | inverse_checksum) != 0xffff: raise BadSNESHeaderException( "Checksum and inverse checksum don't add up: %s %s" % (hex(checksum), hex(inverse_checksum))) if licensee == 0x33: try: maker_code = convert_alphanumeric(header[0xb0:0xb2]) metadata['Licensee'] = maker_code except NotAlphanumericException as nae: raise BadSNESHeaderException( 'Licensee code in extended header not alphanumeric: %s' % header[0xb0:0xb2].decode('ascii', errors='backslashreplace')) from nae try: product_code = convert_alphanumeric(header[0xb2:0xb6]) metadata['Product code'] = product_code except NotAlphanumericException as nae: if header[0xb4:0xb6] == b' ': try: product_code = convert_alphanumeric(header[0xb2:0xb4]) metadata['Product code'] = product_code except NotAlphanumericException as naenae: #get naenaed raise BadSNESHeaderException( '2 char product code not alphanumeric: %s' % header[0xb2:0xb4].decode( 'ascii', errors='backslashreplace')) from naenae else: raise BadSNESHeaderException( '4 char product code not alphanumeric: %s' % header[0xb2:0xb6].decode( 'ascii', errors='backslashreplace')) from nae else: metadata['Licensee'] = '{:02X}'.format(licensee) return metadata
def _parse_ncch(rom: FileROM, metadata: 'Metadata', offset: int): #Skip over SHA-256 siggy and magic header = rom.read(seek_to=offset + 0x104, amount=0x100) #Content size: 0-4 (media unit) #Partition ID: 4-12 try: maker = convert_alphanumeric(header[12:14]) if maker in nintendo_licensee_codes: metadata.publisher = nintendo_licensee_codes[maker] except NotAlphanumericException: pass metadata.specific_info['NCCH Version'] = int.from_bytes( header[14:16], 'little') #Always 2? #Something about a hash: 16-20 #Program ID: 20-28 #Reserved: 28-44 #Logo region hash: 44-76 try: product_code = header[76:86].decode('ascii') metadata.product_code = product_code #As usual, can get country and type from here, but it has more letters and as such you can also get category as well, or like... type 2 electric boogaloo. This also means we can't use convert_alphanumeric because it contains dashes, so I guess I need to fiddle with that method if I want to use it like that #(To be precise: P = retail/cart, N = digital only, M = DLC, T = demos, U = patches) try: metadata.specific_info[ 'Virtual Console Platform'] = _3DSVirtualConsolePlatform( product_code[6]) except ValueError: pass if len(product_code) == 10 and '\0' not in product_code: short_product_code = product_code[6:] add_info_from_tdb(_tdb, metadata, short_product_code) add_cover(metadata, short_product_code) except UnicodeDecodeError: pass #Extended header hash: 92-124 #Extended header size: 124-128 #Reserved: 128-132 flags = header[132:140] is_data = (flags[5] & 1) > 0 is_executable = (flags[5] & 2) > 0 is_not_cxi = is_data and not is_executable metadata.specific_info['Is CXI?'] = not is_not_cxi #Is system update = flags[5] & 4 #Is electronic manual = flags[5] & 8 metadata.specific_info['Is Trial?'] = (flags[5] & 16) > 0 #Is zero key encrypted = flags[7] & 1 is_decrypted = (flags[7] & 4) > 0 metadata.specific_info['Decrypted'] = is_decrypted plain_region_offset = (int.from_bytes(header[140:144], 'little') * media_unit) + offset plain_region_length = (int.from_bytes(header[144:148], 'little') * media_unit) #logo_region_offset = (int.from_bytes(header[148:152], 'little') * media_unit) + offset #logo_region_length = (int.from_bytes(header[152:156], 'little') * media_unit) exefs_offset = (int.from_bytes(header[156:160], 'little') * media_unit) + offset exefs_length = (int.from_bytes(header[160:164], 'little') * media_unit) #romfs_offset = (int.from_bytes(header[172:176], 'little') * media_unit) + offset #romfs_length = (int.from_bytes(header[176:180], 'little') * media_unit) if plain_region_length: _parse_plain_region(rom, metadata, plain_region_offset, plain_region_length) #Logo region: Stuff and things if exefs_length: _parse_exefs(rom, metadata, exefs_offset) #RomFS: Filesystem really if (not is_not_cxi) and is_decrypted: extended_header = rom.read(seek_to=offset + 0x200, amount=0x800) system_control_info = extended_header[0:0x200] #Access control info: 0x200:0x400 #AccessDesc signature: 0x400:0x500 #RSA-2048 public key: 0x500:0x600 #Access control info 2: 0x600:0x800 metadata.specific_info['Executable Name'] = system_control_info[ 0:8].decode('ascii', errors='ignore').rstrip('\0') #Reserved: 0x8:0xd #Flags (bit 0 = CompressExefsCode, bit 1 = SDApplication): 0xd #Remaster version: 0xe:0x10 #Text code set info: 0x10:1c #Stack size: 0x1c:0x20 #Read only code set info: 0x20:0x2c #Reserved: 0x2c:0x30 #Data code set info: 0x30:0x3c #BSS size: 0x3c:0x40 #Dependency module ID list: 0x40:0x1c0 #SystemInfo: 0x1c0:0x200 save_size = int.from_bytes(system_control_info[0x1c0:0x1c8], 'little') metadata.save_type = SaveType.Internal if save_size > 0 else SaveType.Nothing