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