def extract_mod_meta(mod: Path) -> Dict[str, Any]: result: subprocess.CompletedProcess if util.SYSTEM == "Windows": result = subprocess.run( [ get_7z_path(), "e", str(mod.resolve()), "-r", "-so", "info.json", ], capture_output=True, universal_newlines=True, creationflags=util.CREATE_NO_WINDOW, ) else: result = subprocess.run( [ get_7z_path(), "e", str(mod.resolve()), "-r", "-so", "info.json", ], capture_output=True, universal_newlines=True, ) try: assert not result.stderr meta = json.loads(result.stdout) except: return {} return meta
def extract_refs(language: str, tmp_dir: Path, files: set = None): x_args = [ get_7z_path(), "x", str(util.get_exec_dir() / "data" / "text_refs.7z"), f'-o{str(tmp_dir / "refs")}', ] if files: x_args.extend(files) else: x_args.append(language) result: subprocess.CompletedProcess if system() == "Windows": result = subprocess.run( x_args, capture_output=True, creationflags=util.CREATE_NO_WINDOW, check=False, text=True, ) else: result = subprocess.run(x_args, capture_output=True, text=True, check=False) if result.stderr: raise RuntimeError(result.stderr)
def restore_backup(backup: Union[str, Path]): if isinstance(backup, str): backup = Path(backup) if not backup.exists(): raise FileNotFoundError(f'The backup "{backup.name}" does not exist.') print("Clearing installed mods...") for folder in [ item for item in util.get_modpack_dir().glob("*") if item.is_dir() ]: shutil.rmtree(str(folder)) print("Extracting backup...") x_args = [ get_7z_path(), "x", str(backup), f"-o{str(util.get_modpack_dir())}" ] if system() == "Windows": subprocess.run( x_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=util.CREATE_NO_WINDOW, check=True, ) else: subprocess.run(x_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) print("Re-enabling mods in Cemu...") refresh_master_export() print(f'Backup "{backup.name}" restored')
def create_backup(name: str = ""): if not name: name = f'BCML_Backup_{datetime.datetime.now().strftime("%Y-%m-%d")}' else: name = re.sub(r"(?u)[^-\w.]", "", name.strip().replace(" ", "_")) num_mods = len([d for d in util.get_modpack_dir().glob("*") if d.is_dir()]) output = util.get_storage_dir() / "backups" / f"{name}---{num_mods - 1}.7z" output.parent.mkdir(parents=True, exist_ok=True) print(f"Saving backup {name}...") x_args = [ get_7z_path(), "a", str(output), f'{str(util.get_modpack_dir() / "*")}' ] if system() == "Windows": subprocess.run( x_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=util.CREATE_NO_WINDOW, check=True, ) else: subprocess.run(x_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) print(f'Backup "{name}" created')
def upgrade_bnp(self, params=None): path = self.window.create_file_dialog( file_types=tuple(["BOTW Nano Patch (*.bnp)"])) if not path: return path = Path(path if isinstance(path, str) else path[0]) if not path.exists(): return tmp_dir = install.open_mod(path) output = self.window.create_file_dialog( webviewb.SAVE_DIALOG, file_types=tuple(["BOTW Nano Patch (*.bnp)"])) if not output: return output = Path(output if isinstance(output, str) else output[0]) print(f"Saving output file to {str(output)}...") x_args = [ util.get_7z_path(), "a", str(output), f'{str(tmp_dir / "*")}' ] if SYSTEM == "Windows": run( x_args, stdout=PIPE, stderr=PIPE, creationflags=util.CREATE_NO_WINDOW, check=True, ) else: run(x_args, stdout=PIPE, stderr=PIPE, check=True)
def convert_bnp(self, params) -> List[str]: bnp = Path(params["mod"]) mod = install.open_mod(bnp) warnings = dev.convert_mod(mod, params["wiiu"], params["warn"]) out = self.window.create_file_dialog( webviewb.SAVE_DIALOG, file_types=("BOTW Nano Patch (*.bnp)", "All files (*.*)"), save_filename=bnp.stem + f"_{'wiiu' if params['wiiu'] else 'switch'}.bnp", ) if not out: raise Exception("canceled") x_args = [ util.get_7z_path(), "a", out if isinstance(out, str) else out[0], f'{str(mod / "*")}', ] if system() == "Windows": result = run( x_args, capture_output=True, universal_newlines=True, creationflags=util.CREATE_NO_WINDOW, check=False, ) else: result = run(x_args, capture_output=True, universal_newlines=True, check=False) if result.stderr: raise RuntimeError(result.stderr) rmtree(mod, ignore_errors=True) return warnings
def restore_old_backup(self, params=None): if (util.get_cemu_dir() / "bcml_backups").exists(): open_dir = util.get_cemu_dir() / "bcml_backups" else: open_dir = Path.home() try: file = Path( self.window.create_file_dialog( directory=str(open_dir), file_types=("BCML Backups (*.7z)", "All Files (*.*)"), )[0]) except IndexError: return tmp_dir = Path(mkdtemp()) x_args = [get_7z_path(), "x", str(file), f"-o{str(tmp_dir)}"] if system() == "Windows": run( x_args, capture_output=True, creationflags=util.CREATE_NO_WINDOW, check=True, ) else: run(x_args, capture_output=True, check=True) upgrade.convert_old_mods(tmp_dir)
def export(output: Path): print("Loading files...") tmp_dir = Path(mkdtemp()) if tmp_dir.exists(): try: rmtree(tmp_dir) except (OSError, FileNotFoundError, PermissionError) as err: raise RuntimeError( "There was a problem cleaning the temporary export directory. This may be" " a fluke, so consider restarting BCML and trying again." ) from err link_master_mod(tmp_dir) print("Adding rules.txt...") rules_path = tmp_dir / "rules.txt" mods = util.get_installed_mods() if util.get_settings("wiiu"): rules_path.write_text( "[Definition]\n" "titleIds = 00050000101C9300,00050000101C9400,00050000101C9500\n" "name = Exported BCML Mod\n" "path = The Legend of Zelda: Breath of the Wild/Mods/Exported BCML\n" f'description = Exported merge of {", ".join([mod.name for mod in mods])}\n' "version = 4\n", encoding="utf-8", ) if output.suffix == ".bnp" or output.name.endswith(".bnp.7z"): print("Exporting BNP...") dev.create_bnp_mod( mod=tmp_dir, meta={}, output=output, options={"rstb": { "no_guess": util.get_settings("no_guess") }}, ) else: print("Exporting as graphic pack mod...") x_args = [get_7z_path(), "a", str(output), f'{str(tmp_dir / "*")}'] result: subprocess.CompletedProcess if os.name == "nt": result = subprocess.run( x_args, creationflags=util.CREATE_NO_WINDOW, check=False, capture_output=True, universal_newlines=True, ) else: result = subprocess.run(x_args, check=False, capture_output=True, universal_newlines=True) if result.stderr: raise RuntimeError( f"There was an error exporting your mod(s). {result.stderr}") rmtree(tmp_dir, True)
def open_mod(path: Path) -> Path: if isinstance(path, str): path = Path(path) tmpdir = Path(TemporaryDirectory().name) archive_formats = {".rar", ".zip", ".7z", ".bnp"} meta_formats = {".json", ".txt"} if tmpdir.exists(): shutil.rmtree(tmpdir, ignore_errors=True) if path.suffix.lower() in archive_formats: x_args = [get_7z_path(), "x", str(path), f"-o{str(tmpdir)}"] if system() == "Windows": subprocess.run( x_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=util.CREATE_NO_WINDOW, check=False, ) else: subprocess.run(x_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) elif path.suffix.lower() in meta_formats: shutil.copytree(path.parent, tmpdir) else: raise ValueError( "The mod provided was not a supported archive (BNP, ZIP, RAR, or 7z) " "or meta file (rules.txt or info.json).") if not tmpdir.exists(): raise Exception( "No files were extracted. This may be because of an invalid or corrupted " "download. If using the GameBanana tab, you may need to try again later, as" " the problem could be caused by server errors.") rulesdir = tmpdir if (rulesdir / "info.json").exists(): return rulesdir if not (rulesdir / "rules.txt").exists(): for subdir in tmpdir.rglob("*"): if (subdir / "rules.txt").exists(): rulesdir = subdir break else: raise FileNotFoundError( "No <code>info.json</code> or <code>rules.txt</code> file was found in " f'"{path.stem}". This could mean the mod is in an old or unsupported ' "format. For information on creating BNPs, check the in-app help. If " "instead you want to make a graphic pack, check " '<a href="https://zeldamods.org/wiki/Help:Using_mods#Installing_mods_with_the_graphic_pack_menu" target="_blank">' "the guide on ZeldaMods here</a>.") print("Looks like an older mod, let's upgrade it...") upgrade.convert_old_mod(rulesdir, delete_old=True) return rulesdir
def create_bnp_mod(mod: Path, output: Path, meta: dict, options: Optional[dict] = None): if isinstance(mod, str): mod = Path(mod) if not options: options = {"options": {}, "disable": []} if mod.is_file(): print("Extracting mod...") tmp_dir: Path = install.open_mod(mod) elif mod.is_dir(): print(f"Loading mod from {str(mod)}...") tmp_dir = Path(TemporaryDirectory().name) shutil.copytree(mod, tmp_dir) else: print(f"Error: {str(mod)} is neither a valid file nor a directory") return if not ((tmp_dir / util.get_content_path()).exists() or (tmp_dir / util.get_dlc_path()).exists()): if (tmp_dir.parent / util.get_content_path()).exists(): tmp_dir = tmp_dir.parent elif util.get_settings("wiiu") and (tmp_dir / "Content").exists(): (tmp_dir / "Content").rename(tmp_dir / "content") else: raise FileNotFoundError( "This mod does not appear to have a valid folder structure") if (tmp_dir / "rules.txt").exists(): (tmp_dir / "rules.txt").unlink() if "showDepends" in meta: del meta["showDepends"] depend_string = f"{meta['name']}=={meta['version']}" meta["id"] = urlsafe_b64encode(depend_string.encode("utf8")).decode("utf8") any_platform = (options.get("options", dict()).get("general", dict()).get("agnostic", False)) meta["platform"] = ("any" if any_platform else "wiiu" if util.get_settings("wiiu") else "switch") (tmp_dir / "info.json").write_text(dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8") with Pool(maxtasksperchild=500) as pool: yml_files = set(tmp_dir.glob("**/*.yml")) if yml_files: print("Compiling YAML documents...") pool.map(_do_yml, yml_files) hashes = util.get_hash_table(util.get_settings("wiiu")) print("Packing SARCs...") _pack_sarcs(tmp_dir, hashes, pool) for folder in {d for d in tmp_dir.glob("options/*") if d.is_dir()}: _pack_sarcs(folder, hashes, pool) for option_dir in tmp_dir.glob("options/*"): for file in { f for f in option_dir.rglob("**/*") if (f.is_file() and (tmp_dir / f.relative_to(option_dir)).exists()) }: data1 = (tmp_dir / file.relative_to(option_dir)).read_bytes() data2 = file.read_bytes() if data1 == data2: util.vprint( f"Removing {file} from option {option_dir.name}, " "identical to base mod") file.unlink() del data1 del data2 if not options: options = {"disable": [], "options": {}} options["options"]["texts"] = {"all_langs": True} try: _make_bnp_logs(tmp_dir, options) for option_dir in { d for d in tmp_dir.glob("options/*") if d.is_dir() }: _make_bnp_logs(option_dir, options) except Exception as err: # pylint: disable=broad-except pool.terminate() raise Exception( f"There was an error generating change logs for your mod. {str(err)}" ) _clean_sarcs(tmp_dir, hashes, pool) for folder in {d for d in tmp_dir.glob("options/*") if d.is_dir()}: _clean_sarcs(folder, hashes, pool) print("Cleaning any junk files...") for file in {f for f in tmp_dir.rglob("**/*") if f.is_file()}: if "logs" in file.parts: continue if (file.suffix in {".yml", ".json", ".bak", ".tmp", ".old"} and file.stem != "info"): file.unlink() print("Removing blank folders...") for folder in reversed(list(tmp_dir.rglob("**/*"))): if folder.is_dir() and not list(folder.glob("*")): shutil.rmtree(folder) print(f"Saving output file to {str(output)}...") x_args = [util.get_7z_path(), "a", str(output), f'{str(tmp_dir / "*")}'] if system() == "Windows": subprocess.run( x_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=util.CREATE_NO_WINDOW, check=True, ) else: subprocess.run(x_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) shutil.rmtree(tmp_dir, ignore_errors=True) print("Conversion complete.")