Ejemplo n.º 1
0
def generate_logs(tmp_dir: Path,
                  verbose: bool = False,
                  options: dict = None,
                  original_pool: Pool = None) -> List[Path]:
    """Analyzes a mod and generates BCML log files containing its changes"""
    if isinstance(tmp_dir, str):
        tmp_dir = Path(tmp_dir)
    if not options:
        options = {'disable': [], 'options': {}}
    if 'disable' not in options:
        options['disable'] = []

    pool = original_pool or Pool(cpu_count())
    print('Scanning for modified files...')
    modded_files = find_modded_files(tmp_dir,
                                     verbose=verbose,
                                     original_pool=original_pool)
    if not modded_files:
        raise RuntimeError('No modified files were found. Very unusual.')

    (tmp_dir / 'logs').mkdir(parents=True, exist_ok=True)
    for merger_class in [merger_class for merger_class in mergers.get_mergers() \
                        if merger_class.NAME not in options['disable']]:
        merger = merger_class()
        merger.set_pool(pool)
        if options is not None and merger.NAME in options:
            merger.set_options(options[merger.NAME])
        merger.log_diff(tmp_dir, modded_files)
    if not original_pool:
        pool.close()
        pool.join()
    return modded_files
Ejemplo n.º 2
0
def merge_sarcs(file_name: str, sarcs: List[Union[Path, bytes]]) -> (str, bytes):
    opened_sarcs: List[sarc.SARC] = []
    if isinstance(sarcs[0], Path):
        for i, sarc_path in enumerate(sarcs):
            sarcs[i] = sarc_path.read_bytes()
    for sarc_bytes in sarcs:
        sarc_bytes = util.unyaz_if_needed(sarc_bytes)
        try:
            opened_sarcs.append(sarc.SARC(sarc_bytes))
        except ValueError:
            continue

    all_files = {key for open_sarc in opened_sarcs for key in open_sarc.list_files()}
    nested_sarcs = {}
    new_sarc = sarc.SARCWriter(be=True)
    files_added = []

    # for file in all_files:
    #     dm_cache = util.get_master_modpack_dir() / 'logs' / 'dm' / file
    #     if dm_cache.exists():
    #         file_data = dm_cache.read_bytes()
    #         new_sarc.add_file(file, file_data)
    #         files_added.append(file)

    for opened_sarc in reversed(opened_sarcs):
        for file in [file for file in opened_sarc.list_files() if file not in files_added]:
            data = opened_sarc.get_file_data(file).tobytes()
            if util.is_file_modded(file.replace('.s', '.'), data, count_new=True):
                if not Path(file).suffix in util.SARC_EXTS:
                    new_sarc.add_file(file, data)
                    files_added.append(file)
                else:
                    if file not in nested_sarcs:
                        nested_sarcs[file] = []
                    nested_sarcs[file].append(util.unyaz_if_needed(data))
    for file, sarcs in nested_sarcs.items():
        merged_bytes = merge_sarcs(file, sarcs)[1]
        if Path(file).suffix.startswith('.s') and not file.endswith('.sarc'):
            merged_bytes = util.compress(merged_bytes)
        new_sarc.add_file(file, merged_bytes)
        files_added.append(file)
    for file in [file for file in all_files if file not in files_added]:
        for opened_sarc in [open_sarc for open_sarc in opened_sarcs \
                            if file in open_sarc.list_files()]:
            new_sarc.add_file(file, opened_sarc.get_file_data(file).tobytes())
            break

    if 'Bootup.pack' in file_name:
        for merger in [merger() for merger in mergers.get_mergers() if merger.is_bootup_injector()]:
            inject = merger.get_bootup_injection()
            if not inject:
                continue
            file, data = inject
            try:
                new_sarc.delete_file(file)
            except KeyError:
                pass
            new_sarc.add_file(file, data)

    return (file_name, new_sarc.get_bytes())
Ejemplo n.º 3
0
 def get_mod_edits(self, params=None):
     mod = BcmlMod.from_json(params["mod"])
     edits = {}
     merger_list = sorted({m() for m in mergers.get_mergers()}, key=lambda m: m.NAME)
     for merger in merger_list:
         edits[merger.friendly_name] = merger.get_mod_edit_info(mod)
     return {key: sorted({str(v) for v in value}) for key, value in edits.items()}
