Exemple #1
0
    def perform_merge(self):
        # pylint: disable=unsupported-assignment-operation
        user_langs = (
            {util.get_settings("lang")}
            if not self._options["all_langs"]
            else util.get_user_languages()
        )
        print("Loading text mods...")
        diffs = self.consolidate_diffs(self.get_all_diffs())
        if not diffs:
            print("No text merge necessary")
            for bootup in util.get_master_modpack_dir().rglob("**/Bootup_????.pack"):
                bootup.unlink()
            return

        # find a mod lang for each user lang
        lang_map = map_languages(user_langs, set(diffs.keys()))
        for user_lang, mod_lang in lang_map.items():
            print(f"Merging modded texts for {mod_lang} into {user_lang}...")
            rsext.mergers.texts.merge_language(
                json.dumps(diffs[mod_lang]),
                str(util.get_game_file(f"Pack/Bootup_{user_lang}.pack")),
                str(
                    util.get_master_modpack_dir()
                    / util.get_content_path()
                    / "Pack"
                    / f"Bootup_{user_lang}.pack"
                ),
                util.get_settings("wiiu"),
            )
            print(f"{user_lang} texts merged successfully")
Exemple #2
0
def find_modded_files(
    tmp_dir: Path,
    pool: Optional[multiprocessing.pool.Pool] = None
) -> List[Union[Path, str]]:
    modded_files = []
    if isinstance(tmp_dir, str):
        tmp_dir = Path(tmp_dir)

    if (tmp_dir / util.get_dlc_path()).exists():
        try:
            util.get_aoc_dir()
        except FileNotFoundError:
            raise FileNotFoundError(
                "This mod uses DLC files, but BCML cannot locate the DLC folder in "
                "your game dump.")

    aoc_field = (tmp_dir / util.get_dlc_path() /
                 ("0010" if util.get_settings("wiiu") else "") / "Pack" /
                 "AocMainField.pack")
    if aoc_field.exists() and aoc_field.stat().st_size > 0:
        if not list((tmp_dir / util.get_dlc_path() /
                     ("0010" if util.get_settings("wiiu") else "")
                     ).rglob("Map/**/?-?_*.smubin")):
            aoc_pack = oead.Sarc(aoc_field.read_bytes())
            for file in aoc_pack.get_files():
                ex_out = (tmp_dir / util.get_dlc_path() /
                          ("0010" if util.get_settings("wiiu") else "") /
                          file.name)
                ex_out.parent.mkdir(parents=True, exist_ok=True)
                ex_out.write_bytes(file.data)
        aoc_field.write_bytes(b"")

    this_pool = pool or Pool(maxtasksperchild=500)
    results = this_pool.map(
        partial(_check_modded, tmp_dir=tmp_dir),
        {
            f
            for f in tmp_dir.rglob("**/*")
            if f.is_file() and "options" not in f.relative_to(tmp_dir).parts
        },
    )
    for result in results:
        if result:
            modded_files.append(result)
    total = len(modded_files)
    print(f'Found {total} modified file{"s" if total > 1 else ""}')

    total = 0
    sarc_files = {f for f in modded_files if f.suffix in util.SARC_EXTS}
    if sarc_files:
        print("Scanning files packed in SARCs...")
        for files in this_pool.imap_unordered(
                partial(find_modded_sarc_files, tmp_dir=tmp_dir), sarc_files):
            total += len(files)
            modded_files.extend(files)
        print(f'Found {total} modified packed file{"s" if total > 1 else ""}')
    if not pool:
        this_pool.close()
        this_pool.join()
    return modded_files
