Esempio n. 1
0
def launch_game(app_name: str,
                lgd_core: LegendaryCore,
                offline: bool = False,
                skip_version_check: bool = False,
                username_override=None,
                wine_bin: str = None,
                wine_prfix: str = None,
                language: str = None,
                wrapper=None,
                no_wine: bool = os.name == "nt",
                extra: [] = None):
    game = lgd_core.get_installed_game(app_name)
    if not game:
        print("Game not found")
        return None
    if game.is_dlc:
        print("Game is dlc")
        return None
    if not os.path.exists(game.install_path):
        print("Game doesn't exist")
        return None

    if not offline:
        print("logging in")
        if not lgd_core.login():
            return None
        if not skip_version_check and not core.is_noupdate_game(app_name):
            # check updates
            try:
                latest = lgd_core.get_asset(app_name, update=True)
            except ValueError:
                print("Metadata doesn't exist")
                return None
            if latest.build_version != game.version:
                print("Please update game")
                return None
    params, cwd, env = lgd_core.get_launch_parameters(app_name=app_name,
                                                      offline=offline,
                                                      extra_args=extra,
                                                      user=username_override,
                                                      wine_bin=wine_bin,
                                                      wine_pfx=wine_prfix,
                                                      language=language,
                                                      wrapper=wrapper,
                                                      disable_wine=no_wine)
    process = QProcess()
    process.setWorkingDirectory(cwd)
    environment = QProcessEnvironment()
    for e in env:
        environment.insert(e, env[e])
    process.setProcessEnvironment(environment)
    process.start(params[0], params[1:])
    return process