Ejemplo n.º 4
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
Ejemplo n.º 5
0
def refresh_merges():
    print("Cleansing old merges...")
    shutil.rmtree(util.get_master_modpack_dir(), True)
    print("Refreshing merged mods...")
    with Pool(maxtasksperchild=500) as pool:
        for merger in mergers.sort_mergers(
            [merger_class() for merger_class in mergers.get_mergers()]):
            merger.set_pool(pool)
            merger.perform_merge()
Ejemplo n.º 6
0
 def get_options(self):
     opts = []
     for merger in mergers.get_mergers():
         merger = merger()
         opts.append({
             "name": merger.NAME,
             "friendly": merger.friendly_name,
             "options": dict(merger.get_checkbox_options()),
         })
     return opts
Ejemplo n.º 7
0
def disable_mod(mod: BcmlMod, wait_merge: bool = False):
    remergers = []
    print(f"Disabling {mod.name}...")
    for merger in [merger() for merger in mergers.get_mergers()]:
        if merger.is_mod_logged(mod):
            remergers.append(merger)
    (mod.path / ".disabled").write_bytes(b"")
    if not wait_merge:
        print("Remerging...")
        refresh_merges()
    print(f"{mod.name} disabled")
Ejemplo n.º 8
0
def refresh_merges(verbose: bool = False):
    """
    Runs RSTB, pack, and text merges together

    :param verbose: Whether to display more detailed output, defaults to False.
    :type verbose: bool, optional
    """
    print('Cleansing old merges...')
    shutil.rmtree(util.get_master_modpack_dir())
    print('Refreshing merged mods...')
    for merger in mergers.sort_mergers(
        [merger_class() for merger_class in mergers.get_mergers()]):
        merger.perform_merge()
Ejemplo n.º 9
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",
         )
Ejemplo n.º 10
0
 def get_options(self):
     opts = [{
         "name": "general",
         "friendly": "general options",
         "options": {
             "base_priority": "Default to lowest priority"
         },
     }]
     for merger in mergers.get_mergers():
         merger = merger()
         opts.append({
             "name": merger.NAME,
             "friendly": merger.friendly_name,
             "options": dict(merger.get_checkbox_options()),
         })
     return opts
Ejemplo n.º 11
0
def uninstall_mod(mod: Union[Path, BcmlMod, str],
                  wait_merge: bool = False,
                  verbose: bool = False):
    """
    Uninstalls the mod currently installed at the specified path and updates merges as needed

    :param mod: The mod to remove, as a path or a BcmlMod.
    :param wait_merge: Resort mods but don't remerge anything yet, defaults to False.
    :type wait_merge: bool, optional
    :param verbose: Whether to display more detailed output, defaults to False.
    :type verbose: bool, optional
    """
    path = Path(mod) if isinstance(
        mod, str) else mod.path if isinstance(mod, BcmlMod) else mod
    mod_name, mod_priority, _ = util.get_mod_info(path / 'rules.txt') \
                                if not isinstance(mod, BcmlMod) else mod
    print(f'Uninstalling {mod_name}...')
    remergers = set()
    partials = {}
    for merger in [merger() for merger in mergers.get_mergers()]:
        if merger.is_mod_logged(BcmlMod(mod_name, mod_priority, path)):
            remergers.add(merger)
            if merger.can_partial_remerge():
                partials[merger.NAME] = merger.get_mod_affected(mod)

    shutil.rmtree(str(path))
    next_mod = util.get_mod_by_priority(mod_priority + 1)
    if next_mod:
        print('Adjusting mod priorities...')
        change_mod_priority(next_mod,
                            mod_priority,
                            wait_merge=True,
                            verbose=verbose)
        print()

    if not wait_merge:
        pool = Pool(cpu_count())
        for merger in mergers.sort_mergers(remergers):
            merger.set_pool(pool)
            if merger.NAME in partials:
                merger.set_options({'only_these': partials[merger.NAME]})
            merger.perform_merge()
        pool.close()
        pool.join()
    print(f'{mod_name} has been uninstalled.')
