def __init__(self, reader, writer, token): super().__init__(Platform.NintendoGameCube, __version__, reader, writer, token) self.backend_client = BackendClient() self.games = [] self.game_times = get_the_game_times() self.local_games_cache = self.local_games_list()
def __init__(self, reader, writer, token): super().__init__(Platform.Battlenet, version, reader, writer, token) self.local_client = LocalClient(self._update_statuses) self.authentication_client = AuthenticatedHttpClient(self) self.backend_client = BackendClient(self, self.authentication_client) self.watched_running_games = set()
def __init__(self, reader, writer, token): super().__init__(Platform.Battlenet, version, reader, writer, token) log.info(f"Starting Battle.net plugin, version {version}") self.bnet_client = None self.local_client = LocalClient() self.authentication_client = AuthenticatedHttpClient(self) self.backend_client = BackendClient(self, self.authentication_client) self.social_features = SocialFeatures(self.authentication_client) self.error_state = False self.running_task = None self.database_parser = None self.config_parser = None self.uninstaller = None self.owned_games_cache = [] self._classic_games_thread = None self._battlenet_games_thread = None self._installed_battlenet_games = {} self._installed_battlenet_games_lock = Lock() self.installed_games = self._parse_local_data() self.watched_running_games = set() self.notifications_enabled = False loop = asyncio.get_event_loop() loop.create_task(self._register_local_data_watcher())
def __init__(self, reader, writer, token): super().__init__(Platform.Battlenet, version, reader, writer, token) self.local_client = LocalClient(self._update_statuses) self.authentication_client = AuthenticatedHttpClient(self) self.backend_client = BackendClient(self, self.authentication_client) self.social_features = SocialFeatures(self.authentication_client) self.owned_games_cache = [] self.watched_running_games = set() self.local_games_called = False
def __init__(self, reader, writer, token): super().__init__(Platform.SuperNintendoEntertainmentSystem, __version__, reader, writer, token) self.backend_client = BackendClient(self) self.games = [] self.local_games_cache = [] self.proc = None self.running_game_id = "" self.tick_count = 0 self.create_task(self._update_local_games(), "Update local games")
def __init__(self, reader, writer, token): super().__init__(Platform.Uplay, __version__, reader, writer, token) self.client = BackendClient(self) self.local_client = LocalClient() self.cached_game_statuses = {} self.games_collection = GamesCollection() self.process_watcher = ProcessWatcher() self.game_status_notifier = GameStatusNotifier(self.process_watcher) self.tick_count = 0 self.updating_games = False self.owned_games_sent = False self.parsing_club_games = False
def __init__(self, reader, writer, token): super().__init__(Platform.ColecoVision, get_version(), reader, writer, token) try: self.config = Config() # If we can't create a good config, we can't run the plugin. except FileNotFoundError: self.close() else: self.backend_client = BackendClient(self.config) self.games = [] self.local_games_cache = self.local_games_list() self.process = None self.running_game_id = None
def __init__(self, reader, writer, token): super().__init__(Platform.NintendoWii, __version__, reader, writer, token) self.backend_client = BackendClient() self.games = [] if not os.path.exists( os.path.dirname(os.path.realpath(__file__)) + r'\gametimes.xml'): copyfile( os.path.dirname(os.path.realpath(__file__)) + r'\files\gametimes.xml', os.path.dirname(os.path.realpath(__file__)) + r'\gametimes.xml') self.game_times = self.get_the_game_times() self.local_games_cache = self.local_games_list() self.runningGame = self.runningGame = { "game_id": "", "starting_time": 0, "dolphin_running": None, "launched": False }
class PlayStation2Plugin(Plugin): def __init__(self, reader, writer, token): super().__init__(Platform.PlayStation2, __version__, reader, writer, token) self.backend_client = BackendClient() self.games = [] self.local_games_cache = self.local_games_list() async def authenticate(self, stored_credentials=None): return self.do_auth() async def pass_login_credentials(self, step, credentials, cookies): return self.do_auth() def do_auth(self): user_data = {} username = user_config.roms_path user_data["username"] = username self.store_credentials(user_data) return Authentication("pcsx2_user", user_data["username"]) async def launch_game(self, game_id): emu_path = user_config.emu_path no_gui = user_config.emu_no_gui fullscreen = user_config.emu_fullscreen config = user_config.emu_config config_folder = user_config.config_path for game in self.games: if str(game[1]) == game_id: rom_file = os.path.splitext(os.path.basename(game[0]))[0] config_folder_game = config_folder + "/" + rom_file if config and os.path.isdir(config_folder_game): config_arg = '--cfgpath=' + config_folder + "/" + rom_file if no_gui and fullscreen: subprocess.Popen([emu_path, "--nogui", "--fullscreen", config_arg, game[0]]) break if no_gui and not fullscreen: subprocess.Popen([emu_path, "--nogui", config_arg, game[0]]) break if not no_gui and fullscreen: subprocess.Popen([emu_path, "--fullscreen", config_arg, game[0]]) break subprocess.Popen([emu_path, config_arg, game[0]]) break else: if no_gui and fullscreen: subprocess.Popen([emu_path, "--nogui", "--fullscreen", game[0]]) break if no_gui and not fullscreen: subprocess.Popen([emu_path, "--nogui", game[0]]) break if not no_gui and fullscreen: subprocess.Popen([emu_path, "--fullscreen", game[0]]) break subprocess.Popen([emu_path, game[0]]) break return async def install_game(self, game_id): pass async def uninstall_game(self, game_id): pass def local_games_list(self): local_games = [] for game in self.games: local_games.append( LocalGame( str(game[1]), LocalGameState.Installed ) ) return local_games def tick(self): async def update_local_games(): loop = asyncio.get_running_loop() new_local_games_list = await loop.run_in_executor(None, self.local_games_list) notify_list = self.backend_client.get_state_changes(self.local_games_cache, new_local_games_list) self.local_games_cache = new_local_games_list for local_game_notify in notify_list: self.update_local_game_status(local_game_notify) asyncio.create_task(update_local_games()) async def get_owned_games(self): if(user_config.use_database): self.games = self.backend_client.get_games_db() else: self.games = self.backend_client.get_games_gb() owned_games = [] for game in self.games: owned_games.append( Game( str(game[1]), game[2], None, LicenseInfo(LicenseType.SinglePurchase, None) ) ) return owned_games async def get_local_games(self): return self.local_games_cache def shutdown(self): pass
class RPCS3Plugin(Plugin): def __init__(self, reader, writer, token): super().__init__(Platform.ColecoVision, get_version(), reader, writer, token) try: self.config = Config() # If we can't create a good config, we can't run the plugin. except FileNotFoundError: self.close() else: self.backend_client = BackendClient(self.config) self.games = [] self.local_games_cache = self.local_games_list() self.process = None self.running_game_id = None async def authenticate(self, stored_credentials=None): return self.do_auth() async def pass_login_credentials(self, step, credentials, cookies): return self.do_auth() def do_auth(self): username = '' with open(self.config.localusername, 'r') as username_file: username = username_file.read() user_data = {} user_data['username'] = username self.store_credentials(user_data) return Authentication('rpcs3_user', user_data['username']) async def launch_game(self, game_id): args = [] eboot_bin = self.config.joinpath( self.backend_client.get_game_path(game_id), 'USRDIR', 'EBOOT.BIN') if self.config.no_gui: args.append('--no-gui') command = [self.config.rpcs3_exe, eboot_bin] + args self.process = subprocess.Popen(command) self.backend_client.start_game_time() self.running_game_id = game_id return # Only as placeholders so the feature is recognized async def install_game(self, game_id): pass async def uninstall_game(self, game_id): pass async def prepare_game_times_context(self, game_ids): return self.get_game_times(game_ids) async def prepare_achievements_context(self, game_ids): return self.get_trophy_achs() async def get_game_time(self, game_id, context): game_time = context.get(game_id) return game_time async def get_unlocked_achievements(self, game_id, context): achs = context.get(game_id) return achs def get_game_times(self, game_ids): # Get the path of the game times file. base_path = os.path.dirname(os.path.realpath(__file__)) game_times_path = os.path.join(base_path, 'game_times.json') game_times = {} # If the file does not exist, create it with default values. if not os.path.exists(game_times_path): for game in self.games: game_id = str(game[0]) game_times[game_id] = GameTime(game_id, 0, None) with open(game_times_path, 'w', encoding='utf-8') as game_times_file: json.dump(game_times, game_times_file, indent=4) # If (when) the file exists, read it and return the game times. with open(game_times_path, 'r', encoding='utf-8') as game_times_file: game_times_json = json.load(game_times_file) for game_id in game_times_json: if game_id in game_ids: time_played = game_times_json.get(game_id).get('time_played') last_time_played = game_times_json.get(game_id).get('last_time_played') game_times[game_id] = GameTime(game_id, time_played, last_time_played) return game_times def get_trophy_achs(self): game_ids = [] for game in self.games: game_ids.append(game[0]) trophies = None all_achs = {} for game_id in game_ids: game_path = self.backend_client.get_game_path(game_id) try: trophies = Trophy(self.config, game_path) keys = trophies.tropusr.table6.keys() game_achs = [] for key in keys: ach = trophies.trop2ach(key) if ach is not None: game_achs.append(trophies.trop2ach(key)) all_achs[game_id] = game_achs # If tropusr doesn't exist, this game has no trophies. except AttributeError: all_achs[game_id] = [] return all_achs def tick(self): try: if self.process.poll() is not None: self.backend_client.end_game_time() self.update_json_game_time( self.running_game_id, self.backend_client.get_session_duration(), int(time.time())) # Only update recently played games. Updating all game times every second fills up log way too quickly. self.create_task(self.update_galaxy_game_times(self.running_game_id), 'Update Galaxy game times') self.process = None self.running_game_id = None except AttributeError: pass self.create_task(self.update_local_games(), 'Update local games') self.create_task(self.update_achievements(), 'Update achievements') async def update_local_games(self): loop = asyncio.get_running_loop() new_list = await loop.run_in_executor(None, self.local_games_list) notify_list = self.backend_client.get_state_changes(self.local_games_cache, new_list) self.local_games_cache = new_list for local_game_notify in notify_list: self.update_local_game_status(local_game_notify) async def update_galaxy_game_times(self, game_id): # Leave time for Galaxy to fetch games before updating times await asyncio.sleep(60) loop = asyncio.get_running_loop() game_times = await loop.run_in_executor(None, self.get_game_times, [game_id]) for game_id in game_times: self.update_game_time(game_times[game_id]) async def update_achievements(self): # Leave time for Galaxy to fetch games before updating times await asyncio.sleep(60) loop = asyncio.get_running_loop() achs = await loop.run_in_executor(None, self.get_trophy_achs) # for ach in achs: # self.unlock_achievement(ach) # TODO - how/when to handle this? def update_json_game_time(self, game_id, duration, last_time_played): # Get the path of the game times file. base_path = os.path.dirname(os.path.realpath(__file__)) game_times_path = '{}/game_times.json'.format(base_path) game_times_json = None with open(game_times_path, 'r', encoding='utf-8') as game_times_file: game_times_json = json.load(game_times_file) old_time_played = game_times_json.get(game_id).get('time_played') new_time_played = old_time_played + duration game_times_json[game_id]['time_played'] = new_time_played game_times_json[game_id]['last_time_played'] = last_time_played with open(game_times_path, 'w', encoding='utf-8') as game_times_file: json.dump(game_times_json, game_times_file, indent=4) self.update_game_time(GameTime(game_id, new_time_played, last_time_played)) def local_games_list(self): local_games = [] for game in self.games: local_games.append(LocalGame( game[0], LocalGameState.Installed)) return local_games async def get_owned_games(self): self.games = self.backend_client.get_games() owned_games = [] for game in self.games: owned_games.append(Game( game[0], game[1], None, LicenseInfo(LicenseType.SinglePurchase, None))) return owned_games async def get_local_games(self): return self.local_games_cache
class DolphinPlugin(Plugin): def __init__(self, reader, writer, token): super().__init__(Platform.NintendoWii, __version__, reader, writer, token) self.backend_client = BackendClient() self.games = [] if not os.path.exists( os.path.dirname(os.path.realpath(__file__)) + r'\gametimes.xml'): copyfile( os.path.dirname(os.path.realpath(__file__)) + r'\files\gametimes.xml', os.path.dirname(os.path.realpath(__file__)) + r'\gametimes.xml') self.game_times = self.get_the_game_times() self.local_games_cache = self.local_games_list() self.runningGame = self.runningGame = { "game_id": "", "starting_time": 0, "dolphin_running": None, "launched": False } async def authenticate(self, stored_credentials=None): return self.do_auth() def get_the_game_times(self): file = ElementTree.parse( os.path.dirname(os.path.realpath(__file__)) + r'\gametimes.xml') game_times = {} games_xml = file.getroot() for game in games_xml.iter('game'): game_id = str(game.find('id').text) tt = game.find('time').text ltp = game.find('lasttimeplayed').text game_times[game_id] = [tt, ltp] return game_times async def pass_login_credentials(self, step, credentials, cookies): return self.do_auth() def do_auth(self): user_data = {} username = user_config.roms_path user_data["username"] = username self.store_credentials(user_data) return Authentication("Dolphin", user_data["username"]) async def launch_game(self, game_id): emu_path = user_config.emu_path for game in self.games: if game.id == game_id: if not user_config.retroarch: openDolphin = Popen([emu_path, "-b", "-e", game.path]) gameStartingTime = time.time() self.runningGame = { "game_id": game_id, "starting_time": gameStartingTime, "dolphin_running": openDolphin } else: Popen([ user_config.retroarch_executable, "-L", user_config.core_path + r'\dolphin_libretro.dll', game.path ]) break return async def install_game(self, game_id): pass async def uninstall_game(self, game_id): pass async def get_game_time(self, game_id, context=None): game_times = self.game_times game_time = int(game_times[game_id][0]) game_time /= 60 return GameTime(game_id, game_time, game_times[game_id][1]) def local_games_list(self): local_games = [] for game in self.games: local_games.append(LocalGame(game.id, LocalGameState.Installed)) return local_games def tick(self): async def update_local_games(): loop = asyncio.get_running_loop() new_local_games_list = await loop.run_in_executor( None, self.local_games_list) notify_list = self.backend_client.get_state_changes( self.local_games_cache, new_local_games_list) self.local_games_cache = new_local_games_list for local_game_notify in notify_list: self.update_local_game_status(local_game_notify) file = ElementTree.parse( os.path.dirname(os.path.realpath(__file__)) + r'\gametimes.xml') if self.runningGame["dolphin_running"] is not None: if self.runningGame["dolphin_running"].poll() is None: self.runningGame["launched"] = True if self.runningGame["dolphin_running"].poll() is not None: if self.runningGame["launched"]: current_time = round(time.time()) runtime = time.time() - self.runningGame["starting_time"] games_xml = file.getroot() for game in games_xml.iter('game'): if str(game.find( 'id').text) == self.runningGame["game_id"]: previous_time = int(game.find('time').text) total_time = round(previous_time + runtime) game.find('time').text = str(total_time) game.find('lasttimeplayed').text = str( current_time) self.update_game_time( GameTime(self.runningGame["game_id"], int(total_time / 60), current_time)) file.write( os.path.dirname(os.path.realpath(__file__)) + r'\gametimes.xml') self.runningGame["launched"] = False asyncio.create_task(update_local_games()) async def get_owned_games(self): self.games = self.backend_client.get_games_db() owned_games = [] for game in self.games: owned_games.append( Game(game.id, game.name, None, LicenseInfo(LicenseType.SinglePurchase, None))) return owned_games async def get_local_games(self): return self.local_games_cache def shutdown(self): pass
class BNetPlugin(Plugin): PRODUCT_DB_PATH = pathlib.Path(AGENT_PATH) / 'product.db' CONFIG_PATH = CONFIG_PATH def __init__(self, reader, writer, token): super().__init__(Platform.Battlenet, version, reader, writer, token) log.info(f"Starting Battle.net plugin, version {version}") self.bnet_client = None self.local_client = LocalClient() self.authentication_client = AuthenticatedHttpClient(self) self.backend_client = BackendClient(self, self.authentication_client) self.social_features = SocialFeatures(self.authentication_client) self.error_state = False self.running_task = None self.database_parser = None self.config_parser = None self.uninstaller = None self.owned_games_cache = [] self._classic_games_thread = None self._battlenet_games_thread = None self._installed_battlenet_games = {} self._installed_battlenet_games_lock = Lock() self.installed_games = self._parse_local_data() self.watched_running_games = set() self.notifications_enabled = False loop = asyncio.get_event_loop() loop.create_task(self._register_local_data_watcher()) async def _register_local_data_watcher(self): async def ping(event, interval): while True: await asyncio.sleep(interval) if not self.watched_running_games: if not event.is_set(): event.set() parse_local_data_event = asyncio.Event() FileWatcher(self.CONFIG_PATH, parse_local_data_event, interval=1) FileWatcher(self.PRODUCT_DB_PATH, parse_local_data_event, interval=2.5) asyncio.create_task(ping(parse_local_data_event, 30)) while True: await parse_local_data_event.wait() refreshed_games = self._parse_local_data() if not self.notifications_enabled: self._update_statuses(refreshed_games, self.installed_games) self.installed_games = refreshed_games parse_local_data_event.clear() async def _notify_about_game_stop(self, game, starting_timeout): id_to_watch = game.info.id if id_to_watch in self.watched_running_games: log.debug(f'Game {id_to_watch} is already watched. Skipping') return try: self.watched_running_games.add(id_to_watch) await asyncio.sleep(starting_timeout) ProcessProvider().update_games_processes([game]) log.info(f'Setuping process watcher for {game._processes}') loop = asyncio.get_event_loop() await loop.run_in_executor(None, game.wait_until_game_stops) finally: self.update_local_game_status(LocalGame(id_to_watch, LocalGameState.Installed)) self.watched_running_games.remove(id_to_watch) def _update_statuses(self, refreshed_games, previous_games): for blizz_id, refr in refreshed_games.items(): prev = previous_games.get(blizz_id, None) if prev is None: if refr.playable: log.debug('Detected playable game') state = LocalGameState.Installed else: log.debug('Detected installation begin') state = LocalGameState.None_ elif refr.playable and not prev.playable: log.debug('Detected playable game') state = LocalGameState.Installed elif refr.last_played != prev.last_played: log.debug('Detected launched game') state = LocalGameState.Installed | LocalGameState.Running asyncio.create_task(self._notify_about_game_stop(refr, 5)) else: continue log.info(f'Changing game {blizz_id} state to {state}') self.update_local_game_status(LocalGame(blizz_id, state)) for blizz_id, prev in previous_games.items(): refr = refreshed_games.get(blizz_id, None) if refr is None: log.debug('Detected uninstalled game') state = LocalGameState.None_ self.update_local_game_status(LocalGame(blizz_id, state)) def _load_local_files(self): try: product_db = load_product_db(self.PRODUCT_DB_PATH) self.database_parser = DatabaseParser(product_db) except FileNotFoundError as e: log.warning(f"product.db not found: {repr(e)}") return False except WindowsError as e: # 5 WindowsError access denied if e.winerror == 5: log.warning(f"product.db not accessible: {repr(e)}") self.config_parser = ConfigParser(None) return False else: raise () except OSError as e: if e.errno == errno.EACCES: log.warning(f"product.db not accessible: {repr(e)}") self.config_parser = ConfigParser(None) return False else: raise () else: if self.local_client.is_installed != self.database_parser.battlenet_present: self.local_client.refresh() try: config = load_config(self.CONFIG_PATH) self.config_parser = ConfigParser(config) except FileNotFoundError as e: log.warning(f"config file not found: {repr(e)}") self.config_parser = ConfigParser(None) return False except WindowsError as e: # 5 WindowsError access denied if e.winerror == 5: log.warning(f"config file not accessible: {repr(e)}") self.config_parser = ConfigParser(None) return False else: raise () except OSError as e: if e.errno == errno.EACCES: log.warning(f"config file not accessible: {repr(e)}") self.config_parser = ConfigParser(None) return False else: raise () return True def _get_battlenet_installed_games(self): def _add_battlenet_game(config_game, db_game): if config_game.uninstall_tag != db_game.uninstall_tag: return None try: blizzard_game = Blizzard[config_game.uid] except KeyError: log.warning(f'[{config_game.uid}] is not known blizzard game. Skipping') return None try: log.info(f"Adding {blizzard_game.blizzard_id} {blizzard_game.name} to installed games") return InstalledGame( blizzard_game, config_game.uninstall_tag, db_game.version, config_game.last_played, db_game.install_path, db_game.playable ) except FileNotFoundError as e: log.warning(str(e) + '. Probably outdated product.db after uninstall. Skipping') return None games = {} for db_game in self.database_parser.games: for config_game in self.config_parser.games: installed_game = _add_battlenet_game(config_game, db_game) if installed_game: games[installed_game.info.id] = installed_game self._installed_battlenet_games_lock.acquire() self._installed_battlenet_games = games self._installed_battlenet_games_lock.release() def _parse_local_data(self): """Game is considered as installed when present in both config and product.db""" games = {} # give threads 4 seconds to finish join_timeout = 4 if not self._classic_games_thread or not self._classic_games_thread.isAlive(): self._classic_games_thread = Thread(target=self.local_client.find_classic_games, daemon=True) self._classic_games_thread.start() log.info("Started classic games thread") if not self._load_local_files(): self._classic_games_thread.join(join_timeout) if not self.local_client.classics_lock.acquire(False): return [] else: installed_classics = self.local_client.installed_classics self.local_client.classics_lock.release() return installed_classics try: if SYSTEM == pf.WINDOWS and self.uninstaller is None: uninstaller_path = pathlib.Path(AGENT_PATH) / 'Blizzard Uninstaller.exe' self.uninstaller = Uninstaller(uninstaller_path) except FileNotFoundError as e: log.warning('uninstaller not found' + str(e)) try: if self.local_client.is_installed != self.database_parser.battlenet_present: self.local_client.refresh() log.info(f"Games found in db {self.database_parser.games}") log.info(f"Games found in config {self.config_parser.games}") if not self._battlenet_games_thread or not self._battlenet_games_thread.isAlive(): self._battlenet_games_thread = Thread(target=self._get_battlenet_installed_games, daemon=True) self._battlenet_games_thread.start() log.info("Started classic games thread") except Exception as e: log.exception(str(e)) finally: self._classic_games_thread.join(join_timeout) self._battlenet_games_thread.join(join_timeout) if self.local_client.classics_lock.acquire(False): games = self.local_client.installed_classics self.local_client.classics_lock.release() if self._installed_battlenet_games_lock.acquire(False): games = {**self._installed_battlenet_games, **games} self._installed_battlenet_games_lock.release() return games def log_out(self): if self.backend_client: asyncio.create_task(self.authentication_client.shutdown()) self.authentication_client.user_details = None self.owned_games_cache = [] async def open_battlenet_browser(self): url = f"https://www.blizzard.com/apps/battle.net/desktop" log.info(f'Opening battle.net website: {url}') loop = asyncio.get_running_loop() await loop.run_in_executor(None, lambda x: webbrowser.open(x, autoraise=True), url) async def install_game(self, game_id): if not self.authentication_client.is_authenticated(): raise AuthenticationRequired() installed_game = self.installed_games.get(game_id, None) if installed_game and os.access(installed_game.install_path, os.F_OK): log.warning("Received install command on an already installed game") return await self.launch_game(game_id) if game_id in Blizzard.legacy_game_ids: if SYSTEM == pf.WINDOWS: platform = 'windows' elif SYSTEM == pf.MACOS: platform = 'macos' webbrowser.open(f"https://www.blizzard.com/download/confirmation?platform={platform}&locale=enUS&version=LIVE&id={game_id}") return try: self.local_client.refresh() log.info(f'Installing game of id {game_id}') self.local_client.install_game(game_id) except ClientNotInstalledError as e: log.warning(e) await self.open_battlenet_browser() except Exception as e: log.exception(f"Installing game {game_id} failed: {e}") def _open_battlenet_at_id(self, game_id): try: self.local_client.refresh() self.local_client.open_battlenet(game_id) except Exception as e: log.exception(f"Opening battlenet client on specific game_id {game_id} failed {e}") try: self.local_client.open_battlenet() except Exception as e: log.exception(f"Opening battlenet client failed {e}") async def uninstall_game(self, game_id): if not self.authentication_client.is_authenticated(): raise AuthenticationRequired() if game_id == 'wow_classic': # attempting to uninstall classic wow through protocol gives you a message that the game cannot # be uninstalled through protocol and you should use battle.net return self._open_battlenet_at_id(game_id) if SYSTEM == pf.MACOS: self._open_battlenet_at_id(game_id) else: try: installed_game = self.installed_games.get(game_id, None) if installed_game is None or not os.access(installed_game.install_path, os.F_OK): log.error(f'Cannot uninstall {Blizzard[game_id].uid}') self.update_local_game_status(LocalGame(game_id, LocalGameState.None_)) return if not isinstance(installed_game.info, ClassicGame): if self.uninstaller is None: raise FileNotFoundError('Uninstaller not found') uninstall_tag = installed_game.uninstall_tag client_lang = self.config_parser.locale_language self.uninstaller.uninstall_game(installed_game, uninstall_tag, client_lang) except Exception as e: log.exception(f'Uninstalling game {game_id} failed: {e}') async def launch_game(self, game_id): if not self.authentication_client.is_authenticated(): raise AuthenticationRequired() try: if self.installed_games is None: log.error(f'Launching game that is not installed: {game_id}') return await self.install_game(game_id) game = self.installed_games.get(game_id, None) if game is None: log.error(f'Launching game that is not installed: {game_id}') return await self.install_game(game_id) if isinstance(game.info, ClassicGame): log.info(f'Launching game of id: {game_id}, {game} at path {os.path.join(game.install_path, game.info.exe)}') if SYSTEM == pf.WINDOWS: subprocess.Popen(os.path.join(game.install_path, game.info.exe)) elif SYSTEM == pf.MACOS: if not game.info.bundle_id: log.warning(f"{game.name} has no bundle id, help by providing us bundle id of this game") subprocess.Popen(['open', '-b', game.info.bundle_id]) self.update_local_game_status(LocalGame(game_id, LocalGameState.Installed | LocalGameState.Running)) asyncio.create_task(self._notify_about_game_stop(game, 6)) return self.local_client.refresh() log.info(f'Launching game of id: {game_id}, {game}') await self.local_client.launch_game(game, wait_sec=60) self.update_local_game_status(LocalGame(game_id, LocalGameState.Installed | LocalGameState.Running)) self.local_client.close_window() asyncio.create_task(self._notify_about_game_stop(game, 3)) except ClientNotInstalledError as e: log.warning(e) await self.open_battlenet_browser() except TimeoutError as e: log.warning(str(e)) except Exception as e: log.exception(f"Launching game {game_id} failed: {e}") async def authenticate(self, stored_credentials=None): try: if stored_credentials: auth_data = self.authentication_client.process_stored_credentials(stored_credentials) try: await self.authentication_client.create_session() await self.backend_client.refresh_cookies() auth_status = await self.backend_client.validate_access_token(auth_data.access_token) except (BackendNotAvailable, BackendError, NetworkError, UnknownError, BackendTimeout) as e: raise e except Exception: raise InvalidCredentials() if self.authentication_client.validate_auth_status(auth_status): self.authentication_client.user_details = await self.backend_client.get_user_info() return self.authentication_client.parse_user_details() else: return self.authentication_client.authenticate_using_login() except Exception as e: raise e async def pass_login_credentials(self, step, credentials, cookies): if "logout&app=oauth" in credentials['end_uri']: # 2fa expired, repeat authentication return self.authentication_client.authenticate_using_login() if self.authentication_client.attempted_to_set_battle_tag: self.authentication_client.user_details = await self.backend_client.get_user_info() return self.authentication_client.parse_auth_after_setting_battletag() cookie_jar = self.authentication_client.parse_cookies(cookies) auth_data = await self.authentication_client.get_auth_data_login(cookie_jar, credentials) try: await self.authentication_client.create_session() await self.backend_client.refresh_cookies() except (BackendNotAvailable, BackendError, NetworkError, UnknownError, BackendTimeout) as e: raise e except Exception: raise InvalidCredentials() auth_status = await self.backend_client.validate_access_token(auth_data.access_token) if not ("authorities" in auth_status and "IS_AUTHENTICATED_FULLY" in auth_status["authorities"]): raise InvalidCredentials() self.authentication_client.user_details = await self.backend_client.get_user_info() self.authentication_client.set_credentials() return self.authentication_client.parse_battletag() async def get_friends(self): if not self.authentication_client.is_authenticated(): raise AuthenticationRequired() friends_list = await self.social_features.get_friends() return [FriendInfo(user_id=friend.id.low, user_name='') for friend in friends_list] async def get_owned_games(self): if not self.authentication_client.is_authenticated(): raise AuthenticationRequired() def _parse_classic_games(classic_games): for classic_game in classic_games["classicGames"]: log.info(f"looking for {classic_game} in classic games") try: blizzard_game = Blizzard[classic_game["localizedGameName"].replace(u'\xa0', ' ')] log.info(f"match! {blizzard_game}") classic_game["titleId"] = blizzard_game.uid classic_game["gameAccountStatus"] = "Good" except KeyError: continue return classic_games def _get_not_added_free_games(owned_games): owned_games_ids = [] for game in owned_games: if "titleId" in game: owned_games_ids.append(str(game["titleId"])) return [{"titleId": game.blizzard_id, "localizedGameName": game.name, "gameAccountStatus": "Free"} for game in Blizzard.free_games if game.blizzard_id not in owned_games_ids] try: games = await self.backend_client.get_owned_games() classic_games = _parse_classic_games(await self.backend_client.get_owned_classic_games()) owned_games = games["gameAccounts"] + classic_games["classicGames"] # Add wow classic if retail wow is present in owned games for owned_game in owned_games.copy(): if 'titleId' in owned_game: if owned_game['titleId'] == 5730135: owned_games.append({'titleId': 'wow_classic', 'localizedGameName': 'World of Warcraft Classic', 'gameAccountStatus': owned_game['gameAccountStatus']}) free_games_to_add = _get_not_added_free_games(owned_games) owned_games += free_games_to_add log.info(f"Owned games {owned_games} with free games") self.owned_games_cache = owned_games return [ Game( str(game["titleId"]), game["localizedGameName"], [], LicenseInfo(License_Map[game["gameAccountStatus"]]), ) for game in self.owned_games_cache if "titleId" in game ] except Exception as e: log.exception(f"failed to get owned games: {repr(e)}") raise async def get_local_games(self): try: local_games = [] running_games = ProcessProvider().update_games_processes(self.installed_games.values()) log.info(f"Installed games {self.installed_games.items()}") log.info(f"Running games {running_games}") for id_, game in self.installed_games.items(): if game.playable: state = LocalGameState.Installed if id_ in running_games: state |= LocalGameState.Running else: state = LocalGameState.None_ local_games.append(LocalGame(id_, state)) return local_games except Exception as e: log.exception(f"failed to get local games: {str(e)}") raise finally: self.enable_notifications = True async def _get_wow_achievements(self): achievements = [] try: characters_data = await self.backend_client.get_wow_character_data() characters_data = characters_data["characters"] wow_character_data = await asyncio.gather( *[ self.backend_client.get_wow_character_achievements(character["realm"], character["name"]) for character in characters_data ], return_exceptions=True, ) for data in wow_character_data: if isinstance(data, requests.Timeout) or isinstance(data, requests.ConnectionError): raise data wow_achievement_data = [ list( zip( data["achievements"]["achievementsCompleted"], data["achievements"]["achievementsCompletedTimestamp"], ) ) for data in wow_character_data if type(data) is dict ] already_in = set() for char_ach in wow_achievement_data: for ach in char_ach: if ach[0] not in already_in: achievements.append(Achievement(achievement_id=ach[0], unlock_time=int(ach[1] / 1000))) already_in.add(ach[0]) except (AccessTokenExpired, BackendError) as e: log.exception(str(e)) with open('wow.json', 'w') as f: f.write(json.dumps(achievements, cls=DataclassJSONEncoder)) return achievements async def _get_sc2_achievements(self): account_data = await self.backend_client.get_sc2_player_data(self.authentication_client.user_details["id"]) # TODO what if more sc2 accounts? assert len(account_data) == 1 account_data = account_data[0] profile_data = await self.backend_client.get_sc2_profile_data( account_data["regionId"], account_data["realmId"], account_data["profileId"] ) sc2_achievement_data = [ Achievement(achievement_id=achievement["achievementId"], unlock_time=achievement["completionDate"]) for achievement in profile_data["earnedAchievements"] if achievement["isComplete"] ] with open('sc2.json', 'w') as f: f.write(json.dumps(sc2_achievement_data, cls=DataclassJSONEncoder)) return sc2_achievement_data # async def get_unlocked_achievements(self, game_id): # if not self.website_client.is_authenticated(): # raise AuthenticationRequired() # try: # if game_id == "21298": # return await self._get_sc2_achievements() # elif game_id == "5730135": # return await self._get_wow_achievements() # else: # return [] # except requests.Timeout: # raise BackendTimeout() # except requests.ConnectionError: # raise NetworkError() # except Exception as e: # log.exception(str(e)) # return [] async def _tick_runner(self): if not self.bnet_client: return try: self.error_state = await self.bnet_client.tick() except Exception as e: self.error_state = True log.exception(f"error state: {str(e)}") raise def tick(self): if not self.error_state and (not self.running_task or self.running_task.done()): self.running_task = asyncio.create_task(self._tick_runner()) elif self.error_state: sys.exit(1) def shutdown(self): log.info("Plugin shutdown.") asyncio.create_task(self.authentication_client.shutdown())
class UplayPlugin(Plugin): def __init__(self, reader, writer, token): super().__init__(Platform.Uplay, __version__, reader, writer, token) self.client = BackendClient(self) self.local_client = LocalClient() self.cached_game_statuses = {} self.games_collection = GamesCollection() self.process_watcher = ProcessWatcher() self.game_status_notifier = GameStatusNotifier(self.process_watcher) self.tick_count = 0 self.updating_games = False self.owned_games_sent = False self.parsing_club_games = False self.parsed_local_games = False def auth_lost(self): self.lost_authentication() async def authenticate(self, stored_credentials=None): if not stored_credentials: return NextStep("web_session", AUTH_PARAMS, cookies=COOKIES) else: try: user_data = await self.client.authorise_with_stored_credentials( stored_credentials) except (AccessDenied, AuthenticationRequired) as e: log.exception(repr(e)) raise InvalidCredentials() except Exception as e: log.exception(repr(e)) raise e else: self.local_client.initialize(user_data['userId']) self.client.set_auth_lost_callback(self.auth_lost) return Authentication(user_data['userId'], user_data['username']) async def pass_login_credentials(self, step, credentials, cookies): """Called just after CEF authentication (called as NextStep by authenticate)""" user_data = await self.client.authorise_with_cookies(cookies) self.local_client.initialize(user_data['userId']) self.client.set_auth_lost_callback(self.auth_lost) return Authentication(user_data['userId'], user_data['username']) async def get_owned_games(self): if not self.client.is_authenticated(): raise AuthenticationRequired() if SYSTEM == System.WINDOWS: self._parse_local_games() self._parse_local_game_ownership() await self._parse_club_games() try: await self._parse_subscription_games() except Exception as e: log.warning( f"Parsing subscriptions failed, most likely account without subscription {repr(e)}" ) self.owned_games_sent = True for game in self.games_collection: game.considered_for_sending = True return [ game.as_galaxy_game() for game in self.games_collection if game.owned ] async def _parse_subscription_games(self): subscription_games = [] sub_response = await self.client.get_subscription() if not sub_response: return for game in sub_response['games']: subscription_games.append( UbisoftGame(space_id='', launch_id=str(game['uplayGameId']), install_id=str(game['uplayGameId']), third_party_id='', name=game['name'], path='', type=GameType.New, special_registry_path='', exe='', status=GameStatus.Unknown, owned=game['ownership'], activation_id=str(game['id']))) self.games_collection.extend(subscription_games) async def _parse_club_games(self): if not self.parsing_club_games: try: self.parsing_club_games = True games = await self.client.get_club_titles() club_games = [] for game in games: if "platform" in game: if game["platform"] == "PC": log.info( f"Parsed game from Club Request {game['title']}" ) club_games.append( UbisoftGame(space_id=game['spaceId'], launch_id='', install_id='', third_party_id='', name=game['title'], path='', type=GameType.New, special_registry_path='', exe='', status=GameStatus.Unknown, owned=True)) else: log.debug( f"Skipped game from Club Request for {game['platform']}: {game['spaceId']}, {game['title']}" ) self.games_collection.extend(club_games) except ApplicationError as e: log.error( f"Encountered exception while parsing club games {repr(e)}" ) raise e except Exception as e: log.error( f"Encountered exception while parsing club games {repr(e)}" ) finally: self.parsing_club_games = False else: # Wait until club games get parsed if parsing is already in progress while self.parsing_club_games: await asyncio.sleep(0.2) def _parse_local_games(self): """Parsing local files should lead to every game having a launch id. A game in the games_collection which doesn't have a launch id probably means that a game was added through the get_club_titles request but its space id was not present in configuration file and we couldn't find a matching launch id for it.""" if self.local_client.configurations_accessible(): try: configuration_data = self.local_client.read_config() p = LocalParser() games = [] for game in p.parse_games(configuration_data): games.append(game) self.games_collection.extend(games) except scanner.ScannerError as e: log.error( f"Scanner error while parsing configuration, yaml is probably corrupted {repr(e)}" ) def _parse_local_game_ownership(self): if self.local_client.ownership_accessible(): ownership_data = self.local_client.read_ownership() p = LocalParser() ownership_records = p.get_owned_local_games(ownership_data) log.info(f"Ownership Records {ownership_records}") for game in self.games_collection: if game.install_id: if int(game.install_id) in ownership_records: game.owned = True if game.launch_id: if int(game.launch_id) in ownership_records: game.owned = True def _update_games(self): self.updating_games = True self._parse_local_games() self._parse_local_game_ownership() self.updating_games = False def _update_local_games_status(self): cached_statuses = self.cached_game_statuses if cached_statuses is None: return for game in self.games_collection: try: self.game_status_notifier.update_game(game) if game.status != cached_statuses[game.install_id]: log.info( f"Game {game.name} path changed: updating status from {cached_statuses[game.install_id]} to {game.status}" ) self.update_local_game_status(game.as_local_game()) self.cached_game_statuses[game.install_id] = game.status except KeyError: self.game_status_notifier.update_game(game) ''' If a game wasn't previously in a cache then and it appears with an installed or running status it most likely means that client was just installed ''' if game.status in [GameStatus.Installed, GameStatus.Running]: self.update_local_game_status(game.as_local_game()) self.cached_game_statuses[game.install_id] = game.status if SYSTEM == System.WINDOWS: async def get_local_games(self): self._parse_local_games() local_games = [] for game in self.games_collection: self.cached_game_statuses[game.launch_id] = game.status if game.status == GameStatus.Installed or game.status == GameStatus.Running: local_games.append(game.as_local_game()) self._update_local_games_status() self.parsed_local_games = True return local_games async def _add_new_games(self, games): await self._parse_club_games() self._parse_local_game_ownership() for game in games: if game.owned: self.add_game(game.as_galaxy_game()) async def prepare_game_times_context(self, game_ids): return await self.get_playtime(game_ids) async def get_game_time(self, game_id, context): game_time = context.get(game_id) if game_time is None: raise UnknownError("Game {} not owned".format(game_id)) return game_time async def get_playtime(self, game_ids): if not self.client.is_authenticated(): raise AuthenticationRequired() games_playtime = {} blacklist = json.loads( self.persistent_cache.get('games_without_stats', '{}')) current_time = int(time.time()) for game_id in game_ids: if not self.games_collection.get(game_id): await self.get_owned_games() break for game_id in game_ids: try: expire_in = blacklist.get(game_id, 0) - current_time if expire_in > 0: log.debug( f'Cache: No game stats for {game_id}. Recheck in {expire_in}s' ) games_playtime[game_id] = GameTime(game_id, None, None) continue game = self.games_collection[game_id] if not game.space_id: games_playtime[game_id] = GameTime(game_id, None, None) continue try: response = await self.client.get_game_stats(game.space_id) except ApplicationError as err: self._game_time_import_failure(game_id, err) continue statscards = response.get('Statscards', None) if statscards is None: blacklist[ game_id] = current_time + 3600 * 24 * 14 # two weeks games_playtime[game_id] = GameTime(game_id, None, None) continue playtime, last_played = find_times(statscards, game_id) if playtime == 0: playtime = None if last_played == 0: last_played = None log.info( f'Stats for {game.name}: playtime: {playtime}, last_played: {last_played}' ) games_playtime[game_id] = GameTime(game_id, playtime, last_played) except Exception as e: log.error( f"Getting game times for game {game_id} has crashed: " + repr(e)) self._game_time_import_failure(game_id, UnknownError()) self.persistent_cache['games_without_stats'] = json.dumps(blacklist) self.push_cache() return games_playtime async def get_unlocked_challenges(self, game_id): """Challenges are a unique uplay club feature and don't directly translate to achievements""" if not self.client.is_authenticated(): raise AuthenticationRequired() for game in self.games_collection: if game.space_id == game_id or game.launch_id == game_id: if not game.space_id: return [] challenges = await self.client.get_challenges(game.space_id) return [ Achievement(achievement_id=challenge["id"], achievement_name=challenge["name"], unlock_time=int( datetime.datetime.timestamp( dateutil.parser.parse( challenge["completionDate"])))) for challenge in challenges["actions"] if challenge["isCompleted"] and not challenge["isBadge"] ] if SYSTEM == System.WINDOWS: async def launch_game(self, game_id): if not self.parsed_local_games: await self.get_local_games() elif not self.user_can_perform_actions(): return for game in self.games_collection.get_local_games(): if (game.space_id == game_id or game.install_id == game_id or game.launch_id == game_id) and game.status == GameStatus.Installed: if game.type == GameType.Steam: if is_steam_installed(): url = f"start steam://rungameid/{game.third_party_id}" else: url = f"start uplay://open/game/{game.launch_id}" elif game.type == GameType.New or game.type == GameType.Legacy: log.debug('Launching game') self.game_status_notifier._legacy_game_launched = True url = f"start uplay://launch/{game.launch_id}" else: log.error(f"Unsupported game type {game.name}") self.open_uplay_client() return log.info( f"Launching game '{game.name}' by protocol: [{url}]") subprocess.Popen(url, shell=True) self.reset_tick_count() return for game in self.games_collection: if (game.space_id == game_id or game.install_id == game_id) and game.status in [ GameStatus.NotInstalled, GameStatus.Unknown ]: log.warning("Game is not installed, installing") return await self.install_game(game_id) log.info("Failed to launch game, launching client instead.") self.open_uplay_client() async def activate_game(self, activation_id): if not await self.client.activate_game(activation_id): log.info(f"Couldnt activate game with id {activation_id}") return log.info(f"Activated game with id {activation_id}") timeout = time.time() + 3 while timeout >= time.time(): if self.local_client.ownership_changed(): # Will refresh informations in collection about the game await self.get_owned_games() await asyncio.sleep(0.1) if SYSTEM == System.WINDOWS: async def install_game(self, game_id, retry=False): log.debug(self.games_collection) if not self.user_can_perform_actions(): return for game in self.games_collection: game_ids = [game.space_id, game.install_id, game.launch_id] if (game_id in game_ids) and game.owned and game.status in [ GameStatus.NotInstalled, GameStatus.Unknown ]: if game.install_id: log.info(f"Installing game: {game_id}, {game}") subprocess.Popen( f"start uplay://install/{game.install_id}", shell=True) return if (game_id in game_ids) and game.status == GameStatus.Installed: log.warning("Game already installed, launching") return await self.launch_game(game_id) if (game_id in game_ids ) and not game.owned and game.activation_id and not retry: log.warning("Activating game from subscription") if not self.local_client.is_running(): self.open_uplay_client() timeout = time.time() + 10 while not self.local_client.is_running( ) and time.time() <= timeout: await asyncio.sleep(0.1) await self.activate_game(game.activation_id) asyncio.create_task( self.install_game(game_id=game_id, retry=True)) # if launch_id is not known, try to launch local client instead self.open_uplay_client() log.info( f"Did not found game with game_id: {game_id}, proper launch_id and NotInstalled status, launching client." ) if SYSTEM == System.WINDOWS: async def uninstall_game(self, game_id): if not self.user_can_perform_actions(): return for game in self.games_collection.get_local_games(): if (game.space_id == game_id or game.launch_id == game_id) and game.status == GameStatus.Installed: subprocess.Popen( f"start uplay://uninstall/{game.launch_id}", shell=True) return self.open_uplay_client() log.info( f"Did not found game with game_id: {game_id}, proper launch_id and Installed status, launching client." ) def user_can_perform_actions(self): if not self.local_client.is_installed: self.open_uplay_browser() return False if not self.local_client.was_user_logged_in: self.open_uplay_client() return False return True def open_uplay_client(self): subprocess.Popen("start uplay://", shell=True) def open_uplay_browser(self): url = 'https://uplay.ubisoft.com' log.info(f"Opening uplay website: {url}") webbrowser.open(url, autoraise=True) def refresh_game_statuses(self): if not self.local_client.was_user_logged_in: return statuses = self.game_status_notifier.statuses new_games = [] for game in self.games_collection: try: if statuses[ game. install_id] == GameStatus.Installed and game.status in [ GameStatus.NotInstalled, GameStatus.Unknown ]: log.info( f"updating status for {game.name} to installed from not installed" ) game.status = GameStatus.Installed self.update_local_game_status(game.as_local_game()) elif statuses[ game. install_id] == GameStatus.Installed and game.status == GameStatus.Running: log.info( f"updating status for {game.name} to installed from running" ) game.status = GameStatus.Installed self.update_local_game_status(game.as_local_game()) asyncio.create_task(self.prevent_uplay_from_showing()) elif statuses[ game. install_id] == GameStatus.Running and game.status != GameStatus.Running: log.info(f"updating status for {game.name} to running") game.status = GameStatus.Running self.update_local_game_status(game.as_local_game()) elif statuses[game.install_id] in [ GameStatus.NotInstalled, GameStatus.Unknown ] and game.status not in [ GameStatus.NotInstalled, GameStatus.Unknown ]: log.info( f"updating status for {game.name} to not installed") game.status = GameStatus.NotInstalled self.update_local_game_status(game.as_local_game()) except KeyError: continue if self.owned_games_sent and not game.considered_for_sending: game.considered_for_sending = True new_games.append(game) if new_games: asyncio.create_task(self._add_new_games(new_games)) async def get_friends(self): friends = await self.client.get_friends() return [ FriendInfo(user_id=friend["pid"], user_name=friend["nameOnPlatform"]) for friend in friends["friends"] ] async def get_subscriptions(self) -> List[Subscription]: sub_status = await self.client.get_subscription() sub_status = True if sub_status else False return [ Subscription( subscription_name="Uplay+", end_time=None, owned=sub_status, subscription_discovery=SubscriptionDiscovery.AUTOMATIC) ] async def prepare_subscription_games_context( self, subscription_names: List[str]) -> Any: sub_games_response = await self.client.get_subscription() if sub_games_response: return [ SubscriptionGame(game_title=game['name'], game_id=str(game['uplayGameId'])) for game in sub_games_response["games"] ] return None async def get_subscription_games( self, subscription_name: str, context: Any) -> AsyncGenerator[List[SubscriptionGame], None]: yield context if SYSTEM == System.WINDOWS: async def launch_platform_client(self): if self.local_client.is_running(): log.info( "Launch platform client called but Uplay is already running" ) return url = "start uplay://" subprocess.Popen(url, shell=True) # Uplay tries to get focus a couple of times when being launched end_time = time.time() + 15 while time.time() <= end_time: await self.prevent_uplay_from_showing(kill_attempt=False) await asyncio.sleep(0.05) if SYSTEM == System.WINDOWS: async def shutdown_platform_client(self): if self.local_client.is_installed: subprocess.Popen("taskkill.exe /im \"upc.exe\"", shell=True) if SYSTEM == System.WINDOWS: async def prevent_uplay_from_showing(self, kill_attempt=True): if not self.local_client.is_installed: log.info("Local client not installed") return client_popup_wait_time = 5 check_frequency_delay = 0.02 end_time = time.time() + client_popup_wait_time hwnd = ctypes.windll.user32.FindWindowW(None, "Uplay") while not ctypes.windll.user32.IsWindowVisible(hwnd): if time.time() >= end_time: log.info("Timed out post close game uplay popup") break hwnd = ctypes.windll.user32.FindWindowW(None, "Uplay") await asyncio.sleep(check_frequency_delay) if kill_attempt: await self.shutdown_platform_client() else: ctypes.windll.user32.SetForegroundWindow(hwnd) ctypes.windll.user32.CloseWindow(hwnd) if SYSTEM == System.WINDOWS: async def prepare_game_library_settings_context(self, game_ids): if self.local_client.settings_accessible(): library_context = {} settings_data = self.local_client.read_settings() parser = LocalParser() favorite_games, hidden_games = parser.get_game_tags( settings_data) for game_id in game_ids: try: game = self.games_collection[game_id] except KeyError: continue library_context[game_id] = { 'favorite': game.launch_id in favorite_games, 'hidden': game.launch_id in hidden_games } return library_context return None async def get_game_library_settings(self, game_id, context): log.debug(f"Context {context}") if not context: # Unable to retrieve context return GameLibrarySettings(game_id, None, None) game_library_settings = context.get(game_id) if game_library_settings is None: # Able to retrieve context but game is not in its values -> It doesnt have any tags or hidden status set return GameLibrarySettings(game_id, [], False) return GameLibrarySettings( game_id, ['favorite'] if game_library_settings['favorite'] else [], game_library_settings['hidden']) def reset_tick_count(self): # Resetting tick count ensures that certain operations performed on tick will be made with a known delay. self.tick_count = 0 def tick(self): loop = asyncio.get_event_loop() if SYSTEM == System.WINDOWS: self.tick_count += 1 if self.tick_count % 1 == 0: self.refresh_game_statuses() if self.tick_count % 5 == 0: self.game_status_notifier.launcher_log_path = self.local_client.launcher_log_path if self.tick_count % 9 == 0: self._update_local_games_status() if self.local_client.ownership_changed(): if not self.updating_games: log.info( 'Ownership file has been changed or created. Reparsing.' ) loop.run_in_executor(None, self._update_games) return async def shutdown(self): log.info("Plugin shutdown.") await self.client.close()
class NintendoWiiPlugin(Plugin): def __init__(self, reader, writer, token): super().__init__(Platform.NintendoWii, __version__, reader, writer, token) self.backend_client = BackendClient() self.games = [] self.local_games_cache = self.local_games_list() async def authenticate(self, stored_credentials=None): return self.do_auth() async def pass_login_credentials(self, step, credentials, cookies): return self.do_auth() def do_auth(self): user_data = {} username = user_config.roms_path user_data["username"] = username self.store_credentials(user_data) return Authentication("dolphin_user", user_data["username"]) async def launch_game(self, game_id): emu_path = user_config.emu_path for game in self.games: if str(game[1]) == game_id: subprocess.Popen([emu_path, "--exec=" + game[0]]) break return async def install_game(self, game_id): pass async def uninstall_game(self, game_id): pass def local_games_list(self): local_games = [] for game in self.games: local_games.append( LocalGame( str(game[1]), LocalGameState.Installed ) ) return local_games def tick(self): async def update_local_games(): loop = asyncio.get_running_loop() new_local_games_list = await loop.run_in_executor(None, self.local_games_list) notify_list = self.backend_client.get_state_changes(self.local_games_cache, new_local_games_list) self.local_games_cache = new_local_games_list for local_game_notify in notify_list: self.update_local_game_status(local_game_notify) asyncio.create_task(update_local_games()) async def get_owned_games(self): self.games = self.backend_client.get_games_gb() owned_games = [] for game in self.games: owned_games.append( Game( str(game[1]), game[2], None, LicenseInfo(LicenseType.SinglePurchase, None) ) ) return owned_games async def get_local_games(self): return self.local_games_cache
class BNetPlugin(Plugin): def __init__(self, reader, writer, token): super().__init__(Platform.Battlenet, version, reader, writer, token) self.local_client = LocalClient(self._update_statuses) self.authentication_client = AuthenticatedHttpClient(self) self.backend_client = BackendClient(self, self.authentication_client) self.watched_running_games = set() def handshake_complete(self): self.create_task(self.__delayed_handshake(), 'delayed handshake') async def __delayed_handshake(self): """ Adds some minimal delay on Galaxy start before registering local data watchers. Apparently Galaxy may be not ready to receive notifications even after handshake_complete. """ await asyncio.sleep(1) self.create_task(self.local_client.register_local_data_watcher(), 'local data watcher') self.create_task(self.local_client.register_classic_games_updater(), 'classic games updater') async def _notify_about_game_stop(self, game, starting_timeout): id_to_watch = game.info.uid if id_to_watch in self.watched_running_games: log.debug(f'Game {id_to_watch} is already watched. Skipping') return try: self.watched_running_games.add(id_to_watch) await asyncio.sleep(starting_timeout) ProcessProvider().update_games_processes([game]) log.info(f'Setuping process watcher for {game._processes}') loop = asyncio.get_event_loop() await loop.run_in_executor(None, game.wait_until_game_stops) finally: self.update_local_game_status( LocalGame(id_to_watch, LocalGameState.Installed)) self.watched_running_games.remove(id_to_watch) def _update_statuses(self, refreshed_games, previous_games): for blizz_id, refr in refreshed_games.items(): prev = previous_games.get(blizz_id, None) if prev is None: if refr.has_galaxy_installed_state: log.debug('Detected playable game') state = LocalGameState.Installed else: log.debug('Detected not-fully installed game') continue elif refr.has_galaxy_installed_state and not prev.has_galaxy_installed_state: log.debug('Detected playable game') state = LocalGameState.Installed elif refr.last_played != prev.last_played: log.debug('Detected launched game') state = LocalGameState.Installed | LocalGameState.Running self.create_task(self._notify_about_game_stop(refr, 5), 'game stop waiter') else: continue log.info(f'Changing game {blizz_id} state to {state}') self.update_local_game_status(LocalGame(blizz_id, state)) for blizz_id, prev in previous_games.items(): refr = refreshed_games.get(blizz_id, None) if refr is None: log.debug('Detected uninstalled game') state = LocalGameState.None_ self.update_local_game_status(LocalGame(blizz_id, state)) def log_out(self): if self.backend_client: asyncio.create_task(self.authentication_client.shutdown()) self.authentication_client.user_details = None async def open_battlenet_browser(self): url = self.authentication_client.blizzard_battlenet_download_url log.info(f'Opening battle.net website: {url}') loop = asyncio.get_running_loop() await loop.run_in_executor( None, lambda x: webbrowser.open(x, autoraise=True), url) async def install_game(self, game_id): if not self.authentication_client.is_authenticated(): raise AuthenticationRequired() installed_game = self.local_client.get_installed_games().get( game_id, None) if installed_game and os.access(installed_game.install_path, os.F_OK): log.warning( "Received install command on an already installed game") return await self.launch_game(game_id) if game_id in [classic.uid for classic in Blizzard.CLASSIC_GAMES]: if SYSTEM == pf.WINDOWS: platform = 'windows' elif SYSTEM == pf.MACOS: platform = 'macos' webbrowser.open( f"https://www.blizzard.com/download/confirmation?platform={platform}&locale=enUS&version=LIVE&id={game_id}" ) return try: self.local_client.refresh() log.info(f'Installing game of id {game_id}') self.local_client.install_game(game_id) except ClientNotInstalledError as e: log.warning(e) await self.open_battlenet_browser() except Exception as e: log.exception(f"Installing game {game_id} failed: {e}") def _open_battlenet_at_id(self, game_id): try: self.local_client.refresh() self.local_client.open_battlenet(game_id) except Exception as e: log.exception( f"Opening battlenet client on specific game_id {game_id} failed {e}" ) try: self.local_client.open_battlenet() except Exception as e: log.exception(f"Opening battlenet client failed {e}") async def uninstall_game(self, game_id): if not self.authentication_client.is_authenticated(): raise AuthenticationRequired() if game_id == 'wow_classic': # attempting to uninstall classic wow through protocol gives you a message that the game cannot # be uninstalled through protocol and you should use battle.net return self._open_battlenet_at_id(game_id) if SYSTEM == pf.MACOS: self._open_battlenet_at_id(game_id) else: try: installed_game = self.local_client.get_installed_games().get( game_id, None) if installed_game is None or not os.access( installed_game.install_path, os.F_OK): log.error(f'Cannot uninstall {game_id}') self.update_local_game_status( LocalGame(game_id, LocalGameState.None_)) return if not isinstance(installed_game.info, ClassicGame): if self.local_client.uninstaller is None: raise FileNotFoundError('Uninstaller not found') uninstall_tag = installed_game.uninstall_tag client_lang = self.local_client.config_parser.locale_language self.local_client.uninstaller.uninstall_game( installed_game, uninstall_tag, client_lang) except Exception as e: log.exception(f'Uninstalling game {game_id} failed: {e}') async def launch_game(self, game_id): try: game = self.local_client.get_installed_games().get(game_id, None) if game is None: log.error(f'Launching game that is not installed: {game_id}') return await self.install_game(game_id) if isinstance(game.info, ClassicGame): log.info( f'Launching game of id: {game_id}, {game} at path {os.path.join(game.install_path, game.info.exe)}' ) if SYSTEM == pf.WINDOWS: subprocess.Popen( os.path.join(game.install_path, game.info.exe)) elif SYSTEM == pf.MACOS: if not game.info.bundle_id: log.warning( f"{game.name} has no bundle id, help by providing us bundle id of this game" ) subprocess.Popen(['open', '-b', game.info.bundle_id]) self.update_local_game_status( LocalGame( game_id, LocalGameState.Installed | LocalGameState.Running)) asyncio.create_task(self._notify_about_game_stop(game, 6)) return self.local_client.refresh() log.info(f'Launching game of id: {game_id}, {game}') await self.local_client.launch_game(game, wait_sec=60) self.update_local_game_status( LocalGame(game_id, LocalGameState.Installed | LocalGameState.Running)) self.local_client.close_window() asyncio.create_task(self._notify_about_game_stop(game, 3)) except ClientNotInstalledError as e: log.warning(e) await self.open_battlenet_browser() except TimeoutError as e: log.warning(str(e)) except Exception as e: log.exception(f"Launching game {game_id} failed: {e}") async def authenticate(self, stored_credentials=None): try: if stored_credentials: auth_data = self.authentication_client.process_stored_credentials( stored_credentials) try: await self.authentication_client.create_session() await self.backend_client.refresh_cookies() auth_status = await self.backend_client.validate_access_token( auth_data.access_token) except (BackendNotAvailable, BackendError, NetworkError, UnknownError, BackendTimeout) as e: raise e except Exception: raise InvalidCredentials() if self.authentication_client.validate_auth_status( auth_status): self.authentication_client.user_details = await self.backend_client.get_user_info( ) return self.authentication_client.parse_user_details() else: return self.authentication_client.authenticate_using_login() except Exception as e: raise e async def pass_login_credentials(self, step, credentials, cookies): if "logout&app=oauth" in credentials['end_uri']: # 2fa expired, repeat authentication return self.authentication_client.authenticate_using_login() if self.authentication_client.attempted_to_set_battle_tag: self.authentication_client.user_details = await self.backend_client.get_user_info( ) return self.authentication_client.parse_auth_after_setting_battletag( ) cookie_jar = self.authentication_client.parse_cookies(cookies) auth_data = await self.authentication_client.get_auth_data_login( cookie_jar, credentials) try: await self.authentication_client.create_session() await self.backend_client.refresh_cookies() except (BackendNotAvailable, BackendError, NetworkError, UnknownError, BackendTimeout) as e: raise e except Exception: raise InvalidCredentials() auth_status = await self.backend_client.validate_access_token( auth_data.access_token) if not ("authorities" in auth_status and "IS_AUTHENTICATED_FULLY" in auth_status["authorities"]): raise InvalidCredentials() self.authentication_client.user_details = await self.backend_client.get_user_info( ) self.authentication_client.set_credentials() return self.authentication_client.parse_battletag() async def get_owned_games(self): if not self.authentication_client.is_authenticated(): raise AuthenticationRequired() def _parse_battlenet_games( standard_games: dict, cn: bool) -> Dict[BlizzardGame, LicenseType]: licenses = { None: LicenseType.Unknown, "Trial": LicenseType.OtherUserLicense, "Good": LicenseType.SinglePurchase, "Inactive": LicenseType.SinglePurchase, "Banned": LicenseType.SinglePurchase, "Free": LicenseType.FreeToPlay } games = {} for standard_game in standard_games["gameAccounts"]: title_id = standard_game['titleId'] try: game = Blizzard.game_by_title_id(title_id, cn) except KeyError: log.warning( f"Skipping unknown game with titleId: {title_id}") else: games[game] = licenses[standard_game.get( "gameAccountStatus")] # Add wow classic if retail wow is present in owned games wow_license = games.get(Blizzard['wow']) if wow_license is not None: games[Blizzard['wow_classic']] = wow_license return games def _parse_classic_games( classic_games: dict) -> Dict[ClassicGame, LicenseType]: games = {} for classic_game in classic_games["classicGames"]: sanitized_name = classic_game["localizedGameName"].replace( u'\xa0', ' ') for cg in Blizzard.CLASSIC_GAMES: if cg.name == sanitized_name: games[cg] = LicenseType.SinglePurchase break else: log.warning( f"Skipping unknown classic game with name: {sanitized_name}" ) return games cn = self.authentication_client.region == 'cn' battlenet_games = _parse_battlenet_games( await self.backend_client.get_owned_games(), cn) classic_games = _parse_classic_games( await self.backend_client.get_owned_classic_games()) owned_games: Dict[BlizzardGame, LicenseType] = { **battlenet_games, **classic_games } for game in Blizzard.try_for_free_games(cn): if game not in owned_games: owned_games[game] = LicenseType.FreeToPlay return [ Game(game.uid, game.name, None, LicenseInfo(license_type)) for game, license_type in owned_games.items() ] async def get_local_games(self): timeout = time.time() + 2 try: translated_installed_games = [] while not self.local_client.games_finished_parsing(): await asyncio.sleep(0.1) if time.time() >= timeout: break running_games = self.local_client.get_running_games() installed_games = self.local_client.get_installed_games() log.info(f"Installed games {installed_games.items()}") log.info(f"Running games {running_games}") for uid, game in installed_games.items(): if game.has_galaxy_installed_state: state = LocalGameState.Installed if uid in running_games: state |= LocalGameState.Running translated_installed_games.append(LocalGame(uid, state)) self.local_client.installed_games_cache = installed_games return translated_installed_games except Exception as e: log.exception(f"failed to get local games: {str(e)}") raise async def get_game_time(self, game_id, context): total_time = None last_played_time = None blizzard_game = Blizzard[game_id] if blizzard_game.name == "Overwatch": total_time = await self._get_overwatch_time() log.debug(f"Gametime for Overwatch is {total_time} minutes.") for config_info in self.local_client.config_parser.games: if config_info.uid == blizzard_game.uid: if config_info.last_played is not None: last_played_time = int(config_info.last_played) break return GameTime(game_id, total_time, last_played_time) async def _get_overwatch_time(self) -> Union[None, int]: log.debug("Fetching playtime for Overwatch...") player_data = await self.backend_client.get_ow_player_data() if 'message' in player_data: # user not found log.error('No Overwatch profile found.') return None if player_data['private'] == True: log.info('Unable to get data as Overwatch profile is private.') return None qp_time = player_data['playtime'].get('quickplay') if qp_time is None: # user has not played quick play return 0 if qp_time.count(':') == 1: # minutes and seconds match = re.search('(?:(?P<m>\\d+):)(?P<s>\\d+)', qp_time) if match: return int(match.group('m')) elif qp_time.count(':') == 2: # hours, minutes and seconds match = re.search('(?:(?P<h>\\d+):)(?P<m>\\d+)', qp_time) if match: return int(match.group('h')) * 60 + int(match.group('m')) raise UnknownBackendResponse( f'Unknown Overwatch API playtime format: {qp_time}') async def _get_wow_achievements(self): achievements = [] try: characters_data = await self.backend_client.get_wow_character_data( ) characters_data = characters_data["characters"] wow_character_data = await asyncio.gather( *[ self.backend_client.get_wow_character_achievements( character["realm"], character["name"]) for character in characters_data ], return_exceptions=True, ) for data in wow_character_data: if isinstance(data, requests.Timeout) or isinstance( data, requests.ConnectionError): raise data wow_achievement_data = [ list( zip( data["achievements"]["achievementsCompleted"], data["achievements"]["achievementsCompletedTimestamp"], )) for data in wow_character_data if type(data) is dict ] already_in = set() for char_ach in wow_achievement_data: for ach in char_ach: if ach[0] not in already_in: achievements.append( Achievement(achievement_id=ach[0], unlock_time=int(ach[1] / 1000))) already_in.add(ach[0]) except (AccessTokenExpired, BackendError) as e: log.exception(str(e)) with open('wow.json', 'w') as f: f.write(json.dumps(achievements, cls=DataclassJSONEncoder)) return achievements async def _get_sc2_achievements(self): account_data = await self.backend_client.get_sc2_player_data( self.authentication_client.user_details["id"]) # TODO what if more sc2 accounts? assert len(account_data) == 1 account_data = account_data[0] profile_data = await self.backend_client.get_sc2_profile_data( account_data["regionId"], account_data["realmId"], account_data["profileId"]) sc2_achievement_data = [ Achievement(achievement_id=achievement["achievementId"], unlock_time=achievement["completionDate"]) for achievement in profile_data["earnedAchievements"] if achievement["isComplete"] ] with open('sc2.json', 'w') as f: f.write(json.dumps(sc2_achievement_data, cls=DataclassJSONEncoder)) return sc2_achievement_data # async def get_unlocked_achievements(self, game_id): # if not self.website_client.is_authenticated(): # raise AuthenticationRequired() # try: # if game_id == "21298": # return await self._get_sc2_achievements() # elif game_id == "5730135": # return await self._get_wow_achievements() # else: # return [] # except requests.Timeout: # raise BackendTimeout() # except requests.ConnectionError: # raise NetworkError() # except Exception as e: # log.exception(str(e)) # return [] async def launch_platform_client(self): if self.local_client.is_running(): log.info( "Launch platform client called but client is already running") return self.local_client.open_battlenet() await self.local_client.prevent_battlenet_from_showing() async def shutdown_platform_client(self): await self.local_client.shutdown_platform_client() async def shutdown(self): log.info("Plugin shutdown.") await self.authentication_client.shutdown()
class DolphinPlugin(Plugin): def __init__(self, reader, writer, token): super().__init__(Platform.NintendoGameCube, __version__, reader, writer, token) self.backend_client = BackendClient() self.games = [] self.game_times = get_the_game_times() self.local_games_cache = self.local_games_list() async def authenticate(self, stored_credentials=None): return self.do_auth() async def pass_login_credentials(self, step, credentials, cookies): return self.do_auth() def do_auth(self): user_data = {} username = user_config.roms_path user_data["username"] = username self.store_credentials(user_data) return Authentication("Dolphin", user_data["username"]) async def launch_game(self, game_id): emu_path = user_config.emu_path for game in self.games: if str(game[1]) == game_id: if user_config.retroarch is not True: subprocess.Popen([emu_path, "-b", "-e", game[0]]) subprocess.Popen([ os.path.dirname(os.path.realpath(__file__)) + r'\TimeTracker.exe', game_id, game_id ]) else: subprocess.Popen([ user_config.retroarch_executable, "-L", user_config.core_path + r'\dolphin_libretro.dll', game[0] ]) break return async def install_game(self, game_id): pass async def uninstall_game(self, game_id): pass async def get_game_time(self, game_id, context=None): self.game_times = get_the_game_times() game_times = self.game_times game_time = int(game_times[game_id][0]) game_time /= 60 return GameTime(game_id, game_time, game_times[game_id][1]) def local_games_list(self): local_games = [] for game in self.games: local_games.append( LocalGame(str(game[1]), LocalGameState.Installed)) return local_games def tick(self): async def update_local_games(): loop = asyncio.get_running_loop() new_local_games_list = await loop.run_in_executor( None, self.local_games_list) notify_list = self.backend_client.get_state_changes( self.local_games_cache, new_local_games_list) self.local_games_cache = new_local_games_list for local_game_notify in notify_list: self.update_local_game_status(local_game_notify) asyncio.create_task(update_local_games()) async def get_owned_games(self): self.games = self.backend_client.get_games_db() owned_games = [] for game in self.games: owned_games.append( Game(str(game[1]), game[2], None, LicenseInfo(LicenseType.SinglePurchase, None))) return owned_games async def get_local_games(self): return self.local_games_cache def shutdown(self): pass
def __init__(self, reader, writer, token): super().__init__(Platform.PlayStation2, __version__, reader, writer, token) self.backend_client = BackendClient() self.games = [] self.local_games_cache = self.local_games_list()
class UplayPlugin(Plugin): def __init__(self, reader, writer, token): super().__init__(Platform.Uplay, __version__, reader, writer, token) self.client = BackendClient(self) self.local_client = LocalClient() self.cached_game_statuses = {} self.games_collection = GamesCollection() self.process_watcher = ProcessWatcher() self.game_status_notifier = GameStatusNotifier(self.process_watcher) self.tick_count = 0 self.updating_games = False self.owned_games_sent = False self.parsing_club_games = False def auth_lost(self): self.lost_authentication() async def authenticate(self, stored_credentials=None): if not stored_credentials: return NextStep("web_session", AUTH_PARAMS, cookies=COOKIES) else: try: user_data = await self.client.authorise_with_stored_credentials( stored_credentials) except (AccessDenied, AuthenticationRequired) as e: log.exception(repr(e)) raise InvalidCredentials() except Exception as e: log.exception(repr(e)) raise e else: self.local_client.initialize(user_data['userId']) self.client.set_auth_lost_callback(self.auth_lost) return Authentication(user_data['userId'], user_data['username']) async def pass_login_credentials(self, step, credentials, cookies): """Called just after CEF authentication (called as NextStep by authenticate)""" user_data = await self.client.authorise_with_cookies(cookies) self.local_client.initialize(user_data['userId']) self.client.set_auth_lost_callback(self.auth_lost) return Authentication(user_data['userId'], user_data['username']) async def get_owned_games(self): if not self.client.is_authenticated(): raise AuthenticationRequired() self._parse_local_games() self._parse_local_game_ownership() await self._parse_club_games() self.owned_games_sent = True for game in self.games_collection: game.considered_for_sending = True return [ game.as_galaxy_game() for game in self.games_collection if game.owned ] async def _parse_club_games(self): if not self.parsing_club_games: try: self.parsing_club_games = True games = await self.client.get_club_titles() club_games = [] for game in games: if "platform" in game: if game["platform"] == "PC": log.info( f"Parsed game from Club Request {game['title']}" ) club_games.append( UbisoftGame(space_id=game['spaceId'], launch_id='', third_party_id='', name=game['title'], path='', type=GameType.New, special_registry_path='', exe='', status=GameStatus.Unknown, owned=True)) self.games_collection.append(club_games) except ApplicationError as e: log.error( f"Encountered exception while parsing club games {repr(e)}" ) raise e except Exception as e: log.error( f"Encountered exception while parsing club games {repr(e)}" ) finally: self.parsing_club_games = False else: # Wait until club games get parsed if parsing is already in progress while self.parsing_club_games: await asyncio.sleep(0.2) def _parse_local_games(self): """Parsing local files should lead to every game having a launch id. A game in the games_collection which doesn't have a launch id probably means that a game was added through the get_club_titles request but its space id was not present in configuration file and we couldn't find a matching launch id for it.""" if self.local_client.configurations_accessible(): configuration_data = self.local_client.read_config() p = LocalParser() games = [] for game in p.parse_games(configuration_data): games.append(game) self.games_collection.append(games) def _parse_local_game_ownership(self): if self.local_client.ownership_accesible(): ownership_data = self.local_client.read_ownership() p = LocalParser() ownership_records = p.get_owned_local_games(ownership_data) log.info(f" Ownership Records {ownership_records}") for game in self.games_collection: if game.launch_id: if int(game.launch_id) in ownership_records: game.owned = True def _update_games(self): self.updating_games = True self._parse_local_games() self.updating_games = False def _update_local_games_status(self): cached_statuses = self.cached_game_statuses if cached_statuses is None: return for game in self.games_collection: if game.launch_id in cached_statuses: self.game_status_notifier.update_game(game) if game.status != cached_statuses[game.launch_id]: log.info( f"Game {game.name} path changed: updating status from {cached_statuses[game.launch_id]} to {game.status}" ) self.update_local_game_status(game.as_local_game()) self.cached_game_statuses[game.launch_id] = game.status else: self.game_status_notifier.update_game(game) ''' If a game wasn't previously in a cache then and it appears with an installed or running status it most likely means that client was just installed ''' if game.status in [GameStatus.Installed, GameStatus.Running]: self.update_local_game_status(game.as_local_game()) self.cached_game_statuses[game.launch_id] = game.status async def get_local_games(self): self._parse_local_games() local_games = [] for game in self.games_collection: self.cached_game_statuses[game.launch_id] = game.status if game.status == GameStatus.Installed or game.status == GameStatus.Running: local_games.append(game.as_local_game()) self._update_local_games_status() return local_games async def _add_new_games(self, games): await self._parse_club_games() self._parse_local_game_ownership() for game in games: if game.owned: self.add_game(game.as_galaxy_game()) async def get_game_times(self): if not self.client.is_authenticated(): raise AuthenticationRequired() game_times = [] games_with_space = [ game for game in self.games_collection if game.space_id ] try: tasks = [ self.client.get_game_stats(game.space_id) for game in games_with_space ] stats = await asyncio.gather(*tasks) for st, game in zip(stats, games_with_space): statscards = st.get('Statscards', None) if statscards is None: continue playtime, last_played = find_playtime(statscards, default_total_time=0, default_last_played=0) log.info( f'Stats for {game.name}: playtime: {playtime}, last_played: {last_played}' ) if playtime is not None and last_played is not None: game_times.append( GameTime(game.space_id, playtime, last_played)) except ApplicationError as e: log.exception("Game times:" + repr(e)) raise e except Exception as e: log.exception("Game times:" + repr(e)) finally: return game_times async def get_unlocked_challenges(self, game_id): """Challenges are a unique uplay club feature and don't directly translate to achievements""" if not self.client.is_authenticated(): raise AuthenticationRequired() for game in self.games_collection: if game.space_id == game_id or game.launch_id == game_id: if not game.space_id: return [] challenges = await self.client.get_challenges(game.space_id) return [ Achievement(achievement_id=challenge["id"], achievement_name=challenge["name"], unlock_time=int( datetime.datetime.timestamp( dateutil.parser.parse( challenge["completionDate"])))) for challenge in challenges["actions"] if challenge["isCompleted"] and not challenge["isBadge"] ] async def launch_game(self, game_id): if not self.user_can_perform_actions(): return for game in self.games_collection.get_local_games(): if (game.space_id == game_id or game.launch_id == game_id) and game.status == GameStatus.Installed: if game.type == GameType.Steam: if is_steam_installed(): url = f"start steam://rungameid/{game.third_party_id}" else: url = f"start uplay://open/game/{game.launch_id}" elif game.type == GameType.New or game.type == GameType.Legacy: url = f"start uplay://launch/{game.launch_id}" else: log.error(f"Unsupported game type {game.name}") self.open_uplay_client() return log.info(f"Launching game '{game.name}' by protocol: [{url}]") subprocess.Popen(url, shell=True) return log.info("Failed to launch game, launching client instead.") self.open_uplay_client() async def install_game(self, game_id): if not self.user_can_perform_actions(): return for game in self.games_collection: if (game.space_id == game_id or game.launch_id == game_id) and game.status in [ GameStatus.NotInstalled, GameStatus.Unknown ]: if game.launch_id: log.info( f"Found game with game_id: {game_id}, {game.launch_id}" ) subprocess.Popen(f"start uplay://install/{game.launch_id}", shell=True) return # if launch_id is not known, try to launch local client instead self.open_uplay_client() log.info( f"Did not found game with game_id: {game_id}, proper launch_id and NotInstalled status, launching client." ) async def uninstall_game(self, game_id): if not self.user_can_perform_actions(): return for game in self.games_collection.get_local_games(): if (game.space_id == game_id or game.launch_id == game_id) and game.status == GameStatus.Installed: subprocess.Popen(f"start uplay://uninstall/{game.launch_id}", shell=True) return self.open_uplay_client() log.info( f"Did not found game with game_id: {game_id}, proper launch_id and Installed status, launching client." ) def user_can_perform_actions(self): if not self.local_client.is_installed: self.open_uplay_browser() return False if not self.local_client.was_user_logged_in: self.open_uplay_client() return False return True def open_uplay_client(self): subprocess.Popen(f"start uplay://", shell=True) def open_uplay_browser(self): url = f'https://uplay.ubisoft.com' log.info(f"Opening uplay website: {url}") webbrowser.open(url, autoraise=True) def refresh_game_statuses(self): if not self.local_client.was_user_logged_in: return statuses = self.game_status_notifier.statuses new_games = [] for game in self.games_collection: if game.launch_id in statuses: if statuses[ game. launch_id] == GameStatus.Installed and game.status != GameStatus.Installed: log.info(f"updating status for {game.name} to installed") game.status = GameStatus.Installed self.update_local_game_status(game.as_local_game()) elif statuses[ game. launch_id] == GameStatus.Running and game.status != GameStatus.Running: log.info(f"updating status for {game.name} to running") game.status = GameStatus.Running self.update_local_game_status(game.as_local_game()) elif statuses[game.launch_id] in [ GameStatus.NotInstalled, GameStatus.Unknown ] and game.status not in [ GameStatus.NotInstalled, GameStatus.Unknown ]: log.info( f"updating status for {game.name} to not installed") game.status = GameStatus.NotInstalled self.update_local_game_status(game.as_local_game()) if self.owned_games_sent and not game.considered_for_sending: game.considered_for_sending = True new_games.append(game) if new_games: asyncio.create_task(self._add_new_games(new_games)) async def get_friends(self): friends = await self.client.get_friends() return [ FriendInfo(user_id=friend["pid"], user_name=friend["nameOnPlatform"]) for friend in friends["friends"] ] def tick(self): loop = asyncio.get_event_loop() if SYSTEM == System.WINDOWS: self.tick_count += 1 if self.tick_count % 1 == 0: self.refresh_game_statuses() if self.tick_count % 5 == 0: self.game_status_notifier.launcher_log_path = self.local_client.launcher_log_path if self.tick_count % 9 == 0: self._update_local_games_status() if self.local_client.ownership_changed(): if not self.updating_games: log.info( 'Ownership file has been changed or created. Reparsing.' ) loop.run_in_executor(None, self._update_games) return def shutdown(self): log.info("Plugin shutdown.") asyncio.create_task(self.client.close())
class BNetPlugin(Plugin): def __init__(self, reader, writer, token): super().__init__(Platform.Battlenet, version, reader, writer, token) self.local_client = LocalClient(self._update_statuses) self.authentication_client = AuthenticatedHttpClient(self) self.backend_client = BackendClient(self, self.authentication_client) self.owned_games_cache = [] self.watched_running_games = set() self.local_games_called = False async def _notify_about_game_stop(self, game, starting_timeout): if not self.local_games_called: return id_to_watch = game.info.id if id_to_watch in self.watched_running_games: log.debug(f'Game {id_to_watch} is already watched. Skipping') return try: self.watched_running_games.add(id_to_watch) await asyncio.sleep(starting_timeout) ProcessProvider().update_games_processes([game]) log.info(f'Setuping process watcher for {game._processes}') loop = asyncio.get_event_loop() await loop.run_in_executor(None, game.wait_until_game_stops) finally: self.update_local_game_status( LocalGame(id_to_watch, LocalGameState.Installed)) self.watched_running_games.remove(id_to_watch) def _update_statuses(self, refreshed_games, previous_games): if not self.local_games_called: return for blizz_id, refr in refreshed_games.items(): prev = previous_games.get(blizz_id, None) if prev is None: if refr.playable: log.debug('Detected playable game') state = LocalGameState.Installed else: log.debug('Detected installation begin') state = LocalGameState.None_ elif refr.playable and not prev.playable: log.debug('Detected playable game') state = LocalGameState.Installed elif refr.last_played != prev.last_played: log.debug('Detected launched game') state = LocalGameState.Installed | LocalGameState.Running asyncio.create_task(self._notify_about_game_stop(refr, 5)) else: continue log.info(f'Changing game {blizz_id} state to {state}') self.update_local_game_status(LocalGame(blizz_id, state)) for blizz_id, prev in previous_games.items(): refr = refreshed_games.get(blizz_id, None) if refr is None: log.debug('Detected uninstalled game') state = LocalGameState.None_ self.update_local_game_status(LocalGame(blizz_id, state)) def log_out(self): if self.backend_client: asyncio.create_task(self.authentication_client.shutdown()) self.authentication_client.user_details = None self.owned_games_cache = [] async def open_battlenet_browser(self): url = self.authentication_client.blizzard_battlenet_download_url log.info(f'Opening battle.net website: {url}') loop = asyncio.get_running_loop() await loop.run_in_executor( None, lambda x: webbrowser.open(x, autoraise=True), url) async def install_game(self, game_id): if not self.authentication_client.is_authenticated(): raise AuthenticationRequired() installed_game = self.local_client.get_installed_games().get( game_id, None) if installed_game and os.access(installed_game.install_path, os.F_OK): log.warning( "Received install command on an already installed game") return await self.launch_game(game_id) if game_id in Blizzard.legacy_game_ids: if SYSTEM == pf.WINDOWS: platform = 'windows' elif SYSTEM == pf.MACOS: platform = 'macos' webbrowser.open( f"https://www.blizzard.com/download/confirmation?platform={platform}&locale=enUS&version=LIVE&id={game_id}" ) return try: self.local_client.refresh() log.info(f'Installing game of id {game_id}') self.local_client.install_game(game_id) except ClientNotInstalledError as e: log.warning(e) await self.open_battlenet_browser() except Exception as e: log.exception(f"Installing game {game_id} failed: {e}") def _open_battlenet_at_id(self, game_id): try: self.local_client.refresh() self.local_client.open_battlenet(game_id) except Exception as e: log.exception( f"Opening battlenet client on specific game_id {game_id} failed {e}" ) try: self.local_client.open_battlenet() except Exception as e: log.exception(f"Opening battlenet client failed {e}") async def uninstall_game(self, game_id): if not self.authentication_client.is_authenticated(): raise AuthenticationRequired() if game_id == 'wow_classic': # attempting to uninstall classic wow through protocol gives you a message that the game cannot # be uninstalled through protocol and you should use battle.net return self._open_battlenet_at_id(game_id) if SYSTEM == pf.MACOS: self._open_battlenet_at_id(game_id) else: try: installed_game = self.local_client.get_installed_games().get( game_id, None) if installed_game is None or not os.access( installed_game.install_path, os.F_OK): log.error(f'Cannot uninstall {Blizzard[game_id].uid}') self.update_local_game_status( LocalGame(game_id, LocalGameState.None_)) return if not isinstance(installed_game.info, ClassicGame): if self.local_client.uninstaller is None: raise FileNotFoundError('Uninstaller not found') uninstall_tag = installed_game.uninstall_tag client_lang = self.local_client.config_parser.locale_language self.local_client.uninstaller.uninstall_game( installed_game, uninstall_tag, client_lang) except Exception as e: log.exception(f'Uninstalling game {game_id} failed: {e}') async def launch_game(self, game_id): if not self.local_games_called: await self.get_local_games() try: if self.local_client.get_installed_games() is None: log.error(f'Launching game that is not installed: {game_id}') return await self.install_game(game_id) game = self.local_client.get_installed_games().get(game_id, None) if game is None: log.error(f'Launching game that is not installed: {game_id}') return await self.install_game(game_id) if isinstance(game.info, ClassicGame): log.info( f'Launching game of id: {game_id}, {game} at path {os.path.join(game.install_path, game.info.exe)}' ) if SYSTEM == pf.WINDOWS: subprocess.Popen( os.path.join(game.install_path, game.info.exe)) elif SYSTEM == pf.MACOS: if not game.info.bundle_id: log.warning( f"{game.name} has no bundle id, help by providing us bundle id of this game" ) subprocess.Popen(['open', '-b', game.info.bundle_id]) self.update_local_game_status( LocalGame( game_id, LocalGameState.Installed | LocalGameState.Running)) asyncio.create_task(self._notify_about_game_stop(game, 6)) return self.local_client.refresh() log.info(f'Launching game of id: {game_id}, {game}') await self.local_client.launch_game(game, wait_sec=60) self.update_local_game_status( LocalGame(game_id, LocalGameState.Installed | LocalGameState.Running)) self.local_client.close_window() asyncio.create_task(self._notify_about_game_stop(game, 3)) except ClientNotInstalledError as e: log.warning(e) await self.open_battlenet_browser() except TimeoutError as e: log.warning(str(e)) except Exception as e: log.exception(f"Launching game {game_id} failed: {e}") async def authenticate(self, stored_credentials=None): try: if stored_credentials: auth_data = self.authentication_client.process_stored_credentials( stored_credentials) try: await self.authentication_client.create_session() await self.backend_client.refresh_cookies() auth_status = await self.backend_client.validate_access_token( auth_data.access_token) except (BackendNotAvailable, BackendError, NetworkError, UnknownError, BackendTimeout) as e: raise e except Exception: raise InvalidCredentials() if self.authentication_client.validate_auth_status( auth_status): self.authentication_client.user_details = await self.backend_client.get_user_info( ) return self.authentication_client.parse_user_details() else: return self.authentication_client.authenticate_using_login() except Exception as e: raise e async def pass_login_credentials(self, step, credentials, cookies): if "logout&app=oauth" in credentials['end_uri']: # 2fa expired, repeat authentication return self.authentication_client.authenticate_using_login() if self.authentication_client.attempted_to_set_battle_tag: self.authentication_client.user_details = await self.backend_client.get_user_info( ) return self.authentication_client.parse_auth_after_setting_battletag( ) cookie_jar = self.authentication_client.parse_cookies(cookies) auth_data = await self.authentication_client.get_auth_data_login( cookie_jar, credentials) try: await self.authentication_client.create_session() await self.backend_client.refresh_cookies() except (BackendNotAvailable, BackendError, NetworkError, UnknownError, BackendTimeout) as e: raise e except Exception: raise InvalidCredentials() auth_status = await self.backend_client.validate_access_token( auth_data.access_token) if not ("authorities" in auth_status and "IS_AUTHENTICATED_FULLY" in auth_status["authorities"]): raise InvalidCredentials() self.authentication_client.user_details = await self.backend_client.get_user_info( ) self.authentication_client.set_credentials() return self.authentication_client.parse_battletag() async def get_owned_games(self): if not self.authentication_client.is_authenticated(): raise AuthenticationRequired() def _parse_classic_games(classic_games): for classic_game in classic_games["classicGames"]: log.info(f"looking for {classic_game} in classic games") try: blizzard_game = Blizzard[ classic_game["localizedGameName"].replace( u'\xa0', ' ')] log.info(f"match! {blizzard_game}") classic_game["titleId"] = blizzard_game.uid classic_game["gameAccountStatus"] = "Good" except KeyError: continue return classic_games def _get_not_added_free_games(owned_games): owned_games_ids = [] for game in owned_games: if "titleId" in game: owned_games_ids.append(str(game["titleId"])) return [{ "titleId": game.blizzard_id, "localizedGameName": game.name, "gameAccountStatus": "Free" } for game in Blizzard.free_games if game.blizzard_id not in owned_games_ids] try: games = await self.backend_client.get_owned_games() classic_games = _parse_classic_games( await self.backend_client.get_owned_classic_games()) owned_games = games["gameAccounts"] + classic_games["classicGames"] # Add wow classic if retail wow is present in owned games for owned_game in owned_games.copy(): if 'titleId' in owned_game: if owned_game['titleId'] == 5730135: owned_games.append({ 'titleId': 'wow_classic', 'localizedGameName': 'World of Warcraft Classic', 'gameAccountStatus': owned_game['gameAccountStatus'] }) free_games_to_add = _get_not_added_free_games(owned_games) owned_games += free_games_to_add self.owned_games_cache = owned_games return [ Game( str(game["titleId"]), game["localizedGameName"], [], LicenseInfo(License_Map[game["gameAccountStatus"]]), ) for game in self.owned_games_cache if "titleId" in game ] except Exception as e: log.exception(f"failed to get owned games: {repr(e)}") raise async def get_local_games(self): timeout = time.time() + 2 try: translated_installed_games = [] while not self.local_client.games_finished_parsing(): await asyncio.sleep(0.1) if time.time() >= timeout: break running_games = self.local_client.get_running_games() installed_games = self.local_client.get_installed_games() log.info(f"Installed games {installed_games.items()}") log.info(f"Running games {running_games}") for id_, game in installed_games.items(): if game.playable: state = LocalGameState.Installed if id_ in running_games: state |= LocalGameState.Running else: state = LocalGameState.None_ translated_installed_games.append(LocalGame(id_, state)) self.local_client.installed_games_cache = installed_games return translated_installed_games except Exception as e: log.exception(f"failed to get local games: {str(e)}") raise finally: self.local_games_called = True async def get_game_time(self, game_id, context): game_time_minutes = None if game_id == "5272175": game_time_minutes = await self._get_overwatch_time() log.debug( f"Gametime for Overwatch is {game_time_minutes} minutes.") return GameTime(game_id, game_time_minutes, None) async def _get_overwatch_time(self) -> Union[None, int]: log.debug("Fetching playtime for Overwatch...") player_data = await self.backend_client.get_ow_player_data() if 'message' in player_data: # user not found... unfortunately no 404 status code is returned :/ log.error('No Overwatch profile found.') return None if player_data['private'] == True: log.info('Unable to get data as Overwatch profile is private.') return None qp_time = player_data['playtime']['quickplay'] if qp_time is None: # user has not played quick play return 0 if qp_time.count(':') == 1: # minutes and seconds match = re.search('(?:(?P<m>\\d+):)(?P<s>\\d+)', qp_time) if match: return int(match.group('m')) elif qp_time.count(':') == 2: # hours, minutes and seconds match = re.search('(?:(?P<h>\\d+):)(?P<m>\\d+)', qp_time) if match: return int(match.group('h')) * 60 + int(match.group('m')) raise UnknownBackendResponse( f'Unknown Overwatch API playtime format: {qp_time}') async def _get_wow_achievements(self): achievements = [] try: characters_data = await self.backend_client.get_wow_character_data( ) characters_data = characters_data["characters"] wow_character_data = await asyncio.gather( *[ self.backend_client.get_wow_character_achievements( character["realm"], character["name"]) for character in characters_data ], return_exceptions=True, ) for data in wow_character_data: if isinstance(data, requests.Timeout) or isinstance( data, requests.ConnectionError): raise data wow_achievement_data = [ list( zip( data["achievements"]["achievementsCompleted"], data["achievements"]["achievementsCompletedTimestamp"], )) for data in wow_character_data if type(data) is dict ] already_in = set() for char_ach in wow_achievement_data: for ach in char_ach: if ach[0] not in already_in: achievements.append( Achievement(achievement_id=ach[0], unlock_time=int(ach[1] / 1000))) already_in.add(ach[0]) except (AccessTokenExpired, BackendError) as e: log.exception(str(e)) with open('wow.json', 'w') as f: f.write(json.dumps(achievements, cls=DataclassJSONEncoder)) return achievements async def _get_sc2_achievements(self): account_data = await self.backend_client.get_sc2_player_data( self.authentication_client.user_details["id"]) # TODO what if more sc2 accounts? assert len(account_data) == 1 account_data = account_data[0] profile_data = await self.backend_client.get_sc2_profile_data( account_data["regionId"], account_data["realmId"], account_data["profileId"]) sc2_achievement_data = [ Achievement(achievement_id=achievement["achievementId"], unlock_time=achievement["completionDate"]) for achievement in profile_data["earnedAchievements"] if achievement["isComplete"] ] with open('sc2.json', 'w') as f: f.write(json.dumps(sc2_achievement_data, cls=DataclassJSONEncoder)) return sc2_achievement_data # async def get_unlocked_achievements(self, game_id): # if not self.website_client.is_authenticated(): # raise AuthenticationRequired() # try: # if game_id == "21298": # return await self._get_sc2_achievements() # elif game_id == "5730135": # return await self._get_wow_achievements() # else: # return [] # except requests.Timeout: # raise BackendTimeout() # except requests.ConnectionError: # raise NetworkError() # except Exception as e: # log.exception(str(e)) # return [] async def launch_platform_client(self): if self.local_client.is_running(): log.info( "Launch platform client called but client is already running") return self.local_client.open_battlenet() await self.local_client.prevent_battlenet_from_showing() async def shutdown_platform_client(self): await self.local_client.shutdown_platform_client() async def shutdown(self): log.info("Plugin shutdown.") await self.authentication_client.shutdown()
class SuperNintendoEntertainmentSystemPlugin(Plugin): def __init__(self, reader, writer, token): super().__init__(Platform.SuperNintendoEntertainmentSystem, __version__, reader, writer, token) self.backend_client = BackendClient(self) self.games = [] self.local_games_cache = [] self.proc = None self.running_game_id = "" self.tick_count = 0 self.create_task(self._update_local_games(), "Update local games") async def authenticate(self, stored_credentials=None): return self._do_auth() async def pass_login_credentials(self, step, credentials, cookies): return self._do_auth() def _do_auth(self) -> Authentication: user_data = {} username = user_config.roms_path user_data["username"] = username self.store_credentials(user_data) return Authentication("bsnes_user", user_data["username"]) async def launch_game(self, game_id): self.running_game_id = game_id emu_path = user_config.emu_path fullscreen = user_config.emu_fullscreen self._launch_game(game_id, emu_path, fullscreen) self.backend_client._set_session_start() def _launch_game(self, game_id, emu_path, fullscreen) -> None: ''' Returns None Interprets user configurated options and launches Bsnes with the chosen rom ''' for game in self.games: if game.id == game_id: args = [emu_path] if fullscreen: args.append("--fullscreen") args.append(game.path) self.proc = subprocess.Popen(args) break # Only as placeholders so the launch game feature is recognized async def install_game(self, game_id): pass async def uninstall_game(self, game_id): pass async def prepare_game_times_context(self, game_ids): return self._get_games_times_dict() async def get_game_time(self, game_id, context): game_time = context.get(game_id) return game_time def _get_games_times_dict(self) -> dict: ''' Returns a dict of GameTime objects Creates and reads the game_times.json file ''' base_dir = os.path.dirname(os.path.realpath(__file__)) data = {} game_times = {} path = "{}/game_times.json".format(base_dir) # Check if the file exists, otherwise create it with defaults if not os.path.exists(path): for game in self.games: data[game.id] = { "name" : game.name, "time_played" : 0, "last_time_played" : None } with open(path, "w", encoding="utf-8") as game_times_file: json.dump(data, game_times_file, indent=4) # Now read it and return the game times with open(path, encoding="utf-8") as game_times_file: parsed_game_times_file = json.load(game_times_file) for entry in parsed_game_times_file: game_id = entry time_played = parsed_game_times_file.get(entry).get("time_played") last_time_played = parsed_game_times_file.get(entry).get("last_time_played") game_times[game_id] = GameTime(game_id, time_played, last_time_played) return game_times def _local_games_list(self) -> list: ''' Returns a list of LocalGame objects Goes through retrieved games and adds them as local games with default state of "Installed" ''' local_games = [] for game in self.games: local_games.append( LocalGame( game.id, LocalGameState.Installed ) ) return local_games def tick(self): self.tick_count += 1 self._check_proc_status() self.create_task(self._update_local_games(), "Update local games") if self.tick_count % 3 == 0: self.create_task(self._update_all_game_times(), "Update all game times") def _check_proc_status(self) -> None: try: if(self.proc.poll() is not None): self.backend_client._set_session_end() session_duration = self.backend_client._get_session_duration() last_time_played = int(time.time()) self._update_game_time(self.running_game_id, session_duration, last_time_played) self.proc = None except AttributeError: pass async def _update_local_games(self) -> None: loop = asyncio.get_running_loop() new_list = await loop.run_in_executor(None, self._local_games_list) notify_list = self.backend_client._get_state_changes(self.local_games_cache, new_list) self.local_games_cache = new_list for local_game_notify in notify_list: self.update_local_game_status(local_game_notify) async def _update_all_game_times(self) -> None: await asyncio.sleep(60) # Leave time for Galaxy to fetch games before updating times loop = asyncio.get_running_loop() new_game_times = await loop.run_in_executor(None, self._get_games_times_dict) for game_time in new_game_times: self.update_game_time(new_game_times[game_time]) def _update_game_time(self, game_id, session_duration, last_time_played) -> None: ''' Returns None Update the game time of a single game ''' base_dir = os.path.dirname(os.path.realpath(__file__)) game_times_path = "{}/game_times.json".format(base_dir) with open(game_times_path, encoding="utf-8") as game_times_file: data = json.load(game_times_file) data[game_id]["time_played"] = data.get(game_id).get("time_played") + session_duration data[game_id]["last_time_played"] = last_time_played with open(game_times_path, "w", encoding="utf-8") as game_times_file: json.dump(data, game_times_file, indent=4) self.update_game_time(GameTime(game_id, data.get(game_id).get("time_played"), last_time_played)) async def get_owned_games(self): owned_games = [] self.games = self.backend_client._get_games_giant_bomb() for game in self.games: owned_games.append( Game( game.id, game.name, None, LicenseInfo(LicenseType.SinglePurchase, None) ) ) return owned_games async def get_local_games(self): return self.local_games_cache