Exemple #3
0
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
Exemple #4
0
def link_master_mod(output: Path = None):
    util.create_bcml_graphicpack_if_needed()
    if not output:
        if not util.get_settings("export_dir"):
            return
        output = Path(util.get_settings("export_dir"))
    if output.exists():
        shutil.rmtree(output, ignore_errors=True)
    try:
        output.mkdir(parents=True, exist_ok=True)
        if not util.get_settings("no_cemu"):
            output.mkdir(parents=True, exist_ok=True)
            shutil.copy(util.get_master_modpack_dir() / "rules.txt",
                        output / "rules.txt")
    except (OSError, PermissionError, FileExistsError,
            FileNotFoundError) as err:
        raise RuntimeError(
            "There was a problem creating the master BCML graphic pack. "
            "It may be a one time fluke, so try remerging and/or restarting BCML. "
            "This can also happen if BCML and/or Cemu are installed into Program "
            "Files or any folder which requires administrator (or root) permissions. "
            "You can try running BCML as administrator or root, but bear in mind this "
            "is not officially supported. If the problem persists, good luck, because "
            "it's something wonky about your PC, I guess.") from err

    mod_folders: List[Path] = sorted(
        [
            item for item in util.get_modpack_dir().glob("*")
            if item.is_dir() and not (item / ".disabled").exists()
        ],
        reverse=True,
    )
    util.vprint(mod_folders)
    link_or_copy: Any = os.link if not util.get_settings(
        "no_hardlinks") else copyfile
    for mod_folder in mod_folders:
        for item in mod_folder.rglob("**/*"):
            rel_path = item.relative_to(mod_folder)
            exists = (output / rel_path).exists()
            is_log = str(rel_path).startswith("logs")
            is_opt = str(rel_path).startswith("options")
            is_meta = str(rel_path).startswith("meta")
            is_extra = (len(rel_path.parts) == 1 and rel_path.suffix != ".txt"
                        and not item.is_dir())
            if exists or is_log or is_extra or is_meta or is_opt:
                continue
            if item.is_dir():
                (output / rel_path).mkdir(parents=True, exist_ok=True)
            elif item.is_file():
                try:
                    link_or_copy(str(item), str(output / rel_path))
                except OSError:
                    if link_or_copy is os.link:
                        link_or_copy = copyfile
                        link_or_copy(str(item), str(output / rel_path))
                    else:
                        raise
Exemple #5
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)
Exemple #6
0
def link_master_mod(output: Path = None):
    util.create_bcml_graphicpack_if_needed()
    if not output:
        if util.get_settings("no_cemu"):
            return
        output = util.get_cemu_dir() / "graphicPacks" / "BreathOfTheWild_BCML"
    if output.exists():
        shutil.rmtree(output, ignore_errors=True)
    try:
        output.mkdir(parents=True, exist_ok=True)
        if not util.get_settings("no_cemu"):
            shutil.copy(util.get_master_modpack_dir() / "rules.txt",
                        output / "rules.txt")
    except (OSError, PermissionError, FileExistsError,
            FileNotFoundError) as err:
        raise RuntimeError(
            "There was a problem creating the master BCML graphic pack. "
            "It may be a one time fluke, so try remerging and/or restarting BCML. "
            "If the problem persists, good luck, because it's something wonky about your "
            "PC, I guess.") from err

    mod_folders: List[Path] = sorted(
        [
            item for item in util.get_modpack_dir().glob("*")
            if item.is_dir() and not (item / ".disabled").exists()
        ],
        reverse=True,
    )
    util.vprint(mod_folders)
    link_or_copy = os.link if not util.get_settings(
        "no_hardlinks") else copyfile
    for mod_folder in mod_folders:
        for item in mod_folder.rglob("**/*"):
            rel_path = item.relative_to(mod_folder)
            exists = (output / rel_path).exists()
            is_log = str(rel_path).startswith("logs")
            is_opt = str(rel_path).startswith("options")
            is_meta = str(rel_path).startswith("meta")
            is_extra = (len(rel_path.parts) == 1 and rel_path.suffix != ".txt"
                        and not item.is_dir())
            if exists or is_log or is_extra or is_meta or is_opt:
                continue
            if item.is_dir():
                (output / rel_path).mkdir(parents=True, exist_ok=True)
            elif item.is_file():
                try:
                    link_or_copy(str(item), str(output / rel_path))
                except OSError:
                    if link_or_copy is os.link:
                        link_or_copy = copyfile
                        link_or_copy(str(item), str(output / rel_path))
                    else:
                        raise
