예제 #1
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()
예제 #2
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())