Exemple #1
0
def bootup_from_msbts(
    lang: str = 'USen',
    msbt_dir: Path = util.get_work_dir() / 'tmp_text' / 'merged'
) -> (Path, int):
    """
    Generates a new Bootup_XXxx.pack from a directory of MSBT files

    :param lang: The game language to use, defaults to USen.
    :type lang: str, optional
    :param msbt_dir: The directory to pull MSBTs from, defaults to "tmp_text/merged" in BCML's
    working directory.
    :type msbt_dir: class:`pathlib.Path`, optional
    :returns: A tuple with the path to the new Bootup_XXxx.pack and the RSTB size of the new
    Msg_XXxx.product.sarc
    :rtype: (class:`pathlib.Path`, int)
    """
    new_boot_path = msbt_dir.parent / f'Bootup_{lang}.pack'
    with new_boot_path.open('wb') as new_boot:
        s_msg = sarc.SARCWriter(True)
        for new_msbt in msbt_dir.rglob('**/*.msbt'):
            with new_msbt.open('rb') as f_new:
                s_msg.add_file(
                    str(new_msbt.relative_to(msbt_dir)).replace('\\', '/'),
                    f_new.read())
        new_msg_stream = io.BytesIO()
        s_msg.write(new_msg_stream)
        unyaz_bytes = new_msg_stream.getvalue()
        rsize = rstb.SizeCalculator().calculate_file_size_with_ext(
            unyaz_bytes, True, '.sarc')
        new_msg_bytes = util.compress(unyaz_bytes)
        s_boot = sarc.SARCWriter(True)
        s_boot.add_file(f'Message/Msg_{lang}.product.ssarc', new_msg_bytes)
        s_boot.write(new_boot)
    return new_boot_path, rsize
Exemple #2
0
def get_modded_texts(modded_msyts: list, lang: str = 'USen', tmp_dir: Path = \
                     util.get_work_dir() / 'tmp_text') -> dict:
    """
    Builds a dictionary of all edited text entries in modded MSYTs

    :param modded_msyts: A list of MSYT files that have been modified.
    :type modded_msyts: list of str
    :param tmp_dir: The temp directory to use, defaults to "tmp_text" in BCML's working directory.
    :type tmp_dir: class:`pathlib.Path`
    :returns: Returns a dictionary of modified MSYT text entries.
    :rtype: dict
    """
    text_edits = {}
    check_msyts = [
        msyt for msyt in list(tmp_dir.rglob('**/*.msyt'))
        if str(msyt.relative_to(tmp_dir)).replace('\\', '/') in modded_msyts
    ]
    if not check_msyts:
        return {}
    num_threads = min(multiprocessing.cpu_count(), len(check_msyts))
    thread_checker = partial(threaded_compare_texts,
                             tmp_dir=tmp_dir,
                             lang=lang)
    pool = multiprocessing.Pool(processes=num_threads)
    edit_results = pool.map(thread_checker, check_msyts)
    pool.close()
    pool.join()
    for edit in edit_results:
        rel_path, edits = edit
        if edits is None:
            print(f'{rel_path} is corrupt and will not be merged.')
            continue
        if edits['entries']:
            text_edits[rel_path] = edits
    return text_edits
Exemple #3
0
def msbt_to_msyt(tmp_dir: Path = util.get_work_dir() / 'tmp_text'):
    """ Converts MSBTs in given temp dir to MSYTs """
    subprocess.run([
        str(util.get_exec_dir() / 'helpers' / 'msyt.exe'), 'export', '-d',
        str(tmp_dir)
    ],
                   creationflags=util.CREATE_NO_WINDOW)
    fix_msbts = [
        msbt for msbt in tmp_dir.rglob('**/*.msbt')
        if not msbt.with_suffix('.msyt').exists()
    ]
    if fix_msbts:
        print('Some MSBTs failed to convert. Trying again individually...')
        pool = multiprocessing.Pool(
            processes=min(multiprocessing.cpu_count(), len(fix_msbts)))
        pool.map(_msyt_file, fix_msbts)
        pool.close()
        pool.join()
        fix_msbts = [
            msbt for msbt in tmp_dir.rglob('**/*.msbt')
            if not msbt.with_suffix('.msyt').exists()
        ]
    if fix_msbts:
        print(
            f'{len(fix_msbts)} MSBT files failed to convert. They will not be merged.'
        )
    for msbt_file in tmp_dir.rglob('**/*.msbt'):
        Path(msbt_file).unlink()
    return fix_msbts
