Exemple #1
0
def open_mod(path: Path) -> Path:
    """
    Extracts a provided mod and returns the root path of the graphicpack inside

    :param path: The path to the mod archive.
    :type path: class:`pathlib.Path`
    :returns: The path to the extracted root of the mod where the rules.txt file is found.
    :rtype: class:`pathlib.Path`
    """
    if isinstance(path, str):
        path = Path(path)
    tmpdir = util.get_work_dir() / f'tmp_{xxhash.xxh32(str(path)).hexdigest()}'
    formats = ['.rar', '.zip', '.7z', '.bnp']
    if tmpdir.exists():
        shutil.rmtree(tmpdir, ignore_errors=True)
    if path.suffix.lower() in formats:
        x_args = [
            str(util.get_exec_dir() / 'helpers' / '7z.exe'), 'x',
            str(path), f'-o{str(tmpdir)}'
        ]
        subprocess.run(x_args,
                       stdout=subprocess.PIPE,
                       stderr=subprocess.PIPE,
                       creationflags=util.CREATE_NO_WINDOW)
    else:
        raise Exception(
            'The mod provided was not a supported archive (BNP, ZIP, RAR, or 7z).'
        )
    if not tmpdir.exists():
        raise Exception('No files were extracted.')
    rulesdir = tmpdir
    found_rules = (rulesdir / 'rules.txt').exists()
    if not found_rules:
        for subdir in tmpdir.rglob('*'):
            if (subdir / 'rules.txt').exists():
                rulesdir = subdir
                found_rules = True
                break
        if not found_rules:
            raise FileNotFoundError(
                f'No rules.txt was found in "{path.name}".')
    return rulesdir
Exemple #2
0
def main(debug: bool = False):
    set_start_method("spawn", True)
    global logger  # pylint: disable=invalid-name,global-statement
    logger = None

    try:
        if SYSTEM != "Windows":
            chmod(util.get_exec_dir() / "helpers/msyt", int("755", 8))
            chmod(util.get_exec_dir() / "helpers/7z", int("755", 8))
            os.setpgrp()
        LOG.parent.mkdir(parents=True, exist_ok=True)
        for folder in util.get_work_dir().glob("*"):
            rmtree(folder)
        (util.get_data_dir() / "tmp_settings.json").unlink()
    except (FileNotFoundError, OSError, PermissionError):
        pass

    _oneclick.register_handlers()
    oneclick = Thread(target=_oneclick.listen)
    oneclick.daemon = True
    oneclick.start()

    server_port = util.get_open_port()
    server = Process(target=start_server, args=(server_port,))
    server.daemon = True
    server.start()
    host = f"http://localhost:{server_port}"

    api = Api(host)

    gui = "cef" if SYSTEM == "Windows" else "qt"

    if not debug:
        debug = DEBUG or "bcml-debug" in sys.argv

    if SYSTEM == "Windows":
        configure_cef(debug)

    now = datetime.now()
    if (
        now.month == 4
        and now.day == 1
        and not (util.get_data_dir() / ".fooled").exists()
    ):
        (util.get_data_dir() / ".fooled").write_bytes(b"")
        url = (
            [
                "https://www.youtube.com/embed/Lrj2Hq7xqQ8",
                "https://www.youtube.com/embed/8B1fu3AuDrQ",
                "https://www.youtube.com/embed/jRMHp7_kPec",
                "https://www.youtube.com/embed/N9qYF9DZPdw",
                "https://www.youtube.com/embed/j1FGaCNN1aw",
            ][randint(0, 4)]
        ) + "?autoplay=1"
        width, height = 640, 360
    elif (util.get_data_dir() / "settings.json").exists():
        url = f"{host}/index.html"
        width, height = 907, 680
    else:
        url = f"{host}/index.html?firstrun=yes"
        width, height = 750, 600

    api.window = webview.create_window(
        "BOTW Cross-Platform Mod Loader",
        url=url,
        js_api=api,
        text_select=DEBUG,
        width=width,
        height=height,
        min_size=(width if width == 750 else 820, 600),
    )
    logger = Messager(api.window)
    api.window.closing += stop_it

    messager = Messager(api.window)
    with redirect_stderr(sys.stdout):
        with redirect_stdout(messager):  # type: ignore
            sleep(0.5)
            webview.start(
                gui=gui, debug=debug, http_server=True, func=_oneclick.process_arg
            )
    api.cleanup()
    stop_it(messager=messager)
