def __init__(self, reader, writer, token):
     super().__init__(Platform.Minecraft, __version__, reader, writer,
                      token)
     self.local_client = LocalClient()
     self.minecraft_launcher = None
     self.minecraft_uninstall_command = None
     self.minecraft_installation_status = LocalGameState.None_
     self.minecraft_running_check = None
     self.tick_count = 0
 def __init__(self, reader, writer, token):
     super().__init__(
         Platform.RiotGames,  # choose platform from available list
         __version__,  # version
         reader,
         writer,
         token,
     )
     self.local_client = LocalClient()
     self.status = dict.fromkeys(GAME_IDS, LocalGameState.None_)
     self._update_task = None
Beispiel #3
0
 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
Beispiel #4
0
    def __init__(self, reader, writer, token):
        super().__init__(Platform.Bethesda, __version__, reader, writer, token)
        self._http_client = AuthenticatedHttpClient(self.store_credentials)
        self.bethesda_client = BethesdaClient(self._http_client)
        self.local_client = LocalClient()
        self.products_cache = product_cache
        self.owned_games_cache = None

        self._asked_for_local = False

        self.update_game_running_status_task = None
        self.update_game_installation_status_task = None
        self.check_for_new_games_task = None
        self.running_games = {}
        self.launching_lock = None
 def __init__(self, reader, writer, token):
     super().__init__(Platform.RiotGames, __version__, reader, writer, token)
     self.games_cache = games_cache
     self._http_client = AuthenticatedHttpClient(self.store_credentials)
     self._local_client = LocalClient()
     self.total_games_cache = self.create_total_games_cache()
     self.friends_cache = []
     self.owned_games_cache = []
     self.local_games_cache = []
     self.running_games_pids = {}
     self.game_is_loading = True
     self.checking_for_new_games = False
     self.updating_game_statuses = False
     self.buffer = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
     ctypes.windll.shell32.SHGetFolderPathW(None, 5, None, 0, self.buffer)
     self.documents_location = self.buffer.value
 def __init__(self, reader, writer, token):
     super().__init__(Platform.Rockstar, __version__, reader, writer, token)
     self.games_cache = games_cache
     self._http_client = BackendClient(self.store_credentials)
     self._local_client = None
     self.total_games_cache = self.create_total_games_cache()
     self.friends_cache = []
     self.presence_cache = {}
     self.owned_games_cache = []
     self.last_online_game_check = time() - 300
     self.local_games_cache = {}
     self.game_time_cache = {}
     self.running_games_info_list = {}
     self.game_is_loading = True
     self.checking_for_new_games = False
     self.updating_game_statuses = False
     self.buffer = None
     if IS_WINDOWS:
         self._local_client = LocalClient()
         self.buffer = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
         ctypes.windll.shell32.SHGetFolderPathW(None, 5, None, 0, self.buffer)
         self.documents_location = self.buffer.value
    def __init__(self, reader, writer, token):
        super().__init__(Platform.ParadoxPlaza, __version__, reader, writer,
                         token)
        self._http_client = AuthenticatedHttpClient(self.store_credentials)
        self.paradox_client = ParadoxClient(self._http_client)
        self.local_client = LocalClient()
        self.owned_games_cache = None

        self.local_games_cache = {}
        self.running_game = None

        self.tick_counter = 0

        self.local_games_called = None
        self.owned_games_called = None

        self.update_installed_games_task = None
        self.update_running_games_task = None
        self.update_owned_games_task = None
Beispiel #8
0
class BethesdaPlugin(Plugin):
    def __init__(self, reader, writer, token):
        super().__init__(Platform.Bethesda, __version__, reader, writer, token)
        self._http_client = AuthenticatedHttpClient(self.store_credentials)
        self.bethesda_client = BethesdaClient(self._http_client)
        self.local_client = LocalClient()

        self.local_client.local_games_cache = self.persistent_cache.get('local_games')
        if not self.local_client.local_games_cache:
            self.local_client.local_games_cache = {}

        self.products_cache = product_cache
        self.owned_games_cache = None

        self._asked_for_local = False

        self.update_game_running_status_task = None
        self.update_game_installation_status_task = None
        self.betty_client_process_task = None
        self.check_for_new_games_task = None
        self.running_games = {}
        self.launching_lock = None
        self._tick = 1

    async def authenticate(self, stored_credentials=None):
        if not stored_credentials:
            return NextStep("web_session", AUTH_PARAMS, cookies=[Cookie("passedICO", "true", ".bethesda.net")], js=JS)
        try:
            log.info("Got stored credentials")
            cookies = pickle.loads(bytes.fromhex(stored_credentials['cookie_jar']))
            cookies_parsed = []
            for cookie in cookies:
                if cookie.key in cookies_parsed and cookie.domain:
                    self._http_client.update_cookies({cookie.key: cookie.value})
                elif cookie.key not in cookies_parsed:
                    self._http_client.update_cookies({cookie.key: cookie.value})
                cookies_parsed.append(cookie.key)

            log.info("Finished parsing stored credentials, authenticating")
            user = await self._http_client.authenticate()

            self._http_client.set_auth_lost_callback(self.lost_authentication)

            return Authentication(user_id=user['user_id'], user_name=user['display_name'])
        except (AccessDenied, Banned, UnknownError) as e:
            log.error(f"Couldn't authenticate with stored credentials {repr(e)}")
            raise InvalidCredentials()

    async def pass_login_credentials(self, step, credentials, cookies):
        cookiez = {}
        illegal_keys = ['']
        for cookie in cookies:
            if cookie['name'] not in illegal_keys:
                cookiez[cookie['name']] = cookie['value']
        self._http_client.update_cookies(cookiez)

        try:
            user = await self._http_client.authenticate()
        except Exception as e:
            log.error(repr(e))
            raise InvalidCredentials()

        self._http_client.set_auth_lost_callback(self.lost_authentication)
        return Authentication(user_id=user['user_id'], user_name=user['display_name'])

    def _check_for_owned_products(self, owned_ids):
        products_to_consider = [product for product in self.products_cache if
                                'reference_id' in self.products_cache[product]]
        owned_product_ids = []

        for entitlement_id in owned_ids:
            for product in products_to_consider:
                for reference_id in self.products_cache[product]['reference_id']:
                    if entitlement_id in reference_id:
                        self.products_cache[product]['owned'] = True
                        owned_product_ids.append(entitlement_id)
        return owned_product_ids

    async def _get_owned_pre_orders(self, pre_order_ids):
        games_to_send = []
        for pre_order in pre_order_ids:
            pre_order_details = await self.bethesda_client.get_game_details(pre_order)
            if pre_order_details and 'Entry' in pre_order_details:
                entries_to_consider = [entry for entry in pre_order_details['Entry']
                                       if 'fields' in entry and 'productName' in entry['fields']]
                for entry in entries_to_consider:
                    if entry['fields']['productName'] in self.products_cache:
                        self.products_cache[entry['fields']['productName']]['owned'] = True
                    else:
                        games_to_send.append(Game(pre_order, entry['fields']['productName'] +
                                                  " (Pre Order)", None, LicenseInfo(LicenseType.SinglePurchase)))
                    break
        return games_to_send

    def _get_owned_games(self):
        games_to_send = []
        for product in self.products_cache:
            if self.products_cache[product]["owned"] and self.products_cache[product]["free_to_play"]:
                games_to_send.append(Game(self.products_cache[product]['local_id'], product, None, LicenseInfo(LicenseType.FreeToPlay)))
            elif self.products_cache[product]["owned"]:
                games_to_send.append(Game(self.products_cache[product]['local_id'], product, None, LicenseInfo(LicenseType.SinglePurchase)))
        return games_to_send

    async def get_owned_games(self):
        owned_ids = []
        games_to_send = []

        try:
            owned_ids = await self.bethesda_client.get_owned_ids()
        except (UnknownError, BackendError) as e:
            log.warning(f"No owned games detected {repr(e)}")

        log.info(f"Owned Ids: {owned_ids}")
        product_ids = self._check_for_owned_products(owned_ids)
        pre_order_ids = set(owned_ids) - set(product_ids)

        games_to_send.extend(await self._get_owned_pre_orders(pre_order_ids))
        games_to_send.extend(self._get_owned_games())

        log.info(f"Games to send (with free games): {games_to_send}")
        self.owned_games_cache = games_to_send
        return games_to_send

    async def get_local_games(self):
        if sys.platform != 'win32':
            log.error(f"Incompatible platform {sys.platform}")
            return []
        local_games = []

        installed_products = self.local_client.get_installed_products(timeout=2, products_cache=self.products_cache)

        log.info(f"Installed products {installed_products}")
        for product in self.products_cache:
            for installed_product in installed_products:
                if installed_products[installed_product] == self.products_cache[product]['local_id']:
                    self.products_cache[product]['installed'] = True
                    local_games.append(LocalGame(installed_products[installed_product], LocalGameState.Installed))

        self._asked_for_local = True
        log.info(f"Returning local games {local_games}")
        return local_games

    async def prepare_local_size_context(self, game_ids: List[str]) -> Any:
        return None

    async def get_local_size(self, game_id: str, context: Any) -> Optional[int]:
        local_cache = self.local_client.local_games_cache.copy()
        for game in local_cache:
            if local_cache[game]['local_id'] == game_id:
                return await self.local_client.get_size_at_path(local_cache[game]['path'])


    async def install_game(self, game_id):
        if sys.platform != 'win32':
            log.error(f"Incompatible platform {sys.platform}")
            return

        if not self.local_client.is_installed():
            await self._open_betty_browser()
            return

        if self.local_client.betty_client_process:
            self.local_client.focus_client_window()
            await self.launch_game(game_id)
        else:
            uuid = None
            for product in self.products_cache:

                if self.products_cache[product]['local_id'] == game_id:
                    if self.products_cache[product]['installed']:
                        log.warning("Got install on already installed game, launching")
                        return await self.launch_game(game_id)
                    uuid = "\"" + self.products_cache[product]['uuid'] + "\""
            cmd = "\"" + self.local_client.client_exe_path + "\"" + f" --installproduct={uuid}"
            log.info(f"Calling install game with command {cmd}")
            subprocess.Popen(cmd, shell=True)

    async def launch_game(self, game_id):
        if sys.platform != 'win32':
            log.error(f"Incompatible platform {sys.platform}")
            return
        if not self.local_client.is_installed():
            await self._open_betty_browser()
            return

        for product in self.products_cache:
            if self.products_cache[product]['local_id'] == game_id:
                if not self.products_cache[product]['installed']:
                    if not self.local_client.betty_client_process:
                        log.warning("Got launch on a not installed game, installing")
                        return await self.install_game(game_id)
                else:
                    if not self.local_client.betty_client_process:
                        self.launching_lock = time.time() + 45
                    else:
                        self.launching_lock = time.time() + 30
                    self.running_games[game_id] = None
                    self.update_local_game_status(
                        LocalGame(game_id, LocalGameState.Installed | LocalGameState.Running))
                    self.update_game_running_status_task.cancel()

        cmd = f"start bethesdanet://run/{game_id}"
        log.info(f"Calling launch command for id {game_id}, {cmd}")
        subprocess.Popen(cmd, shell=True)

    async def uninstall_game(self, game_id):
        if sys.platform != 'win32':
            log.error(f"Incompatible platform {sys.platform}")
            return

        if not self.local_client.is_installed():
            await self._open_betty_browser()
            return

        for product in self.products_cache:
            if self.products_cache[product]['local_id'] == game_id:
                if not self.products_cache[product]['installed']:
                    return

        log.info(f"Calling uninstall command for id {game_id}")
        cmd = f"start bethesdanet://uninstall/{game_id}"

        subprocess.Popen(cmd, shell=True)

        if not self.local_client.betty_client_process:
            await asyncio.sleep(2)  # QOL, bethesda slowly reacts to uninstall command,
            self.local_client.focus_client_window()

    async def _open_betty_browser(self):
        url = "https://bethesda.net/game/bethesda-launcher"
        log.info(f"Opening Bethesda website on url {url}")
        webbrowser.open(url)

    async def _heavy_installation_status_check(self):
        installed_products = self.local_client.get_installed_products(4, self.products_cache)
        changed = False

        products_cache_installed_products = {}

        for product in self.products_cache:
            if self.products_cache[product]['installed']:
                products_cache_installed_products[product] = self.products_cache[product]['local_id']

        for installed_product in installed_products:
            if installed_product not in products_cache_installed_products:
                self.products_cache[installed_product]["installed"] = True
                self.update_local_game_status(LocalGame(installed_products[installed_product], LocalGameState.Installed))
                changed = True

        for installed_product in products_cache_installed_products:
            if installed_product not in installed_products:
                self.products_cache[installed_product]["installed"] = False
                self.update_local_game_status(LocalGame(products_cache_installed_products[installed_product], LocalGameState.None_))
                changed = True

        return changed

    def _light_installation_status_check(self):
        changed = False
        for local_game in self.local_client.local_games_cache:
            local_game_installed = self.local_client.is_local_game_installed(self.local_client.local_games_cache[local_game])

            if local_game_installed and not self.products_cache[local_game]["installed"]:
                self.products_cache[local_game]["installed"] = True
                self.update_local_game_status(LocalGame(self.local_client.local_games_cache[local_game]['local_id'], LocalGameState.Installed))
                changed = True

            elif not local_game_installed and self.products_cache[local_game]["installed"]:
                self.products_cache[local_game]["installed"] = False
                self.update_local_game_status(LocalGame(self.local_client.local_games_cache[local_game]['local_id'], LocalGameState.None_))
                changed = True

        return changed

    async def update_game_installation_status(self):

        if self.local_client.clientgame_changed() or self.local_client.launcher_children_number_changed():
            await asyncio.sleep(1)
            log.info("Starting heavy installation status check")
            if await self._heavy_installation_status_check():
                # Game status has changed
                self.persistent_cache['local_games'] = self.local_client.local_games_cache
                self.push_cache()
        else:
            if self._light_installation_status_check():
                # Game status has changed
                self.persistent_cache['local_games'] = self.local_client.local_games_cache
                self.push_cache()

    async def _scan_running_games(self, process_iter_interval):
        for process in process_iter():
            await asyncio.sleep(process_iter_interval)
            for local_game in self.local_client.local_games_cache:
                if not process.binary_path:
                    continue
                if process.binary_path in self.local_client.local_games_cache[local_game]['execs']:
                    log.info(f"Found a running game! {local_game}")
                    local_id = self.local_client.local_games_cache[local_game]['local_id']
                    if local_id not in self.running_games:
                        self.update_local_game_status(LocalGame(local_id,
                                                                LocalGameState.Installed | LocalGameState.Running))
                    self.running_games[local_id] = RunningGame(self.local_client.local_games_cache[local_game]['execs'], process)

    async def _update_status_of_already_running_games(self, process_iter_interval, dont_downgrade_status):
        for running_game in self.running_games.copy():
            if not self.running_games[running_game] and dont_downgrade_status:
                log.info(f"Found 'just launched' game {running_game}")
                continue
            elif not self.running_games[running_game]:
                log.info(f"Found 'just launched' game but its still without pid and its time run out {running_game}")
                self.running_games.pop(running_game)
                self.update_local_game_status(
                    LocalGame(running_game, LocalGameState.Installed))
                continue

            for process in process_iter():
                await asyncio.sleep(process_iter_interval)
                if process.binary_path in self.running_games[running_game].execs:
                    return True

            self.running_games.pop(running_game)
            self.update_local_game_status(
                LocalGame(running_game, LocalGameState.Installed))

    async def update_game_running_status(self):

        process_iter_interval = 0.02
        dont_downgrade_status = False

        if self.launching_lock and self.launching_lock >= time.time():
            process_iter_interval = 0.01
            dont_downgrade_status = True

        if self.running_games:
            # Don't iterate over processes if a game is already running, assuming user is playing one game at a time.
            if not await self._update_status_of_already_running_games(process_iter_interval, dont_downgrade_status):
                await self._scan_running_games(process_iter_interval)
            await asyncio.sleep(1)
            return

        await self._scan_running_games(process_iter_interval)
        await asyncio.sleep(1)

    async def check_for_new_games(self):
        games_cache = self.owned_games_cache
        owned_games = await self.get_owned_games()
        for owned_game in owned_games:
            if owned_game not in games_cache:
                self.add_game(owned_game)
        await asyncio.sleep(60)

    async def close_bethesda_window(self):
        if sys.platform != 'win32':
            return
        window_name = "Bethesda.net Launcher"
        max_delay = 10
        intermediate_sleep = 0.05
        stop_time = time.time() + max_delay

        def timed_out():
            if time.time() >= stop_time:
                log.warning(f"Timed out trying to close {window_name}")
                return True
            return False

        try:
            hwnd = ctypes.windll.user32.FindWindowW(None, window_name)
            while not ctypes.windll.user32.IsWindowVisible(hwnd):
                hwnd = ctypes.windll.user32.FindWindowW(None, window_name)
                await asyncio.sleep(intermediate_sleep)
                if timed_out():
                    return

            while ctypes.windll.user32.IsWindowVisible(hwnd):
                await asyncio.sleep(intermediate_sleep)
                ctypes.windll.user32.CloseWindow(hwnd)
                if timed_out():
                    return
        except Exception as e:
            log.error(f"Exception when checking if window is visible {repr(e)}")

    async def shutdown_platform_client(self):
        if sys.platform != 'win32':
            return
        log.info("killing bethesda")
        subprocess.Popen("taskkill.exe /im \"BethesdaNetLauncher.exe\"")

    async def launch_platform_client(self):
        if not self.local_client.betty_client_process:
            return
        if sys.platform != 'win32':
            return
        log.info("launching bethesda")
        subprocess.Popen('start bethesdanet://', shell=True)
        asyncio.create_task(self.close_bethesda_window())

    def tick(self):
        if sys.platform == 'win32':
            if self._asked_for_local and (not self.update_game_installation_status_task or self.update_game_installation_status_task.done()):
                self.update_game_installation_status_task = asyncio.create_task(self.update_game_installation_status())

            if self._asked_for_local and (not self.update_game_running_status_task or self.update_game_running_status_task.done()):
                self.update_game_running_status_task = asyncio.create_task(self.update_game_running_status())

            if not self.betty_client_process_task or self.betty_client_process_task.done():
                self.betty_client_process_task = asyncio.create_task(self.local_client.is_running())

        if self._http_client.bearer:
            if self.owned_games_cache and (not self.check_for_new_games_task or self.check_for_new_games_task.done()):
                self.check_for_new_games_task = asyncio.create_task(self.check_for_new_games())

    async def shutdown(self):
        await self._http_client.close()
