From 76c78e6d85eb628fa3a0501e89130fb08c544ff3 Mon Sep 17 00:00:00 2001 From: NinjaCheetah <58050615+NinjaCheetah@users.noreply.github.com> Date: Sat, 16 Nov 2024 15:13:48 -0500 Subject: [PATCH] Added tmd edit command to change properties of a TMD file, same options as wad edit Stubbed theme command temporarily as it will not be part of WiiPy v1.4.0. Workflow also adjusted to build for x86_64 and arm64 on macOS --- .github/workflows/python-build.yaml | 37 ++++++++-- Makefile | 3 +- commands/title/tmd.py | 34 +++++++++ commands/title/wad.py | 108 +++++++++------------------- modules/title.py | 57 +++++++++++++++ wiipy.py | 47 +++++++----- 6 files changed, 190 insertions(+), 96 deletions(-) create mode 100644 modules/title.py diff --git a/.github/workflows/python-build.yaml b/.github/workflows/python-build.yaml index d1a120c..df01427 100644 --- a/.github/workflows/python-build.yaml +++ b/.github/workflows/python-build.yaml @@ -39,9 +39,9 @@ jobs: uses: actions/upload-artifact@v4.3.0 with: path: ~/WiiPy.tar - name: WiiPy-linux-bin + name: WiiPy-Linux-bin - build-macos: + build-macos-x86_64: runs-on: macos-latest @@ -56,7 +56,7 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt - name: Build Application - run: make all + run: ARCH_FLAGS=--macos-target-arch=x86_64 make all - name: Prepare Package for Upload run: | mv wiipy ~/wiipy @@ -66,7 +66,34 @@ jobs: uses: actions/upload-artifact@v4.3.0 with: path: ~/WiiPy.tar - name: WiiPy-macos-bin + name: WiiPy-macOS-x86_64-bin + + build-macos-arm64: + + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Build Application + run: ARCH_FLAGS=--macos-target-arch=arm64 make all + - name: Prepare Package for Upload + run: | + mv wiipy ~/wiipy + cd ~ + tar cvf WiiPy.tar wiipy + - name: Upload Application + uses: actions/upload-artifact@v4.3.0 + with: + path: ~/WiiPy.tar + name: WiiPy-macOS-arm64-bin build-windows: @@ -90,4 +117,4 @@ jobs: uses: actions/upload-artifact@v4.3.0 with: path: D:\a\WiiPy\WiiPy\wiipy.exe - name: WiiPy-windows-bin + name: WiiPy-Windows-bin diff --git a/Makefile b/Makefile index 99a5a46..0c83913 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ CC=python -m nuitka +ARCH_FLAGS?= all: - $(CC) --show-progress --assume-yes-for-downloads --onefile wiipy.py --onefile-tempdir-spec="{CACHE_DIR}/NinjaCheetah/WiiPy" -o wiipy + $(CC) --show-progress --assume-yes-for-downloads --onefile wiipy.py --onefile-tempdir-spec="{CACHE_DIR}/NinjaCheetah/WiiPy" $(ARCH_FLAGS) -o wiipy install: install wiipy /usr/bin/ diff --git a/commands/title/tmd.py b/commands/title/tmd.py index 168d10c..d13af22 100644 --- a/commands/title/tmd.py +++ b/commands/title/tmd.py @@ -4,6 +4,40 @@ import pathlib import libWiiPy from modules.core import fatal_error +from modules.title import tmd_edit_ios, tmd_edit_tid, tmd_edit_type + + +def handle_tmd_edit(args): + input_path = pathlib.Path(args.input) + if args.output is not None: + output_path = pathlib.Path(args.output) + else: + output_path = pathlib.Path(args.input) + + tmd = libWiiPy.title.TMD() + tmd.load(input_path.read_bytes()) + + # State variable to make sure that changes are made. + edits_made = False + # Go over every possible change, and apply them if they were specified. + if args.tid is not None: + tmd = tmd_edit_tid(tmd, args.tid) + edits_made = True + if args.ios is not None: + tmd = tmd_edit_ios(tmd, args.ios) + edits_made = True + if args.type is not None: + tmd = tmd_edit_type(tmd, args.type) + edits_made = True + + if not edits_made: + fatal_error("You must specify at least one change to make!") + + # Fakesign the title since any changes have already invalidated the signature. + tmd.fakesign() + output_path.write_bytes(tmd.dump()) + + print("Successfully edited TMD file!") def handle_tmd_remove(args): diff --git a/commands/title/wad.py b/commands/title/wad.py index 1385297..528b62b 100644 --- a/commands/title/wad.py +++ b/commands/title/wad.py @@ -1,12 +1,11 @@ # "commands/title/wad.py" from WiiPy by NinjaCheetah # https://github.com/NinjaCheetah/WiiPy -import binascii import pathlib from random import randint import libWiiPy - from modules.core import fatal_error +from modules.title import tmd_edit_ios, tmd_edit_tid, tmd_edit_type def handle_wad_add(args): @@ -138,6 +137,39 @@ def handle_wad_convert(args): print(f"Successfully converted {source} WAD to {target} WAD \"{output_path.name}\"!") +def handle_wad_edit(args): + input_path = pathlib.Path(args.input) + if args.output is not None: + output_path = pathlib.Path(args.output) + else: + output_path = pathlib.Path(args.input) + + title = libWiiPy.title.Title() + title.load_wad(input_path.read_bytes()) + + # State variable to make sure that changes are made. + edits_made = False + # Go over every possible change, and apply them if they were specified. + if args.tid is not None: + title.tmd = tmd_edit_tid(title.tmd, args.tid) + edits_made = True + if args.ios is not None: + title.tmd = tmd_edit_ios(title.tmd, args.ios) + edits_made = True + if args.type is not None: + title.tmd = tmd_edit_type(title.tmd, args.type) + edits_made = True + + if not edits_made: + fatal_error("You must specify at least one change to make!") + + # Fakesign the title since any changes have already invalidated the signature. + title.fakesign() + output_path.write_bytes(title.dump_wad()) + + print("Successfully edited WAD file!") + + def handle_wad_pack(args): input_path = pathlib.Path(args.input) output_path = pathlib.Path(args.output) @@ -368,75 +400,3 @@ def handle_wad_unpack(args): output_path.joinpath(content_file_name).write_bytes(title.get_content_by_index(content_file, skip_hash)) print("WAD file unpacked!") - - -def handle_wad_edit(args): - input_path = pathlib.Path(args.input) - if args.output is not None: - output_path = pathlib.Path(args.output) - else: - output_path = pathlib.Path(args.input) - - title = libWiiPy.title.Title() - title.load_wad(input_path.read_bytes()) - - # State variable to make sure that changes are made. - edits_made = False - # Go over every possible change, and apply them if they were specified. - if args.tid is not None: - # Setting a new TID, only changing TID low since this expects a 4 character ASCII input. - new_tid = args.tid - if len(new_tid) != 4: - fatal_error(f"The specified Title ID is not valid! The new Title ID should be 4 characters long.") - if not new_tid.isalnum(): - fatal_error(f"The specified Title ID is not valid! The new Title ID should be alphanumeric.") - # Get the current TID high, because we want to preserve the title type while only changing the TID low. - tid_high = title.tmd.title_id[:8] - new_tid = f"{tid_high}{str(binascii.hexlify(new_tid.encode()), 'ascii')}" - title.set_title_id(new_tid) - edits_made = True - if args.ios is not None: - # Setting a new required IOS. - new_ios = None - try: - new_ios = int(args.ios) - except ValueError: - fatal_error("The specified IOS is not valid! The new IOS should be a valid integer.") - if new_ios < 3 or new_ios > 255: - fatal_error("The specified IOS is not valid! The new IOS version should be between 3 and 255.") - new_ios_tid = f"00000001{new_ios:08X}" - title.tmd.ios_tid = new_ios_tid - edits_made = True - if args.type is not None: - # Setting a new title type. - new_type = args.type - new_tid_high = None - match new_type: - case "System": - new_tid_high = "00000001" - case "Channel": - new_tid_high = "00010001" - case "SystemChannel": - new_tid_high = "00010002" - case "GameChannel": - new_tid_high = "00010004" - case "DLC": - new_tid_high = "00010005" - case "HiddenChannel": - new_tid_high = "00010008" - case _: - fatal_error("The specified type is not valid! The new type must be one of: System, Channel, " - "SystemChannel, GameChannel, DLC, HiddenChannel.") - tid_low = title.tmd.title_id[8:] - new_tid = f"{new_tid_high}{tid_low}" - title.set_title_id(new_tid) - edits_made = True - - if not edits_made: - fatal_error("You must specify at least one change to make!") - - # Fakesign the title since any changes have already invalidated the signature. - title.fakesign() - output_path.write_bytes(title.dump_wad()) - - print("Successfully edited WAD file!") diff --git a/modules/title.py b/modules/title.py new file mode 100644 index 0000000..8c1281e --- /dev/null +++ b/modules/title.py @@ -0,0 +1,57 @@ +# "modules/title.py" from WiiPy by NinjaCheetah +# https://github.com/NinjaCheetah/WiiPy + +import binascii +import libWiiPy +from modules.core import fatal_error + + +def tmd_edit_ios(tmd: libWiiPy.title.TMD, new_ios: str) -> libWiiPy.title.TMD: + # Setting a new required IOS. + try: + new_ios = int(new_ios) + except ValueError: + fatal_error("The specified IOS is not valid! The new IOS should be a valid integer.") + if new_ios < 3 or new_ios > 255: + fatal_error("The specified IOS is not valid! The new IOS version should be between 3 and 255.") + new_ios_tid = f"00000001{new_ios:08X}" + tmd.ios_tid = new_ios_tid + return tmd + + +def tmd_edit_tid(tmd: libWiiPy.title.TMD, new_tid: str) -> libWiiPy.title.TMD: + # Setting a new TID, only changing TID low since this expects a 4 character ASCII input. + if len(new_tid) != 4: + fatal_error(f"The specified Title ID is not valid! The new Title ID should be 4 characters long.") + if not new_tid.isalnum(): + fatal_error(f"The specified Title ID is not valid! The new Title ID should be alphanumeric.") + # Get the current TID high, because we want to preserve the title type while only changing the TID low. + tid_high = tmd.title_id[:8] + new_tid = f"{tid_high}{str(binascii.hexlify(new_tid.encode()), 'ascii')}" + tmd.set_title_id(new_tid) + return tmd + + +def tmd_edit_type(tmd: libWiiPy.title.TMD, new_type: str) -> libWiiPy.title.TMD: + # Setting a new title type. + new_tid_high = None + match new_type: + case "System": + new_tid_high = "00000001" + case "Channel": + new_tid_high = "00010001" + case "SystemChannel": + new_tid_high = "00010002" + case "GameChannel": + new_tid_high = "00010004" + case "DLC": + new_tid_high = "00010005" + case "HiddenChannel": + new_tid_high = "00010008" + case _: + fatal_error("The specified type is not valid! The new type must be one of: System, Channel, " + "SystemChannel, GameChannel, DLC, HiddenChannel.") + tid_low = tmd.title_id[8:] + new_tid = f"{new_tid_high}{tid_low}" + tmd.set_title_id(new_tid) + return tmd diff --git a/wiipy.py b/wiipy.py index e328bcd..a302d9a 100644 --- a/wiipy.py +++ b/wiipy.py @@ -5,7 +5,7 @@ import argparse from importlib.metadata import version from commands.archive.ash import * -from commands.archive.theme import * +#from commands.archive.theme import * from commands.archive.u8 import * from commands.nand.emunand import * from commands.nand.setting import * @@ -197,24 +197,39 @@ if __name__ == "__main__": setting_gen_parser.add_argument("region", metavar="REGION", type=str, help="region of the console these settings are for (USA, EUR, JPN, or KOR)") - # Argument parser for the theme subcommand. - theme_parser = subparsers.add_parser("theme", help="apply custom themes to the Wii Menu", - description="apply custom themes to the Wii Menu") - theme_subparsers = theme_parser.add_subparsers(dest="subcommand", required=True) - # MYM theme subcommand. - theme_mym_parser = theme_subparsers.add_parser("mym", help="apply an MYM theme to the Wii Menu", - description="apply an MYM theme to the Wii Menu") - theme_mym_parser.set_defaults(func=handle_apply_mym) - theme_mym_parser.add_argument("mym", metavar="MYM", type=str, help="MYM theme to apply") - theme_mym_parser.add_argument("base", metavar="BASE", type=str, - help="base Wii Menu assets to apply the theme to (000000xx.app)") - theme_mym_parser.add_argument("output", metavar="OUT", type=str, - help="path to output the finished theme to (.csm)") + # # Argument parser for the theme subcommand. + # theme_parser = subparsers.add_parser("theme", help="apply custom themes to the Wii Menu", + # description="apply custom themes to the Wii Menu") + # theme_subparsers = theme_parser.add_subparsers(dest="subcommand", required=True) + # # MYM theme subcommand. + # theme_mym_parser = theme_subparsers.add_parser("mym", help="apply an MYM theme to the Wii Menu", + # description="apply an MYM theme to the Wii Menu") + # theme_mym_parser.set_defaults(func=handle_apply_mym) + # theme_mym_parser.add_argument("mym", metavar="MYM", type=str, help="MYM theme to apply") + # theme_mym_parser.add_argument("base", metavar="BASE", type=str, + # help="base Wii Menu assets to apply the theme to (000000xx.app)") + # theme_mym_parser.add_argument("output", metavar="OUT", type=str, + # help="path to output the finished theme to (.csm)") # Argument parser for the TMD subcommand. tmd_parser = subparsers.add_parser("tmd", help="edit a TMD file", description="edit a TMD file") tmd_subparsers = tmd_parser.add_subparsers(dest="subcommand", required=True) + # Edit TMD subcommand. + tmd_edit_parser = tmd_subparsers.add_parser("edit", help="edit the properties of a TMD file", + description="edit the properties of a TMD file; by default, this will " + "overwrite the input file unless an output is specified") + tmd_edit_parser.set_defaults(func=handle_tmd_edit) + tmd_edit_parser.add_argument("input", metavar="IN", type=str, help="TMD file to edit") + tmd_edit_parser.add_argument("--tid", metavar="TID", type=str, + help="a new Title ID for this title (formatted as 4 ASCII characters)") + tmd_edit_parser.add_argument("--ios", metavar="IOS", type=str, + help="a new IOS version for this title (formatted as the decimal IOS version, eg. 58)") + tmd_edit_parser.add_argument("--type", metavar="TYPE", type=str, + help="a new type for this title (valid options: System, Channel, SystemChannel, " + "GameChannel, DLC, HiddenChannel)") + tmd_edit_parser.add_argument("-o", "--output", metavar="OUT", type=str, + help="file to output the updated TMD to (optional)") # Remove TMD subcommand. tmd_remove_parser = tmd_subparsers.add_parser("remove", help="remove a content record from a TMD file", description="remove a content record from a TMD file, either by its " @@ -248,8 +263,8 @@ if __name__ == "__main__": u8_unpack_parser.add_argument("output", metavar="OUT", type=str, help="folder to output to") # Argument parser for the WAD subcommand. - wad_parser = subparsers.add_parser("wad", help="pack/unpack a WAD file", - description="pack/unpack a WAD file") + wad_parser = subparsers.add_parser("wad", help="pack/unpack/edit a WAD file", + description="pack/unpack/edit a WAD file") wad_subparsers = wad_parser.add_subparsers(dest="subcommand", required=True) # Add WAD subcommand. wad_add_parser = wad_subparsers.add_parser("add", help="add decrypted content to a WAD file",