Ejemplo n.º 12
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:
        raise RuntimeError(
            f"No modified files were found in {str(tmp_dir)}."
            "This probably means this mod is not in a supported format.")

    (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)
    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
Ejemplo n.º 13
0
 def remerge(self, params):
     try:
         if not util.get_installed_mods():
             if util.get_master_modpack_dir().exists():
                 rmtree(util.get_master_modpack_dir())
                 install.link_master_mod()
             return
         if params["name"] == "all":
             install.refresh_merges()
         else:
             [
                 m() for m in mergers.get_mergers()
                 if m().friendly_name == params["name"]
             ][0].perform_merge()
     except Exception as err:  # pylint: disable=broad-except
         raise Exception(
             f"There was an error merging your mods. {str(err)}\n"
             "Note that this could leave your game in an unplayable state.")
Ejemplo n.º 14
0
def disable_mod(mod: BcmlMod, wait_merge: bool = False):
    remergers = []
    partials = {}
    print(f'Disabling {mod.name}...')
    for merger in [merger() for merger in mergers.get_mergers()]:
        if merger.is_mod_logged(mod):
            remergers.append(merger)
            if merger.can_partial_remerge():
                partials[merger.NAME] = merger.get_mod_affected(mod)
    rules_path: Path = mod.path / 'rules.txt'
    rules_path.rename(rules_path.with_suffix('.txt.disable'))
    if not wait_merge:
        print(f'Remerging affected files...')
        for merger in remergers:
            if merger.NAME in partials:
                merger.set_options({'only_these': partials[merger.NAME]})
            merger.perform_merge()
    print(f'{mod.name} disabled')
Ejemplo n.º 15
0
 def get_mod_info(self, params):
     mod = BcmlMod.from_json(params["mod"])
     util.vprint(mod)
     img = ""
     try:
         img = base64.b64encode(mod.get_preview().read_bytes()).decode("utf8")
     except:  # pylint: disable=bare-except
         pass
     return {
         "changes": [
             m.NAME.upper() for m in mergers.get_mergers() if m().is_mod_logged(mod)
         ],
         "desc": mod.description,
         "date": mod.date,
         "processed": (mod.path / ".processed").exists(),
         "image": img,
         "url": mod.url,
     }
Ejemplo n.º 16
0
    def setupUi(self, OptionsDialog):
        OptionsDialog.setObjectName("OptionsDialog")
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum,
                                           QtWidgets.QSizePolicy.Minimum)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            OptionsDialog.sizePolicy().hasHeightForWidth())
        OptionsDialog.setSizePolicy(sizePolicy)
        self.verticalLayout = QtWidgets.QVBoxLayout(OptionsDialog)
        self.verticalLayout.setObjectName("verticalLayout")
        self.label = QtWidgets.QLabel(OptionsDialog)
        self.label.setObjectName("label")
        self.verticalLayout.addWidget(self.label)
        self.checkboxes = []
        for merger_class in mergers.get_mergers():
            merger = merger_class()
            chkMerger = QtWidgets.QCheckBox(OptionsDialog)
            chkMerger.setObjectName('chkDisable' + merger.NAME)
            chkMerger.setText('Disable ' + merger.friendly_name())
            setattr(chkMerger, 'disable_name', merger.NAME)
            self.verticalLayout.addWidget(chkMerger)
            self.checkboxes.append(chkMerger)
            for option in merger.get_checkbox_options():
                chkOption = QtWidgets.QCheckBox(OptionsDialog)
                chkOption.setObjectName('chk' + merger.NAME + option[0])
                setattr(chkOption, 'option_name', option[0])
                setattr(chkOption, 'merger', merger.NAME)
                chkOption.setText(option[1])
                self.verticalLayout.addWidget(chkOption)
                self.checkboxes.append(chkOption)
        self.buttonBox = QtWidgets.QDialogButtonBox(OptionsDialog)
        self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
        self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel
                                          | QtWidgets.QDialogButtonBox.Ok)
        self.buttonBox.setObjectName("buttonBox")
        self.verticalLayout.addWidget(self.buttonBox)

        self.retranslateUi(OptionsDialog)
        QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("accepted()"),
                               OptionsDialog.accept)
        QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("rejected()"),
                               OptionsDialog.reject)
        QtCore.QMetaObject.connectSlotsByName(OptionsDialog)
