"""Reads the Dolphin game database, stored in a binary format""" import os from lutris.util.log import logger DOLPHIN_GAME_CACHE_FILE = os.path.expanduser("~/.cache/dolphin-emu/gamelist.cache") SUPPORTED_CACHE_VERSION = 24 def get_hex_string(string): """Return the hexadecimal representation of a string""" return " ".join("{:02x}".format(c) for c in string) def get_word_len(string): """Return the length of a string as specified in the Dolphin format""" return int("0x" + "".join("{:02x}".format(c) for c in string[::-1]), 0) # https://github.com/dolphin-emu/dolphin/blob/90a994f93780ef8a7cccfc02e00576692e0f2839/Source/Core/UICommon/GameFile.h#L140 # https://github.com/dolphin-emu/dolphin/blob/90a994f93780ef8a7cccfc02e00576692e0f2839/Source/Core/UICommon/GameFile.cpp#L318 class DolphinCacheReader: header_size = 20 structure = { "valid": "b", "file_path": "s", "file_name": "s", "file_size": 8, "volume_size": 8, "volume_size_type": 4, "is_datel_disc": "b", "is_nkit": "b", "short_names": "a", "long_names": "a", "short_makers": "a", "long_makers": "a", "descriptions": "a", "internal_name": "s", "game_id": "s", "gametdb_id": "s", "title_id": 8, "maker_id": "s", "region": 4, "country": 4, "platform": 1, "platform_": 3, "blob_type": 4, "block_size": 8, "compression_method": "s", "revision": 2, "disc_number": 1, "apploader_date": "s", "custom_name": "s", "custom_description": "s", "custom_maker": "s", "volume_banner": "i", "custom_banner": "i", "default_cover": "c", "custom_cover": "c", } def __init__(self): self.offset = 0 with open(DOLPHIN_GAME_CACHE_FILE, "rb") as dolphin_cache_file: self.cache_content = dolphin_cache_file.read() cache_version = get_word_len(self.cache_content[:4]) if cache_version != SUPPORTED_CACHE_VERSION: logger.warning("Dolphin cache version expected %s but found %s", SUPPORTED_CACHE_VERSION, cache_version) def get_game(self): game = {} for key, i in self.structure.items(): if i == "s": game[key] = self.get_string() elif i == "b": game[key] = self.get_boolean() elif i == "a": game[key] = self.get_array() elif i == "i": game[key] = self.get_image() elif i == "c": game[key] = self.get_cover() else: game[key] = self.get_raw(i) return game def get_games(self): self.offset += self.header_size games = [] while self.offset < len(self.cache_content): try: games.append(self.get_game()) except Exception as ex: logger.error("Failed to read Dolphin database: %s", ex) return games def get_boolean(self): res = bool(get_word_len(self.cache_content[self.offset : self.offset + 1])) self.offset += 1 return res def get_array(self): array_len = get_word_len(self.cache_content[self.offset : self.offset + 4]) self.offset += 4 array = {} for _i in range(array_len): array_key = self.get_raw(4) array[array_key] = self.get_string() return array def get_image(self): data_len = get_word_len(self.cache_content[self.offset : self.offset + 4]) self.offset += 4 res = self.cache_content[self.offset : self.offset + data_len * 4] # vector self.offset += data_len * 4 width = get_word_len(self.cache_content[self.offset : self.offset + 4]) self.offset += 4 height = get_word_len(self.cache_content[self.offset : self.offset + 4]) self.offset += 4 return (width, height), res def get_cover(self): array_len = get_word_len(self.cache_content[self.offset : self.offset + 4]) self.offset += 4 return self.get_raw(array_len) def get_raw(self, word_len): res = get_hex_string(self.cache_content[self.offset : self.offset + word_len]) self.offset += word_len return res def get_string(self): word_len = get_word_len(self.cache_content[self.offset : self.offset + 4]) self.offset += 4 string = self.cache_content[self.offset : self.offset + word_len] self.offset += word_len return string.decode("utf8")