Ejemplo n.º 1
0
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
Ejemplo n.º 2
0
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)
Ejemplo n.º 3
0
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')
Ejemplo n.º 4
0
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')
Ejemplo n.º 5
0
 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)
Ejemplo n.º 6
0
 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
Ejemplo n.º 7
0
 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)
Ejemplo n.º 8
0
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)
Ejemplo n.º 9
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
Ejemplo n.º 10
0
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.")