class RiotPlugin(Plugin):
    def __init__(self, reader, writer, token):
        super().__init__(
            Platform.RiotGames,  # choose platform from available list
            __version__,  # version
            reader,
            writer,
            token,
        )
        self.local_client = LocalClient()
        self.status = dict.fromkeys(GAME_IDS, LocalGameState.None_)
        self._update_task = None

    async def authenticate(self, stored_credentials=None):
        self.store_credentials({"dummy": "dummy"})
        return Authentication("riot_user", "Riot User")

    async def get_owned_games(self):
        log.info("Getting owned games")
        return [
            Game(
                GameID.league_of_legends,
                "League of Legends",
                None,
                LicenseInfo(LicenseType.FreeToPlay),
            ),
            Game(
                GameID.legends_of_runeterra,
                "Legends of Runeterra",
                None,
                LicenseInfo(LicenseType.FreeToPlay),
            ),
            Game(
                GameID.valorant,
                "Valorant",
                None,
                LicenseInfo(LicenseType.FreeToPlay),
            ),
        ]

    async def get_local_games(self):
        log.info("Getting local games")
        local_games = []
        for game_id in GAME_IDS:
            if self.local_client.game_installed(game_id):
                local_game = LocalGame(game_id, LocalGameState.Installed)
                local_games.append(local_game)
        log.debug(f"RIOT_INSTALLED_GAMES: {local_games}")
        return local_games

    async def prepare_local_size_context(self, game_ids):
        sizes = []
        for game_id in GAME_IDS:
            size = await misc.get_size_at_path(
                self.local_client.install_location[game_id], if_none=0
            )
            if game_id == GameID.valorant:
                size += await misc.get_size_at_path(
                    self.local_client.install_location[GameID.vanguard], if_none=0
                )
            if size == 0:
                size = None
            sizes.append(size)
        return dict(zip(game_ids, sizes))

    async def get_local_size(self, game_id: str, context):
        return context[game_id]

    async def uninstall_game(self, game_id):
        self.local_client.update_installed()
        self.local_client.uninstall(game_id)

    async def launch_game(self, game_id):
        log.debug("RCS location: " + self.local_client.riot_client_services_path)
        self.local_client.update_installed()
        self.local_client.launch(game_id)

    async def get_os_compatibility(self, game_id, context):
        log.info("Getting os compatibility")
        if game_id == GameID.league_of_legends:
            return OSCompatibility.Windows | OSCompatibility.MacOS
        elif game_id == GameID.legends_of_runeterra:
            return OSCompatibility.Windows
        elif game_id == GameID.valorant:
            return OSCompatibility.Windows

    async def install_game(self, game_id):
        log.info("Installing game")
        self.local_client.update_installed()
        if self.local_client.riot_client_services_path is None:
            misc.download(DOWNLOAD_URL[game_id], misc.open_path)
        else:
            self.local_client.launch(game_id, save_process=False)

    async def _update(self):
        def update(game_id, status: LocalGameState):
            if self.status[game_id] != status:
                self.status[game_id] = status
                self.update_local_game_status(LocalGame(game_id, status))
                log.info(f"Updated {game_id} to {status}")
                return True  # return true if needed to update
            return False

        self.local_client.update_installed()
        for game_id in GAME_IDS:
            if self.local_client.game_running(game_id):
                if update(game_id, LocalGameState.Installed | LocalGameState.Running):
                    self.game_time_tracker.start_tracking_game(game_id)
            elif self.local_client.game_installed(game_id):
                if update(game_id, LocalGameState.Installed):
                    if game_id in self.game_time_tracker.get_tracking_games():
                        self.game_time_tracker.stop_tracking_game(game_id)
            else:
                update(game_id, LocalGameState.None_)

        log.debug(f"self.local_client.install_location: {self.local_client.install_location}")
        log.debug(f"self.status: {self.status}")

        await asyncio.sleep(5)

    def tick(self):
        if self._update_task is None or self._update_task.done():
            self._update_task = self.create_task(self._update(), "Update Task")

    # Time Tracker

    async def get_game_time(self, game_id, context):
        try:
            return self.game_time_tracker.get_tracked_time(game_id)
        except time_tracker.GameNotTrackedException:
            return None

    def handshake_complete(self):
        misc.cleanup()

        self.game_time_cache = None
        if "game_time_cache" in self.persistent_cache:
            self.game_time_cache = pickle.loads(
                bytes.fromhex(self.persistent_cache["game_time_cache"])
            )
        else:
            if os.path.isfile(LOCAL_FILE_CACHE):
                with open(LOCAL_FILE_CACHE, "r") as file:
                    for line in file.readlines():
                        if line[:1] != "#":
                            self.game_time_cache = pickle.loads(bytes.fromhex(line))
                            break

        self.game_time_tracker = time_tracker.TimeTracker(game_time_cache=self.game_time_cache)

    async def shutdown(self):
        misc.kill_all_processes()
        for game_id in self.game_time_tracker.get_tracking_games():
            self.game_time_tracker.stop_tracking_game(game_id)
        if self.game_time_cache is not None:
            with open(LOCAL_FILE_CACHE, "w+") as file:
                file.write("# DO NOT EDIT THIS FILE\n")
                file.write(self.game_time_tracker.get_time_cache_hex())
                log.info("Wrote to local file cache")
        await super().shutdown()

    def game_times_import_complete(self):
        if len(self.game_time_tracker.get_tracking_games()) > 0:
            log.debug("Game time still being tracked. Not setting cache yet.")
        else:
            self.game_time_cache = self.game_time_tracker.get_time_cache()
            log.debug(f"game_time_cache: {self.game_time_cache}")
            self.persistent_cache["game_time_cache"] = self.game_time_tracker.get_time_cache_hex()
            self.push_cache()
