[cli/core/models/utils] macOS cloud save support

This commit is contained in:
derrod 2021-12-02 15:24:01 +01:00
parent 4eaa608370
commit 1dfc5aabe7
4 changed files with 46 additions and 19 deletions

View file

@ -447,7 +447,7 @@ class LegendaryCLI:
continue continue
game = self.core.get_game(igame.app_name) 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: if igame.app_name in latest_save:
# this should never happen unless cloud save support was removed from a game # 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?!') 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 there is no saved save path, try to get one
if not igame.save_path: 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 # ask user if path is correct if computing for the first time
logger.info(f'Computed save path: "{save_path}"') logger.info(f'Computed save path: "{save_path}"')
@ -902,7 +902,7 @@ class LegendaryCLI:
end_t = time.time() end_t = time.time()
if not args.no_install: if not args.no_install:
# Allow setting savegame directory at install time so sync-saves will work immediately # 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 igame.save_path = args.save_path
postinstall = self.core.install_game(igame) postinstall = self.core.install_game(igame)
@ -930,7 +930,7 @@ class LegendaryCLI:
self.install_game(args) self.install_game(args)
args.yes, args.app_name = _yes, _app_name 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 # todo option to automatically download saves after the installation
# args does not have the required attributes for sync_saves in here, # args does not have the required attributes for sync_saves in here,
# not sure how to solve that elegantly. # not sure how to solve that elegantly.
@ -1399,13 +1399,20 @@ class LegendaryCLI:
game.app_version(args.platform))) game.app_version(args.platform)))
all_versions = {k: v.build_version for k,v in game.asset_infos.items()} 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)) 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_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: if game.supports_cloud_saves:
cs_dir = game.metadata['customAttributes']['CloudSaveFolder']['value'] cs_dir = game.metadata['customAttributes']['CloudSaveFolder']['value']
else: game_infos.append(InfoItem('Cloud save folder (Windows)', 'cloud_save_folder', cs_dir, cs_dir))
cs_dir = None
game_infos.append(InfoItem('Cloud save folder', '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)) game_infos.append(InfoItem('Is DLC', 'is_dlc', game.is_dlc, game.is_dlc))
# Find custom launch options, if available # Find custom launch options, if available
launch_options = [] launch_options = []

View file

@ -14,6 +14,7 @@ from multiprocessing import Queue
from platform import system from platform import system
from requests import session from requests import session
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from sys import platform as sys_platform
from uuid import uuid4 from uuid import uuid4
from urllib.parse import urlencode, parse_qsl 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 update_assets and (not game or force_refresh or (game and asset_updated)):
if game and asset_updated: if game and asset_updated:
self.log.info(f'Updating meta for {game.app_name} due to build version mismatch') 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 # namespace/catalog item are the same for all platforms, so we can just use the first one
_ga = next(iter(app_assets.values())) _ga = next(iter(app_assets.values()))
eg_meta = self.egs.get_game_info(_ga.namespace, _ga.catalog_item_id) eg_meta = self.egs.get_game_info(_ga.namespace, _ga.catalog_item_id)
@ -673,9 +674,14 @@ class LegendaryCore:
return _saves 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) 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: if not save_path:
raise ValueError('Game does not support cloud saves') raise ValueError('Game does not support cloud saves')
@ -689,13 +695,19 @@ class LegendaryCore:
'{epicid}': self.lgd.userdata['account_id'] '{epicid}': self.lgd.userdata['account_id']
} }
if os.name == 'nt': if sys_platform == 'win32':
path_vars.update({ path_vars.update({
'{appdata}': os.path.expandvars('%LOCALAPPDATA%'), '{appdata}': os.path.expandvars('%LOCALAPPDATA%'),
'{userdir}': os.path.expandvars('%userprofile%/documents'), '{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') '{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: else:
# attempt to get WINE prefix from config # attempt to get WINE prefix from config
wine_pfx = self.lgd.config.get(app_name, 'wine_prefix', fallback=None) 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 # these paths should always use a forward slash
new_save_path = [path_vars.get(p.lower(), p) for p in save_path.split('/')] 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)) absolute_path = os.path.realpath(os.path.join(*new_save_path))
# attempt to resolve as much as possible on case-insensitive file-systems # attempt to resolve as much as possible on case-sensitive file-systems
if os.name != 'nt': if os.name != 'nt' and platform != 'Mac':
absolute_path = case_insensitive_path_search(absolute_path) absolute_path = case_insensitive_path_search(absolute_path)
return absolute_path return absolute_path
@ -765,6 +777,7 @@ class LegendaryCore:
game = self.lgd.get_game_meta(app_name) game = self.lgd.get_game_meta(app_name)
custom_attr = game.metadata['customAttributes'] custom_attr = game.metadata['customAttributes']
save_path = custom_attr.get('CloudSaveFolder', {}).get('value') save_path = custom_attr.get('CloudSaveFolder', {}).get('value')
save_path_mac = custom_attr.get('CloudSaveFolder_MAC', {}).get('value')
include_f = exclude_f = None include_f = exclude_f = None
if not disable_filtering: if not disable_filtering:
@ -774,12 +787,12 @@ class LegendaryCore:
if (_exclude := custom_attr.get('CloudExcludeList', {}).get('value', None)) is not None: if (_exclude := custom_attr.get('CloudExcludeList', {}).get('value', None)) is not None:
exclude_f = _exclude.split(',') 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') raise ValueError('Game does not support cloud saves')
sgh = SaveGameHelper() sgh = SaveGameHelper()
files = sgh.package_savegame(save_dir, app_name, self.egs.user.get('account_id'), 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: if not files:
self.log.info('No files to upload. If you believe this is incorrect run command with "--disable-filters"') self.log.info('No files to upload. If you believe this is incorrect run command with "--disable-filters"')

View file

@ -75,6 +75,10 @@ class Game:
def supports_cloud_saves(self): def supports_cloud_saves(self):
return self.metadata and (self.metadata.get('customAttributes', {}).get('CloudSaveFolder') is not None) 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 @property
def catalog_item_id(self): def catalog_item_id(self):
if not self.metadata: if not self.metadata:

View file

@ -51,8 +51,8 @@ class SaveGameHelper:
_tmp_file.seek(0) _tmp_file.seek(0)
return ci return ci
def package_savegame(self, input_folder: str, app_name: str = '', def package_savegame(self, input_folder: str, app_name: str = '', epic_id: str = '',
epic_id: str = '', cloud_folder: str = '', cloud_folder: str = '', cloud_folder_mac: str = '',
include_filter: list = None, include_filter: list = None,
exclude_filter: list = None, exclude_filter: list = None,
manifest_dt: datetime = None): manifest_dt: datetime = None):
@ -61,6 +61,7 @@ class SaveGameHelper:
:param app_name: App name for savegame being stored :param app_name: App name for savegame being stored
:param epic_id: Epic account ID :param epic_id: Epic account ID
:param cloud_folder: Folder the savegame resides in (based on game metadata) :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 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 exclude_filter: list of patterns for files to exclude (includes all others)
:param manifest_dt: datetime for the manifest name (optional) :param manifest_dt: datetime for the manifest name (optional)
@ -77,6 +78,8 @@ class SaveGameHelper:
manifest_dt = datetime.utcnow() manifest_dt = datetime.utcnow()
m.meta.build_version = manifest_dt.strftime('%Y.%m.%d-%H.%M.%S') m.meta.build_version = manifest_dt.strftime('%Y.%m.%d-%H.%M.%S')
m.custom_fields['CloudSaveFolder'] = cloud_folder 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}') self.log.info(f'Packing savegame for "{app_name}", input folder: {input_folder}')
files = [] files = []