Exemple #7
0
def set_size(entry: str, size: int):
    rstb_path = (util.get_master_modpack_dir() / util.get_content_path() /
                 "System" / "Resource" /
                 "ResourceSizeTable.product.srsizetable")
    if rstb_path.exists():
        table = read_rstb(rstb_path, be=util.get_settings("wiiu"))
    else:
        table = get_stock_rstb()
        rstb_path.parent.mkdir(parents=True, exist_ok=True)
    table.set_size(entry, size)
    buf = io.BytesIO()
    table.write(buf, be=util.get_settings("wiiu"))
    rstb_path.write_bytes(util.compress(buf.getvalue()))
Exemple #8
0
 def get_ver(self, params=None):
     updated = util.get_settings("last_version") < VERSION
     res = {
         "version":
         USER_VERSION,
         "update": (util.get_latest_bcml() > VERSION
                    and not util.get_settings("suppress_update")),
         "showChangelog":
         updated and util.get_settings("changelog"),
     }
     if updated:
         util.get_settings()["last_version"] = VERSION
         util.save_settings()
     return res
Exemple #9
0
 def uninstall_all(self):
     for folder in {d for d in util.get_modpack_dir().glob("*") if d.is_dir()}:
         rmtree(folder, onerror=install.force_del)
     if not util.get_settings("no_cemu"):
         shutil.rmtree(
             util.get_cemu_dir() / "graphicPacks" / "bcmlPatches", ignore_errors=True
         )
Exemple #10
0
def _clean_sarc(old_sarc: oead.Sarc,
                base_sarc: oead.Sarc) -> Optional[oead.SarcWriter]:
    old_files = {f.name for f in old_sarc.get_files()}
    new_sarc = oead.SarcWriter(endian=oead.Endianness.Big if util.get_settings(
        "wiiu") else oead.Endianness.Little)
    can_delete = True
    for nest_file, file_data in [(f.name, f.data)
                                 for f in base_sarc.get_files()]:
        ext = Path(nest_file).suffix
        if ext in {".yml", ".bak"}:
            continue
        if nest_file in old_files:
            old_data = util.unyaz_if_needed(old_sarc.get_file(nest_file).data)
        file_data = util.unyaz_if_needed(file_data)
        if nest_file not in old_files or (file_data != old_data
                                          and ext not in util.AAMP_EXTS):
            if (ext in util.SARC_EXTS and nest_file in old_files
                    and nest_file not in SPECIAL):
                nest_old_sarc = oead.Sarc(old_data)
                nest_base_sarc = oead.Sarc(file_data)
                nest_new_sarc = _clean_sarc(nest_old_sarc, nest_base_sarc)
                if nest_new_sarc:
                    new_bytes = nest_new_sarc.write()[1]
                    if ext.startswith(".s") and ext != ".sarc":
                        new_bytes = util.compress(new_bytes)
                    new_sarc.files[nest_file] = oead.Bytes(new_bytes)
                    can_delete = False
                else:
                    continue
            else:
                if ext.startswith(".s") and ext != ".sarc":
                    file_data = util.compress(file_data)
                new_sarc.files[nest_file] = oead.Bytes(file_data)
                can_delete = False
    return None if can_delete else new_sarc
Exemple #11
0
def calculate_size(
    path: Union[Path, str], data: ByteString = None, guess: bool = True
) -> int:
    ext = path.suffix if isinstance(path, Path) else path[path.rindex(".") :]
    data = util.unyaz_if_needed(path.read_bytes() if isinstance(path, Path) else data)
    try:
        be = util.get_settings("wiiu")  # pylint: disable=invalid-name
        size = getattr(calculate_size, "calculator").calculate_file_size_with_ext(
            data, wiiu=be, ext=ext, force=False
        )
        if ext == ".bdmgparam":
            size = 0
        if ext == ".hkrb":
            size += 40
        if ext == ".baniminfo":
            size = int(
                (((len(data) + 31) & -32) * (1.5 if len(data) > 36864 else 4))
                + 0xE4
                + 0x24C
            )
            if not be:
                size = int(size * 1.5)
        if size == 0 and guess:
            if ext in util.AAMP_EXTS:
                size = guess_aamp_size(data, be, ext)
            elif ext in {".bfres", ".sbfres"}:
                size = guess_bfres_size(
                    data, be, path if isinstance(path, str) else path.name,
                )
        return size
    except struct.error:
        return 0