Beispiel #10
0
class RockstarPlugin(Plugin):
    def __init__(self, reader, writer, token):
        super().__init__(Platform.RiotGames, __version__, reader, writer,
                         token)
        self.games_cache = games_cache
        self._http_client = AuthenticatedHttpClient(self.store_credentials)
        self._local_client = LocalClient()
        self.total_games_cache = self.create_total_games_cache()
        self.friends_cache = []
        self.owned_games_cache = []
        self.local_games_cache = {}
        self.running_games_pids = {}
        self.game_is_loading = True
        self.checking_for_new_games = False
        self.updating_game_statuses = False
        self.buffer = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
        ctypes.windll.shell32.SHGetFolderPathW(None, 5, None, 0, self.buffer)
        self.documents_location = self.buffer.value

    def is_authenticated(self):
        return self._http_client.is_authenticated()

    async def authenticate(self, stored_credentials=None):
        self._http_client.create_session(stored_credentials)
        if not stored_credentials:
            return NextStep("web_session", AUTH_PARAMS)
        try:
            log.info("INFO: The credentials were successfully obtained.")
            cookies = pickle.loads(
                bytes.fromhex(stored_credentials['session_object'])).cookies
            log.debug("ROCKSTAR_COOKIES_FROM_HEX: " + str(cookies))
            for cookie in cookies:
                cookie_object = {
                    "name": cookie.name,
                    "value": cookie.value,
                    "domain": cookie.domain,
                    "path": cookie.path
                }
                self._http_client.update_cookie(cookie_object)
            self._http_client.set_current_auth_token(
                stored_credentials['current_auth_token'])
            log.info(
                "INFO: The stored credentials were successfully parsed. Beginning authentication..."
            )
            user = await self._http_client.authenticate()
            return Authentication(user_id=user['rockstar_id'],
                                  user_name=user['display_name'])
        except Exception as e:
            log.warning(
                "ROCKSTAR_AUTH_WARNING: The exception " + repr(e) +
                " was thrown, presumably because of "
                "outdated credentials. Attempting to get new credentials...")
            self._http_client.set_auth_lost_callback(self.lost_authentication)
            try:
                user = await self._http_client.authenticate()
                return Authentication(user_id=user['rockstar_id'],
                                      user_name=user['display_name'])
            except Exception as e:
                log.error(
                    "ROCKSTAR_AUTH_FAILURE: Something went terribly wrong with the re-authentication. "
                    + repr(e))
                log.exception("ROCKSTAR_STACK_TRACE")
                raise InvalidCredentials()

    async def pass_login_credentials(self, step, credentials, cookies):
        log.debug("ROCKSTAR_COOKIE_LIST: " + str(cookies))
        for cookie in cookies:
            if cookie['name'] == "ScAuthTokenData":
                self._http_client.set_current_auth_token(cookie['value'])
            cookie_object = {
                "name": cookie['name'],
                "value": cookie['value'],
                "domain": cookie['domain'],
                "path": cookie['path']
            }
            self._http_client.update_cookie(cookie_object)
        try:
            user = await self._http_client.authenticate()
        except Exception as e:
            log.error(repr(e))
            raise InvalidCredentials()
        return Authentication(user_id=user["rockstar_id"],
                              user_name=user["display_name"])

    async def shutdown(self):
        await self._http_client.close()

    def create_total_games_cache(self):
        cache = []
        for title_id in list(games_cache):
            cache.append(self.create_game_from_title_id(title_id))
        return cache

    async def get_friends(self):
        # This method is currently causing the plugin to crash after its second call. I am unsure of why this is
        # happening, but it needs to be fixed.

        # NOTE: This will return a list of type FriendInfo.
        # The Social Club website returns a list of the current user's friends through the url
        # https://scapi.rockstargames.com/friends/getFriendsFiltered?onlineService=sc&nickname=&pageIndex=0&pageSize=30.
        # The nickname URL parameter is left blank because the website instead uses the bearer token to get the correct
        # information. The last two parameters are of great importance, however. The parameter pageSize determines the
        # number of friends given on that page's list, while pageIndex keeps track of the page that the information is
        # on. The maximum number for pageSize is 30, so that is what we will use to cut down the number of HTTP
        # requests.

        # We first need to get the number of friends.
        url = (
            "https://scapi.rockstargames.com/friends/getFriendsFiltered?onlineService=sc&nickname=&"
            "pageIndex=0&pageSize=30")
        current_page = await self._http_client.get_json_from_request_strict(url
                                                                            )
        log.debug("ROCKSTAR_FRIENDS_REQUEST: " + str(current_page))
        num_friends = current_page['rockstarAccountList']['totalFriends']
        num_pages_required = num_friends / 30 if num_friends % 30 != 0 else (
            num_friends / 30) - 1

        # Now, we need to get the information about the friends.
        friends_list = current_page['rockstarAccountList']['rockstarAccounts']
        return_list = []
        for i in range(0, len(friends_list)):
            friend = FriendInfo(friends_list[i]['rockstarId'],
                                friends_list[i]['displayName'])
            return_list.append(FriendInfo)
            for cached_friend in self.friends_cache:
                if cached_friend.user_id == friend.user_id:
                    break
            else:
                self.friends_cache.append(friend)
                self.add_friend(friend)
            log.debug("ROCKSTAR_FRIEND: Found " + friend.user_name +
                      " (Rockstar ID: " + str(friend.user_id) + ")")

        # The first page is finished, but now we need to work on any remaining pages.
        if num_pages_required > 0:
            for i in range(1, int(num_pages_required + 1)):
                url = (
                    "https://scapi.rockstargames.com/friends/getFriendsFiltered?onlineService=sc&nickname=&"
                    "pageIndex=" + str(i) + "&pageSize=30")
                return_list.append(friend
                                   for friend in await self._get_friends(url))
        return return_list

    async def _get_friends(self, url):
        current_page = self._http_client.get_json_from_request_strict(url)
        friends_list = current_page['rockstarAccountList']['rockstarAccounts']
        return_list = []
        for i in range(0, len(friends_list)):
            friend = FriendInfo(friends_list[i]['rockstarId'],
                                friends_list[i]['displayName'])
            return_list.append(FriendInfo)
            for cached_friend in self.friends_cache:
                if cached_friend.user_id == friend.user_id:
                    break
            else:  # An else-statement occurs after a for-statement if the latter finishes WITHOUT breaking.
                self.friends_cache.append(friend)
                self.add_friend(friend)
            log.debug("ROCKSTAR_FRIEND: Found " + friend.user_name +
                      " (Rockstar ID: " + str(friend.user_id) + ")")
        return return_list

    async def get_owned_games(self):
        # Here is the actual implementation of getting the user's owned games:
        # -Get the list of games_played from rockstargames.com/auth/get-user.json.
        #   -If possible, use the launcher log to confirm which games are actual launcher games and which are
        #   Steam/Retail games.
        #   -If it is not possible to use the launcher log, then just use the list provided by the website.
        if not self.is_authenticated():
            raise AuthenticationRequired()

        # Get the list of games_played from https://www.rockstargames.com/auth/get-user.json.
        owned_title_ids = []
        online_check_success = True
        try:
            played_games = await self._http_client.get_played_games()
            for game in played_games:
                title_id = get_game_title_id_from_online_title_id(game)
                owned_title_ids.append(title_id)
                log.debug("ROCKSTAR_ONLINE_GAME: Found played game " +
                          title_id + "!")
        except Exception as e:
            log.error(
                "ROCKSTAR_PLAYED_GAMES_ERROR: The exception " + repr(e) +
                " was thrown when attempting to get the"
                " user's played games online. Falling back to log file check..."
            )
            online_check_success = False

        # The log is in the Documents folder.
        current_log_count = 0
        log_file = None
        log_file_append = ""
        # The Rockstar Games Launcher generates 10 log files before deleting them in a FIFO fashion. Old log files are
        # given a number ranging from 1 to 9 in their name. In case the first log file does not have all of the games,
        # we need to check the other log files, if possible.
        while current_log_count < 10:
            try:
                if current_log_count != 0:
                    log_file_append = ".0" + str(current_log_count)
                log_file = os.path.join(
                    self.documents_location,
                    "Rockstar Games\\Launcher\\launcher" + log_file_append +
                    ".log")
                log.debug("ROCKSTAR_LOG_LOCATION: Checking the file " +
                          log_file + "...")
                owned_title_ids = await self.parse_log_file(
                    log_file, owned_title_ids, online_check_success)
                break
            except NoGamesInLogException:
                log.warning(
                    "ROCKSTAR_LOG_WARNING: There are no owned games listed in "
                    + str(log_file) + ". Moving to "
                    "the next log file...")
                current_log_count += 1
            except NoLogFoundException:
                log.warning(
                    "ROCKSTAR_LAST_LOG_REACHED: There are no more log files that can be found and/or read "
                    "from. Assuming that the online list is correct...")
                break
            except Exception:
                # This occurs after ROCKSTAR_LOG_ERROR.
                break
        if current_log_count == 10:
            log.warning(
                "ROCKSTAR_LAST_LOG_REACHED: There are no more log files that can be found and/or read "
                "from. Assuming that the online list is correct...")
        remove_all = False
        if len(self.owned_games_cache) == 0:
            remove_all = True
        for title_id in owned_title_ids:
            game = self.create_game_from_title_id(title_id)
            if game not in self.owned_games_cache:
                log.debug("ROCKSTAR_ADD_GAME: Adding " + title_id +
                          " to owned games cache...")
                self.add_game(game)
                self.owned_games_cache.append(game)
        if remove_all is True:
            for key, value in games_cache.items():
                if key not in owned_title_ids:
                    log.debug("ROCKSTAR_REMOVE_GAME: Removing " + key +
                              " from owned games cache...")
                    self.remove_game(value['rosTitleId'])
        else:
            for game in self.owned_games_cache:
                if get_game_title_id_from_ros_title_id(
                        game.game_id) not in owned_title_ids:
                    log.debug(
                        "ROCKSTAR_REMOVE_GAME: Removing " +
                        get_game_title_id_from_ros_title_id(game.game_id) +
                        " from owned games cache...")
                    self.remove_game(game.game_id)
                    self.owned_games_cache.remove(game)
        return self.owned_games_cache

    @staticmethod
    async def parse_log_file(log_file, owned_title_ids, online_check_success):
        owned_title_ids_ = owned_title_ids
        checked_games_count = 0
        total_games_count = len(games_cache)
        if os.path.exists(log_file):
            with FileReadBackwards(log_file, encoding="utf-8") as frb:
                while checked_games_count < total_games_count:
                    try:
                        line = frb.readline()
                    except UnicodeDecodeError:
                        log.warning(
                            "ROCKSTAR_LOG_UNICODE_WARNING: An invalid Unicode character was found in the line "
                            + line + ". Continuing to next line...")
                        continue
                    except Exception as e:
                        log.error(
                            "ROCKSTAR_LOG_ERROR: Reading " + line +
                            " from the log file resulted in the "
                            "exception " + repr(e) +
                            " being thrown. Using the online list... (Please report "
                            "this issue on the plugin's GitHub page!)")
                        raise
                    if not line:
                        log.error(
                            "ROCKSTAR_LOG_FINISHED_ERROR: The entire log file was read, but all of the games "
                            "could not be accounted for. Proceeding to import the games that have been "
                            "confirmed...")
                        raise NoGamesInLogException()
                    # We need to do two main things with the log file:
                    # 1. If a game is present in owned_title_ids but not owned according to the log file, then it is
                    #    assumed to be a non-Launcher game, and is removed from the list.
                    # 2. If a game is owned according to the log file but is not already present in owned_title_ids,
                    #    then it is assumed that the user has purchased the game on the Launcher, but has not yet played
                    #    it. In this case, the game will be added to owned_title_ids.
                    if ("launcher" not in line) and ("on branch "
                                                     in line):  # Found a game!
                        # Each log line for a title branch report describes the title id of the game starting at
                        # character 65. Interestingly, the lines all have the same colon as character 75. This implies
                        # that this format was intentionally done by Rockstar, so they likely will not change it anytime
                        # soon.
                        title_id = line[65:75].strip()
                        log.debug(
                            "ROCKSTAR_LOG_GAME: The game with title ID " +
                            title_id + " is owned!")
                        if title_id not in owned_title_ids_:
                            if online_check_success is True:
                                # Case 2: The game is owned, but has not been played.
                                log.warning(
                                    "ROCKSTAR_UNPLAYED_GAME: The game with title ID "
                                    + title_id +
                                    " is owned, but it has never been played!")
                            owned_title_ids_.append(title_id)
                        checked_games_count += 1
                    elif "no branches!" in line:
                        title_id = line[65:75].strip()
                        if title_id in owned_title_ids_:
                            # Case 1: The game is not actually owned on the launcher.
                            log.warning(
                                "ROCKSTAR_FAKE_GAME: The game with title ID " +
                                title_id + " is not owned on "
                                "the Rockstar Games Launcher!")
                            owned_title_ids_.remove(title_id)
                        checked_games_count += 1
                    if checked_games_count == total_games_count:
                        break
            return owned_title_ids_
        else:
            raise NoLogFoundException()

    async def get_local_games(self):
        # Since the API requires that get_local_games returns a list of LocalGame objects, local_list is the value that
        # needs to be returned. However, for internal use (the self.local_games_cache field), the dictionary local_games
        # is used for greater flexibility.
        local_games = {}
        local_list = []
        for game in self.total_games_cache:
            title_id = get_game_title_id_from_ros_title_id(str(game.game_id))
            check = self._local_client.get_path_to_game(title_id)
            if check is not None:
                if (title_id in self.running_games_pids
                        and check_if_process_exists(
                            self.running_games_pids[title_id][0])):
                    local_game = self.create_local_game_from_title_id(
                        title_id, True, True)
                else:
                    local_game = self.create_local_game_from_title_id(
                        title_id, False, True)
            else:
                local_game = self.create_local_game_from_title_id(
                    title_id, False, False)
            local_games[title_id] = local_game
            local_list.append(local_game)
        self.local_games_cache = local_games
        log.debug("ROCKSTAR_INSTALLED_GAMES: " + str(local_games))
        return local_list

    async def check_for_new_games(self):
        self.checking_for_new_games = True
        await self.get_owned_games()
        await asyncio.sleep(60)
        self.checking_for_new_games = False

    async def check_game_statuses(self):
        self.updating_game_statuses = True
        old_local_game_cache = self.local_games_cache
        await self.get_local_games()
        for title_id, local_game in self.local_games_cache.items():
            if (local_game.local_game_state !=
                (old_local_game_cache[title_id]).local_game_state):
                log.debug("ROCKSTAR_LOCAL_CHANGE: The status for " + title_id +
                          " has changed.")
                self.update_local_game_status(local_game)
        #   else:
        #       log.debug("ROCKSTAR_LOCAL_REMAIN: The status for " + title_id + " has not changed.") - Reduce Console
        #       Spam (Enable this if you need to.)
        await asyncio.sleep(5)
        self.updating_game_statuses = False

    async def launch_game(self, game_id):
        title_id = get_game_title_id_from_ros_title_id(game_id)
        self.running_games_pids[title_id] = [
            await self._local_client.launch_game_from_title_id(title_id), True
        ]
        log.debug("ROCKSTAR_PIDS: " + str(self.running_games_pids))
        if self.running_games_pids[title_id][0] != '-1':
            self.update_local_game_status(
                LocalGame(game_id,
                          LocalGameState.Running | LocalGameState.Installed))

    async def install_game(self, game_id):
        title_id = get_game_title_id_from_ros_title_id(game_id)
        log.debug("ROCKSTAR_INSTALL_REQUEST: Requesting to install " +
                  title_id + "...")
        self._local_client.install_game_from_title_id(title_id)
        # If the game is not released yet, then we should allow them to see this on the Rockstar Games Launcher, but the
        # game's installation status should not be changed.
        if games_cache[title_id]["isPreOrder"]:
            return
        self.update_local_game_status(
            LocalGame(game_id, LocalGameState.Installed))

    async def uninstall_game(self, game_id):
        title_id = get_game_title_id_from_ros_title_id(game_id)
        log.debug("ROCKSTAR_UNINSTALL_REQUEST: Requesting to uninstall " +
                  title_id + "...")
        self._local_client.uninstall_game_from_title_id(title_id)
        self.update_local_game_status(LocalGame(game_id, LocalGameState.None_))

    def create_game_from_title_id(self, title_id):
        return Game(self.games_cache[title_id]["rosTitleId"],
                    self.games_cache[title_id]["friendlyName"], None,
                    self.games_cache[title_id]["licenseInfo"])

    def create_local_game_from_title_id(self, title_id, is_running,
                                        is_installed):
        if is_running:
            return LocalGame(self.games_cache[title_id]["rosTitleId"],
                             LocalGameState.Running | LocalGameState.Installed)
        elif is_installed:
            return LocalGame(self.games_cache[title_id]["rosTitleId"],
                             LocalGameState.Installed)
        else:
            return LocalGame(self.games_cache[title_id]["rosTitleId"],
                             LocalGameState.None_)

    def tick(self):
        if not self.is_authenticated():
            return
        if not self.checking_for_new_games:
            log.debug("Checking for new games...")
            asyncio.create_task(self.check_for_new_games())
        if not self.updating_game_statuses:
            log.debug("Checking local game statuses...")
            asyncio.create_task(self.check_game_statuses())