Ejemplo n.º 17
0
def merge_sarcs(file_name: str, sarcs: List[Union[Path,
                                                  bytes]]) -> (str, bytes):
    opened_sarcs: List[oead.Sarc] = []
    if "ThunderRodLv2" in file_name:
        print()
    if isinstance(sarcs[0], Path):
        for i, sarc_path in enumerate(sarcs):
            sarcs[i] = sarc_path.read_bytes()
    for sarc_bytes in sarcs:
        sarc_bytes = util.unyaz_if_needed(sarc_bytes)
        try:
            opened_sarcs.append(oead.Sarc(sarc_bytes))
        except (ValueError, RuntimeError, oead.InvalidDataError):
            continue

    all_files = {
        file.name
        for open_sarc in opened_sarcs for file in open_sarc.get_files()
    }
    nested_sarcs = {}
    new_sarc = oead.SarcWriter(endian=oead.Endianness.Big if util.get_settings(
        "wiiu") else oead.Endianness.Little)
    files_added = set()

    for opened_sarc in reversed(opened_sarcs):
        for file in [
                f for f in opened_sarc.get_files() if f.name not in files_added
        ]:
            file_data = oead.Bytes(file.data)
            if (file.name[file.name.rindex("."):] in util.SARC_EXTS -
                    EXCLUDE_EXTS) and file.name not in SPECIAL:
                if file.name not in nested_sarcs:
                    nested_sarcs[file.name] = []
                nested_sarcs[file.name].append(util.unyaz_if_needed(file_data))
            elif util.is_file_modded(file.name.replace(".s", "."),
                                     file_data,
                                     count_new=True):
                new_sarc.files[file.name] = file_data
                files_added.add(file.name)
    util.vprint(set(nested_sarcs.keys()))
    for file, sarcs in nested_sarcs.items():
        if not sarcs:
            continue
        merged_bytes = merge_sarcs(file, sarcs[::-1])[1]
        if Path(file).suffix.startswith(".s") and not file.endswith(".sarc"):
            merged_bytes = util.compress(merged_bytes)
        new_sarc.files[file] = merged_bytes
        files_added.add(file)
    for file in [file for file in all_files if file not in files_added]:
        for opened_sarc in [
                open_sarc for open_sarc in opened_sarcs
                if (file in [f.name for f in open_sarc.get_files()])
        ]:
            new_sarc.files[file] = oead.Bytes(opened_sarc.get_file(file).data)
            break

    if "Bootup.pack" in file_name:
        for merger in [
                merger() for merger in mergers.get_mergers()
                if merger.is_bootup_injector()
        ]:
            inject = merger.get_bootup_injection()
            if not inject:
                continue
            file, data = inject
            new_sarc.files[file] = data

    return (file_name, bytes(new_sarc.write()[1]))