Exemple #12
0
def disable_bcml_gfx():
    if not util.get_settings("no_cemu"):
        settings = util.parse_cemu_settings()
        try:
            gpack = settings.getElementsByTagName("GraphicPack")[0]
        except IndexError:
            gpack = settings.createElement("GraphicPack")
            settings.appendChild(gpack)
        new_cemu = True
        entry: minidom.Element
        for entry in gpack.getElementsByTagName("Entry"):
            if new_cemu and entry.getElementsByTagName("filename"):
                new_cemu = False
            try:
                if ("bcml" in entry.getElementsByTagName("filename")
                    [0].childNodes[0].data.lower()):
                    gpack.removeChild(entry)
            except IndexError:
                if "bcml" in entry.getAttribute("filename").lower():
                    gpack.removeChild(entry)
        settings.writexml(
            (util.get_cemu_dir() / "settings.xml").open("w", encoding="utf-8"),
            addindent="    ",
            newl="\n",
        )
Exemple #13
0
def uninstall_mod(mod: BcmlMod, wait_merge: bool = False):
    has_patches = (mod.path / "patches").exists()
    try:
        shutil.rmtree(str(mod.path), onerror=force_del)
    except (OSError, PermissionError, WindowsError) as err:
        raise RuntimeError(
            f"The folder for {mod.name} could not be removed. "
            "You may need to delete it manually and remerge, or "
            "close all open programs (including BCML and Windows Explorer) "
            "and try again. The location of the folder is "
            f"<code>{str(mod.path)}</code>.") from err

    for fall_mod in [
            m for m in util.get_installed_mods(True)
            if m.priority > mod.priority
    ]:
        fall_mod.change_priority(fall_mod.priority - 1)

    if not util.get_installed_mods():
        shutil.rmtree(util.get_master_modpack_dir())
        util.create_bcml_graphicpack_if_needed()
    else:
        if not wait_merge:
            refresh_merges()

    if has_patches and not util.get_settings("no_cemu"):
        shutil.rmtree(
            util.get_cemu_dir() / "graphicPacks" / "bcmlPatches" /
            util.get_safe_pathname(mod.name),
            ignore_errors=True,
        )

    print(f"{mod.name} has been uninstalled.")
Exemple #14
0
 def __init__(self) -> None:
     self._gameid = "5866" if util.get_settings("wiiu") else "6386"
     if not GB_DATA.exists():
         GB_DATA.write_bytes(
             util.decompress(
                 (util.get_exec_dir() / "data" / "gb.sjson").read_bytes()))
     self._data = json.loads(GB_DATA.read_text("utf-8"))
Exemple #15
0
    def perform_merge(self):
        merged_quests = util.get_master_modpack_dir() / "logs" / "quests.byml"
        print("Loading quest mods...")
        diffs = self.consolidate_diffs(self.get_all_diffs())
        if not diffs:
            print("No quest merging necessary")
            if merged_quests.exists():
                merged_quests.unlink()
                try:
                    util.inject_file_into_sarc(
                        "Quest/QuestProduct.sbquestpack",
                        util.get_nested_file_bytes(
                            (f'{str(util.get_game_file("Pack/TitleBG.pack"))}'
                             "//Quest/QuestProduct.sbquestpack"),
                            unyaz=False,
                        ),
                        "Pack/TitleBG.pack",
                    )
                except FileNotFoundError:
                    pass
            return
        print("Loading stock quests...")
        quests = get_stock_quests()
        stock_names = [q["Name"] for q in quests]

        print("Merging quest mods...")
        for name, mod in diffs["mod"].items():
            try:
                quests[stock_names.index(name)] = mod
            except (IndexError, ValueError):
                diffs["add"].append(mod)
        for delete in reversed(diffs["del"]):
            try:
                quests.remove(quests[stock_names.index(delete)])
            except ValueError:
                pass
            except IndexError:
                raise RuntimeError(
                    f"An error occurred when attempting to remove a quest from your "
                    "game. Most of the time this means the mod was accidentally made "
                    "using the base game 1.0 TitleBG.pack instead of the latest updated "
                    "version. Please contact the mod author for assistance.")
        added_names = set()
        for add in diffs["add"]:
            if add["Name"] not in added_names:
                quests.append(add)
                added_names.add(add["Name"])

        print("Writing new quest pack...")
        data = oead.byml.to_binary(quests,
                                   big_endian=util.get_settings("wiiu"))
        merged_quests.parent.mkdir(parents=True, exist_ok=True)
        merged_quests.write_bytes(data)
        util.inject_file_into_sarc(
            "Quest/QuestProduct.sbquestpack",
            util.compress(data),
            "Pack/TitleBG.pack",
            create_sarc=True,
        )
