예제 #1
0
파일: _api.py 프로젝트: VelouriasMoon/BCML
 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",
         )
예제 #2
0
    def generate_diff(self, mod_dir: Path, modded_files: List[Union[Path,
                                                                    str]]):
        if (f"{util.get_content_path()}/Pack/TitleBG.pack//Quest/QuestProduct.sbquestpack"
                not in modded_files):
            return {}
        print("Logging modified quests...")
        stock_quests = get_stock_quests()
        stock_names = [q["Name"] for q in stock_quests]

        title_sarc = oead.Sarc((mod_dir / util.get_content_path() / "Pack" /
                                "TitleBG.pack").read_bytes())
        mod_quests = oead.byml.from_binary(
            util.decompress(
                title_sarc.get_file("Quest/QuestProduct.sbquestpack").data))
        mod_names = [q["Name"] for q in mod_quests]
        diffs = oead.byml.Hash({
            "add":
            oead.byml.Array(),
            "mod":
            oead.byml.Hash(),
            "del":
            oead.byml.Array({q
                             for q in stock_names if q not in mod_names}),
        })

        for i, quest in enumerate(mod_quests):
            quest_name = quest["Name"]
            quest["prev_quest"] = mod_quests[
                i - 1]["Name"] if i > 0 else "--index_zero"
            if quest_name not in stock_names:
                diffs["add"].append(quest)
            elif quest != stock_quests[stock_names.index(quest_name)]:
                diffs["mod"][quest_name] = quest

        return diffs
예제 #3
0
파일: texts.py 프로젝트: NiceneNerd/BCML
    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")
예제 #4
0
def _make_bnp_logs(tmp_dir: Path, options: dict):
    util.vprint(install.generate_logs(tmp_dir, options=options))

    print("Removing unnecessary files...")

    if (tmp_dir / "logs" / "map.yml").exists():
        print("Removing map units...")
        for file in [
            file
            for file in tmp_dir.rglob("**/*.smubin")
            if fnmatch(file.name, "[A-Z]-[0-9]_*.smubin") and "MainField" in file.parts
        ]:
            file.unlink()

    if set((tmp_dir / "logs").glob("*texts*")):
        print("Removing language bootup packs...")
        for bootup_lang in (tmp_dir / util.get_content_path() / "Pack").glob(
            "Bootup_*.pack"
        ):
            bootup_lang.unlink()

    if (tmp_dir / "logs" / "actorinfo.yml").exists() and (
        tmp_dir / util.get_content_path() / "Actor" / "ActorInfo.product.sbyml"
    ).exists():
        print("Removing ActorInfo.product.sbyml...")
        (
            tmp_dir / util.get_content_path() / "Actor" / "ActorInfo.product.sbyml"
        ).unlink()

    if (tmp_dir / "logs" / "gamedata.yml").exists() or (
        tmp_dir / "logs" / "savedata.yml"
    ).exists():
        print("Removing gamedata sarcs...")
        bsarc = oead.Sarc(
            (tmp_dir / util.get_content_path() / "Pack" / "Bootup.pack").read_bytes()
        )
        csarc = oead.SarcWriter.from_sarc(bsarc)
        bsarc_files = {f.name for f in bsarc.get_files()}
        if "GameData/gamedata.ssarc" in bsarc_files:
            del csarc.files["GameData/gamedata.ssarc"]
        if "GameData/savedataformat.ssarc" in bsarc_files:
            del csarc.files["GameData/savedataformat.ssarc"]
        (tmp_dir / util.get_content_path() / "Pack" / "Bootup.pack").write_bytes(
            csarc.write()[1]
        )
예제 #5
0
파일: actors.py 프로젝트: arkhamsaiyan/BCML
    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")
예제 #6
0
파일: rstable.py 프로젝트: Endrr/BCML
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()))
예제 #7
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

        bin_data = oead.byml.to_binary(modded_actors, False)
        rsext.mergers.actorinfo.merge_actorinfo(bin_data)
        print("Actor info merged successfully")
