def generate_diff(self, mod_dir: Path, modded_files: List[Union[Path, str]]) -> ParameterIO: print("Logging changes to shop files...") diffs = ParameterIO() file_names = ParameterObject() for file in [ file for file in modded_files if Path(file).suffix in EXT_FOLDERS ]: try: mod_bytes = util.get_nested_file_bytes( str(mod_dir) + "/" + str(file)) nests = str(file).split("//", 1) try: ref_path = str(util.get_game_file(Path( nests[0]))) + "//" + nests[1] except FileNotFoundError: continue try: ref_bytes = util.get_nested_file_bytes(ref_path) except AttributeError: continue shop_type = str(file).split(".")[-1] mod_pio = get_named_pio(ParameterIO.from_binary(mod_bytes), shop_type) ref_pio = get_named_pio(ParameterIO.from_binary(ref_bytes), shop_type) file_names.params[oead.aamp.Name(file).hash] = Parameter(file) diffs.lists[file] = gen_diffs(ref_pio, mod_pio) except (KeyError, AttributeError) as err: raise err diffs.objects["Filenames"] = file_names return diffs
def get_aamp_diff(file: Union[Path, str], tmp_dir: Path): """ Diffs a modded AAMP file from the stock game version :param file: The modded AAMP file to diff :type file: class:`typing.Union[class:pathlib.Path, str]` :param tmp_dir: The temp directory containing the mod :type tmp_dir: class:`pathlib.Path` :return: Returns a string representation of the AAMP file diff """ if isinstance(file, str): nests = file.split('//') mod_bytes = util.get_nested_file_bytes(file) ref_path = str(util.get_game_file( Path(nests[0]).relative_to(tmp_dir))) + '//' + '//'.join(nests[1:]) ref_bytes = util.get_nested_file_bytes(ref_path) else: with file.open('rb') as m_file: mod_bytes = m_file.read() mod_bytes = util.unyaz_if_needed(mod_bytes) with util.get_game_file(file.relative_to(tmp_dir)).open('rb') as r_file: ref_bytes = r_file.read() ref_bytes = util.unyaz_if_needed(ref_bytes) ref_aamp = aamp.Reader(ref_bytes).parse() mod_aamp = aamp.Reader(mod_bytes).parse() return _aamp_diff(ref_aamp, mod_aamp)
def log_drop_file(file: str, mod_dir: Path): if "Bootup.pack" in file: return {} drop = ParameterIO.from_binary( util.get_nested_file_bytes(str(mod_dir) + "/" + file)) drop_table = _drop_to_dict(drop) del drop try: base_file = file[:file.index("//")] sub_file = file[file.index("//"):] ref_drop = ParameterIO.from_binary( util.get_nested_file_bytes( str(util.get_game_file(base_file)) + sub_file)) ref_table = _drop_to_dict(ref_drop) del ref_drop for table, contents in drop_table.items(): if table not in ref_table: continue for item, prob in {(i, p) for i, p in contents["items"].items() if i in ref_table[table]["items"]}: if prob == ref_table[table]["items"][item]: drop_table[table]["items"][item] = util.UNDERRIDE del ref_table except ( FileNotFoundError, oead.InvalidDataError, AttributeError, RuntimeError, ValueError, ): util.vprint(f"Could not load stock {file}") return {file: drop_table}
def merge_drop_file(file: str, drop_table: dict): base_path = file[:file.index("//")] sub_path = file[file.index("//"):] try: ref_drop = _drop_to_dict( ParameterIO.from_binary( util.get_nested_file_bytes( str(util.get_game_file(base_path)) + sub_path))) for table in set(ref_drop.keys()): if table not in drop_table: del ref_drop[table] else: for item in set(ref_drop[table]["items"].keys()): if item not in drop_table[table]["items"]: del ref_drop[table]["items"][item] util.dict_merge(ref_drop, drop_table) drop_table = ref_drop except (FileNotFoundError, AttributeError, RuntimeError): pass actor_name_matches = re.search(r"Pack\/(.+)\.sbactorpack", file) if actor_name_matches: actor_name = actor_name_matches.groups()[0] else: raise ValueError(f"No actor name found in {file}") pio = _dict_to_drop(drop_table) util.inject_files_into_actor(actor_name, {file.split("//")[-1]: pio.to_binary()})
def perform_merge(self): merged_quests = util.get_master_modpack_dir() / "logs" / "quests.byml" print("Loading quest mods...") diffs = self.consolidate_diffs(self.get_all_diffs()) if not diffs: print("No quest merging necessary") if merged_quests.exists(): merged_quests.unlink() try: util.inject_file_into_sarc( "Quest/QuestProduct.sbquestpack", util.get_nested_file_bytes( (f'{str(util.get_game_file("Pack/TitleBG.pack"))}' "//Quest/QuestProduct.sbquestpack"), unyaz=False, ), "Pack/TitleBG.pack", ) except FileNotFoundError: pass return print("Loading stock quests...") quests = get_stock_quests() stock_names = [q["Name"] for q in quests] print("Merging quest mods...") for name, mod in diffs["mod"].items(): try: quests[stock_names.index(name)] = mod except (IndexError, ValueError): diffs["add"].append(mod) for delete in reversed(diffs["del"]): try: quests.remove(quests[stock_names.index(delete)]) except ValueError: pass except IndexError: raise RuntimeError( f"An error occurred when attempting to remove a quest from your " "game. Most of the time this means the mod was accidentally made " "using the base game 1.0 TitleBG.pack instead of the latest updated " "version. Please contact the mod author for assistance.") added_names = set() for add in diffs["add"]: if add["Name"] not in added_names: quests.append(add) added_names.add(add["Name"]) print("Writing new quest pack...") data = oead.byml.to_binary(quests, big_endian=util.get_settings("wiiu")) merged_quests.parent.mkdir(parents=True, exist_ok=True) merged_quests.write_bytes(data) util.inject_file_into_sarc( "Quest/QuestProduct.sbquestpack", util.compress(data), "Pack/TitleBG.pack", create_sarc=True, )
def perform_merge(self): merged_events = util.get_master_modpack_dir() / "logs" / "eventinfo.byml" event_merge_log = util.get_master_modpack_dir() / "logs" / "eventinfo.log" print("Loading event info mods...") modded_events = self.consolidate_diffs(self.get_all_diffs()) event_mod_hash = hash(str(modded_events)) if not modded_events: print("No event info merging necessary") if merged_events.exists(): merged_events.unlink() event_merge_log.unlink() try: stock_eventinfo = util.get_nested_file_bytes( ( str(util.get_game_file("Pack/Bootup.pack")) + "//Event/EventInfo.product.sbyml" ), unyaz=False, ) util.inject_file_into_sarc( "Event/EventInfo.product.sbyml", stock_eventinfo, "Pack/Bootup.pack", ) except FileNotFoundError: pass return if event_merge_log.exists() and event_merge_log.read_text() == event_mod_hash: print("No event info merging necessary") return new_events = get_stock_eventinfo() for event, data in modded_events.items(): new_events[event] = data del modded_events print("Writing new event info...") event_bytes = oead.byml.to_binary( new_events, big_endian=util.get_settings("wiiu") ) del new_events util.inject_file_into_sarc( "Event/EventInfo.product.sbyml", util.compress(event_bytes), "Pack/Bootup.pack", create_sarc=True, ) print("Saving event info merge log...") event_merge_log.parent.mkdir(parents=True, exist_ok=True) event_merge_log.write_text(str(event_mod_hash)) merged_events.write_bytes(event_bytes) print("Updating RSTB...") rstb_size = rstb.SizeCalculator().calculate_file_size_with_ext( bytes(event_bytes), True, ".byml" ) del event_bytes rstable.set_size("Event/EventInfo.product.byml", rstb_size)
def get_stock_eventinfo() -> {}: """ Gets the contents of the stock `EventInfo.product.sbyml` """ if not hasattr(get_stock_eventinfo, 'event_info'): get_stock_eventinfo.event_info = byml.Byml( util.get_nested_file_bytes( str(util.get_game_file('Pack/Bootup.pack')) + '//Event/EventInfo.product.sbyml', unyaz=True ) ).parse() return deepcopy(get_stock_eventinfo.event_info)
def perform_merge(self): merged_effects = util.get_master_modpack_dir() / "logs" / "effects.byml" print("Loading status effect mods...") diffs = self.consolidate_diffs(self.get_all_diffs()) if not diffs: print("No status effect merging necessary...") if merged_effects.exists(): merged_effects.unlink() try: stock_effects = util.get_nested_file_bytes( ( str(util.get_game_file("Pack/Bootup.pack")) + "//Ecosystem/StatusEffectList.sbyml" ), unyaz=False, ) util.inject_file_into_sarc( "Ecosystem/StatusEffectList.sbyml", stock_effects, "Pack/Bootup.pack", ) del stock_effects except FileNotFoundError: pass return util.vprint("All status effect diffs:") util.vprint(diffs) effects = get_stock_effects() util.dict_merge(effects, diffs, overwrite_lists=True) del diffs print("Writing new effects list...") effect_bytes = oead.byml.to_binary( oead.byml.Array([effects]), big_endian=util.get_settings("wiiu") ) del effects util.inject_file_into_sarc( "Ecosystem/StatusEffectList.sbyml", util.compress(effect_bytes), "Pack/Bootup.pack", create_sarc=True, ) print("Saving status effect merge log...") merged_effects.parent.mkdir(parents=True, exist_ok=True) merged_effects.write_bytes(effect_bytes) print("Updating RSTB...") rstb_size = rstb.SizeCalculator().calculate_file_size_with_ext( effect_bytes, True, ".byml" ) del effect_bytes rstable.set_size("Ecosystem/StatusEffectList.byml", rstb_size)
def perform_merge(self): merged_quests = util.get_master_modpack_dir() / "logs" / "quests.byml" print("Loading quest mods...") diffs = self.consolidate_diffs(self.get_all_diffs()) if not diffs: print("No quest merging necessary") if merged_quests.exists(): merged_quests.unlink() try: util.inject_file_into_sarc( "Quest/QuestProduct.sbquestpack", util.get_nested_file_bytes( (f'{str(util.get_game_file("Pack/TitleBG.pack"))}' "//Quest/QuestProduct.sbquestpack"), unyaz=False, ), "Pack/TitleBG.pack", ) except FileNotFoundError: pass return print("Loading stock quests...") quests = get_stock_quests() stock_names = [q["Name"] for q in quests] print("Merging quest mods...") for name, mod in diffs["mod"].items(): try: quests[stock_names.index(name)] = mod except (IndexError, ValueError): diffs["add"].append(mod) for delete in reversed(diffs["del"]): try: quests.remove(quests[stock_names.index(delete)]) except ValueError: pass added_names = set() for add in diffs["add"]: if add["Name"] not in added_names: quests.append(add) added_names.add(add["Name"]) print("Writing new quest pack...") data = oead.byml.to_binary(quests, big_endian=util.get_settings("wiiu")) merged_quests.parent.mkdir(parents=True, exist_ok=True) merged_quests.write_bytes(data) util.inject_file_into_sarc( "Quest/QuestProduct.sbquestpack", util.compress(data), "Pack/TitleBG.pack", create_sarc=True, )
def _convert_actorpack(actor_pack: Path, to_wiiu: bool) -> Union[None, str]: error = None sarc = oead.Sarc(util.unyaz_if_needed(actor_pack.read_bytes())) new_sarc = oead.SarcWriter.from_sarc(sarc) new_sarc.set_endianness(oead.Endianness.Big if to_wiiu else oead.Endianness.Little) for file in sarc.get_files(): if "Physics/" in file.name and "Actor/" not in file.name: ext = file.name[file.name.rindex(".") :] if ext in NO_CONVERT_EXTS: if not util.is_file_modded( util.get_canon_name(file.name, allow_no_source=True), file.data, count_new=True, ): actor_name = file.name[ file.name.rindex("/") : file.name.rindex(".") ] try: pack_path = util.get_game_file( f"Actor/Pack/{actor_name}.sbactorpack" ) stock_data = util.get_nested_file_bytes( f"{str(pack_path)}//{file.name}" ) if stock_data: new_sarc.files[file.name] = stock_data else: raise FileNotFoundError(file.name) except (FileNotFoundError, AttributeError): error = ( "This mod contains a Havok file not supported by the " f"converter: {file.name}" ) else: error = ( "This mod contains a Havok file not supported by the" f" converter: {file.name}" ) else: if file.data[0:4] == b"AAMP": continue try: hk = Havok.from_bytes(bytes(file.data)) except: # pylint: disable=bare-except return f"Could not parse Havok file {file.name}" if to_wiiu: hk.to_wiiu() else: hk.to_switch() hk.serialize() new_sarc.files[file.name] = hk.to_bytes() actor_pack.write_bytes(util.compress(new_sarc.write()[1])) return error
def get_stock_eventinfo() -> oead.byml.Hash: if not hasattr(get_stock_eventinfo, "event_info"): get_stock_eventinfo.event_info = oead.byml.to_text( oead.byml.from_binary( util.get_nested_file_bytes( str(util.get_game_file("Pack/Bootup.pack")) + "//Event/EventInfo.product.sbyml", unyaz=True, ) ) ) return oead.byml.from_text(get_stock_eventinfo.event_info)
def merge_events(): """ Merges all installed event info mods """ event_mods = [mod for mod in util.get_installed_mods() \ if (mod.path / 'logs' / 'eventinfo.yml').exists()] merged_events = util.get_master_modpack_dir() / 'logs' / 'eventinfo.byml' event_merge_log = util.get_master_modpack_dir() / 'logs' / 'eventinfo.log' event_mod_hash = str(hash(tuple(event_mods))) if not event_mods: print('No event info merging necessary') if merged_events.exists(): merged_events.unlink() event_merge_log.unlink() try: stock_eventinfo = util.get_nested_file_bytes( str(util.get_game_file('Pack/Bootup.pack')) + '//Event/EventInfo.product.sbyml', unyaz=False ) util.inject_file_into_bootup( 'Event/EventInfo.product.sbyml', stock_eventinfo ) except FileNotFoundError: pass return if event_merge_log.exists() and event_merge_log.read_text() == event_mod_hash: print('No event info merging necessary') return print('Loading event info mods...') modded_events = {} for mod in event_mods: modded_events.update(get_events_for_mod(mod)) new_events = get_stock_eventinfo() for event, data in modded_events.items(): new_events[event] = data print('Writing new event info...') event_bytes = byml.Writer(new_events, be=True).get_bytes() util.inject_file_into_bootup( 'Event/EventInfo.product.sbyml', util.compress(event_bytes), create_bootup=True ) print('Saving event info merge log...') event_merge_log.write_text(event_mod_hash) merged_events.write_bytes(event_bytes) print('Updating RSTB...') rstb_size = rstb.SizeCalculator().calculate_file_size_with_ext(event_bytes, True, '.byml') rstable.set_size('Event/EventInfo.product.byml', rstb_size)
def rem_underride(data: dict): for file, tables in data.items(): stock: Optional[dict] = None for name, table in tables.items(): for actor, prob in table["items"].items(): if prob == util.UNDERRIDE: if stock == None: base_file = file[:file.index("//")] sub_file = file[file.index("//"):] ref_drop = ParameterIO.from_binary( util.get_nested_file_bytes( str(util.get_game_file(base_file)) + sub_file)) stock = _drop_to_dict(ref_drop) data[file][name]["items"][actor] = stock[name][ "items"][actor]
def get_stock_areadata() -> oead.byml.Hash: if not hasattr(get_stock_areadata, "areadata"): get_stock_areadata.areadata = oead.byml.to_text( oead.byml.Hash( { str(area["AreaNumber"].v): area for area in oead.byml.from_binary( util.get_nested_file_bytes( str(util.get_game_file("Pack/Bootup.pack")) + "//Ecosystem/AreaData.sbyml", unyaz=True, ) ) } ) ) return oead.byml.from_text(get_stock_areadata.areadata)
def perform_merge(self): merged_areadata = util.get_master_modpack_dir() / "logs" / "areadata.byml" areadata_merge_log = util.get_master_modpack_dir() / "logs" / "areadata.log" print("Loading area data mods...") modded_areadata = self.consolidate_diffs(self.get_all_diffs()) areadata_mod_hash = hash(str(modded_areadata)) if not modded_areadata: print("No area data merging necessary") if merged_areadata.exists(): merged_areadata.unlink() areadata_merge_log.unlink() try: stock_areadata = util.get_nested_file_bytes( ( str(util.get_game_file("Pack/Bootup.pack")) + "//Ecosystem/AreaData.sbyml" ), unyaz=False, ) util.inject_file_into_sarc( "Ecosystem/AreaData.sbyml", stock_areadata, "Pack/Bootup.pack", ) except FileNotFoundError: pass return if ( areadata_merge_log.exists() and areadata_merge_log.read_text() == areadata_mod_hash ): print("No area data merging necessary") return new_areadata = get_stock_areadata() util.dict_merge(new_areadata, modded_areadata, overwrite_lists=True) print("Writing new area data...") areadata_bytes = oead.byml.to_binary( oead.byml.Array( [v for _, v in sorted(new_areadata.items(), key=lambda x: int(x[0]))] ), big_endian=util.get_settings("wiiu"), ) del new_areadata util.inject_file_into_sarc( "Ecosystem/AreaData.sbyml", util.compress(areadata_bytes), "Pack/Bootup.pack", create_sarc=True, ) print("Saving area data merge log...") areadata_merge_log.parent.mkdir(parents=True, exist_ok=True) areadata_merge_log.write_text(str(areadata_mod_hash)) merged_areadata.write_bytes(areadata_bytes) print("Updating RSTB...") rstb_size = rstb.SizeCalculator().calculate_file_size_with_ext( bytes(areadata_bytes), True, ".byml" ) del areadata_bytes rstable.set_size("Ecosystem/AreaData.byml", rstb_size)