Exemple #16
0
    def perform_merge(self):
        merged_events = util.get_master_modpack_dir() / "logs" / "eventinfo.byml"
        event_merge_log = util.get_master_modpack_dir() / "logs" / "eventinfo.log"

        print("Loading event info mods...")
        modded_events = self.consolidate_diffs(self.get_all_diffs())
        event_mod_hash = hash(str(modded_events))
        if not modded_events:
            print("No event info merging necessary")
            if merged_events.exists():
                merged_events.unlink()
                event_merge_log.unlink()
                try:
                    stock_eventinfo = util.get_nested_file_bytes(
                        (
                            str(util.get_game_file("Pack/Bootup.pack"))
                            + "//Event/EventInfo.product.sbyml"
                        ),
                        unyaz=False,
                    )
                    util.inject_file_into_sarc(
                        "Event/EventInfo.product.sbyml",
                        stock_eventinfo,
                        "Pack/Bootup.pack",
                    )
                except FileNotFoundError:
                    pass
            return
        if event_merge_log.exists() and event_merge_log.read_text() == event_mod_hash:
            print("No event info merging necessary")
            return

        new_events = get_stock_eventinfo()
        for event, data in modded_events.items():
            new_events[event] = data
        del modded_events

        print("Writing new event info...")
        event_bytes = oead.byml.to_binary(
            new_events, big_endian=util.get_settings("wiiu")
        )
        del new_events
        util.inject_file_into_sarc(
            "Event/EventInfo.product.sbyml",
            util.compress(event_bytes),
            "Pack/Bootup.pack",
            create_sarc=True,
        )
        print("Saving event info merge log...")
        event_merge_log.parent.mkdir(parents=True, exist_ok=True)
        event_merge_log.write_text(str(event_mod_hash))
        merged_events.write_bytes(event_bytes)

        print("Updating RSTB...")
        rstb_size = rstb.SizeCalculator().calculate_file_size_with_ext(
            bytes(event_bytes), True, ".byml"
        )
        del event_bytes
        rstable.set_size("Event/EventInfo.product.byml", rstb_size)
Exemple #17
0
def _yml_to_byml(file: Path):
    data = oead.byml.to_binary(
        oead.byml.from_text(file.read_text("utf-8")),
        big_endian=util.get_settings("wiiu"),
    )
    out = file.with_suffix("")
    out.write_bytes(data if not out.suffix.startswith(".s") else util.compress(data))
    file.unlink()
Exemple #18
0
 def get_ver(self, params=None):
     return {
         "version":
         USER_VERSION,
         "update":
         util.get_latest_bcml() > VERSION
         and not util.get_settings("suppress_update"),
     }
Exemple #19
0
def get_stock_rstb() -> rstb.ResourceSizeTable:
    if not hasattr(get_stock_rstb, "table"):
        get_stock_rstb.table = read_rstb(
            str(
                util.get_game_file(
                    "System/Resource/ResourceSizeTable.product.srsizetable")),
            util.get_settings("wiiu"),
        )
    return deepcopy(get_stock_rstb.table)
Exemple #20
0
 def mods(self):
     return [
         m for m in self._data["mods"].values()
         if (("WiiU" in m["game"] if self._gameid == "5866" else "Switch" in
              m["game"]) and (util.get_settings("nsfw") or all(
                  nsfw not in (m["name"] + m["description"] +
                               m["text"]).lower()
                  for nsfw in {"nsfw", "thicc", "boob", "cosplay"})))
     ]
