From 38f5bbd93449a99f80fba00b35afe06982da04f7 Mon Sep 17 00:00:00 2001 From: derrod Date: Wed, 20 May 2020 14:06:55 +0200 Subject: [PATCH] [cli/core/models] Add support for importing already installed games Fixes #10 though will need further improvement. --- legendary/cli.py | 66 ++++++++++++++++++++++++++++++++++++++-- legendary/core.py | 30 ++++++++++++++++++ legendary/models/game.py | 5 ++- 3 files changed, 98 insertions(+), 3 deletions(-) diff --git a/legendary/cli.py b/legendary/cli.py index de91645..c102b96 100644 --- a/legendary/cli.py +++ b/legendary/cli.py @@ -389,6 +389,12 @@ class LegendaryCLI: subprocess.Popen(params, cwd=cwd, env=env) def install_game(self, args): + if self.core.is_installed(args.app_name): + igame = self.core.get_installed_game(args.app_name) + if igame.needs_verification and not args.repair_mode: + logger.info('Game needs to be verified before updating, switching to repair mode...') + args.repair_mode = True + repair_file = None if args.subparser_name == 'download': logger.info('Setting --no-install flag since "download" command was used') @@ -472,6 +478,11 @@ class LegendaryCLI: if not analysis.dl_size: logger.info('Download size is 0, the game is either already up to date or has not changed. Exiting...') if args.repair_mode and os.path.exists(repair_file): + igame = self.core.get_installed_game(game.app_name) + if igame.needs_verification: + igame.needs_verification = False + self.core.install_game(igame) + logger.debug('Removing repair file.') os.remove(repair_file) exit(0) @@ -557,6 +568,11 @@ class LegendaryCLI: logger.info(f'To download saves for this game run "legendary sync-saves {args.app_name}"') if args.repair_mode and os.path.exists(repair_file): + igame = self.core.get_installed_game(game.app_name) + if igame.needs_verification: + igame.needs_verification = False + self.core.install_game(igame) + logger.debug('Removing repair file.') os.remove(repair_file) @@ -670,6 +686,46 @@ class LegendaryCLI: if print_command: logger.info(f'Run "legendary repair {args.app_name}" to repair your game installation.') + def import_game(self, args): + if not os.path.exists(args.app_path): + logger.error(f'Specified path "{args.app_path}" does not exist!') + exit(1) + + if self.core.is_installed(args.app_name): + logger.error('Game is already installed!') + exit(0) + + if not self.core.login(): + logger.error('Log in failed!') + exit(1) + + # do some basic checks + game = self.core.get_game(args.app_name, update_meta=True) + if not game: + logger.fatal(f'Did not find game "{args.app_name}" on account.') + exit(1) + + # todo: if there is an Epic Games Launcher manifest in the install path use that instead + # get everything needed for import from core, then run additional checks. + manifest, igame = self.core.import_game(game, args.app_path) + # check if most files at least exist or if user might have specified the wrong directory + total = len(manifest.file_manifest_list.elements) + found = sum(os.path.exists(os.path.join(args.app_path, f.filename)) + for f in manifest.file_manifest_list.elements) + if found != total: + ratio = found / total + if ratio < 0.95: + logger.fatal(f'{total-found}/{total} files are missing, cannot import.') + exit(1) + logger.warning('Some files are missing from the game installation, this may be due to newer updates.') + else: + logger.info('Game install appears to be complete.') + + self.core.install_game(igame) + logger.info(f'NOTE: The game installation will have to be verified before it can be updated with legendary. ' + f'Run "legendary repair {args.app_name}" to do so.') + logger.info('Game has been imported.') + def main(): parser = argparse.ArgumentParser(description=f'Legendary v{__version__} - "{__codename__}"') @@ -697,6 +753,7 @@ def main(): download_saves_parser = subparsers.add_parser('download-saves', help='Download all cloud saves') sync_saves_parser = subparsers.add_parser('sync-saves', help='Sync cloud saves') verify_parser = subparsers.add_parser('verify-game', help='Verify a game\'s local files') + import_parser = subparsers.add_parser('import-game', help='Import an already installed game') install_parser.add_argument('app_name', help='Name of the app', metavar='') uninstall_parser.add_argument('app_name', help='Name of the app', metavar='') @@ -709,7 +766,10 @@ def main(): help='Name of the app (optional)') sync_saves_parser.add_argument('app_name', nargs='?', metavar='', default='', help='Name of the app (optional)') - verify_parser.add_argument('app_name', help='Name of the app (optional)', metavar='') + verify_parser.add_argument('app_name', help='Name of the app', metavar='') + import_parser.add_argument('app_name', help='Name of the app', metavar='') + import_parser.add_argument('app_path', help='Path where the game is installed', + metavar='') # importing only works on Windows right now if os.name == 'nt': @@ -835,7 +895,7 @@ def main(): if args.subparser_name not in ('auth', 'list-games', 'list-installed', 'list-files', 'launch', 'download', 'uninstall', 'install', 'update', 'repair', 'list-saves', 'download-saves', 'sync-saves', - 'verify-game'): + 'verify-game', 'import-game'): print(parser.format_help()) # Print the main help *and* the help for all of the subcommands. Thanks stackoverflow! @@ -883,6 +943,8 @@ def main(): cli.sync_saves(args) elif args.subparser_name == 'verify-game': cli.verify_game(args) + elif args.subparser_name == 'import-game': + cli.import_game(args) except KeyboardInterrupt: logger.info('Command was aborted via KeyboardInterrupt, cleaning up...') diff --git a/legendary/core.py b/legendary/core.py index 0004214..40e0d1b 100644 --- a/legendary/core.py +++ b/legendary/core.py @@ -754,6 +754,36 @@ class LegendaryCore: igame.prereq_info['installed'] = True self.lgd.set_installed_game(app_name, igame) + def import_game(self, game: Game, app_path: str) -> (Manifest, InstalledGame): + self.log.info(f'Downloading latest manifest for "{game.app_name}"') + manifest_data, base_urls = self.get_cdn_manifest(game) + if not game.base_urls: + game.base_urls = base_urls + self.lgd.set_game_meta(game.app_name, game) + + # parse and save manifest to disk for verification step of import + new_manifest = self.load_manfiest(manifest_data) + self.lgd.save_manifest(game.app_name, manifest_data) + self.lgd.save_manifest(game.app_name, manifest_data, + version=new_manifest.meta.build_version) + + prereq = None + if new_manifest.meta.prereq_ids: + prereq = dict(ids=new_manifest.meta.prereq_ids, name=new_manifest.meta.prereq_name, + path=new_manifest.meta.prereq_path, args=new_manifest.meta.prereq_args) + + offline = game.metadata.get('customAttributes', {}).get('CanRunOffline', {}).get('value', 'true') + ot = game.metadata.get('customAttributes', {}).get('OwnershipToken', {}).get('value', 'false') + igame = InstalledGame(app_name=game.app_name, title=game.app_title, prereq_info=prereq, base_urls=base_urls, + install_path=app_path, version=new_manifest.meta.build_version, is_dlc=game.is_dlc, + executable=new_manifest.meta.launch_exe, can_run_offline=offline == 'true', + launch_parameters=new_manifest.meta.launch_command, requires_ot=ot == 'true', + needs_verification=True) + + return new_manifest, igame + + + def exit(self): """ Do cleanup, config saving, and exit. diff --git a/legendary/models/game.py b/legendary/models/game.py index 2852ee1..36b2b03 100644 --- a/legendary/models/game.py +++ b/legendary/models/game.py @@ -78,7 +78,8 @@ class Game: class InstalledGame: def __init__(self, app_name='', title='', version='', manifest_path='', base_urls=None, install_path='', executable='', launch_parameters='', prereq_info=None, - can_run_offline=False, requires_ot=False, is_dlc=False, save_path=None): + can_run_offline=False, requires_ot=False, is_dlc=False, save_path=None, + needs_verification=False): self.app_name = app_name self.title = title self.version = version @@ -93,6 +94,7 @@ class InstalledGame: self.requires_ot = requires_ot self.is_dlc = is_dlc self.save_path = save_path + self.needs_verification = needs_verification @classmethod def from_json(cls, json): @@ -112,6 +114,7 @@ class InstalledGame: tmp.requires_ot = json.get('requires_ot', False) tmp.is_dlc = json.get('is_dlc', False) tmp.save_path = json.get('save_path', None) + tmp.needs_verification = json.get('needs_verification', None) return tmp