Esempio n. 2
0
class LegendaryCLI:
    def __init__(self):
        self.core = LegendaryCore()
        self.logger = logging.getLogger('cli')
        self.logging_queue = None

    def setup_threaded_logging(self):
        self.logging_queue = MPQueue(-1)
        shandler = logging.StreamHandler()
        sformatter = logging.Formatter(
            '[%(asctime)s] [%(name)s] %(levelname)s: %(message)s')
        shandler.setFormatter(sformatter)
        ql = QueueListener(self.logging_queue, shandler)
        ql.start()
        return ql

    def auth(self, args):
        try:
            logger.info('Testing existing login data if present...')
            if self.core.login():
                logger.info(
                    'Stored credentials are still valid, if you wish to switch to a different'
                    'account, delete ~/.config/legendary/user.json and try again.'
                )
                exit(0)
        except ValueError:
            pass
        except InvalidCredentialsError:
            logger.error(
                'Stored credentials were found but were no longer valid. Continuing with login...'
            )
            self.core.lgd.invalidate_userdata()

        if os.name == 'nt' and args.import_egs_auth:
            logger.info('Importing login session from the Epic Launcher...')
            try:
                if self.core.auth_import():
                    logger.info(
                        'Successfully imported login session from EGS!')
                    logger.info(
                        f'Now logged in as user "{self.core.lgd.userdata["displayName"]}"'
                    )
                    exit(0)
                else:
                    logger.warning(
                        'Login session from EGS seems to no longer be valid.')
                    exit(1)
            except ValueError:
                logger.error(
                    'No EGS login session found, please login normally.')
                exit(1)

        # unfortunately the captcha stuff makes a complete CLI login flow kinda impossible right now...
        print('Please login via the epic web login!')
        webbrowser.open(
            'https://www.epicgames.com/id/login?redirectUrl=https%3A%2F%2Fwww.epicgames.com%2Fid%2Fapi%2Fexchange'
        )
        print('If web page did not open automatically, please navigate '
              'to https://www.epicgames.com/id/login in your web browser')
        print(
            '- In case you opened the link manually; please open https://www.epicgames.com/id/api/exchange '
            'in your web browser after you have finished logging in.')
        exchange_code = input('Please enter code from JSON response: ')
        exchange_token = exchange_code.strip().strip('"')

        if self.core.auth_code(exchange_token):
            logger.info(
                f'Successfully logged in as "{self.core.lgd.userdata["displayName"]}"'
            )
        else:
            logger.error('Login attempt failed, please see log for details.')

    def list_games(self, args):
        logger.info('Logging in...')
        if not self.core.login():
            logger.error('Login failed, cannot continue!')
            exit(1)
        logger.info('Getting game list... (this may take a while)')
        games, dlc_list = self.core.get_game_and_dlc_list(
            platform_override=args.platform_override,
            skip_ue=not args.include_ue)

        print('\nAvailable games:')
        for game in sorted(games, key=lambda x: x.app_title):
            print(
                f'  * {game.app_title} (App name: {game.app_name}, version: {game.app_version})'
            )
            for dlc in sorted(dlc_list[game.asset_info.catalog_item_id],
                              key=lambda d: d.app_title):
                print(
                    f'    + {dlc.app_title} (App name: {dlc.app_name}, version: {dlc.app_version})'
                )

        print(f'\nTotal: {len(games)}')

    def list_installed(self, args):
        games = self.core.get_installed_list()

        if args.check_updates:
            logger.info('Logging in to check for updates...')
            if not self.core.login():
                logger.error('Login failed! Not checking for updates.')
            else:
                self.core.get_assets(True)

        print('\nInstalled games:')
        for game in sorted(games, key=lambda x: x.title):
            print(
                f'  * {game.title} (App name: {game.app_name}, version: {game.version})'
            )
            game_asset = self.core.get_asset(game.app_name)
            if game_asset.build_version != game.version:
                print(
                    f'    -> Update available! Installed: {game.version}, Latest: {game_asset.build_version}'
                )

        print(f'\nTotal: {len(games)}')

    def launch_game(self, args, extra):
        app_name = args.app_name
        if not self.core.is_installed(app_name):
            logger.error(f'Game {app_name} is not currently installed!')
            exit(1)

        if self.core.is_dlc(app_name):
            logger.error(
                f'{app_name} is DLC; please launch the base game instead!')
            exit(1)

        # override with config value
        args.offline = self.core.is_offline_game(app_name)
        if not args.offline:
            logger.info('Logging in...')
            if not self.core.login():
                logger.error('Login failed, cannot continue!')
                exit(1)

            if not args.skip_version_check and not self.core.is_noupdate_game(
                    app_name):
                logger.info('Checking for updates...')
                installed = self.core.lgd.get_installed_game(app_name)
                latest = self.core.get_asset(app_name, update=True)
                if latest.build_version != installed.version:
                    logger.error(
                        'Game is out of date, please update or launch with update check skipping!'
                    )
                    exit(1)

        params, cwd, env = self.core.get_launch_parameters(
            app_name=app_name,
            offline=args.offline,
            extra_args=extra,
            user=args.user_name_override)

        logger.info(f'Launching {app_name}...')
        if args.dry_run:
            logger.info(f'Launch parameters: {shlex.join(params)}')
            logger.info(f'Working directory: {cwd}')
            if env:
                logger.info('Environment overrides:', env)
        else:
            logger.debug(f'Launch parameters: {shlex.join(params)}')
            logger.debug(f'Working directory: {cwd}')
            if env:
                logger.debug('Environment overrides:', env)

            subprocess.Popen(params, cwd=cwd, env=env)

    def install_game(self, args):
        if not self.core.login():
            logger.error(
                'Login failed! Cannot continue with download process.')
            exit(1)

        if args.update_only:
            if not self.core.is_installed(args.app_name):
                logger.error(
                    f'Update requested for "{args.app_name}", but app not installed!'
                )
                exit(1)

        if args.platform_override:
            args.no_install = True

        game = self.core.get_game(args.app_name, update_meta=True)

        if not game:
            logger.error(
                f'Could not find "{args.app_name}" in list of available games,'
                f'did you type the name correctly?')
            exit(1)

        if game.is_dlc:
            logger.info('Install candidate is DLC')
            app_name = game.metadata['mainGameItem']['releaseInfo'][0]['appId']
            base_game = self.core.get_game(app_name)
            # check if base_game is actually installed
            if not self.core.is_installed(app_name):
                # download mode doesn't care about whether or not something's installed
                if not args.no_install:
                    logger.fatal(f'Base game "{app_name}" is not installed!')
                    exit(1)
        else:
            base_game = None

        # todo use status queue to print progress from CLI
        dlm, analysis, igame = self.core.prepare_download(
            game=game,
            base_game=base_game,
            base_path=args.base_path,
            force=args.force,
            max_shm=args.shared_memory,
            max_workers=args.max_workers,
            game_folder=args.game_folder,
            disable_patching=args.disable_patching,
            override_manifest=args.override_manifest,
            override_old_manifest=args.override_old_manifest,
            override_base_url=args.override_base_url,
            platform_override=args.platform_override)

        # game is either up to date or hasn't changed, so we have nothing to do
        if not analysis.dl_size:
            logger.info(
                'Download size is 0, the game is either already up to date or has not changed. Exiting...'
            )
            exit(0)

        logger.info(
            f'Install size: {analysis.install_size / 1024 / 1024:.02f} MiB')
        compression = (
            1 - (analysis.dl_size / analysis.uncompressed_dl_size)) * 100
        logger.info(
            f'Download size: {analysis.dl_size / 1024 / 1024:.02f} MiB '
            f'(Compression savings: {compression:.01f}%)')
        logger.info(
            f'Reusable size: {analysis.reuse_size / 1024 / 1024:.02f} MiB (chunks) / '
            f'{analysis.unchanged / 1024 / 1024:.02f} MiB (unchanged)')

        res = self.core.check_installation_conditions(analysis=analysis,
                                                      install=igame)

        if res.failures:
            logger.fatal(
                'Download cannot proceed, the following errors occured:')
            for msg in sorted(res.failures):
                logger.fatal(msg)
            exit(1)

        if res.warnings:
            logger.warning(
                'Installation requirements check returned the following warnings:'
            )
            for warn in sorted(res.warnings):
                logger.warning(warn)

        if not args.yes:
            choice = input(f'Do you wish to install "{igame.title}"? [Y/n]: ')
            if choice and choice.lower()[0] != 'y':
                print('Aborting...')
                exit(0)

        start_t = time.time()

        try:
            # set up logging stuff (should be moved somewhere else later)
            dlm.logging_queue = self.logging_queue
            dlm.proc_debug = args.dlm_debug

            dlm.start()
            dlm.join()
        except Exception as e:
            end_t = time.time()
            logger.info(
                f'Installation failed after {end_t - start_t:.02f} seconds.')
            logger.warning(
                f'The following exception occured while waiting for the donlowader to finish: {e!r}. '
                f'Try restarting the process, the resume file will be used to start where it failed. '
                f'If it continues to fail please open an issue on GitHub.')
        else:
            end_t = time.time()
            if not args.no_install:
                dlcs = self.core.get_dlc_for_game(game.app_name)
                if dlcs:
                    print('The following DLCs are available for this game:')
                    for dlc in dlcs:
                        print(
                            f' - {dlc.app_title} (App name: {dlc.app_name}, version: {dlc.app_version})'
                        )
                    # todo recursively call install with modified args to install DLC automatically (after confirm)
                    print(
                        'Installing DLCs works the same as the main game, just use the DLC app name instead.'
                    )
                    print(
                        '(Automatic installation of DLC is currently not supported.)'
                    )

                postinstall = self.core.install_game(igame)
                if postinstall:
                    self._handle_postinstall(postinstall, igame, yes=args.yes)

            logger.info(
                f'Finished installation process in {end_t - start_t:.02f} seconds.'
            )

    def _handle_postinstall(self, postinstall, igame, yes=False):
        print('This game lists the following prequisites to be installed:')
        print(
            f'- {postinstall["name"]}: {" ".join((postinstall["path"], postinstall["args"]))}'
        )
        if os.name == 'nt':
            if yes:
                c = 'n'  # we don't want to launch anything, just silent install.
            else:
                choice = input(
                    'Do you wish to install the prerequisites? ([y]es, [n]o, [i]gnore): '
                )
                c = choice.lower()[0]

            if c == 'i':  # just set it to installed
                print('Marking prerequisites as installed...')
                self.core.prereq_installed(igame.app_name)
            elif c == 'y':  # set to installed and launch installation
                print('Launching prerequisite executable..')
                self.core.prereq_installed(igame.app_name)
                req_path, req_exec = os.path.split(postinstall['path'])
                work_dir = os.path.join(igame.install_path, req_path)
                fullpath = os.path.join(work_dir, req_exec)
                subprocess.Popen([fullpath, postinstall['args']], cwd=work_dir)
        else:
            logger.info('Automatic installation not available on Linux.')

    def uninstall_game(self, args):
        igame = self.core.get_installed_game(args.app_name)
        if not igame:
            logger.error(
                f'Game {args.app_name} not installed, cannot uninstall!')
            exit(0)
        if igame.is_dlc:
            logger.error('Uninstalling DLC is not supported.')
            exit(1)

        if not args.yes:
            choice = input(
                f'Do you wish to uninstall "{igame.title}"? [y/N]: ')
            if not choice or choice.lower()[0] != 'y':
                print('Aborting...')
                exit(0)

        try:
            logger.info(
                f'Removing "{igame.title}" from "{igame.install_path}"...')
            self.core.uninstall_game(igame)

            # DLCs are already removed once we delete the main game, so this just removes them from the list
            dlcs = self.core.get_dlc_for_game(igame.app_name)
            for dlc in dlcs:
                idlc = self.core.get_installed_game(dlc.app_name)
                if self.core.is_installed(dlc.app_name):
                    logger.info(f'Uninstalling DLC "{dlc.app_name}"...')
                    self.core.uninstall_game(idlc, delete_files=False)

            logger.info('Game has been uninstalled.')
        except Exception as e:
            logger.warning(
                f'Removing game failed: {e!r}, please remove {igame.install_path} manually.'
            )