def generate_logs(tmp_dir: Path, verbose: bool = False, options: dict = None, original_pool: Pool = None) -> List[Path]: """Analyzes a mod and generates BCML log files containing its changes""" if isinstance(tmp_dir, str): tmp_dir = Path(tmp_dir) if not options: options = {'disable': [], 'options': {}} if 'disable' not in options: options['disable'] = [] pool = original_pool or Pool(cpu_count()) print('Scanning for modified files...') modded_files = find_modded_files(tmp_dir, verbose=verbose, original_pool=original_pool) if not modded_files: raise RuntimeError('No modified files were found. Very unusual.') (tmp_dir / 'logs').mkdir(parents=True, exist_ok=True) for merger_class in [merger_class for merger_class in mergers.get_mergers() \ if merger_class.NAME not in options['disable']]: merger = merger_class() merger.set_pool(pool) if options is not None and merger.NAME in options: merger.set_options(options[merger.NAME]) merger.log_diff(tmp_dir, modded_files) if not original_pool: pool.close() pool.join() return modded_files
def merge_sarcs(file_name: str, sarcs: List[Union[Path, bytes]]) -> (str, bytes): opened_sarcs: List[sarc.SARC] = [] if isinstance(sarcs[0], Path): for i, sarc_path in enumerate(sarcs): sarcs[i] = sarc_path.read_bytes() for sarc_bytes in sarcs: sarc_bytes = util.unyaz_if_needed(sarc_bytes) try: opened_sarcs.append(sarc.SARC(sarc_bytes)) except ValueError: continue all_files = {key for open_sarc in opened_sarcs for key in open_sarc.list_files()} nested_sarcs = {} new_sarc = sarc.SARCWriter(be=True) files_added = [] # for file in all_files: # dm_cache = util.get_master_modpack_dir() / 'logs' / 'dm' / file # if dm_cache.exists(): # file_data = dm_cache.read_bytes() # new_sarc.add_file(file, file_data) # files_added.append(file) for opened_sarc in reversed(opened_sarcs): for file in [file for file in opened_sarc.list_files() if file not in files_added]: data = opened_sarc.get_file_data(file).tobytes() if util.is_file_modded(file.replace('.s', '.'), data, count_new=True): if not Path(file).suffix in util.SARC_EXTS: new_sarc.add_file(file, data) files_added.append(file) else: if file not in nested_sarcs: nested_sarcs[file] = [] nested_sarcs[file].append(util.unyaz_if_needed(data)) for file, sarcs in nested_sarcs.items(): merged_bytes = merge_sarcs(file, sarcs)[1] if Path(file).suffix.startswith('.s') and not file.endswith('.sarc'): merged_bytes = util.compress(merged_bytes) new_sarc.add_file(file, merged_bytes) files_added.append(file) for file in [file for file in all_files if file not in files_added]: for opened_sarc in [open_sarc for open_sarc in opened_sarcs \ if file in open_sarc.list_files()]: new_sarc.add_file(file, opened_sarc.get_file_data(file).tobytes()) break if 'Bootup.pack' in file_name: for merger in [merger() for merger in mergers.get_mergers() if merger.is_bootup_injector()]: inject = merger.get_bootup_injection() if not inject: continue file, data = inject try: new_sarc.delete_file(file) except KeyError: pass new_sarc.add_file(file, data) return (file_name, new_sarc.get_bytes())
def get_mod_edits(self, params=None): mod = BcmlMod.from_json(params["mod"]) edits = {} merger_list = sorted({m() for m in mergers.get_mergers()}, key=lambda m: m.NAME) for merger in merger_list: edits[merger.friendly_name] = merger.get_mod_edit_info(mod) return {key: sorted({str(v) for v in value}) for key, value in edits.items()}
def generate_logs( tmp_dir: Path, options: dict = None, pool: Optional[multiprocessing.pool.Pool] = None, ) -> List[Union[Path, str]]: if isinstance(tmp_dir, str): tmp_dir = Path(tmp_dir) if not options: options = {"disable": [], "options": {}} if "disable" not in options: options["disable"] = [] util.vprint(options) this_pool = pool or Pool(maxtasksperchild=500) print("Scanning for modified files...") modded_files = find_modded_files(tmp_dir, pool=pool) if not (modded_files or (tmp_dir / "patches").exists() or (tmp_dir / "logs").exists()): if "options" in tmp_dir.parts: message = ( f"No modified files were found in {str(tmp_dir)}. " f"This may mean that this option's files are identical to the " f"base mod's, or that the folder has an improper structure.") else: message = ( f"No modified files were found in {str(tmp_dir)}. " f"This probably means this mod is not in a supported format.") raise RuntimeError(message) (tmp_dir / "logs").mkdir(parents=True, exist_ok=True) try: for i, merger_class in enumerate([ merger_class for merger_class in mergers.get_mergers() if merger_class.NAME not in options["disable"] ]): merger = merger_class() # type: ignore util.vprint( f"Merger {merger.NAME}, #{i+1} of {len(mergers.get_mergers())}" ) if options is not None and merger.NAME in options["options"]: merger.set_options(options["options"][merger.NAME]) merger.set_pool(this_pool) merger.log_diff(tmp_dir, modded_files) if util.get_settings("strip_gfx"): dev._clean_sarcs(tmp_dir, util.get_hash_table(util.get_settings("wiiu")), this_pool) except: # pylint: disable=bare-except this_pool.close() this_pool.join() this_pool.terminate() raise if not pool: this_pool.close() this_pool.join() util.vprint(modded_files) return modded_files
def refresh_merges(): print("Cleansing old merges...") shutil.rmtree(util.get_master_modpack_dir(), True) print("Refreshing merged mods...") with Pool(maxtasksperchild=500) as pool: for merger in mergers.sort_mergers( [merger_class() for merger_class in mergers.get_mergers()]): merger.set_pool(pool) merger.perform_merge()
def get_options(self): opts = [] for merger in mergers.get_mergers(): merger = merger() opts.append({ "name": merger.NAME, "friendly": merger.friendly_name, "options": dict(merger.get_checkbox_options()), }) return opts
def disable_mod(mod: BcmlMod, wait_merge: bool = False): remergers = [] print(f"Disabling {mod.name}...") for merger in [merger() for merger in mergers.get_mergers()]: if merger.is_mod_logged(mod): remergers.append(merger) (mod.path / ".disabled").write_bytes(b"") if not wait_merge: print("Remerging...") refresh_merges() print(f"{mod.name} disabled")
def refresh_merges(verbose: bool = False): """ Runs RSTB, pack, and text merges together :param verbose: Whether to display more detailed output, defaults to False. :type verbose: bool, optional """ print('Cleansing old merges...') shutil.rmtree(util.get_master_modpack_dir()) print('Refreshing merged mods...') for merger in mergers.sort_mergers( [merger_class() for merger_class in mergers.get_mergers()]): merger.perform_merge()
def gen_rstb(self, params=None): try: mod = Path(self.get_folder()) assert mod.exists() except (FileNotFoundError, IndexError, AssertionError): return with util.TempModContext(): if not ((mod / "info.json").exists() or (mod / "rules.txt").exists()): (mod / "info.json").write_text( json.dumps( { "name": "Temp", "desc": "Temp pack", "url": "", "id": "VGVtcD0wLjA=", "image": "", "version": "1.0.0", "depends": [], "options": {}, "platform": "wiiu" if util.get_settings("wiiu") else "switch", } ) ) install.install_mod( mod, merge_now=True, options={ "options": {}, "disable": [ m.NAME for m in mergers.get_mergers() if m.NAME != "rstb" ], }, ) (mod / util.get_content_path() / "System" / "Resource").mkdir( parents=True, exist_ok=True ) copyfile( util.get_master_modpack_dir() / util.get_content_path() / "System" / "Resource" / "ResourceSizeTable.product.srsizetable", mod / util.get_content_path() / "System" / "Resource" / "ResourceSizeTable.product.srsizetable", )
def get_options(self): opts = [{ "name": "general", "friendly": "general options", "options": { "base_priority": "Default to lowest priority" }, }] for merger in mergers.get_mergers(): merger = merger() opts.append({ "name": merger.NAME, "friendly": merger.friendly_name, "options": dict(merger.get_checkbox_options()), }) return opts
def uninstall_mod(mod: Union[Path, BcmlMod, str], wait_merge: bool = False, verbose: bool = False): """ Uninstalls the mod currently installed at the specified path and updates merges as needed :param mod: The mod to remove, as a path or a BcmlMod. :param wait_merge: Resort mods but don't remerge anything yet, defaults to False. :type wait_merge: bool, optional :param verbose: Whether to display more detailed output, defaults to False. :type verbose: bool, optional """ path = Path(mod) if isinstance( mod, str) else mod.path if isinstance(mod, BcmlMod) else mod mod_name, mod_priority, _ = util.get_mod_info(path / 'rules.txt') \ if not isinstance(mod, BcmlMod) else mod print(f'Uninstalling {mod_name}...') remergers = set() partials = {} for merger in [merger() for merger in mergers.get_mergers()]: if merger.is_mod_logged(BcmlMod(mod_name, mod_priority, path)): remergers.add(merger) if merger.can_partial_remerge(): partials[merger.NAME] = merger.get_mod_affected(mod) shutil.rmtree(str(path)) next_mod = util.get_mod_by_priority(mod_priority + 1) if next_mod: print('Adjusting mod priorities...') change_mod_priority(next_mod, mod_priority, wait_merge=True, verbose=verbose) print() if not wait_merge: pool = Pool(cpu_count()) for merger in mergers.sort_mergers(remergers): merger.set_pool(pool) if merger.NAME in partials: merger.set_options({'only_these': partials[merger.NAME]}) merger.perform_merge() pool.close() pool.join() print(f'{mod_name} has been uninstalled.')
def generate_logs( tmp_dir: Path, options: dict = None, pool: Optional[multiprocessing.pool.Pool] = None, ) -> List[Union[Path, str]]: if isinstance(tmp_dir, str): tmp_dir = Path(tmp_dir) if not options: options = {"disable": [], "options": {}} if "disable" not in options: options["disable"] = [] util.vprint(options) this_pool = pool or Pool(maxtasksperchild=500) print("Scanning for modified files...") modded_files = find_modded_files(tmp_dir, pool=pool) if not modded_files: raise RuntimeError( f"No modified files were found in {str(tmp_dir)}." "This probably means this mod is not in a supported format.") (tmp_dir / "logs").mkdir(parents=True, exist_ok=True) try: for i, merger_class in enumerate([ merger_class for merger_class in mergers.get_mergers() if merger_class.NAME not in options["disable"] ]): merger = merger_class() # type: ignore util.vprint( f"Merger {merger.NAME}, #{i+1} of {len(mergers.get_mergers())}" ) if options is not None and merger.NAME in options["options"]: merger.set_options(options["options"][merger.NAME]) merger.set_pool(this_pool) merger.log_diff(tmp_dir, modded_files) except: # pylint: disable=bare-except this_pool.close() this_pool.join() this_pool.terminate() raise if not pool: this_pool.close() this_pool.join() util.vprint(modded_files) return modded_files
def remerge(self, params): try: if not util.get_installed_mods(): if util.get_master_modpack_dir().exists(): rmtree(util.get_master_modpack_dir()) install.link_master_mod() return if params["name"] == "all": install.refresh_merges() else: [ m() for m in mergers.get_mergers() if m().friendly_name == params["name"] ][0].perform_merge() except Exception as err: # pylint: disable=broad-except raise Exception( f"There was an error merging your mods. {str(err)}\n" "Note that this could leave your game in an unplayable state.")
def disable_mod(mod: BcmlMod, wait_merge: bool = False): remergers = [] partials = {} print(f'Disabling {mod.name}...') for merger in [merger() for merger in mergers.get_mergers()]: if merger.is_mod_logged(mod): remergers.append(merger) if merger.can_partial_remerge(): partials[merger.NAME] = merger.get_mod_affected(mod) rules_path: Path = mod.path / 'rules.txt' rules_path.rename(rules_path.with_suffix('.txt.disable')) if not wait_merge: print(f'Remerging affected files...') for merger in remergers: if merger.NAME in partials: merger.set_options({'only_these': partials[merger.NAME]}) merger.perform_merge() print(f'{mod.name} disabled')
def get_mod_info(self, params): mod = BcmlMod.from_json(params["mod"]) util.vprint(mod) img = "" try: img = base64.b64encode(mod.get_preview().read_bytes()).decode("utf8") except: # pylint: disable=bare-except pass return { "changes": [ m.NAME.upper() for m in mergers.get_mergers() if m().is_mod_logged(mod) ], "desc": mod.description, "date": mod.date, "processed": (mod.path / ".processed").exists(), "image": img, "url": mod.url, }
def setupUi(self, OptionsDialog): OptionsDialog.setObjectName("OptionsDialog") sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( OptionsDialog.sizePolicy().hasHeightForWidth()) OptionsDialog.setSizePolicy(sizePolicy) self.verticalLayout = QtWidgets.QVBoxLayout(OptionsDialog) self.verticalLayout.setObjectName("verticalLayout") self.label = QtWidgets.QLabel(OptionsDialog) self.label.setObjectName("label") self.verticalLayout.addWidget(self.label) self.checkboxes = [] for merger_class in mergers.get_mergers(): merger = merger_class() chkMerger = QtWidgets.QCheckBox(OptionsDialog) chkMerger.setObjectName('chkDisable' + merger.NAME) chkMerger.setText('Disable ' + merger.friendly_name()) setattr(chkMerger, 'disable_name', merger.NAME) self.verticalLayout.addWidget(chkMerger) self.checkboxes.append(chkMerger) for option in merger.get_checkbox_options(): chkOption = QtWidgets.QCheckBox(OptionsDialog) chkOption.setObjectName('chk' + merger.NAME + option[0]) setattr(chkOption, 'option_name', option[0]) setattr(chkOption, 'merger', merger.NAME) chkOption.setText(option[1]) self.verticalLayout.addWidget(chkOption) self.checkboxes.append(chkOption) self.buttonBox = QtWidgets.QDialogButtonBox(OptionsDialog) self.buttonBox.setOrientation(QtCore.Qt.Horizontal) self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok) self.buttonBox.setObjectName("buttonBox") self.verticalLayout.addWidget(self.buttonBox) self.retranslateUi(OptionsDialog) QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("accepted()"), OptionsDialog.accept) QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("rejected()"), OptionsDialog.reject) QtCore.QMetaObject.connectSlotsByName(OptionsDialog)
def merge_sarcs(file_name: str, sarcs: List[Union[Path, bytes]]) -> (str, bytes): opened_sarcs: List[oead.Sarc] = [] if "ThunderRodLv2" in file_name: print() if isinstance(sarcs[0], Path): for i, sarc_path in enumerate(sarcs): sarcs[i] = sarc_path.read_bytes() for sarc_bytes in sarcs: sarc_bytes = util.unyaz_if_needed(sarc_bytes) try: opened_sarcs.append(oead.Sarc(sarc_bytes)) except (ValueError, RuntimeError, oead.InvalidDataError): continue all_files = { file.name for open_sarc in opened_sarcs for file in open_sarc.get_files() } nested_sarcs = {} new_sarc = oead.SarcWriter(endian=oead.Endianness.Big if util.get_settings( "wiiu") else oead.Endianness.Little) files_added = set() for opened_sarc in reversed(opened_sarcs): for file in [ f for f in opened_sarc.get_files() if f.name not in files_added ]: file_data = oead.Bytes(file.data) if (file.name[file.name.rindex("."):] in util.SARC_EXTS - EXCLUDE_EXTS) and file.name not in SPECIAL: if file.name not in nested_sarcs: nested_sarcs[file.name] = [] nested_sarcs[file.name].append(util.unyaz_if_needed(file_data)) elif util.is_file_modded(file.name.replace(".s", "."), file_data, count_new=True): new_sarc.files[file.name] = file_data files_added.add(file.name) util.vprint(set(nested_sarcs.keys())) for file, sarcs in nested_sarcs.items(): if not sarcs: continue merged_bytes = merge_sarcs(file, sarcs[::-1])[1] if Path(file).suffix.startswith(".s") and not file.endswith(".sarc"): merged_bytes = util.compress(merged_bytes) new_sarc.files[file] = merged_bytes files_added.add(file) for file in [file for file in all_files if file not in files_added]: for opened_sarc in [ open_sarc for open_sarc in opened_sarcs if (file in [f.name for f in open_sarc.get_files()]) ]: new_sarc.files[file] = oead.Bytes(opened_sarc.get_file(file).data) break if "Bootup.pack" in file_name: for merger in [ merger() for merger in mergers.get_mergers() if merger.is_bootup_injector() ]: inject = merger.get_bootup_injection() if not inject: continue file, data = inject new_sarc.files[file] = data return (file_name, bytes(new_sarc.write()[1]))
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
def install_mod( mod: Path, options: dict = None, selects: dict = None, pool: Optional[multiprocessing.pool.Pool] = None, insert_priority: int = 0, merge_now: bool = False, updated: bool = False, ): if not insert_priority: insert_priority = get_next_priority() try: if isinstance(mod, str): mod = Path(mod) if mod.is_file(): print("Opening mod...") tmp_dir = open_mod(mod) elif mod.is_dir(): if not ((mod / "rules.txt").exists() or (mod / "info.json").exists()): print( f"Cannot open mod at {str(mod)}, no rules.txt or info.json found" ) return print(f"Loading mod from {str(mod)}...") tmp_dir = Path(mkdtemp()) if tmp_dir.exists(): shutil.rmtree(tmp_dir) shutil.copytree(str(mod), str(tmp_dir)) if (mod / "rules.txt").exists() and not (mod / "info.json").exists(): print("Upgrading old mod format...") upgrade.convert_old_mod(mod, delete_old=True) else: print(f"Error: {str(mod)} is neither a valid file nor a directory") return except Exception as err: # pylint: disable=broad-except raise util.InstallError(err) from err if not options: options = {"options": {}, "disable": []} this_pool: Optional[multiprocessing.pool.Pool] = None # type: ignore try: rules = json.loads((tmp_dir / "info.json").read_text("utf-8")) mod_name = rules["name"].strip(" '\"").replace("_", "") print(f"Identified mod: {mod_name}") if rules["depends"]: try: installed_metas = { v[0]: v[1] for m in util.get_installed_mods() for v in util.BcmlMod.meta_from_id(m.id) } except (IndexError, TypeError) as err: raise RuntimeError( f"This BNP has invalid or corrupt dependency data.") for depend in rules["depends"]: depend_name, depend_version = util.BcmlMod.meta_from_id(depend) if (depend_name not in installed_metas) or ( depend_name in installed_metas and depend_version > installed_metas[depend_name]): raise RuntimeError( f"{mod_name} requires {depend_name} version {depend_version}, " f"but it is not installed. Please install {depend_name} and " "try again.") friendly_plaform = lambda p: "Wii U" if p == "wiiu" else "Switch" user_platform = "wiiu" if util.get_settings("wiiu") else "switch" if rules["platform"] != user_platform: raise ValueError( f'"{mod_name}" is for {friendly_plaform(rules["platform"])}, not ' f" {friendly_plaform(user_platform)}.'") if "priority" in rules and rules["priority"] == "base": insert_priority = 100 logs = tmp_dir / "logs" if logs.exists(): print("Loading mod logs...") for merger in [ merger() # type: ignore for merger in mergers.get_mergers() if merger.NAME in options["disable"] ]: if merger.is_mod_logged(BcmlMod(tmp_dir)): (tmp_dir / "logs" / merger.log_name).unlink() else: this_pool = pool or Pool(maxtasksperchild=500) dev._pack_sarcs(tmp_dir, util.get_hash_table(util.get_settings("wiiu")), this_pool) generate_logs(tmp_dir=tmp_dir, options=options, pool=this_pool) if not util.get_settings("strip_gfx"): (tmp_dir / ".processed").touch() except Exception as err: # pylint: disable=broad-except try: name = mod_name except NameError: name = "your mod, the name of which could not be detected" raise util.InstallError(err, name) from err if selects: for opt_dir in { d for d in (tmp_dir / "options").glob("*") if d.is_dir() }: if opt_dir.name not in selects: shutil.rmtree(opt_dir, ignore_errors=True) else: file: Path for file in { f for f in opt_dir.rglob("**/*") if ("logs" not in f.parts and f.is_file()) }: out = tmp_dir / file.relative_to(opt_dir) out.parent.mkdir(parents=True, exist_ok=True) try: os.link(file, out) except FileExistsError: if file.suffix in util.SARC_EXTS: try: old_sarc = oead.Sarc( util.unyaz_if_needed(out.read_bytes())) except (ValueError, oead.InvalidDataError, RuntimeError): out.unlink() os.link(file, out) try: link_sarc = oead.Sarc( util.unyaz_if_needed(file.read_bytes())) except (ValueError, oead.InvalidDataError, RuntimeError): del old_sarc continue new_sarc = oead.SarcWriter.from_sarc(link_sarc) link_files = { f.name for f in link_sarc.get_files() } for sarc_file in old_sarc.get_files(): if sarc_file.name not in link_files: new_sarc.files[sarc_file.name] = bytes( sarc_file.data) del old_sarc del link_sarc out.write_bytes(new_sarc.write()[1]) del new_sarc else: out.unlink() os.link(file, out) rstb_path = (tmp_dir / util.get_content_path() / "System" / "Resource" / "ResourceSizeTable.product.srsizetable") if rstb_path.exists(): rstb_path.unlink() 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: if not updated: for existing_mod in util.get_installed_mods(True): if existing_mod.priority >= priority: existing_mod.change_priority(existing_mod.priority + 1) if (tmp_dir / "patches").exists() and not util.get_settings("no_cemu"): patch_dir = (util.get_cemu_dir() / "graphicPacks" / f"bcmlPatches" / util.get_safe_pathname(rules["name"])) patch_dir.mkdir(parents=True, exist_ok=True) for file in { f for f in (tmp_dir / "patches").rglob("*") if f.is_file() }: out = patch_dir / file.relative_to(tmp_dir / "patches") out.parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(file, out) mod_dir.parent.mkdir(parents=True, exist_ok=True) 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.rmtree(str(mod_dir)) shutil.copytree(str(tmp_dir), str(mod_dir)) shutil.rmtree(str(tmp_dir), ignore_errors=True) 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)) shutil.rmtree(tmp_dir, ignore_errors=True) rules["priority"] = priority (mod_dir / "info.json").write_text(json.dumps(rules, ensure_ascii=False, indent=2), encoding="utf-8") (mod_dir / "options.json").write_text(json.dumps(options, ensure_ascii=False, indent=2), encoding="utf-8") output_mod = BcmlMod(mod_dir) try: util.get_mod_link_meta(rules) util.get_mod_preview(output_mod) except Exception: # pylint: disable=broad-except pass except Exception as err: # pylint: disable=broad-except if mod_dir.exists(): try: uninstall_mod(mod_dir, wait_merge=True) except Exception: # pylint: disable=broad-except shutil.rmtree(str(mod_dir)) raise util.InstallError(err, mod_name) from err try: if merge_now: for merger in [m() for m in mergers.get_mergers()]: if this_pool or pool: merger.set_pool(this_pool or pool) if merger.NAME in options["options"]: merger.set_options(options["options"][merger.NAME]) merger.perform_merge() except Exception as err: # pylint: disable=broad-except raise util.MergeError(err) from err if this_pool and not pool: this_pool.close() this_pool.join() return output_mod
def get_setup(self): return { "hasCemu": not util.get_settings("no_cemu"), "mergers": [m().friendly_name for m in mergers.get_mergers()], }
def change_mod_priority(path: Path, new_priority: int, wait_merge: bool = False, verbose: bool = False): """ Changes the priority of a mod :param path: The path to the mod. :type path: class:`pathlib.Path` :param new_priority: The new priority of the mod. :type new_priority: int :param wait_merge: Resort priorities but don't remerge anything yet, defaults to False. :type wait_merge: bool, optional :param verbose: Whether to display more detailed output, defaults to False. :type verbose: bool, optional """ mod = util.get_mod_info(path / 'rules.txt') print( f'Changing priority of {mod.name} from {mod.priority} to {new_priority}...' ) mods = util.get_installed_mods() if new_priority > mods[len(mods) - 1][1]: new_priority = len(mods) - 1 mods.remove(mod) mods.insert(new_priority - 100, util.BcmlMod(mod.name, new_priority, path)) all_mergers = [merger() for merger in mergers.get_mergers()] remergers = set() partials = {} for merger in all_mergers: if merger.is_mod_logged(mod): remergers.add(merger) if merger.can_partial_remerge(): partials[merger.NAME] = set(merger.get_mod_affected(mod)) print('Resorting other affected mods...') for mod in mods: if mod.priority != (mods.index(mod) + 100): adjusted_priority = mods.index(mod) + 100 mods.remove(mod) mods.insert(adjusted_priority - 100, BcmlMod(mod.name, adjusted_priority, mod.path)) if verbose: print(f'Changing priority of {mod.name} from' f'{mod.priority} to {adjusted_priority}...') for mod in mods: if not mod.path.stem.startswith(f'{mod.priority:04}'): for merger in all_mergers: if merger.is_mod_logged(mod): remergers.add(merger) if merger.can_partial_remerge(): if merger.NAME not in partials: partials[merger.NAME] = set() partials[merger.NAME] |= set( merger.get_mod_affected(mod)) new_mod_id = util.get_mod_id(mod[0], mod[1]) shutil.move(str(mod[2]), str(mod[2].parent / new_mod_id)) rules = util.RulesParser() rules.read(str(mod.path.parent / new_mod_id / 'rules.txt')) rules['Definition']['fsPriority'] = str(mod[1]) with (mod[2].parent / new_mod_id / 'rules.txt').open( 'w', encoding='utf-8') as r_file: rules.write(r_file) refresh_cemu_mods() if not wait_merge: for merger in mergers.sort_mergers(remergers): if merger.NAME in partials: merger.set_options({'only_these': partials[merger.NAME]}) merger.perform_merge() if wait_merge: print('Mods resorted, will need to remerge later') print('Finished updating mod priorities.')