예제 #8
0
파일: data.py 프로젝트: arkhamsaiyan/BCML
 def generate_diff(self, mod_dir: Path, modded_files: List[Union[Path,
                                                                 str]]):
     if (f"{util.get_content_path()}/Pack/Bootup.pack//GameData/gamedata.ssarc"
             in modded_files):
         print("Logging changes to game data flags...")
         bootup_sarc = oead.Sarc(
             util.unyaz_if_needed((mod_dir / util.get_content_path() /
                                   "Pack" / "Bootup.pack").read_bytes()))
         data_sarc = oead.Sarc(
             util.decompress(
                 bootup_sarc.get_file("GameData/gamedata.ssarc").data))
         diff = get_modded_gamedata_entries(data_sarc, pool=self._pool)
         del bootup_sarc
         del data_sarc
         return diff
     else:
         return {}
예제 #9
0
파일: texts.py 프로젝트: StoneyMoonCat/BCML
    def generate_diff(self, mod_dir: Path, modded_files: List[Union[str,
                                                                    Path]]):
        print("Checking for modified languages...")
        languages = {
            util.get_file_language(file)
            for file in modded_files
            if (isinstance(file, Path) and "Bootup_" in file.name
                and "Graphic" not in file.name)
        }
        if not languages:
            return None
        util.vprint(f'Languages: {",".join(languages)}')

        language_map = {}
        save_langs = (LANGUAGES if self._options.get("all_langs", False) else
                      [util.get_settings("lang")])
        for lang in save_langs:
            if lang in languages:
                language_map[lang] = lang
            elif lang[2:4] in [l[2:4] for l in languages]:
                language_map[lang] = [
                    l for l in languages if l[2:4] == lang[2:4]
                ][0]
            else:
                language_map[lang] = [l for l in LANGUAGES
                                      if l in languages][0]
        util.vprint(f"Language map:")
        util.vprint(language_map)

        language_diffs = {}
        for language in set(language_map.values()):
            print(f"Logging text changes for {language}...")
            language_diffs[language] = diff_language(
                mod_dir / util.get_content_path() / "Pack" /
                f"Bootup_{language}.pack",
                pool=self._pool,
            )

        return {
            save_lang: language_diffs[map_lang]
            for save_lang, map_lang in language_map.items()
        }
예제 #10
0
 def generate_diff(self, mod_dir: Path, modded_files: List[Union[Path, str]]):
     if (
         f"{util.get_content_path()}/Pack/Bootup.pack//Ecosystem/AreaData.sbyml"
         in modded_files
     ):
         print("Logging modded areadata...")
         bootup_sarc = oead.Sarc(
             (
                 mod_dir / util.get_content_path() / "Pack" / "Bootup.pack"
             ).read_bytes()
         )
         areadata = oead.byml.from_binary(
             util.decompress(bootup_sarc.get_file("Ecosystem/AreaData.sbyml").data)
         )
         diff = get_modded_areadata(areadata)
         del bootup_sarc
         del areadata
         return diff
     else:
         return {}
예제 #11
0
파일: effects.py 프로젝트: NiceneNerd/BCML
 def generate_diff(self, mod_dir: Path, modded_files: List[Union[str,
                                                                 Path]]):
     needle = f"{util.get_content_path()}/Pack/Bootup.pack//Ecosystem/StatusEffectList.sbyml"
     if needle not in modded_files:
         return {}
     print("Logging changes to effect status levels...")
     stock_effects = get_stock_effects()
     bootup_sarc = oead.Sarc((mod_dir / util.get_content_path() / "Pack" /
                              "Bootup.pack").read_bytes())
     mod_effects = oead.byml.from_binary(
         util.decompress(
             bootup_sarc.get_file(
                 "Ecosystem/StatusEffectList.sbyml").data))[0]
     diff = oead.byml.Hash({
         effect: params
         for effect, params in mod_effects.items()
         if stock_effects[effect] != params
     })
     del stock_effects
     del mod_effects