Ejemplo n.º 18
0
def install_mod(mod: Path,
                verbose: bool = False,
                options: dict = None,
                wait_merge: bool = False,
                insert_priority: int = 0):
    """
    Installs a graphic pack mod, merging RSTB changes and optionally packs and texts

    :param mod: Path to the mod to install. Must be a RAR, 7z, or ZIP archive or a graphicpack
    directory containing a rules.txt file.
    :type mod: class:`pathlib.Path`
    :param verbose: Whether to display more detailed output, defaults to False.
    :type verbose: bool, optional
    :param wait_merge: Install mod and log changes, but wait to run merge manually,
    defaults to False.
    :type wait_merge: bool, optional
    :param insert_priority: Insert mod(s) at priority specified, defaults to get_next_priority().
    :type insert_priority: int
    """
    if insert_priority == 0:
        insert_priority = get_next_priority()
    util.create_bcml_graphicpack_if_needed()
    if isinstance(mod, str):
        mod = Path(mod)
    if mod.is_file():
        print('Extracting mod...')
        tmp_dir = open_mod(mod)
    elif mod.is_dir():
        if (mod / 'rules.txt').exists():
            print(f'Loading mod from {str(mod)}...')
            tmp_dir = util.get_work_dir() / f'tmp_{mod.name}'
            shutil.copytree(str(mod), str(tmp_dir))
        else:
            print(f'Cannot open mod at {str(mod)}, no rules.txt found')
            return
    else:
        print(f'Error: {str(mod)} is neither a valid file nor a directory')
        return

    pool: Pool = None
    try:
        rules = util.RulesParser()
        rules.read(tmp_dir / 'rules.txt')
        mod_name = str(rules['Definition']['name']).strip(' "\'')
        print(f'Identified mod: {mod_name}')

        logs = tmp_dir / 'logs'
        if logs.exists():
            print('This mod supports Quick Install! Loading changes...')
            for merger in [merger() for merger in mergers.get_mergers() \
                           if merger.NAME in options['disable']]:
                if merger.is_mod_logged(BcmlMod('', 0, tmp_dir)):
                    (tmp_dir / 'logs' / merger.log_name()).unlink()
        else:
            pool = Pool(cpu_count())
            generate_logs(tmp_dir=tmp_dir,
                          verbose=verbose,
                          options=options,
                          original_pool=pool)
    except Exception as e:  # pylint: disable=broad-except
        if hasattr(e, 'error_text'):
            raise e
        clean_error = RuntimeError()
        try:
            name = mod_name
        except NameError:
            name = 'your mod, the name of which could not be detected'
        clean_error.error_text = (
            f'There was an error while processing {name}. '
            'This could indicate there is a problem with the mod itself, '
            'but it could also reflect a new or unusual edge case BCML does '
            'not anticipate. Here is the error:\n\n'
            f'{traceback.format_exc(limit=-4)}')
        raise clean_error

    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:
        for existing_mod in util.get_installed_mods():
            if existing_mod.priority >= priority:
                priority_shifted = existing_mod.priority + 1
                new_id = util.get_mod_id(existing_mod.name, priority_shifted)
                new_path = util.get_modpack_dir() / new_id
                shutil.move(str(existing_mod.path), str(new_path))
                existing_mod_rules = util.RulesParser()
                existing_mod_rules.read(str(new_path / 'rules.txt'))
                existing_mod_rules['Definition']['fsPriority'] = str(
                    priority_shifted)
                with (new_path / 'rules.txt').open('w',
                                                   encoding='utf-8') as r_file:
                    existing_mod_rules.write(r_file)

        mod_dir.parent.mkdir(parents=True, exist_ok=True)
        print()
        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.copytree(str(tmp_dir), str(mod_dir))
                    try:
                        shutil.rmtree(str(tmp_dir))
                    except Exception:  # pylint: disable=broad-except
                        pass
                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))

        rulepath = os.path.basename(rules['Definition']['path']).replace(
            '"', '')
        rules['Definition']['path'] = f'{{BCML: DON\'T TOUCH}}/{rulepath}'
        rules['Definition']['fsPriority'] = str(priority)
        with Path(mod_dir / 'rules.txt').open('w', encoding='utf-8') as r_file:
            rules.write(r_file)

        output_mod = BcmlMod(mod_name, priority, mod_dir)
        try:
            util.get_mod_link_meta(rules)
            util.get_mod_preview(output_mod, rules)
        except Exception:  # pylint: disable=broad-except
            pass

        print(f'Enabling {mod_name} in Cemu...')
        refresh_cemu_mods()
    except Exception:  # pylint: disable=broad-except
        clean_error = RuntimeError()
        clean_error.error_text = (
            f'There was an error installing {mod_name}. '
            'It processed successfully, but could not be added to your BCML '
            'mods. This may indicate a problem with your BCML installation. '
            'Here is the error:\n\n'
            f'{traceback.format_exc(limit=-4)}\n\n'
            f'{mod_name} is being removed and no changes will be made.')
        if mod_dir.exists():
            try:
                uninstall_mod(mod_dir, wait_merge=True)
            except Exception:  # pylint: disable=broad-except
                shutil.rmtree(str(mod_dir))
        raise clean_error

    if wait_merge:
        print('Mod installed, merge still pending...')
    else:
        try:
            if not pool:
                pool = Pool(cpu_count)
            print('Performing merges...')
            if not options:
                options = {}
            if 'disable' not in options:
                options['disable'] = []
            for merger in mergers.sort_mergers([cls() for cls in mergers.get_mergers() \
                                                if cls.NAME not in options['disable']]):
                merger.set_pool(pool)
                if merger.NAME in options:
                    merger.set_options(options[merger.NAME])
                if merger.is_mod_logged(output_mod):
                    merger.perform_merge()
            print()
            print(f'{mod_name} installed successfully!')
            pool.close()
            pool.join()
        except Exception:  # pylint: disable=broad-except
            clean_error = RuntimeError()
            clean_error.error_text = (
                f'There was an error merging {mod_name}. '
                'It processed and installed without error, but it has not '
                'successfully merged with your other mods. '
                'Here is the error:\n\n'
                f'{traceback.format_exc(limit=-4)}\n\n'
                f'To protect your mod setup, BCML will remove {mod_name} '
                'and remerge.')
            try:
                uninstall_mod(mod_dir)
            except FileNotFoundError:
                pass
            raise clean_error
    return output_mod
