From 662f6e7bd0b8c546400744b6ac2f84f99c7bdd97 Mon Sep 17 00:00:00 2001 From: derrod Date: Sat, 18 Apr 2020 02:11:58 +0200 Subject: [PATCH] [cli/core/models] Add basic support for DLCs --- legendary/cli.py | 48 +++++++++++++++++++++++++++++++++---- legendary/core.py | 51 ++++++++++++++++++++++++++++++++-------- legendary/models/game.py | 10 ++++++-- 3 files changed, 93 insertions(+), 16 deletions(-) diff --git a/legendary/cli.py b/legendary/cli.py index 01aaa48..d0c94b4 100644 --- a/legendary/cli.py +++ b/legendary/cli.py @@ -151,12 +151,14 @@ def main(): if not core.login(): logger.error('Login failed, cannot continue!') exit(1) - logger.info('Getting game list...') - games = core.get_game_list() + logger.info('Getting game list... (this may take a while)') + games, dlc_list = core.get_game_and_dlc_list() 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)}') @@ -185,6 +187,10 @@ def main(): logger.error(f'Game {app_name} is not currently installed!') exit(1) + if core.is_dlc(app_name): + logger.error(f'{app_name} is DLC; please launch the base game instead!') + exit(1) + if not args.offline and not core.is_offline_game(app_name): logger.info('Logging in...') if not core.login(): @@ -233,9 +239,23 @@ def main(): logger.fatal(f'Could not find "{target_app}" in list of available games, 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 = core.get_game(app_name) + # check if base_game is actually installed + if not core.get_installed_game(app_name): + # download mode doesn't care about whether or not something's installed + if args.install or args.update: + 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 = core.prepare_download(game=game, base_path=args.base_path, force=args.force, - max_shm=args.shared_memory, max_workers=args.max_workers, + dlm, analysis, igame = 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, disable_patching=args.disable_patching, override_manifest=args.override_manifest, override_base_url=args.override_base_url) @@ -284,6 +304,15 @@ def main(): end_t = time.time() if args.install or args.update: postinstall = core.install_game(igame) + + dlcs = 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})') + 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.') + if postinstall: logger.info('This game lists the following prequisites to be installed:') logger.info(f'{postinstall["name"]}: {" ".join((postinstall["path"], postinstall["args"]))}') @@ -307,10 +336,21 @@ def main(): igame = core.get_installed_game(target_app) if not igame: logger.error(f'Game {target_app} not installed, cannot uninstall!') + if igame.is_dlc: + logger.error('Uninstalling DLC is not supported.') + exit(1) try: logger.info(f'Removing "{igame.title}" from "{igame.install_path}"...') core.uninstall_game(igame) + + dlcs = core.get_dlc_for_game(igame.app_name) + for dlc in dlcs: + idlc = core.get_installed_game(dlc.app_name) + if core.is_installed(dlc.app_name): + logger.info(f'Uninstalling DLC "{dlc.app_name}"...') + 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.') diff --git a/legendary/core.py b/legendary/core.py index e5b2148..2435ee4 100644 --- a/legendary/core.py +++ b/legendary/core.py @@ -8,10 +8,11 @@ import shlex import shutil from base64 import b64decode +from collections import defaultdict from datetime import datetime from random import choice as randchoice from requests.exceptions import HTTPError -from typing import List +from typing import List, Dict from legendary.api.egs import EPCAPI from legendary.downloader.manager import DLManager @@ -141,7 +142,11 @@ class LegendaryCore: 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) -> (List[Game], Dict[str, Game]): _ret = [] + _dlc = defaultdict(list) for ga in self.get_assets(update_assets=update_assets): if ga.namespace == 'ue': # skip UE demo content @@ -156,12 +161,24 @@ class LegendaryCore: game = Game(app_name=ga.app_name, app_version=ga.build_version, app_title=eg_meta['title'], asset_info=ga, metadata=eg_meta) self.lgd.set_game_meta(game.app_name, game) - _ret.append(game) - return _ret + 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) + _, 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 self.lgd.get_installed_list() + 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) @@ -254,6 +271,12 @@ class LegendaryCore: def is_installed(self, app_name: str) -> bool: return self.lgd.get_installed_game(app_name) is not None + def is_dlc(self, app_name: str) -> bool: + meta = self.lgd.get_game_meta(app_name) + if not meta: + raise ValueError('Game unknown!') + return meta.is_dlc + @staticmethod def load_manfiest(data: bytes) -> Manifest: if data[0:1] == b'{': @@ -261,7 +284,7 @@ class LegendaryCore: else: return Manifest.read_all(data) - def prepare_download(self, game: Game, base_path: str = '', + def prepare_download(self, game: Game, base_game: Game = None, base_path: str = '', max_shm: int = 0, max_workers: int = 0, force: bool = False, disable_patching: bool = False, override_manifest: str = '', override_base_url: str = '') -> (DLManager, AnalysisResult, ManifestMeta): @@ -323,10 +346,17 @@ class LegendaryCore: if not base_path: base_path = self.get_default_install_dir() - install_path = os.path.join( - base_path, - game.metadata.get('customAttributes', {}).get('FolderName', {}).get('value', game.app_name) - ) + if game.is_dlc: + install_path = os.path.join( + base_path, + base_game.metadata.get('customAttributes', {}).get('FolderName', {}).get('value', game.app_name) + ) + else: + install_path = os.path.join( + base_path, + game.metadata.get('customAttributes', {}).get('FolderName', {}).get('value', game.app_name) + ) + if not os.path.exists(install_path): os.makedirs(install_path) @@ -362,7 +392,8 @@ class LegendaryCore: prereq_info=prereq, manifest_path=override_manifest, base_urls=base_urls, install_path=install_path, executable=new_manifest.meta.launch_exe, launch_parameters=new_manifest.meta.launch_command, - can_run_offline=offline == 'true', requires_ot=ot == 'true') + can_run_offline=offline == 'true', requires_ot=ot == 'true', + is_dlc=base_game is not None) return dlm, anlres, igame diff --git a/legendary/models/game.py b/legendary/models/game.py index f8a594d..b8d7934 100644 --- a/legendary/models/game.py +++ b/legendary/models/game.py @@ -47,6 +47,10 @@ class Game: self.app_title = app_title self.base_urls = [] # base urls for download, only really used when cached manifest is current + @property + def is_dlc(self): + return self.metadata and 'mainGameItem' in self.metadata + @classmethod def from_json(cls, json): tmp = cls() @@ -69,7 +73,7 @@ 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): + can_run_offline=False, requires_ot=False, is_dlc=False): self.app_name = app_name self.title = title self.version = version @@ -82,6 +86,7 @@ class InstalledGame: self.prereq_info = prereq_info self.can_run_offline = can_run_offline self.requires_ot = requires_ot + self.is_dlc = is_dlc @classmethod def from_json(cls, json): @@ -97,6 +102,7 @@ class InstalledGame: tmp.launch_parameters = json.get('launch_parameters', '') tmp.prereq_info = json.get('prereq_info', None) - tmp.can_run_offline = json.get('can_run_offline', True) + tmp.can_run_offline = json.get('can_run_offline', False) tmp.requires_ot = json.get('requires_ot', False) + tmp.is_dlc = json.get('is_dlc', False) return tmp