예제 #12
0
 def generate_diff(self, mod_dir: Path, modded_files: List[Union[Path, str]]):
     if (
         f"{util.get_content_path()}/Pack/Bootup.pack//Event/EventInfo.product.sbyml"
         in modded_files
     ):
         print("Logging modded events...")
         bootup_sarc = oead.Sarc(
             (mod_dir / util.get_content_path() / "Pack" / "Bootup.pack").read_bytes()
         )
         event_info = oead.byml.from_binary(
             util.decompress(
                 bootup_sarc.get_file("Event/EventInfo.product.sbyml").data
             )
         )
         diff = get_modded_events(event_info)
         del bootup_sarc
         del event_info
         return diff
     else:
         return {}
예제 #13
0
파일: texts.py 프로젝트: NiceneNerd/BCML
    def generate_diff(self, mod_dir: Path, modded_files: List[Union[str, Path]]):
        print("Checking for modified languages...")
        mod_langs = {
            util.get_file_language(file)
            for file in modded_files
            if (
                isinstance(file, Path)
                and "Bootup_" in file.name
                and "Graphic" not in file.name
            )
        }
        if not mod_langs:
            return None
        util.vprint(f'Languages: {",".join(mod_langs)}')

        # find a user lang for each mod lang
        language_map = map_languages(mod_langs, util.get_user_languages())
        util.vprint("Language map:")
        util.vprint(language_map)

        language_diffs = {}
        for mod_lang, user_lang in language_map.items():
            print(f"Logging text changes for {user_lang}...")
            mod_pack = (
                mod_dir / util.get_content_path() / "Pack" / f"Bootup_{mod_lang}.pack"
            )
            if not user_lang == mod_lang:
                mod_pack = swap_region(mod_pack, user_lang)
            ref_pack = util.get_game_file(f"Pack/Bootup_{user_lang}.pack")
            language_diffs[user_lang] = rsext.mergers.texts.diff_language(
                str(mod_pack), str(ref_pack), user_lang[2:4] != mod_lang[2:4]
            )
            if not user_lang == mod_lang:
                mod_pack.unlink()

        return language_diffs
예제 #14
0
파일: texts.py 프로젝트: StoneyMoonCat/BCML
    def perform_merge(self):
        # pylint: disable=unsupported-assignment-operation
        langs = ({util.get_settings("lang")}
                 if not self._options["all_langs"] else get_user_languages())
        for lang in langs:
            print("Loading text mods...")
            diffs = self.consolidate_diffs(self.get_all_diffs())
            if not diffs or lang not in diffs:
                print("No text merge necessary")
                for bootup in util.get_master_modpack_dir().rglob(
                        "**/Bootup_????.pack"):
                    bootup.unlink()
                return
            util.vprint({
                lang: {
                    file: list(entries.keys())
                    for file, entries in diffs[lang].items()
                }
            })

            print(f"Merging modded texts for {lang}...")
            saved_files = set()
            with TemporaryDirectory() as tmp:
                tmp_dir = Path(tmp)
                ref_lang = "XXen" if lang.endswith("en") else lang
                extract_refs(ref_lang, tmp_dir)
                tmp_dir = tmp_dir / "refs" / ref_lang
                this_pool = self._pool or multiprocessing.Pool(
                    maxtasksperchild=500)
                this_pool.map(partial(merge_msyt, tmp_dir=tmp_dir),
                              diffs[lang].items())
                if not self._pool:
                    this_pool.close()
                    this_pool.join()

                m_args = [
                    MSYT_PATH,
                    "create",
                    "-d",
                    str(tmp_dir),
                    "-p",
                    "wiiu" if util.get_settings("wiiu") else "switch",
                    "-o",
                    str(tmp_dir),
                ]
                result: subprocess.CompletedProcess
                if system() == "Windows":
                    result = subprocess.run(
                        m_args,
                        capture_output=True,
                        creationflags=util.CREATE_NO_WINDOW,
                        check=False,
                        text=True,
                    )

                else:
                    result = subprocess.run(
                        m_args,
                        capture_output=True,
                        check=False,
                        text=True,
                    )
                if result.stderr:
                    raise RuntimeError(
                        f"There was an error merging game texts. {result.stderr}"
                    )

                msg_sarc = oead.SarcWriter(
                    endian=oead.Endianness.Big if util.
                    get_settings("wiiu") else oead.Endianness.Little)
                for file in tmp_dir.rglob("**/*.msbt"):
                    msg_sarc.files[file.relative_to(
                        tmp_dir).as_posix()] = file.read_bytes()
                    saved_files.add(file.relative_to(tmp_dir).as_posix())
            bootup_sarc = oead.SarcWriter(
                endian=oead.Endianness.Big if util.
                get_settings("wiiu") else oead.Endianness.Little)
            bootup_sarc.files[
                f"Message/Msg_{lang}.product.ssarc"] = util.compress(
                    msg_sarc.write()[1])

            bootup_path = (util.get_master_modpack_dir() /
                           util.get_content_path() / "Pack" /
                           f"Bootup_{lang}.pack")
            bootup_path.parent.mkdir(parents=True, exist_ok=True)
            bootup_path.write_bytes(bootup_sarc.write()[1])
            del bootup_sarc
            del msg_sarc
            print(f"{lang} texts merged successfully")
