Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 36 additions & 7 deletions legendary/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ def list_installed(self, args):
versions = dict()
for game in games:
try:
versions[game.app_name] = self.core.get_asset(game.app_name, platform=game.platform).build_version
versions[game.app_name] = self.core.get_asset(game.app_name, platform=game.platform, namespace=game.namespace).build_version
except ValueError:
logger.warning(f'Metadata for "{game.app_name}" is missing, the game may have been removed from '
f'your account or not be in legendary\'s database yet, try rerunning the command '
Expand Down Expand Up @@ -907,6 +907,23 @@ def install_game(self, args):
else:
logger.warning(f'No asset found for platform "{args.platform}", '
f'trying anyway since --no-install is set.')
elif not args.namespace and len(game.asset_infos[args.platform]) > 1:
asset_infos = []
for asset in game.asset_infos[args.platform]:
namespace_info = game.namespaces.get(asset.namespace)
if namespace_info:
asset_infos.append((namespace_info.get('sandboxType'), namespace_info.get('sandboxName'), asset.namespace))
type_name_str = '\n'.join([f'{_t}\t{_n}\t{_ns}' for _t,_n,_ns in asset_infos])
logger.error('You have access to more than one asset for this game\n'
f'Type\tName\tNamespace\n'
+type_name_str
+'\nuse --namespace to pick one')
exit(1)

if args.namespace and not args.namespace in game.namespaces:
available_namespaces = '\n'.join(list(game.namespaces.keys()))
logger.error("Unknown namespace\n" + available_namespaces)
exit(1)

if game.is_dlc:
logger.info('Install candidate is DLC')
Expand Down Expand Up @@ -1012,6 +1029,7 @@ def install_game(self, args):
override_delta_manifest=args.override_delta_manifest,
preferred_cdn=args.preferred_cdn,
disable_https=args.disable_https,
namespace=args.namespace,
bind_ip=args.bind_ip)

# game is either up-to-date or hasn't changed, so we have nothing to do
Expand Down Expand Up @@ -1677,6 +1695,7 @@ def info(self, args):

manifest_data = None
entitlements = None
namespace = args.namespace or game.namespace
# load installed manifest or URI
if args.offline or manifest_uri:
if app_name and self.core.is_installed(app_name):
Expand All @@ -1692,20 +1711,21 @@ def info(self, args):
logger.info('Game not installed and offline mode enabled, cannot load manifest.')
elif game:
entitlements = self.core.egs.get_user_entitlements_full()
egl_meta = self.core.egs.get_game_info(game.namespace, game.catalog_item_id)
egl_meta = self.core.egs.get_game_info(namespace, game.catalog_item_id)
game.metadata = egl_meta
# Get manifest if asset exists for current platform
if args.platform in game.asset_infos:
manifest_data, _ = self.core.get_cdn_manifest(game, args.platform)
manifest_data, _ = self.core.get_cdn_manifest(game, args.platform, namespace)

if game:
game_infos = info_items['game']
game_infos.append(InfoItem('App name', 'app_name', game.app_name, game.app_name))
game_infos.append(InfoItem('Title', 'title', game.app_title, game.app_title))
game_infos.append(InfoItem('Latest version', 'version', game.app_version(args.platform),
game.app_version(args.platform)))
all_versions = {k: v.build_version for k, v in game.asset_infos.items()}
game_infos.append(InfoItem('All versions', 'platform_versions', all_versions, all_versions))
_v = game.app_version(args.platform, args.namespace)
game_infos.append(InfoItem('Latest version', 'version', _v, _v))
all_versions = {k: '; '.join([a.build_version for a in v]) for k, v in game.asset_infos.items()}
all_versions_json = {k: [a.__dict__ for a in v] for k,v in game.asset_infos.items()}
game_infos.append(InfoItem('All versions', 'platform_versions', all_versions, all_versions_json))
# Cloud save support for Mac and Windows
game_infos.append(InfoItem('Cloud saves supported', 'cloud_saves_supported',
game.supports_cloud_saves or game.supports_mac_cloud_saves,
Expand Down Expand Up @@ -1780,6 +1800,13 @@ def info(self, args):
else:
game_infos.append(InfoItem('Owned DLC', 'owned_dlc', None, []))

if len(game.namespaces.keys()) > 0:
all_namespaces = {_n['sandboxName']: '({}) - {}'.format(_n['sandboxType'], _n['namespace']) for _n in game.namespaces.values()}
game_infos.append(InfoItem('Namespaces', 'namespaces', all_namespaces, list(game.namespaces.values())))
else:
game_infos.append(InfoItem('Namespaces', 'namespaces', None, []))


igame = self.core.get_installed_game(app_name)
if igame:
installation_info = info_items['install']
Expand Down Expand Up @@ -2911,6 +2938,7 @@ def main():
help='Do not ask about installing DLCs.')
install_parser.add_argument('--bind', dest='bind_ip', action='store', metavar='<IPs>', type=str,
help='Comma-separated list of IPs to bind to for downloading')
install_parser.add_argument('--namespace', dest='namespace', help='Specify namespace to pick sandbox from which to install')

uninstall_parser.add_argument('--keep-files', dest='keep_files', action='store_true',
help='Keep files but remove game from Legendary database')
Expand Down Expand Up @@ -3074,6 +3102,7 @@ def main():
help='Output information in JSON format')
info_parser.add_argument('--platform', dest='platform', action='store', metavar='<Platform>', type=str,
help='Platform to fetch info for (default: installed or Mac on macOS, Windows otherwise)')
info_parser.add_argument('--namespace', dest='namespace', type=str, help='Specify namespace to return primary data of')

