def ask_user_to_choose_in_a_list(text, file_list): """ Defines the container to use Print every *container* file in the given and make the user choose one :param containers_list: A list of identified to be containers :returns: The chosen container filename """ max_container_number = len(file_list) min_container_number = 1 choice_index = 0 while choice_index == 0: Logger.logPrint('\nWhich container would you like to convert ?') print_list_elements(file_list) choice_index = input() try: choice_index = int(choice_index) verify_choice_input(choice_index, min_container_number, max_container_number) except ValueError: choice_index = 0 Logger.logPrint( f'Please use only values between {min_container_number} and {max_container_number}' ) return file_list[choice_index - 1]
def ask_for_multiple_choices(maximum_value) -> list: """ Let the user choose multiple numbers between 0 and a maximum value If the user choice is 0 then return an array with all values, the user choices are reduce by 1 in order to match future array indexes :Example: User choices: - [1,2,4] - returns 0, 1, 2 - [0] - returns all choices :return: The list of numbers :exception: None (repeat until the choices are valid) """ choices = [] while not choices: choices = input() try: choices = process_multiple_choices_input(choices) verify_choices_input(choices, maximum_value) except ValueError: choices = [] Logger.logPrint( f'Please use only values between 1 and {maximum_value} or 0 alone' ) if choices == [-1]: return list(range(0, maximum_value)) else: return choices
def __init__(self, container_file_path): """ Extracts the saves from an Astroneer save container Reads the container file, divides it into chunks and regroups the chunks into save objects Arguments: container_file_path -- Path to the container file Returns: The AstroSaveContainer object Exception: None """ self.full_path = container_file_path self.save_list = [] Logger.logPrint('full_path: {self.full_path}', "debug") with open(self.full_path, "rb") as container: # The Astroneer file type is contained in at least the first 2 bytes of the file self.header = container.read(2) if not self.is_valid_container_header(self.header): raise Exception( f'The save container {self.full_path} is not valid (First two bytes:{self.header})' ) # Skipping 2 bytes that may be part of the header container.read(2) # Next 4 bytes are the number of saves chunk self.chunk_count = int.from_bytes(container.read(4), byteorder='little') # Parsing saves chunks current_save_name = None for _ in range(self.chunk_count): current_chunk = container.read(CHUNK_METADATA_SIZE) current_chunk_name = self.extract_name_from_chunk( current_chunk) if current_chunk_name != current_save_name: if current_save_name != None: # Parsing a new save, storing the current save self.save_list.append( AstroSave(current_save_name, current_chunks_names)) current_chunks_names = [] current_save_name = current_chunk_name current_chunks_names.append( self.extract_chunk_file_name_from_chunk(current_chunk)) # Saving the last save of the container file self.save_list.append( AstroSave(current_save_name, current_chunks_names))
def backup_win_before_steam_export() -> str: Logger.logPrint( '\nFor safety reasons, we will now copy your current Microsoft Astroneer saves' ) backup_path = ask_copy_target('MicrosoftAstroneerSavesBackup') astroneer_save_folder = AstroMicrosoftSaveFolder.backup_microsoft_save_folder( backup_path) return astroneer_save_folder
def seek_microsoft_save_folder(appdata_path) -> str: folders = get_save_folders_from_path(appdata_path) if not folders: Logger.logPrint(f'No save folder found.', 'debug') raise FileNotFoundError elif len(folders) != 1: # We are not supposed to have more than one save folder Logger.logPrint(f'More than one save folders was found:\n {folders}', 'debug') raise MultipleFolderFoundError return folders[0]
def ask_overwrite_if_file_exists(filename: str, target: str) -> bool: file_url = utils.join_paths(target, filename) if utils.is_path_exists(file_url): do_overwrite = None while do_overwrite not in ('y', 'n'): Logger.logPrint( f'\nFile {filename} already exists, overwrite it ? (y/n)') do_overwrite = input().lower() return do_overwrite == 'y' else: return True
def ask_conversion_type() -> AstroConvType: Logger.logPrint(f'\nWhich conversion do you want to do ?') Logger.logPrint("\t1) Convert a Microsoft save into a Steam save") Logger.logPrint('\t2) Convert a Steam save into a Microsoft save') choice = input() while choice not in ('1', '2'): Logger.logPrint(f'\nPlease choose 1 or 2') choice = input() Logger.logPrint(f'convert_choice {choice}', 'debug') if choice == '1': return AstroConvType.WIN2STEAM else: return AstroConvType.STEAM2WIN
def ask_rename_saves(saves_indexes, container): """ Guide the user in order to rename a save :param save_indexes: List of the saves in the container.save_list you want to rename :param container: Container from which to rename the save """ do_rename = None while do_rename not in ('y', 'n'): Logger.logPrint('\nWould you like to rename a save ? (y/n)') do_rename = input().lower() if do_rename == 'y': for index in saves_indexes: save = container.save_list[index] rename_save(save)
def ask_rename_saves(saves_indexes, save_list): """ Guide the user in order to rename a save :param save_indexes: List of the saves in the save_list you want to rename :param save_list: List of the save objects you may rename """ do_rename = None while do_rename not in ('y', 'n'): Logger.logPrint('\nWould you like to rename a save ? (y/n)') do_rename = input().lower() if do_rename == 'y': for index in saves_indexes: save = save_list[index] rename_save(save)
def rename_save(save): """ Rename a save Rename can be skipped by pressing Enter directly :param save: Save object to be renamed """ new_name = None while new_name is None: new_name = input( f'\nNew name for {save.name.split("$")[0]}: [ENTER = unchanged] > ' ).upper() if (new_name != ''): try: save.rename(new_name) except ValueError: new_name = None Logger.logPrint(f'Please use only alphanum and a length < 30')
def get_save_folders_from_path(path) -> list: microsoft_save_folders = [] for root, _, files in os.walk(path): for file in files: if re.search(r'^container\.', file): container_full_path = utils.join_paths(root, file) Logger.logPrint(f'Container file found:{container_full_path}', 'debug') container_text = read_container_text_from_path(container_full_path) if do_container_text_match_date(container_text): Logger.logPrint(f'Matching save folder {root}', 'debug') microsoft_save_folders.append(root) return microsoft_save_folders
def convert_to_xbox(self, source: str) -> Tuple[List[uuid.UUID], List[BytesIO]]: """Exports a save as a tuple in its Xbox file format The save is returned as a tuple (chunks names, chunk buffers) representing all of its chunks Each element of the list is a chunk of the save. The order of the elements matters. Arguments: source: In which folder to read the Steam save Returns: A tuple containing the names and the buffers uuid of the Xbox chunks """ # TODO [enhance] this functionned could be renamed by something like load_save_from_steam_file # and the whole AstroSave class modified to store the uuids list instead of chunk names list + to store the whole buffer of each chunk # That would make more sense and the tuple wouldn't need to be returned # The reading of the saves from a container would also be simplified by a lot (by building a uuid from the bytes read in the container) buffer_uuids = [] buffers = [] self.chunks_names = [] len_read = XBOX_CHUNK_SIZE save_file_path = source with open(save_file_path, 'rb') as save_file: while len_read == XBOX_CHUNK_SIZE: buffer = BytesIO() file_uuid = uuid.uuid4() Logger.logPrint(f'UUID generated: {file_uuid}', "debug") buffer.write(save_file.read(XBOX_CHUNK_SIZE)) len_read = len(buffer.getvalue()) self.chunks_names.append(file_uuid.hex.upper()) buffer_uuids.append(file_uuid) buffers.append(buffer) return (buffer_uuids, buffers)
def get_microsoft_save_folder() -> str: """ Retrieves the microsoft save folders from %LocalAppdata% We know that the saves are stored along with a container.* file. We look for that specific container by checking if it contains a save date in order to return the whole path :return: The list of the microsoft save folder content found in %appdata% :exception: FileNotFoundError if no save folder is found :exception: MultipleFolderFoundError if multiple save folder are found """ try: target = os.environ[ 'LOCALAPPDATA'] + '\\Packages\\SystemEraSoftworks*\\SystemAppData\\wgs' except KeyError: Logger.logPrint("Local Appdata are missing, maybe you're on linux ?") Logger.logPrint("Press any key to exit") utils.wait_and_exit(1) microsoft_save_paths = list(glob.iglob(target)) for path in microsoft_save_paths: Logger.logPrint(f'SES path found in appadata: {path}', 'debug') SES_appdata_path = microsoft_save_paths[-1] microsoft_save_folder = seek_microsoft_save_folder(SES_appdata_path) return microsoft_save_folder
def ask_custom_folder_path() -> str: Logger.logPrint(f'\nEnter your custom folder path:') path = input() Logger.logPrint(f'save_folder_path {path}', 'debug') if utils.is_folder_a_dir(path): return path else: Logger.logPrint( f'\nWrong path for save folder, please enter a valid path : ') return ask_custom_folder_path()
def ask_saves_to_export(save_list): Logger.logPrint('Extracted save list :') print_save_from_container(save_list) Logger.logPrint( '\nWhich saves would you like to convert ? (Choose 0 for all of them)') Logger.logPrint('(Multi-convert is supported. Ex: "1,2,4")') maximum_save_number = len(save_list) saves_to_export = ask_for_multiple_choices(maximum_save_number) return saves_to_export
def ask_saves_to_export(save_list: List[AstroSave]) -> List[int]: """TODO [doc] explain that this function returns the indexes in the save list and not a sublist of the save_list """ Logger.logPrint('Extracted save list :') print_save_from_container(save_list) Logger.logPrint( '\nWhich saves would you like to convert ? (Choose 0 for all of them)') Logger.logPrint('(Multi-convert is supported. Ex: "1,2,4")') maximum_save_number = len(save_list) saves_to_export = ask_for_multiple_choices(maximum_save_number) return saves_to_export
def get_steam_save_folder() -> str: """ Retrieves the Steam save folders from %LocalAppdata% :return: The Steam save folder content found in %appdata% :exception: FileNotFoundError if no save folder is found :exception: MultipleFolderFoundError if multiple save folder are found """ try: target = os.environ['LOCALAPPDATA'] + '\\Astro\\Saved\\SaveGames' except KeyError: Logger.logPrint("Local Appdata are missing, maybe you're on linux ?") Logger.logPrint("Press any key to exit") utils.wait_and_exit(1) steam_save_paths = list(glob.iglob(target)) for path in steam_save_paths: Logger.logPrint(f'SES path found in appadata: {path}', 'debug') return steam_save_paths[0]
def steam_to_windows_conversion(original_save_path: str) -> None: Logger.logPrint('\n\n/!\\ WARNING /!\\') Logger.logPrint( '/!\\ Astroneer needs to be closed longer than 20 seconds before we can start exporting your saves /!\\' ) Logger.logPrint( '/!\\ More info and save restoring procedure are available on Github (cf. README) /!\\' ) loading_bar = LoadingBar(15) loading_bar.start_loading() xbox_astroneer_save_folder = Scenario.backup_win_before_steam_export() steamsave_files_list = AstroSave.get_steamsaves_list(original_save_path) saves_list = AstroSave.init_saves_list_from(steamsave_files_list) original_saves_name = [] for save in saves_list: original_saves_name.append(save.name) saves_indexes_to_export = Scenario.ask_saves_to_export(saves_list) Scenario.ask_rename_saves(saves_indexes_to_export, saves_list) Logger.logPrint( f'\nExtracting saves {str([i+1 for i in saves_indexes_to_export])}') Logger.logPrint( f'Working folder: {original_save_path} Export to: {xbox_astroneer_save_folder}', "debug") for save_index in saves_indexes_to_export: save = saves_list[save_index] original_save_full_path = utils.join_paths( original_save_path, original_saves_name[save_index] + '.savegame') Scenario.export_save_to_xbox(save, original_save_full_path, xbox_astroneer_save_folder) Logger.logPrint(f"\nSave {save.name} has been exported succesfully.")
def windows_to_steam_conversion(original_save_path: str) -> None: containers_list = Container.get_containers_list(original_save_path) Logger.logPrint('\nContainers found:' + str(containers_list)) container_name = Scenario.ask_for_containers_to_convert( containers_list) if len(containers_list) > 1 else containers_list[0] container_url = utils.join_paths(original_save_path, container_name) Logger.logPrint('\nInitializing Astroneer save container...') container = Container(container_url) Logger.logPrint(f'Detected chunks: {container.chunk_count}') Logger.logPrint('Container file loaded successfully !\n') saves_to_export = Scenario.ask_saves_to_export(container.save_list) Scenario.ask_rename_saves(saves_to_export, container.save_list) to_path = utils.join_paths(original_save_path, 'Steam saves') utils.make_dir_if_doesnt_exists(to_path) Logger.logPrint( f'\nExtracting saves {str([i+1 for i in saves_to_export])}') Logger.logPrint(f'Container: {container.full_path} Export to: {to_path}', "debug") for save_index in saves_to_export: save = container.save_list[save_index] Scenario.ask_overwrite_save_while_file_exists(save, to_path) Scenario.export_save_to_steam(save, original_save_path, to_path) Logger.logPrint(f"\nSave {save.name} has been exported succesfully.")
f'Working folder: {original_save_path} Export to: {xbox_astroneer_save_folder}', "debug") for save_index in saves_indexes_to_export: save = saves_list[save_index] original_save_full_path = utils.join_paths( original_save_path, original_saves_name[save_index] + '.savegame') Scenario.export_save_to_xbox(save, original_save_full_path, xbox_astroneer_save_folder) Logger.logPrint(f"\nSave {save.name} has been exported succesfully.") if __name__ == "__main__": try: Logger.setup_logging(os.getcwd()) try: os.system( "title AstroSaveConverter 2.0 - Convert your Astroneer saves between Microsoft and Steam" ) except: pass args = get_args() conversion_type = Scenario.ask_conversion_type() try: if not args.savesPath: original_save_path = Scenario.ask_for_save_folder(
def ask_for_save_folder(conversion_type: AstroConvType) -> str: """ Obtains the save folder Lets the user pick between automatic save retrieving/copying or a custom save folder Arguments: conversion_type : Type of save conversion (for automatic folder retrieval purpose) Returns: The save folder path """ while 1: try: Logger.logPrint("Which folder would you like to work with ?") Logger.logPrint( "\t1) Automatically detect and copy my save folder (Please close Astroneer first)" ) Logger.logPrint("\t2) Chose a custom folder") work_choice = input() while work_choice not in ('1', '2'): Logger.logPrint(f'\nPlease choose 1 or 2') work_choice = input() Logger.logPrint(f'folder_type {work_choice}', 'debug') if work_choice == '1': if conversion_type == AstroConvType.WIN2STEAM: astroneer_save_folder = AstroMicrosoftSaveFolder.get_microsoft_save_folder( ) Logger.logPrint( f'Microsoft folder path: {astroneer_save_folder}', 'debug') else: astroneer_save_folder = AstroSteamSaveFolder.get_steam_save_folder( ) Logger.logPrint( f'Steam folder path: {astroneer_save_folder}', 'debug') save_path = ask_copy_target('AstroSaveFolder') utils.copy_files(astroneer_save_folder, save_path) Logger.logPrint(f'Save files copied to: {save_path}') elif work_choice == '2': save_path = ask_custom_folder_path() return save_path except MultipleFolderFoundError: Logger.logPrint( f'\nToo many save folders found ! Please use custom folder mode.' ) except FileNotFoundError as e: Logger.logPrint('\nNo container found in path: ' + save_path) Logger.logPrint(e, 'exception')
def print_list_elements(elements): for i, container in elements: Logger.logPrint(f'\t {i+1}) {container}')
def steam_to_windows_conversion(original_save_path: str) -> None: Logger.logPrint('\n\n/!\\ WARNING /!\\') Logger.logPrint( '/!\\ Astroneer needs to be closed longer than 20 seconds before we can start exporting your saves /!\\' ) Logger.logPrint( '/!\\ More info and save restoring procedure are available on Github (cf. README) /!\\' ) # TODO Elfou loading bar => "safety bar" (15 sec) xbox_astroneer_save_folder = Scenario.backup_win_before_steam_export() steamsave_files_list = AstroSave.get_steamsaves_list(original_save_path) saves_list = AstroSave.init_saves_list_from(steamsave_files_list) saves_indexes_to_export = Scenario.ask_saves_to_export(saves_list) Scenario.ask_rename_saves(saves_indexes_to_export, saves_list) Logger.logPrint( f'\nExtracting saves {str([i+1 for i in saves_indexes_to_export])}') Logger.logPrint( f'Working folder: {original_save_path} Export to: {xbox_astroneer_save_folder}', "debug") for save_index in saves_indexes_to_export: save = saves_list[save_index] Scenario.export_save_to_xbox(save, original_save_path, xbox_astroneer_save_folder) # TODO # Retrieve the real Microsoft gamepass astro container full path # Export save objects to container (prepare chunks in hexa and concat them to the container) # If export failed, delete all the chunk files written to disk Logger.logPrint(f"\nSave {save.name} has been exported succesfully.")
def print_save_from_container(save_list): """ Displays the human readable saves of a container """ for i, save in enumerate(save_list): Logger.logPrint(f'\t {str(i+1)}) {save.name}')
def export_save_to_xbox(save: AstroSave, from_file: str, to_path: str) -> None: chunk_uuids, converted_chunks = save.convert_to_xbox(from_file) chunk_count = len(chunk_uuids) if chunk_count >= 10: Logger.logPrint( f'The selected save contains {chunk_count} which is over the 9 chunks limit AstroSaveconverter can handle yet' ) Logger.logPrint( f'Congrats for having such a huge save, please open an issue on the GitHub :D' ) for i in range(chunk_count): # The file name is the HEX upper form of the uuid chunk_name = save.chunks_names[i] Logger.logPrint(f'UUID as file name: {chunk_name}', "debug") target_full_path = utils.join_paths(to_path, chunk_name) Logger.logPrint(f'Chunk file written to: {target_full_path}', "debug") # Regenerating chunk name if it already exists. Very, very unlikely while utils.is_path_exists(target_full_path): Logger.logPrint(f'UUID: {chunk_name} already exists ! (omg)', "debug") chunk_uuids[i] = save.regenerate_uuid(i) chunk_name = save.chunks_names[i] Logger.logPrint(f'Regenerated UUID: {chunk_name}', "debug") target_full_path = utils.join_paths(to_path, chunk_name) # TODO [enhance] raise exception if can't write, catch it then delete all the chunks already written and exit utils.write_buffer_to_file(target_full_path, converted_chunks[i]) # Container is updated only after all the chunks of the save have been written successfully container_file_name = Container.get_containers_list(to_path)[0] container_full_path = utils.join_paths(to_path, container_file_name) with open(container_full_path, "r+b") as container: container.read(4) current_container_chunk_count = int.from_bytes(container.read(4), byteorder='little') new_container_chunk_count = current_container_chunk_count + chunk_count container.seek(-4, 1) container.write( new_container_chunk_count.to_bytes(4, byteorder='little')) chunks_buffer = BytesIO() for i in range(chunk_count): total_written_len = 0 encoded_save_name = save.name.encode('utf-16le', errors='ignore') total_written_len += chunks_buffer.write(encoded_save_name) if chunk_count > 1: # Multi-chunks save. Adding metadata, format: '$${i}${chunk_count}$1' chunk_metadata = f'$${i}${chunk_count}$1' encoded_metadata = chunk_metadata.encode('utf-16le', errors='ignore') total_written_len += chunks_buffer.write(encoded_metadata) chunks_buffer.write(b"\00" * (144 - total_written_len)) chunks_buffer.write(chunk_uuids[i].bytes_le) Logger.logPrint(f'Editing container: {container_full_path}', "debug") utils.append_buffer_to_file(container_full_path, chunks_buffer)
def ask_copy_target(): Logger.logPrint('Where would you like to copy your save folder ?') Logger.logPrint('\t1) New folder on my desktop') Logger.logPrint("\t2) New folder in a custom path") choice = input() while choice not in ('1', '2'): Logger.logPrint(f'\nPlease choose 1 or 2') choice = input() Logger.logPrint(f'copy_choice {choice}', 'debug') if choice == '1': # Winpath is needed here because Windows user can have a custom Desktop location save_path = utils.get_windows_desktop_path() elif choice == '2': Logger.logPrint(f'\nEnter your custom folder path:') save_path = input() Logger.logPrint(f'save_path {save_path}', 'debug') return utils.join_paths(save_path, utils.create_folder_name('AstroSaveFolder'))
def steam_to_windows_conversion(original_save_path: str) -> None: Logger.logPrint('\nThis conversion is not supported yet. Exiting...') utils.wait_and_exit(0)
def ask_copy_target(folder_main_name: str): ''' Requests a target folder to the user TODO [doc] to explain the folder name format Arguments: folder_main_name: Returns ... ''' Logger.logPrint('Where would you like to copy your save folder ?') Logger.logPrint('\t1) New folder on my desktop') Logger.logPrint("\t2) New folder in a custom path") choice = input() while choice not in ('1', '2'): Logger.logPrint(f'\nPlease choose 1 or 2') choice = input() Logger.logPrint(f'copy_choice {choice}', 'debug') if choice == '1': # Winpath is needed here because Windows user can have a custom Desktop location save_path = utils.get_windows_desktop_path() elif choice == '2': Logger.logPrint(f'\nEnter your custom folder path:') save_path = input() Logger.logPrint(f'save_path {save_path}', 'debug') return utils.join_paths(save_path, utils.create_folder_name(folder_main_name))