Exemple #3
0
def main(debug: bool = False):
    set_start_method("spawn", True)
    global logger  # pylint: disable=invalid-name,global-statement
    logger = None

    try:
        if SYSTEM != "Windows":
            chmod(util.get_exec_dir() / "helpers/msyt", int("755", 8))
            chmod(util.get_exec_dir() / "helpers/7z", int("755", 8))
            os.setpgrp()
        LOG.parent.mkdir(parents=True, exist_ok=True)
        for folder in util.get_work_dir().glob("*"):
            rmtree(folder)
    except (FileNotFoundError, OSError, PermissionError):
        pass

    _oneclick.register_handlers()
    oneclick = Thread(target=_oneclick.listen)
    oneclick.daemon = True
    oneclick.start()

    server_port = util.get_open_port()
    server = Process(target=start_server, args=(server_port, ))
    server.daemon = True
    server.start()
    host = f"http://localhost:{server_port}"

    api = Api(host)

    gui = "cef" if SYSTEM == "Windows" else "qt"

    if (util.get_data_dir() / "settings.json").exists():
        url = f"{host}/index.html"
        width, height = 907, 680
    else:
        url = f"{host}/index.html?firstrun=yes"
        width, height = 750, 600

    api.window = webview.create_window(
        "BOTW Cross-Platform Mod Loader",
        url=url,
        js_api=api,
        text_select=DEBUG,
        width=width,
        height=height,
        min_size=(width if width == 750 else 820, 600),
    )
    logger = Messager(api.window)
    api.window.closing += stop_it

    if not debug:
        debug = DEBUG or "bcml-debug" in sys.argv

    messager = Messager(api.window)
    with redirect_stderr(sys.stdout):
        with redirect_stdout(messager):
            sleep(0.5)
            webview.start(gui=gui,
                          debug=debug,
                          http_server=True,
                          func=_oneclick.process_arg)
    stop_it(messager=messager)