activate_parser.add_argument('-s','--summary', dest='summary', action='store_true',
help='Only print information about the activation status (uplay)')
Expand Down
87 changes: 57 additions & 30 deletions legendary/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,13 +367,20 @@ def get_assets(self, update_assets=False, platform='Windows') -> List[GameAsset]
self.lgd.assets = assets

return self.lgd.assets[platform]

def get_asset(self, app_name, platform='Windows', update=False) -> GameAsset:

def get_library_items(self, include_metadata=True, force_refresh=False):
if force_refresh or not self.lgd.library_items:
lib = self.egs.get_library_items(include_metadata)
if self.lgd.library_items != lib:
self.lgd.library_items = lib
return self.lgd.library_items

def get_asset(self, app_name, platform='Windows', update=False, namespace=None) -> GameAsset:
if update or platform not in self.lgd.assets:
self.get_assets(update_assets=True, platform=platform)

try:
return next(i for i in self.lgd.assets[platform] if i.app_name == app_name)
return next(i for i in self.lgd.assets[platform] if i.app_name == app_name and (namespace is None or i.namespace == namespace))
except StopIteration:
raise ValueError

Expand Down Expand Up @@ -414,38 +421,44 @@ def get_game_and_dlc_list(self, update_assets=True, platform='Windows',
for _platform in platforms:
self.get_assets(update_assets=update_assets, platform=_platform)

library_items = self.get_library_items(force_refresh=update_assets)

if not self.lgd.assets:
return _ret, _dlc

assets = {}
for _platform, _assets in self.lgd.assets.items():
for _asset in _assets:
if _asset.app_name in assets:
assets[_asset.app_name][_platform] = _asset
assets[_asset.app_name][_platform].append(_asset)
else:
assets[_asset.app_name] = {_platform: _asset}
assets[_asset.app_name] = {_platform: [_asset]}

fetch_list = []
games = {}
sidecars = {}

for app_name, app_assets in sorted(assets.items()):
if skip_ue and any(v.namespace == 'ue' for v in app_assets.values()):
if skip_ue and any(v.namespace == 'ue' for _assets in app_assets.values() for v in _assets):
continue

game = self.lgd.get_game_meta(app_name)
asset_updated = sidecar_updated = False
if game:
asset_updated = any(game.app_version(_p) != app_assets[_p].build_version for _p in app_assets.keys())
asset_updated = any(game.app_version(_p, _a.namespace) != _a.build_version for _p in app_assets.keys() for _a in app_assets[_p])
# assuming sidecar data is the same for all platforms, just check the baseline (Windows) for updates.
sidecar_updated = (app_assets['Windows'].sidecar_rev > 0 and
(not game.sidecar or game.sidecar.rev != app_assets['Windows'].sidecar_rev))
sidecar_updated = any(_a.sidecar_rev > 0 and
(not game.sidecars or _a.namespace not in game.sidecars
or game.sidecars[_a.namespace].rev != _a.sidecar_rev)
for _a in app_assets['Windows'])
games[app_name] = game

if update_assets and (not game or force_refresh or (game and (asset_updated or sidecar_updated))):
self.log.debug(f'Scheduling metadata update for {app_name}')
# namespace/catalog item are the same for all platforms, so we can just use the first one
_ga = next(iter(app_assets.values()))
fetch_list.append((app_name, _ga.namespace, _ga.catalog_item_id, sidecar_updated))
_gas = next(iter(app_assets.values()))
for _ga in _gas:
fetch_list.append((app_name, _ga.namespace, _ga.catalog_item_id, sidecar_updated))
meta_updated = True

def fetch_game_meta(args):
Expand All @@ -455,7 +468,6 @@ def fetch_game_meta(args):
self.log.warning(f'App {app_name} does not have any metadata!')
eg_meta = dict(title='Unknown')

sidecar = None
if update_sidecar:
self.log.debug(f'Updating sidecar information for {app_name}...')
manifest_api_response = self.egs.get_game_manifest(namespace, catalog_item_id, app_name)
Expand All @@ -464,9 +476,13 @@ def fetch_game_meta(args):
if 'sidecar' in manifest_info:
sidecar_json = json.loads(manifest_info['sidecar']['config'])
sidecar = Sidecar(config=sidecar_json, rev=manifest_info['sidecar']['rvn'])
if not app_name in sidecars:
sidecars[app_name] = {namespace: sidecar}
else:
sidecars[app_name].update({namespace:sidecar})

game = Game(app_name=app_name, app_title=eg_meta['title'], metadata=eg_meta, asset_infos=assets[app_name],
sidecar=sidecar)
sidecars=sidecars.get(app_name))
self.lgd.set_game_meta(game.app_name, game)
games[app_name] = game
try:
Expand All @@ -484,23 +500,30 @@ def fetch_game_meta(args):
executor.map(fetch_game_meta, fetch_list, timeout=60.0)