class RockstarPlugin(Plugin):
    def __init__(self, reader, writer, token):
        super().__init__(Platform.Rockstar, __version__, reader, writer, token)
        self.games_cache = games_cache
        self._http_client = BackendClient(self.store_credentials)
        self._local_client = None
        self.total_games_cache = self.create_total_games_cache()
        self.friends_cache = []
        self.presence_cache = {}
        self.owned_games_cache = []
        self.last_online_game_check = time() - 300
        self.local_games_cache = {}
        self.game_time_cache = {}
        self.running_games_info_list = {}
        self.game_is_loading = True
        self.checking_for_new_games = False
        self.updating_game_statuses = False
        self.buffer = None
        if IS_WINDOWS:
            self._local_client = LocalClient()
            self.buffer = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
            ctypes.windll.shell32.SHGetFolderPathW(None, 5, None, 0, self.buffer)
            self.documents_location = self.buffer.value

    def is_authenticated(self):
        return self._http_client.is_authenticated()

    @staticmethod
    def loads_js(file):
        with open(os.path.abspath(os.path.join(__file__, '..', 'js', file)), 'r') as f:
            return f.read()

    def handshake_complete(self):
        game_time_cache_in_persistent_cache = False
        for key, value in self.persistent_cache.items():
            # if "achievements_" in key:
                # log.debug("ROCKSTAR_CACHE_IMPORT: Importing " + key + " from persistent cache...")
                # self._all_achievements_cache[key] = pickle.loads(bytes.fromhex(value))
            if key == "game_time_cache":
                self.game_time_cache = pickle.loads(bytes.fromhex(value))
                game_time_cache_in_persistent_cache = True
        if IS_WINDOWS and not game_time_cache_in_persistent_cache:
            # The game time cache was not found in the persistent cache, so the plugin will instead attempt to get the
            # cache from the user's file stored on their disk.
            file_location = os.path.join(self.documents_location, "RockstarPlayTimeCache.txt")
            try:
                file = open(file_location, "r")
                for line in file.readlines():
                    if line[:1] != "#":
                        log.debug("ROCKSTAR_LOCAL_GAME_TIME_FROM_FILE: " + str(pickle.loads(bytes.fromhex(line))))
                        self.game_time_cache = pickle.loads(bytes.fromhex(line))
                        break
                if not self.game_time_cache:
                    log.warning("ROCKSTAR_NO_GAME_TIME: The user's played time could not be found in neither the "
                                "persistent cache nor the designated local file. Let's hope that the user is new...")
            except FileNotFoundError:
                log.warning("ROCKSTAR_NO_GAME_TIME: The user's played time could not be found in neither the persistent"
                            " cache nor the designated local file. Let's hope that the user is new...")

    async def authenticate(self, stored_credentials=None):
        try:
            self._http_client.create_session(stored_credentials)
        except KeyError:
            log.error("ROCKSTAR_OLD_LOG_IN: The user has likely previously logged into the plugin with a version less "
                      "than v0.3, and their credentials might be corrupted. Forcing a log-out...")
            raise InvalidCredentials()
        if not stored_credentials:
            # We will create the fingerprint JavaScript dictionary here.
            fingerprint_js = {
                r'https://www.rockstargames.com/': [
                    self.loads_js("fingerprint2.js"),
                    self.loads_js("HashGen.js"),
                    self.loads_js("GenerateFingerprint.js")
                ]
            }
            return NextStep("web_session", AUTH_PARAMS, js=fingerprint_js)
        try:
            log.info("INFO: The credentials were successfully obtained.")
            if LOG_SENSITIVE_DATA:
                cookies = pickle.loads(bytes.fromhex(stored_credentials['cookie_jar']))
                log.debug("ROCKSTAR_COOKIES_FROM_HEX: " + str(cookies))  # sensitive data hidden by default
            # for cookie in cookies:
            #   self._http_client.update_cookies({cookie.name: cookie.value})
            self._http_client.set_current_auth_token(stored_credentials['current_auth_token'])
            self._http_client.set_current_sc_token(stored_credentials['current_sc_token'])
            self._http_client.set_refresh_token_absolute(
                pickle.loads(bytes.fromhex(stored_credentials['refresh_token'])))
            self._http_client.set_fingerprint(stored_credentials['fingerprint'])
            log.info("INFO: The stored credentials were successfully parsed. Beginning authentication...")
            user = await self._http_client.authenticate()
            return Authentication(user_id=user['rockstar_id'], user_name=user['display_name'])
        except (NetworkError, UnknownError):
            raise
        except Exception as e:
            log.warning("ROCKSTAR_AUTH_WARNING: The exception " + repr(e) + " was thrown, presumably because of "
                        "outdated credentials. Attempting to get new credentials...")
            self._http_client.set_auth_lost_callback(True)
            try:
                user = await self._http_client.authenticate()
                return Authentication(user_id=user['rockstar_id'], user_name=user['display_name'])
            except Exception as e:
                log.error("ROCKSTAR_AUTH_FAILURE: Something went terribly wrong with the re-authentication. " + repr(e))
                log.exception("ROCKSTAR_STACK_TRACE")
                raise InvalidCredentials

    async def pass_login_credentials(self, step, credentials, cookies):
        if LOG_SENSITIVE_DATA:
            log.debug("ROCKSTAR_COOKIE_LIST: " + str(cookies))
        for cookie in cookies:
            if cookie['name'] == "ScAuthTokenData":
                self._http_client.set_current_auth_token(cookie['value'])
            if cookie['name'] == "BearerToken":
                self._http_client.set_current_sc_token(cookie['value'])
            if cookie['name'] == "RMT":
                if cookie['value'] != "":
                    if LOG_SENSITIVE_DATA:
                        log.debug("ROCKSTAR_REMEMBER_ME: Got RMT: " + cookie['value'])
                    else:
                        log.debug("ROCKSRAR_REMEMBER_ME: Got RMT: ***")  # Only asterisks are shown here for consistency
                        # with the output when the user has a blank RMT from multi-factor authentication.
                    self._http_client.set_refresh_token(cookie['value'])
                else:
                    if LOG_SENSITIVE_DATA:
                        log.debug("ROCKSTAR_REMEMBER_ME: Got RMT: [Blank!]")
                    else:
                        log.debug("ROCKSTAR_REMEMBER_ME: Got RMT: ***")
                    self._http_client.set_refresh_token('')
            if cookie['name'] == "fingerprint":
                if LOG_SENSITIVE_DATA:
                    log.debug("ROCKSTAR_FINGERPRINT: Got fingerprint: " + cookie['value'].replace("$", ";"))
                else:
                    log.debug("ROCKSTAR_FINGERPRINT: Got fingerprint: ***")
                self._http_client.set_fingerprint(cookie['value'].replace("$", ";"))
                # We will not add the fingerprint as a cookie to the session; it will instead be stored with the user's
                # credentials.
                continue
            if re.search("^rsso", cookie['name']):
                if LOG_SENSITIVE_DATA:
                    log.debug("ROCKSTAR_RSSO: Got " + cookie['name'] + ": " + cookie['value'])
                else:
                    log.debug(f"ROCKSTAR_RSSO: Got rsso-***: {cookie['value'][:5]}***{cookie['value'][-3:]}")
            cookie_object = {
                "name": cookie['name'],
                "value": cookie['value'],
                "domain": cookie['domain'],
                "path": cookie['path']
            }
            self._http_client.update_cookie(cookie_object)
        try:
            user = await self._http_client.authenticate()
        except Exception as e:
            log.error(repr(e))
            raise InvalidCredentials
        return Authentication(user_id=user["rockstar_id"], user_name=user["display_name"])

    async def shutdown(self):
        # At this point, we can write to a file to keep a cached copy of the user's played time.
        # This will prevent the play time from being erased if the user loses authentication.
        if IS_WINDOWS and self.game_time_cache:
            # For the sake of convenience, we will store this file in the user's Documents folder.
            # Obviously, this feature is only compatible with (and relevant for) Windows machines.
            file_location = os.path.join(self.documents_location, "RockstarPlayTimeCache.txt")
            file = open(file_location, "w+")
            file.write("# This file contains a cached copy of the user's play time for the Rockstar plugin for GOG "
                       "Galaxy 2.0.\n")
            file.write("# DO NOT EDIT THIS FILE IN ANY WAY, LEST THE CACHE GETS CORRUPTED AND YOUR PLAY TIME IS LOST!\n"
                       )
            file.write(pickle.dumps(self.game_time_cache).hex())
            file.close()
        await self._http_client.close()
        await super().shutdown()

    def create_total_games_cache(self):
        cache = []
        for title_id in list(games_cache):
            cache.append(self.create_game_from_title_id(title_id))
        return cache

    if ARE_ACHIEVEMENTS_IMPLEMENTED:
        async def get_unlocked_achievements(self, game_id, context):
            # The Social Club API has an authentication endpoint located at https://scapi.rockstargames.com/
            # achievements/awardedAchievements?title=[game-id]&platform=pc&rockstarId=[rockstar-ID], which returns a
            # list of the user's unlocked achievements for the specified game. It uses the Social Club standard for
            # authentication (a request header named Authorization containing "Bearer [Bearer-Token]").

            title_id = get_game_title_id_from_ros_title_id(game_id)
            if games_cache[title_id]["achievementId"] is None or \
                    (games_cache[title_id]["isPreOrder"]):
                return []
            log.debug("ROCKSTAR_ACHIEVEMENT_CHECK: Beginning achievements check for " +
                      title_id + " (Achievement ID: " + get_achievement_id_from_ros_title_id(game_id) + ")...")
            # Now, we can begin getting the user's achievements for the specified game.
            achievement_id = get_achievement_id_from_ros_title_id(game_id)
            url = (f"https://scapi.rockstargames.com/achievements/awardedAchievements?title={achievement_id}"
                   f"&platform=pc&rockstarId={self._http_client.get_rockstar_id()}")
            unlocked_achievements = await self._http_client.get_json_from_request_strict(url)
            achievements_dict = unlocked_achievements["awardedAchievements"]
            achievements_list = []
            for key, value in achievements_dict.items():
                # What if an achievement is added to the Social Club after the cache was already made? In this event, we
                # need to refresh the cache.
                achievement_num = key
                unlock_time = await get_unix_epoch_time_from_date(value["dateAchieved"])
                achievements_list.append(Achievement(unlock_time, achievement_id=achievement_num))
            return achievements_list

    async def get_friends(self) -> List[UserInfo]:
        # The Social Club website returns a list of the current user's friends through the url
        # https://scapi.rockstargames.com/friends/getFriendsFiltered?onlineService=sc&nickname=&pageIndex=0&pageSize=30.
        # The nickname URL parameter is left blank because the website instead uses the bearer token to get the correct
        # information. The last two parameters are of great importance, however. The parameter pageSize determines the
        # number of friends given on that page's list, while pageIndex keeps track of the page that the information is
        # on. The maximum number for pageSize is 30, so that is what we will use to cut down the number of HTTP
        # requests.

        # We first need to get the number of friends.
        url = ("https://scapi.rockstargames.com/friends/getFriendsFiltered?onlineService=sc&nickname=&"
               "pageIndex=0&pageSize=30")
        try:
            current_page = await self._http_client.get_json_from_request_strict(url)
        except TimeoutError:
            log.warning("ROCKSTAR_FRIENDS_TIMEOUT: The request to get the user's friends at page index 0 timed out. "
                        "Returning the cached list...")
            return self.friends_cache
        if LOG_SENSITIVE_DATA:
            log.debug("ROCKSTAR_FRIENDS_REQUEST: " + str(current_page))
        else:
            log.debug("ROCKSTAR_FRIENDS_REQUEST: ***")
        num_friends = current_page['rockstarAccountList']['totalFriends']
        num_pages_required = num_friends / 30 if num_friends % 30 != 0 else (num_friends / 30) - 1

        # Now, we need to get the information about the friends.
        friends_list = current_page['rockstarAccountList']['rockstarAccounts']
        return_list = await self._parse_friends(friends_list)

        # The first page is finished, but now we need to work on any remaining pages.
        if num_pages_required > 0:
            for i in range(1, int(num_pages_required + 1)):
                try:
                    url = ("https://scapi.rockstargames.com/friends/getFriendsFiltered?onlineService=sc&nickname=&"
                           "pageIndex=" + str(i) + "&pageSize=30")
                    for friend in await self._get_friends(url):
                        return_list.append(friend)
                except TimeoutError:
                    log.warning(f"ROCKSTAR_FRIENDS_TIMEOUT: The request to get the user's friends at page index {i} "
                                f"timed out. Returning the cached list...")
                    return self.friends_cache
        return return_list

    async def _get_friends(self, url: str) -> List[UserInfo]:
        try:
            current_page = await self._http_client.get_json_from_request_strict(url)
        except TimeoutError:
            raise
        friends_list = current_page['rockstarAccountList']['rockstarAccounts']
        return await self._parse_friends(friends_list)

    async def _parse_friends(self, friends_list: dict) -> List[UserInfo]:
        return_list = []
        for i in range(0, len(friends_list)):
            avatar_uri = f"https://a.rsg.sc/n/{friends_list[i]['displayName'].lower()}/l"
            profile_uri = f"https://socialclub.rockstargames.com/member/{friends_list[i]['displayName']}/"
            friend = UserInfo(user_id=str(friends_list[i]['rockstarId']),
                              user_name=friends_list[i]['displayName'],
                              avatar_url=avatar_uri,
                              profile_url=profile_uri)
            return_list.append(friend)
            for cached_friend in self.friends_cache:
                if cached_friend.user_id == friend.user_id:
                    break
            else:  # An else-statement occurs after a for-statement if the latter finishes WITHOUT breaking.
                self.friends_cache.append(friend)
            if LOG_SENSITIVE_DATA:
                log.debug("ROCKSTAR_FRIEND: Found " + friend.user_name + " (Rockstar ID: " +
                          str(friend.user_id) + ")")
            else:
                log.debug(f"ROCKSTAR_FRIEND: Found {friend.user_name[:1]}*** (Rockstar ID: ***)")
        return return_list

    async def get_owned_games_online(self):
        # Get the list of games_played from https://socialclub.rockstargames.com/ajax/getGoogleTagManagerSetupData.
        owned_title_ids = []
        online_check_success = True
        self.last_online_game_check = time()
        try:
            played_games = await self._http_client.get_played_games()
            for game in played_games:
                owned_title_ids.append(game)
                log.debug("ROCKSTAR_ONLINE_GAME: Found played game " + game + "!")
        except Exception as e:
            log.error("ROCKSTAR_PLAYED_GAMES_ERROR: The exception " + repr(e) + " was thrown when attempting to get"
                      " the user's played games online. Falling back to log file check...")
            online_check_success = False
        return owned_title_ids, online_check_success

    async def get_owned_games(self, owned_title_ids=None, online_check_success=False):
        # Here is the actual implementation of getting the user's owned games:
        # -Get the list of games_played from rockstargames.com/auth/get-user.json.
        #   -If possible, use the launcher log to confirm which games are actual launcher games and which are
        #   Steam/Retail games.
        #   -If it is not possible to use the launcher log, then just use the list provided by the website.
        if owned_title_ids is None:
            owned_title_ids = []
        if not self.is_authenticated():
            raise AuthenticationRequired()

        # The log is in the Documents folder.
        current_log_count = 0
        log_file = None
        log_file_append = ""
        # The Rockstar Games Launcher generates 10 log files before deleting them in a FIFO fashion. Old log files are
        # given a number ranging from 1 to 9 in their name. In case the first log file does not have all of the games,
        # we need to check the other log files, if possible.
        while current_log_count < 10:
            # We need to prevent the log file check for Mac users.
            if not IS_WINDOWS:
                break
            try:
                if current_log_count != 0:
                    log_file_append = ".0" + str(current_log_count)
                log_file = os.path.join(self.documents_location, "Rockstar Games\\Launcher\\launcher" + log_file_append
                                        + ".log")
                if LOG_SENSITIVE_DATA:
                    log.debug("ROCKSTAR_LOG_LOCATION: Checking the file " + log_file + "...")
                else:
                    log.debug("ROCKSTAR_LOG_LOCATION: Checking the file ***...")  # The path to the Launcher log file
                    # likely contains the user's PC profile name (C:\Users\[Name]\Documents...).
                owned_title_ids = await self.parse_log_file(log_file, owned_title_ids, online_check_success)
                break
            except NoGamesInLogException:
                log.warning("ROCKSTAR_LOG_WARNING: There are no owned games listed in " + str(log_file) + ". Moving to "
                            "the next log file...")
                current_log_count += 1
            except NoLogFoundException:
                log.warning("ROCKSTAR_LAST_LOG_REACHED: There are no more log files that can be found and/or read "
                            "from. Assuming that the online list is correct...")
                break
            except Exception:
                # This occurs after ROCKSTAR_LOG_ERROR.
                break
        if current_log_count == 10:
            log.warning("ROCKSTAR_LAST_LOG_REACHED: There are no more log files that can be found and/or read "
                        "from. Assuming that the online list is correct...")

        for title_id in owned_title_ids:
            game = self.create_game_from_title_id(title_id)
            if game not in self.owned_games_cache:
                log.debug("ROCKSTAR_ADD_GAME: Adding " + title_id + " to owned games cache...")
                self.owned_games_cache.append(game)

        return self.owned_games_cache

    if IS_WINDOWS:
        async def get_local_size(self, game_id: str, context: Any) -> Optional[int]:
            title_id = get_game_title_id_from_ros_title_id(game_id)
            game_status = self.check_game_status(title_id)
            if game_status.local_game_state == LocalGameState.None_:
                return 0
            return await self._local_client.get_game_size_in_bytes(title_id)

    @staticmethod
    async def parse_log_file(log_file, owned_title_ids, online_check_success):
        owned_title_ids_ = owned_title_ids
        checked_games_count = 0
        total_games_count = len(games_cache) - 1  # We need to subtract 1 to account for the Launcher.
        if os.path.exists(log_file):
            with FileReadBackwards(log_file, encoding="utf-8") as frb:
                while checked_games_count < total_games_count:
                    try:
                        line = frb.readline()
                    except UnicodeDecodeError:
                        log.warning("ROCKSTAR_LOG_UNICODE_WARNING: An invalid Unicode character was found in the line "
                                    + line + ". Continuing to next line...")
                        continue
                    except Exception as e:
                        log.error("ROCKSTAR_LOG_ERROR: Reading " + line + " from the log file resulted in the "
                                  "exception " + repr(e) + " being thrown. Using the online list... (Please report "
                                  "this issue on the plugin's GitHub page!)")
                        raise
                    if not line:
                        log.error("ROCKSTAR_LOG_FINISHED_ERROR: The entire log file was read, but all of the games "
                                  "could not be accounted for. Proceeding to import the games that have been "
                                  "confirmed...")
                        raise NoGamesInLogException()
                    # We need to do two main things with the log file:
                    # 1. If a game is present in owned_title_ids but not owned according to the log file, then it is
                    #    assumed to be a non-Launcher game, and is removed from the list.
                    # 2. If a game is owned according to the log file but is not already present in owned_title_ids,
                    #    then it is assumed that the user has purchased the game on the Launcher, but has not yet played
                    #    it. In this case, the game will be added to owned_title_ids.
                    if ("launcher" not in line) and ("on branch " in line):  # Found a game!
                        # Each log line for a title branch report describes the title id of the game starting at
                        # character 65. Interestingly, the lines all have the same colon as character 75. This implies
                        # that this format was intentionally done by Rockstar, so they likely will not change it anytime
                        # soon.
                        title_id = line[65:75].strip()
                        log.debug("ROCKSTAR_LOG_GAME: The game with title ID " + title_id + " is owned!")
                        if title_id not in owned_title_ids_:
                            if online_check_success is True:
                                # Case 2: The game is owned, but has not been played.
                                log.warning("ROCKSTAR_UNPLAYED_GAME: The game with title ID " + title_id +
                                            " is owned, but it has never been played!")
                            owned_title_ids_.append(title_id)
                        checked_games_count += 1
                    elif "no branches!" in line:
                        title_id = line[65:75].strip()
                        if title_id in owned_title_ids_:
                            # Case 1: The game is not actually owned on the launcher.
                            log.warning("ROCKSTAR_FAKE_GAME: The game with title ID " + title_id + " is not owned on "
                                        "the Rockstar Games Launcher!")
                            owned_title_ids_.remove(title_id)
                        checked_games_count += 1
                    if checked_games_count == total_games_count:
                        break
            return owned_title_ids_
        else:
            raise NoLogFoundException()

    async def get_game_time(self, game_id, context):
        # Although the Rockstar Games Launcher does track the played time for each game, there is currently no known
        # method for accessing this information. As such, game time will be recorded when games are launched through the
        # Galaxy 2.0 client.

        title_id = get_game_title_id_from_ros_title_id(game_id)
        if title_id in self.running_games_info_list:
            # The game is running (or has been running).
            start_time = self.running_games_info_list[title_id].get_start_time()
            self.running_games_info_list[title_id].update_start_time()
            current_time = datetime.datetime.now().timestamp()
            minutes_passed = (current_time - start_time) / 60
            if not self.running_games_info_list[title_id].get_pid():
                # The PID has been set to None, which means that the game has exited (see self.check_game_status). Now
                # that the start time is recorded, the game can be safely removed from the list of running games.
                del self.running_games_info_list[title_id]
            if self.game_time_cache[title_id]['time_played']:
                # The game has been played before, so the time will need to be added to the existing cached time.
                total_time_played = self.game_time_cache[title_id]['time_played'] + minutes_passed
                self.game_time_cache[title_id]['time_played'] = total_time_played
                self.game_time_cache[title_id]['last_played'] = current_time
                return GameTime(game_id=game_id, time_played=int(total_time_played), last_played_time=int(current_time))
            else:
                # The game has not been played before, so a new entry in the game_time_cache dictionary must be made.
                self.game_time_cache[title_id] = {
                    'time_played': minutes_passed,
                    'last_played': current_time
                }
                return GameTime(game_id=game_id, time_played=int(minutes_passed), last_played_time=int(current_time))
        else:
            # The game is no longer running (and there is no relevant entry in self.running_games_info_list).
            if title_id not in self.game_time_cache:
                self.game_time_cache[title_id] = {
                    'time_played': None,
                    'last_played': None
                }
            return GameTime(game_id=game_id, time_played=self.game_time_cache[title_id]['time_played'],
                            last_played_time=self.game_time_cache[title_id]['last_played'])

    def game_times_import_complete(self):
        log.debug("ROCKSTAR_GAME_TIME: Pushing the cache of played game times to the persistent cache...")
        self.persistent_cache['game_time_cache'] = pickle.dumps(self.game_time_cache).hex()
        self.push_cache()

    def get_friend_user_name_from_user_id(self, user_id):
        for friend in self.friends_cache:
            if friend.user_id == user_id:
                return friend.user_name
        return None

    async def prepare_user_presence_context(self, user_id_list: List[str]) -> Any:
        if CONFIG_OPTIONS['user_presence_mode'] == 2 or CONFIG_OPTIONS['user_presence_mode'] == 3:
            game = "gtav" if CONFIG_OPTIONS['user_presence_mode'] == 2 else "rdr2"
            return await self._http_client.get_json_from_request_strict("https://scapi.rockstargames.com/friends/"
                                                                        f"getFriendsWhoPlay?title={game}&platform=pc")
        return None

    async def get_user_presence(self, user_id, context):
        # For user presence settings 2 and 3, we need to verify that the specified user owns the game to get their
        # stats.

        friend_name = self.get_friend_user_name_from_user_id(user_id)
        if LOG_SENSITIVE_DATA:
            log.debug(f"ROCKSTAR_PRESENCE_START: Getting user presence for {friend_name} (Rockstar ID: {user_id})...")
        if context:
            for player in context['onlineFriends']:
                if player['userId'] == user_id:
                    # This user owns the specified game, so we can return this information.
                    break
            else:
                # The user does not own the specified game, so we need to return their last played game.
                return await self._http_client.get_last_played_game(friend_name)
        if CONFIG_OPTIONS['user_presence_mode'] == 0:
            self.presence_cache[user_id] = UserPresence(presence_state=PresenceState.Unknown)
            # 0 - Disable User Presence
        else:
            switch = {
                1: self._http_client.get_last_played_game(friend_name),
                # 1 - Get Last Played Game
                2: self._http_client.get_gta_online_stats(user_id, friend_name),
                # 2 - Get GTA Online Character Stats
                3: self._http_client.get_rdo_stats(user_id, friend_name)
                # 3 - Get Red Dead Online Character Stats
            }
            self.presence_cache[user_id] = await asyncio.create_task(switch[CONFIG_OPTIONS['user_presence_mode']])
        return self.presence_cache[user_id]

    async def open_rockstar_browser(self):
        # This method allows the user to install the Rockstar Games Launcher, if it is not already installed.
        url = "https://www.rockstargames.com/downloads"

        log.info(f"Opening Rockstar website {url}")
        webbrowser.open(url)

    def check_game_status(self, title_id):
        state = LocalGameState.None_

        game_installed = self._local_client.get_path_to_game(title_id)
        if game_installed:
            state |= LocalGameState.Installed

            if (title_id in self.running_games_info_list and
                    check_if_process_exists(self.running_games_info_list[title_id].get_pid())):
                state |= LocalGameState.Running
            elif title_id in self.running_games_info_list:
                # We will leave the info in the list, because it still contains the game start time for game time
                # tracking. However, we will set the PID to None to indicate that the game has been closed.
                self.running_games_info_list[title_id].clear_pid()

        return LocalGame(str(self.games_cache[title_id]["rosTitleId"]), state)

    if IS_WINDOWS:
        async def get_local_games(self):
            # Since the API requires that get_local_games returns a list of LocalGame objects, local_list is the value
            # that needs to be returned. However, for internal use (the self.local_games_cache field), the dictionary
            # local_games is used for greater flexibility.
            local_games = {}
            local_list = []
            for game in self.total_games_cache:
                title_id = get_game_title_id_from_ros_title_id(str(game.game_id))
                local_game = self.check_game_status(title_id)
                local_games[title_id] = local_game
                local_list.append(local_game)
            self.local_games_cache = local_games
            log.debug(f"ROCKSTAR_INSTALLED_GAMES: {local_games}")
            return local_list

    async def check_for_new_games(self):
        self.checking_for_new_games = True
        # The Social Club prevents the user from making too many requests in a given time span to prevent a denial of
        # service attack. As such, we need to limit online checking to every 5 minutes. For Windows devices, log file
        # checks will still occur every minute, but for other users, checking games only happens every 5 minutes.
        owned_title_ids = None
        online_check_success = False
        if not self.last_online_game_check or time() >= self.last_online_game_check + 300:
            owned_title_ids, online_check_success = await self.get_owned_games_online()
        elif IS_WINDOWS:
            log.debug("ROCKSTAR_SC_ONLINE_GAMES_SKIP: No attempt has been made to scrape the user's games from the "
                      "Social Club, as it has not been 5 minutes since the last check.")
        await self.get_owned_games(owned_title_ids, online_check_success)
        await asyncio.sleep(60 if IS_WINDOWS else 300)
        self.checking_for_new_games = False

    async def check_game_statuses(self):
        self.updating_game_statuses = True

        for title_id, current_local_game in self.local_games_cache.items():
            new_local_game = self.check_game_status(title_id)
            if new_local_game != current_local_game:
                log.debug(f"ROCKSTAR_LOCAL_CHANGE: The status for {title_id} has changed from: {current_local_game} to "
                          f"{new_local_game}.")
                self.update_local_game_status(new_local_game)
                self.local_games_cache[title_id] = new_local_game

        await asyncio.sleep(5)
        self.updating_game_statuses = False

    def list_running_game_pids(self):
        info_list = []
        for key, value in self.running_games_info_list.items():
            info_list.append(value.get_pid())
        return str(info_list)

    if IS_WINDOWS:
        async def launch_platform_client(self):
            if not self._local_client.get_local_launcher_path():
                await self.open_rockstar_browser()
                return

            pid = await self._local_client.launch_game_from_title_id("launcher")
            if not pid:
                log.warning("ROCKSTAR_LAUNCHER_FAILED: The Rockstar Games Launcher could not be launched!")

    if IS_WINDOWS:
        async def shutdown_platform_client(self):
            if not self._local_client.get_local_launcher_path():
                await self.open_rockstar_browser()
                return

            await self._local_client.kill_launcher()

    if IS_WINDOWS:
        async def launch_game(self, game_id):
            if not self._local_client.get_local_launcher_path():
                await self.open_rockstar_browser()
                return

            title_id = get_game_title_id_from_ros_title_id(game_id)
            game_pid = await self._local_client.launch_game_from_title_id(title_id)
            if game_pid:
                self.running_games_info_list[title_id] = RunningGameInfo()
                self.running_games_info_list[title_id].set_info(game_pid)
                log.debug(f"ROCKSTAR_PIDS: {self.list_running_game_pids()}")
                local_game = LocalGame(game_id, LocalGameState.Running | LocalGameState.Installed)
                self.update_local_game_status(local_game)
                self.local_games_cache[title_id] = local_game
            else:
                log.error(f'cannot start game: {title_id}')

    if IS_WINDOWS:
        async def install_game(self, game_id):
            if not self._local_client.get_local_launcher_path():
                await self.open_rockstar_browser()
                return

            title_id = get_game_title_id_from_ros_title_id(game_id)
            log.debug("ROCKSTAR_INSTALL_REQUEST: Requesting to install " + title_id + "...")
            # There is no need to check if the game is a pre-order, since the InstallLocation registry key will be
            # unavailable if it is.
            self._local_client.install_game_from_title_id(title_id)

    if IS_WINDOWS:
        async def uninstall_game(self, game_id):
            if not self._local_client.get_local_launcher_path():
                await self.open_rockstar_browser()
                return

            title_id = get_game_title_id_from_ros_title_id(game_id)
            log.debug("ROCKSTAR_UNINSTALL_REQUEST: Requesting to uninstall " + title_id + "...")
            self._local_client.uninstall_game_from_title_id(title_id)

    def create_game_from_title_id(self, title_id):
        return Game(str(self.games_cache[title_id]["rosTitleId"]), self.games_cache[title_id]["friendlyName"], None,
                    self.games_cache[title_id]["licenseInfo"])

    def tick(self):
        if not self.is_authenticated():
            return
        if not self.checking_for_new_games:
            log.debug("Checking for new games...")
            asyncio.create_task(self.check_for_new_games())
        if not self.updating_game_statuses and IS_WINDOWS:
            log.debug("Checking local game statuses...")
            asyncio.create_task(self.check_game_statuses())