Exemple #21
0
 def save_settings(self, params):
     print("Saving settings, BCML will reload momentarily...")
     if util.get_settings("wiiu") != params["settings"]["wiiu"]:
         util.clear_all_caches()
         if hasattr(self, "gb_api"):
             self.gb_api.reset_update_time(params["settings"]["wiiu"])
             del self.gb_api
             self.gb_api = GameBananaDb()
     util.get_settings.settings = params["settings"]
     util.save_settings()
Exemple #22
0
    def perform_merge(self):
        actor_path = (util.get_master_modpack_dir() / util.get_content_path() /
                      "Actor" / "ActorInfo.product.sbyml")
        print("Loading modded actor info...")
        modded_actors = self.consolidate_diffs(self.get_all_diffs())
        if not modded_actors:
            print("No actor info merging necessary.")
            if actor_path.exists():
                actor_path.unlink()
            return

        print("Loading unmodded actor info...")
        actorinfo = get_stock_actorinfo()
        stock_actors = {
            crc32(actor["name"].encode("utf8")): actor
            for actor in actorinfo["Actors"]
        }

        print("Merging changes...")
        new_hashes = set()
        for actor_hash, actor_info in modded_actors.items():
            if isinstance(actor_hash, str):
                actor_hash = int(actor_hash)
            if actor_hash in stock_actors:
                util.dict_merge(stock_actors[actor_hash],
                                actor_info,
                                overwrite_lists=True)
            else:
                actorinfo["Actors"].append(actor_info)
                new_hashes.add(actor_hash)

        print("Sorting new actor info...")
        actorinfo["Hashes"] = oead.byml.Array([
            oead.S32(x) if x < 2147483648 else oead.U32(x)
            for x in sorted(new_hashes | set(stock_actors.keys()))
        ])
        try:
            actorinfo["Actors"] = sorted(
                actorinfo["Actors"],
                key=lambda x: crc32(x["name"].encode("utf-8")))
        except KeyError as err:
            if str(err) == "":
                raise RuntimeError(
                    "Your actor info mods could not be merged. "
                    "This usually indicates a corrupt game dump.") from err
            else:
                raise

        print("Saving new actor info...")
        actor_path.parent.mkdir(parents=True, exist_ok=True)
        actor_path.write_bytes(
            util.compress(
                oead.byml.to_binary(actorinfo,
                                    big_endian=util.get_settings("wiiu"))))
        print("Actor info merged successfully")
Exemple #23
0
def swap_region(mod_pack: Path, user_lang: str) -> Path:
    mod_sarc = oead.Sarc(mod_pack.read_bytes())
    mod_msg_data = mod_sarc.get_file(0).data
    new_pack = oead.SarcWriter(
        endian=oead.Endianness.Big
        if util.get_settings("wiiu")
        else oead.Endianness.Little
    )
    new_pack.files[f"Message/Msg_{user_lang}.product.ssarc"] = oead.Bytes(mod_msg_data)
    new_pack_path = mod_pack.with_name(f"Bootup_{user_lang}.temppack")
    new_pack_path.write_bytes(new_pack.write()[1])
    return new_pack_path
Exemple #24
0
    def perform_merge(self):
        merged_quests = util.get_master_modpack_dir() / "logs" / "quests.byml"
        print("Loading quest mods...")
        diffs = self.consolidate_diffs(self.get_all_diffs())
        if not diffs:
            print("No quest merging necessary")
            if merged_quests.exists():
                merged_quests.unlink()
                try:
                    util.inject_file_into_sarc(
                        "Quest/QuestProduct.sbquestpack",
                        util.get_nested_file_bytes(
                            (f'{str(util.get_game_file("Pack/TitleBG.pack"))}'
                             "//Quest/QuestProduct.sbquestpack"),
                            unyaz=False,
                        ),
                        "Pack/TitleBG.pack",
                    )
                except FileNotFoundError:
                    pass
            return
        print("Loading stock quests...")
        quests = get_stock_quests()
        stock_names = [q["Name"] for q in quests]

        print("Merging quest mods...")
        for name, mod in diffs["mod"].items():
            try:
                quests[stock_names.index(name)] = mod
            except (IndexError, ValueError):
                diffs["add"].append(mod)
        for delete in reversed(diffs["del"]):
            try:
                quests.remove(quests[stock_names.index(delete)])
            except ValueError:
                pass
        added_names = set()
        for add in diffs["add"]:
            if add["Name"] not in added_names:
                quests.append(add)
                added_names.add(add["Name"])

        print("Writing new quest pack...")
        data = oead.byml.to_binary(quests,
                                   big_endian=util.get_settings("wiiu"))
        merged_quests.parent.mkdir(parents=True, exist_ok=True)
        merged_quests.write_bytes(data)
        util.inject_file_into_sarc(
            "Quest/QuestProduct.sbquestpack",
            util.compress(data),
            "Pack/TitleBG.pack",
            create_sarc=True,
        )
