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
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 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())