Beispiel #12
0
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 MinecraftPlugin(Plugin):
    def __init__(self, reader, writer, token):
        super().__init__(Platform.Minecraft, __version__, reader, writer,
                         token)
        self.local_client = LocalClient()
        self.minecraft_launcher = None
        self.minecraft_uninstall_command = None
        self.minecraft_installation_status = LocalGameState.None_
        self.minecraft_running_check = None
        self.tick_count = 0

    async def authenticate(self, stored_credentials=None):
        self.store_credentials({'dummy': 'dummy'})
        return Authentication(user_id='Minecraft_ID',
                              user_name='Minecraft Player')

    async def get_owned_games(self):
        return [
            Game('1', 'Minecraft', None,
                 LicenseInfo(LicenseType.SinglePurchase))
        ]

    async def get_local_games(self):
        self.minecraft_launcher = self.local_client.get_minecraft_launcher_path(
        )
        if self.minecraft_launcher:
            return [LocalGame('1', LocalGameState.Installed)]
        return []

    async def install_game(self, game_id):
        if sys.platform == 'win32':
            r = requests.get(
                "https://launcher.mojang.com/download/MinecraftInstaller.msi")
            installer_path = os.path.join(tempfile.gettempdir(),
                                          'MinecraftInstaller.msi')
            open(installer_path, 'wb').write(r.content)
            subprocess.Popen(installer_path, shell=True)
        else:
            r = requests.get(
                "https://launcher.mojang.com/download/Minecraft.dmg")
            installer_path = os.path.join(tempfile.gettempdir(),
                                          'Minecraft.dmg')
            open(installer_path, 'wb').write(r.content)

            subprocess.Popen("open " + installer_path, shell=True)

    async def launch_game(self, game_id):
        if sys.platform == 'win32':
            cmd = f'"{self.minecraft_launcher}"'
            log.info(f"Launching minecraft by command {cmd}")
            subprocess.Popen(cmd)
        else:
            cmd = f"open {self.minecraft_launcher}"
            log.info(f"Launching minecraft by command {cmd}")
            subprocess.Popen(cmd, shell=True)

    async def uninstall_game(self, game_id):
        if sys.platform == 'win32':
            if not self.minecraft_uninstall_command:
                self.minecraft_uninstall_command = self.local_client.find_minecraft_uninstall_command(
                )
            if self.minecraft_uninstall_command:
                log.info(f"calling {self.minecraft_uninstall_command}")
                subprocess.Popen(f'{self.minecraft_uninstall_command}')
            else:
                log.error("Unable to find minecraft uninstall command")
        else:
            shutil.rmtree(self.minecraft_launcher)

    def tick(self):
        potential_path = self.local_client.get_minecraft_launcher_path()

        if potential_path and not self.minecraft_launcher:
            self.minecraft_launcher = potential_path
            self.update_local_game_status(
                LocalGame('1', LocalGameState.Installed))
            self.minecraft_uninstall_command = self.local_client.find_minecraft_uninstall_command(
            )
        elif not potential_path and self.minecraft_launcher:
            self.minecraft_launcher = None
            self.update_local_game_status(LocalGame('1', LocalGameState.None_))
            self.minecraft_uninstall_command = None

        self.tick_count += 1
        if self.local_client.running_process and (
                not self.minecraft_running_check
                or self.minecraft_running_check.done()):
            self.local_client.is_minecraft_still_running()
        elif self.tick_count % 5 == 0:
            if self.minecraft_launcher and (
                    not self.minecraft_running_check
                    or self.minecraft_running_check.done()):
                self.minecraft_running_check = asyncio.create_task(
                    self.local_client.was_minecraft_launched())

        if self.minecraft_launcher and self.local_client.running_process and self.minecraft_installation_status != LocalGameState.Installed | LocalGameState.Running:
            self.minecraft_installation_status = LocalGameState.Installed | LocalGameState.Running
            self.update_local_game_status(
                LocalGame('1',
                          LocalGameState.Installed | LocalGameState.Running))
        if self.minecraft_launcher and not self.local_client.running_process and self.minecraft_installation_status == LocalGameState.Installed | LocalGameState.Running:
            self.minecraft_installation_status = LocalGameState.Installed
            self.update_local_game_status(
                LocalGame('1', LocalGameState.Installed))

    def shutdown(self):
        pass
 def __init__(self, reader, writer, token):
     super().__init__(Platform(manifest['platform']), manifest['version'],
                      reader, writer, token)
     self._api = ApiClient(self.store_credentials, self.lost_authentication)
     self._local = LocalClient()