for app_name, app_assets in sorted(assets.items()):
if skip_ue and any(v.namespace == 'ue' for v in app_assets.values()):
if skip_ue and any(v.namespace == 'ue' for a in app_assets.values() for v in a):
continue

game = games.get(app_name)
# retry if metadata is still missing/threaded loading wasn't used
if not game or app_name in still_needs_update:
if use_threads:
self.log.warning(f'Fetching metadata for {app_name} failed, retrying')
_ga = next(iter(app_assets.values()))
fetch_game_meta((app_name, _ga.namespace, _ga.catalog_item_id, True))
_gas = next(iter(app_assets.values()))
for _ga in _gas:
fetch_game_meta((app_name, _ga.namespace, _ga.catalog_item_id, True))
game = games[app_name]

if game.is_dlc and platform in app_assets:
_dlc[game.metadata['mainGameItem']['id']].append(game)
elif not any(i['path'] == 'mods' for i in game.metadata.get('categories', [])) and platform in app_assets:
_ret.append(game)

# append info about each library entry per namespace
for record in library_items:
if game := games.get(record["appName"]):
game.namespaces.update({record["namespace"]: record})
self.lgd.set_game_meta(game.app_name, game)

self.update_aliases(force=meta_updated)
if meta_updated:
self._prune_metadata()
Expand Down Expand Up @@ -543,7 +566,7 @@ def get_non_asset_library_items(self, force_refresh=False,
# broken old app name that we should always ignore
ignore |= {'1'}

for libitem in self.egs.get_library_items():
for libitem in self.get_library_items():
if libitem['namespace'] == 'ue' and skip_ue:
continue
if 'appName' not in libitem:
Expand Down Expand Up @@ -779,11 +802,13 @@ def get_launch_parameters(self, app_name: str, offline: bool = False,
'-AUTH_TYPE=exchangecode',
f'-epicapp={app_name}',
'-epicenv=Prod'])

namespace = install.namespace or game.namespace