Exemple #25
0
    def perform_merge(self):
        merged_effects = util.get_master_modpack_dir() / "logs" / "effects.byml"
        print("Loading status effect mods...")
        diffs = self.consolidate_diffs(self.get_all_diffs())
        if not diffs:
            print("No status effect merging necessary...")
            if merged_effects.exists():
                merged_effects.unlink()
                try:
                    stock_effects = util.get_nested_file_bytes(
                        (
                            str(util.get_game_file("Pack/Bootup.pack"))
                            + "//Ecosystem/StatusEffectList.sbyml"
                        ),
                        unyaz=False,
                    )
                    util.inject_file_into_sarc(
                        "Ecosystem/StatusEffectList.sbyml",
                        stock_effects,
                        "Pack/Bootup.pack",
                    )
                    del stock_effects
                except FileNotFoundError:
                    pass
            return
        util.vprint("All status effect diffs:")
        util.vprint(diffs)

        effects = get_stock_effects()
        util.dict_merge(effects, diffs, overwrite_lists=True)
        del diffs

        print("Writing new effects list...")
        effect_bytes = oead.byml.to_binary(
            oead.byml.Array([effects]), big_endian=util.get_settings("wiiu")
        )
        del effects
        util.inject_file_into_sarc(
            "Ecosystem/StatusEffectList.sbyml",
            util.compress(effect_bytes),
            "Pack/Bootup.pack",
            create_sarc=True,
        )
        print("Saving status effect merge log...")
        merged_effects.parent.mkdir(parents=True, exist_ok=True)
        merged_effects.write_bytes(effect_bytes)

        print("Updating RSTB...")
        rstb_size = rstb.SizeCalculator().calculate_file_size_with_ext(
            effect_bytes, True, ".byml"
        )
        del effect_bytes
        rstable.set_size("Ecosystem/StatusEffectList.byml", rstb_size)
Exemple #26
0
 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",
         )
Exemple #27
0
def find_modded_files(
    tmp_dir: Path,
    pool: Optional[multiprocessing.pool.Pool] = None
) -> List[Union[Path, str]]:
    modded_files = []
    if isinstance(tmp_dir, str):
        tmp_dir = Path(tmp_dir)

    if (tmp_dir / util.get_dlc_path()).exists():
        try:
            util.get_aoc_dir()
        except FileNotFoundError:
            raise FileNotFoundError(
                "This mod uses DLC files, but BCML cannot locate the DLC folder in "
                "your game dump.")

    aoc_field = (tmp_dir / util.get_dlc_path() /
                 ("0010" if util.get_settings("wiiu") else "") / "Pack" /
                 "AocMainField.pack")
    if aoc_field.exists() and aoc_field.stat().st_size > 0:
        if not list((tmp_dir / util.get_dlc_path() /
                     ("0010" if util.get_settings("wiiu") else "")
                     ).rglob("Map/**/?-?_*.smubin")):
            aoc_pack = oead.Sarc(aoc_field.read_bytes())
            for file in aoc_pack.get_files():
                ex_out = (tmp_dir / util.get_dlc_path() /
                          ("0010" if util.get_settings("wiiu") else "") /
                          file.name)
                ex_out.parent.mkdir(parents=True, exist_ok=True)
                ex_out.write_bytes(file.data)
        aoc_field.write_bytes(b"")

    modded_files = [
        f if "//" in f else Path(f) for f in rsext.find_modified_files(
            str(tmp_dir), util.get_settings("wiiu"))
    ]
    return modded_files
