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 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 = [ZPATH, "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.") 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 converting mods, see " '<a href="https://gamebanana.com/tuts/12493">' "this tutorial</a>.") print("Looks like an older mod, let's upgrade it...") upgrade.convert_old_mod(rulesdir, delete_old=True) return rulesdir
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