[cli/core/models] Add basic support for DLCs

This commit is contained in:
derrod 2020-04-18 02:11:58 +02:00
parent 0031e5908a
commit 662f6e7bd0
3 changed files with 93 additions and 16 deletions

View file

@ -151,12 +151,14 @@ def main():
if not core.login(): if not core.login():
logger.error('Login failed, cannot continue!') logger.error('Login failed, cannot continue!')
exit(1) exit(1)
logger.info('Getting game list...') logger.info('Getting game list... (this may take a while)')
games = core.get_game_list() games, dlc_list = core.get_game_and_dlc_list()
print('\nAvailable games:') print('\nAvailable games:')
for game in sorted(games, key=lambda x: x.app_title): 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})') 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)}') print(f'\nTotal: {len(games)}')
@ -185,6 +187,10 @@ def main():
logger.error(f'Game {app_name} is not currently installed!') logger.error(f'Game {app_name} is not currently installed!')
exit(1) 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): if not args.offline and not core.is_offline_game(app_name):
logger.info('Logging in...') logger.info('Logging in...')
if not core.login(): 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?') logger.fatal(f'Could not find "{target_app}" in list of available games, did you type the name correctly?')
exit(1) 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 # 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, dlm, analysis, igame = core.prepare_download(game=game, base_game=base_game, base_path=args.base_path,
max_shm=args.shared_memory, max_workers=args.max_workers, force=args.force, max_shm=args.shared_memory,
max_workers=args.max_workers,
disable_patching=args.disable_patching, disable_patching=args.disable_patching,
override_manifest=args.override_manifest, override_manifest=args.override_manifest,
override_base_url=args.override_base_url) override_base_url=args.override_base_url)
@ -284,6 +304,15 @@ def main():
end_t = time.time() end_t = time.time()
if args.install or args.update: if args.install or args.update:
postinstall = core.install_game(igame) 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: if postinstall:
logger.info('This game lists the following prequisites to be installed:') logger.info('This game lists the following prequisites to be installed:')
logger.info(f'{postinstall["name"]}: {" ".join((postinstall["path"], postinstall["args"]))}') logger.info(f'{postinstall["name"]}: {" ".join((postinstall["path"], postinstall["args"]))}')
@ -307,10 +336,21 @@ def main():
igame = core.get_installed_game(target_app) igame = core.get_installed_game(target_app)
if not igame: if not igame:
logger.error(f'Game {target_app} not installed, cannot uninstall!') logger.error(f'Game {target_app} not installed, cannot uninstall!')
if igame.is_dlc:
logger.error('Uninstalling DLC is not supported.')
exit(1)
try: try:
logger.info(f'Removing "{igame.title}" from "{igame.install_path}"...') logger.info(f'Removing "{igame.title}" from "{igame.install_path}"...')
core.uninstall_game(igame) 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.') logger.info('Game has been uninstalled.')
except Exception as e: except Exception as e:
logger.warning(f'Removing game failed: {e!r}, please remove {igame.install_path} manually.') logger.warning(f'Removing game failed: {e!r}, please remove {igame.install_path} manually.')

View file

@ -8,10 +8,11 @@ import shlex
import shutil import shutil
from base64 import b64decode from base64 import b64decode
from collections import defaultdict
from datetime import datetime from datetime import datetime
from random import choice as randchoice from random import choice as randchoice
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from typing import List from typing import List, Dict
from legendary.api.egs import EPCAPI from legendary.api.egs import EPCAPI
from legendary.downloader.manager import DLManager from legendary.downloader.manager import DLManager
@ -141,7 +142,11 @@ class LegendaryCore:
return self.lgd.get_game_meta(app_name) return self.lgd.get_game_meta(app_name)
def get_game_list(self, update_assets=True) -> List[Game]: 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 = [] _ret = []
_dlc = defaultdict(list)
for ga in self.get_assets(update_assets=update_assets): for ga in self.get_assets(update_assets=update_assets):
if ga.namespace == 'ue': # skip UE demo content 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, game = Game(app_name=ga.app_name, app_version=ga.build_version,
app_title=eg_meta['title'], asset_info=ga, metadata=eg_meta) app_title=eg_meta['title'], asset_info=ga, metadata=eg_meta)
self.lgd.set_game_meta(game.app_name, game) 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]: 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: def get_installed_game(self, app_name) -> InstalledGame:
return self.lgd.get_installed_game(app_name) return self.lgd.get_installed_game(app_name)
@ -254,6 +271,12 @@ class LegendaryCore:
def is_installed(self, app_name: str) -> bool: def is_installed(self, app_name: str) -> bool:
return self.lgd.get_installed_game(app_name) is not None 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 @staticmethod
def load_manfiest(data: bytes) -> Manifest: def load_manfiest(data: bytes) -> Manifest:
if data[0:1] == b'{': if data[0:1] == b'{':
@ -261,7 +284,7 @@ class LegendaryCore:
else: else:
return Manifest.read_all(data) 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, max_shm: int = 0, max_workers: int = 0, force: bool = False,
disable_patching: bool = False, override_manifest: str = '', disable_patching: bool = False, override_manifest: str = '',
override_base_url: str = '') -> (DLManager, AnalysisResult, ManifestMeta): override_base_url: str = '') -> (DLManager, AnalysisResult, ManifestMeta):
@ -323,10 +346,17 @@ class LegendaryCore:
if not base_path: if not base_path:
base_path = self.get_default_install_dir() base_path = self.get_default_install_dir()
install_path = os.path.join( if game.is_dlc:
base_path, install_path = os.path.join(
game.metadata.get('customAttributes', {}).get('FolderName', {}).get('value', game.app_name) 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): if not os.path.exists(install_path):
os.makedirs(install_path) os.makedirs(install_path)
@ -362,7 +392,8 @@ class LegendaryCore:
prereq_info=prereq, manifest_path=override_manifest, base_urls=base_urls, prereq_info=prereq, manifest_path=override_manifest, base_urls=base_urls,
install_path=install_path, executable=new_manifest.meta.launch_exe, install_path=install_path, executable=new_manifest.meta.launch_exe,
launch_parameters=new_manifest.meta.launch_command, 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 return dlm, anlres, igame

View file

@ -47,6 +47,10 @@ class Game:
self.app_title = app_title self.app_title = app_title
self.base_urls = [] # base urls for download, only really used when cached manifest is current 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 @classmethod
def from_json(cls, json): def from_json(cls, json):
tmp = cls() tmp = cls()
@ -69,7 +73,7 @@ class Game:
class InstalledGame: class InstalledGame:
def __init__(self, app_name='', title='', version='', manifest_path='', base_urls=None, def __init__(self, app_name='', title='', version='', manifest_path='', base_urls=None,
install_path='', executable='', launch_parameters='', prereq_info=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.app_name = app_name
self.title = title self.title = title
self.version = version self.version = version
@ -82,6 +86,7 @@ class InstalledGame:
self.prereq_info = prereq_info self.prereq_info = prereq_info
self.can_run_offline = can_run_offline self.can_run_offline = can_run_offline
self.requires_ot = requires_ot self.requires_ot = requires_ot
self.is_dlc = is_dlc
@classmethod @classmethod
def from_json(cls, json): def from_json(cls, json):
@ -97,6 +102,7 @@ class InstalledGame:
tmp.launch_parameters = json.get('launch_parameters', '') tmp.launch_parameters = json.get('launch_parameters', '')
tmp.prereq_info = json.get('prereq_info', None) 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.requires_ot = json.get('requires_ot', False)
tmp.is_dlc = json.get('is_dlc', False)
return tmp return tmp