def _add_gamecube_disc_metadata(rom: FileROM, metadata: Metadata, header: bytes, tgc_data: Optional[Mapping[str, int]]=None): #TODO: Use namedtuple/dataclass for tgc_data metadata.platform = 'GameCube' if rom.extension != 'tgc': #Not gonna bother working out what's going on with apploader offsets in tgc add_apploader_date(header, metadata) region_code = int.from_bytes(header[0x458:0x45c], 'big') try: metadata.specific_info['Region Code'] = NintendoDiscRegion(region_code) except ValueError: pass if tgc_data: fst_offset = tgc_data['fst offset'] fst_size = tgc_data['fst size'] else: fst_offset = int.from_bytes(header[0x424:0x428], 'big') fst_size = int.from_bytes(header[0x428:0x42c], 'big') try: if tgc_data: add_fst_info(rom, metadata, fst_offset, fst_size, tgc_data['file offset']) else: add_fst_info(rom, metadata, fst_offset, fst_size) except (IndexError, ValueError) as ex: if main_config.debug: print(rom.path, 'encountered error when parsing FST', ex)
def parse_woz_info_chunk(metadata: Metadata, chunk_data: bytes): info_version = chunk_data[0] #1: Disk type = 5.25" if 1 else 3.25 if 2 #2: 1 if write protected if info_version == 2: compatible_hardware = int.from_bytes(chunk_data[40:42], 'little') if compatible_hardware: machines = set() if compatible_hardware & 1: machines.add(AppleIIHardware.AppleII) if compatible_hardware & 2: machines.add(AppleIIHardware.AppleIIPlus) if compatible_hardware & 4: machines.add(AppleIIHardware.AppleIIE) if compatible_hardware & 8: machines.add(AppleIIHardware.AppleIIC) if compatible_hardware & 16: machines.add(AppleIIHardware.AppleIIEEnhanced) if compatible_hardware & 32: machines.add(AppleIIHardware.AppleIIgs) if compatible_hardware & 64: machines.add(AppleIIHardware.AppleIICPlus) if compatible_hardware & 128: machines.add(AppleIIHardware.AppleIII) if compatible_hardware & 256: machines.add(AppleIIHardware.AppleIIIPlus) metadata.specific_info['Machine'] = machines minimum_ram = int.from_bytes(chunk_data[42:44], 'little') if minimum_ram: metadata.specific_info['Minimum RAM'] = minimum_ram
def parse_genre(self, metadata: Metadata, genre_list: str): #genres = [g.title() for g in genre_list.split(',')] genres = genre_list.split(',') if 'software' in genres: #This isn't really a genre so much as a category genres.remove('software') if not genres: return items = tuple(self._organize_genres(genres).items()) if items: metadata.genre = items[0][0].title() subgenres = items[0][1] if subgenres: metadata.subgenre = ', '.join(s.title() for s in subgenres) if len(items) > 1: metadata.specific_info['Additional Genres'] = ', '.join( [g[0].title() for g in items[1:]]) additional_subgenres = { s.title() for g in items[1:] for s in g[1] } if additional_subgenres: metadata.specific_info['Additional Subgenres'] = ', '.join( additional_subgenres)
def add_peripherals_info(metadata: Metadata, peripherals): metadata.specific_info['Uses Windows CE?'] = (peripherals & 1) > 0 metadata.specific_info['Supports VGA?'] = (peripherals & (1 << 4)) > 0 metadata.specific_info['Uses Other Expansions?'] = ( peripherals & (1 << 8)) > 0 #How very vague and mysterious… metadata.specific_info['Force Feedback?'] = (peripherals & (1 << 9)) > 0 metadata.specific_info['Supports Microphone?'] = (peripherals & (1 << 10)) > 0 metadata.save_type = SaveType.MemoryCard if peripherals & ( 1 << 11) else SaveType.Nothing #TODO: Set up metadata.input_info button_bits = { 'Start, A, B, D-pad': 12, 'C': 13, #Naomi? 'D': 14, #Naomi? 'X': 15, 'Y': 16, 'Z': 17, 'Exanded D-pad': 18, #Some kind of second dpad? 8-way? What does this mean 'Analog R': 19, 'Analog L': 20, 'Analog Horizontal': 21, 'Analog Vertical': 22, 'Expanded Analog Horizontal': 23, #What does this mean 'Expanded Analog Vertical': 24, } buttons = {k for k, v in button_bits.items() if peripherals & (1 << v)} metadata.specific_info['Controls Used'] = buttons metadata.specific_info['Uses Gun?'] = (peripherals & (1 << 25)) > 0 metadata.specific_info['Uses Keyboard?'] = (peripherals & (1 << 26)) > 0 metadata.specific_info['Uses Mouse?'] = (peripherals & (1 << 27)) > 0
def add_related_images(self, metadata: Metadata): for image_name, config_key in image_config_keys.items(): image = get_image(config_key, self.software_list_name, self.name) if image: metadata.images[image_name] = image continue if self.parent_name: image = get_image(config_key, self.software_list_name, self.parent_name) if image: metadata.images[image_name] = image
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_gamecube_banner_text(metadata: Metadata, banner_bytes: bytes, encoding: str, lang: Optional[str]=None): short_title_line_1 = banner_bytes[0:0x20].decode(encoding, errors='backslashreplace').rstrip('\0 ') short_title_line_2 = banner_bytes[0x20:0x40].decode(encoding, errors='backslashreplace').rstrip('\0 ') title_line_1 = banner_bytes[0x40:0x80].decode(encoding, errors='backslashreplace').rstrip('\0 ') title_line_2 = banner_bytes[0x80:0xc0].decode(encoding, errors='backslashreplace').rstrip('\0 ') description = banner_bytes[0xc0:0x140].decode(encoding, errors='backslashreplace').rstrip('\0 ').replace('\n', ' ') prefix = 'Banner' if lang: prefix = '{0} {1}'.format(lang, prefix) metadata.add_alternate_name(short_title_line_1, '{0} Short Title'.format(prefix)) metadata.specific_info['{0} Short Title Line 2'.format(prefix)] = short_title_line_2 metadata.add_alternate_name(title_line_1, '{0} Title'.format(prefix)) metadata.specific_info['{0} Title Line 2'.format(prefix)] = title_line_2 metadata.descriptions['{0} Description'.format(prefix)] = description
def add_apploader_date(header: bytes, metadata: Metadata): try: apploader_date = header[0x2440:0x2450].decode('ascii').rstrip('\0') try: actual_date = datetime.strptime(apploader_date, '%Y/%m/%d') year = actual_date.year month = actual_date.month day = actual_date.day metadata.specific_info['Build Date'] = Date(year, month, day) if not metadata.release_date or metadata.release_date.is_guessed: metadata.release_date = Date(year, month, day, True) except ValueError: pass except UnicodeDecodeError: pass
def add_banner_info(rom: ROM, metadata: Metadata, banner: bytes): banner_magic = banner[:4] if banner_magic in {b'BNR1', b'BNR2'}: #(BNR2 has 6 instances of all of these with English, German, French, Spanish, Italian, Dutch in that order) #Dolphin uses line 2 as Publisher field but that's not always accurate (e.g. Paper Mario: The Thousand Year Door puts subtitle of the game's name on line 2) so it won't be used here #Very often, short title and not-short title are exactly the same, but not always. I guess it just be like that encoding = 'shift_jis' if metadata.specific_info['Region Code'] == NintendoDiscRegion.NTSC_J else 'latin-1' parse_gamecube_banner_text(metadata, banner[0x1820:0x1960], encoding) if banner_magic == b'BNR2': languages = { #0: English, but we have already done that 1: 'German', 2: 'French', 3: 'Spanish', 4: 'Italian', 5: 'Dutch', } for i, lang_name in languages.items(): offset = 0x1820 + (i * 0x140) parse_gamecube_banner_text(metadata, banner[offset: offset + 0x140], encoding, lang_name) if have_pillow: metadata.images['Banner'] = decode_icon(banner) else: if main_config.debug: print('Invalid banner magic', rom.path, banner_magic)
def _parse_peripherals(metadata: Metadata, peripherals: Collection[str]): for peripheral_char in peripherals: if peripheral_char == 'M': #3 buttons if I'm not mistaken mouse = input_metadata.Mouse() mouse.buttons = 3 metadata.input_info.add_option(mouse) elif peripheral_char == 'V': #Is this just the SMS paddle? metadata.input_info.add_option(input_metadata.Paddle()) elif peripheral_char == 'A': xe_1_ap = input_metadata.NormalController() xe_1_ap.face_buttons = 10 xe_1_ap.shoulder_buttons = 4 xe_1_ap.analog_sticks = 2 #The second one only has one axis, though metadata.input_info.add_option(xe_1_ap) elif peripheral_char == 'G': menacer = input_metadata.LightGun() menacer.buttons = 2 #Also pause button metadata.input_info.add_option(menacer) elif peripheral_char == 'K': xband_keyboard = input_metadata.Keyboard() xband_keyboard.keys = 68 #I think I counted that right... I was just looking at the picture metadata.input_info.add_option(xband_keyboard) elif peripheral_char == 'J': metadata.input_info.add_option(standard_gamepad) elif peripheral_char == '6': six_button_gamepad = input_metadata.NormalController() six_button_gamepad.face_buttons = 6 six_button_gamepad.dpads = 1 metadata.input_info.add_option(six_button_gamepad) metadata.specific_info['Uses 6-Button Controller?'] = True elif peripheral_char == '0': sms_gamepad = input_metadata.NormalController() sms_gamepad.face_buttons = 2 sms_gamepad.dpads = 1 metadata.input_info.add_option(sms_gamepad) elif peripheral_char == 'L': #Activator metadata.input_info.add_option(input_metadata.MotionControls()) elif peripheral_char in {'4', 'O'}: #Team Play and J-Cart respectively #num_players = 4 pass elif peripheral_char == 'C': metadata.specific_info['Uses CD?'] = True
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_info_from_copyright_string(metadata: Metadata, copyright_string: str): metadata.specific_info['Copyright'] = copyright_string copyright_match = _copyright_regex.match(copyright_string) if copyright_match: maker = copyright_match[1].strip().rstrip(',') maker = _t_with_zero.sub('T-', maker) maker = _t_not_followed_by_dash.sub('T-', maker) if maker in _licensee_codes: metadata.publisher = _licensee_codes[maker] year = copyright_match[2] month: Union[str, int] try: month = datetime.strptime(copyright_match[3], '%b').month except ValueError: #There are other spellings such as JUR, JLY out there, but oh well month = '??' metadata.specific_info['Copyright Date'] = Date(year, month) if not metadata.release_date: metadata.release_date = Date(year, month, is_guessed=True)
def add_woz_metadata(rom: 'FileROM', metadata: Metadata): #https://applesaucefdc.com/woz/reference1/ #https://applesaucefdc.com/woz/reference2/ magic = rom.read(amount=8) if magic == b'WOZ1\xff\n\r\n': metadata.specific_info['ROM Format'] = 'WOZ v1' elif magic == b'WOZ2\xff\n\r\n': metadata.specific_info['ROM Format'] = 'WOZ v2' else: print('Weird .woz magic', rom.path, magic) return position = 12 size = rom.size while position: position = parse_woz_chunk(rom, metadata, position) if position >= size: break if 'Header-Title' in metadata.names and 'Subtitle' in metadata.specific_info: metadata.add_alternate_name(metadata.names['Header Title'] + ': ' + metadata.specific_info['Subtitle'], 'Header Title with Subtitle')
def add_atari_5200_footer_garbage_info(rom: 'FileROM', metadata: Metadata): footer = rom.read(seek_to=rom.size - 24, amount=24) year = footer[20:22] #Y2K incompliant whee #Entry point: 22-23, lil' endian if year[1] != 255: #If set to this, the BIOS is skipped? title_bytes = footer[:20].rstrip(b'\0') if title_bytes: title = ''.join( atari_5200_charset.get(b, '\0x{0:x}'.format(b)) for b in title_bytes) metadata.add_alternate_name(title.strip(), 'Banner Title') try: year_first_digit = int(atari_5200_charset[year[0]]) year_second_digit = int(atari_5200_charset[year[1]]) terrible_date = Date(year=1900 + (year_first_digit * 10) + year_second_digit, is_guessed=True) if terrible_date.is_better_than(metadata.release_date): metadata.release_date = terrible_date except (ValueError, KeyError): pass
def add_info_from_system_cnf(metadata: Metadata, system_cnf: str): for line in system_cnf.splitlines(): boot_line_match = _boot_line_regex.match(line) if boot_line_match: filename = boot_line_match[1] metadata.specific_info['Executable Name'] = filename boot_file_match = _boot_file_regex.match(filename) if boot_file_match: metadata.product_code = boot_file_match[ 1] + '-' + boot_file_match[2] + boot_file_match[3] #Can look this up in /usr/local/share/games/PCSX2/GameIndex.dbf to get PCSX2 compatibility I guess else: other_line_match = _other_systemcnf_line_regex.match(line) if other_line_match: key = other_line_match[1] value = other_line_match[2] if key == 'VER': metadata.specific_info['Version'] = value elif key == 'VMODE': try: metadata.specific_info['TV Type'] = TVSystem[value] except ValueError: pass
def add_cover(metadata: Metadata, product_code: str, licensee_code: str): #Intended for the covers database from GameTDB if 'Wii' not in platform_configs: return covers_path = platform_configs['Wii'].options.get('covers_path') if not covers_path: return cover_path = covers_path.joinpath(product_code + licensee_code) for ext in ('png', 'jpg'): potential_cover_path = cover_path.with_suffix(os.extsep + ext) if potential_cover_path.is_file(): metadata.images['Cover'] = potential_cover_path return
def add_info_from_uze_header(header: bytes, metadata: Metadata): #Header version: 6 #Target: 7 (0 = ATmega644, 1 = reserved for ATmega1284) #Program size: 8-0xc (LE) metadata.release_date = Date(int.from_bytes(header[0xc:0xe], 'little')) metadata.add_alternate_name( header[0xe:0x2e].decode('ascii', errors='backslashreplace').rstrip('\0'), 'Banner Title') metadata.developer = metadata.publisher = header[0x2e:0x4e].decode( 'ascii', errors='backslashreplace').rstrip('\0') #Icon (sadly unused) (16 x 16, BBGGGRRR): 0x4e:0x14e #CRC32: 0x14e:0x152 uses_mouse = header[0x152] == 1 metadata.specific_info['Uses Mouse?'] = uses_mouse #Potentially it could use other weird SNES peripherals but this should do metadata.input_info.add_option( snes_controllers.mouse if uses_mouse else snes_controllers.controller) description = header[0x153:0x193].decode( 'ascii', errors='backslashreplace').rstrip('\0') if description: #Official documentation claims this is unused, but it seems that it is used after all (although often identical to title) metadata.descriptions['Banner Description'] = description
def add_megadrive_software_list_metadata(software: 'Software', metadata: Metadata): add_generic_software_info(software, metadata) if software.get_shared_feature('addon') == 'SVP': metadata.specific_info['Expansion Chip'] = 'SVP' if software.get_shared_feature('incompatibility') == 'TMSS': metadata.specific_info['Bad TMSS?'] = True slot = software.get_part_feature('slot') if slot == 'rom_eeprom' or software.has_data_area('sram'): metadata.save_type = SaveType.Cart elif metadata.platform == 'Mega Drive': metadata.save_type = SaveType.Nothing if software.name == 'aqlian': #This is naughty, but this bootleg game doesn't run on some stuff so I want to be able to detect it metadata.specific_info['Mapper'] = 'aqlian' else: if slot: if slot not in ('rom_sram', 'rom_fram'): mapper = slot[4:] if slot.startswith('rom_') else slot if mapper in { 'eeprom', 'nbajam_alt', 'nbajamte', 'nflqb96', 'cslam', 'nhlpa', 'blara', 'eeprom_mode1' }: metadata.specific_info['Mapper'] = 'EEPROM' elif mapper == 'jcart': metadata.specific_info['Mapper'] = 'J-Cart' elif mapper in {'codemast', 'mm96'}: metadata.specific_info['Mapper'] = 'J-Cart + EEPROM' else: #https://github.com/mamedev/mame/blob/master/src/devices/bus/megadrive/md_carts.cpp metadata.specific_info['Mapper'] = mapper if software.name == 'pokemon' and software.software_list_name == 'megadriv': #This is also a bit naughty, but Pocket Monsters has different compatibility compared to other games with rom_kof99 metadata.specific_info['Mapper'] = slot[4:] + '_pokemon'
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 add_nes_software_list_metadata(software: 'Software', metadata: Metadata): software.add_standard_metadata(metadata) nes_peripheral = None #FIXME: Acktually, you can have multiple feature = peripherals #See also: SMB / Duck Hunt / World Class Track Meet multicart, with both zapper and powerpad #Actually, how does that even work in real life? Are the controllers hotplugged? Different ports? peripheral = software.get_part_feature('peripheral') if peripheral == 'zapper': nes_peripheral = NESPeripheral.Zapper zapper = input_metadata.LightGun() zapper.buttons = 1 metadata.input_info.add_option(zapper) elif peripheral == 'vaus': nes_peripheral = NESPeripheral.ArkanoidPaddle vaus = input_metadata.Paddle() vaus.buttons = 1 metadata.input_info.add_option(vaus) #Can still use standard controller metadata.input_info.add_option(_standard_controller) elif peripheral in {'powerpad', 'ftrainer', 'fffitness'}: nes_peripheral = NESPeripheral.PowerPad power_pad = input_metadata.NormalController() power_pad.face_buttons = 12 #"face" metadata.input_info.add_option(power_pad) elif peripheral == 'powerglove': nes_peripheral = NESPeripheral.PowerGlove #Hmm... apparently it functions as a standard NES controller, but there are 2 games specifically designed for glove usage? So it must do something extra I guess power_glove = input_metadata.MotionControls() #game.metadata.input_info.buttons = 11 #Standard A + B + 9 program buttons metadata.input_info.add_option(power_glove) elif peripheral == 'rob': nes_peripheral = NESPeripheral.ROB #I'll leave input info alone, because I'm not sure how I would classify ROB metadata.input_info.add_option(_standard_controller) elif peripheral == 'fc_keyboard': nes_peripheral = NESPeripheral.FamicomKeyboard famicom_keyboard = input_metadata.Keyboard() famicom_keyboard.keys = 72 metadata.input_info.add_option(famicom_keyboard) elif peripheral == 'subor_keyboard': nes_peripheral = NESPeripheral.SuborKeyboard subor_keyboard = input_metadata.Keyboard() subor_keyboard.keys = 96 metadata.input_info.add_option(subor_keyboard) elif peripheral == 'mpiano': nes_peripheral = NESPeripheral.Piano #Apparently, it's actually just a MIDI keyboard, hence the MAME driver adds MIDI in/out ports miracle_piano = input_metadata.Custom('88-key piano') #game.metadata.input_info.buttons = 88 metadata.input_info.add_option(miracle_piano) else: metadata.input_info.add_option(_standard_controller) #Well, it wouldn't be a controller... not sure how this one works exactly metadata.specific_info['Uses 3D Glasses?'] = peripheral == '3dglasses' if peripheral == 'turbofile': #Thing that goes into Famicom controller expansion port and saves stuff metadata.save_type = SaveType.MemoryCard #There's a "battlebox" which Armadillo (Japan) uses? #Barcode World (Japan) uses "barcode" #Peripheral = 4p_adapter: 4 players #Gimmi a Break stuff: "partytap"? #Hyper Olympic (Japan): "hypershot" #Ide Yousuke Meijin no Jissen Mahjong (Jpn, Rev. A): "mjcontroller" (mahjong controller?) #RacerMate Challenge 2: "racermate" #Top Rider (Japan): "toprider" metadata.add_notes(software.infos.get('usage')) #This only works on a Famicom with Mahjong Controller attached #This only is only supported by Famicom [sic?] if nes_peripheral: metadata.specific_info['Peripheral'] = nes_peripheral
def __init__(self, machine: 'Machine', platform_config: 'PlatformConfig'): super().__init__(platform_config) self.machine = machine self.metadata = Metadata() self._add_metadata_fields()
def add_standard_metadata(self, metadata: Metadata): metadata.specific_info['MAME Software'] = self #We'll need to use that as more than just a name, though, I think; and by that I mean I get dizzy if I think about whether I need to do that or not right now #TODO: Whatever is checking metadata.names needs to just check for game.software etc manually rather than this being here, I think metadata.add_alternate_name(self.description, 'Software List Name') metadata.specific_info['MAME Software List'] = self.software_list if not metadata.product_code: metadata.product_code = self.serial barcode = self.infos.get('barcode') if barcode: metadata.specific_info['Barcode'] = barcode ring_code = self.infos.get('ring_code') if ring_code: metadata.specific_info['Ring Code'] = ring_code version = self.infos.get('version') if version: if version[0].isdigit(): version = 'v' + version metadata.specific_info['Version'] = version alt_title = self.infos.get('alt_title', self.infos.get('alt_name', self.infos.get('alt_disk'))) if alt_title: _add_alt_titles(metadata, alt_title) year_text = self.xml.findtext('year') if year_text: year_guessed = False if len(year_text) == 5 and year_text[-1] == '?': #Guess I've created a year 10000 problem, please fix this code in several millennia to be more smart year_guessed = True year_text = year_text[:-1] year = Date(year_text, is_guessed=year_guessed) if year.is_better_than(metadata.release_date): metadata.release_date = year release = self.infos.get('release') release_date: Optional[Date] = None if release: release_date = _parse_release_date(release) if release_date: if release_date.is_better_than(metadata.release_date): metadata.release_date = release_date developer = consistentify_manufacturer(self.infos.get('developer')) if not developer: developer = consistentify_manufacturer(self.infos.get('author')) if not developer: developer = consistentify_manufacturer(self.infos.get('programmer')) if developer: metadata.developer = developer publisher = consistentify_manufacturer(self.xml.findtext('publisher')) if publisher: already_has_publisher = metadata.publisher and (not metadata.publisher.startswith('<unknown')) if publisher in {'<doujin>', '<homebrew>', '<unlicensed>'} and developer: metadata.publisher = developer elif not (already_has_publisher and (publisher == '<unknown>')): if ' / ' in publisher: publishers: Collection[str] = set(cast(str, consistentify_manufacturer(p)) for p in publisher.split(' / ')) if main_config.sort_multiple_dev_names: publishers = sorted(publishers) publisher = ', '.join(publishers) metadata.publisher = publisher self.add_related_images(metadata) add_history(metadata, self.software_list_name, self.name)
def parse_woz_kv(rompath: str, metadata: Metadata, key: str, value: str): #rompath is just here for making warnings look better which is a bit silly I think… hm if key in {'side', 'side_name', 'contributor', 'image_date', 'collection', 'requires_platform'}: #No use for these #"collection" is not part of the spec but it shows up and it just says where the image came from #requires_platform is not either, it just seems to be "apple2" so far and I don't get it return if key == 'version': #Note that this is free text if not value.startswith('v'): value = 'v' + value metadata.specific_info['Version'] = value elif key == 'title': metadata.add_alternate_name(value, 'Header Title') elif key == 'subtitle': metadata.specific_info['Subtitle'] = value elif key == 'requires_machine': if metadata.specific_info.get('Machine'): #Trust the info from the INFO chunk more if it exists return machines = set() for machine in value.split('|'): if machine in woz_meta_machines: machines.add(woz_meta_machines[machine]) else: print('Unknown compatible machine in Woz META chunk', rompath, machine) metadata.specific_info['Machine'] = machines elif key == 'requires_ram': #Should be in INFO chunk, but sometimes isn't if value[-1].lower() == 'k': value = value[:-1] try: metadata.specific_info['Minimum RAM'] = int(value) except ValueError: pass elif key == 'publisher': metadata.publisher = consistentify_manufacturer(value) elif key == 'developer': metadata.developer = consistentify_manufacturer(value) elif key == 'copyright': metadata.specific_info['Copyright'] = value try: metadata.release_date = Date(value) except ValueError: pass elif key == 'language': metadata.languages = {lang for lang in (get_language_by_english_name(lang_name) for lang_name in value.split('|')) if lang} elif key == 'genre': #This isn't part of the specification, but I've seen it if value == 'rpg': metadata.genre = 'RPG' else: metadata.genre = value.capitalize() if value.islower() else value elif key == 'notes': #This isn't part of the specification, but I've seen it metadata.add_notes(value) else: if main_config.debug: print('Unknown Woz META key', rompath, key, value)
def _add_alt_titles(metadata: Metadata, alt_title: str): #Argh this is annoying because we don't want to split in the middle of brackets for piece in _split_preserve_brackets.split(alt_title): ends_with_brackets_match = _ends_with_brackets.match(piece) if ends_with_brackets_match: name_type = ends_with_brackets_match[2] if name_type in {'Box', 'USA Box', 'US Box', 'French Box', 'Box?', 'Cart', 'cart', 'Label', 'label', 'Fra Box'}: #There must be a better way for me to do this… metadata.add_alternate_name(ends_with_brackets_match[1], name_type.title() + ' Title') elif name_type in {'Box, Cart', 'Box/Card'}: #Grr metadata.add_alternate_name(ends_with_brackets_match[1], 'Box Title') metadata.add_alternate_name(ends_with_brackets_match[1], 'Cart Title') elif name_type == 'Japan': metadata.add_alternate_name(ends_with_brackets_match[1], 'Japanese Name') elif name_type == 'China': metadata.add_alternate_name(ends_with_brackets_match[1], 'Chinese Name') else: #Sometimes the brackets are actually part of the name metadata.add_alternate_name(piece, name_type) else: metadata.add_alternate_name(piece)
def _add_info_from_tdb_entry(tdb: TDB, db_entry: ElementTree.Element, metadata: Metadata): metadata.add_alternate_name(db_entry.attrib['name'], 'GameTDB Name') #(Pylint is on drugs if I don't add more text here) id: What we just found #(it thinks I need an indented block) type: 3DS, 3DSWare, VC, etc (we probably don't need to worry about that) #region: PAL, etc (we can see region code already) #languages: "EN" "JA" etc (I guess we could parse this if the filename isn't good enough for us) #rom: What they think the ROM should be named #case: Has "color" and "versions" attribute? I don't know what versions does but I presume it all has to do with the db_entry box if main_config.debug: for element in db_entry: if element.tag not in ('developer', 'publisher', 'date', 'rating', 'id', 'type', 'region', 'languages', 'locale', 'genre', 'wi-fi', 'input', 'rom', 'case', 'save'): print('uwu', db_entry.attrib['name'], 'has unknown', element, 'tag') developer = db_entry.findtext('developer') if developer and developer != 'N/A': metadata.developer = _clean_up_company_name(developer) publisher = db_entry.findtext('publisher') if publisher: metadata.publisher = _clean_up_company_name(publisher) date = db_entry.find('date') if date is not None: year = date.attrib.get('year') month = date.attrib.get('month') day = date.attrib.get('day') if any([year, month, day]): metadata.release_date = Date(year, month, day) genre = db_entry.findtext('genre') if genre: tdb.parse_genre(metadata, genre) for locale in db_entry.iterfind('locale'): synopsis = locale.findtext('synopsis') if synopsis: key_name = f"Synopsis-{locale.attrib.get('lang')}" if 'lang' in locale.attrib else 'Synopsis' metadata.descriptions[key_name] = synopsis rating = db_entry.find('rating') if rating is not None: #Rating board (attrib "type") is implied by region (db_entrys released in e.g. both Europe and Australia just tend to not have this here) value = rating.attrib.get('value') if value: metadata.specific_info['Age Rating'] = value descriptors = {e.text for e in rating.iterfind('descriptor')} if descriptors: metadata.specific_info['Content Warnings'] = descriptors #This stuff will depend on platform… save = db_entry.find('save') if save is not None: blocks = save.attrib.get('blocks') #Other platforms may have "size" instead, also there are "copy" and "move" attributes which we'll ignore if blocks: if metadata.platform == 'Wii': metadata.save_type = SaveType.Internal elif metadata.platform == 'GameCube': metadata.save_type = SaveType.MemoryCard #Have not seen a db_entry with blocks = 0 or missing blocks or size if metadata.platform != 'GameCube': wifi = db_entry.find('wi-fi') if wifi: features = {feature.text for feature in wifi.iterfind('feature')} metadata.specific_info['Wifi Features'] = features #online, download, score, nintendods input_element = db_entry.find('input') if input_element is not None: #TODO: DS has players-multi-cart and players-single-cart instead (which one do I want?) number_of_players = input_element.attrib.get('players', None) if number_of_players is not None: #Maybe 0 could be a valid amount? For like demos or something metadata.specific_info['Number of Players'] = number_of_players if metadata.platform != 'GameCube': #wiimote, nunchuk, motionplus, db_entrycube, nintendods, classiccontroller, wheel, zapper, balanceboard, wiispeak, microphone, guitar, drums, dancepad, keyboard, draw optional_controls = set() required_controls = set() for control in input_element.iterfind('control'): control_type = control.attrib.get('type') if control.attrib.get('required', 'false') == 'true': required_controls.add(control_type) else: optional_controls.add(control_type) #cbf setting up input_info just yet metadata.specific_info[ 'Optional Additional Controls'] = optional_controls metadata.specific_info[ 'Required Additional Controls'] = required_controls
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 parse_unif_chunk(metadata: Metadata, chunk_type: str, chunk_data: bytes): if chunk_type == 'PRG0': metadata.specific_info['PRG CRC'] = get_crc32_for_software_list(chunk_data) elif chunk_type.startswith('CHR'): metadata.specific_info['CHR CRC'] = get_crc32_for_software_list(chunk_data) elif chunk_type == 'MAPR': metadata.specific_info['Mapper'] = chunk_data.decode('utf-8', errors='ignore').rstrip('\0') elif chunk_type == 'TVCI': tv_type = chunk_data[0] if tv_type == 0: metadata.specific_info['TV Type'] = TVSystem.NTSC elif tv_type == 1: metadata.specific_info['TV Type'] = TVSystem.PAL elif tv_type == 2: metadata.specific_info['TV Type'] = TVSystem.Agnostic elif chunk_type == 'BATR': metadata.save_type = SaveType.Cart if chunk_data[0] else SaveType.Nothing elif chunk_type == 'CTRL': controller_info = chunk_data[0] #TODO: This is a bitfield, so actually one could have multiple peripherals if controller_info & 16: metadata.specific_info['Peripheral'] = NESPeripheral.PowerPad if controller_info & 8: metadata.specific_info['Peripheral'] = NESPeripheral.ArkanoidPaddle if controller_info & 4: metadata.specific_info['Peripheral'] = NESPeripheral.ROB if controller_info & 2: metadata.specific_info['Peripheral'] = NESPeripheral.Zapper if controller_info & 1: metadata.specific_info['Peripheral'] = NESPeripheral.NormalController elif chunk_type == 'READ': metadata.add_notes(chunk_data.decode('utf-8', errors='ignore').rstrip('\0')) elif chunk_type == 'NAME': metadata.add_alternate_name(chunk_data.decode('utf-8', errors='ignore').rstrip('\0'), 'Header Title')
def add_megadrive_info(metadata: Metadata, header: bytes): try: console_name = header[:16].decode('ascii') except UnicodeDecodeError: metadata.specific_info['Bad TMSS?'] = True return if not console_name.startswith('SEGA') and not console_name.startswith( ' SEGA') and console_name not in ('IMA IKUNOUJYUKU ', 'IMA IKUNOJYUKU ', 'SAMSUNG PICO '): metadata.specific_info['Console Name'] = console_name metadata.specific_info['Bad TMSS?'] = True return if metadata.platform == 'Mega CD' and console_name.startswith('SEGA 32X'): #Could also set platform to something like "Mega CD 32X" I guess metadata.specific_info['32X Only?'] = True try: copyright_string = header[16:32].decode('ascii') _add_info_from_copyright_string(metadata, copyright_string) except UnicodeDecodeError: pass domestic_title = header[32:80].decode( 'shift_jis', errors='backslashreplace').rstrip('\0 ') overseas_title = header[80:128].decode( 'shift_jis', errors='backslashreplace').rstrip('\0 ') if domestic_title: metadata.specific_info['Internal Title'] = domestic_title if overseas_title: #Often the same as domestic title, but for games that get their names changed yet work on multiple regions, domestic is the title in Japan and and overseas is in USA (and maybe Europe). I don't know what happens if a game is originally in USA then gets its name changed when it goes to Japan, but it might just be "Japan is domestic and everwhere else is overseas" metadata.specific_info['Internal Overseas Title'] = overseas_title #Product type: 128:130, it's usually GM for game but then some other values appear too (especially in Sega Pico) #Space for padding: 130 try: serial = header[131:142].decode('ascii') metadata.product_code = serial[:8].rstrip('\0 ') #- in between version = serial[-2] if version.isdigit(): metadata.specific_info['Revision'] = int(version) except UnicodeDecodeError: pass #Checksum: header[142:144] peripherals = { c for c in header[144:160].decode('ascii', errors='ignore') if c not in ('\x00', ' ') } _parse_peripherals(metadata, peripherals) if metadata.platform == 'Mega Drive': save_id = header[0xb0:0xb4] #Apparently... what the heck #This seems to be different on Mega CD, and also 32X metadata.save_type = SaveType.Cart if save_id[: 2] == b'RA' else SaveType.Nothing modem_info = header[0xbc:0xc8] memo_bytes = header[0xc8:0xf0] modem_string = None if modem_info[:2] == b'MO': metadata.specific_info['Supports Modem?'] = True elif modem_info[:11] == b'No modem...': metadata.specific_info['Supports Modem?'] = False else: modem_string = modem_info.decode('ascii', errors='ignore').strip('\0 ') try: memo = memo_bytes.decode('ascii').strip('\0 ') if modem_string: #Not really correct, but a few homebrews use the modem part to put in a longer message (and sometimes, varying amounts of it - the first 2 or 4 bytes might be filled with garbage data…) memo = modem_string + memo if memo: if memo == 'SV': metadata.specific_info['Expansion Chip'] = 'SVP' else: #This only seems to really be used for homebrews bootlegs etc metadata.descriptions['Memo'] = memo except UnicodeDecodeError: pass regions = header[0xf0:0xf3] region_codes = _parse_region_codes(regions) metadata.specific_info['Region Code'] = region_codes if console_name[:12] == 'SEGA GENESIS' and not region_codes: #Make a cheeky guess (if it wasn't USA it would be SEGA MEGADRIVE etc presumably) metadata.specific_info['Region Code'] = [MegadriveRegionCodes.USA]
def __init__(self) -> None: self.metadata = Metadata()
def __init__(self, software, platform, media_type): self.software = software self.platform = platform self.media_type = media_type self.metadata = Metadata()