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
This commit is contained in:
Campbell 2024-11-16 15:13:48 -05:00
parent 554bbfb7cb
commit 76c78e6d85
Signed by: NinjaCheetah
GPG Key ID: B547958AF96ED344
6 changed files with 190 additions and 96 deletions

View File

@ -39,9 +39,9 @@ jobs:
uses: actions/upload-artifact@v4.3.0 uses: actions/upload-artifact@v4.3.0
with: with:
path: ~/WiiPy.tar path: ~/WiiPy.tar
name: WiiPy-linux-bin name: WiiPy-Linux-bin
build-macos: build-macos-x86_64:
runs-on: macos-latest runs-on: macos-latest
@ -56,7 +56,7 @@ jobs:
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -r requirements.txt pip install -r requirements.txt
- name: Build Application - name: Build Application
run: make all run: ARCH_FLAGS=--macos-target-arch=x86_64 make all
- name: Prepare Package for Upload - name: Prepare Package for Upload
run: | run: |
mv wiipy ~/wiipy mv wiipy ~/wiipy
@ -66,7 +66,34 @@ jobs:
uses: actions/upload-artifact@v4.3.0 uses: actions/upload-artifact@v4.3.0
with: with:
path: ~/WiiPy.tar 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: build-windows:
@ -90,4 +117,4 @@ jobs:
uses: actions/upload-artifact@v4.3.0 uses: actions/upload-artifact@v4.3.0
with: with:
path: D:\a\WiiPy\WiiPy\wiipy.exe path: D:\a\WiiPy\WiiPy\wiipy.exe
name: WiiPy-windows-bin name: WiiPy-Windows-bin

View File

@ -1,7 +1,8 @@
CC=python -m nuitka CC=python -m nuitka
ARCH_FLAGS?=
all: 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:
install wiipy /usr/bin/ install wiipy /usr/bin/

View File

@ -4,6 +4,40 @@
import pathlib import pathlib
import libWiiPy import libWiiPy
from modules.core import fatal_error 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): def handle_tmd_remove(args):

View File

@ -1,12 +1,11 @@
# "commands/title/wad.py" from WiiPy by NinjaCheetah # "commands/title/wad.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy # https://github.com/NinjaCheetah/WiiPy
import binascii
import pathlib import pathlib
from random import randint from random import randint
import libWiiPy import libWiiPy
from modules.core import fatal_error from modules.core import fatal_error
from modules.title import tmd_edit_ios, tmd_edit_tid, tmd_edit_type
def handle_wad_add(args): 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}\"!") 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): def handle_wad_pack(args):
input_path = pathlib.Path(args.input) input_path = pathlib.Path(args.input)
output_path = pathlib.Path(args.output) 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)) output_path.joinpath(content_file_name).write_bytes(title.get_content_by_index(content_file, skip_hash))
print("WAD file unpacked!") 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!")

57
modules/title.py Normal file
View File

@ -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

View File

@ -5,7 +5,7 @@ import argparse
from importlib.metadata import version from importlib.metadata import version
from commands.archive.ash import * from commands.archive.ash import *
from commands.archive.theme import * #from commands.archive.theme import *
from commands.archive.u8 import * from commands.archive.u8 import *
from commands.nand.emunand import * from commands.nand.emunand import *
from commands.nand.setting import * from commands.nand.setting import *
@ -197,24 +197,39 @@ if __name__ == "__main__":
setting_gen_parser.add_argument("region", metavar="REGION", type=str, setting_gen_parser.add_argument("region", metavar="REGION", type=str,
help="region of the console these settings are for (USA, EUR, JPN, or KOR)") help="region of the console these settings are for (USA, EUR, JPN, or KOR)")
# Argument parser for the theme subcommand. # # Argument parser for the theme subcommand.
theme_parser = subparsers.add_parser("theme", help="apply custom themes to the Wii Menu", # theme_parser = subparsers.add_parser("theme", help="apply custom themes to the Wii Menu",
description="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) # theme_subparsers = theme_parser.add_subparsers(dest="subcommand", required=True)
# MYM theme subcommand. # # MYM theme subcommand.
theme_mym_parser = theme_subparsers.add_parser("mym", help="apply an MYM theme to the Wii Menu", # 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") # description="apply an MYM theme to the Wii Menu")
theme_mym_parser.set_defaults(func=handle_apply_mym) # 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("mym", metavar="MYM", type=str, help="MYM theme to apply")
theme_mym_parser.add_argument("base", metavar="BASE", type=str, # theme_mym_parser.add_argument("base", metavar="BASE", type=str,
help="base Wii Menu assets to apply the theme to (000000xx.app)") # help="base Wii Menu assets to apply the theme to (000000xx.app)")
theme_mym_parser.add_argument("output", metavar="OUT", type=str, # theme_mym_parser.add_argument("output", metavar="OUT", type=str,
help="path to output the finished theme to (<filename>.csm)") # help="path to output the finished theme to (<filename>.csm)")
# Argument parser for the TMD subcommand. # Argument parser for the TMD subcommand.
tmd_parser = subparsers.add_parser("tmd", help="edit a TMD file", tmd_parser = subparsers.add_parser("tmd", help="edit a TMD file",
description="edit a TMD file") description="edit a TMD file")
tmd_subparsers = tmd_parser.add_subparsers(dest="subcommand", required=True) 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. # Remove TMD subcommand.
tmd_remove_parser = tmd_subparsers.add_parser("remove", help="remove a content record from a TMD file", 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 " 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") u8_unpack_parser.add_argument("output", metavar="OUT", type=str, help="folder to output to")
# Argument parser for the WAD subcommand. # Argument parser for the WAD subcommand.
wad_parser = subparsers.add_parser("wad", help="pack/unpack a WAD file", wad_parser = subparsers.add_parser("wad", help="pack/unpack/edit a WAD file",
description="pack/unpack a WAD file") description="pack/unpack/edit a WAD file")
wad_subparsers = wad_parser.add_subparsers(dest="subcommand", required=True) wad_subparsers = wad_parser.add_subparsers(dest="subcommand", required=True)
# Add WAD subcommand. # Add WAD subcommand.
wad_add_parser = wad_subparsers.add_parser("add", help="add decrypted content to a WAD file", wad_add_parser = wad_subparsers.add_parser("add", help="add decrypted content to a WAD file",