예제 #15
0
파일: dev.py 프로젝트: StoneyMoonCat/BCML
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.")
예제 #16
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
예제 #17
0
    def perform_merge(self):
        pool = self._pool or Pool(maxtasksperchild=500)
        if not self._table:
            self._table = get_stock_rstb()
        diffs = self.consolidate_diffs(self.get_all_diffs())
        master = util.get_master_modpack_dir()
        master_files = {
            f for f in master.rglob("**/*") if f.is_file() and "logs" not in f.parts
        }
        diffs.update(
            {
                k: v
                for r in pool.map(
                    partial(
                        _get_modded_file_size,
                        mod_dir=master,
                        guess=not self._options.get("no_guess", False),
                    ),
                    {f for f in master_files if f.suffix not in EXCLUDE_EXTS},
                )
                for k, v in r.items()
                if r is not None and not self.should_exclude(k, v)
            }
        )

        diffs.update(
            {
                k: v
                for r in pool.map(
                    partial(_get_sizes_in_sarc, guess=not util.get_settings("no_guess"),),
                    {
                        f
                        for f in master_files
                        if f.suffix in util.SARC_EXTS - SARC_EXCLUDES
                    },
                )
                for k, v in r.items()
                if r is not None and not self.should_exclude(k, v)
            }
        )
        table = self._table
        for canon, size in diffs.copy().items():
            if size == 0:
                if table.is_in_table(canon):
                    table.delete_entry(canon)
                else:
                    continue
            elif table.is_in_table(canon) and size < table.get_size(canon):
                del diffs[canon]
                continue
            else:
                table.set_size(canon, size)
        if table.is_in_table(f"Message/Msg_{util.get_settings('lang')}.product.sarc"):
            table.delete_entry(f"Message/Msg_{util.get_settings('lang')}.product.sarc")

        out = (
            master
            / util.get_content_path()
            / "System"
            / "Resource"
            / "ResourceSizeTable.product.srsizetable"
        )
        out.parent.mkdir(parents=True, exist_ok=True)
        with io.BytesIO() as buf:
            table.write(buf, util.get_settings("wiiu"))
            out.write_bytes(util.compress(buf.getvalue()))

        log = master / "logs" / "rstb.json"
        log.parent.mkdir(parents=True, exist_ok=True)
        log.write_text(json.dumps(diffs, ensure_ascii=False, indent=2, sort_keys=True))