def get_msbt_hashes(lang: str = 'USen') -> {}: """ Gets the MSBT hash table for the given language, or US English by default :param lang: The game language to use, defaults to USen. :type lang: str, optional :returns: A dictionary of MSBT files and their vanilla hashes. :rtype: dict of str: str """ if not hasattr(get_msbt_hashes, 'texthashes'): get_msbt_hashes.texthashes = {} if lang not in get_msbt_hashes.texthashes: hash_table = util.get_exec_dir() / 'data' / 'msyt' / \ f'Msg_{lang}_hashes.csv' if hash_table.exists(): get_msbt_hashes.texthashes[lang] = {} with hash_table.open('r') as h_file: csv_loop = csv.reader(h_file) for row in csv_loop: get_msbt_hashes.texthashes[lang][row[0]] = row[1] elif util.get_game_file(f'Pack/Bootup_{lang}.pack').exists(): get_msbt_hashes.texthashes[lang] = {} 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) for msbt in msg_pack.list_files(): get_msbt_hashes.texthashes[lang][msbt] = xxhash.xxh32( msg_pack.get_file_data(msbt)).hexdigest() return get_msbt_hashes.texthashes[lang]
def inject_savedata_into_bootup(bgsvdata: sarc.SARCWriter, bootup_path: Path = None) -> int: """ Packs a savedata SARC into Bootup.pack and returns the RSTB size of the new savedataformat.sarc :param bgsvdata: A SARCWriter for the new savedata :type bgsvdata: class:`sarc.SARCWriter` :param bootup_path: Path to the Bootup.pack to update, defaults to a master BCML copy :type bootup_path: class:`pathlib.Path`, optional :returns: Returns the RSTB size of the new savedataformat.sarc :rtype: int """ if not bootup_path: master_boot = util.get_master_modpack_dir() / 'content' / 'Pack' / 'Bootup.pack' bootup_path = master_boot if master_boot.exists() \ else util.get_game_file('Pack/Bootup.pack') with bootup_path.open('rb') as b_file: bootup_pack = sarc.read_file_and_make_sarc(b_file) new_pack = sarc.make_writer_from_sarc(bootup_pack) new_pack.delete_file('GameData/savedataformat.ssarc') savedata_bytes = bgsvdata.get_bytes() new_pack.add_file('GameData/savedataformat.ssarc', util.compress(savedata_bytes)) (util.get_master_modpack_dir() / 'content' / 'Pack').mkdir(parents=True, exist_ok=True) with (util.get_master_modpack_dir() / 'content' / 'Pack' / 'Bootup.pack').open('wb') as b_file: new_pack.write(b_file) return rstb.SizeCalculator().calculate_file_size_with_ext(savedata_bytes, True, '.sarc')
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)
def merge_dungeonstatic(diffs: dict = None): """Merges all changes to the CDungeon Static.smubin""" if not diffs: diffs = {} loader = yaml.CSafeLoader yaml_util.add_constructors(loader) for mod in [mod for mod in util.get_installed_mods() \ if (mod.path / 'logs' / 'dstatic.yml').exists()]: diffs.update( yaml.load((mod.path / 'logs' / 'dstatic.yml').read_bytes(), Loader=loader)) if not diffs: return new_static = byml.Byml( util.decompress_file( str(util.get_game_file( 'aoc/0010/Map/CDungeon/Static.smubin')))).parse() base_dungeons = [dungeon['Map'] for dungeon in new_static['StartPos']] for dungeon, diff in diffs.items(): if dungeon not in base_dungeons: new_static['StartPos'].append(diff) else: for key, value in diff.items(): new_static['StartPos'][base_dungeons.index( dungeon)][key] = value output_static = util.get_master_modpack_dir() / 'aoc' / '0010' / 'Map' / \ 'CDungeon' / 'Static.smubin' output_static.parent.mkdir(parents=True, exist_ok=True) output_static.write_bytes( util.compress(byml.Writer(new_static, True).get_bytes()))
def get_dungeonstatic_diff(file: Path) -> dict: """Returns the changes made to the Static.smubin containing shrine entrance coordinates :param file: The Static.mubin file to diff :type file: class:`pathlib.Path` :return: Returns a dict of shrines and their updated entrance coordinates :rtype: dict of str: dict """ base_file = util.get_game_file('aoc/0010/Map/CDungeon/Static.smubin', aoc=True) base_pos = byml.Byml(util.decompress_file( str(base_file))).parse()['StartPos'] mod_pos = byml.Byml(util.decompress_file(str(file))).parse()['StartPos'] base_dungeons = [dungeon['Map'] for dungeon in base_pos] diffs = {} for dungeon in mod_pos: if dungeon['Map'] not in base_dungeons: diffs[dungeon['Map']] = dungeon else: base_dungeon = base_pos[base_dungeons.index(dungeon['Map'])] if dungeon['Rotate'] != base_dungeon['Rotate']: diffs[dungeon['Map']] = {'Rotate': dungeon['Rotate']} if dungeon['Translate'] != base_dungeon['Translate']: if dungeon['Map'] not in diffs: diffs[dungeon['Map']] = {} diffs[dungeon['Map']]['Translate'] = dungeon['Translate'] return diffs
def perform_merge(self): diffs = self.consolidate_diffs(self.get_all_diffs()) output: Path static_data: Path try: util.get_aoc_dir() output = (util.get_master_modpack_dir() / util.get_dlc_path() / ("0010" if util.get_settings("wiiu") else "") / STATIC_PATH) static_data = util.get_game_file("Map/MainField/Static.smubin", aoc=True).read_bytes() except FileNotFoundError: output = util.get_master_modpack_dir( ) / "logs" / "mainstatic.smubin" static_data = util.get_nested_file_bytes( (str(util.get_game_file("Pack/Bootup.pack")) + "//Map/MainField/Static.smubin"), unyaz=False, ) if not diffs: try: output.unlink() except: pass return stock_static = oead.byml.from_binary(util.decompress(static_data)) merged = Hash() for cat in stock_static: if cat in diffs: items = {get_id(item): item for item in stock_static[cat]} util.dict_merge(items, diffs[cat]) merged[cat] = Array([ item for _, item in items.items() if "remove" not in item ]) else: merged[cat] = stock_static[cat] data = util.compress( oead.byml.to_binary(merged, big_endian=util.get_settings("wiiu"))) output.parent.mkdir(parents=True, exist_ok=True) output.write_bytes(data) if "mainstatic" in str(output): util.inject_file_into_sarc( "Map/MainField/Static.smubin", data, "Pack/Bootup.pack", create_sarc=True, )
def get_stock_gamedata() -> sarc.SARC: """ Gets the contents of the unmodded gamedata.sarc """ if not hasattr(get_stock_gamedata, 'gamedata'): with util.get_game_file('Pack/Bootup.pack').open('rb') as b_file: bootup = sarc.read_file_and_make_sarc(b_file) get_stock_gamedata.gamedata = sarc.SARC(util.decompress( bootup.get_file_data('GameData/gamedata.ssarc'))) return get_stock_gamedata.gamedata
def generate_diff(self, mod_dir: Path, modded_files: List[Union[str, Path]]): mod_data: bytes stock_data: bytes if (mod_dir / util.get_dlc_path() / ("0010" if util.get_settings("wiiu") else "") / STATIC_PATH) in modded_files: mod_data = (mod_dir / util.get_dlc_path() / ("0010" if util.get_settings("wiiu") else "") / STATIC_PATH).read_bytes() stock_data = util.get_game_file("Map/MainField/Static.smubin", aoc=True).read_bytes() elif (f"{util.get_content_path()}/Pack/Bootup.pack//Map/MainField/Static.smubin" ) in modded_files: mod_data = util.get_nested_file_bytes( (str(mod_dir / util.get_content_path() / "Pack" / "Bootup.pack") + "//Map/MainField/Static.smubin"), unyaz=False, ) stock_data = util.get_nested_file_bytes( (str(util.get_game_file("Pack/Bootup.pack")) + "//Map/MainField/Static.smubin"), unyaz=False, ) else: return None stock_static: Hash = oead.byml.from_binary(util.decompress(stock_data)) mod_static: Hash = oead.byml.from_binary(util.decompress(mod_data)) diffs = Hash() for cat in stock_static: if cat not in stock_static: continue stock_items = {get_id(item): item for item in stock_static[cat]} mod_items = {get_id(item): item for item in mod_static[cat]} diffs[cat] = Hash({ item_id: item for item_id, item in mod_items.items() if item_id not in stock_items or item != stock_items[item_id] }) for item_id, item in [(i1, i2) for i1, i2 in stock_items.items() if i1 not in mod_items]: item["remove"] = True diffs[cat][item_id] = item if not diffs[cat]: del diffs[cat] return diffs
def get_stock_actorinfo() -> dict: """ Gets the unmodded contents of ActorInfo.product.sbyml """ actorinfo = util.get_game_file('Actor/ActorInfo.product.sbyml') with actorinfo.open('rb') as a_file: return byml.Byml(util.decompress(a_file.read())).parse()
def perform_merge(self): shutil.rmtree( str(util.get_master_modpack_dir() / util.get_dlc_path() / ("0010" if util.get_settings("wiiu") else "") / "Map" / "MainField"), ignore_errors=True, ) shutil.rmtree( str(util.get_master_modpack_dir() / util.get_content_path() / "Map" / "MainField"), ignore_errors=True, ) log_path = util.get_master_modpack_dir() / "logs" / "map.log" if log_path.exists(): log_path.unlink() print("Loading map mods...") map_diffs = self.consolidate_diffs(self.get_all_diffs()) util.vprint("All map diffs:") util.vprint(map_diffs) if not map_diffs: print("No map merge necessary") return aoc_pack = (util.get_master_modpack_dir() / util.get_dlc_path() / ("0010" if util.get_settings("wiiu") else "") / "Pack" / "AocMainField.pack") if not aoc_pack.exists() or aoc_pack.stat().st_size > 0: print("Emptying AocMainField.pack...") aoc_pack.parent.mkdir(parents=True, exist_ok=True) aoc_pack.write_bytes(b"") rstb_vals = {} rstb_calc = rstb.SizeCalculator() print("Merging modded map units...") pool = self._pool or Pool(maxtasksperchild=500) rstb_results = pool.map( partial(merge_map, rstb_calc=rstb_calc), map_diffs.items(), ) for result in rstb_results: rstb_vals[result[util.get_dlc_path()][0]] = result[ util.get_dlc_path()][1] rstb_vals[result["main"][0]] = result["main"][1] if not self._pool: pool.close() pool.join() stock_static = [m for m in map_diffs if m[1] == "Static"] if stock_static: title_path = (util.get_master_modpack_dir() / util.get_content_path() / "Pack" / "TitleBG.pack") if not title_path.exists(): title_path.parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(util.get_game_file("Pack/TitleBG.pack"), title_path) title_bg: oead.SarcWriter = oead.SarcWriter.from_sarc( oead.Sarc(title_path.read_bytes())) for static in stock_static: del title_bg.files[ f"Map/MainField/{static[0]}/{static[0]}_Static.smubin"] title_path.write_bytes(title_bg.write()[1]) print("Adjusting RSTB...") log_path.parent.mkdir(parents=True, exist_ok=True) with log_path.open("w", encoding="utf-8") as l_file: for canon, val in rstb_vals.items(): l_file.write(f"{canon},{val}\n") print("Map merge complete")
def get_stock_map(map_unit: Union[Map, tuple], force_vanilla: bool = False) -> Hash: if isinstance(map_unit, tuple): map_unit = Map(*map_unit) try: aoc_dir = util.get_aoc_dir() except FileNotFoundError: force_vanilla = True map_bytes = None map_path: Union[str, Path] if force_vanilla: try: if util.get_settings("wiiu"): update = util.get_update_dir() else: update = util.get_game_dir() map_path = ( update / "Map/MainField/" f"{map_unit.section}/{map_unit.section}_{map_unit.type}.smubin" ) if not map_path.exists(): map_path = ( util.get_game_dir() / "Map/MainField/" f"{map_unit.section}/{map_unit.section}_{map_unit.type}.smubin" ) map_bytes = map_path.read_bytes() except FileNotFoundError: try: title_pack = oead.Sarc( util.get_game_file("Pack/TitleBG.pack").read_bytes()) map_bytes = title_pack.get_file( f"Map/MainField/{map_unit.section}/{map_unit.section}_{map_unit.type}" ".smubin").data except (KeyError, RuntimeError, AttributeError): map_bytes = None else: if (aoc_dir / "Pack" / "AocMainField.pack").exists(): try: map_pack = oead.Sarc( (aoc_dir / "Pack" / "AocMainField.pack").read_bytes()) map_bytes = map_pack.get_file( f"Map/MainField/{map_unit.section}/{map_unit.section}_{map_unit.type}" ".smubin").data except (KeyError, RuntimeError, AttributeError): map_bytes = None if not map_bytes: map_path = f"Map/MainField/{map_unit.section}/{map_unit.section}_{map_unit.type}.smubin" try: map_bytes = util.get_game_file(map_path, aoc=True).read_bytes() except FileNotFoundError: try: map_bytes = util.get_game_file(map_path).read_bytes() except FileNotFoundError: try: title_pack = oead.Sarc( util.get_game_file( "Pack/TitleBG.pack").read_bytes()) map_bytes = bytes( title_pack.get_file( f"Map/MainField/{map_unit.section}/" f"{map_unit.section}_{map_unit.type}.smubin"). data) except (KeyError, RuntimeError, AttributeError): map_bytes = None if not map_bytes: raise FileNotFoundError( f"The stock map file {map_unit.section}_{map_unit.type}.smubin could not be found." ) map_bytes = util.decompress(map_bytes) return oead.byml.from_binary(map_bytes)
def get_stock_map(map_unit: Union[Map, tuple], force_vanilla: bool = False) -> dict: """ Finds the most significant available map unit from the unmodded game and returns its contents as a dict. :param map: The map section and type. :type map: class:`bcml.mubin.Map` :return: Returns a dict representation of the requested map unit. :rtype: dict """ if isinstance(map_unit, tuple): map_unit = Map(*map_unit) try: aoc_dir = util.get_aoc_dir() except FileNotFoundError: force_vanilla = True map_bytes = None if force_vanilla: try: map_path = ( util.get_update_dir() / 'Map/MainField/' f'{map_unit.section}/{map_unit.section}_{map_unit.type}.smubin' ) if not map_path.exists(): map_path = ( util.get_game_dir() / 'Map/MainField/' f'{map_unit.section}/{map_unit.section}_{map_unit.type}.smubin' ) map_bytes = map_path.read_bytes() except FileNotFoundError: with util.get_game_file('Pack/TitleBG.pack').open('rb') \ as s_file: title_pack = sarc.read_file_and_make_sarc(s_file) if title_pack: try: map_bytes = title_pack.get_file_data( f'Map/MainField/{map_unit.section}/{map_unit.section}_{map_unit.type}' '.smubin').tobytes() except KeyError: map_bytes = None else: if (aoc_dir / 'Pack' / 'AocMainField.pack').exists(): with (aoc_dir / 'Pack' / 'AocMainField.pack').open('rb') as s_file: map_pack = sarc.read_file_and_make_sarc(s_file) if map_pack: try: map_bytes = map_pack.get_file_data( f'Map/MainField/{map_unit.section}/{map_unit.section}_{map_unit.type}' '.smubin').tobytes() except KeyError: map_bytes = None if not map_bytes: map_path = f'Map/MainField/{map_unit.section}/{map_unit.section}_{map_unit.type}.smubin' try: map_bytes = util.get_game_file(map_path, aoc=True).read_bytes() except FileNotFoundError: try: map_bytes = util.get_game_file(map_path).read_bytes() except FileNotFoundError: with util.get_game_file('Pack/TitleBG.pack').open('rb') \ as s_file: title_pack = sarc.read_file_and_make_sarc(s_file) if title_pack: try: map_bytes = title_pack.get_file_data( f'Map/MainField/{map_unit.section}/' f'{map_unit.section}_{map_unit.type}.smubin' ).tobytes() except KeyError: map_bytes = None if not map_bytes: raise FileNotFoundError( f'The stock map file {map_unit.section}_{map_unit.type}.smubin could not be found.' ) map_bytes = util.decompress(map_bytes) return byml.Byml(map_bytes).parse()