Exemple #4
0
def extract_ref_msyts(lang: str = 'USen',
                      for_merge: bool = False,
                      tmp_dir: Path = util.get_work_dir() / 'tmp_text'):
    """
    Extracts the reference MSYT texts for the given language to a temp dir

    :param lang: The game language to use, defaults to USen.
    :type lang: str, optional
    :param for_merge: Whether the output is to be merged (or as reference), defaults to False
    :type for_merge: bool
    :param tmp_dir: The temp directory to extract to, defaults to "tmp_text" in BCML's working
    directory.
    :type tmp_dir: class:`pathlib.Path`, optional
    """
    if tmp_dir.exists():
        shutil.rmtree(tmp_dir, ignore_errors=True)

    with util.get_game_file(f'Pack/Bootup_{lang}.pack').open('rb') as b_file:
        bootup_pack = sarc.read_file_and_make_sarc(b_file)
    msg_bytes = util.decompress(
        bootup_pack.get_file_data(
            f'Message/Msg_{lang}.product.ssarc').tobytes())
    msg_pack = sarc.SARC(msg_bytes)
    if not for_merge:
        merge_dir = tmp_dir / 'ref'
    else:
        merge_dir = tmp_dir / 'merged'
    msg_pack.extract_to_dir(str(merge_dir))
    msbt_to_msyt(merge_dir)
Exemple #5
0
def get_modded_msyts(msg_sarc: sarc.SARC, lang: str = 'USen',
                     tmp_dir: Path = util.get_work_dir() / 'tmp_text') -> (list, dict):
    """
    Gets a list of modified game text files in a given message SARC

    :param msg_sarc: The message SARC to scan for changes.
    :type msg_sarc: class:`sarc.SARC`
    :param lang: The game language to use, defaults to USen.
    :type lang: str, optional
    :param tmp_dir: The temp directory to use, defaults to "tmp_text" in BCML's working directory.
    :type tmp_dir: class:`pathlib.Path`, optional
    :returns: Returns a tuple containing a list of modded text files and a dict of new text
    files with their contents.
    :rtype: (list of str, dict of str: bytes)
    """
    hashes = get_msbt_hashes(lang)
    modded_msyts = []
    added_msbts = {}
    write_msbts = []
    for msbt in msg_sarc.list_files():
        if any(exclusion in msbt for exclusion in EXCLUDE_TEXTS):
            continue
        m_data = msg_sarc.get_file_data(msbt)
        m_hash = xxhash.xxh32(m_data).hexdigest()
        if msbt not in hashes:
            added_msbts[msbt] = m_data
        elif m_hash != hashes[msbt]:
            write_msbts.append((tmp_dir / msbt, m_data.tobytes()))
            modded_msyts.append(msbt.replace('.msbt', '.msyt'))
    if write_msbts:
        pool = multiprocessing.Pool()
        pool.map(write_msbt, write_msbts)
        pool.close()
        pool.join()
    return modded_msyts, added_msbts
Exemple #6
0
def get_text_mods_from_bootup(bootup_path: Union[Path, str],
                              tmp_dir: Path = util.get_work_dir() / 'tmp_text',
                              verbose: bool = False, lang: str = ''):
    """
    Detects modifications to text files inside a given Bootup_XXxx.pack

    :param bootup_path: Path to the Bootup_XXxx.pack file.
    :type bootup_path: class:`pathlib.Path`
    :param tmp_dir: The temp directory to use, defaults to "tmp_text" in BCML's working directory.
    :type tmp_dir: class:`pathlib.Path`
    :param verbose: Whether to display more detailed output, defaults to False.
    :type verbose: bool, optional
    :returns: Return a tuple containing a dict of modded text entries, a SARC containing added text
    MSBTs, and the game language of the bootup pack.
    :rtype: (dict, class:`sarc.SARCWriter`, str)
    """
    if not lang:
        lang = util.get_file_language(bootup_path)
    print(f'Scanning text modifications for language {lang}...')
    spaces = '  '

    if verbose:
        print(f'{spaces}Identifying modified text files...')
    with open(bootup_path, 'rb') as b_file:
        bootup_sarc = sarc.read_file_and_make_sarc(b_file)
    msg_bytes = util.decompress(bootup_sarc.get_file_data(f'Message/Msg_{lang}.product.ssarc'))
    msg_sarc = sarc.SARC(msg_bytes)
    if not msg_sarc:
        print(f'Failed to open Msg_{lang}.product.ssarc, could not analyze texts')
    modded_msyts, added_msbts = get_modded_msyts(msg_sarc, lang)
    added_text_store = None
    if added_msbts:
        added_text_store = store_added_texts(added_msbts)

    if verbose:
        for modded_text in modded_msyts:
            print(f'{spaces}{spaces}{modded_text} has been changed')
        for added_text in added_msbts:
            print(f'{spaces}{spaces}{added_text} has been added')

    problems = msbt_to_msyt()
    for problem in problems:
        msyt_name = problem.relative_to(tmp_dir).with_suffix('.msyt').as_posix()
        try:
            modded_msyts.remove(msyt_name)
        except ValueError:
            pass
    if verbose:
        print(f'{spaces}Scanning texts files for modified entries...')
    modded_texts = get_modded_texts(modded_msyts, lang=lang)
    s_modded = 's' if len(modded_texts) != 1 else ''
    s_added = 's' if len(added_msbts) != 1 else ''
    print(f'Language {lang} has total {len(modded_texts)} modified text file{s_modded} and '
          f'{len(added_msbts)} new text file{s_added}')
    shutil.rmtree(tmp_dir)
    return modded_texts, added_text_store, lang
