def _add_wad_metadata(rom: FileROM, metadata: 'Metadata'): header = rom.read(amount=0x40) header_size = int.from_bytes(header[0:4], 'big') #WAD type: 4-8 cert_chain_size = int.from_bytes(header[8:12], 'big') #Reserved: 12-16 ticket_size = int.from_bytes(header[16:20], 'big') tmd_size = int.from_bytes(header[20:24], 'big') data_size = int.from_bytes(header[24:28], 'big') footer_size = int.from_bytes(header[28:32], 'big') #All blocks are stored in that order: header > cert chain > ticket > TMD > data; aligned to multiple of 64 bytes cert_chain_offset = _round_up_to_multiple(header_size, 64) ticket_offset = cert_chain_offset + _round_up_to_multiple(cert_chain_size, 64) tmd_offset = ticket_offset + _round_up_to_multiple(ticket_size, 64) real_tmd_size = _round_up_to_multiple(tmd_size, 64) if real_tmd_size >= 768: tmd = rom.read(seek_to=tmd_offset, amount=real_tmd_size) _parse_tmd(metadata, tmd) data_offset = tmd_offset + _round_up_to_multiple(tmd_size, 64) footer_offset = data_offset + _round_up_to_multiple(data_size, 64) #Dolphin suggests that this is opening.bnr actually footer = rom.read(seek_to=footer_offset, amount=_round_up_to_multiple(footer_size, 64)) _parse_opening_bnr(metadata, footer)
def add_tgc_metadata(rom: FileROM, metadata: Metadata): tgc_header = rom.read(0, 60) #Actually it is bigger than that magic = tgc_header[0:4] if magic != b'\xae\x0f8\xa2': if main_config.debug: print('Hmm', rom.path, 'is .tgc but TGC magic is invalid', magic) return tgc_header_size = int.from_bytes(tgc_header[8:12], 'big') fst_real_offset = int.from_bytes(tgc_header[16:20], 'big') fst_real_size = int.from_bytes(tgc_header[20:24], 'big') #apploader_real_offset = int.from_bytes(tgc_header[28:32], 'big') #apploader_real_size = int.from_bytes(tgc_header[32:36], 'big') #These fields are "?" in YAGD, but Dolphin uses them, so they probably know what they're doing and this is probably the right way real_offset = int.from_bytes(tgc_header[36:40], 'big') virtual_offset = int.from_bytes(tgc_header[52:56], 'big') file_offset = real_offset - virtual_offset header = rom.read(tgc_header_size, 0x460) _add_gamecube_disc_metadata(rom, metadata, header, { #'apploader offset': apploader_real_offset, #'apploader size': apploader_real_size, 'fst offset': fst_real_offset, 'fst size': fst_real_size, 'file offset': file_offset, })
def parse_ncsd(rom: FileROM, metadata: 'Metadata'): #Assuming CCI (.3ds) here #Skip over SHA-256 signature and magic header = rom.read(seek_to=0x104, amount=0x100) #ROM size: 0-4 #Media ID: 4-12 #Partition types: 12-20 #Partition crypt types: 20-28 partition_offsets = [] partition_lengths = [] for i in range(0, 8): partition_offset = int.from_bytes( header[28 + (i * 4):28 + (i * 4) + 4], 'little') * media_unit partition_length = int.from_bytes( header[32 + (i * 4):32 + (i * 4) + 4], 'little') * media_unit partition_offsets.append(partition_offset) partition_lengths.append(partition_length) if partition_lengths[0]: #Ignore lengths, we're not just gonna read the whole NCCH in one block because that would use a heckton of memory and whatnot _parse_ncch(rom, metadata, partition_offsets[0]) #Partition 1: Electronic manual #Partition 2: Download Play child #Partition 6: New 3DS update data #Partition 7: Update data card_info_header = rom.read(seek_to=0x200, amount=0x314) card2_writeable_address = int.from_bytes(card_info_header[:4], 'little') if card2_writeable_address != 0xffffffff: metadata.save_type = SaveType.Cart metadata.specific_info['Title Version'] = int.from_bytes( card_info_header[0x210:0x212], 'little') metadata.specific_info['Card Version'] = int.from_bytes( card_info_header[0x212:0x214], 'little')
def just_read_the_wia_rvz_header_for_now(rom: FileROM, metadata: Metadata): #I'll get around to it I swear wia_header = rom.read(amount=0x48) wia_disc_struct_size = int.from_bytes(wia_header[12:16], 'big') wia_disc_struct = rom.read(seek_to=0x48, amount=wia_disc_struct_size) disc_header = wia_disc_struct[16:128] add_gamecube_wii_disc_metadata(rom, metadata, disc_header)
def add_fds_metadata(rom: FileROM, metadata: Metadata): if _nes_config and _nes_config.options.get('set_fds_as_different_platform'): metadata.platform = 'FDS' header = rom.read(amount=56) if header[:4] == b'FDS\x1a': metadata.specific_info['Headered?'] = True metadata.specific_info['Header Format'] = 'fwNES' rom.header_length_for_crc_calculation = 16 header = rom.read(seek_to=16, amount=56) else: metadata.specific_info['Headered?'] = False licensee_code = f'{header[15]:02X}' if licensee_code in _nintendo_licensee_codes: metadata.publisher = _nintendo_licensee_codes[licensee_code] metadata.specific_info['Revision'] = header[20] #Uses Showa years (hence 1925), in theory... but then some disks (notably Zelda) seem to use 19xx years, as it has an actual value of 0x86 which results in it being Showa 86 = 2011, but it should be [Feb 21] 1986, so... hmm year = decode_bcd(header[31]) #Showa 61 = 1986 when the FDS was released. Year > 99 wouldn't be valid BCD, so... I'll check back in 2025 to see if anyone's written homebrew for the FDS in that year and then I'll figure out what I'm doing. But homebrew right now seems to leave the year as 00 anyway, though year = 1925 + year if 61 <= year <= 99 else 1900 + year month = decode_bcd(header[32]) day = decode_bcd(header[33]) if not metadata.release_date: metadata.release_date = Date(year, month, day, True)
def add_unif_metadata(rom: FileROM, metadata: Metadata): metadata.specific_info['Headered?'] = True metadata.specific_info['Header Format'] = 'UNIF' pos = 32 size = rom.size while pos < size: chunk = rom.read(amount=8, seek_to=pos) chunk_type = chunk[0:4].decode('ascii', errors='ignore') chunk_length = int.from_bytes(chunk[4:8], 'little') chunk_data = rom.read(amount=chunk_length, seek_to=pos+8) parse_unif_chunk(metadata, chunk_type, chunk_data) pos += 8 + chunk_length
def _parse_smdh(rom: FileROM, metadata: 'Metadata', offset: int = 0, length: int = -1): metadata.specific_info['Has SMDH?'] = True #At this point it's fine to just read in the whole thing smdh = rom.read(seek_to=offset, amount=length) _parse_smdh_data(metadata, smdh)
def add_fst_info(rom: FileROM, metadata: Metadata, fst_offset: int, fst_size: int, offset: int=0): if fst_offset and fst_size and fst_size < (128 * 1024 * 1024): fst = rom.read(fst_offset, fst_size) number_of_fst_entries = int.from_bytes(fst[8:12], 'big') if fst_size < (number_of_fst_entries * 12): if main_config.debug: print('Invalid FST in', rom.path, ':', fst_size, '<', number_of_fst_entries * 12) return string_table = fst[number_of_fst_entries * 12:] for i in range(1, number_of_fst_entries): entry = fst[i * 12: (i * 12) + 12] if entry[0] != 0: continue offset_into_string_table = int.from_bytes(entry[1:4], 'big') #Actually it's a null terminated string but we only care about the one file, so cbf finding a null, I'll just check for the expected length banner_name = string_table[offset_into_string_table:offset_into_string_table+len('opening.bnr')] if banner_name == b'opening.bnr': file_offset = int.from_bytes(entry[4:8], 'big') + offset file_length = int.from_bytes(entry[8:12], 'big') banner = rom.read(file_offset, file_length) add_banner_info(rom, metadata, banner)
def parse_3dsx(rom: FileROM, metadata: 'Metadata'): header = rom.read(amount=0x20) header_size = int.from_bytes(header[4:6], 'little') has_extended_header = header_size > 32 look_for_smdh_file = True if has_extended_header: extended_header = rom.read(seek_to=0x20, amount=12) smdh_offset = int.from_bytes(extended_header[0:4], 'little') smdh_size = int.from_bytes(extended_header[4:8], 'little') if smdh_size: look_for_smdh_file = False _parse_smdh(rom, metadata, smdh_offset, smdh_size) if look_for_smdh_file: smdh_name = rom.path.with_suffix('.smdh') try: with smdh_name.open('rb') as smdh_file: _parse_smdh_data(metadata, smdh_file.read()) except FileNotFoundError: pass
def _parse_exefs(rom: FileROM, metadata: 'Metadata', offset: int): header = rom.read(seek_to=offset, amount=0x200) for i in range(0, 10): try: filename = header[(i * 16):(i * 16) + 8].decode('ascii').rstrip('\x00') except UnicodeDecodeError: continue file_offset = int.from_bytes(header[(i * 16) + 8:(i * 16) + 8 + 4], 'little') + 0x200 + offset file_length = int.from_bytes(header[(i * 16) + 12:(i * 16) + 12 + 4], 'little') if filename == 'icon': _parse_smdh(rom, metadata, file_offset, file_length)
def add_dreamcast_rom_info(rom: FileROM, metadata: Metadata): if rom.extension == 'gdi': data = rom.read().decode('utf8', errors='backslashreplace') for line in data.splitlines(): match = _gdi_regex.match(line) if match: track_number = int(match['trackNumber']) #is_data = match['type'] == '4' sector_size = int(match['sectorSize']) filename = match['name_unquoted'] if match[ 'name_unquoted'] else match['name'] #print(game.rom.path, track_number, is_data, sector_size, filename) if track_number == 3: full_name = Path(filename) if filename.startswith( '/') else rom.path.parent.joinpath(filename) add_info_from_main_track(metadata, full_name, sector_size)
def _parse_banner(rom: FileROM, metadata: 'Metadata', header: bytes, is_dsi: bool, banner_offset: int): #The extended part of the banner if is_dsi contains animated icon frames, so we don't really need it banner_size = int.from_bytes(header[0x208:0x20c], 'little') if is_dsi else 0xA00 banner = rom.read(seek_to=banner_offset, amount=banner_size) version = int.from_bytes(banner[0:2], 'little') metadata.specific_info['Banner Version'] = version #2 = has Chinese, 3 = has Korean, 0x103, has DSi stuff if version in {1, 2, 3, 0x103}: banner_titles = {} banner_languages = { 0: 'Japanese', 1: 'English', 2: 'French', 3: 'German', 4: 'Italian', 5: 'Spanish', 6: 'Chinese', #Version >= 2 7: 'Korean' #Version >= 3 } for i in range(7): try: banner_title = banner[0x240 + (i * 256):0x240 + ( i * 256) + 256].decode('utf-16le').rstrip('\0 \uffff') #if banner_title and not all([c == '\uffff' for c in banner_title]): if banner_title: banner_titles[banner_languages[i]] = banner_title except (UnicodeDecodeError, IndexError): continue for lang, title in banner_titles.items(): _add_banner_title_metadata(metadata, title, lang) if banner_titles: banner_title = banner_titles.get( 'English', next(iter(banner_titles.values()))) _add_banner_title_metadata(metadata, banner_title) if len(banner) >= 0x240: if have_pillow: icon_bitmap = banner[0x20:0x220] icon_palette = struct.unpack('H' * 16, banner[0x220:0x240]) metadata.images['Icon'] = _decode_icon(icon_bitmap, icon_palette)
def _get_smd_header(rom: FileROM): #Just get the first block which is all that's needed for the header, otherwise this would be a lot more complicated (just something to keep in mind if you ever need to convert a whole-ass .smd ROM) block = rom.read(seek_to=512, amount=16384) buf = bytearray(16386) midpoint = 8192 even = 1 #Hmm, maybe I have these the wrong way around odd = 2 for i, b in enumerate(block): if i <= midpoint: buf[even] = b even += 2 else: buf[odd] = b odd += 2 return bytes(buf[0x100:0x200])
def _parse_gbx_footer(rom: FileROM, metadata: 'Metadata'): footer = rom.read(seek_to=rom.size - 64, amount=64) if footer[60:64] != b'GBX!': if main_config.debug: print(rom.path, 'GBX footer is invalid, siggy is', footer[60:64]) return if int.from_bytes(footer[48:52], 'big') != 64 or int.from_bytes(footer[52:56], 'big') != 1: if main_config.debug: print(rom.path, 'GBX has unsupported major version:', int.from_bytes(footer[52:56], 'big'), 'or size:', int.from_bytes(footer[48:52], 'big')) return #56:60 is minor version, which we expect to be 0, but it'd be okay if not original_mapper = metadata.specific_info.get('Mapper', 'None') metadata.specific_info['Stated Mapper'] = original_mapper new_mapper = _gbx_mappers.get(footer[0:4]) if not new_mapper: if main_config.debug: print(rom.path, 'GBX has unknown spooky mapper:', footer[0:4]) new_mapper = footer[0:4].decode() if new_mapper != original_mapper: #For now we're going to assume other emus don't actually do .gbx properly metadata.specific_info['Override Mapper?'] = True metadata.specific_info['Mapper'] = new_mapper
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_plain_region(rom: FileROM, metadata: 'Metadata', offset: int, length: int): #Plain region stores the libraries used, at least for official games #See also: https://github.com/Zowayix/ROMniscience/wiki/3DS-libraries-used for research #Hmm… since I sort of abandoned ROMniscience I should put that somewhere else plain_region = rom.read(seek_to=offset, amount=length) libraries = (lib.decode('ascii', errors='backslashreplace') for lib in plain_region.split(b'\x00') if lib) #TODO: If a game has an update which adds functionality identified by one of these library names, then that'll be a separate file, so it's like... how do we know that Super Smash Bros the .3ds file has amiibo support when Super Smash Bros 1.1.7 update data the .cxi is where it says that, because with and only with the update data it would support amiibos, etc; if that all makes sense #Unless like... I search ~/.local/share/citra-emu/sdmc/Nintendo 3DS for what update CIAs are installed and... aaaaaaaaaaaaaaaa for library in libraries: if library.startswith('[SDK+ISP:QRDec'): metadata.specific_info['Reads QR Codes?'] = True elif library.startswith('[SDK+ISP:QREnc'): metadata.specific_info['Makes QR Codes?'] = True elif library == '[SDK+NINTENDO:ExtraPad]': metadata.specific_info['Uses Circle Pad Pro?'] = True #ZL + ZR + right analog stick; New 3DS has these too but the extra controls there are internally represented as a Circle Pad Pro for compatibility so this all works out I think inbuilt_controller = cast( input_metadata.NormalController, cast(input_metadata.CombinedController, metadata.input_info. input_options[0].inputs[0]).components[0]) inbuilt_controller.analog_sticks += 1 inbuilt_controller.shoulder_buttons += 2 elif library == '[SDK+NINTENDO:Gyroscope]': metadata.specific_info['Uses Gyroscope?'] = True metadata.input_info.input_options[0].inputs.append( input_metadata.MotionControls()) elif library == '[SDK+NINTENDO:IsRunOnSnake]': #There's also an IsRunOnSnakeForApplet found in some not-completely-sure-what-they-are builtin apps and amiibo Settings. Not sure if it does what I think it does metadata.specific_info['New 3DS Enhanced?'] = True elif library == '[SDK+NINTENDO:NFP]': metadata.specific_info['Uses Amiibo?'] = True elif library.startswith('[SDK+NINTENDO:CTRFaceLibrary-'): metadata.specific_info['Uses Miis?'] = True
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
def add_ines_metadata(rom: FileROM, metadata: Metadata, header: bytes): metadata.specific_info['Headered?'] = True #Some emulators are okay with not having a header if they have something like an internal database, others are not. #Note that \x00 at the end instead of \x1a indicates this is actually Wii U VC, but it's still the same header format rom.header_length_for_crc_calculation = 16 #We use a custom software list matcher anyway, but we need to just chop the header off to find it in libretro-database prg_size = header[4] chr_size = header[5] flags = header[6] has_battery = (flags & 2) > 0 metadata.save_type = SaveType.Cart if has_battery else SaveType.Nothing if (flags & 4) > 0: metadata.specific_info['Has iNES Trainer?'] = True mapper_lower_nibble = (flags & 0b1111_0000) >> 4 more_flags = header[7] if (more_flags & 3) == 1: metadata.specific_info['Arcade System'] = 'VS Unisystem' elif (more_flags & 3) == 2: metadata.specific_info['Arcade System'] = 'PlayChoice-10' mapper_upper_nibble = more_flags & 0b1111_0000 is_nes_2_0 = ((more_flags & 0b_00_00_11_00) >> 2) == 2 if is_nes_2_0: metadata.specific_info['Header Format'] = 'NES 2.0' mapper_upper_upper_nibble = header[8] & 0b1111 mapper = mapper_lower_nibble | mapper_upper_nibble | (mapper_upper_upper_nibble << 8) metadata.specific_info['Mapper Number'] = mapper if mapper in _ines_mappers: metadata.specific_info['Mapper'] = _ines_mappers[mapper] else: metadata.specific_info['Mapper'] = 'NES 2.0 Mapper %d' % mapper metadata.specific_info['Submapper'] = (header[8] & 0b1111_0000) >> 4 prg_size_msb = ((header[9] & 0b1111) << 4) metadata.specific_info['PRG Size'] = (prg_size_msb | prg_size) * 16 * 1024 if prg_size_msb != 15 else (2 ** ((prg_size & 0b1111_1100) >> 2)) * (((prg_size & 0b11) * 2) + 1) chr_size_msb = (header[9] & 0b1111_0000) metadata.specific_info['CHR Size'] = (chr_size_msb | chr_size) * 8 * 1024 if chr_size_msb != 15 else (2 ** ((chr_size & 0b1111_1100) >> 2)) * (((chr_size & 0b11) * 2) + 1) #9/10: PRG/CHR RAM and NVRAM size cpu_ppu_timing = header[12] & 0b11 if cpu_ppu_timing == 0: metadata.specific_info['TV Type'] = TVSystem.NTSC elif cpu_ppu_timing == 1: metadata.specific_info['TV Type'] = TVSystem.PAL elif cpu_ppu_timing == 2: metadata.specific_info['TV Type'] = TVSystem.Agnostic elif cpu_ppu_timing == 3: metadata.specific_info['Is Dendy?'] = True if (header[7] & 3) == 3: #If header[7] = 1, specifies VS System type metadata.specific_info['Extended Console Type'] = extended_console_types.get(header[13], header[13]) if header[15]: default_expansion_device = default_expansion_devices.get(header[15], header[15]) metadata.specific_info['Default Expansion Device'] = default_expansion_device if default_expansion_device == 1: metadata.specific_info['Peripheral'] = NESPeripheral.NormalController #42 = multicart also exists I guess but it doesn't mean much to us else: metadata.specific_info['Header Format'] = 'iNES' mapper = mapper_lower_nibble | mapper_upper_nibble metadata.specific_info['Mapper Number'] = mapper if mapper in _ines_mappers: metadata.specific_info['Mapper'] = _ines_mappers[mapper] else: metadata.specific_info['Mapper'] = 'iNES Mapper %d' % mapper metadata.specific_info['PRG Size'] = prg_size * 16 * 1024 metadata.specific_info['CHR Size'] = chr_size * 8 * 1024
def _add_wii_disc_metadata(rom: FileROM, metadata: 'Metadata'): wii_header = rom.read(0x40_000, 0xf000) game_partition_offset = None for i in range(4): partition_group = wii_header[8 * i: (8 * i) + 8] partition_count = int.from_bytes(partition_group[0:4], 'big') partition_table_entry_offset = int.from_bytes(partition_group[4:8], 'big') << 2 for j in range(partition_count): seek_to = partition_table_entry_offset + (j * 8) partition_table_entry = rom.read(seek_to, 8) partition_offset = int.from_bytes(partition_table_entry[0:4], 'big') << 2 partition_type = int.from_bytes(partition_table_entry[4:8], 'big') #if partition_type > 0xf: # #SSBB Masterpiece partitions use ASCII title IDs here; realistically other partition types should be 0 (game) 1 (update) or 2 (channel) # partition_type = partition_table_entry[4:8].decode('ascii', errors='backslashreplace') #Seemingly most games have an update partition at 0x50_000 and a game partition at 0xf_800_000. That's just an observation though and may not be 100% the case #print(rom.path, 'has partition type', partition_type, 'at', hex(partition_offset)) if partition_type == 1: metadata.specific_info['Has Update Partition?'] = True elif partition_type == 0 and game_partition_offset is None: game_partition_offset = partition_offset common_key = None if _wii_config: common_key = _wii_config.options.get('common_key') if common_key: if game_partition_offset and have_pycrypto: game_partition_header = rom.read(game_partition_offset, 0x2c0) title_iv = game_partition_header[0x1dc:0x1e4] + (b'\x00' * 8) data_offset = int.from_bytes(game_partition_header[0x2b8:0x2bc], 'big') << 2 master_key = bytes.fromhex(common_key) aes = AES.new(master_key, AES.MODE_CBC, title_iv) encrypted_key = game_partition_header[0x1bf:0x1cf] key = aes.decrypt(encrypted_key) chunk_offset = game_partition_offset + data_offset # + (index * 0x8000) but we only need 1st chunk (0x7c00 bytes of encrypted data each chunk) chunk = rom.read(chunk_offset, 0x8000) chunk_iv = chunk[0x3d0:0x3e0] aes = AES.new(key, AES.MODE_CBC, chunk_iv) decrypted_chunk = aes.decrypt(chunk[0x400:]) #TODO: Try and read filesystem to see if there is an opening.bnr in there (should be) try: apploader_date = decrypted_chunk[0x2440:0x2450].decode('ascii').rstrip('\0') try: d = datetime.strptime(apploader_date, '%Y/%m/%d') metadata.specific_info['Build Date'] = Date(d.year, d.month, d.day) guessed = Date(d.year, d.month, d.day, True) if guessed.is_better_than(metadata.release_date): metadata.release_date = guessed except ValueError: pass except UnicodeDecodeError: pass #Unused (presumably would be region-related stuff): 0xe004:0xe010 region_code = int.from_bytes(wii_header[0xe000:0xe004], 'big') try: metadata.specific_info['Region Code'] = NintendoDiscRegion(region_code) except ValueError: pass parse_ratings(metadata, wii_header[0xe010:0xe020])