def uninstall_mod(mod: BcmlMod, wait_merge: bool = False): has_patches = (mod.path / "patches").exists() try: shutil.rmtree(str(mod.path), onerror=force_del) except (OSError, PermissionError, WindowsError) as err: raise RuntimeError( f"The folder for {mod.name} could not be removed. " "You may need to delete it manually and remerge, or " "close all open programs (including BCML and Windows Explorer) " "and try again. The location of the folder is " f"<code>{str(mod.path)}</code>.") from err for fall_mod in [ m for m in util.get_installed_mods(True) if m.priority > mod.priority ]: fall_mod.change_priority(fall_mod.priority - 1) if not util.get_installed_mods(): shutil.rmtree(util.get_master_modpack_dir()) util.create_bcml_graphicpack_if_needed() else: if not wait_merge: refresh_merges() if has_patches and not util.get_settings("no_cemu"): shutil.rmtree( util.get_cemu_dir() / "graphicPacks" / "bcmlPatches" / util.get_safe_pathname(mod.name), ignore_errors=True, ) print(f"{mod.name} has been uninstalled.")
def uninstall_mod(mod: BcmlMod, wait_merge: bool = False): print(f"Uninstalling {mod.name}...") try: shutil.rmtree(str(mod.path)) except (OSError, PermissionError) as err: raise RuntimeError( f"The folder for {mod.name} could not be removed. " "You may need to delete it manually and remerge, or " "close all open programs (including BCML and Windows Explorer) " "and try again. The location of the folder is " f"<code>{str(mod.path)}</code>.") from err for fall_mod in [ m for m in util.get_installed_mods(True) if m.priority > mod.priority ]: fall_mod.change_priority(fall_mod.priority - 1) if not util.get_installed_mods(): shutil.rmtree(util.get_master_modpack_dir()) util.create_bcml_graphicpack_if_needed() else: if not wait_merge: refresh_merges() print(f"{mod.name} has been uninstalled.")
def link_master_mod(output: Path = None): util.create_bcml_graphicpack_if_needed() if not output: if not util.get_settings("export_dir"): return output = Path(util.get_settings("export_dir")) if output.exists(): shutil.rmtree(output, ignore_errors=True) try: output.mkdir(parents=True, exist_ok=True) if not util.get_settings("no_cemu"): output.mkdir(parents=True, exist_ok=True) shutil.copy(util.get_master_modpack_dir() / "rules.txt", output / "rules.txt") except (OSError, PermissionError, FileExistsError, FileNotFoundError) as err: raise RuntimeError( "There was a problem creating the master BCML graphic pack. " "It may be a one time fluke, so try remerging and/or restarting BCML. " "This can also happen if BCML and/or Cemu are installed into Program " "Files or any folder which requires administrator (or root) permissions. " "You can try running BCML as administrator or root, but bear in mind this " "is not officially supported. If the problem persists, good luck, because " "it's something wonky about your PC, I guess.") from err mod_folders: List[Path] = sorted( [ item for item in util.get_modpack_dir().glob("*") if item.is_dir() and not (item / ".disabled").exists() ], reverse=True, ) util.vprint(mod_folders) link_or_copy: Any = os.link if not util.get_settings( "no_hardlinks") else copyfile for mod_folder in mod_folders: for item in mod_folder.rglob("**/*"): rel_path = item.relative_to(mod_folder) exists = (output / rel_path).exists() is_log = str(rel_path).startswith("logs") is_opt = str(rel_path).startswith("options") is_meta = str(rel_path).startswith("meta") is_extra = (len(rel_path.parts) == 1 and rel_path.suffix != ".txt" and not item.is_dir()) if exists or is_log or is_extra or is_meta or is_opt: continue if item.is_dir(): (output / rel_path).mkdir(parents=True, exist_ok=True) elif item.is_file(): try: link_or_copy(str(item), str(output / rel_path)) except OSError: if link_or_copy is os.link: link_or_copy = copyfile link_or_copy(str(item), str(output / rel_path)) else: raise
def link_master_mod(output: Path = None): util.create_bcml_graphicpack_if_needed() if not output: if util.get_settings("no_cemu"): return output = util.get_cemu_dir() / "graphicPacks" / "BreathOfTheWild_BCML" if output.exists(): shutil.rmtree(output, ignore_errors=True) try: output.mkdir(parents=True, exist_ok=True) if not util.get_settings("no_cemu"): shutil.copy(util.get_master_modpack_dir() / "rules.txt", output / "rules.txt") except (OSError, PermissionError, FileExistsError, FileNotFoundError) as err: raise RuntimeError( "There was a problem creating the master BCML graphic pack. " "It may be a one time fluke, so try remerging and/or restarting BCML. " "If the problem persists, good luck, because it's something wonky about your " "PC, I guess.") from err mod_folders: List[Path] = sorted( [ item for item in util.get_modpack_dir().glob("*") if item.is_dir() and not (item / ".disabled").exists() ], reverse=True, ) util.vprint(mod_folders) link_or_copy = os.link if not util.get_settings( "no_hardlinks") else copyfile for mod_folder in mod_folders: for item in mod_folder.rglob("**/*"): rel_path = item.relative_to(mod_folder) exists = (output / rel_path).exists() is_log = str(rel_path).startswith("logs") is_opt = str(rel_path).startswith("options") is_meta = str(rel_path).startswith("meta") is_extra = (len(rel_path.parts) == 1 and rel_path.suffix != ".txt" and not item.is_dir()) if exists or is_log or is_extra or is_meta or is_opt: continue if item.is_dir(): (output / rel_path).mkdir(parents=True, exist_ok=True) elif item.is_file(): try: link_or_copy(str(item), str(output / rel_path)) except OSError: if link_or_copy is os.link: link_or_copy = copyfile link_or_copy(str(item), str(output / rel_path)) else: raise
def link_master_mod(output: Path = None): util.create_bcml_graphicpack_if_needed() try: rsext.manager.link_master_mod(str(output) if output else None) except (OSError, RuntimeError) as err: if err is OSError or (err is RuntimeError and "junction" in str(err)): raise RuntimeError( "BCML failed to create the link to the merged mod. This is " "probably because Cemu is on an external USB, eSATA, or " "network drive. You can fix this in one of two ways:\n" "- Make sure Cemu is on an internal drive\n" "- Turn on the 'no hard links' option in BCML's settings " "(but be aware that mod merging will take much longer)" ) from err else: raise
def install_mod(mod: Path, verbose: bool = False, options: dict = None, wait_merge: bool = False, insert_priority: int = 0): """ Installs a graphic pack mod, merging RSTB changes and optionally packs and texts :param mod: Path to the mod to install. Must be a RAR, 7z, or ZIP archive or a graphicpack directory containing a rules.txt file. :type mod: class:`pathlib.Path` :param verbose: Whether to display more detailed output, defaults to False. :type verbose: bool, optional :param wait_merge: Install mod and log changes, but wait to run merge manually, defaults to False. :type wait_merge: bool, optional :param insert_priority: Insert mod(s) at priority specified, defaults to get_next_priority(). :type insert_priority: int """ if insert_priority == 0: insert_priority = get_next_priority() util.create_bcml_graphicpack_if_needed() if isinstance(mod, str): mod = Path(mod) if mod.is_file(): print('Extracting mod...') tmp_dir = open_mod(mod) elif mod.is_dir(): if (mod / 'rules.txt').exists(): print(f'Loading mod from {str(mod)}...') tmp_dir = util.get_work_dir() / f'tmp_{mod.name}' shutil.copytree(str(mod), str(tmp_dir)) else: print(f'Cannot open mod at {str(mod)}, no rules.txt found') return else: print(f'Error: {str(mod)} is neither a valid file nor a directory') return pool: Pool = None try: rules = util.RulesParser() rules.read(tmp_dir / 'rules.txt') mod_name = str(rules['Definition']['name']).strip(' "\'') print(f'Identified mod: {mod_name}') logs = tmp_dir / 'logs' if logs.exists(): print('This mod supports Quick Install! Loading changes...') for merger in [merger() for merger in mergers.get_mergers() \ if merger.NAME in options['disable']]: if merger.is_mod_logged(BcmlMod('', 0, tmp_dir)): (tmp_dir / 'logs' / merger.log_name()).unlink() else: pool = Pool(cpu_count()) generate_logs(tmp_dir=tmp_dir, verbose=verbose, options=options, original_pool=pool) except Exception as e: # pylint: disable=broad-except if hasattr(e, 'error_text'): raise e clean_error = RuntimeError() try: name = mod_name except NameError: name = 'your mod, the name of which could not be detected' clean_error.error_text = ( f'There was an error while processing {name}. ' 'This could indicate there is a problem with the mod itself, ' 'but it could also reflect a new or unusual edge case BCML does ' 'not anticipate. Here is the error:\n\n' f'{traceback.format_exc(limit=-4)}') raise clean_error priority = insert_priority print(f'Assigned mod priority of {priority}') mod_id = util.get_mod_id(mod_name, priority) mod_dir = util.get_modpack_dir() / mod_id try: for existing_mod in util.get_installed_mods(): if existing_mod.priority >= priority: priority_shifted = existing_mod.priority + 1 new_id = util.get_mod_id(existing_mod.name, priority_shifted) new_path = util.get_modpack_dir() / new_id shutil.move(str(existing_mod.path), str(new_path)) existing_mod_rules = util.RulesParser() existing_mod_rules.read(str(new_path / 'rules.txt')) existing_mod_rules['Definition']['fsPriority'] = str( priority_shifted) with (new_path / 'rules.txt').open('w', encoding='utf-8') as r_file: existing_mod_rules.write(r_file) mod_dir.parent.mkdir(parents=True, exist_ok=True) print() print(f'Moving mod to {str(mod_dir)}...') if mod.is_file(): try: shutil.move(str(tmp_dir), str(mod_dir)) except Exception: # pylint: disable=broad-except try: shutil.copytree(str(tmp_dir), str(mod_dir)) try: shutil.rmtree(str(tmp_dir)) except Exception: # pylint: disable=broad-except pass except Exception: # pylint: disable=broad-except raise OSError( 'BCML could not transfer your mod from the temp directory ' 'to the BCML directory.') elif mod.is_dir(): shutil.copytree(str(tmp_dir), str(mod_dir)) rulepath = os.path.basename(rules['Definition']['path']).replace( '"', '') rules['Definition']['path'] = f'{{BCML: DON\'T TOUCH}}/{rulepath}' rules['Definition']['fsPriority'] = str(priority) with Path(mod_dir / 'rules.txt').open('w', encoding='utf-8') as r_file: rules.write(r_file) output_mod = BcmlMod(mod_name, priority, mod_dir) try: util.get_mod_link_meta(rules) util.get_mod_preview(output_mod, rules) except Exception: # pylint: disable=broad-except pass print(f'Enabling {mod_name} in Cemu...') refresh_cemu_mods() except Exception: # pylint: disable=broad-except clean_error = RuntimeError() clean_error.error_text = ( f'There was an error installing {mod_name}. ' 'It processed successfully, but could not be added to your BCML ' 'mods. This may indicate a problem with your BCML installation. ' 'Here is the error:\n\n' f'{traceback.format_exc(limit=-4)}\n\n' f'{mod_name} is being removed and no changes will be made.') if mod_dir.exists(): try: uninstall_mod(mod_dir, wait_merge=True) except Exception: # pylint: disable=broad-except shutil.rmtree(str(mod_dir)) raise clean_error if wait_merge: print('Mod installed, merge still pending...') else: try: if not pool: pool = Pool(cpu_count) print('Performing merges...') if not options: options = {} if 'disable' not in options: options['disable'] = [] for merger in mergers.sort_mergers([cls() for cls in mergers.get_mergers() \ if cls.NAME not in options['disable']]): merger.set_pool(pool) if merger.NAME in options: merger.set_options(options[merger.NAME]) if merger.is_mod_logged(output_mod): merger.perform_merge() print() print(f'{mod_name} installed successfully!') pool.close() pool.join() except Exception: # pylint: disable=broad-except clean_error = RuntimeError() clean_error.error_text = ( f'There was an error merging {mod_name}. ' 'It processed and installed without error, but it has not ' 'successfully merged with your other mods. ' 'Here is the error:\n\n' f'{traceback.format_exc(limit=-4)}\n\n' f'To protect your mod setup, BCML will remove {mod_name} ' 'and remerge.') try: uninstall_mod(mod_dir) except FileNotFoundError: pass raise clean_error return output_mod