Exemple #7
0
def msyt_to_msbt(tmp_dir: Path = util.get_work_dir() / 'tmp_text'):
    """ Converts merged MSYTs in given temp dir to MSBTs """
    msyt_bin = util.get_exec_dir() / 'helpers' / 'msyt.exe'
    merge_dir = tmp_dir / 'merged'
    m_args = [str(msyt_bin), 'create', '-d', str(merge_dir),
              '-p', 'wiiu', '-o', str(merge_dir)]
    subprocess.run(m_args, stdout=subprocess.PIPE,
                   stderr=subprocess.PIPE, creationflags=util.CREATE_NO_WINDOW)
    for merged_msyt in merge_dir.rglob('**/*.msyt'):
        merged_msyt.unlink()
Exemple #8
0
def merge_texts(lang: str = 'USen',
                tmp_dir: Path = util.get_work_dir() / 'tmp_text',
                verbose: bool = False,
                original_pool: multiprocessing.Pool = None):
    """
    Merges installed text mods and saves the new Bootup_XXxx.pack, fixing the RSTB if needed

    :param lang: The game language to use, defaults to USen.
    :type lang: str, optional
    :param tmp_dir: The temp directory to extract to, defaults to "tmp_text" in BCML's work dir.
    :type tmp_dir: class:`pathlib.Path`, optional
    :param verbose: Whether to display more detailed output, defaults to False
    :type verbose: bool, optional
    """
    print(f'Loading text mods for language {lang}...')
    text_mods = get_modded_text_entries(lang)
    if not text_mods:
        print('No text merging necessary.')
        old_path = util.get_master_modpack_dir() / 'content' / 'Pack' / \
            f'Bootup_{lang}.pack'
        if old_path.exists():
            old_path.unlink()
        return
    if verbose:
        print(f'  Found {len(text_mods)} text mods to be merged')

    if tmp_dir.exists():
        if verbose:
            print('Cleaning temp directory...')
        shutil.rmtree(tmp_dir, ignore_errors=True)
    print('Extracting clean MSYTs...')
    try:
        extract_ref_msyts(lang, for_merge=True, tmp_dir=tmp_dir)
    except FileNotFoundError:
        return
    merge_dir = tmp_dir / 'merged'
    merge_dir.mkdir(parents=True, exist_ok=True)

    print('Merging modified text files...')
    modded_text_files = list(merge_dir.rglob('**/*.msyt'))
    num_threads = min(multiprocessing.cpu_count(), len(modded_text_files))
    pool = original_pool or multiprocessing.Pool(processes=num_threads)
    thread_merger = partial(threaded_merge_texts,
                            merge_dir=merge_dir,
                            text_mods=text_mods,
                            verbose=verbose)
    results = pool.map(thread_merger, modded_text_files)
    for merge_count, rel_path in results:
        if merge_count > 0:
            print(f'  Merged {merge_count} versions of {rel_path}')
    if not original_pool:
        pool.close()
        pool.join()
    print('Generating merged MSBTs...')
    msyt_to_msbt(tmp_dir)

    added_texts = get_added_text_mods(lang)
    if added_texts:
        print('Adding mod-original MSBTs...')
        for added_text in added_texts:
            for msbt in added_text.list_files():
                Path(merge_dir / msbt).parent.mkdir(parents=True,
                                                    exist_ok=True)
                Path(merge_dir / msbt).write_bytes(
                    added_text.get_file_data(msbt).tobytes())

    print(f'Creating new Bootup_{lang}.pack...')
    tmp_boot_path = bootup_from_msbts(lang)[0]
    merged_boot_path = util.get_modpack_dir() / '9999_BCML' / 'content' / \
        'Pack' / f'Bootup_{lang}.pack'
    if merged_boot_path.exists():
        if verbose:
            print(f'  Removing old Bootup_{lang}.pack...')
        merged_boot_path.unlink()
    merged_boot_path.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy(str(tmp_boot_path), str(merged_boot_path))

    rstb_path = util.get_modpack_dir() / '9999_BCML' / 'content' / 'System' / 'Resource' /\
                                         'ResourceSizeTable.product.srsizetable'
    if rstb_path.exists():
        table: rstb.ResourceSizeTable = rstb.util.read_rstb(
            str(rstb_path), True)
    else:
        table = rstable.get_stock_rstb()
    msg_path = f'Message/Msg_{lang}.product.sarc'
    if table.is_in_table(msg_path):
        print('Correcting RSTB...')
        table.delete_entry(msg_path)
    rstb_path.parent.mkdir(parents=True, exist_ok=True)
    rstb.util.write_rstb(table, str(rstb_path), True)