if install.requires_ot and not offline:
self.log.info('Getting ownership token.')
ovt = self.egs.get_ownership_token(game.namespace, game.catalog_item_id)
ovt_path = os.path.join(self.lgd.get_tmp_path(), f'{game.namespace}{game.catalog_item_id}.ovt')
ovt = self.egs.get_ownership_token(namespace, game.catalog_item_id)
ovt_path = os.path.join(self.lgd.get_tmp_path(), f'{namespace}{game.catalog_item_id}.ovt')
with open(ovt_path, 'wb') as f:
f.write(ovt)
params.egl_parameters.append(f'-epicovt={ovt_path}')
Expand All @@ -797,10 +822,10 @@ def get_launch_parameters(self, app_name: str, offline: bool = False,
f'-epicusername={user_name}',
f'-epicuserid={account_id}',
f'-epiclocale={language_code}',
f'-epicsandboxid={game.namespace}'
f'-epicsandboxid={namespace}'
])

if sidecar := game.sidecar:
if sidecar := game.sidecars and game.sidecars.get(namespace):
if deployment_id := sidecar.config.get('deploymentId', None):
params.egl_parameters.append(f'-epicdeploymentid={deployment_id}')

Expand Down Expand Up @@ -1253,8 +1278,9 @@ def get_installed_manifest(self, app_name):
old_bytes = self.lgd.load_manifest(app_name, igame.version, igame.platform)
return old_bytes, igame.base_urls

def get_cdn_urls(self, game, platform='Windows'):
m_api_r = self.egs.get_game_manifest(game.namespace, game.catalog_item_id,
def get_cdn_urls(self, game, platform='Windows', namespace=None):
ns = namespace or game.namespace
m_api_r = self.egs.get_game_manifest(ns, game.catalog_item_id,
game.app_name, platform)

# never seen this outside the launcher itself, but if it happens: PANIC!
Expand All @@ -1277,8 +1303,8 @@ def get_cdn_urls(self, game, platform='Windows'):

return manifest_urls, base_urls, manifest_hash

def get_cdn_manifest(self, game, platform='Windows', disable_https=False):
manifest_urls, base_urls, manifest_hash = self.get_cdn_urls(game, platform)
def get_cdn_manifest(self, game, platform='Windows', namespace=None, disable_https=False):
manifest_urls, base_urls, manifest_hash = self.get_cdn_urls(game, platform, namespace)
if not manifest_urls:
raise ValueError('No manifest URLs returned by API')

Expand Down Expand Up @@ -1336,7 +1362,7 @@ def prepare_download(self, game: Game, base_game: Game = None, base_path: str =
override_old_manifest: str = '', override_base_url: str = '',
platform: str = 'Windows', file_prefix_filter: list = None,
file_exclude_filter: list = None, file_install_tag: list = None,
read_files: bool = False,
read_files: bool = False, namespace: str = None,
dl_optimizations: bool = False, dl_timeout: int = 10,
repair: bool = False, repair_use_latest: bool = False,
disable_delta: bool = False, override_delta_manifest: str = '',
Expand Down Expand Up @@ -1373,7 +1399,7 @@ def prepare_download(self, game: Game, base_game: Game = None, base_path: str =
if _base_urls:
base_urls = _base_urls
else:
new_manifest_data, base_urls = self.get_cdn_manifest(game, platform, disable_https=disable_https)
new_manifest_data, base_urls = self.get_cdn_manifest(game, platform, namespace=namespace, disable_https=disable_https)
# overwrite base urls in metadata with current ones to avoid using old/dead CDNs
game.base_urls = base_urls
# save base urls to game metadata
Expand Down Expand Up @@ -1566,7 +1592,8 @@ def prepare_download(self, game: Game, base_game: Game = None, base_path: str =
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,
platform=platform, uninstaller=uninstaller)
platform=platform, uninstaller=uninstaller,
namespace=namespace)

return dlm, anlres, igame

Expand Down Expand Up @@ -1908,10 +1935,10 @@ def egl_export(self, app_name):
# copy manifest and create mancpn file in .egstore folder
with open(os.path.join(egstore_folder, f'{egl_game.installation_guid}.manifest', ), 'wb') as mf:
mf.write(manifest_data)

ns = lgd_igame.namespace or lgd_game.namespace
mancpn = dict(FormatVersion=0, AppName=app_name,
CatalogItemId=lgd_game.catalog_item_id,
CatalogNamespace=lgd_game.namespace)
CatalogNamespace=ns)
with open(os.path.join(egstore_folder, f'{egl_game.installation_guid}.mancpn', ), 'w') as mcpnf:
json.dump(mancpn, mcpnf, indent=4, sort_keys=True)

Expand Down
Loading
Loading