예제 #1
0
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
예제 #2
0
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
예제 #3
0
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