Exemple #4
0
def create_bnp_mod(mod: Path, output: Path, options: dict = None):
    """[summary]
    
    :param mod: [description]
    :type mod: Path
    :param output: [description]
    :type output: Path
    :param options: [description], defaults to {}
    :type options: dict, optional
    """
    if isinstance(mod, str):
        mod = Path(mod)
    if mod.is_file():
        print('Extracting mod...')
        tmp_dir: Path = open_mod(mod)
    elif mod.is_dir():
        print(f'Loading mod from {str(mod)}...')
        tmp_dir: Path = util.get_work_dir() / \
            f'tmp_{xxhash.xxh32(str(mod)).hexdigest()}'
        shutil.copytree(str(mod), str(tmp_dir))
    else:
        print(f'Error: {str(mod)} is neither a valid file nor a directory')
        return

    print('Packing loose files...')
    pack_folders = sorted(
        {
            d
            for d in tmp_dir.rglob('**/*')
            if d.is_dir() and d.suffix in util.SARC_EXTS
        },
        key=lambda d: len(d.parts),
        reverse=True)
    for folder in pack_folders:
        new_tmp: Path = folder.with_suffix(folder.suffix + '.tmp')
        shutil.move(folder, new_tmp)
        new_sarc = sarc.SARCWriter(be=True)
        for file in {f for f in new_tmp.rglob('**/*') if f.is_file()}:
            new_sarc.add_file(
                file.relative_to(new_tmp).as_posix(), file.read_bytes())
        sarc_bytes = new_sarc.get_bytes()
        if str(folder.suffix).startswith('.s') and folder.suffix != '.sarc':
            sarc_bytes = util.compress(sarc_bytes)
        folder.write_bytes(sarc_bytes)
        shutil.rmtree(new_tmp)

    if not options:
        options = {}
    options['texts'] = {'user_only': False}
    pool = Pool(cpu_count())
    logged_files = generate_logs(tmp_dir, options=options, original_pool=pool)

    print('Removing unnecessary files...')
    if (tmp_dir / 'logs' / 'map.yml').exists():
        print('Removing map units...')
        for file in [file for file in logged_files if isinstance(file, Path) and \
                           fnmatch(file.name, '[A-Z]-[0-9]_*.smubin')]:
            file.unlink()
    if [file for file in (tmp_dir / 'logs').glob('*texts*')]:
        print('Removing language bootup packs...')
        for bootup_lang in (tmp_dir / 'content' /
                            'Pack').glob('Bootup_*.pack'):
            bootup_lang.unlink()
    if (tmp_dir / 'logs' / 'actorinfo.yml').exists() and \
       (tmp_dir / 'content' / 'Actor' / 'ActorInfo.product.sbyml').exists():
        print('Removing ActorInfo.product.sbyml...')
        (tmp_dir / 'content' / 'Actor' / 'ActorInfo.product.sbyml').unlink()
    if (tmp_dir / 'logs' / 'gamedata.yml').exists() or (
            tmp_dir / 'logs' / 'savedata.yml').exists():
        print('Removing gamedata sarcs...')
        with (tmp_dir / 'content' / 'Pack' /
              'Bootup.pack').open('rb') as b_file:
            bsarc = sarc.read_file_and_make_sarc(b_file)
        csarc = sarc.make_writer_from_sarc(bsarc)
        bsarc_files = list(bsarc.list_files())
        if 'GameData/gamedata.ssarc' in bsarc_files:
            csarc.delete_file('GameData/gamedata.ssarc')
        if 'GameData/savedataformat.ssarc' in bsarc_files:
            csarc.delete_file('GameData/savedataformat.ssarc')
        with (tmp_dir / 'content' / 'Pack' /
              'Bootup.pack').open('wb') as b_file:
            csarc.write(b_file)

    hashes = util.get_hash_table()
    print('Creating partial packs...')
    sarc_files = {
        file
        for file in tmp_dir.rglob('**/*') if file.suffix in util.SARC_EXTS
    }
    if sarc_files:
        pool.map(partial(_clean_sarc, hashes=hashes, tmp_dir=tmp_dir),
                 sarc_files)
        pool.close()
        pool.join()

        sarc_files = {
            file
            for file in tmp_dir.rglob('**/*') if file.suffix in util.SARC_EXTS
        }
        if sarc_files:
            with (tmp_dir / 'logs' / 'packs.log').open(
                    'w', encoding='utf-8') as p_file:
                final_packs = [
                    file for file in list(tmp_dir.rglob('**/*'))
                    if file.suffix in util.SARC_EXTS
                ]
                if final_packs:
                    p_file.write('name,path\n')
                    for file in final_packs:
                        p_file.write(
                            f'{util.get_canon_name(file.relative_to(tmp_dir))},'
                            f'{file.relative_to(tmp_dir)}\n')
    else:
        if (tmp_dir / 'logs' / 'packs.log').exists():
            (tmp_dir / 'logs' / 'packs.log').unlink()

    print('Cleaning any junk files...')
    for file in tmp_dir.rglob('**/*'):
        if file.parent.stem == 'logs':
            continue
        if file.suffix in ['.yml', '.bak', '.tmp', '.old']:
            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 = [
        str(util.get_exec_dir() / 'helpers' / '7z.exe'), 'a',
        str(output), f'{str(tmp_dir / "*")}'
    ]
    subprocess.run(x_args,
                   stdout=subprocess.PIPE,
                   stderr=subprocess.PIPE,
                   creationflags=util.CREATE_NO_WINDOW)
    print('Conversion complete.')
Exemple #5
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