Example #1
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.")
Example #2
0
def uninstall_mod(mod: BcmlMod, wait_merge: bool = False):
    print(f"Uninstalling {mod.name}...")
    try:
        shutil.rmtree(str(mod.path))
    except (OSError, PermissionError) 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()

    print(f"{mod.name} has been uninstalled.")
Example #3
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
Example #4
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
Example #5
0
def link_master_mod(output: Path = None):
    util.create_bcml_graphicpack_if_needed()
    try:
        rsext.manager.link_master_mod(str(output) if output else None)
    except (OSError, RuntimeError) as err:
        if err is OSError or (err is RuntimeError and "junction" in str(err)):
            raise RuntimeError(
                "BCML failed to create the link to the merged mod. This is "
                "probably because Cemu is on an external USB, eSATA, or "
                "network drive. You can fix this in one of two ways:\n"
                "- Make sure Cemu is on an internal drive\n"
                "- Turn on the 'no hard links' option in BCML's settings "
                "(but be aware that mod merging will take much longer)"
            ) from err
        else:
            raise
Example #6
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