diff --git a/legendary/cli.py b/legendary/cli.py index dca3f7e..5c6e305 100644 --- a/legendary/cli.py +++ b/legendary/cli.py @@ -447,7 +447,7 @@ class LegendaryCLI: continue game = self.core.get_game(igame.app_name) - if not game or not game.supports_cloud_saves: + if not game or not (game.supports_cloud_saves or game.supports_mac_cloud_saves): if igame.app_name in latest_save: # this should never happen unless cloud save support was removed from a game logger.warning(f'{igame.app_name} has remote save(s) but does not support cloud saves?!') @@ -462,7 +462,7 @@ class LegendaryCLI: # if there is no saved save path, try to get one if not igame.save_path: - save_path = self.core.get_save_path(igame.app_name) + save_path = self.core.get_save_path(igame.app_name, platform=igame.platform) # ask user if path is correct if computing for the first time logger.info(f'Computed save path: "{save_path}"') @@ -902,7 +902,7 @@ class LegendaryCLI: end_t = time.time() if not args.no_install: # Allow setting savegame directory at install time so sync-saves will work immediately - if game.supports_cloud_saves and args.save_path: + if (game.supports_cloud_saves or game.supports_mac_cloud_saves) and args.save_path: igame.save_path = args.save_path postinstall = self.core.install_game(igame) @@ -930,7 +930,7 @@ class LegendaryCLI: self.install_game(args) args.yes, args.app_name = _yes, _app_name - if game.supports_cloud_saves and not game.is_dlc: + if (game.supports_cloud_saves or game.supports_mac_cloud_saves) and not game.is_dlc: # todo option to automatically download saves after the installation # args does not have the required attributes for sync_saves in here, # not sure how to solve that elegantly. @@ -1399,13 +1399,20 @@ class LegendaryCLI: 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)) + # Cloud save support for Mac and Windows game_infos.append(InfoItem('Cloud saves supported', 'cloud_saves_supported', - game.supports_cloud_saves, game.supports_cloud_saves)) + game.supports_cloud_saves or game.supports_mac_cloud_saves, + game.supports_cloud_saves or game.supports_mac_cloud_saves)) + cs_dir = None if game.supports_cloud_saves: cs_dir = game.metadata['customAttributes']['CloudSaveFolder']['value'] - else: - cs_dir = None - game_infos.append(InfoItem('Cloud save folder', 'cloud_save_folder', cs_dir, cs_dir)) + game_infos.append(InfoItem('Cloud save folder (Windows)', 'cloud_save_folder', cs_dir, cs_dir)) + + cs_dir = None + if game.supports_mac_cloud_saves: + cs_dir = game.metadata['customAttributes']['CloudSaveFolder_MAC']['value'] + game_infos.append(InfoItem('Cloud save folder (Mac)', 'cloud_save_folder_mac', cs_dir, cs_dir)) + game_infos.append(InfoItem('Is DLC', 'is_dlc', game.is_dlc, game.is_dlc)) # Find custom launch options, if available launch_options = [] diff --git a/legendary/core.py b/legendary/core.py index 46b7b6f..10703f7 100644 --- a/legendary/core.py +++ b/legendary/core.py @@ -14,6 +14,7 @@ from multiprocessing import Queue from platform import system from requests import session from requests.exceptions import HTTPError +from sys import platform as sys_platform from uuid import uuid4 from urllib.parse import urlencode, parse_qsl @@ -398,7 +399,7 @@ class LegendaryCore: if update_assets and (not game or force_refresh or (game and asset_updated)): if game and asset_updated: self.log.info(f'Updating meta for {game.app_name} due to build version mismatch') - + # namespace/catalog item are the same for all platforms, so we can just use the first one _ga = next(iter(app_assets.values())) eg_meta = self.egs.get_game_info(_ga.namespace, _ga.catalog_item_id) @@ -673,9 +674,14 @@ class LegendaryCore: return _saves - def get_save_path(self, app_name): + def get_save_path(self, app_name, platform='Windows'): game = self.lgd.get_game_meta(app_name) - save_path = game.metadata['customAttributes'].get('CloudSaveFolder', {}).get('value') + + if platform == 'Mac': + save_path = game.metadata['customAttributes'].get('CloudSaveFolder_MAC', {}).get('value') + else: + save_path = game.metadata['customAttributes'].get('CloudSaveFolder', {}).get('value') + if not save_path: raise ValueError('Game does not support cloud saves') @@ -689,13 +695,19 @@ class LegendaryCore: '{epicid}': self.lgd.userdata['account_id'] } - if os.name == 'nt': + if sys_platform == 'win32': path_vars.update({ '{appdata}': os.path.expandvars('%LOCALAPPDATA%'), '{userdir}': os.path.expandvars('%userprofile%/documents'), - # '{userprofile}': os.path.expandvars('%userprofile%'), # possibly wrong + '{userprofile}': os.path.expandvars('%userprofile%'), '{usersavedgames}': os.path.expandvars('%userprofile%/Saved Games') }) + elif sys_platform == 'darwin' and platform == 'Mac': + path_vars.update({ + '{appdata}': os.path.expandvars('~/Library/Application Support'), + '{userdir}': os.path.expandvars('~/Documents'), + '{userlibrary}': os.path.expandvars('~/Library') + }) else: # attempt to get WINE prefix from config wine_pfx = self.lgd.config.get(app_name, 'wine_prefix', fallback=None) @@ -724,8 +736,8 @@ class LegendaryCore: # these paths should always use a forward slash new_save_path = [path_vars.get(p.lower(), p) for p in save_path.split('/')] absolute_path = os.path.realpath(os.path.join(*new_save_path)) - # attempt to resolve as much as possible on case-insensitive file-systems - if os.name != 'nt': + # attempt to resolve as much as possible on case-sensitive file-systems + if os.name != 'nt' and platform != 'Mac': absolute_path = case_insensitive_path_search(absolute_path) return absolute_path @@ -765,6 +777,7 @@ class LegendaryCore: game = self.lgd.get_game_meta(app_name) custom_attr = game.metadata['customAttributes'] save_path = custom_attr.get('CloudSaveFolder', {}).get('value') + save_path_mac = custom_attr.get('CloudSaveFolder_MAC', {}).get('value') include_f = exclude_f = None if not disable_filtering: @@ -774,12 +787,12 @@ class LegendaryCore: if (_exclude := custom_attr.get('CloudExcludeList', {}).get('value', None)) is not None: exclude_f = _exclude.split(',') - if not save_path: + if not save_path and not save_path_mac: raise ValueError('Game does not support cloud saves') sgh = SaveGameHelper() files = sgh.package_savegame(save_dir, app_name, self.egs.user.get('account_id'), - save_path, include_f, exclude_f, local_dt) + save_path, save_path_mac, include_f, exclude_f, local_dt) if not files: self.log.info('No files to upload. If you believe this is incorrect run command with "--disable-filters"') diff --git a/legendary/models/game.py b/legendary/models/game.py index df36282..077682d 100644 --- a/legendary/models/game.py +++ b/legendary/models/game.py @@ -75,6 +75,10 @@ class Game: def supports_cloud_saves(self): return self.metadata and (self.metadata.get('customAttributes', {}).get('CloudSaveFolder') is not None) + @property + def supports_mac_cloud_saves(self): + return self.metadata and (self.metadata.get('customAttributes', {}).get('CloudSaveFolder_MAC') is not None) + @property def catalog_item_id(self): if not self.metadata: diff --git a/legendary/utils/savegame_helper.py b/legendary/utils/savegame_helper.py index d7b36e8..2c01ddf 100644 --- a/legendary/utils/savegame_helper.py +++ b/legendary/utils/savegame_helper.py @@ -51,8 +51,8 @@ class SaveGameHelper: _tmp_file.seek(0) return ci - def package_savegame(self, input_folder: str, app_name: str = '', - epic_id: str = '', cloud_folder: str = '', + def package_savegame(self, input_folder: str, app_name: str = '', epic_id: str = '', + cloud_folder: str = '', cloud_folder_mac: str = '', include_filter: list = None, exclude_filter: list = None, manifest_dt: datetime = None): @@ -61,6 +61,7 @@ class SaveGameHelper: :param app_name: App name for savegame being stored :param epic_id: Epic account ID :param cloud_folder: Folder the savegame resides in (based on game metadata) + :param cloud_folder_mac: Folder the macOS savegame resides in (based on game metadata) :param include_filter: list of patterns for files to include (excludes all others) :param exclude_filter: list of patterns for files to exclude (includes all others) :param manifest_dt: datetime for the manifest name (optional) @@ -77,6 +78,8 @@ class SaveGameHelper: manifest_dt = datetime.utcnow() m.meta.build_version = manifest_dt.strftime('%Y.%m.%d-%H.%M.%S') m.custom_fields['CloudSaveFolder'] = cloud_folder + if cloud_folder_mac: + m.custom_fields['CloudSaveFolder_MAC'] = cloud_folder_mac self.log.info(f'Packing savegame for "{app_name}", input folder: {input_folder}') files = []