diff --git a/modules/title/ciosbuild.py b/modules/title/ciosbuild.py new file mode 100644 index 0000000..1c1f3df --- /dev/null +++ b/modules/title/ciosbuild.py @@ -0,0 +1,125 @@ +# "modules/title/ciosbuild.py" from WiiPy by NinjaCheetah +# https://github.com/NinjaCheetah/WiiPy + +import io +import os +import xml.etree.ElementTree as ET +import pathlib +import libWiiPy + + +def build_cios(args): + base_path = pathlib.Path(args.base) + map_path = pathlib.Path(args.map) + if args.modules: + modules_path = pathlib.Path(args.modules) + else: + modules_path = pathlib.Path(os.getcwd()) + output_path = pathlib.Path(args.output) + + if not base_path.exists(): + raise FileNotFoundError(base_path) + if not map_path.exists(): + raise FileNotFoundError(map_path) + if not modules_path.exists(): + raise FileNotFoundError(modules_path) + + title = libWiiPy.title.Title() + title.load_wad(open(base_path, 'rb').read()) + + cios_tree = ET.parse(map_path) + cios_root = cios_tree.getroot() + + # Iterate over all tags to find the cIOS that was requested, and return an error if it doesn't match + # any of the groups in the provided map. + target_cios = None + for child in cios_root: + cios = child.get("name") + if args.cios_ver == cios: + target_cios = child + break + if target_cios is None: + raise ValueError("The target cIOS could not be found in the provided map!") + + # Iterate over all bases in the target cIOS to find a base that matches the provided WAD. If one is found, ensure + # that the version of the base in the map matches the version of the IOS WAD. + target_base = None + provided_base = int(title.tmd.title_id[-2:], 16) + for child in target_cios: + base = int(child.get("ios")) + if base == provided_base: + target_base = child + break + if target_base is None: + raise ValueError("The provided base IOS doesn't match any bases found in the provided map!") + base_version = int(target_base.get("version")) + if title.tmd.title_version != base_version: + raise ValueError("The provided base IOS does not match the required version for this base!") + + # We're ready to begin building the cIOS now. Find all the tags that have tags, and then apply + # the patches listed in them to the content. + for content in target_base.findall("content"): + patches = content.findall("patch") + if patches: + cid = int(content.get("id"), 16) + dec_content = title.get_content_by_cid(cid) + content_index = title.content.get_index_from_cid(cid) + with io.BytesIO(dec_content) as content_data: + for patch in patches: + # Read patch info from the map. This requires some conversion since ciosmap files seem to use a + # comma-separated list of bytes. + offset = int(patch.get("offset"), 16) + original_data = b'' + original_data_map = patch.get("originalbytes").split(",") + for byte in original_data_map: + original_data += bytes.fromhex(byte[2:]) + new_data = b'' + new_data_map = patch.get("newbytes").split(",") + for byte in new_data_map: + new_data += bytes.fromhex(byte[2:]) + # Seek to the target offset and apply the patches. One last sanity check to ensure this + # original data exists. + if original_data in dec_content: + content_data.seek(offset) + content_data.write(new_data) + else: + raise Exception("An error occurred while patching! Please make sure your base IOS is valid.") + content_data.seek(0x0) + dec_content = content_data.read() + # Set the content in the title to the newly-patched content, and set the type to normal. + title.set_content(dec_content, content_index, content_type=libWiiPy.title.ContentType.NORMAL) + + # Next phase of cIOS building is to add the required extra modules to the end. + for content in target_base.findall("content"): + target_module = content.get("module") + if target_module is not None: + # The cIOS map supplies a Content ID to use for each additional module. + cid = int(content.get("id")[-2:], 16) + target_path = modules_path.joinpath(target_module + ".app") + if target_path.exists(): + new_module = open(target_path, "rb").read() + title.add_content(new_module, cid, libWiiPy.title.ContentType.NORMAL) + else: + raise Exception(f"A required module \"{target_module}.app\" could not be found!") + + # Last cIOS building step, we need to set the slot and version. + slot = args.slot + if 3 <= slot <= 255: + tid = title.tmd.title_id[:-2] + f"{slot:02X}" + title.set_title_id(tid) + else: + raise ValueError(f"The provided slot \"{slot}\" is not valid!") + try: + title.set_title_version(args.version) + except ValueError: + raise ValueError(f"The provided version \"{args.version}\" is not valid!") + + # Ensure the WAD is fakesigned. + title.fakesign() + + # Write the new cIOS to the specified output path. + out_file = open(output_path, "wb") + out_file.write(title.dump_wad()) + out_file.close() + + print("success") diff --git a/modules/title/wad.py b/modules/title/wad.py index 9e90475..0d3fbdf 100644 --- a/modules/title/wad.py +++ b/modules/title/wad.py @@ -64,7 +64,7 @@ def handle_wad_add(args): target_type = libWiiPy.title.ContentType.NORMAL # Call add_content to add our new content with the set parameters. - title.content.add_content(content_data, target_cid, target_type, title.ticket.get_title_key()) + title.add_content(content_data, target_cid, target_type) out_file = open(output_path, 'wb') out_file.write(title.dump_wad()) diff --git a/wiipy.py b/wiipy.py index 3c42259..de5b625 100644 --- a/wiipy.py +++ b/wiipy.py @@ -6,6 +6,7 @@ from importlib.metadata import version from modules.archive.ash import * from modules.archive.u8 import * +from modules.title.ciosbuild import * from modules.title.emunand import * from modules.title.fakesign import * from modules.title.info import * @@ -18,7 +19,7 @@ if __name__ == "__main__": parser = argparse.ArgumentParser( description="A simple command line tool to manage file formats used by the Wii.") parser.add_argument("--version", action="version", - version=f"WiiPy v1.3.0, based on libWiiPy v{version('libWiiPy')} (from branch \'main\')") + version=f"WiiPy v1.4.0, based on libWiiPy v{version('libWiiPy')} (from branch \'main\')") subparsers = parser.add_subparsers(title="subcommands", dest="subcommand", required=True) # Argument parser for the ASH subcommand. @@ -35,6 +36,22 @@ if __name__ == "__main__": ash_parser.add_argument("--dist-bits", metavar="DIST_BITS", type=int, help="number of bits in each distance tree leaf (default: 11)", default=11) + # Argument parser for the cIOS command + cios_parser = subparsers.add_parser("cios", help="build a cIOS from a base IOS and provided map", + description="build a cIOS from a base IOS and provided map") + cios_parser.set_defaults(func=build_cios) + cios_parser.add_argument("base", metavar="BASE", type=str, help="base IOS WAD") + cios_parser.add_argument("map", metavar="MAP", type=str, help="cIOS map file") + cios_parser.add_argument("output", metavar="OUT", type=str, help="file to output the cIOS to") + cios_parser.add_argument("-c", "--cios-ver", metavar="CIOS", type=str, + help="cIOS version from the map to build", required=True) + cios_parser.add_argument("-m", "--modules", metavar="MODULES", type=str, + help="directory to look for cIOS modules in (optional, defaults to current directory)") + cios_parser.add_argument("-s", "--slot", metavar="SLOT", type=int, + help="slot that this cIOS will install to (optional, defaults to 249)", default=249) + cios_parser.add_argument("-v", "--version", metavar="VERSION", type=int, + help="version that this cIOS will be (optional, defaults to 65535)", default=65535) + # Argument parser for the EmuNAND subcommand. emunand_parser = subparsers.add_parser("emunand", help="manage Wii EmuNAND directories", description="manage Wii EmuNAND directories")