def replace_file(root_sarc: Sarc, file: str, new_data: bytes) -> Sarc: if file.endswith("/"): file = file[0:-1] parent = get_parent_sarc(root_sarc, file) filename = file.split("//")[-1] new_sarc: SarcWriter = SarcWriter.from_sarc(parent) new_sarc.files[filename] = new_data while root_sarc != parent: _, child = new_sarc.write() file = file[0:file.rindex("//")] if file.endswith("/"): file = file[:-1] parent = get_parent_sarc(root_sarc, file) new_sarc = SarcWriter.from_sarc(parent) ext = file[file.rindex("."):] new_sarc.files[file] = (child if not (ext.startswith(".s") and ext != ".sarc") else compress(child)) return Sarc(new_sarc.write()[1])
def _build_actorinfo(params: BuildParams): actors = [] for actor_file in (params.out / params.content / "Actor" / "ActorInfo").glob( "*.info" ): actors.append(oead.byml.from_binary(actor_file.read_bytes())) actor_file.unlink() hashes = oead.byml.Array( [ oead.S32(crc) if crc < 2147483648 else oead.U32(crc) for crc in sorted({crc32(a["name"].encode("utf8")) for a in actors}) ] ) actors.sort(key=lambda actor: crc32(actor["name"].encode("utf8"))) actor_info = oead.byml.Hash({"Actors": oead.byml.Array(actors), "Hashes": hashes}) info_path = params.out / params.content / "Actor" / "ActorInfo.product.sbyml" info_path.parent.mkdir(exist_ok=True, parents=True) info_path.write_bytes( compress(oead.byml.to_binary(actor_info, big_endian=params.be)) )
def _build_actorinfo(params: BuildParams): actors = [] for actor_file in (params.mod / params.content / 'Actor' / 'ActorInfo').glob('*.info.yml'): actors.append( oead.byml.from_text(actor_file.read_text(encoding='utf-8'))) hashes = oead.byml.Array([ oead.S32(crc) if crc < 2147483648 else oead.U32(crc) for crc in sorted({crc32(a['name'].encode('utf8')) for a in actors}) ]) actors.sort(key=lambda actor: crc32(actor['name'].encode('utf8'))) actor_info = oead.byml.Hash({ 'Actors': oead.byml.Array(actors), 'Hashes': hashes }) info_path = params.out / params.content / 'Actor' / 'ActorInfo.product.sbyml' info_path.parent.mkdir(exist_ok=True, parents=True) info_path.write_bytes( compress(oead.byml.to_binary(actor_info, big_endian=params.be)))
def save_yaml(self, yaml: str, obj_type: str, big_endian: bool, path: str) -> dict: if not path: result = self.window.create_file_dialog(webview.SAVE_DIALOG) if result: path = result if isinstance(result, str) else result[0] else: return {"error": {"msg": "Cancelled", "traceback": ""}} try: data = _yaml.save_yaml(yaml, obj_type, big_endian) pathy_path = Path(path) if pathy_path.suffix.startswith(".s"): data = compress(data) if not path.startswith("SARC:"): pathy_path.write_bytes(data) else: self._open_sarc, _, modded = _sarc.open_sarc( _sarc.add_file(self._open_sarc, path, data)) return {"modded": modded} except Exception as err: # pylint: disable=broad-except return {"error": {"msg": str(err), "traceback": format_exc(-5)}} return {}
def rename_file(root_sarc: Sarc, file: str, new_name: str) -> Sarc: if file.endswith("/"): file = file[0:-1] if any(char in new_name for char in r"""\/:*?"'<>|"""): raise ValueError(f"{new_name} is not a valid file name.") parent = get_parent_sarc(root_sarc, file) filename = file.split("//")[-1] new_sarc: SarcWriter = SarcWriter.from_sarc(parent) new_sarc.files[(Path(filename).parent / new_name).as_posix()] = Bytes( parent.get_file(filename).data) del new_sarc.files[filename] while root_sarc != parent: _, child = new_sarc.write() file = file[0:file.rindex("//")] if file.endswith("/"): file = file[:-1] parent = get_parent_sarc(root_sarc, file) new_sarc = SarcWriter.from_sarc(parent) ext = file[file.rindex("."):] new_sarc.files[file] = (child if not (ext.startswith(".s") and ext != ".sarc") else compress(child)) return Sarc(new_sarc.write()[1])
def build_mod(args): mod = Path(args.directory) meta = {} if (mod / "config.yml").exists(): meta = _parse_config(mod / "config.yml", args) content = "content" if args.be else "01007EF00011E000/romfs" aoc = "aoc" if args.be else "01007EF00011F001/romfs" if not ((mod / content).exists() or (mod / aoc).exists()): print( "The specified directory does not appear to have a valid folder structure." ) print("Run `hyrule_builder build --help` for more information.") sys.exit(2) out = mod.with_name(f"{mod.name}_build") if not args.output else Path(args.output) if out.exists(): print("Removing old build...") shutil.rmtree(out) params = BuildParams( mod=mod, out=out, be=args.be, guess=not args.no_guess, verbose=args.verbose, content=content, aoc=aoc, titles=set(args.title_actors.split(",")), table=StockHashTable(args.be), warn=not args.no_warn, strict=args.hard_warn, ) print("Scanning source files...") files = { f for f in mod.rglob("**/*") if f.is_file() # and "ActorInfo" not in f.parts and not str(f.relative_to(mod)).startswith(".") } other_files = {f for f in files if f.suffix not in {".yml", ".msyt"}} yml_files = {f for f in files if f.suffix == ".yml"} f: Path rvs = {} if not args.single: p = Pool() print("Copying miscellaneous files...") if args.single or len(other_files) < 2: for f in other_files: rvs.update(_copy_file(f, params)) else: results = p.map(partial(_copy_file, params=params), other_files) for r in results: rvs.update(r) if (mod / content).exists(): msg_dirs = { d for d in mod.glob(f"{content}/Pack/Bootup_*.pack") if d.is_dir() and not d.name == "Bootup_Graphics.pack" } if msg_dirs: print("Building MSBT files...") for d in msg_dirs: msg_dir = next(d.glob("Message/*")) new_dir = out / msg_dir.relative_to(mod).with_suffix(".ssarc.ssarc") pymsyt.create(msg_dir, new_dir) print("Building AAMP and BYML files...") if args.single or len(yml_files) < 2: for f in yml_files: rvs.update(_build_yml(f, params)) else: try: results = p.map(partial(_build_yml, params=params), yml_files) except RuntimeError as err: print(err) sys.exit(1) for r in results: rvs.update(r) main_aoc = Path(aoc) / ("0010" if params.be else "") if (params.out / main_aoc / "Map" / "MainField").exists(): (params.out / main_aoc / "Pack").mkdir(parents=True, exist_ok=True) (params.out / main_aoc / "Pack" / "AocMainField.pack").write_bytes(b"") if (mod / content / "Actor" / "ActorInfo").is_dir(): print("Building actor info...") _build_actorinfo(params) actors = {f for f in (out / content / "Actor" / "ActorLink").glob("*.bxml")} if actors: (out / content / "Actor" / "Pack").mkdir(parents=True, exist_ok=True) print("Building actor packs...") if args.single or len(actors) < 2: for a in actors: rvs.update(_build_actor(a, params)) else: try: results = p.map(partial(_build_actor, params=params), actors) except RuntimeError as err: print(err) sys.exit(1) for r in results: rvs.update(r) for d in (out / content / "Physics").glob("*"): if d.stem not in ["StaticCompound", "TeraMeshRigidBody"]: shutil.rmtree(d) { # pylint: disable=expression-not-assigned shutil.rmtree(d) for d in (out / content / "Actor").glob("*") if d.is_dir() and d.stem != "Pack" } print("Building SARC files...") dirs = {d for d in out.rglob("**/*") if d.is_dir()} sarc_folders = {d for d in dirs if d.suffix in SARC_EXTS and d.suffix != ".pack"} pack_folders = {d for d in dirs if d.suffix == ".pack"} if args.single or (len(sarc_folders) + len(pack_folders)) < 3: for d in sarc_folders: rvs.update(_build_sarc(d, params)) for d in pack_folders: rvs.update(_build_sarc(d, params)) else: sarc_func = partial(_build_sarc, params=params) results = p.map(sarc_func, sarc_folders) for r in results: rvs.update(r) results = p.map(sarc_func, pack_folders) for r in results: rvs.update(r) if p: p.close() p.join() rp = out / content / "System" / "Resource" / "ResourceSizeTable.product.json" if rp.exists() or rvs: print("Updating RSTB...") table: ResourceSizeTable if args.no_rstb: if rp.exists(): table = load_rstb(args.be, file=rp) else: if rp.exists(): table = load_rstb(args.be, file=rp) else: table = load_rstb(args.be) rp.parent.mkdir(parents=True, exist_ok=True) if rvs and not (len(rvs) == 1 and list(rvs.keys())[0] is None): for p, v in rvs.items(): if not p: continue msg: str = "" if table.is_in_table(p): if v > table.get_size(p) > 0: table.set_size(p, v) msg = f"Updated {p} to {v}" elif v == 0: table.delete_entry(p) msg = f"Deleted {p}" else: msg = f"Skipped {p}" else: if v > 0 and p not in STOCK_FILES: table.set_size(p, v) msg = f"Added {p}, set to {v}" if args.verbose and msg: print(msg) buf = BytesIO() table.write(buf, args.be) rp.with_suffix(".srsizetable").write_bytes(compress(buf.getvalue())) if rp.exists(): rp.unlink() if meta: with (out / "rules.txt").open("w", encoding="utf-8") as rules: rules.write("[Definition]\n") rules.write( "titleIds = 00050000101C9300,00050000101C9400,00050000101C9500\n" ) for key, val in meta.items(): rules.write(f"{key} = {val}\n") if "path" not in meta and "name" in meta: rules.write( f"path = The Legend of Zelda: Breath of the Wild/Mods/{meta['name']}\n" ) rules.write("version = 4\n") print("Mod built successfully")
def _build_actor(link: Path, params: BuildParams): pack = oead.SarcWriter( endian=oead.Endianness.Big if params.be else oead.Endianness.Little) actor_name = link.stem actor = oead.aamp.ParameterIO.from_binary(link.read_bytes()) actor_path = params.out / params.content / 'Actor' targets = actor.objects['LinkTarget'] modified = False try: files = {f'Actor/ActorLink/{actor_name}.bxml': link} for p, name in targets.params.items(): name = name.v if name == 'Dummy': continue if p.hash in LINK_MAP: path = LINK_MAP[p.hash].replace('*', name) files['Actor/' + path] = actor_path / path elif p == 110127898: # ASUser list_path = actor_path / 'ASList' / f'{name}.baslist' aslist_bytes = list_path.read_bytes() files[f'Actor/ASList/{name}.baslist'] = list_path aslist = oead.aamp.ParameterIO.from_binary(aslist_bytes) for _, anim in aslist.lists['ASDefines'].objects.items(): filename = anim.params["Filename"].v if filename != 'Dummy': files[ f'Actor/AS/{filename}.bas'] = actor_path / 'AS' / f'{filename}.bas' elif p == 1086735552: # AttentionUser list_path = actor_path / 'AttClientList' / f'{name}.batcllist' atcllist_bytes = list_path.read_bytes() files[f'Actor/AttClientList/{name}.batcllist'] = list_path atcllist = oead.aamp.ParameterIO.from_binary(atcllist_bytes) for _, atcl in atcllist.lists['AttClients'].objects.items(): filename = atcl.params['FileName'].v if filename != 'Dummy': files[ f'Actor/AttClient/{filename}.batcl'] = actor_path / 'AttClient' / f'{filename}.batcl' elif p == 4022948047: # RgConfigListUser list_path = actor_path / 'RagdollConfigList' / f'{name}.brgconfiglist' rgconfiglist_bytes = list_path.read_bytes() files[ f'Actor/RagdollConfigList/{name}.brgconfiglist'] = list_path rgconfiglist = oead.aamp.ParameterIO.from_binary( rgconfiglist_bytes) for _, impl in rgconfiglist.lists[ 'ImpulseParamList'].objects.items(): filename = impl.params['FileName'].v if filename != 'Dummy': files[f'Actor/RagdollConfig/{filename}.brgconfig'] = actor_path / \ 'RagdollConfig' / f'{filename}.brgconfig' elif p == 2366604039: # PhysicsUser phys_source = params.out / params.content / 'Physics' phys_path = actor_path / 'Physics' / f'{name}.bphysics' phys_bytes = phys_path.read_bytes() files[f'Actor/Physics/{name}.bphysics'] = phys_path phys = oead.aamp.ParameterIO.from_binary(phys_bytes) types = phys.lists['ParamSet'].objects[1258832850] if types.params['use_ragdoll'].v: rg_path = str(phys.lists['ParamSet'].objects['Ragdoll']. params['ragdoll_setup_file_path'].v) files[ f'Physics/Ragdoll/{rg_path}'] = phys_source / 'Ragdoll' / rg_path if types.params['use_support_bone'].v: sb_path = str( phys.lists['ParamSet'].objects['SupportBone']. params['support_bone_setup_file_path'].v) files[ f'Physics/SupportBone/{sb_path}'] = phys_source / 'SupportBone' / sb_path if types.params['use_cloth'].v: cloth_path = str( phys.lists['ParamSet'].lists['Cloth'].objects[ 'ClothHeader'].params['cloth_setup_file_path'].v) files[ f'Physics/Cloth/{cloth_path}'] = phys_source / 'Cloth' / cloth_path if types.params['use_rigid_body_set_num'].v > 0: for _, rigid in phys.lists['ParamSet'].lists[ 'RigidBodySet'].lists.items(): try: rigid_path = str(rigid.objects[4288596824]. params['setup_file_path'].v) if (phys_path / 'RigidBody' / rigid_path).exists(): files[ f'Physics/RigidBody/{rigid_path}'] = phys_path / 'RigidBody' / rigid_path except KeyError: continue for name, path in files.items(): data = path.read_bytes() pack.files[name] = data if not modified and params.table.is_file_modded( name.replace('.s', ''), memoryview(data), True): modified = True except FileNotFoundError as e: print( f'Failed to build actor "{actor_name}": Could not find linked file "{e.filename}".' ) return {} _, sb = pack.write() dest: Path if actor_name in TITLE_ACTORS: dest = params.out / params.content / 'Pack' / 'TitleBG.pack' / 'Actor' / 'Pack' / f'{actor_name}.sbactorpack' else: dest = params.out / params.content / 'Actor' / 'Pack' / f'{actor_name}.sbactorpack' if not dest.parent.exists(): dest.parent.mkdir(parents=True, exist_ok=True) dest.write_bytes(compress(sb)) if modified: return { f'Actor/Pack/{actor_name}.bactorpack': _get_rstb_val('.sbactorpack', sb, params.guess, params.be) } return {}
def build(self): if not ((self.mod / self.content).exists() or (self.mod / self.aoc).exists()): print( "The specified directory does not appear to have a valid folder structure." ) print("Run `hyrule_builder build --help` for more information.") sys.exit(2) if self.out.exists(): print("Removing old build...") shutil.rmtree(self.out) print("Scanning source files...") files = { f for f in self.mod.rglob("**/*") if f.is_file() and "build" not in f.parts and not str(f.relative_to(self.mod)).startswith(".") } other_files = {f for f in files if f.suffix not in {".yml", ".msyt"}} yml_files = {f for f in files if f.suffix == ".yml"} f: Path rvs = {} if not self.single: p = Pool(maxtasksperchild=256) print("Copying miscellaneous files...") if self.single or len(other_files) < 2: for f in other_files: rvs.update(self._copy_file(f)) else: results = p.map(self._copy_file, other_files) for r in results: rvs.update(r) if (self.mod / self.content).exists(): msg_dirs = { d for d in self.mod.glob(f"{self.content}/Pack/Bootup_*.pack") if d.is_dir() and not d.name == "Bootup_Graphics.pack" } if msg_dirs: print("Building MSBT files...") for d in msg_dirs: msg_dir = next(d.glob("Message/*")) new_dir = self.out / msg_dir.relative_to( self.mod).with_suffix(".ssarc") pymsyt.create(str(msg_dir), self.be, output=str(new_dir)) print("Building AAMP and BYML files...") if self.single or len(yml_files) < 2: for f in yml_files: rvs.update(self._build_yml(f)) else: try: results = p.map(self._build_yml, yml_files) except RuntimeError as err: print(err) sys.exit(1) for r in results: rvs.update(r) main_aoc = Path(self.aoc) / ("0010" if self.be else "") if (self.out / main_aoc / "Map" / "MainField").exists(): (self.out / main_aoc / "Pack").mkdir(parents=True, exist_ok=True) (self.out / main_aoc / "Pack" / "AocMainField.pack").write_bytes(b"") if (self.mod / self.content / "Actor" / "ActorInfo").is_dir(): print("Building actor info...") self._build_actorinfo() actors = { f for f in (self.out / self.content / "Actor" / "ActorLink").glob("*.bxml") } if actors: (self.out / self.content / "Actor" / "Pack").mkdir(parents=True, exist_ok=True) print("Building actor packs...") if self.single or len(actors) < 2: for a in actors: rvs.update(self._build_actor(a)) else: try: results = p.map(self._build_actor, actors) except RuntimeError as err: print(err) sys.exit(1) for r in results: rvs.update(r) for d in (self.out / self.content / "Physics").glob("*"): if d.stem not in ["StaticCompound", "TeraMeshRigidBody"]: shutil.rmtree(d) { # pylint: disable=expression-not-assigned shutil.rmtree(d) for d in (self.out / self.content / "Actor").glob("*") if d.is_dir() and d.stem != "Pack" } print("Building SARC files...") dirs = {d for d in self.out.rglob("**/*") if d.is_dir()} sarc_folders = { d for d in dirs if d.suffix in SARC_EXTS and d.suffix != ".pack" } pack_folders = {d for d in dirs if d.suffix == ".pack"} if self.single or (len(sarc_folders) + len(pack_folders)) < 3: for d in sarc_folders: rvs.update(self._build_sarc(d)) for d in pack_folders: rvs.update(self._build_sarc(d)) else: results = p.map(self._build_sarc, sarc_folders) for r in results: rvs.update(r) results = p.map(self._build_sarc, pack_folders) for r in results: rvs.update(r) if p: p.close() p.join() rp = (self.out / self.content / "System" / "Resource" / "ResourceSizeTable.product.json") if rp.exists() or rvs: print("Updating RSTB...") table: ResourceSizeTable if self.no_rstb: if rp.exists(): table = self.load_rstb(file=rp) else: if rp.exists(): table = self.load_rstb(file=rp) else: table = self.load_rstb() rp.parent.mkdir(parents=True, exist_ok=True) if rvs and not (len(rvs) == 1 and list(rvs.keys())[0] is None): for p, v in rvs.items(): if not p: continue msg: str = "" if table.is_in_table(p): if v > table.get_size(p) > 0: table.set_size(p, v) msg = f"Updated {p} to {v}" elif v == 0: table.delete_entry(p) msg = f"Deleted {p}" else: msg = f"Skipped {p}" else: if v > 0 and p not in STOCK_FILES: table.set_size(p, v) msg = f"Added {p}, set to {v}" if self.verbose and msg: print(msg) buf = BytesIO() table.write(buf, self.be) rp.with_suffix(".srsizetable").write_bytes(compress( buf.getvalue())) if rp.exists(): rp.unlink() if self.meta: with (self.out / "rules.txt").open("w", encoding="utf-8") as rules: rules.write("[Definition]\n") if "path" not in self.meta and "name" in self.meta: self.meta[ "path"] = f"The Legend of Zelda: Breath of the Wild/Mods/{self.meta['name']}" if "titleIds" not in self.meta: self.meta[ "titleIds"] = "00050000101C9300,00050000101C9400,00050000101C9500" for key, val in self.meta.items(): rules.write(f"{key} = {val}\n") rules.write("version = 4\n")
def write_rstb(rstb: ResourceSizeTable, path: Path, be: bool): buf = BytesIO() rstb.write(buf, be) path.write_bytes(compress(buf.getvalue()))