diff --git a/legendary/cli.py b/legendary/cli.py index 92c1df5..383cc55 100644 --- a/legendary/cli.py +++ b/legendary/cli.py @@ -16,6 +16,7 @@ from logging.handlers import QueueListener from multiprocessing import freeze_support, Queue as MPQueue from platform import platform from sys import exit, stdout, platform as sys_platform +from epic_expreval import Tokenizer from legendary import __version__, __codename__ from legendary.core import LegendaryCore @@ -27,7 +28,7 @@ from legendary.utils.custom_parser import HiddenAliasSubparsersAction from legendary.utils.env import is_windows_mac_or_pyi from legendary.lfs.eos import add_registry_entries, query_registry_entries, remove_registry_entries from legendary.lfs.utils import validate_files, clean_filename -from legendary.utils.selective_dl import get_sdl_data, LGDEvaluationContext +from legendary.utils.selective_dl import get_sdl_data, LGDEvaluationContext, EXTRA_FUNCTIONS from legendary.lfs.wine_helpers import read_registry, get_shell_folders, case_insensitive_file_search # todo custom formatter for cli logger (clean info, highlighted error/warning) @@ -911,55 +912,59 @@ class LegendaryCLI: else: logger.info(f'Using existing repair file: {repair_file}') - # check if SDL should be disabled - sdl_enabled = not args.install_tag - config_tags = self.core.lgd.config.get(game.app_name, 'install_opts', fallback=None) - config_disable_sdl = self.core.lgd.config.getboolean(game.app_name, 'disable_sdl', fallback=False) - # remove config flag if SDL is reset - if config_disable_sdl and args.reset_sdl and not args.disable_sdl: - self.core.lgd.config.remove_option(game.app_name, 'disable_sdl') - # if config flag is not yet set, set it and remove previous install tags - elif not config_disable_sdl and args.disable_sdl: - logger.info('Clearing install tags from config and disabling SDL for title.') - if config_tags: - self.core.lgd.config.remove_option(game.app_name, 'install_tags') - config_tags = None - self.core.lgd.config.set(game.app_name, 'disable_sdl', 'true') - sdl_enabled = False - # just disable SDL, but keep config tags that have been manually specified - elif config_disable_sdl or args.disable_sdl: - sdl_enabled = False - - if sdl_enabled: - # FIXME: Consider UpgradePathLogic - it lets automatically select options in new manifests when corresponding option was selected with older version - if not self.core.is_installed(game.app_name) or config_tags is None or args.reset_sdl: - context = LGDEvaluationContext(self.core) - sdl_data = get_sdl_data(self.core.lgd.egl_content_path, game.app_name, game.app_version(args.platform)) - if sdl_data: - if args.skip_sdl: - args.install_tag = [] - for entry in sdl_data['Data']: - if entry.get('IsRequired', 'false').lower() == 'true': - args.install_tag.extend(entry.get('Tags', [])) - else: - args.install_tag = sdl_prompt(sdl_data, game.app_title, context) - # self.core.lgd.config.set(game.app_name, 'install_tags', ','.join(args.install_tag)) - else: - logger.error(f'Unable to get SDL data for {game.app_name}') + logger.info('Checking for install components') + config_options = self.core.lgd.config.get(game.app_name, 'install_components', fallback=None) + install_components = args.install_component or [] + sdl_data = get_sdl_data(self.core.lgd.egl_content_path, game.app_name, game.app_version(args.platform)) + context = LGDEvaluationContext(self.core) + if not self.core.is_installed(game.app_name) or config_options is None or args.reset_sdl: + if sdl_data: + if not args.skip_sdl: + install_components = sdl_prompt(sdl_data, game.app_title, context) else: - args.install_tag = config_tags.split(',') - elif args.install_tag and not game.is_dlc and not args.no_install: - config_tags = ','.join(args.install_tag) - logger.info(f'Saving install tags for "{game.app_name}" to config: {config_tags}') - elif not game.is_dlc: - if config_tags and args.reset_sdl: - logger.info('Clearing install tags from config.') - self.core.lgd.config.remove_option(game.app_name, 'install_tags') - elif config_tags: - logger.info(f'Using install tags from config: {config_tags}') - args.install_tag = config_tags.split(',') - - logger.debug(f'Selected tags: {args.install_tag}') + logger.error(f'Unable to get SDL data for {game.app_name}') + else: + install_components = config_options.split(',') + context.selection = set(install_components) + + install_tags = set() + if sdl_data: + for element in sdl_data['Data']: + if element.get('IsRequired', 'false').lower() == 'true': + install_tags.update(element.get('Tags', [])) + if element['UniqueId'] not in install_components: + install_components.append(element['UniqueId']) + continue + if element.get('Invisible', 'false').lower() == 'true': + tk = Tokenizer(element['InvisibleSelectedExpression'], context) + tk.extend_functions(EXTRA_FUNCTIONS) + tk.compile() + if tk.execute(''): + logger.info('Selecting invisible component') + install_tags.update(element.get('Tags', [])) + if element['UniqueId'] not in install_components: + install_components.append(element['UniqueId']) + + # The ids may change from revision to revision, this property lets us match options against older options + upgrade_id = element.get('UpgradePathLogic') + if upgrade_id and upgrade_id in install_components: + install_tags.update(element.get('Tags', [])) + # Replace component id with upgraded one + install_components = [element['UniqueId'] if el == upgrade_id else el for el in install_components] + + if element['UniqueId'] in install_components: + install_tags.update(element.get('Tags', [])) + + if element.get('ConfigHandler'): + for child in element.get('Children', []): + if child['UniqueId'] in install_components: + install_tags.update(child.get('Tags', [])) + + if install_components: + self.core.lgd.config.set(game.app_name, 'install_components', ','.join(install_components)) + install_tags = list(install_tags) + logger.debug(f'Selected components: {install_components}') + logger.debug(f'Selected tags: {install_tags}') logger.info(f'Preparing download for "{game.app_title}" ({game.app_name})...') # todo use status queue to print progress from CLI # This has become a little ridiculous hasn't it? @@ -974,7 +979,8 @@ class LegendaryCLI: file_prefix_filter=args.file_prefix, file_suffix_filter=args.file_suffix, file_exclude_filter=args.file_exclude_prefix, - file_install_tag=args.install_tag, + file_install_tag=install_tags, + game_install_components=install_components, dl_optimizations=args.order_opt, dl_timeout=args.dl_timeout, repair=args.repair_mode, @@ -1646,6 +1652,7 @@ class LegendaryCLI: f'not being available on the selected platform or currently logged-in account.') args.offline = True + sdl_data = get_sdl_data(self.core.lgd.egl_content_path, app_name, game.app_version(args.platform)) or {} manifest_data = None entitlements = None # load installed manifest or URI @@ -1765,11 +1772,11 @@ class LegendaryCLI: igame.save_path)) installation_info.append(InfoItem('EGL sync GUID', 'synced_egl_guid', igame.egl_guid, igame.egl_guid)) - if igame.install_tags: - tags = ', '.join(igame.install_tags) + if igame.install_components: + opts = ', '.join(igame.install_components) else: - tags = '(None, all game data selected for install)' - installation_info.append(InfoItem('Install tags', 'install_tags', tags, igame.install_tags)) + opts = '(None, all game data selected for install)' + installation_info.append(InfoItem('Install components', 'install_components', opts, igame.install_components)) installation_info.append(InfoItem('Requires ownership verification token (DRM)', 'requires_ovt', igame.requires_ot, igame.requires_ot)) @@ -1843,9 +1850,23 @@ class LegendaryCLI: for tag in fm.install_tags: install_tags.add(tag) - install_tags = sorted(install_tags) - install_tags_human = ', '.join(i if i else '(empty)' for i in install_tags) - manifest_info.append(InfoItem('Install tags', 'install_tags', install_tags_human, install_tags)) + if sdl_data: + install_tags_human = [] + for element in sdl_data['Data']: + if not element.get('Title'): + continue + is_required = element.get('IsRequired','false')=='true' + required_txt = ' (required)' if is_required else '' + if element.get('Children'): + install_tags_human.append(f'{element["Title"]}{required_txt}') + for child in element.get('Children', []): + install_tags_human.append(f'\t{child["UniqueId"]} - {child["Title"]}') + else: + install_tags_human.append(f'{element["UniqueId"]} - {element["Title"]}{required_txt}') + else: + install_tags_human = '(none)' + + manifest_info.append(InfoItem('Install components', 'install_components', install_tags_human, sdl_data.get('Data'))) # file and chunk count manifest_info.append(InfoItem('Files', 'num_files', manifest.file_manifest_list.count, manifest.file_manifest_list.count)) @@ -1862,6 +1883,7 @@ class LegendaryCLI: chunk_size, total_size)) # if there are install tags break down size by tag + """ tag_disk_size = [] tag_disk_size_human = [] tag_download_size = [] @@ -1896,6 +1918,7 @@ class LegendaryCLI: tag_disk_size_human or 'N/A', tag_disk_size)) manifest_info.append(InfoItem('Download size by install tag', 'tag_download_size', tag_download_size_human or 'N/A', tag_download_size)) + """ if not args.json: def print_info_item(item: InfoItem): @@ -2772,8 +2795,8 @@ def main(): help='Only fetch files whose path ends with (case insensitive)') install_parser.add_argument('--exclude', dest='file_exclude_prefix', action='append', metavar='', type=str, help='Exclude files starting with (case insensitive)') - install_parser.add_argument('--install-tag', dest='install_tag', action='append', metavar='', - type=str, help='Only download files with the specified install tag') + install_parser.add_argument('--install-component', dest='install_component', action='append', metavar='', + type=str, help='Only download files with the specified optional download id') install_parser.add_argument('--enable-reordering', dest='order_opt', action='store_true', help='Enable reordering optimization to reduce RAM requirements ' 'during download (may have adverse results for some titles)') @@ -2902,8 +2925,8 @@ def main(): list_files_parser.add_argument('--json', dest='json', action='store_true', help='Output in JSON format') list_files_parser.add_argument('--hashlist', dest='hashlist', action='store_true', help='Output file hash list in hashcheck/sha1sum -c compatible format') - list_files_parser.add_argument('--install-tag', dest='install_tag', action='store', metavar='', - type=str, help='Show only files with specified install tag') + list_files_parser.add_argument('--install-component', dest='install_component', action='store', metavar='', + type=str, help='Show only files with specified optional download id') sync_saves_parser.add_argument('--skip-upload', dest='download_only', action='store_true', help='Only download new saves from cloud, don\'t upload') diff --git a/legendary/core.py b/legendary/core.py index 4b5fa97..0610011 100644 --- a/legendary/core.py +++ b/legendary/core.py @@ -1349,7 +1349,7 @@ class LegendaryCore: repair_use_latest: bool = False, disable_delta: bool = False, override_delta_manifest: str = '', egl_guid: str = '', preferred_cdn: str = None, disable_https: bool = False, - bind_ip: str = None) -> (DLManager, AnalysisResult, ManifestMeta): + bind_ip: str = None, game_install_components: list = None) -> (DLManager, AnalysisResult, ManifestMeta): # load old manifest old_manifest = None @@ -1558,6 +1558,7 @@ class LegendaryCore: can_run_offline=offline == 'true', requires_ot=ot == 'true', is_dlc=base_game is not None, install_size=anlres.install_size, egl_guid=egl_guid, install_tags=file_install_tag, + install_components=game_install_components, platform=platform, uninstaller=uninstaller) return dlm, anlres, igame @@ -1871,9 +1872,10 @@ class LegendaryCore: version=new_manifest.meta.build_version, platform='Windows') + # TODO: Migrate this to components instead? # transfer install tag choices to config - if lgd_igame.install_tags: - self.lgd.config.set(app_name, 'install_tags', ','.join(lgd_igame.install_tags)) + #if lgd_igame.install_tags: + # self.lgd.config.set(app_name, 'install_tags', ','.join(lgd_igame.install_tags)) # mark game as installed _ = self._install_game(lgd_igame) diff --git a/legendary/models/egl.py b/legendary/models/egl.py index 164939d..2d7476c 100644 --- a/legendary/models/egl.py +++ b/legendary/models/egl.py @@ -59,6 +59,7 @@ class EGLManifest: self.install_location = None self.install_size = None self.install_tags = None + self.install_components = None self.installation_guid = None self.launch_command = None self.executable = None @@ -87,6 +88,7 @@ class EGLManifest: tmp.install_location = json.pop('InstallLocation', '') tmp.install_size = json.pop('InstallSize', 0) tmp.install_tags = json.pop('InstallTags', []) + tmp.install_components = json.pop('InstallComponents', []) tmp.installation_guid = json.pop('InstallationGuid', '') tmp.launch_command = json.pop('LaunchCommand', '') tmp.executable = json.pop('LaunchExecutable', '') @@ -114,6 +116,7 @@ class EGLManifest: out['InstallLocation'] = self.install_location out['InstallSize'] = self.install_size out['InstallTags'] = self.install_tags + out['InstallComponents'] = self.install_components out['InstallationGuid'] = self.installation_guid out['LaunchCommand'] = self.launch_command out['LaunchExecutable'] = self.executable @@ -140,6 +143,7 @@ class EGLManifest: tmp.install_location = igame.install_path tmp.install_size = igame.install_size tmp.install_tags = igame.install_tags + tmp.install_components = igame.install_components tmp.installation_guid = igame.egl_guid tmp.launch_command = igame.launch_parameters tmp.executable = igame.executable @@ -159,4 +163,4 @@ class EGLManifest: launch_parameters=self.launch_command, can_run_offline=self.can_run_offline, requires_ot=self.ownership_token, is_dlc=False, needs_verification=self.needs_validation, install_size=self.install_size, - egl_guid=self.installation_guid, install_tags=self.install_tags) + egl_guid=self.installation_guid, install_tags=self.install_tags, install_components=self.install_components) diff --git a/legendary/models/game.py b/legendary/models/game.py index 5a7eec6..85c2e9f 100644 --- a/legendary/models/game.py +++ b/legendary/models/game.py @@ -183,6 +183,7 @@ class InstalledGame: executable: str = '' install_size: int = 0 install_tags: List[str] = field(default_factory=list) + install_components: List[str] = field(default_factory=list) is_dlc: bool = False launch_parameters: str = '' manifest_path: str = '' @@ -218,6 +219,7 @@ class InstalledGame: tmp.install_size = json.get('install_size', 0) tmp.egl_guid = json.get('egl_guid', '') tmp.install_tags = json.get('install_tags', []) + tmp.install_components = json.get('install_components', []) return tmp diff --git a/legendary/utils/cli.py b/legendary/utils/cli.py index 303b440..9a421b4 100644 --- a/legendary/utils/cli.py +++ b/legendary/utils/cli.py @@ -51,8 +51,6 @@ def get_int_choice(prompt, default=None, min_choice=None, max_choice=None, retur def sdl_prompt(sdl_data, title, context): - tags = set() - print(f'You are about to install {title}, this application supports selective downloads.') choices = [] required_categories = {} @@ -86,27 +84,7 @@ def sdl_prompt(sdl_data, title, context): choices=choices, cycle=True, validate=lambda selected: not required_categories or all(any(item in selected for item in category) for category in required_categories.values())).execute() - context.selection = set(selected_packs) - - for element in sdl_data['Data']: - if element.get('IsRequired', 'false').lower() == 'true': - tags.update(element.get('Tags', [])) - continue - if element.get('Invisible', 'false').lower() == 'true': - tk = Tokenizer(element['InvisibleSelectedExpression'], context) - tk.extend_functions(EXTRA_FUNCTIONS) - tk.compile() - if tk.execute(''): - tags.update(element.get('Tags', [])) - - if element['UniqueId'] in selected_packs: - tags.update(element.get('Tags', [])) - if element.get('ConfigHandler'): - for child in element.get('Children', []): - if child['UniqueId'] in selected_packs: - tags.update(child.get('Tags', [])) - - return list(tags) + return selected_packs def strtobool(val): diff --git a/legendary/utils/selective_dl.py b/legendary/utils/selective_dl.py index 5ada8b0..b5e273b 100644 --- a/legendary/utils/selective_dl.py +++ b/legendary/utils/selective_dl.py @@ -44,4 +44,4 @@ def get_sdl_data(location, app_name, app_version): if applying_meta: return applying_meta[-1] - return None \ No newline at end of file + return None