Exemple #28
0
def enable_bcml_gfx():
    if util.get_settings("no_cemu"):
        return

    settings = util.parse_cemu_settings()
    try:
        gpack = settings.getElementsByTagName("GraphicPack")[0]
    except IndexError:
        gpack = settings.createElement("GraphicPack")
        settings.appendChild(gpack)
    new_cemu = True

    def create_entry(path: str):
        def entry_matches(entry):
            try:
                return (path == entry.getElementsByTagName("filename")
                        [0].childNodes[0].data)
            except IndexError:
                return path == entry.getAttribute("filename")

        if any(
                entry_matches(entry)
                for entry in gpack.getElementsByTagName("Entry")):
            return
        entry: minidom.Element = settings.createElement("Entry")
        if new_cemu:
            entry.setAttribute("filename", path)
        else:
            entryfile = settings.createElement("filename")
            entryfile.appendChild(settings.createTextNode(path))
            entry.appendChild(entryfile)
        entrypreset = settings.createElement("preset")
        entrypreset.appendChild(settings.createTextNode(""))
        entry.appendChild(entrypreset)
        gpack.appendChild(entry)

    create_entry("graphicPacks\\BreathOfTheWild_BCML\\rules.txt")

    if (util.get_cemu_dir() / "graphicPacks" / "bcmlPatches").exists():
        for rules in (util.get_cemu_dir() / "graphicPacks" /
                      "bcmlPatches").rglob("rules.txt"):
            create_entry(str(rules.relative_to(util.get_cemu_dir())))

        settings.writexml(
            (util.get_cemu_dir() / "settings.xml").open("w", encoding="utf-8"),
            addindent="    ",
            newl="\n",
        )
Exemple #29
0
def _pack_sarc(folder: Path, tmp_dir: Path, hashes: dict):
    packed = oead.SarcWriter(
        endian=oead.Endianness.Big
        if util.get_settings("wiiu")
        else oead.Endianness.Little
    )
    try:
        canon = util.get_canon_name(
            folder.relative_to(tmp_dir).as_posix(), allow_no_source=True
        )
        if canon not in hashes:
            raise FileNotFoundError("File not in game dump")
        stock_file = util.get_game_file(folder.relative_to(tmp_dir))
        try:
            old_sarc = oead.Sarc(util.unyaz_if_needed(stock_file.read_bytes()))
        except (RuntimeError, ValueError, oead.InvalidDataError):
            raise ValueError("Cannot open file from game dump")
        old_files = {f.name for f in old_sarc.get_files()}
    except (FileNotFoundError, ValueError):
        for file in {f for f in folder.rglob("**/*") if f.is_file()}:
            packed.files[file.relative_to(folder).as_posix()] = file.read_bytes()
    else:
        for file in {
            f
            for f in folder.rglob("**/*")
            if f.is_file() and not f.suffix in EXCLUDE_EXTS
        }:
            file_data = file.read_bytes()
            xhash = xxhash.xxh64_intdigest(util.unyaz_if_needed(file_data))
            file_name = file.relative_to(folder).as_posix()
            if file_name in old_files:
                old_hash = xxhash.xxh64_intdigest(
                    util.unyaz_if_needed(old_sarc.get_file(file_name).data)
                )
            if file_name not in old_files or (xhash != old_hash):
                packed.files[file_name] = file_data
    finally:
        shutil.rmtree(folder)
        if not packed.files:
            return  # pylint: disable=lost-exception
        sarc_bytes = packed.write()[1]
        folder.write_bytes(
            util.compress(sarc_bytes)
            if (folder.suffix.startswith(".s") and not folder.suffix == ".sarc")
            else sarc_bytes
        )
Exemple #30
0
 def old_settings(self):
     old = util.get_data_dir() / "settings.ini"
     if old.exists():
         try:
             upgrade.convert_old_settings()
             settings = util.get_settings()
             return {
                 "exists": True,
                 "message": "Your old settings were converted successfully.",
                 "settings": settings,
             }
         except:  # pylint: disable=bare-except
             return {
                 "exists": True,
                 "message": "Your old settings could not be converted.",
             }
     else:
         return {"exists": False, "message": "No old settings found."}