mirror of
https://github.com/derrod/legendary.git
synced 2024-12-22 17:55:27 +00:00
[cli/core/models] Add basic support for DLCs
This commit is contained in:
parent
0031e5908a
commit
662f6e7bd0
|
@ -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.')
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
if game.is_dlc:
|
||||||
|
_dlc[game.metadata['mainGameItem']['id']].append(game)
|
||||||
|
else:
|
||||||
_ret.append(game)
|
_ret.append(game)
|
||||||
|
|
||||||
return _ret
|
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()
|
||||||
|
|
||||||
|
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(
|
install_path = os.path.join(
|
||||||
base_path,
|
base_path,
|
||||||
game.metadata.get('customAttributes', {}).get('FolderName', {}).get('value', game.app_name)
|
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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue