def _add_version_resource_info(self, vers: 'macresources.Resource') -> None: version, revision = vers[0:2] self.metadata.specific_info['Version'] = str(version) + '.' + '.'.join('{0:x}'.format(revision)) if vers[2] != 0x80: try: self.metadata.specific_info['Build Stage'] = BuildStage(vers[2]) except ValueError: pass if not self.metadata.categories: self.metadata.categories = ('Betas', ) if vers[3]: #"Non-release" / build number self.metadata.specific_info['Revision'] = vers[3] language_code = int.from_bytes(vers[4:6], 'big') #Or is it a country? I don't know try: #TODO: Fill out region/language fields using this self.metadata.specific_info['Language Code'] = CountryCode(language_code) except ValueError: self.metadata.specific_info['Language Code'] = language_code try: short_version_length = vers[6] #Pascal style strings long_version_length = vers[7+short_version_length] actual_short_version = None actual_long_version = None if short_version_length: short_version = vers[7:7+short_version_length].decode('mac-roman') if short_version.startswith('©'): self.metadata.specific_info['Short Copyright'] = short_version else: actual_short_version = short_version if long_version_length: long_version = vers[7+short_version_length + 1:7+short_version_length + 1 + long_version_length].decode('mac-roman') copyright_string = None if ', ©' in long_version: actual_long_version, copyright_string = long_version.split(', ©') elif ' ©' in long_version: actual_long_version, copyright_string = long_version.split(' ©') elif '©' in long_version: actual_long_version, copyright_string = long_version.split('©') else: actual_long_version = long_version if copyright_string: copyright_string = copyright_string.rstrip('\0') if copyright_string[:4].isdigit() and (len(copyright_string) == 4 or copyright_string[5] in {',', ' '}): copyright_year = Date(year=copyright_string[:4], is_guessed=True) if copyright_year.is_better_than(self.metadata.release_date): self.metadata.release_date = copyright_year self.metadata.specific_info['Copyright'] = '©' + copyright_string if actual_short_version: self.metadata.specific_info['Version'] = actual_short_version if actual_long_version and actual_long_version != actual_short_version: self.metadata.specific_info['Long Version'] = actual_long_version except UnicodeDecodeError: pass
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 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_ps2_custom_info(game: 'ROMGame'): #.bin/cue also has this system.cnf but I'd need to know how to get pycdlib to work with that if game.rom.extension == 'iso' and have_pycdlib: iso = PyCdlib() try: iso.open(game.rom.path) system_cnf_buf = io.BytesIO() try: #I dunno what the ;1 is for iso.get_file_from_iso_fp(system_cnf_buf, iso_path='/SYSTEM.CNF;1') date_record = iso.get_record(iso_path='/SYSTEM.CNF;1').date #This would be more like a build date (seems to be the same across all files) rather than the release date, but it seems to be close enough year = date_record.years_since_1900 + 1900 month = date_record.month day = date_record.day_of_month build_date = Date(year, month, day) game.metadata.specific_info['Build Date'] = build_date guessed_date = Date(year, month, day, True) if guessed_date.is_better_than(game.metadata.release_date): game.metadata.release_date = guessed_date system_cnf = system_cnf_buf.getvalue().decode( 'utf-8', errors='backslashreplace') add_info_from_system_cnf(game.metadata, system_cnf) except PyCdlibInvalidInput: if main_config.debug: print(game.rom.path, 'has no SYSTEM.CNF inside') #Modules are in IOP, MODULES or IRX but I don't know if we can get any interesting info from that #TODO: Sometimes there is a system.ini that looks like this: #[SYSTEM] #NUMBER = SLUS-21448 #VERSION = 100 #VMODE = NTSC #COUNTRY = AMERICA #LANGUAGE = ENGLISH #WARNING = NO except PyCdlibInvalidISO as ex: if main_config.debug: print(game.rom.path, 'is invalid ISO', ex) except struct.error as ex: print(game.rom.path, 'is invalid ISO and has some struct.error', ex) finally: iso.close() #.elf is just a standard ordinary whole entire .elf if game.metadata.product_code: parse_product_code(game.metadata, game.metadata.product_code)
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 parse_sdsc_header(rom: 'FileROM', metadata: 'Metadata', header: bytes): major_version = decode_bcd(header[0]) minor_version = decode_bcd(header[1]) metadata.specific_info['Version'] = 'v{0}.{1}'.format(major_version, minor_version) day = decode_bcd(header[2]) month = decode_bcd(header[3]) year = _decode_bcd_multi(header[4:6]) metadata.release_date = Date(year, month, day) author_offset = int.from_bytes(header[6:8], 'little') name_offset = int.from_bytes(header[8:10], 'little') description_offset = int.from_bytes(header[10:12], 'little') if 0 < author_offset < 0xffff: #Assume sane maximum of 255 chars try: metadata.developer = metadata.publisher = rom.read(seek_to=author_offset, amount=255).partition(b'\x00')[0].decode('ascii') except UnicodeDecodeError: pass if 0 < name_offset < 0xffff: try: metadata.add_alternate_name(rom.read(seek_to=name_offset, amount=255).partition(b'\x00')[0].decode('ascii'), 'Header Title') except UnicodeDecodeError: pass if 0 < description_offset < 0xffff: try: metadata.descriptions['Description'] = rom.read(seek_to=description_offset, amount=255).partition(b'\x00')[0].decode('ascii') except UnicodeDecodeError: pass
def _add_metadata_fields(self) -> None: self._has_inited_metadata = True self.metadata.specific_info['Source File'] = self.machine.source_file self.metadata.specific_info['Family'] = self.machine.family if self.machine.has_parent: self.metadata.specific_info['Has Parent?'] = True self.metadata.release_date = Date(self.machine.xml.findtext('year')) self.metadata.specific_info[ 'Number of Players'] = self.machine.number_of_players if self.machine.is_mechanical: self.metadata.specific_info['Is Mechanical?'] = True if self.machine.uses_device('ticket_dispenser'): self.metadata.specific_info['Dispenses Tickets?'] = True self.metadata.specific_info['Coin Slots'] = self.machine.coin_slots if self.machine.requires_chds: self.metadata.specific_info['Requires CHD?'] = True if self.machine.romless: self.metadata.specific_info['Romless'] = True self.metadata.specific_info['Slot Names'] = { next(iter(slot.instances))[0] for slot in self.machine.media_slots if slot.instances } #I guess I only expect one? self.metadata.specific_info[ 'Software Lists'] = self.machine.software_list_names self.metadata.series = self.machine.series bios = self.machine.bios if bios: self.metadata.specific_info['BIOS Used'] = bios if self.machine.samples_used: self.metadata.specific_info[ 'Samples Used'] = self.machine.samples_used arcade_system = self.machine.arcade_system if arcade_system: self.metadata.specific_info['Arcade System'] = arcade_system licensed_from = self.machine.licensed_from if self.machine.licensed_from: self.metadata.specific_info['Licensed From'] = licensed_from hacked_by = self.machine.hacked_by if self.machine.hacked_by: self.metadata.specific_info['Hacked By'] = hacked_by self.metadata.developer, self.metadata.publisher = self.machine.developer_and_publisher self.metadata.specific_info[ 'BestGames Rating'] = self.machine.bestgames_opinion self.metadata.specific_info[ 'Version Added'] = self.machine.version_added if self.machine.requires_artwork: self.metadata.specific_info['Requires Artwork?'] = True if self.machine.unofficial: self.metadata.specific_info['Is Unofficial?'] = True if self.machine.no_sound_hardware: self.metadata.specific_info['Has No Sound Hardware?'] = True if self.machine.incomplete: self.metadata.specific_info['Is Incomplete?'] = 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_metadata_for_raw_exe(path: str, metadata: 'Metadata'): props = get_exe_properties(path) if not props: return #Possible values to expect: https://docs.microsoft.com/en-us/windows/win32/api/winver/nf-winver-verqueryvaluea#remarks # if props.get('InternalName'): # if props.get('InternalName') != props.get('OriginalFilename'): # print(path, props.get('InternalName'), props.get('OriginalFilename')) if not metadata.publisher and not metadata.developer: company_name = props.get('CompanyName') if company_name: while junk_suffixes.search(company_name): company_name = junk_suffixes.sub('', company_name) metadata.publisher = company_name product_name = props.get('ProductName') if product_name: metadata.add_alternate_name(product_name, 'Name') copyright_string = props.get('LegalCopyright') if copyright_string: metadata.specific_info['Copyright'] = copyright_string description = props.get('FileDescription') if description and description != product_name: metadata.descriptions['File Description'] = description comments = props.get('Comments') if comments and comments != product_name: metadata.specific_info['File Comment'] = comments trademarks = props.get('LegalTrademarks') if trademarks and trademarks != copyright_string: metadata.specific_info['Trademarks'] = trademarks timedatestamp = props.get('TimeDateStamp') if timedatestamp: if not (timedatestamp > datetime.datetime.now() or timedatestamp.year < 1993): #If the date has not even happened yet, or is before Windows NT 3.1 and hence the PE format was even invented, I think the f**k not build_date = Date(timedatestamp.year, timedatestamp.month, timedatestamp.day) metadata.specific_info['Build Date'] = build_date guessed_date = Date(build_date.year, build_date.month, build_date.day, True) if guessed_date.is_better_than(metadata.release_date): metadata.release_date = guessed_date
def add_psp_iso_info(path: str, metadata: 'Metadata'): iso = PyCdlib() try: iso.open(path) param_sfo_buf = io.BytesIO() try: iso.get_file_from_iso_fp(param_sfo_buf, iso_path='/PSP_GAME/PARAM.SFO') date = iso.get_record(iso_path='/PSP_GAME/PARAM.SFO').date #This would be more like a build date (seems to be the same across all files) rather than the release date year = date.years_since_1900 + 1900 month = date.month day = date.day_of_month metadata.specific_info['Build Date'] = Date(year, month, day) guessed = Date(year, month, day, True) if guessed.is_better_than(metadata.release_date): metadata.release_date = guessed parse_param_sfo(path, metadata, param_sfo_buf.getvalue()) except PyCdlibInvalidInput: try: iso.get_record(iso_path='/UMD_VIDEO/PARAM.SFO') #We could parse this PARAM.SFO but there's not much point given we aren't going to make a launcher for UMD videos at this stage #TODO There is also potentially /UMD_AUDIO/ I think too so I should rewrite this one day metadata.specific_info['PlayStation Category'] = 'UMD Video' return except PyCdlibInvalidInput: if main_config.debug: print(path, 'has no PARAM.SFO inside') if have_pillow: metadata.images['Banner'] = get_image_from_iso( iso, '/PSP_GAME/ICON0.PNG') metadata.images['Icon 1'] = get_image_from_iso( iso, '/PSP_GAME/ICON1.PNG') metadata.images['Picture 0'] = get_image_from_iso( iso, '/PSP_GAME/PIC0.PNG') metadata.images['Background Image'] = get_image_from_iso( iso, '/PSP_GAME/PIC1.PNG') except PyCdlibInvalidISO as ex: if main_config.debug: print(path, 'is invalid ISO', ex) except struct.error as ex: print(path, 'is invalid ISO and has some struct.error', ex) finally: iso.close()
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 _parse_release_date(release_info: str) -> Optional[Date]: if _is_release_date_with_thing_at_end.match(release_info): release_info = release_info[:8] if len(release_info) != 8: return None year = release_info[0:4] month = release_info[4:6] day = release_info[6:8] return Date(year=None if year == 'xxxx' else year, month=None if month == 'xx' else month, day=None if day == 'xx' else day, is_guessed='x' in release_info or '?' in release_info)
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 get_date_from_filename_tags(tags: Sequence[str]) -> Optional[Date]: for tag in tags: if (date_match := _date_regex.match(tag)): groupdict = date_match.groupdict() #I _hate_ this. There's no way I can find to make this code not suck titty balls _year = groupdict.get('year') _year2 = groupdict.get('year2') _year3 = groupdict.get('year3') year = _year if _year else (_year2 if _year2 else _year3) _month = groupdict.get('month') _month2 = groupdict.get('month2') month_match = _month if _month else _month2 _day = groupdict.get('day') _day2 = groupdict.get('day2') day = _day if _day else _day2 return Date(year, month_match, day)
def add_metadata(self) -> None: self.metadata.platform = self.platform_config.name #TODO Not necessarily a thing self.metadata.media_type = MediaType.Executable if 'developer' in self.info: self.metadata.developer = self.info['developer'] if 'publisher' in self.info: self.metadata.publisher = self.info['publisher'] if 'year' in self.info: self.metadata.release_date = Date(self.info['year']) if 'category' in self.info: self.metadata.categories = [self.info['category']] if 'genre' in self.info: self.metadata.genre = self.info['genre'] if 'subgenre' in self.info: self.metadata.subgenre = self.info['subgenre'] if 'notes' in self.info: self.metadata.add_notes(self.info['notes']) self.additional_metadata()
def _add_homebrew_meta_xml_metadata(rom: ROM, metadata: 'Metadata', meta_xml: ElementTree.ElementTree): name = meta_xml.findtext('name') if name: rom.ignore_name = True metadata.add_alternate_name(name, 'Banner Title') metadata.developer = metadata.publisher = meta_xml.findtext('coder') metadata.specific_info['Version'] = meta_xml.findtext('version') url = meta_xml.findtext('url') if url: metadata.documents['Homepage'] = url release_date_text = meta_xml.findtext('release_date') if release_date_text: metadata.release_date = Date(release_date_text[0:4], release_date_text[4:6], release_date_text[6:8]) short_description = meta_xml.findtext('short_description') if short_description: metadata.descriptions['Short Description'] = short_description long_description = meta_xml.findtext('long_description') if long_description: metadata.descriptions['Long Description'] = long_description metadata.specific_info['Homebrew Category'] = meta_xml.findtext('category') or 'None' #Makes me wonder if it's feasible to include an option to get categories not from folders…
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_metadata_from_libretro_database_entry(metadata: 'Metadata', database: LibretroDatabaseType, key: Union[str, int]): database_entry = cast( Optional[dict[str, Any]], database.get(key) ) #TODO: Hmm what's the best way to do this - we don't want mypy complaining about all the different things GameValueType could be if database_entry: name = database_entry.get('comment', database_entry.get('name')) if name: metadata.add_alternate_name(name, 'Libretro Database Name') if 'serial' in database_entry and not metadata.product_code: metadata.product_code = database_entry['serial'] #seems name = description = comment = usually just the name of the file from No-Intro/Redump, region we already know, enhancement_hw we already know (just SNES and Mega Drive) if 'description' in database_entry: description = database_entry['description'] if description not in (database_entry.get('comment'), database_entry.get('name')): metadata.descriptions['Libretro Description'] = description date = Date() if 'releaseyear' in database_entry: date.year = database_entry['releaseyear'] elif 'year' in database_entry: #Unusual but can happen apparently date.year = database_entry['year'] if 'releasemonth' in database_entry: date.month = database_entry['releasemonth'] if 'releaseday' in database_entry: date.day = database_entry['releaseday'] if date.is_better_than(metadata.release_date): metadata.release_date = date if 'developer' in database_entry: developer = database_entry['developer'] while junk_suffixes.search(developer): developer = junk_suffixes.sub('', developer) metadata.developer = company_name_overrides.get( developer, developer) if 'publisher' in database_entry: publisher = database_entry['publisher'] while junk_suffixes.search(publisher): publisher = junk_suffixes.sub('', publisher) metadata.publisher = company_name_overrides.get( publisher, publisher) if 'manufacturer' in database_entry: publisher = database_entry['manufacturer'] while junk_suffixes.search(publisher): publisher = junk_suffixes.sub('', publisher) metadata.publisher = company_name_overrides.get( publisher, publisher) if 'genre' in database_entry: genre = database_entry['genre'] if '/' in genre: metadata.genre, metadata.subgenre = genre.split('/', 1) else: metadata.genre = genre if 'franchise' in database_entry: metadata.series = database_entry['franchise'] if 'version' in database_entry: metadata.specific_info['Version'] = database_entry['version'] if 'users' in database_entry: metadata.specific_info['Number of Players'] = database_entry[ 'users'] if 'homepage' in database_entry: metadata.documents['Homepage'] = database_entry['homepage'] if 'patch' in database_entry: metadata.documents['Patch Homepage'] = database_entry['patch'] if 'esrb_rating' in database_entry: metadata.specific_info['ESRB Rating'] = database_entry[ 'esrb_rating'] if 'bbfc_rating' in database_entry: metadata.specific_info['BBFC Rating'] = database_entry[ 'bbfc_rating'] if 'elspa_rating' in database_entry: metadata.specific_info['ELSPA Rating'] = database_entry[ 'elspa_rating'] if 'origin' in database_entry: metadata.specific_info['Development Origin'] = database_entry[ 'origin'] if 'edge_review' in database_entry: metadata.descriptions['EDGE Review'] = database_entry[ 'edge_review'] if 'edge_rating' in database_entry: metadata.specific_info['EDGE Rating'] = database_entry[ 'edge_rating'] if 'edge_issue' in database_entry: metadata.specific_info['EDGE Issue'] = database_entry['edge_issue'] if 'famitsu_rating' in database_entry: metadata.specific_info['Famitsu Rating'] = database_entry[ 'famitsu_rating'] if database_entry.get('analog', 0) == 1: #This is PS1 specific metadata.specific_info['Uses Analog?'] = True if database_entry.get('rumble', 0) == 1: metadata.specific_info['Force Feedback?'] = True # for k, v in database_entry.items(): # if k not in ('name', 'description', 'region', 'releaseyear', 'releasemonth', 'releaseday', 'genre', 'developer', 'serial', 'comment', 'franchise', 'version', 'homepage', 'patch', 'publisher', 'users', 'esrb_rating', 'origin', 'enhancement_hw', 'edge_review', 'edge_rating', 'edge_issue', 'famitsu_rating', 'analog', 'rumble'): # print('uwu', database_entry.get('comment'), k, v) return True return False
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])
def add_saturn_info(rom_path_for_warning: str, metadata: 'Metadata', header: bytes): hardware_id = header[0:16].decode('ascii', errors='ignore') if hardware_id != 'SEGA SEGASATURN ': #Won't boot on a real Saturn, also if this is some emulator only thing then nothing in the header can be considered valid metadata.specific_info['Hardware ID'] = hardware_id metadata.specific_info['Invalid Hardware ID?'] = True return try: maker = header[16:32].decode('ascii').rstrip() if maker.startswith('SEGA TP '): #"Sega Third Party", I guess maker_code = maker.removeprefix('SEGA TP ') if maker_code.startswith('T '): #You're not supposed to do that, stop that maker_code = 'T-' + maker_code[2:] if maker_code in _licensee_codes: metadata.publisher = _licensee_codes[maker_code] elif maker == 'SEGA ENTERPRISES': metadata.publisher = 'Sega' else: metadata.publisher = maker except UnicodeDecodeError: pass try: metadata.product_code = header[32:42].decode('ascii').rstrip() except UnicodeDecodeError: pass try: version = header[42:48].decode('ascii').rstrip() if version[0] == 'V' and version[2] == '.': metadata.specific_info['Version'] = 'v' + version[1:] except UnicodeDecodeError: pass release_date = header[48:56].decode('ascii', errors='backslashreplace').rstrip() if not release_date.startswith('0') and '-' not in release_date: #If it starts with 0 the date format is WRONG stop it because I know the Saturn wasn't invented yet before 1000 AD #Also sometimes it's formatted with dashes which means there are 2 bytes that shouldn't be there and are technically part of device info? Weird try: year = release_date[0:4] month = release_date[4:6] day = release_date[6:8] metadata.specific_info['Header Date'] = Date(year, month, day) guessed = Date(year, month, day, True) if guessed.is_better_than(metadata.release_date): metadata.release_date = guessed except IndexError: if main_config.debug: print(rom_path_for_warning, 'has invalid date in header:', release_date) except ValueError: pass device_info = header[56:64].decode('ascii', errors='ignore').rstrip() if device_info.startswith('CD-'): #CART16M is seen here instead of "CD-1/1" on some protos? disc_number, _, disc_total = device_info[3:].partition('/') try: metadata.disc_number = int(disc_number) metadata.disc_total = int(disc_total) except ValueError: pass region_info = header[64:80].rstrip() #Only 10 characters are used region_codes = set() if b'J' in region_info: region_codes.add(SaturnRegionCodes.Japan) if b'U' in region_info: region_codes.add(SaturnRegionCodes.USA) if b'E' in region_info: region_codes.add(SaturnRegionCodes.Europe) #Some other region codes appear sometimes, but I haven't been able to verify _exactly_ what they are, and I don't really wanna make guesses #T = Taiwan? #K = Korea? #B = Brazil? #A and L seen on some homebrews and devkits? metadata.specific_info['Region Code'] = region_codes peripherals = header[80:96].decode('ascii', errors='backslashreplace').rstrip() _parse_peripherals(metadata, peripherals) internal_name = header[96:208].decode('ascii', errors='backslashreplace').rstrip() #Sometimes / : - are used as delimiters, and there can also be J:JapaneseNameU:USAName if internal_name: metadata.specific_info['Internal Title'] = internal_name
def additional_metadata(self) -> None: self.metadata.specific_info['Executable Name'] = self.path.split(':')[-1] if have_machfs: file = self._get_file() if not file: raise ValueError('Somehow MacApp.additional_metadata was called with invalid file') carbon_path = self._carbon_path if carbon_path: self.metadata.specific_info['Is Carbon?'] = True self.metadata.specific_info['Carbon Path'] = carbon_path self.metadata.specific_info['Architecture'] = 'PPC' #This has to be manually specified because some pretend to be fat binaries? creator = file.creator if creator in {b'PJ93', b'PJ97'}: self.metadata.specific_info['Engine'] = 'Macromedia Director' self.metadata.specific_info['Creator Code'] = creator.decode('mac-roman', errors='backslashreplace') #Can also get mddate if wanted creation_datetime = mac_epoch + datetime.timedelta(seconds=file.crdate) creation_date = Date(creation_datetime.year, creation_datetime.month, creation_datetime.day, True) if creation_date.is_better_than(self.metadata.release_date): self.metadata.release_date = creation_date #self.metadata.specific_info['File Flags'] = file.flags if have_macresources: #If you have machfs you do have macresources too, but still if have_pillow: self.metadata.images['Icon'] = self._get_icon() sizes = self._get_resources().get(b'SIZE') if sizes: #Supposed to be -1, 0 and 1 are created when user manually changes preferred/minimum RAM? size = sizes.get(-1, sizes.get(0, sizes.get(1))) if size: #Remember this is big endian so you will need to go backwards #Bit 0: Save screen (obsolete) #Bit 1: Accept suspend/resume events #Bit 2: Disable option (obsolete) #Bit 3: Can background #Bit 4: Does activate on FG switch #Bit 6: Get front clicks #Bit 7: Accept app died events (debuggers) (the good book says "app launchers use this" and apparently applications use ignoreAppDiedEvents) #Bit 9 (bit 1 of second byte): High level event aware #Bit 10: Local and remote high level events #Bit 11: Stationery aware #Bit 12: Use text edit services ("inline services"?) if size[0] or size[1]: #If all flags are 0 then this is probably lies #if size[0] & (1 << (8 - 5)) != 0: # #Documented as "Only background"? But also that #TODO: I don't think this does what I think it does # self.metadata.specific_info['Has User Interface?'] = False if size[1] & (1 << (15 - 8)) == 0: #Wait is that even correct, and if these size resources are just ints, should they be combined to make this easier self.metadata.specific_info['Not 32 Bit Clean?'] = True self.metadata.specific_info['Minimum RAM'] = format_byte_size(int.from_bytes(size[6:10], 'big')) if file.type == b'APPL' and 'Architecture' not in self.metadata.specific_info: #According to https://support.apple.com/kb/TA21606?locale=en_AU this should work has_ppc = b'cfrg' in self._get_resources() #Code fragment, ID always 0 has_68k = b'CODE' in self._get_resources() if has_ppc: if has_68k: self.metadata.specific_info['Architecture'] = 'Fat' else: self.metadata.specific_info['Architecture'] = 'PPC' elif has_68k: self.metadata.specific_info['Architecture'] = '68k' else: self.metadata.specific_info['Architecture'] = 'Unknown' #Maybe this will happen for really old stuff verses = self._get_resources().get(b'vers', {}) vers = verses.get(1, verses.get(128)) #There are other vers resources too but 1 is the main one (I think?), 128 is used in older apps? maybe? if vers: self._add_version_resource_info(vers) if 'arch' in self.info: #Allow manual override (sometimes apps are jerks and have 68K code just for the sole purpose of showing you a dialog box saying you can't run it on a 68K processor) self.metadata.specific_info['Architecture'] = self.info['arch']
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 _add_meta_xml_metadata(metadata: 'Metadata', meta_xml: ElementTree.ElementTree): #version = 33 for digital stuff, sometimes 32 otherwise?, content_platform = WUP, ext_dev_urcc = some kiosk related thingo #logo_type = 2 on third party stuff?, app_launch_type = 1 on parental controls/H&S/Wii U Chat and 0 on everything else?, invisible_flag = maybe just for keeping stuff out of the daily log?, no_managed_flag, no_event_log, no_icon_database, launching_flag, install_flag, closing_msg, group_id, boss_id, os_version, app_size, common_boss_size, account_boss_size, save_no_rollback, join_game_id, join_game_mode_mask, bg_daemon_enable, olv_accesskey, wood_tin, e_manual = I guess it's 1 if it has a manual, e_manual_version, eula_version, direct_boot, reserved_flag{0-7}, add_on_unique_id{0-31} = DLC probs? product_code = meta_xml.findtext('product_code') if product_code: metadata.product_code = product_code try: metadata.specific_info['Virtual Console Platform'] = WiiUVirtualConsolePlatform(metadata.product_code[6]) except ValueError: pass gametdb_id = product_code[-4:] add_info_from_tdb(_tdb, metadata, gametdb_id) company_code = meta_xml.findtext('company_code') if company_code: if company_code in _nintendo_licensee_codes: metadata.publisher = _nintendo_licensee_codes[company_code] elif len(company_code) == 4 and company_code.startswith('00'): if company_code[2:] in _nintendo_licensee_codes: metadata.publisher = _nintendo_licensee_codes[company_code[2:]] if product_code and company_code: _add_cover(metadata, product_code[-4:], company_code[2:]) mastering_date_text = meta_xml.findtext('mastering_date') #Usually blank? Sometimes exists though if mastering_date_text: try: mastering_datetime = datetime.fromisoformat(mastering_date_text[:10]) mastering_date = Date(mastering_datetime.year, mastering_datetime.month, mastering_datetime.day) metadata.specific_info['Mastering Date'] = mastering_date guessed_date = Date(mastering_date.year, mastering_date.month, mastering_date.day, True) if guessed_date.is_better_than(metadata.release_date): metadata.release_date = guessed_date except ValueError: #print(mastering_date_text) pass #Maybe we can use these to figure out if it creates a save file or not… metadata.specific_info['Common Save Size'] = int(meta_xml.findtext('common_save_size') or '0', 16) metadata.specific_info['Account Save Size'] = int(meta_xml.findtext('account_save_size') or '0', 16) metadata.specific_info['Title ID'] = meta_xml.findtext('title_id') version = meta_xml.findtext('title_version') if version: metadata.specific_info['Version'] = 'v' + version region = meta_xml.findtext('region') region_codes = set() if region: try: region_flags = int(region, 16) for region_code in WiiU3DSRegionCode: if region_code in (WiiU3DSRegionCode.RegionFree, WiiU3DSRegionCode.WiiURegionFree): continue if region_code.value & region_flags: region_codes.add(region_code) metadata.specific_info['Region Code'] = region_codes except ValueError: metadata.specific_info['Region Code'] = '0x' + region #Tempted to reuse wii.parse_ratings, but I might not because it's just a bit different rating_tags = {tag: int(tag.text) for tag in meta_xml.iter() if tag.tag.startswith('pc_') and tag.text} ratings = {tag.tag: rating & 0b0001_1111 for tag, rating in rating_tags.items() if (rating & 0b1000_0000) == 0 and (rating & 0b0100_0000) == 0} if ratings: try: rating = statistics.mode(ratings.values()) except statistics.StatisticsError: rating = max(ratings.values()) metadata.specific_info['Age Rating'] = rating if 'pc_cero' in ratings: metadata.specific_info['CERO Rating'] = ratings['pc_cero'] if 'pc_esrb' in ratings: metadata.specific_info['ESRB Rating'] = ratings['pc_esrb'] if 'pc_usk' in ratings: metadata.specific_info['USK Rating'] = ratings['pc_usk'] if 'pc_pegi_gen' in ratings: metadata.specific_info['PEGI Rating'] = ratings['pc_pegi_gen'] #There are more but that will do # #These may not be accurate at all? # metadata.specific_info['Uses-Nunchuk'] = meta_xml.findtext('ext_dev_nunchaku') != '0' # metadata.specific_info['Uses-Classic-Controller'] = meta_xml.findtext('ext_dev_classic') != '0' # metadata.specific_info['Uses-Balance-Board'] = meta_xml.findtext('ext_dev_board') != '0' #maybe? # metadata.specific_info['Uses-USB-Keyboard'] = meta_xml.findtext('ext_dev_usb_keyboard') != '0' # uses_etc = meta_xml.findtext('ext_dev_etc') != '0' #??? # if uses_etc: # metadata.specific_info['Uses-Etc'] = meta_xml.findtext('ext_dev_etc_name') #drc = meta_xml.findtext('drc_use') != '0' #network = meta_xml.findtext('network_use') != '0' #online_account = meta_xml.findtext('online_account_use') != '0' short_names = {} long_names = {} publishers = {} for lang_code, lang_name in _languages.items(): short_name = meta_xml.findtext('shortname_' + lang_code) if short_name: short_names[lang_name] = short_name long_name = meta_xml.findtext('longname_' + lang_code) if long_name: long_names[lang_name] = long_name.replace('\n', ': ') #Newlines seem to be used here to separate subtitles publisher = meta_xml.findtext('publisher_' + lang_code) if publisher: publishers[lang_name] = publisher add_info_from_local_titles(metadata, short_names, long_names, publishers, region_codes) def _add_homebrew_meta_xml_metadata(rom: ROM, metadata: 'Metadata', meta_xml: ElementTree.ElementTree): name = meta_xml.findtext('name') if name: rom.ignore_name = True metadata.add_alternate_name(name, 'Banner Title') metadata.developer = metadata.publisher = meta_xml.findtext('coder') metadata.specific_info['Version'] = meta_xml.findtext('version') url = meta_xml.findtext('url') if url: metadata.documents['Homepage'] = url release_date_text = meta_xml.findtext('release_date') if release_date_text: metadata.release_date = Date(release_date_text[0:4], release_date_text[4:6], release_date_text[6:8]) short_description = meta_xml.findtext('short_description') if short_description: metadata.descriptions['Short Description'] = short_description long_description = meta_xml.findtext('long_description') if long_description: metadata.descriptions['Long Description'] = long_description metadata.specific_info['Homebrew Category'] = meta_xml.findtext('category') or 'None' #Makes me wonder if it's feasible to include an option to get categories not from folders… def _add_rpx_metadata(rom: ROM, metadata: 'Metadata'): #The .rpx itself is not interesting and basically just a spicy ELF #This is going to assume we are looking at a homebrew folder try: #info.json has the same info? But it's not always there _add_homebrew_meta_xml_metadata(rom, metadata, ElementTree.parse(rom.path.with_name('meta.xml'))) if metadata.categories[-1] == rom.path.parent.name: metadata.categories = metadata.categories[:-1] except FileNotFoundError: pass homebrew_banner_path = rom.path.with_name('icon.png') if homebrew_banner_path.is_file(): metadata.images['Banner'] = homebrew_banner_path def add_folder_metadata(rom: FolderROM, metadata: 'Metadata'): content_dir = rom.get_subfolder('content') meta_dir = rom.get_subfolder('meta') assert content_dir and meta_dir, 'It should be impossible for content_dir or meta_dir to be none, otherwise this would not have even been detected as a folder' metadata.specific_info['Executable Name'] = rom.relevant_files['rpx'].name #TODO: Move this over to engine_detect if rom.path.joinpath('code', 'UnityEngine_dll.rpl').is_file(): #Unity games on Wii U just have a "Data" folder under content with no executable (because it's over here in code), so our usual detection won't work; not sure about other cross platform engines metadata.specific_info['Engine'] = 'Unity' if content_dir.joinpath('assets').is_dir() and all(content_dir.joinpath('app', file).is_dir() for file in ('appinfo.xml', 'config.xml', 'index.html')): metadata.specific_info['Engine'] = 'Nintendo Web Framework' engine = try_and_detect_engine_from_folder(content_dir, metadata) if engine: metadata.specific_info['Engine'] = engine #Seemingly this can actually sometimes be all lowercase? I should make this check case insensitive but I don't really care too much icon_path = meta_dir.joinpath('iconTex.tga') if icon_path.is_file(): metadata.images['Icon'] = icon_path boot_drc_path = meta_dir.joinpath('bootDrcTex.tga') #Image displayed on the gamepad while loading if boot_drc_path.is_file(): metadata.images['Gamepad Boot Image'] = boot_drc_path boot_tv_path = meta_dir.joinpath('bootTvTex.tga') #Generally just bootDrcTex but higher resolution (and for the TV) if boot_tv_path.is_file(): metadata.images['TV Boot Image'] = boot_tv_path boot_logo_path = meta_dir.joinpath('bootLogoTex.tga') if boot_logo_path.is_file(): metadata.images['Boot Logo'] = boot_logo_path #There is also a Manual.bfma in here, bootMovie.h264 and bootSound.btsnd, and some ratings images like "CERO_ja.jpg" and "PEGI_en.jpg" except they're 1 byte so I dunno meta_xml_path = meta_dir.joinpath('meta.xml') try: meta_xml = ElementTree.parse(meta_xml_path) _add_meta_xml_metadata(metadata, meta_xml) except FileNotFoundError: pass if metadata.specific_info.get('Virtual Console Platform') == WiiUVirtualConsolePlatform.GBAOrPCEngine: metadata.specific_info['Virtual Console Platform'] = WiiUVirtualConsolePlatform.GBA if rom.name == 'm2engage' else WiiUVirtualConsolePlatform.PCEngine def add_wii_u_custom_info(game: 'ROMGame'): if game.rom.is_folder: add_folder_metadata(cast(FolderROM, game.rom), game.metadata) if game.rom.extension == 'rpx': _add_rpx_metadata(game.rom, game.metadata)
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_wii_homebrew_metadata(rom: FolderROM, metadata: 'Metadata'): metadata.specific_info['Executable Name'] = rom.relevant_files['boot.dol'].name icon_path = rom.get_file('icon.png', True) if icon_path: metadata.images['Banner'] = str(icon_path) #Unfortunately the aspect ratio means it's not really great as an icon xml_path = rom.relevant_files['meta.xml'] if xml_path.is_file(): try: meta_xml = ElementTree.parse(str(xml_path)) name = meta_xml.findtext('name') if name: metadata.add_alternate_name(name, 'Banner Title') rom.ignore_name = True coder = meta_xml.findtext('coder') if not coder: coder = meta_xml.findtext('author') metadata.developer = metadata.publisher = coder version = meta_xml.findtext('version') if version: version = version.removeprefix('rev').removeprefix('r').lstrip() if version[0] != 'v': version = 'v' + version metadata.specific_info['Version'] = version release_date = meta_xml.findtext('release_date') if release_date: #Not interested in hour/minute/second/etc release_date = release_date[0:8] actual_date = None date_formats = [ '%Y%m%d', #The one actually specified by the meta.xml format '%Y%m%d%H%M%S', '%d/%m/%Y', #Hmm this might be risky because of potential ambiguity with American dates '%Y-%m-%d', ] for date_format in date_formats: try: actual_date = datetime.strptime(release_date, date_format) break except ValueError: continue if actual_date: year = actual_date.year month = actual_date.month day = actual_date.day metadata.release_date = Date(year, month, day) short_description = meta_xml.findtext('short_description') if short_description: metadata.descriptions['Description'] = short_description long_description = meta_xml.findtext('long_description') if long_description: metadata.descriptions['Long Description'] = long_description except ElementTree.ParseError as etree_error: if main_config.debug: print('Ah bugger this Wii homebrew XML has problems', rom.path, etree_error)
def _add_metadata_from_receipt(self) -> None: if not self.receipt: return game = self.receipt['game'] upload = self.receipt['upload'] #build, files, installerName probably not needed title = game.get('title') if title: self._name = fix_name(title) self.metadata.specific_info['Game ID'] = game.get('id') self.metadata.documents['Homepage'] = game.get('url') description = game.get('shortText') if description: self.metadata.descriptions['Description'] = description self.game_type = game.get( 'type', 'default') #Default, flash, unity, java, html self.category = game.get( 'classification', 'game' ) #game, tool, assets, game_mod, physical_game, soundtrack, other, comic, book created_at = game.get('createdAt') published_at = game.get('publishedAt') if created_at: creation_date = datetime.date.fromisoformat(created_at[:10]) self.metadata.specific_info['Creation Date'] = Date( creation_date.year, creation_date.month, creation_date.day) if published_at: release_date = datetime.date.fromisoformat(published_at[:10]) self.metadata.release_date = Date(release_date.year, release_date.month, release_date.day) #coverUrl, stillCoverUrl might be useful? I dunno #platforms is what platforms the game _can_ be available for, but it doesn't tell us about this exe #minPrice, canBeBought, sale aren't so useful here as this receipt is generated at the time this game is downloaded I think which might be out of date user = game.get('user') if user: user_name = user.get('displayName') #not using the second param of .get here because we also don't want it to be an empty string if not user_name: user_name = user.get('username') if user_name: self.metadata.developer = self.metadata.publisher = user_name #developer and pressUser here just indicate if this user (who has uploaded the game) has ticked a box saying they are a developer or press, which doesn't seem to matter self.metadata.documents['Developer Homepage'] = user.get('url') if upload: build_name = upload.get('displayName') if not build_name: build_name = upload.get('filename') self.metadata.specific_info['Build Name'] = build_name self.is_demo = upload.get('demo') if self.is_demo and not 'demo' in self.name.lower(): self._name += ' (Demo)' self.metadata.specific_info['Upload Type'] = upload.get( 'type', 'default' ) #default, flash, unity, java, html, soundtrack, book, video, documentation, mod, audio_assets, graphical_assets, sourcecode, other self.platforms = tuple(upload.get( 'platforms', {}).keys()) #I think the values show if it's x86/x64 but eh #Not sure what channelName or preorder does upload_created_at = upload.get('createdAt') upload_updated_at = upload.get('updatedAt') if upload_created_at: upload_creation_date = datetime.date.fromisoformat( upload_created_at[:10]) self.metadata.specific_info['Upload Creation Date'] = Date( upload_creation_date.year, upload_creation_date.month, upload_creation_date.day) if upload_updated_at: upload_date = datetime.date.fromisoformat( upload_updated_at[:10]) self.metadata.specific_info['Upload Date'] = Date( upload_date.year, upload_date.month, upload_date.day)
def add_info_from_main_track(metadata: Metadata, track_path: Path, sector_size: int): try: header = cd_read.read_mode_1_cd(track_path, sector_size, amount=256) except NotImplementedError: return hardware_id = header[0:16].decode('ascii', errors='ignore') if hardware_id != 'SEGA SEGAKATANA ': #Won't boot on a real Dreamcast. I should check how much emulators care... metadata.specific_info['Hardware ID'] = hardware_id metadata.specific_info['Invalid Hardware ID?'] = True return copyright_info = header[16:32].decode('ascii', errors='ignore') #Seems to be always "SEGA ENTERPRISES"? metadata.specific_info['Copyright'] = copyright_info device_info = header[32:48].decode('ascii', errors='ignore').rstrip() device_info_match = _device_info_regex.match(device_info) if device_info_match: try: metadata.disc_number = int(device_info_match['discNum']) metadata.disc_total = int(device_info_match['totalDiscs']) except ValueError: pass region_info = header[48:56].rstrip() region_codes = set() if b'J' in region_info: region_codes.add(SaturnRegionCodes.Japan) if b'U' in region_info: region_codes.add(SaturnRegionCodes.USA) if b'E' in region_info: region_codes.add(SaturnRegionCodes.Europe) #Some other region codes appear sometimes but they might not be entirely valid metadata.specific_info['Region Code'] = region_codes try: peripherals = int(header[56:64], 16) add_peripherals_info(metadata, peripherals) except ValueError: pass metadata.product_code = header[64:74].decode( 'ascii', errors='backslashreplace').rstrip() try: version = header[74:80].decode('ascii').rstrip() if version[0] == 'V' and version[2] == '.': metadata.specific_info['Version'] = 'v' + version[1:] except UnicodeDecodeError: pass release_date = header[80:96].decode('ascii', errors='backslashreplace').rstrip() try: year = release_date[0:4] month = release_date[4:6] day = release_date[6:8] metadata.specific_info['Header Date'] = Date(year, month, day) guessed = Date(year, month, day, True) if guessed.is_better_than(metadata.release_date): metadata.release_date = guessed except ValueError: pass try: metadata.specific_info['Executable Name'] = header[96:112].decode( 'ascii').rstrip() except UnicodeDecodeError: pass try: maker = header[112:128].decode('ascii').rstrip() if maker == 'SEGA ENTERPRISES': metadata.publisher = 'Sega' elif maker.startswith(('SEGA LC-', 'SEGA-LC-')): maker_code = maker[len('SEGA LC-'):] if maker_code in _licensee_codes: metadata.publisher = _licensee_codes[maker_code] elif maker: metadata.publisher = maker except UnicodeDecodeError: pass metadata.specific_info['Internal Title'] = header[128:256].decode( 'ascii', errors='backslashreplace').rstrip('\0 ')
def add_metadata_from_appinfo_common_section(game: 'SteamGame', common: Mapping[bytes, Any]): if 'Icon' not in game.metadata.images: add_icon_from_common_section(game, common) #eulas is a list, so it could be used to detect if game has third-party EULA #small_capsule and header_image refer to image files that don't seem to be there so I dunno #workshop_visible and community_hub_visible could also tell you stuff about if the game has a workshop and a... community hub #releasestate: 'released' might be to do with early access? #exfgls = exclude from game library sharing #b'requireskbmouse' and b'kbmousegame' are also things, but don't seem to be 1:1 with games that have controllersupport = none oslist = common.get(b'oslist') if not main_config.use_steam_as_platform: #It's comma separated, but we can assume platform if there's only one (and sometimes config section doesn't do the thing) if oslist == b'windows': game.metadata.platform = 'Windows' if oslist == b'macos': game.metadata.platform = 'Mac' if oslist == b'linux': game.metadata.platform = 'Linux' #osarch is something like b'32' or b'64', osextended is sometimes 'macos64' etc app_retired_publisher_request = common.get(b'app_retired_publisher_request') if app_retired_publisher_request: game.metadata.specific_info['No Longer Purchasable'] = app_retired_publisher_request.data == 1 #You can't know if a game's delisted entirely unless you go to the store API to find if that returns success or not, because the appinfo stuff is a cache and holds on to data that no longer exists language_list = common.get(b'languages') if language_list: game.metadata.languages = translate_language_list(language_list) else: supported_languages = common.get(b'supported_languages') if supported_languages: #Hmm… this one goes into more detail actually, you have not just "supported" but "full_audio" and "subtitles" #But for now let's just look at what else exists game.metadata.languages = translate_language_list(supported_languages) add_genre(game, common) steam_release_timestamp = common.get(b'steam_release_date') #Seems that original_release_date is here sometimes, and original_release_date sometimes appears along with steam_release_date where a game was only put on Steam later than when it was actually released elsewhere #Sometimes these are equal, or off by like one day (which is possibly timezone related) original_release_timestamp = common.get(b'original_release_date') release_date = original_release_timestamp if not release_date: release_date = steam_release_timestamp #Maybe I should put in an option to prefer Steam release date if release_date: release_datetime = datetime.datetime.fromtimestamp(release_date.data) game.metadata.release_date = Date(release_datetime.year, release_datetime.month, release_datetime.day) if original_release_timestamp and steam_release_timestamp: steam_release_datetime = datetime.datetime.fromtimestamp(steam_release_timestamp.data) game.metadata.specific_info['Steam Release Date'] = Date(steam_release_datetime.year, steam_release_datetime.month, steam_release_datetime.day) store_asset_mtime = common.get(b'store_asset_mtime') if store_asset_mtime: store_asset_timestamp = datetime.datetime.fromtimestamp(store_asset_mtime.data) game.metadata.specific_info['Store Asset Modification Time'] = Date(store_asset_timestamp.year, store_asset_timestamp.month, store_asset_timestamp.day) store_categories_list = common.get(b'category') if store_categories_list: #keys are category_X where X is some arbitrary ID, values are always Integer = 1 #This is the thing where you go to the store sidebar and it's like "Single-player" "Multi-player" "Steam Achievements" etc" cats = {store_categories.get(key, key) for key in (key.decode('utf-8', errors='backslashreplace') for key in store_categories_list.keys())} game.metadata.specific_info['Store Categories'] = cats #meow game.metadata.specific_info['Has Achievements?'] = 'Steam Achievements' in cats game.metadata.specific_info['Has Trading Cards?'] = 'Steam Trading Cards' in cats is_single_player_only = True for cat in cats: if 'multiplayer' in cat.lower() or 'multi-player' in cat.lower() or 'co-op' in cat.lower() or 'split screen' in cat.lower(): is_single_player_only = False break if is_single_player_only: game.metadata.specific_info['Number of Players'] = 1 has_adult_content = common.get(b'has_adult_content') #Integer object with data = 0 or 1, as most bools here seem to be if has_adult_content: game.metadata.specific_info['Has Adult Content?'] = bool(has_adult_content.data) has_violence = common.get(b'has_adult_content_violence') if has_violence: game.metadata.specific_info['Has Violent Content?'] = bool(has_violence.data) has_sex = common.get(b'has_adult_content_sex') #uwu if has_sex: game.metadata.specific_info['Has Sexual Content?'] = bool(has_sex.data) only_vr = common.get(b'onlyvrsupport') vr_support = common.get(b'openvrsupport') if only_vr is not None and only_vr.data: game.metadata.specific_info['VR Support'] = 'Required' elif vr_support: #b'1' game.metadata.specific_info['VR Support'] = 'Optional' metacritic_score = common.get(b'metacritic_score') if metacritic_score: #Well why not game.metadata.specific_info['Metacritic Score'] = metacritic_score.data metacritic_url = common.get(b'metacritic_fullurl') if metacritic_url: game.metadata.documents['Metacritic Page'] = metacritic_url.decode('utf8', errors='ignore') metacritic_name = common.get(b'metacritic_name') if metacritic_name: game.metadata.add_alternate_name(metacritic_name.decode('utf8', errors='ignore'), 'Metacritic Name') review_score = common.get(b'review_score') #This is Steam's own review section, I guess? #This seems to be a number from 2 to 9 inclusive. Not sure what it means though #There is also review_score_bombs? What the heck if review_score: game.metadata.specific_info['Review Score'] = review_score.data review_percentage = common.get(b'review_percentage') #Also seemingly related to Steam reviews, and there is also a review_percentage_bombs, but I still don't know exactly what this does if review_percentage: game.metadata.specific_info['Review Percentage'] = review_percentage.data sortas = common.get(b'sortas') if sortas: game.metadata.specific_info['Sort Name'] = sortas.decode('utf8', errors='backslashreplace') game.metadata.specific_info['Controlller Support'] = common.get(b'controller_support', b'none').decode('utf-8', errors='backslashreplace') if steam_installation.localization_available: store_tag_names = steam_installation.localization['localization']['english']['store_tags'] store_tag_ids_list = common.get(b'store_tags') if store_tag_ids_list: store_tags = {store_tag_names.get(id, id) for id in (str(value.data) for value in store_tag_ids_list.values())} game.metadata.specific_info['Store Tags'] = store_tags franchise_name = None associations = common.get(b'associations') if associations: associations_dict: MutableMapping[str, list[str]] = {} for association in associations.values(): association_type_value = association.get(b'type') if isinstance(association_type_value, appinfo.Integer): association_type = str(association_type_value.data) else: association_type = association_type_value.decode('utf-8', errors='ignore') association_name_value = association.get(b'name') if isinstance(association_name_value, appinfo.Integer): association_name = str(association_name_value.data) else: association_name = association_name_value.decode('utf-8', errors='ignore') if association_type not in associations_dict: associations_dict[association_type] = [] associations_dict[association_type].append(association_name) if 'franchise' in associations_dict: franchise_name = associations_dict['franchise'][0] franchise_name.removesuffix(' Franchise') franchise_name.removesuffix(' Series') franchise_name.removeprefix('The ') franchise_name = normalize_name_case(franchise_name) not_actual_franchises = ('Playism', 'Hentai', 'Coming-of-Age', 'Wolf RPG Editor', 'Winter Wolves Games', 'Team17 Digital', '"caves rd"', 'Jackbox Games', 'Franchise', 'PopCap') if not any(franchise_name.lower() == assoc[0].lower() for assoc_type, assoc in associations_dict.items() if assoc_type != 'franchise') and franchise_name not in not_actual_franchises: game.metadata.series = remove_capital_article(franchise_name) if 'developer' in associations_dict: devs = [] for dev in associations_dict['developer']: dev = normalize_developer(dev) if dev.endswith(' (Mac)'): game.metadata.specific_info['Mac Developer'] = dev.removesuffix(' (Mac)') elif dev.endswith(' (Linux)'): game.metadata.specific_info['Linux Developer'] = dev.removesuffix(' (Linux)') elif dev not in devs: devs.append(dev) game.metadata.developer = ', '.join(devs) if 'publisher' in associations_dict: pubs = [] for pub in associations_dict['publisher']: pub = normalize_developer(pub) if pub in {'none', 'Self Published'} and game.metadata.developer: pub = game.metadata.developer if pub.endswith(' (Mac)'): game.metadata.specific_info['Mac Publisher'] = pub.removesuffix(' (Mac)') elif pub.endswith(' (Linux)'): game.metadata.specific_info['Linux Publisher'] = pub.removesuffix(' (Linux)') elif pub not in pubs: pubs.append(pub) game.metadata.publisher = ', '.join(pubs)