Ejemplo n.º 1
0
    def __init__(self):
        self.log = logging.getLogger('Core')
        self.egs = EPCAPI()
        self.lgd = LGDLFS()

        # epic lfs only works on Windows right now
        if os.name == 'nt':
            self.egl = EPCLFS()
        else:
            self.egl = None
Ejemplo n.º 2
0
class LegendaryCore:
    """
    LegendaryCore handles most of the lower level interaction with
    the downloader, lfs, and api components to make writing CLI/GUI
    code easier and cleaner and avoid duplication.
    """
    def __init__(self):
        self.log = logging.getLogger('Core')
        self.egs = EPCAPI()
        self.lgd = LGDLFS()

        # epic lfs only works on Windows right now
        if os.name == 'nt':
            self.egl = EPCLFS()
        else:
            self.egl = None

    def auth(self, username, password):
        """
        Attempts direct non-web login, raises CaptchaError if manual login is required

        :param username:
        :param password:
        :return:
        """
        raise NotImplementedError

    def auth_code(self, code) -> bool:
        """
        Handles authentication via exchange code (either retrieved manually or automatically)
        """
        try:
            self.lgd.userdata = self.egs.start_session(exchange_token=code)
            return True
        except Exception as e:
            self.log.error(f'Logging in failed with {e!r}, please try again.')
            return False

    def auth_import(self) -> bool:
        """Import refresh token from EGL installation and use it for logging in"""
        self.egl.read_config()
        remember_me_data = self.egl.config.get('RememberMe', 'Data')
        re_data = json.loads(b64decode(remember_me_data))[0]
        if 'Token' not in re_data:
            raise ValueError('No login session in config')
        refresh_token = re_data['Token']
        try:
            self.lgd.userdata = self.egs.start_session(
                refresh_token=refresh_token)
            return True
        except Exception as e:
            self.log.error(f'Logging in failed with {e!r}, please try again.')
            return False

    def login(self) -> bool:
        """
        Attempts logging in with existing credentials.

        raises ValueError if no existing credentials or InvalidCredentialsError if the API return an error
        """
        if not self.lgd.userdata:
            raise ValueError('No saved credentials')

        if self.lgd.userdata['expires_at']:
            dt_exp = datetime.fromisoformat(
                self.lgd.userdata['expires_at'][:-1])
            dt_now = datetime.utcnow()
            td = dt_now - dt_exp

            # if session still has at least 10 minutes left we can re-use it.
            if dt_exp > dt_now and abs(td.total_seconds()) > 600:
                self.log.info('Trying to re-use existing login session...')
                try:
                    self.egs.resume_session(self.lgd.userdata)
                    return True
                except InvalidCredentialsError as e:
                    self.log.warning(
                        f'Resuming failed due to invalid credentials: {e!r}')
                except Exception as e:
                    self.log.warning(
                        f'Resuming failed for unknown reason: {e!r}')
                # If verify fails just continue the normal authentication process
                self.log.info('Falling back to using refresh token...')

        try:
            self.log.info('Logging in...')
            userdata = self.egs.start_session(
                self.lgd.userdata['refresh_token'])
        except InvalidCredentialsError:
            self.log.error(
                'Stored credentials are no longer valid! Please login again.')
            self.lgd.invalidate_userdata()
            return False
        except HTTPError as e:
            self.log.error(
                f'HTTP request for login failed: {e!r}, please try again later.'
            )
            return False

        self.lgd.userdata = userdata
        return True

    def get_assets(self,
                   update_assets=False,
                   platform_override=None) -> List[GameAsset]:
        # do not save and always fetch list when platform is overriden
        if platform_override:
            return [
                GameAsset.from_egs_json(a)
                for a in self.egs.get_game_assets(platform=platform_override)
            ]

        if not self.lgd.assets or update_assets:
            self.lgd.assets = [
                GameAsset.from_egs_json(a) for a in self.egs.get_game_assets()
            ]

        return self.lgd.assets

    def get_asset(self, app_name, update=False) -> GameAsset:
        if update:
            self.get_assets(update_assets=True)

        return next(i for i in self.lgd.assets if i.app_name == app_name)

    def get_game(self, app_name, update_meta=False) -> Game:
        if update_meta:
            self.get_game_list(True)
        return self.lgd.get_game_meta(app_name)

    def get_game_list(self, update_assets=True) -> List[Game]:
        return self.get_game_and_dlc_list(update_assets=update_assets)[0]

    def get_game_and_dlc_list(self,
                              update_assets=True,
                              platform_override=None,
                              skip_ue=True) -> (List[Game], Dict[str, Game]):
        _ret = []
        _dlc = defaultdict(list)

        for ga in self.get_assets(update_assets=update_assets,
                                  platform_override=platform_override):
            if ga.namespace == 'ue' and skip_ue:
                continue

            game = self.lgd.get_game_meta(ga.app_name)
            if not game or (game and game.app_version != ga.build_version
                            and not platform_override):
                if game and game.app_version != ga.build_version and not platform_override:
                    self.log.info(
                        f'Updating meta for {game.app_name} due to build version mismatch'
                    )

                eg_meta = self.egs.get_game_info(ga.namespace,
                                                 ga.catalog_item_id)
                game = Game(app_name=ga.app_name,
                            app_version=ga.build_version,
                            app_title=eg_meta['title'],
                            asset_info=ga,
                            metadata=eg_meta)

                if not platform_override:
                    self.lgd.set_game_meta(game.app_name, game)

            if game.is_dlc:
                _dlc[game.metadata['mainGameItem']['id']].append(game)
            else:
                _ret.append(game)

        return _ret, _dlc

    def get_dlc_for_game(self, app_name):
        game = self.get_game(app_name)
        if game.is_dlc:  # dlc shouldn't have DLC
            return []

        _, dlcs = self.get_game_and_dlc_list(update_assets=False)
        return dlcs[game.asset_info.catalog_item_id]

    def get_installed_list(self) -> List[InstalledGame]:
        return [g for g in self.lgd.get_installed_list() if not g.is_dlc]

    def get_installed_dlc_list(self) -> List[InstalledGame]:
        return [g for g in self.lgd.get_installed_list() if g.is_dlc]

    def get_installed_game(self, app_name) -> InstalledGame:
        return self.lgd.get_installed_game(app_name)

    def get_launch_parameters(self,
                              app_name: str,
                              offline: bool = False,
                              user: str = None,
                              extra_args: list = None) -> (list, str, dict):
        install = self.lgd.get_installed_game(app_name)
        game = self.lgd.get_game_meta(app_name)

        game_token = ''
        if not offline:
            self.log.info('Getting authentication token...')
            game_token = self.egs.get_game_token()['code']
        elif not install.can_run_offline:
            self.log.warning(
                'Game is not approved for offline use and may not work correctly.'
            )

        user_name = self.lgd.userdata['displayName']
        account_id = self.lgd.userdata['account_id']
        if user:
            user_name = user

        game_exe = os.path.join(install.install_path, install.executable)
        working_dir = os.path.split(game_exe)[0]

        params = []

        if os.name != 'nt':
            # check if there's a default override
            wine_binary = self.lgd.config.get('default',
                                              'wine_executable',
                                              fallback='wine')
            # check if there's a game specific override
            wine_binary = self.lgd.config.get(app_name,
                                              'wine_executable',
                                              fallback=wine_binary)
            params.append(wine_binary)

        params.append(game_exe)

        if install.launch_parameters:
            params.extend(shlex.split(install.launch_parameters))

        params.extend([
            '-AUTH_LOGIN=unused', f'-AUTH_PASSWORD={game_token}',
            '-AUTH_TYPE=exchangecode', f'-epicapp={app_name}', '-epicenv=Prod'
        ])

        if install.requires_ot and not offline:
            self.log.info('Getting ownership token.')
            ovt = self.egs.get_ownership_token(game.asset_info.namespace,
                                               game.asset_info.catalog_item_id)
            ovt_path = os.path.join(
                self.lgd.get_tmp_path(),
                f'{game.asset_info.namespace}{game.asset_info.catalog_item_id}.ovt'
            )
            with open(ovt_path, 'wb') as f:
                f.write(ovt)
            params.append(f'-epicovt={ovt_path}')

        params.extend([
            '-EpicPortal', f'-epicusername={user_name}',
            f'-epicuserid={account_id}', '-epiclocale=en'
        ])

        if extra_args:
            params.extend(extra_args)

        if config_args := self.lgd.config.get(app_name,
                                              'start_params',
                                              fallback=None):
            params.extend(shlex.split(config_args.strip()))

        # get environment overrides from config
        env = os.environ.copy()
        if f'{app_name}.env' in self.lgd.config:
            env.update(dict(self.lgd.config[f'{app_name}.env']))
        elif 'default.env' in self.lgd.config:
            env.update(dict(self.lgd.config['default.env']))

        return params, working_dir, env