Ejemplo n.º 19
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
Ejemplo n.º 20
0
 def get_setup(self):
     return {
         "hasCemu": not util.get_settings("no_cemu"),
         "mergers": [m().friendly_name for m in mergers.get_mergers()],
     }
Ejemplo n.º 21
0
def change_mod_priority(path: Path,
                        new_priority: int,
                        wait_merge: bool = False,
                        verbose: bool = False):
    """
    Changes the priority of a mod

    :param path: The path to the mod.
    :type path: class:`pathlib.Path`
    :param new_priority: The new priority of the mod.
    :type new_priority: int
    :param wait_merge: Resort priorities but don't remerge anything yet, defaults to False.
    :type wait_merge: bool, optional
    :param verbose: Whether to display more detailed output, defaults to False.
    :type verbose: bool, optional
    """
    mod = util.get_mod_info(path / 'rules.txt')
    print(
        f'Changing priority of {mod.name} from {mod.priority} to {new_priority}...'
    )
    mods = util.get_installed_mods()
    if new_priority > mods[len(mods) - 1][1]:
        new_priority = len(mods) - 1
    mods.remove(mod)
    mods.insert(new_priority - 100, util.BcmlMod(mod.name, new_priority, path))

    all_mergers = [merger() for merger in mergers.get_mergers()]
    remergers = set()
    partials = {}
    for merger in all_mergers:
        if merger.is_mod_logged(mod):
            remergers.add(merger)
            if merger.can_partial_remerge():
                partials[merger.NAME] = set(merger.get_mod_affected(mod))

    print('Resorting other affected mods...')
    for mod in mods:
        if mod.priority != (mods.index(mod) + 100):
            adjusted_priority = mods.index(mod) + 100
            mods.remove(mod)
            mods.insert(adjusted_priority - 100,
                        BcmlMod(mod.name, adjusted_priority, mod.path))
            if verbose:
                print(f'Changing priority of {mod.name} from'
                      f'{mod.priority} to {adjusted_priority}...')
    for mod in mods:
        if not mod.path.stem.startswith(f'{mod.priority:04}'):
            for merger in all_mergers:
                if merger.is_mod_logged(mod):
                    remergers.add(merger)
                    if merger.can_partial_remerge():
                        if merger.NAME not in partials:
                            partials[merger.NAME] = set()
                        partials[merger.NAME] |= set(
                            merger.get_mod_affected(mod))

            new_mod_id = util.get_mod_id(mod[0], mod[1])
            shutil.move(str(mod[2]), str(mod[2].parent / new_mod_id))
            rules = util.RulesParser()
            rules.read(str(mod.path.parent / new_mod_id / 'rules.txt'))
            rules['Definition']['fsPriority'] = str(mod[1])
            with (mod[2].parent / new_mod_id / 'rules.txt').open(
                    'w', encoding='utf-8') as r_file:
                rules.write(r_file)
            refresh_cemu_mods()
    if not wait_merge:
        for merger in mergers.sort_mergers(remergers):
            if merger.NAME in partials:
                merger.set_options({'only_these': partials[merger.NAME]})
            merger.perform_merge()
    if wait_merge:
        print('Mods resorted, will need to remerge later')
    print('Finished updating mod priorities.')