Beispiel #15
0
class BethesdaPlugin(Plugin):
    def __init__(self, reader, writer, token):
        super().__init__(Platform.Bethesda, __version__, reader, writer, token)
        self._http_client = AuthenticatedHttpClient(self.store_credentials)
        self.bethesda_client = BethesdaClient(self._http_client)
        self.local_client = LocalClient()
        self.products_cache = product_cache
        self.owned_games_cache = None

        self._asked_for_local = False

        self.update_game_running_status_task = None
        self.update_game_installation_status_task = None
        self.check_for_new_games_task = None
        self.running_games = {}
        self.launching_lock = None

    async def authenticate(self, stored_credentials=None):
        if not stored_credentials:
            return NextStep(
                "web_session",
                AUTH_PARAMS,
                cookies=[Cookie("passedICO", "true", ".bethesda.net")])
        try:
            log.info("Got stored credentials")
            cookies = pickle.loads(
                bytes.fromhex(stored_credentials['cookie_jar']))
            cookies_parsed = []
            for cookie in cookies:
                if cookie.key in cookies_parsed and cookie.domain:
                    self._http_client.update_cookies(
                        {cookie.key: cookie.value})
                elif cookie.key not in cookies_parsed:
                    self._http_client.update_cookies(
                        {cookie.key: cookie.value})
                cookies_parsed.append(cookie.key)

            log.info("Finished parsing stored credentials, authenticating")
            user = await self._http_client.authenticate()

            return Authentication(user_id=user['user_id'],
                                  user_name=user['display_name'])
        except Exception as e:
            log.error(
                f"Couldn't authenticate with stored credentials {repr(e)}")
            raise InvalidCredentials()

    async def pass_login_credentials(self, step, credentials, cookies):
        cookiez = {}
        for cookie in cookies:
            cookiez[cookie['name']] = cookie['value']
        self._http_client.update_cookies(cookiez)

        try:
            user = await self._http_client.authenticate()
        except Exception as e:
            log.error(repr(e))
            raise InvalidCredentials()

        return Authentication(user_id=user['user_id'],
                              user_name=user['display_name'])

    async def get_owned_games(self):
        owned_ids = []
        matched_ids = []
        games_to_send = []
        pre_orders = []
        try:
            owned_ids = await self.bethesda_client.get_owned_ids()
        except UnknownError as e:
            log.warning(f"No owned games detected {repr(e)}")

        log.info(f"Owned Ids: {owned_ids}")

        if owned_ids:
            for entitlement_id in owned_ids:
                for product in self.products_cache:
                    if 'reference_id' in self.products_cache[product]:
                        for reference_id in self.products_cache[product][
                                'reference_id']:
                            if entitlement_id in reference_id:
                                self.products_cache[product]['owned'] = True
                                matched_ids.append(entitlement_id)
            pre_orders = set(owned_ids) - set(matched_ids)

        for pre_order in pre_orders:
            pre_order_details = await self.bethesda_client.get_game_details(
                pre_order)
            if pre_order_details and 'Entry' in pre_order_details:
                for entry in pre_order_details['Entry']:
                    if 'fields' in entry and 'productName' in entry['fields']:
                        if entry['fields'][
                                'productName'] in self.products_cache:
                            self.products_cache[
                                entry['fields']['productName']]['owned'] = True
                        else:
                            games_to_send.append(
                                Game(
                                    pre_order, entry['fields']['productName'] +
                                    " (Pre Order)", None,
                                    LicenseInfo(LicenseType.SinglePurchase)))
                        break

        for product in self.products_cache:
            if self.products_cache[product]["owned"] and self.products_cache[
                    product]["free_to_play"]:
                games_to_send.append(
                    Game(self.products_cache[product]['local_id'], product,
                         None, LicenseInfo(LicenseType.FreeToPlay)))
            elif self.products_cache[product]["owned"]:
                games_to_send.append(
                    Game(self.products_cache[product]['local_id'], product,
                         None, LicenseInfo(LicenseType.SinglePurchase)))

        log.info(f"Games to send (with free games): {games_to_send}")
        self.owned_games_cache = games_to_send
        return games_to_send

    async def get_local_games(self):
        local_games = []
        installed_products = self.local_client.get_installed_games(
            self.products_cache)
        log.info(f"Installed products {installed_products}")
        for product in self.products_cache:
            for installed_product in installed_products:
                if installed_products[
                        installed_product] == self.products_cache[product][
                            'local_id']:
                    self.products_cache[product]['installed'] = True
                    local_games.append(
                        LocalGame(installed_products[installed_product],
                                  LocalGameState.Installed))

        self._asked_for_local = True
        log.info(f"Returning local games {local_games}")
        return local_games

    async def install_game(self, game_id):
        if sys.platform != 'win32':
            log.error(f"Incompatible platform {sys.platform}")
            return

        if not self.local_client.is_installed:
            await self._open_betty_browser()
            return

        if self.local_client.is_running:
            self.local_client.focus_client_window()
            await self.launch_game(game_id)
        else:
            uuid = None
            for product in self.products_cache:

                if self.products_cache[product]['local_id'] == game_id:
                    if self.products_cache[product]['installed']:
                        log.warning(
                            "Got install on already installed game, launching")
                        return await self.launch_game(game_id)
                    uuid = "\"" + self.products_cache[product]['uuid'] + "\""
            cmd = "\"" + self.local_client.client_exe_path + "\"" + f" --installproduct={uuid}"
            log.info(f"Calling install game with command {cmd}")
            subprocess.Popen(cmd, shell=True)

    async def launch_game(self, game_id):
        if sys.platform != 'win32':
            log.error(f"Incompatible platform {sys.platform}")
            return
        if not self.local_client.is_installed:
            await self._open_betty_browser()
            return

        for product in self.products_cache:
            if self.products_cache[product]['local_id'] == game_id:
                if not self.products_cache[product]['installed']:
                    if not self.local_client.is_running:
                        log.warning(
                            "Got launch on a not installed game, installing")
                        return await self.install_game(game_id)
                else:
                    if not self.local_client.is_running:
                        self.launching_lock = time.time() + 45
                    else:
                        self.launching_lock = time.time() + 30
                    self.running_games[game_id] = None
                    self.update_local_game_status(
                        LocalGame(
                            game_id,
                            LocalGameState.Installed | LocalGameState.Running))
                    self.update_game_running_status_task.cancel()

        log.info(f"Calling launch command for id {game_id}")
        cmd = f"start bethesdanet://run/{game_id}"
        subprocess.Popen(cmd, shell=True)

    async def uninstall_game(self, game_id):
        if sys.platform != 'win32':
            log.error(f"Incompatible platform {sys.platform}")
            return

        if not self.local_client.is_installed:
            await self._open_betty_browser()
            return

        for product in self.products_cache:
            if self.products_cache[product]['local_id'] == game_id:
                if not self.products_cache[product]['installed']:
                    return

        log.info(f"Calling uninstall command for id {game_id}")
        cmd = f"start bethesdanet://uninstall/{game_id}"

        subprocess.Popen(cmd, shell=True)

        if self.local_client.is_running:
            await asyncio.sleep(
                2)  # QOL, bethesda slowly reacts to uninstall command,
            self.local_client.focus_client_window()

    async def _open_betty_browser(self):
        url = "https://bethesda.net/game/bethesda-launcher"
        log.info(f"Opening Bethesda website on url {url}")
        webbrowser.open(url)

    async def _heavy_installation_status_check(self):
        installed_products = self.local_client.get_installed_games(
            self.products_cache)
        products_cache_installed_products = {}

        for product in self.products_cache:
            if self.products_cache[product]['installed']:
                products_cache_installed_products[
                    product] = self.products_cache[product]['local_id']

        for installed_product in installed_products:
            if installed_product not in products_cache_installed_products:
                self.products_cache[installed_product]["installed"] = True
                self.update_local_game_status(
                    LocalGame(installed_products[installed_product],
                              LocalGameState.Installed))

        for installed_product in products_cache_installed_products:
            if installed_product not in installed_products:
                self.products_cache[installed_product]["installed"] = False
                self.update_local_game_status(
                    LocalGame(
                        products_cache_installed_products[installed_product],
                        LocalGameState.None_))

    def _light_installation_status_check(self):
        for local_game in self.local_client.local_games_cache:
            local_game_installed = self.local_client.is_local_game_installed(
                self.local_client.local_games_cache[local_game])
            if local_game_installed and not self.products_cache[local_game][
                    "installed"]:
                self.products_cache[local_game]["installed"] = True
                self.update_local_game_status(
                    LocalGame(
                        self.local_client.local_games_cache[local_game]
                        ['local_id'], LocalGameState.Installed))
            elif not local_game_installed and self.products_cache[local_game][
                    "installed"]:
                self.products_cache[local_game]["installed"] = False
                self.update_local_game_status(
                    LocalGame(
                        self.local_client.local_games_cache[local_game]
                        ['local_id'], LocalGameState.None_))

    async def update_game_installation_status(self):

        if self.local_client.clientgame_changed():
            await asyncio.sleep(1)
            await self._heavy_installation_status_check()
        else:
            self._light_installation_status_check()

    async def update_game_running_status(self):

        process_iter_interval = 0.10
        dont_downgrade_status = False

        if self.launching_lock and self.launching_lock >= time.time():
            dont_downgrade_status = True
            process_iter_interval = 0.01

        for running_game in self.running_games.copy():
            if not self.running_games[running_game] and dont_downgrade_status:
                log.info(f"Found 'just launched' game {running_game}")
                continue
            elif not self.running_games[running_game]:
                log.info(
                    f"Found 'just launched' game but its still without pid and its time run out {running_game}"
                )
                self.running_games.pop(running_game)
                self.update_local_game_status(
                    LocalGame(running_game, LocalGameState.Installed))
                continue

            if self.running_games[running_game].is_running():
                return
            self.running_games.pop(running_game)
            self.update_local_game_status(
                LocalGame(running_game, LocalGameState.Installed))

        for process in psutil.process_iter(attrs=['name'], ad_value=''):
            await asyncio.sleep(process_iter_interval)
            for local_game in self.local_client.local_games_cache:
                try:
                    if process.name().lower(
                    ) in self.local_client.local_games_cache[local_game][
                            'execs']:
                        log.info(f"Found a running game! {local_game}")
                        local_id = self.local_client.local_games_cache[
                            local_game]['local_id']
                        if local_id not in self.running_games:
                            self.update_local_game_status(
                                LocalGame(
                                    local_id, LocalGameState.Installed
                                    | LocalGameState.Running))
                        self.running_games[local_id] = process
                        return
                except (psutil.AccessDenied, psutil.NoSuchProcess):
                    break

        await asyncio.sleep(3)

    async def check_for_new_games(self):
        owned_games = await self.get_owned_games()
        for owned_game in owned_games:
            if owned_game not in self.owned_games_cache:
                self.add_game(owned_game)
        self.owned_games_cache = owned_games
        await asyncio.sleep(60)

    def tick(self):

        if self._asked_for_local and (
                not self.update_game_installation_status_task
                or self.update_game_installation_status_task.done()):
            self.update_game_installation_status_task = asyncio.create_task(
                self.update_game_installation_status())

        if self._asked_for_local and (
                not self.update_game_running_status_task
                or self.update_game_running_status_task.done()):
            self.update_game_running_status_task = asyncio.create_task(
                self.update_game_running_status())

        if self.owned_games_cache and (not self.check_for_new_games_task or
                                       self.check_for_new_games_task.done()):
            self.check_for_new_games_task = asyncio.create_task(
                self.check_for_new_games())

    def shutdown(self):
        asyncio.create_task(self._http_client.close())