23 Commits

Author SHA1 Message Date
76c78e6d85 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
2024-11-16 15:13:48 -05:00
554bbfb7cb Added wad edit command to change properties of a WAD file
Info command also now shows the 4 character ASCII version of the Title ID when applicable
2024-11-16 12:56:39 -05:00
42fc37de65 Updated Makefile and build instructions 2024-11-15 19:05:29 -05:00
71450b5204 Add Build.ps1 to replicate Makefile functionality on Windows 2024-11-15 18:31:47 -05:00
2733b70e18 Cleaned up warnings related to error handling changes 2024-11-10 21:51:46 -05:00
6336791be0 Rewrote error output to be much clearer, no longer raises Python exceptions 2024-11-10 19:58:31 -05:00
19dc956d25 Added error handling to theme application to catch invalid themes 2024-11-10 17:41:18 -05:00
3de4dbd6e6 Added experimental MYM application code, bumped to Python 3.12 2024-11-10 17:20:15 -05:00
ec7cb1063f Restructured command files, updated U8 command syntax to match others 2024-11-07 13:57:33 -05:00
33197c36f1 Use custom cache dir to hopefully speed up launches after the first launch 2024-11-01 16:04:14 -04:00
b77587771c Throw missing version warning before downloading content in nus module 2024-10-30 14:11:28 -04:00
4941cf8b94 Fix tiny bug in how CIDs were parsed in cIOS builder 2024-10-29 22:04:04 -04:00
73a877090b New command to edit TMDs, currently just supports removing content records 2024-10-28 18:10:36 -04:00
530afd4189 Joined all WAD re-encryption commands into "wad convert" 2024-10-27 11:33:12 -04:00
a4c06cae36 Don't remove vWii flag when building vWii cIOSes 2024-10-27 09:16:59 -04:00
bcdb4fed21 cIOS builder now re-encrypts vWii IOSes with the common key
Also added new wad vwii2wii command that will re-encrypt any input vWii WAD with the common key.
2024-10-26 19:14:29 -04:00
55e38caf46 Experimental update to how cIOSes are built 2024-10-25 14:30:13 -04:00
2342657e70 Fixed issue with content ordering when packing/unpacking 2024-10-24 14:32:48 -04:00
af9b639a6b Fix bug in nus.py where --version didn't actually download a specific version for TMDs 2024-10-20 22:07:07 -04:00
1612d2ecb9 Optimized large amounts of file I/O code that was very long-winded before 2024-10-20 21:50:48 -04:00
31635a8015 Improve syntax for ASH command to match other new commands 2024-10-16 11:21:16 -04:00
cde90c474d Bump workflow to use macos-latest 2024-10-13 22:16:39 -04:00
4636deeb0a Added wad retail2dev and wad dev2retail to convert dev and retail WADs back and forth 2024-10-13 22:07:02 -04:00
23 changed files with 1063 additions and 610 deletions

View File

@@ -1,5 +1,4 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python # This workflow will install Python dependencies and then build WiiPy for all platforms
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
name: Python application name: Python application
@@ -21,17 +20,16 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install ccache for Nuitka - name: Install ccache for Nuitka
run: sudo apt update && sudo apt install -y ccache libicu70 run: sudo apt update && sudo apt install -y ccache libicu70
- name: Set up Python 3.11 - name: Set up Python 3.12
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.12"
- name: Install Dependencies - name: Install Dependencies
run: | run: |
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: | run: make all
python -m nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py -o wiipy
- name: Prepare Package for Upload - name: Prepare Package for Upload
run: | run: |
mv wiipy ~/wiipy mv wiipy ~/wiipy
@@ -41,25 +39,24 @@ 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-12 runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Python 3.11 - name: Set up Python 3.12
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.12"
- name: Install Dependencies - name: Install Dependencies
run: | run: |
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: | run: ARCH_FLAGS=--macos-target-arch=x86_64 make all
python -m nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py -o wiipy
- name: Prepare Package for Upload - name: Prepare Package for Upload
run: | run: |
mv wiipy ~/wiipy mv wiipy ~/wiipy
@@ -69,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:
@@ -79,19 +103,18 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Enable Developer Command Prompt - name: Enable Developer Command Prompt
uses: ilammy/msvc-dev-cmd@v1.13.0 uses: ilammy/msvc-dev-cmd@v1.13.0
- name: Set up Python 3.11 - name: Set up Python 3.12
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.12"
- name: Install Dependencies - name: Install Dependencies
run: | run: |
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: | run: .\Build.ps1
python -m nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py
- name: Upload Application - name: Upload Application
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

32
Build.ps1 Normal file
View File

@@ -0,0 +1,32 @@
# Build.ps1 for WiiPy
# Default option is to run build, like a Makefile
param(
[string]$Task = "build"
)
$buildWiiPy = {
Write-Host "Building WiiPy..."
python -m nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py --onefile-tempdir-spec="{CACHE_DIR}/NinjaCheetah/WiiPy"
}
$cleanWiiPy = {
Write-Host "Cleaning..."
Remove-Item -Recurse -Force wiipy.exe, ./wiipy.build/, ./wiipy.dist/, ./wiipy.onefile-build/
}
switch ($Task.ToLower()) {
"build" {
& $buildWiiPy
break
}
"clean" {
& $cleanWiiPy
break
}
default {
Write-Host "Unknown task: $Task" -ForegroundColor Red
Write-Host "Available tasks: build, clean"
break
}
}

View File

@@ -1,9 +1,10 @@
CC=python -m nuitka CC=python -m nuitka
ARCH_FLAGS?=
linux: all:
$(CC) --show-progress --assume-yes-for-downloads --onefile wiipy.py -o wiipy $(CC) --show-progress --assume-yes-for-downloads --onefile wiipy.py --onefile-tempdir-spec="{CACHE_DIR}/NinjaCheetah/WiiPy" $(ARCH_FLAGS) -o wiipy
linux-install: install:
install wiipy /usr/bin/ install wiipy /usr/bin/
clean: clean:

View File

@@ -18,7 +18,7 @@ pip install -r requirements.txt
## Usage ## Usage
Basic usage for WiiPy is very simple. Basic usage for WiiPy is very simple.
```shell ```shell
python3 wiipy.py <subcommand> python3 wiipy.py <command>
``` ```
You can use `--help` to see a list of all commands, or use `<command> --help` to see usage instructions for that command. This also applies to subcommands, with the syntax `<command> <subcommand> --help`. You can use `--help` to see a list of all commands, or use `<command> --help` to see usage instructions for that command. This also applies to subcommands, with the syntax `<command> <subcommand> --help`.
@@ -26,24 +26,27 @@ Available subcommands will expand as support for more features are added into li
## Compiling ## Compiling
If you'd like to compile WiiPy from Python into something a little more native, you can use one of the following commands. Make sure you've installed Nuitka first (it's included in `requirements.txt`). If you'd like to compile WiiPy from Python into something a little more native, you can use one of the following commands. Make sure that you're in your venv, and that you've installed Nuitka first (it's included in `requirements.txt`).
```shell
python -m nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py
```
On macOS and Linux, this will give you a binary named `wiipy.bin` in the same directory as the Python file. On Windows, you'll get a binary named `wiipy.exe` instead.
### For Linux Users:
A Makefile has been included to both build and install WiiPy on Linux. This will install `wiipy` to `/usr/bin/`.
First, use make to build WiiPy (this automates the step above): ### Linux and macOS
A Makefile is available to both build and install WiiPy on Linux and macOS. This will give you an executable named `wiipy` in the root of the project directory.
```shell ```shell
make linux make all
``` ```
Then, run the install command with `sudo` (or your favorite alternative): Optionally, you can install WiiPy so that it's available system-wide. This will install it into `/usr/bin/`.
```shell ```shell
sudo make linux-install sudo make install
``` ```
### Windows
On Windows, you can use the PowerShell script `Build.ps1` in place of the Makefile. This will give you an executable mamed `wiipy.exe` in the root of the project directory.
```shell
.\Build.ps1
```
### A Note About Scripts ### A Note About Scripts
WiiPy's source includes a directory named `scripts`, which is currently where miscellaneous libWiiPy-based scripts live. Note that they are not part of WiiPy releases, and are not tested the same way the WiiPy is. They are simply here for those who may find them useful. WiiPy's source includes a directory named `scripts`, which is currently where miscellaneous libWiiPy-based scripts live. Note that they are not part of WiiPy releases, and are not tested the same way the WiiPy is. They are simply here for those who may find them useful.

33
commands/archive/ash.py Normal file
View File

@@ -0,0 +1,33 @@
# "commands/archive/ash.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import pathlib
import libWiiPy
from modules.core import fatal_error
def handle_ash_compress(args):
print("Compression is not implemented yet.")
def handle_ash_decompress(args):
input_path = pathlib.Path(args.input)
if args.output is not None:
output_path = pathlib.Path(args.output)
else:
output_path = pathlib.Path(input_path.name + ".arc")
# These default to 9 and 11, respectively, so we can always read them.
sym_tree_bits = args.sym_bits
dist_tree_bits = args.dist_bits
if not input_path.exists():
fatal_error(f"The specified ASH file \"{input_path}\" does not exist!")
ash_data = input_path.read_bytes()
# Decompress ASH file using the provided symbol/distance tree widths.
ash_decompressed = libWiiPy.archive.decompress_ash(ash_data, sym_tree_bits=sym_tree_bits,
dist_tree_bits=dist_tree_bits)
output_path.write_bytes(ash_decompressed)
print("ASH file decompressed!")

65
commands/archive/theme.py Normal file
View File

@@ -0,0 +1,65 @@
# "commands/archive/theme.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import configparser
import pathlib
import shutil
import tempfile
import zipfile
import libWiiPy
from modules.core import fatal_error
def handle_apply_mym(args):
mym_path = pathlib.Path(args.mym)
base_path = pathlib.Path(args.base)
output_path = pathlib.Path(args.output)
if not mym_path.exists():
fatal_error(f"The specified MYM file \"{mym_path}\" does not exist!")
if not base_path.exists():
fatal_error(f"The specified base file \"{base_path}\" does not exist!")
if output_path.suffix != ".csm":
output_path = output_path.with_suffix(".csm")
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_path = pathlib.Path(tmp_dir)
# Extract the MYM file into the temp directory.
# MYM files are just ZIP files, so if zipfile doesn't accept it then it can't be valid.
try:
with zipfile.ZipFile(mym_path) as mym:
mym.extractall(tmp_path.joinpath("mym_out"))
except zipfile.BadZipfile:
fatal_error("The provided MYM theme is not valid!")
mym_tmp_path = pathlib.Path(tmp_path.joinpath("mym_out"))
# Extract the asset archive into the temp directory.
try:
libWiiPy.archive.extract_u8(base_path.read_bytes(), str(tmp_path.joinpath("base_out")))
except ValueError:
fatal_error("The provided base assets are not valid!")
base_temp_path = pathlib.Path(tmp_path.joinpath("base_out"))
# Parse the mym.ini file in the root of the extracted MYM file.
mym_ini = configparser.ConfigParser()
if not mym_tmp_path.joinpath("mym.ini").exists():
fatal_error("mym.ini could not be found in the theme! The provided theme is not valid.")
mym_ini.read(mym_tmp_path.joinpath("mym.ini"))
# Iterate over every key in the ini file and apply the theme based the source and target of each key.
for section in mym_ini.sections():
# Build the source path by adjusting the path in the ini file.
source_file = mym_tmp_path
for piece in mym_ini[section]["source"].replace("\\", "/").split("/"):
source_file = source_file.joinpath(piece)
# Check that this source file is actually valid, and error out if it isn't.
if not source_file.exists():
fatal_error(f"A source file specified in mym.ini, \"{mym_ini[section]['source']}\", does not exist! "
f"The provided theme is not valid.")
# Build the target path the same way.
target_file = base_temp_path
for piece in mym_ini[section]["file"].replace("\\", "/").split("/"):
target_file = target_file.joinpath(piece)
# Move the source file into place over the target file.
shutil.move(source_file, target_file)
# Repack the now-themed asset archive and write it out.
output_path.write_bytes(libWiiPy.archive.pack_u8(base_temp_path))
print(f"Applied theme \"{mym_path.name}\" to \"{output_path.name}\"!")

33
commands/archive/u8.py Normal file
View File

@@ -0,0 +1,33 @@
# "commands/archive/u8.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import pathlib
import libWiiPy
from modules.core import fatal_error
def handle_u8_pack(args):
input_path = pathlib.Path(args.input)
output_path = pathlib.Path(args.output)
u8_data = None
try:
u8_data = libWiiPy.archive.pack_u8(input_path)
except ValueError:
fatal_error(f"The specified input file/folder \"{input_path}\" does not exist!")
output_path.write_bytes(u8_data)
print("U8 archive packed!")
def handle_u8_unpack(args):
input_path = pathlib.Path(args.input)
output_path = pathlib.Path(args.output)
if not input_path.exists():
fatal_error(f"The specified input file \"{input_path}\" does not exist!")
# Output path is deliberately not checked in any way because libWiiPy already has those checks, and it's easier
# and cleaner to only have one component doing all the checks.
libWiiPy.archive.extract_u8(input_path.read_bytes(), str(output_path))
print("U8 archive unpacked!")

View File

@@ -1,8 +1,9 @@
# "modules/nand/emunand.py" from WiiPy by NinjaCheetah # "commands/nand/emunand.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy # https://github.com/NinjaCheetah/WiiPy
import pathlib import pathlib
import libWiiPy import libWiiPy
from modules.core import fatal_error
def handle_emunand_title(args): def handle_emunand_title(args):
@@ -17,12 +18,12 @@ def handle_emunand_title(args):
input_path = pathlib.Path(args.install) input_path = pathlib.Path(args.install)
if not input_path.exists(): if not input_path.exists():
raise FileNotFoundError(input_path) fatal_error("The specified WAD file does not exist!")
if input_path.is_dir(): if input_path.is_dir():
wad_files = list(input_path.glob("*.[wW][aA][dD]")) wad_files = list(input_path.glob("*.[wW][aA][dD]"))
if not wad_files: if not wad_files:
raise FileNotFoundError("No WAD files were found in the provided input directory!") fatal_error("No WAD files were found in the provided input directory!")
wad_count = 0 wad_count = 0
for wad in wad_files: for wad in wad_files:
title = libWiiPy.title.Title() title = libWiiPy.title.Title()
@@ -50,7 +51,7 @@ def handle_emunand_title(args):
target_tid = input_str target_tid = input_str
if len(target_tid) != 16: if len(target_tid) != 16:
raise ValueError("Invalid Title ID! Title IDs must be 16 characters long.") fatal_error("The provided Title ID is invalid! Title IDs must be 16 characters long.")
emunand.uninstall_title(target_tid) emunand.uninstall_title(target_tid)

View File

@@ -1,8 +1,9 @@
# "modules/nand/setting.py" from WiiPy by NinjaCheetah # "commands/nand/setting.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy # https://github.com/NinjaCheetah/WiiPy
import pathlib import pathlib
import libWiiPy import libWiiPy
from modules.core import fatal_error
def handle_setting_decrypt(args): def handle_setting_decrypt(args):
@@ -13,13 +14,13 @@ def handle_setting_decrypt(args):
output_path = pathlib.Path(input_path.stem + "_dec" + input_path.suffix) output_path = pathlib.Path(input_path.stem + "_dec" + input_path.suffix)
if not input_path.exists(): if not input_path.exists():
raise FileNotFoundError(input_path) fatal_error("The specified setting file does not exist!")
# Load and decrypt the provided file. # Load and decrypt the provided file.
setting = libWiiPy.nand.SettingTxt() setting = libWiiPy.nand.SettingTxt()
setting.load(open(input_path, "rb").read()) setting.load(input_path.read_bytes())
# Write out the decrypted data. # Write out the decrypted data.
open(output_path, "w").write(setting.dump_decrypted()) output_path.write_text(setting.dump_decrypted())
print("Successfully decrypted setting.txt!") print("Successfully decrypted setting.txt!")
@@ -31,24 +32,24 @@ def handle_setting_encrypt(args):
output_path = pathlib.Path("setting.txt") output_path = pathlib.Path("setting.txt")
if not input_path.exists(): if not input_path.exists():
raise FileNotFoundError(input_path) fatal_error("The specified setting file does not exist!")
# Load and encrypt the provided file. # Load and encrypt the provided file.
setting = libWiiPy.nand.SettingTxt() setting = libWiiPy.nand.SettingTxt()
setting.load_decrypted(open(input_path, "r").read()) setting.load_decrypted(input_path.read_text())
# Write out the encrypted data. # Write out the encrypted data.
open(output_path, "wb").write(setting.dump()) output_path.write_bytes(setting.dump())
print("Successfully encrypted setting.txt!") print("Successfully encrypted setting.txt!")
def handle_setting_gen(args): def handle_setting_gen(args):
# Validate the provided SN. It should be 2 or 3 letters followed by 9 numbers. # Validate the provided SN. It should be 2 or 3 letters followed by 9 numbers.
if len(args.serno) != 11 and len(args.serno) != 12: if len(args.serno) != 11 and len(args.serno) != 12:
raise ValueError("The provided Serial Number is not valid!") fatal_error("The provided Serial Number is not valid!")
try: try:
int(args.serno[-9:]) int(args.serno[-9:])
except ValueError: except ValueError:
raise ValueError("The provided Serial Number is not valid!") fatal_error("The provided Serial Number is not valid!")
prefix = args.serno[:-9] prefix = args.serno[:-9]
# Detect the console revision based on the SN. # Detect the console revision based on the SN.
match prefix[0].upper(): match prefix[0].upper():
@@ -64,11 +65,13 @@ def handle_setting_gen(args):
# of 11 characters, while other regions have a three-letter prefix for a total length of 12 characters. # of 11 characters, while other regions have a three-letter prefix for a total length of 12 characters.
valid_regions = ["USA", "EUR", "JPN", "KOR"] valid_regions = ["USA", "EUR", "JPN", "KOR"]
if args.region not in valid_regions: if args.region not in valid_regions:
raise ValueError("The provided region is not valid!") fatal_error(f"The provided region \"{args.region}\" is not valid!")
if len(prefix) == 2 and args.region != "USA": if len(prefix) == 2 and args.region != "USA":
raise ValueError("The provided region does not match the provided Serial Number!") fatal_error(f"The provided region \"{args.region}\" does not match the provided Serial Number "
f"\"{args.serno}\"!\"")
elif len(prefix) == 3 and args.region == "USA": elif len(prefix) == 3 and args.region == "USA":
raise ValueError("The provided region does not match the provided Serial Number!") fatal_error(f"The provided region \"{args.region}\" does not match the provided Serial Number "
f"\"{args.serno}\"!\"")
# Get the values for VIDEO and GAME. # Get the values for VIDEO and GAME.
video = "" video = ""
game = "" game = ""

View File

@@ -1,4 +1,4 @@
# "modules/title/ciosbuild.py" from WiiPy by NinjaCheetah # "commands/title/ciosbuild.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy # https://github.com/NinjaCheetah/WiiPy
import io import io
@@ -6,6 +6,7 @@ import os
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import pathlib import pathlib
import libWiiPy import libWiiPy
from modules.core import fatal_error
def build_cios(args): def build_cios(args):
@@ -18,11 +19,11 @@ def build_cios(args):
output_path = pathlib.Path(args.output) output_path = pathlib.Path(args.output)
if not base_path.exists(): if not base_path.exists():
raise FileNotFoundError(base_path) fatal_error(f"The specified base IOS file \"{base_path}\" does not exist!")
if not map_path.exists(): if not map_path.exists():
raise FileNotFoundError(map_path) fatal_error(f"The specified cIOS map file \"{map_path}\" does not exist!")
if not modules_path.exists(): if not modules_path.exists():
raise FileNotFoundError(modules_path) fatal_error(f"The specified cIOS modules directory \"{modules_path}\" does not exist!")
title = libWiiPy.title.Title() title = libWiiPy.title.Title()
title.load_wad(open(base_path, 'rb').read()) title.load_wad(open(base_path, 'rb').read())
@@ -39,7 +40,7 @@ def build_cios(args):
target_cios = child target_cios = child
break break
if target_cios is None: if target_cios is None:
raise ValueError("The target cIOS could not be found in the provided map!") fatal_error(f"The target cIOS \"{args.cios_ver}\" 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 # 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. # that the version of the base in the map matches the version of the IOS WAD.
@@ -51,13 +52,16 @@ def build_cios(args):
target_base = child target_base = child
break break
if target_base is None: if target_base is None:
raise ValueError("The provided base IOS doesn't match any bases found in the provided map!") fatal_error(f"The provided base (IOS{provided_base}) doesn't match any bases found in the provided map!")
base_version = int(target_base.get("version")) base_version = int(target_base.get("version"))
if title.tmd.title_version != base_version: if title.tmd.title_version != base_version:
raise ValueError("The provided base IOS does not match the required version for this base!") fatal_error(f"The provided base (IOS{provided_base} v{title.tmd.title_version}) doesn't match the required "
f"version (v{base_version})!")
print(f"Building cIOS \"{args.cios_ver}\" from base IOS{target_base.get('ios')} v{base_version}...")
# We're ready to begin building the cIOS now. Find all the <content> tags that have <patch> tags, and then apply # We're ready to begin building the cIOS now. Find all the <content> tags that have <patch> tags, and then apply
# the patches listed in them to the content. # the patches listed in them to the content.
print("Patching existing modules...")
for content in target_base.findall("content"): for content in target_base.findall("content"):
patches = content.findall("patch") patches = content.findall("patch")
if patches: if patches:
@@ -83,24 +87,34 @@ def build_cios(args):
content_data.seek(offset) content_data.seek(offset)
content_data.write(new_data) content_data.write(new_data)
else: else:
raise Exception("An error occurred while patching! Please make sure your base IOS is valid.") fatal_error("An error occurred while patching! Please make sure your base IOS is valid.")
content_data.seek(0x0) content_data.seek(0x0)
dec_content = content_data.read() dec_content = content_data.read()
# Set the content in the title to the newly-patched content, and set the type to normal. # 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) 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. # Next phase of cIOS building is to add the required extra modules.
print("Adding required additional modules...")
for content in target_base.findall("content"): for content in target_base.findall("content"):
target_module = content.get("module") target_module = content.get("module")
if target_module is not None: if target_module is not None:
target_index = int(content.get("tmdmoduleid"), 16)
# The cIOS map supplies a Content ID to use for each additional module. # The cIOS map supplies a Content ID to use for each additional module.
cid = int(content.get("id")[-2:], 16) cid = int(content.get("id"), 16)
target_path = modules_path.joinpath(target_module + ".app") target_path = modules_path.joinpath(target_module + ".app")
if target_path.exists(): if not target_path.exists():
new_module = open(target_path, "rb").read() fatal_error(f"A required module \"{target_module}\" could not be found!")
# Check where this module belongs. If it's -1, add it to the end. If it's any other value, this module needs
# to go at the index specified.
new_module = target_path.read_bytes()
if target_index == -1:
title.add_content(new_module, cid, libWiiPy.title.ContentType.NORMAL) title.add_content(new_module, cid, libWiiPy.title.ContentType.NORMAL)
else: else:
raise Exception(f"A required module \"{target_module}.app\" could not be found!") existing_module = title.get_content_by_index(target_index)
existing_cid = title.content.content_records[target_index].content_id
existing_type = title.content.content_records[target_index].content_type
title.set_content(new_module, target_index, cid, libWiiPy.title.ContentType.NORMAL)
title.add_content(existing_module, existing_cid, existing_type)
# Last cIOS building step, we need to set the slot and version. # Last cIOS building step, we need to set the slot and version.
slot = args.slot slot = args.slot
@@ -108,18 +122,24 @@ def build_cios(args):
tid = title.tmd.title_id[:-2] + f"{slot:02X}" tid = title.tmd.title_id[:-2] + f"{slot:02X}"
title.set_title_id(tid) title.set_title_id(tid)
else: else:
raise ValueError(f"The provided slot \"{slot}\" is not valid!") fatal_error(f"The specified slot \"{slot}\" is not valid!")
try: try:
title.set_title_version(args.version) title.set_title_version(args.version)
except ValueError: except ValueError:
raise ValueError(f"The provided version \"{args.version}\" is not valid!") fatal_error(f"The specified version \"{args.version}\" is not valid!")
print(f"Set cIOS slot to \"{slot}\" and cIOS version to \"{args.version}\"!")
# If this is a vWii cIOS, then we need to re-encrypt it with the Wii Common key so that it's installable from
# within Wii mode.
title_key_dec = title.ticket.get_title_key()
title_key_common = libWiiPy.title.encrypt_title_key(title_key_dec, 0, title.tmd.title_id)
title.ticket.title_key_enc = title_key_common
title.ticket.common_key_index = 0
# Ensure the WAD is fakesigned. # Ensure the WAD is fakesigned.
title.fakesign() title.fakesign()
# Write the new cIOS to the specified output path. # Write the new cIOS to the specified output path.
out_file = open(output_path, "wb") output_path.write_bytes(title.dump_wad())
out_file.write(title.dump_wad())
out_file.close()
print("success") print(f"Successfully built cIOS \"{args.cios_ver}\"!")

View File

@@ -1,8 +1,9 @@
# "modules/title/fakesign.py" from WiiPy by NinjaCheetah # "commands/title/fakesign.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy # https://github.com/NinjaCheetah/WiiPy
import pathlib import pathlib
import libWiiPy import libWiiPy
from modules.core import fatal_error
def handle_fakesign(args): def handle_fakesign(args):
@@ -13,25 +14,25 @@ def handle_fakesign(args):
output_path = pathlib.Path(args.input) output_path = pathlib.Path(args.input)
if not input_path.exists(): if not input_path.exists():
raise FileNotFoundError(input_path) fatal_error(f"The specified input file \"{input_path}\" does not exist!")
if input_path.suffix.lower() == ".tmd": if input_path.suffix.lower() == ".tmd":
tmd = libWiiPy.title.TMD() tmd = libWiiPy.title.TMD()
tmd.load(open(input_path, "rb").read()) tmd.load(input_path.read_bytes())
tmd.fakesign() tmd.fakesign()
open(output_path, "wb").write(tmd.dump()) output_path.write_bytes(tmd.dump())
print("TMD fakesigned successfully!") print("TMD fakesigned successfully!")
elif input_path.suffix.lower() == ".tik": elif input_path.suffix.lower() == ".tik":
tik = libWiiPy.title.Ticket() tik = libWiiPy.title.Ticket()
tik.load(open(input_path, "rb").read()) tik.load(input_path.read_bytes())
tik.fakesign() tik.fakesign()
open(output_path, "wb").write(tik.dump()) output_path.write_bytes(tik.dump())
print("Ticket fakesigned successfully!") print("Ticket fakesigned successfully!")
elif input_path.suffix.lower() == ".wad": elif input_path.suffix.lower() == ".wad":
title = libWiiPy.title.Title() title = libWiiPy.title.Title()
title.load_wad(open(input_path, "rb").read()) title.load_wad(input_path.read_bytes())
title.fakesign() title.fakesign()
open(output_path, "wb").write(title.dump_wad()) output_path.write_bytes(title.dump_wad())
print("WAD fakesigned successfully!") print("WAD fakesigned successfully!")
else: else:
raise TypeError("This does not appear to be a TMD, Ticket, or WAD! Cannot fakesign.") fatal_error("The provided file does not appear to be a TMD, Ticket, or WAD and cannot be fakesigned!")

View File

@@ -1,15 +1,19 @@
# "modules/title/info.py" from WiiPy by NinjaCheetah # "commands/title/info.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy # https://github.com/NinjaCheetah/WiiPy
import pathlib import pathlib
import binascii import binascii
import libWiiPy import libWiiPy
from modules.core import fatal_error
def _print_tmd_info(tmd: libWiiPy.title.TMD): def _print_tmd_info(tmd: libWiiPy.title.TMD):
# Get all important keys from the TMD and print them out nicely. # Get all important keys from the TMD and print them out nicely.
print("Title Info") print("Title Info")
print(f" Title ID: {tmd.title_id}") try:
print(f" Title ID: {tmd.title_id.upper()} ({str(bytes.fromhex(tmd.title_id[8:]).decode()).upper()})")
except UnicodeDecodeError:
print(f" Title ID: {tmd.title_id.upper()}")
# This type of version number really only applies to the System Menu and IOS. # This type of version number really only applies to the System Menu and IOS.
if tmd.title_id[:8] == "00000001": if tmd.title_id[:8] == "00000001":
print(f" Title Version: {tmd.title_version} ({tmd.title_version_converted})") print(f" Title Version: {tmd.title_version} ({tmd.title_version_converted})")
@@ -20,7 +24,7 @@ def _print_tmd_info(tmd: libWiiPy.title.TMD):
if tmd.ios_tid == "0000000000000000": if tmd.ios_tid == "0000000000000000":
print(f" Required IOS: N/A") print(f" Required IOS: N/A")
else: else:
print(f" Required IOS: IOS{int(tmd.ios_tid[-2:], 16)} ({tmd.ios_tid})") print(f" Required IOS: IOS{int(tmd.ios_tid[-2:], 16)} ({tmd.ios_tid.upper()})")
if tmd.signature_issuer.find("CP00000004") != -1: if tmd.signature_issuer.find("CP00000004") != -1:
print(f" Certificate: CP00000004 (Retail)") print(f" Certificate: CP00000004 (Retail)")
print(f" Certificate Issuer: Root-CA00000001 (Retail)") print(f" Certificate Issuer: Root-CA00000001 (Retail)")
@@ -44,7 +48,7 @@ def _print_tmd_info(tmd: libWiiPy.title.TMD):
region = "KOR" region = "KOR"
case _: case _:
region = "None" region = "None"
elif tmd.title_id[:8] == "00000001": elif tmd.get_title_type() == "System":
region = "None" region = "None"
else: else:
region = tmd.get_title_region() region = tmd.get_title_region()
@@ -70,7 +74,11 @@ def _print_tmd_info(tmd: libWiiPy.title.TMD):
def _print_ticket_info(ticket: libWiiPy.title.Ticket): def _print_ticket_info(ticket: libWiiPy.title.Ticket):
# Get all important keys from the TMD and print them out nicely. # Get all important keys from the TMD and print them out nicely.
print(f"Ticket Info") print(f"Ticket Info")
print(f" Title ID: {ticket.title_id.decode()}") try:
print(f" Title ID: {ticket.title_id.decode().upper()} "
f"({str(bytes.fromhex(ticket.title_id.decode()[8:]).decode()).upper()})")
except UnicodeDecodeError:
print(f" Title ID: {ticket.title_id.decode().upper()}")
# This type of version number really only applies to the System Menu and IOS. # This type of version number really only applies to the System Menu and IOS.
if ticket.title_id.decode()[:8] == "00000001": if ticket.title_id.decode()[:8] == "00000001":
print(f" Title Version: {ticket.title_version} " print(f" Title Version: {ticket.title_version} "
@@ -88,14 +96,17 @@ def _print_ticket_info(ticket: libWiiPy.title.Ticket):
print(f" Certificate Info: {ticket.signature_issuer} (Unknown)") print(f" Certificate Info: {ticket.signature_issuer} (Unknown)")
match ticket.common_key_index: match ticket.common_key_index:
case 0: case 0:
key = "Common" if ticket.is_dev:
key = "Common (Development)"
else:
key = "Common (Retail)"
case 1: case 1:
key = "Korean" key = "Korean"
case 2: case 2:
key = "vWii" key = "vWii"
case _: case _:
key = "Unknown (Likely Common)" key = "Unknown (Likely Common)"
print(f" Common Key: {key}") print(f" Decryption Key: {key}")
print(f" Title Key (Encrypted): {binascii.hexlify(ticket.title_key_enc).decode()}") print(f" Title Key (Encrypted): {binascii.hexlify(ticket.title_key_enc).decode()}")
print(f" Title Key (Decrypted): {binascii.hexlify(ticket.get_title_key()).decode()}") print(f" Title Key (Decrypted): {binascii.hexlify(ticket.get_title_key()).decode()}")
@@ -133,7 +144,7 @@ def handle_info(args):
input_path = pathlib.Path(args.input) input_path = pathlib.Path(args.input)
if not input_path.exists(): if not input_path.exists():
raise FileNotFoundError(input_path) fatal_error(f"The specified input file \"{input_path}\" does not exist!")
if input_path.suffix.lower() == ".tmd": if input_path.suffix.lower() == ".tmd":
tmd = libWiiPy.title.TMD() tmd = libWiiPy.title.TMD()
@@ -148,4 +159,4 @@ def handle_info(args):
title.load_wad(open(input_path, "rb").read()) title.load_wad(open(input_path, "rb").read())
_print_wad_info(title) _print_wad_info(title)
else: else:
raise TypeError("This does not appear to be a TMD, Ticket, or WAD! No info can be provided.") fatal_error("This does not appear to be a TMD, Ticket, or WAD! No information can be provided.")

View File

@@ -1,8 +1,9 @@
# "modules/title/iospatcher.py" from WiiPy by NinjaCheetah # "commands/title/iospatcher.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy # https://github.com/NinjaCheetah/WiiPy
import pathlib import pathlib
import libWiiPy import libWiiPy
from modules.core import fatal_error
def _patch_fakesigning(ios_patcher: libWiiPy.title.IOSPatcher) -> int: def _patch_fakesigning(ios_patcher: libWiiPy.title.IOSPatcher) -> int:
@@ -59,15 +60,20 @@ def _patch_drive_inquiry(ios_patcher: libWiiPy.title.IOSPatcher) -> int:
def handle_iospatch(args): def handle_iospatch(args):
input_path = pathlib.Path(args.input) 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)
if not input_path.exists(): if not input_path.exists():
raise FileNotFoundError(input_path) fatal_error(f"The specified IOS file \"{input_path}\" does not exist!")
title = libWiiPy.title.Title() title = libWiiPy.title.Title()
title.load_wad(open(input_path, "rb").read()) title.load_wad(input_path.read_bytes())
tid = title.tmd.title_id tid = title.tmd.title_id
if tid[:8] != "00000001" or tid[8:] == "00000001" or tid[8:] == "00000002": if tid[:8] != "00000001" or tid[8:] == "00000001" or tid[8:] == "00000002":
raise ValueError("This WAD does not appear to contain an IOS! Patching cannot continue.") fatal_error(f"The provided WAD does not appear to contain an IOS! No patches can be applied.")
patch_count = 0 patch_count = 0
@@ -105,8 +111,8 @@ def handle_iospatch(args):
print(f"\nTotal patches applied: {patch_count}") print(f"\nTotal patches applied: {patch_count}")
if patch_count == 0 and args.version is None and args.slot is None: if patch_count == 0 and args.version is None and args.slot is None:
raise ValueError("No patches were applied! Please select patches to apply, and ensure that selected patches are" fatal_error("No patches were applied! Please specify patches to apply, and ensure that selected patches are "
" compatible with this IOS.") "compatible with this IOS.")
if patch_count > 0 or args.version is not None or args.slot is not None: if patch_count > 0 or args.version is not None or args.slot is not None:
# Set patched content to non-shared if that argument was passed. # Set patched content to non-shared if that argument was passed.
@@ -116,14 +122,6 @@ def handle_iospatch(args):
ios_patcher.title.content.content_records[ios_patcher.dip_module_index].content_type = 1 ios_patcher.title.content.content_records[ios_patcher.dip_module_index].content_type = 1
ios_patcher.title.fakesign() # Signature is broken anyway, so fakesign for maximum installation openings ios_patcher.title.fakesign() # Signature is broken anyway, so fakesign for maximum installation openings
if args.output is not None: output_path.write_bytes(ios_patcher.title.dump_wad())
output_path = pathlib.Path(args.output)
output_file = open(output_path, "wb")
output_file.write(ios_patcher.title.dump_wad())
output_file.close()
else:
output_file = open(input_path, "wb")
output_file.write(ios_patcher.title.dump_wad())
output_file.close()
print("IOS successfully patched!") print("IOS successfully patched!")

View File

@@ -1,11 +1,11 @@
# "modules/title/nus.py" from WiiPy by NinjaCheetah # "commands/title/nus.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy # https://github.com/NinjaCheetah/WiiPy
import os
import hashlib import hashlib
import pathlib import pathlib
import binascii import binascii
import libWiiPy import libWiiPy
from modules.core import fatal_error
def handle_nus_title(args): def handle_nus_title(args):
@@ -24,8 +24,7 @@ def handle_nus_title(args):
try: try:
title_version = int(args.version) title_version = int(args.version)
except ValueError: except ValueError:
print("Enter a valid integer for the Title Version.") fatal_error("The specified Title Version must be a valid integer!")
return
# If --wad was passed, check to make sure the path is okay. # If --wad was passed, check to make sure the path is okay.
if args.wad is not None: if args.wad is not None:
@@ -37,12 +36,10 @@ def handle_nus_title(args):
if args.output is not None: if args.output is not None:
output_dir = pathlib.Path(args.output) output_dir = pathlib.Path(args.output)
if output_dir.exists(): if output_dir.exists():
if output_dir.is_dir() and next(os.scandir(output_dir), None): if output_dir.is_file():
raise ValueError("Output folder is not empty!") fatal_error("A file already exists with the provided directory name!")
elif output_dir.is_file():
raise ValueError("A file already exists with the provided directory name!")
else: else:
os.mkdir(output_dir) output_dir.mkdir()
# Download the title from the NUS. This is done "manually" (as opposed to using download_title()) so that we can # Download the title from the NUS. This is done "manually" (as opposed to using download_title()) so that we can
# provide verbose output. # provide verbose output.
@@ -62,9 +59,7 @@ def handle_nus_title(args):
title_version = title.tmd.title_version title_version = title.tmd.title_version
# Write out the TMD to a file. # Write out the TMD to a file.
if output_dir is not None: if output_dir is not None:
tmd_out = open(output_dir.joinpath("tmd." + str(title_version)), "wb") output_dir.joinpath("tmd." + str(title_version)).write_bytes(title.tmd.dump())
tmd_out.write(title.tmd.dump())
tmd_out.close()
# Download the ticket, if we can. # Download the ticket, if we can.
print(" - Downloading and parsing Ticket...") print(" - Downloading and parsing Ticket...")
@@ -72,16 +67,13 @@ def handle_nus_title(args):
title.load_ticket(libWiiPy.title.download_ticket(tid, wiiu_endpoint=wiiu_nus_enabled)) title.load_ticket(libWiiPy.title.download_ticket(tid, wiiu_endpoint=wiiu_nus_enabled))
can_decrypt = True can_decrypt = True
if output_dir is not None: if output_dir is not None:
ticket_out = open(output_dir.joinpath("tik"), "wb") output_dir.joinpath("tik").write_bytes(title.ticket.dump())
ticket_out.write(title.ticket.dump())
ticket_out.close()
except ValueError: except ValueError:
# If libWiiPy returns an error, then no ticket is available. Log this, and disable options requiring a # If libWiiPy returns an error, then no ticket is available. Log this, and disable options requiring a
# ticket so that they aren't attempted later. # ticket so that they aren't attempted later.
print(" - No Ticket is available!") print(" - No Ticket is available!")
if wad_file is not None and output_dir is None: if wad_file is not None and output_dir is None:
print("--wad was passed, but this title cannot be packed into a WAD!") fatal_error("--wad was passed, but this title has no common ticket and cannot be packed into a WAD!")
return
# Load the content records from the TMD, and begin iterating over the records. # Load the content records from the TMD, and begin iterating over the records.
title.load_content_records() title.load_content_records()
@@ -100,9 +92,7 @@ def handle_nus_title(args):
print(" - Done!") print(" - Done!")
# If we're supposed to be outputting to a folder, then write these files out. # If we're supposed to be outputting to a folder, then write these files out.
if output_dir is not None: if output_dir is not None:
enc_content_out = open(output_dir.joinpath(content_file_name), "wb") output_dir.joinpath(content_file_name).write_bytes(content_list[content])
enc_content_out.write(content_list[content])
enc_content_out.close()
title.content.content_list = content_list title.content.content_list = content_list
# Try to decrypt the contents for this title if a ticket was available. # Try to decrypt the contents for this title if a ticket was available.
@@ -113,9 +103,7 @@ def handle_nus_title(args):
" (Content ID: " + str(title.tmd.content_records[content].content_id) + ")...") " (Content ID: " + str(title.tmd.content_records[content].content_id) + ")...")
dec_content = title.get_content_by_index(content) dec_content = title.get_content_by_index(content)
content_file_name = f"{title.tmd.content_records[content].content_id:08X}".lower() + ".app" content_file_name = f"{title.tmd.content_records[content].content_id:08X}".lower() + ".app"
dec_content_out = open(output_dir.joinpath(content_file_name), "wb") output_dir.joinpath(content_file_name).write_bytes(dec_content)
dec_content_out.write(dec_content)
dec_content_out.close()
else: else:
print("Title has no Ticket, so content will not be decrypted!") print("Title has no Ticket, so content will not be decrypted!")
@@ -129,9 +117,7 @@ def handle_nus_title(args):
if wad_file.suffix != ".wad": if wad_file.suffix != ".wad":
wad_file = wad_file.with_suffix(".wad") wad_file = wad_file.with_suffix(".wad")
# Have libWiiPy dump the WAD, and write that data out. # Have libWiiPy dump the WAD, and write that data out.
file = open(wad_file, "wb") pathlib.Path(wad_file).write_bytes(title.dump_wad())
file.write(title.dump_wad())
file.close()
print("Downloaded title with Title ID \"" + args.tid + "\"!") print("Downloaded title with Title ID \"" + args.tid + "\"!")
@@ -140,7 +126,6 @@ def handle_nus_content(args):
tid = args.tid tid = args.tid
cid = args.cid cid = args.cid
version = args.version version = args.version
out = args.output
if args.decrypt: if args.decrypt:
decrypt_content = True decrypt_content = True
else: else:
@@ -148,43 +133,42 @@ def handle_nus_content(args):
# Only accepting the 000000xx format because it's the one that would be most commonly known, rather than using the # Only accepting the 000000xx format because it's the one that would be most commonly known, rather than using the
# actual integer that the hex Content ID translates to. # actual integer that the hex Content ID translates to.
content_id = None
try: try:
content_id = int.from_bytes(binascii.unhexlify(cid)) content_id = int.from_bytes(binascii.unhexlify(cid))
except binascii.Error: except binascii.Error:
print("Invalid Content ID! Content ID must be in format \"000000xx\"!") fatal_error("The provided Content ID is invalid! The Content ID must be in the format \"000000xx\"!")
return
# Use the supplied output path if one was specified, otherwise generate one using the Content ID. # Use the supplied output path if one was specified, otherwise generate one using the Content ID.
if out is None: if args.output is None:
content_file_name = f"{content_id:08X}".lower() content_file_name = f"{content_id:08X}".lower()
output_path = pathlib.Path(content_file_name) output_path = pathlib.Path(content_file_name)
else: else:
output_path = pathlib.Path(out) output_path = pathlib.Path(args.output)
# Ensure that a version was supplied before downloading, because we need the matching TMD for decryption to work.
if decrypt_content is True and version is None:
fatal_error("You must specify the version that the requested content belongs to for decryption!")
# Try to download the content, and catch the ValueError libWiiPy will throw if it can't be found. # Try to download the content, and catch the ValueError libWiiPy will throw if it can't be found.
print("Downloading content with Content ID " + cid + "...") print("Downloading content with Content ID " + cid + "...")
content_data = None
try: try:
content_data = libWiiPy.title.download_content(tid, content_id) content_data = libWiiPy.title.download_content(tid, content_id)
except ValueError: except ValueError:
print("The Title ID or Content ID you specified could not be found!") fatal_error("The specified Title ID or Content ID could not be found!")
return
if decrypt_content is True: if decrypt_content is True:
# Ensure that a version was supplied, because we need the matching TMD for decryption to work.
if version is None:
print("You must specify the version that the requested content belongs to for decryption!")
return
output_path = output_path.with_suffix(".app") output_path = output_path.with_suffix(".app")
tmd = libWiiPy.title.TMD() tmd = libWiiPy.title.TMD()
tmd.load(libWiiPy.title.download_tmd(tid, version)) tmd.load(libWiiPy.title.download_tmd(tid, version))
# Try to get a Ticket for the title, if a common one is available. # Try to get a Ticket for the title, if a common one is available.
ticket = None
try: try:
ticket = libWiiPy.title.Ticket() ticket = libWiiPy.title.Ticket()
ticket.load(libWiiPy.title.download_ticket(tid, wiiu_endpoint=True)) ticket.load(libWiiPy.title.download_ticket(tid, wiiu_endpoint=True))
except ValueError: except ValueError:
print("No Ticket is available! Content cannot be decrypted!") fatal_error("No Ticket is available! Content cannot be decrypted.")
return
content_hash = 'gggggggggggggggggggggggggggggggggggggggg' content_hash = 'gggggggggggggggggggggggggggggggggggggggg'
content_size = 0 content_size = 0
@@ -198,57 +182,52 @@ def handle_nus_content(args):
# If the default hash never changed, then a content record matching the downloaded content couldn't be found, # If the default hash never changed, then a content record matching the downloaded content couldn't be found,
# which most likely means that the wrong version was specified. # which most likely means that the wrong version was specified.
if content_hash == 'gggggggggggggggggggggggggggggggggggggggg': if content_hash == 'gggggggggggggggggggggggggggggggggggggggg':
print("Content was not found in the TMD from the specified version! Content cannot be decrypted!") fatal_error("Content was not found in the TMD for the specified version! Content cannot be decrypted.")
return
# Manually decrypt the content and verify its hash, which is what libWiiPy's get_content() methods do. We just # Manually decrypt the content and verify its hash, which is what libWiiPy's get_content() methods do. We just
# can't really use that here because that require setting up a lot more of the title than is necessary. # can't really use that here because that require setting up a lot more of the title than is necessary.
content_dec = libWiiPy.title.decrypt_content(content_data, ticket.get_title_key(), content_index, content_size) content_dec = libWiiPy.title.decrypt_content(content_data, ticket.get_title_key(), content_index, content_size)
content_dec_hash = hashlib.sha1(content_dec).hexdigest() content_dec_hash = hashlib.sha1(content_dec).hexdigest()
if content_hash != content_dec_hash: if content_hash != content_dec_hash:
raise ValueError("The decrypted content provided does not match the record at the provided index. \n" fatal_error("The decrypted content provided does not match the record at the provided index. \n"
"Expected hash is: {}\n".format(content_hash) + "Expected hash is: {}\n".format(content_hash) +
"Actual hash is: {}".format(content_dec_hash)) "Actual hash is: {}".format(content_dec_hash))
file = open(output_path, "wb") output_path.write_bytes(content_dec)
file.write(content_dec)
file.close()
else: else:
file = open(output_path, "wb") output_path.write_bytes(content_data)
file.write(content_data)
file.close()
print("Downloaded content with Content ID \"" + cid + "\"!") print(f"Downloaded content with Content ID \"{cid}\"!")
def handle_nus_tmd(args): def handle_nus_tmd(args):
tid = args.tid tid = args.tid
version = args.version
out = args.output
# Check if --version was passed, because it'll be None if it wasn't. # Check if --version was passed, because it'll be None if it wasn't.
version = None
if args.version is not None: if args.version is not None:
try: try:
title_version = int(args.version) version = int(args.version)
except ValueError: except ValueError:
print("Enter a valid integer for the Title Version.") fatal_error("The specified TMD version must be a valid integer!")
return
# Use the supplied output path if one was specified, otherwise generate one using the Title ID. # Use the supplied output path if one was specified, otherwise generate one using the Title ID. If a version has
if out is None: # been specified, append the version to the end of the path as well.
output_path = pathlib.Path(tid + ".tmd") if args.output is None:
if version is not None:
output_path = pathlib.Path(f"{tid}.tmd.{version}")
else: else:
output_path = pathlib.Path(out) output_path = pathlib.Path(f"{tid}.tmd")
else:
output_path = pathlib.Path(args.output)
# Try to download the TMD, and catch the ValueError libWiiPy will throw if it can't be found. # Try to download the TMD, and catch the ValueError libWiiPy will throw if it can't be found.
print("Downloading TMD for title " + tid + "...") print(f"Downloading TMD for title {tid}...")
tmd_data = None
try: try:
tmd_data = libWiiPy.title.download_tmd(tid, version) tmd_data = libWiiPy.title.download_tmd(tid, version)
except ValueError: except ValueError:
print("The Title ID or version you specified could not be found!") fatal_error("The specified Title ID or version could not be found!")
return
file = open(output_path, "wb") output_path.write_bytes(tmd_data)
file.write(tmd_data)
file.close()
print("Downloaded TMD for title \"" + tid + "\"!") print(f"Downloaded TMD for title \"{tid}\"!")

82
commands/title/tmd.py Normal file
View File

@@ -0,0 +1,82 @@
# "commands/title/tmd.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
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):
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)
if not input_path.exists():
fatal_error("The specified TMD files does not exist!")
tmd = libWiiPy.title.TMD()
tmd.load(input_path.read_bytes())
if args.index is not None:
# Make sure the target index exists, then remove it from the TMD.
if args.index >= len(tmd.content_records):
fatal_error("The specified index could not be found in the provided TMD!")
tmd.content_records.pop(args.index)
tmd.num_contents -= 1
# Auto fakesign because we've edited the TMD.
tmd.fakesign()
output_path.write_bytes(tmd.dump())
print(f"Removed content record at index {args.index}!")
elif args.cid is not None:
if len(args.cid) != 8:
fatal_error("The specified Content ID is invalid!")
target_cid = int(args.cid, 16)
# List Contents IDs in the title, and ensure that the target Content ID exists.
valid_ids = []
for record in tmd.content_records:
valid_ids.append(record.content_id)
if target_cid not in valid_ids:
fatal_error("The specified Content ID could not be found in the provided TMD!")
tmd.content_records.pop(valid_ids.index(target_cid))
tmd.num_contents -= 1
# Auto fakesign because we've edited the TMD.
tmd.fakesign()
output_path.write_bytes(tmd.dump())
print(f"Removed content record with Content ID \"{target_cid:08X}\"!")

402
commands/title/wad.py Normal file
View File

@@ -0,0 +1,402 @@
# "commands/title/wad.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
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):
input_path = pathlib.Path(args.input)
content_path = pathlib.Path(args.content)
if args.output is not None:
output_path = pathlib.Path(args.output)
else:
output_path = pathlib.Path(args.input)
if not input_path.exists():
fatal_error(f"The specified WAD file \"{input_path}\" does not exist!")
if not content_path.exists():
fatal_error(f"The specified content file \"{content_path}\" does not exist!")
title = libWiiPy.title.Title()
title.load_wad(input_path.read_bytes())
content_data = content_path.read_bytes()
# Prepare the CID so it's ready when we go to add this content to the WAD.
# We need to both validate that this is a real CID, and also that it isn't already taken by another content.
if args.cid is not None:
if len(args.cid) != 8:
fatal_error("The specified Content ID is not valid!")
target_cid = int(args.cid, 16)
for record in title.content.content_records:
if target_cid == record.content_id:
fatal_error("The specified Content ID is already in use by this title!")
print(f"Using provided Content ID \"{target_cid:08X}\".")
# If we weren't given a CID, then we need to randomly assign one, and ensure it isn't being used.
else:
used_cids = []
for record in title.content.content_records:
used_cids.append(record.content_id)
target_cid = randint(0, 0x000000FF)
while target_cid in used_cids:
target_cid = randint(0, 0x000000FF)
print(f"Using randomly assigned Content ID \"{target_cid:08X}\" since none were provided.")
# Get the type of the new content.
target_type = libWiiPy.title.ContentType.NORMAL
if args.type is not None:
match str.lower(args.type):
case "normal":
target_type = libWiiPy.title.ContentType.NORMAL
case "shared":
target_type = libWiiPy.title.ContentType.SHARED
case "dlc":
target_type = libWiiPy.title.ContentType.DLC
case _:
fatal_error(f"The provided content type \"{args.type}\" is not valid!")
# Call add_content to add our new content with the set parameters.
title.add_content(content_data, target_cid, target_type)
# Auto fakesign because we've edited the title.
title.fakesign()
output_path.write_bytes(title.dump_wad())
print(f"Successfully added new content with Content ID \"{target_cid:08X}\" and type \"{target_type.name}\"!")
def handle_wad_convert(args):
input_path = pathlib.Path(args.input)
target = None
if args.dev:
target = "development"
elif args.retail:
target = "retail"
elif args.vwii:
target = "vWii"
else:
fatal_error("No valid encryption target was specified!")
output_path = pathlib.Path(args.output)
if args.output is None:
match target:
case "development":
output_path = pathlib.Path(input_path.stem + "_dev" + input_path.suffix)
case "retail":
output_path = pathlib.Path(input_path.stem + "_retail" + input_path.suffix)
case "vWii":
output_path = pathlib.Path(input_path.stem + "_vWii" + input_path.suffix)
case _:
fatal_error("No valid encryption target was specified!")
if not input_path.exists():
fatal_error(f"The specified WAD file \"{input_path}\" does not exist!")
title = libWiiPy.title.Title()
title.load_wad(input_path.read_bytes())
# First, verify that this WAD isn't already the type we're trying to convert to.
if title.ticket.is_dev and target == "development":
fatal_error("This is already a development WAD!")
elif not title.ticket.is_dev and not title.tmd.vwii and target == "retail":
fatal_error("This is already a retail WAD!")
elif not title.ticket.is_dev and title.tmd.vwii and target == "vWii":
fatal_error("This is already a vWii WAD!")
# Get the current type to display later.
if title.ticket.is_dev:
source = "development"
elif title.tmd.vwii:
source = "vWii"
else:
source = "retail"
# Extract the Title Key so it can be re-encrypted with the correct key later.
title_key = title.ticket.get_title_key()
title_key_new = None
if target == "development":
# Set the development signature info.
title.ticket.signature_issuer = "Root-CA00000002-XS00000006" + title.ticket.signature_issuer[26:]
title.tmd.signature_issuer = "Root-CA00000002-CP00000007" + title.tmd.signature_issuer[26:]
# Re-encrypt the title key with the dev key, and set that in the Ticket.
title_key_new = libWiiPy.title.encrypt_title_key(title_key, 0, title.ticket.title_id, True)
title.ticket.common_key_index = 0
else:
# Set the retail signature info.
title.ticket.signature_issuer = "Root-CA00000001-XS00000003" + title.ticket.signature_issuer[26:]
title.tmd.signature_issuer = "Root-CA00000001-CP00000004" + title.tmd.signature_issuer[26:]
if target == "retail":
title_key_new = libWiiPy.title.encrypt_title_key(title_key, 0, title.ticket.title_id, False)
title.ticket.common_key_index = 0
elif target == "vWii":
title_key_new = libWiiPy.title.encrypt_title_key(title_key, 2, title.ticket.title_id, False)
title.ticket.common_key_index = 2
title.ticket.title_key_enc = title_key_new
title.fakesign()
output_path.write_bytes(title.dump_wad())
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)
# Make sure input path both exists and is a directory. Separate checks because this provides more relevant
# errors than just a NotADirectoryError if the actual issue is that there's nothing at all.
if not input_path.exists():
fatal_error(f"The specified input directory \"{input_path}\" does not exist!")
if not input_path.is_dir():
fatal_error(f"The specified input path \"{input_path}\" is not a directory!")
# Get a list of all files ending in .tmd, and then make sure that that list has *only* 1 entry. More than 1
# means we can't pack a WAD because we couldn't really tell which TMD is intended for this WAD.
tmd_list = list(input_path.glob('*.[tT][mM][dD]'))
if len(tmd_list) > 1:
fatal_error("More than one TMD file was found! Only one TMD can be packed into a WAD.")
elif len(tmd_list) == 0:
fatal_error("No TMD file was found! Cannot pack WAD.")
tmd_file = pathlib.Path(tmd_list[0])
# Repeat the same process as above for all .tik files.
ticket_list = list(input_path.glob('*.[tT][iI][kK]'))
if len(ticket_list) > 1:
fatal_error("More than one Ticket file was found! Only one Ticket can be packed into a WAD.")
elif len(ticket_list) == 0:
fatal_error("No Ticket file was found! Cannot pack WAD.")
ticket_file = pathlib.Path(ticket_list[0])
# And one more time for all .cert files.
cert_list = list(input_path.glob('*.[cC][eE][rR][tT]'))
if len(cert_list) > 1:
fatal_error("More than one certificate file was found! Only one certificate can be packed into a WAD.")
elif len(cert_list) == 0:
fatal_error("No certificate file was found! Cannot pack WAD.")
cert_file = pathlib.Path(cert_list[0])
# Make sure that there's at least one content to pack.
content_files = list(input_path.glob("*.[aA][pP][pP]"))
if not content_files:
fatal_error("No content files were found! Cannot pack WAD.")
# Open the output file, and load all the component files that we've now verified we have into a libWiiPy Title()
# object.
title = libWiiPy.title.Title()
title.load_tmd(tmd_file.read_bytes())
title.load_ticket(ticket_file.read_bytes())
title.wad.set_cert_data(cert_file.read_bytes())
# Footers are not super common and are not required, so we don't care about one existing until we get to
# the step where we'd pack it.
footer_file = pathlib.Path(list(input_path.glob("*.[fF][oO][oO][tT][eE][rR]"))[0])
if footer_file.exists():
title.wad.set_meta_data(footer_file.read_bytes())
# Method to ensure that the title's content records match between the TMD() and ContentRegion() objects.
title.load_content_records()
# Sort the contents based on the records. May still be kinda hacky.
content_indices = []
for record in title.content.content_records:
content_indices.append(record.index)
content_files_ordered = []
for _ in content_files:
content_files_ordered.append(None)
for index in range(len(content_files)):
target_index = content_indices.index(int(content_files[index].stem, 16))
content_files_ordered[target_index] = content_files[index]
# Iterate over every file in the content_files list, and set them in the Title().
for index in range(title.content.num_contents):
dec_content = content_files_ordered[index].read_bytes()
title.set_content(dec_content, index)
# Fakesign the TMD and Ticket using the trucha bug, if enabled. This is built-in in libWiiPy v0.4.1+.
if args.fakesign:
title.fakesign()
output_path.write_bytes(title.dump_wad())
print("WAD file packed!")
def handle_wad_remove(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)
if not input_path.exists():
fatal_error(f"The specified WAD file \"{input_path}\" does not exist!")
title = libWiiPy.title.Title()
title.load_wad(input_path.read_bytes())
# TODO: see if this implementation is problematic now
if args.index is not None:
# List indices in the title, and ensure that the target content index exists.
valid_indices = []
for record in title.content.content_records:
valid_indices.append(record.index)
if args.index not in valid_indices:
fatal_error("The specified content index could not be found in this title!")
title.content.remove_content_by_index(args.index)
# Auto fakesign because we've edited the title.
title.fakesign()
output_path.write_bytes(title.dump_wad())
print(f"Removed content at content index {args.index}!")
elif args.cid is not None:
if len(args.cid) != 8:
fatal_error("The specified Content ID is not valid!")
target_cid = int(args.cid, 16)
# List Contents IDs in the title, and ensure that the target Content ID exists.
valid_ids = []
for record in title.content.content_records:
valid_ids.append(record.content_id)
if target_cid not in valid_ids:
fatal_error("The specified Content ID could not be found in this title!")
title.content.remove_content_by_cid(target_cid)
# Auto fakesign because we've edited the title.
title.fakesign()
output_path.write_bytes(title.dump_wad())
print(f"Removed content with Content ID \"{target_cid:08X}\"!")
def handle_wad_set(args):
input_path = pathlib.Path(args.input)
content_path = pathlib.Path(args.content)
if args.output is not None:
output_path = pathlib.Path(args.output)
else:
output_path = pathlib.Path(args.input)
if not input_path.exists():
fatal_error(f"The specified WAD file \"{input_path}\" does not exist!")
if not content_path.exists():
fatal_error(f"The specified content file \"{content_path}\" does not exist!")
title = libWiiPy.title.Title()
title.load_wad(input_path.read_bytes())
content_data = content_path.read_bytes()
# Get the new type of the content, if one was specified.
target_type = None
if args.type is not None:
match str.lower(args.type):
case "normal":
target_type = libWiiPy.title.ContentType.NORMAL
case "shared":
target_type = libWiiPy.title.ContentType.SHARED
case "dlc":
target_type = libWiiPy.title.ContentType.DLC
case _:
fatal_error(f"The provided content type \"{args.type}\" is not valid!\"")
if args.index is not None:
# If we're replacing based on the index, then make sure the specified index exists.
existing_indices = []
for record in title.content.content_records:
existing_indices.append(record.index)
if args.index not in existing_indices:
fatal_error("The specified index could not be found in this title!")
if target_type:
title.set_content(content_data, args.index, content_type=target_type)
else:
title.set_content(content_data, args.index)
# Auto fakesign because we've edited the title.
title.fakesign()
output_path.write_bytes(title.dump_wad())
print(f"Replaced content at content index {args.index}!")
elif args.cid is not None:
# If we're replacing based on the CID, then make sure the specified CID is valid and exists.
if len(args.cid) != 8:
fatal_error("The specified Content ID is not valid!")
target_cid = int(args.cid, 16)
existing_cids = []
for record in title.content.content_records:
existing_cids.append(record.content_id)
if target_cid not in existing_cids:
fatal_error("The specified Content ID could not be found in this title!")
target_index = title.content.get_index_from_cid(target_cid)
if target_type:
title.set_content(content_data, target_index, content_type=target_type)
else:
title.set_content(content_data, target_index)
# Auto fakesign because we've edited the title.
title.fakesign()
output_path.write_bytes(title.dump_wad())
print(f"Replaced content with Content ID \"{target_cid:08X}\"!")
def handle_wad_unpack(args):
input_path = pathlib.Path(args.input)
output_path = pathlib.Path(args.output)
if not input_path.exists():
fatal_error(f"The specified WAD file \"{input_path}\" does not exist!")
# Check if the output path already exists, and if it does, ensure that it is both a directory and empty.
if output_path.exists():
if output_path.is_file():
fatal_error(f"A file already exists with the provided output directory name!")
else:
output_path.mkdir()
# Step through each component of a WAD and dump it to a file.
title = libWiiPy.title.Title()
title.load_wad(input_path.read_bytes())
cert_name = title.tmd.title_id + ".cert"
output_path.joinpath(cert_name).write_bytes(title.wad.get_cert_data())
tmd_name = title.tmd.title_id + ".tmd"
output_path.joinpath(tmd_name).write_bytes(title.wad.get_tmd_data())
ticket_name = title.tmd.title_id + ".tik"
output_path.joinpath(ticket_name).write_bytes(title.wad.get_ticket_data())
meta_name = title.tmd.title_id + ".footer"
output_path.joinpath(meta_name).write_bytes(title.wad.get_meta_data())
# Skip validating hashes if -s/--skip-hash was passed.
if args.skip_hash:
skip_hash = True
else:
skip_hash = False
for content_file in range(0, title.tmd.num_contents):
content_index = title.content.content_records[content_file].index
content_file_name = f"{content_index:08X}".lower() + ".app"
output_path.joinpath(content_file_name).write_bytes(title.get_content_by_index(content_file, skip_hash))
print("WAD file unpacked!")

View File

@@ -1,38 +0,0 @@
# "modules/archive/ash.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import pathlib
import libWiiPy
def handle_ash(args):
input_path = pathlib.Path(args.input)
output_path = pathlib.Path(args.output)
# Code for if --compress was passed.
# ASH compression has not been implemented in libWiiPy yet, but it'll be filled in here when it has.
if args.compress:
print("Compression is not implemented yet.")
# Code for if --decompress was passed.
elif args.decompress:
# These default to 9 and 11, respectively, so we can always read them.
sym_tree_bits = args.sym_bits
dist_tree_bits = args.dist_bits
if not input_path.exists():
raise FileNotFoundError(input_path)
ash_file = open(input_path, "rb")
ash_data = ash_file.read()
ash_file.close()
# Decompress ASH file using the provided symbol/distance tree widths.
ash_decompressed = libWiiPy.archive.decompress_ash(ash_data, sym_tree_bits=sym_tree_bits,
dist_tree_bits=dist_tree_bits)
ash_out = open(output_path, "wb")
ash_out.write(ash_decompressed)
ash_out.close()
print("ASH file decompressed!")

View File

@@ -1,37 +0,0 @@
# "modules/archive/u8.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import pathlib
import libWiiPy
def handle_u8(args):
input_path = pathlib.Path(args.input)
output_path = pathlib.Path(args.output)
# Code for if the --pack argument was passed.
if args.pack:
try:
u8_data = libWiiPy.archive.pack_u8(input_path)
except ValueError:
print("Error: Specified input file/folder does not exist!")
return
out_file = open(output_path, "wb")
out_file.write(u8_data)
out_file.close()
print("U8 archive packed!")
# Code for if the --unpack argument was passed.
elif args.unpack:
if not input_path.exists():
raise FileNotFoundError(args.input)
u8_data = open(input_path, "rb").read()
# Output path is deliberately not checked in any way because libWiiPy already has those checks, and it's easier
# and cleaner to only have one component doing all the checks.
libWiiPy.archive.extract_u8(u8_data, str(output_path))
print("U8 archive unpacked!")

9
modules/core.py Normal file
View File

@@ -0,0 +1,9 @@
# "modules/core.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import sys
def fatal_error(msg: str) -> None:
print(f"\033[31mError:\033[0m {msg}")
sys.exit(-1)

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

@@ -1,334 +0,0 @@
# "modules/title/wad.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import os
import pathlib
from random import randint
import libWiiPy
def handle_wad_add(args):
input_path = pathlib.Path(args.input)
content_path = pathlib.Path(args.content)
if args.output is not None:
output_path = pathlib.Path(args.output)
else:
output_path = pathlib.Path(args.input)
if not input_path.exists():
raise FileNotFoundError(input_path)
if not content_path.exists():
raise FileNotFoundError(content_path)
wad_file = open(input_path, 'rb')
title = libWiiPy.title.Title()
title.load_wad(wad_file.read())
wad_file.close()
content_file = open(content_path, 'rb')
content_data = content_file.read()
content_file.close()
# Prepare the CID so it's ready when we go to add this content to the WAD.
# We need to both validate that this is a real CID, and also that it isn't already taken by another content.
if args.cid is not None:
if len(args.cid) != 8:
raise ValueError("The provided Content ID is invalid!")
target_cid = int(args.cid, 16)
for record in title.content.content_records:
if target_cid == record.content_id:
raise ValueError("The provided Content ID is already being used by this title!")
print(f"Using provided Content ID \"{target_cid:08X}\".")
# If we weren't given a CID, then we need to randomly assign one, and ensure it isn't being used.
else:
used_cids = []
for record in title.content.content_records:
used_cids.append(record.content_id)
target_cid = randint(0, 0x000000FF)
while target_cid in used_cids:
target_cid = randint(0, 0x000000FF)
print(f"Using randomly assigned Content ID \"{target_cid:08X}\" since none were provided.")
# Get the type of the new content.
if args.type is not None:
match str.lower(args.type):
case "normal":
target_type = libWiiPy.title.ContentType.NORMAL
case "shared":
target_type = libWiiPy.title.ContentType.SHARED
case "dlc":
target_type = libWiiPy.title.ContentType.DLC
case _:
raise ValueError("The provided content type is invalid!")
else:
target_type = libWiiPy.title.ContentType.NORMAL
# Call add_content to add our new content with the set parameters.
title.add_content(content_data, target_cid, target_type)
# Auto fakesign because we've edited the title.
title.fakesign()
out_file = open(output_path, 'wb')
out_file.write(title.dump_wad())
out_file.close()
print(f"Successfully added new content with Content ID \"{target_cid:08X}\" and type \"{target_type.name}\"!")
def handle_wad_pack(args):
input_path = pathlib.Path(args.input)
output_path = pathlib.Path(args.output)
# Make sure input path both exists and is a directory. Separate checks because this provides more relevant
# errors than just a NotADirectoryError if the actual issue is that there's nothing at all.
if not input_path.exists():
raise FileNotFoundError(input_path)
if not input_path.is_dir():
raise NotADirectoryError(input_path)
# Get a list of all files ending in .tmd, and then make sure that that list has *only* 1 entry. More than 1
# means we can't pack a WAD because we couldn't really tell which TMD is intended for this WAD.
tmd_list = list(input_path.glob('*.[tT][mM][dD]'))
if len(tmd_list) > 1:
raise FileExistsError("More than one TMD file was found! Only one TMD can be packed into a WAD.")
elif len(tmd_list) == 0:
raise FileNotFoundError("No TMD file found! Cannot pack WAD.")
else:
tmd_file = tmd_list[0]
# Repeat the same process as above for all .tik files.
ticket_list = list(input_path.glob('*.[tT][iI][kK]'))
if len(ticket_list) > 1:
raise FileExistsError("More than one Ticket file was found! Only one Ticket can be packed into a WAD.")
elif len(ticket_list) == 0:
raise FileNotFoundError("No Ticket file found! Cannot pack WAD.")
else:
ticket_file = ticket_list[0]
# And one more time for all .cert files.
cert_list = list(input_path.glob('*.[cC][eE][rR][tT]'))
if len(cert_list) > 1:
raise FileExistsError("More than one certificate file was found! Only one certificate can be packed into a "
"WAD.")
elif len(cert_list) == 0:
raise FileNotFoundError("No certificate file found! Cannot pack WAD.")
else:
cert_file = cert_list[0]
# Make sure that there's at least one content to pack.
content_files = list(input_path.glob("*.[aA][pP][pP]"))
if not content_files:
raise FileNotFoundError("No contents found! Cannot pack WAD.")
# Semi-hacky sorting method, but it works. Should maybe be changed eventually.
content_files_ordered = []
for index in range(len(content_files)):
content_files_ordered.append(None)
for content_file in content_files:
content_index = int(content_file.stem, 16)
content_files_ordered[content_index] = content_file
# Open the output file, and load all the component files that we've now verified we have into a libWiiPy Title()
# object.
with open(output_path, "wb") as output_path:
title = libWiiPy.title.Title()
title.load_tmd(open(tmd_file, "rb").read())
title.load_ticket(open(ticket_file, "rb").read())
title.wad.set_cert_data(open(cert_file, "rb").read())
# Footers are not super common and are not required, so we don't care about one existing until we get to
# the step where we'd pack it.
footer_file = list(input_path.glob("*.[fF][oO][oO][tT][eE][rR]"))[0]
if footer_file.exists():
title.wad.set_meta_data(open(footer_file, "rb").read())
# Method to ensure that the title's content records match between the TMD() and ContentRegion() objects.
title.load_content_records()
# Iterate over every file in the content_files list, and set them in the Title().
for record in title.content.content_records:
index = title.content.content_records.index(record)
dec_content = open(content_files_ordered[index], "rb").read()
title.set_content(dec_content, index)
# Fakesign the TMD and Ticket using the trucha bug, if enabled. This is built-in in libWiiPy v0.4.1+.
if args.fakesign:
title.fakesign()
output_path.write(title.dump_wad())
print("WAD file packed!")
def handle_wad_remove(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)
if not input_path.exists():
raise FileNotFoundError(input_path)
wad_file = open(input_path, 'rb')
title = libWiiPy.title.Title()
title.load_wad(wad_file.read())
wad_file.close()
if args.index is not None:
# List indices in the title, and ensure that the target content index exists.
valid_indices = []
for record in title.content.content_records:
valid_indices.append(record.index)
if args.index not in valid_indices:
raise ValueError("The provided content index could not be found in this title!")
title.content.remove_content_by_index(args.index)
# Auto fakesign because we've edited the title.
title.fakesign()
out_file = open(output_path, 'wb')
out_file.write(title.dump_wad())
out_file.close()
print(f"Removed content at content index {args.index}!")
elif args.cid is not None:
if len(args.cid) != 8:
raise ValueError("The provided Content ID is invalid!")
target_cid = int(args.cid, 16)
# List Contents IDs in the title, and ensure that the target Content ID exists.
valid_ids = []
for record in title.content.content_records:
valid_ids.append(record.content_id)
if target_cid not in valid_ids:
raise ValueError("The provided Content ID could not be found in this title!")
title.content.remove_content_by_cid(target_cid)
# Auto fakesign because we've edited the title.
title.fakesign()
out_file = open(output_path, 'wb')
out_file.write(title.dump_wad())
out_file.close()
print(f"Removed content with Content ID \"{target_cid:08X}\"!")
def handle_wad_set(args):
input_path = pathlib.Path(args.input)
content_path = pathlib.Path(args.content)
if args.output is not None:
output_path = pathlib.Path(args.output)
else:
output_path = pathlib.Path(args.input)
if not input_path.exists():
raise FileNotFoundError(input_path)
if not content_path.exists():
raise FileNotFoundError(content_path)
title = libWiiPy.title.Title()
title.load_wad(open(input_path, "rb").read())
content_data = open(content_path, "rb").read()
# Get the new type of the content, if one was specified.
if args.type is not None:
match str.lower(args.type):
case "normal":
target_type = libWiiPy.title.ContentType.NORMAL
case "shared":
target_type = libWiiPy.title.ContentType.SHARED
case "dlc":
target_type = libWiiPy.title.ContentType.DLC
case _:
raise ValueError("The provided content type is invalid!")
else:
target_type = None
if args.index is not None:
# If we're replacing based on the index, then make sure the specified index exists.
existing_indices = []
for record in title.content.content_records:
existing_indices.append(record.index)
if args.index not in existing_indices:
raise ValueError("The provided index could not be found in this title!")
if target_type:
title.set_content(content_data, args.index, content_type=target_type)
else:
title.set_content(content_data, args.index)
# Auto fakesign because we've edited the title.
title.fakesign()
open(output_path, "wb").write(title.dump_wad())
print(f"Replaced content at content index {args.index}!")
elif args.cid is not None:
# If we're replacing based on the CID, then make sure the specified CID is valid and exists.
if len(args.cid) != 8:
raise ValueError("The provided Content ID is invalid!")
target_cid = int(args.cid, 16)
existing_cids = []
for record in title.content.content_records:
existing_cids.append(record.content_id)
if target_cid not in existing_cids:
raise ValueError("The provided Content ID could not be found in this title!")
target_index = title.content.get_index_from_cid(target_cid)
if target_type:
title.set_content(content_data, target_index, content_type=target_type)
else:
title.set_content(content_data, target_index)
# Auto fakesign because we've edited the title.
title.fakesign()
open(output_path, "wb").write(title.dump_wad())
print(f"Replaced content with Content ID \"{target_cid:08X}\"!")
def handle_wad_unpack(args):
input_path = pathlib.Path(args.input)
output_path = pathlib.Path(args.output)
if not input_path.exists():
raise FileNotFoundError(input_path)
# Check if the output path already exists, and if it does, ensure that it is both a directory and empty.
if output_path.exists():
# if output_path.is_dir() and next(os.scandir(output_path), None):
# raise ValueError("Output folder is not empty!")
if output_path.is_file():
raise ValueError("A file already exists with the provided directory name!")
else:
os.mkdir(output_path)
# Step through each component of a WAD and dump it to a file.
with open(args.input, "rb") as wad_file:
title = libWiiPy.title.Title()
title.load_wad(wad_file.read())
cert_name = title.tmd.title_id + ".cert"
cert_out = open(output_path.joinpath(cert_name), "wb")
cert_out.write(title.wad.get_cert_data())
cert_out.close()
tmd_name = title.tmd.title_id + ".tmd"
tmd_out = open(output_path.joinpath(tmd_name), "wb")
tmd_out.write(title.wad.get_tmd_data())
tmd_out.close()
ticket_name = title.tmd.title_id + ".tik"
ticket_out = open(output_path.joinpath(ticket_name), "wb")
ticket_out.write(title.wad.get_ticket_data())
ticket_out.close()
meta_name = title.tmd.title_id + ".footer"
meta_out = open(output_path.joinpath(meta_name), "wb")
meta_out.write(title.wad.get_meta_data())
meta_out.close()
# Skip validating hashes if -s/--skip-hash was passed.
if args.skip_hash:
skip_hash = True
else:
skip_hash = False
for content_file in range(0, title.tmd.num_contents):
content_file_name = f"{content_file:08X}".lower() + ".app"
content_out = open(output_path.joinpath(content_file_name), "wb")
content_out.write(title.get_content_by_index(content_file, skip_hash))
content_out.close()
print("WAD file unpacked!")

View File

@@ -1,2 +1,2 @@
git+https://github.com/NinjaCheetah/libWiiPy git+https://github.com/NinjaCheetah/libWiiPy
nuitka==2.3.11 nuitka

169
wiipy.py
View File

@@ -4,38 +4,57 @@
import argparse import argparse
from importlib.metadata import version from importlib.metadata import version
from modules.archive.ash import * from commands.archive.ash import *
from modules.archive.u8 import * #from commands.archive.theme import *
from modules.nand.emunand import * from commands.archive.u8 import *
from modules.nand.setting import * from commands.nand.emunand import *
from modules.title.ciosbuild import * from commands.nand.setting import *
from modules.title.fakesign import * from commands.title.ciosbuild import *
from modules.title.info import * from commands.title.fakesign import *
from modules.title.iospatcher import * from commands.title.info import *
from modules.title.nus import * from commands.title.iospatcher import *
from modules.title.wad import * from commands.title.nus import *
from commands.title.tmd import *
from commands.title.wad import *
wiipy_ver = "1.4.0"
if __name__ == "__main__": if __name__ == "__main__":
# Main argument parser. # Main argument parser.
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="A simple command line tool to manage file formats used by the Wii.") description="A simple command line tool to manage file formats used by the Wii.")
parser.add_argument("--version", action="version", parser.add_argument("--version", action="version",
version=f"WiiPy v1.4.0, based on libWiiPy v{version('libWiiPy')} (from branch \'main\')") version=f"WiiPy v{wiipy_ver}, based on libWiiPy v{version('libWiiPy')} (from branch \'main\')")
subparsers = parser.add_subparsers(title="subcommands", dest="subcommand", required=True) subparsers = parser.add_subparsers(title="subcommands", dest="subcommand", required=True)
# Argument parser for the ASH subcommand. # Argument parser for the ASH subcommand.
ash_parser = subparsers.add_parser("ash", help="compress/decompress an ASH file", ash_parser = subparsers.add_parser("ash", help="compress/decompress an ASH file",
description="compress/decompress an ASH file") description="compress/decompress an ASH file")
ash_parser.set_defaults(func=handle_ash) ash_subparsers = ash_parser.add_subparsers(title="emunand", dest="emunand", required=True)
ash_group = ash_parser.add_mutually_exclusive_group(required=True) # ASH compress parser.
ash_group.add_argument("-c", "--compress", help="compress a file into an ASH file", action="store_true") ash_compress_parser = ash_subparsers.add_parser("compress", help="compress a file into an ASH file",
ash_group.add_argument("-d", "--decompress", help="decompress an ASH file", action="store_true") description="compress a file into an ASH file; by default, this "
ash_parser.add_argument("input", metavar="IN", type=str, help="input file") "will output to <input file>.ash")
ash_parser.add_argument("output", metavar="OUT", type=str, help="output file") ash_compress_parser.set_defaults(func=handle_ash_compress)
ash_parser.add_argument("--sym-bits", metavar="SYM_BITS", type=int, ash_compress_parser.add_argument("input", metavar="IN", type=str, help="file to compress")
ash_compress_parser.add_argument("--sym-bits", metavar="SYM_BITS", type=int,
help="number of bits in each symbol tree leaf (default: 9)", default=9) help="number of bits in each symbol tree leaf (default: 9)", default=9)
ash_parser.add_argument("--dist-bits", metavar="DIST_BITS", type=int, ash_compress_parser.add_argument("--dist-bits", metavar="DIST_BITS", type=int,
help="number of bits in each distance tree leaf (default: 11)", default=11) help="number of bits in each distance tree leaf (default: 11)", default=11)
ash_compress_parser.add_argument("-o", "--output", metavar="OUT", type=str,
help="file to output the ASH file to (optional)")
# ASH decompress parser.
ash_decompress_parser = ash_subparsers.add_parser("decompress", help="decompress an ASH file",
description="decompress an ASH file; by default, this will "
"output to <input file>.arc")
ash_decompress_parser.set_defaults(func=handle_ash_decompress)
ash_decompress_parser.add_argument("input", metavar="IN", type=str, help="ASH file to decompress")
ash_decompress_parser.add_argument("--sym-bits", metavar="SYM_BITS", type=int,
help="number of bits in each symbol tree leaf (default: 9)", default=9)
ash_decompress_parser.add_argument("--dist-bits", metavar="DIST_BITS", type=int,
help="number of bits in each distance tree leaf (default: 11)", default=11)
ash_decompress_parser.add_argument("-o", "--output", metavar="OUT", type=str,
help="file to output the ASH file to (optional)")
# Argument parser for the cIOS command # Argument parser for the cIOS command
cios_parser = subparsers.add_parser("cios", help="build a cIOS from a base IOS and provided map", cios_parser = subparsers.add_parser("cios", help="build a cIOS from a base IOS and provided map",
@@ -47,7 +66,7 @@ if __name__ == "__main__":
cios_parser.add_argument("-c", "--cios-ver", metavar="CIOS", type=str, cios_parser.add_argument("-c", "--cios-ver", metavar="CIOS", type=str,
help="cIOS version from the map to build", required=True) help="cIOS version from the map to build", required=True)
cios_parser.add_argument("-m", "--modules", metavar="MODULES", type=str, cios_parser.add_argument("-m", "--modules", metavar="MODULES", type=str,
help="directory to look for cIOS modules in (optional, defaults to current directory)") help="directory to look for cIOS commands in (optional, defaults to current directory)")
cios_parser.add_argument("-s", "--slot", metavar="SLOT", type=int, cios_parser.add_argument("-s", "--slot", metavar="SLOT", type=int,
help="slot that this cIOS will install to (optional, defaults to 249)", default=249) help="slot that this cIOS will install to (optional, defaults to 249)", default=249)
cios_parser.add_argument("-v", "--version", metavar="VERSION", type=int, cios_parser.add_argument("-v", "--version", metavar="VERSION", type=int,
@@ -89,7 +108,7 @@ if __name__ == "__main__":
# Argument parser for the iospatch command. # Argument parser for the iospatch command.
iospatch_parser = subparsers.add_parser("iospatch", help="patch IOS WADs to re-enable exploits", iospatch_parser = subparsers.add_parser("iospatch", help="patch IOS WADs to re-enable exploits",
description="patch IOS WADs to re-enable exploits; by default, this will" description="patch IOS WADs to re-enable exploits; by default, this will "
"overwrite the input file in place unless you use -o/--output") "overwrite the input file in place unless you use -o/--output")
iospatch_parser.set_defaults(func=handle_iospatch) iospatch_parser.set_defaults(func=handle_iospatch)
iospatch_parser.add_argument("input", metavar="IN", type=str, help="input file") iospatch_parser.add_argument("input", metavar="IN", type=str, help="input file")
@@ -100,7 +119,7 @@ if __name__ == "__main__":
iospatch_parser.add_argument("-vd", "--version-downgrading", action="store_true", iospatch_parser.add_argument("-vd", "--version-downgrading", action="store_true",
help="patch in version downgrading support") help="patch in version downgrading support")
iospatch_parser.add_argument("-di", "--drive-inquiry", action="store_true", iospatch_parser.add_argument("-di", "--drive-inquiry", action="store_true",
help="patches out the drive inquiry check") help="patches out the drive inquiry (EXPERIMENTAL)")
iospatch_parser.add_argument("-v", "--version", metavar="VERSION", type=int, help="set the IOS version") iospatch_parser.add_argument("-v", "--version", metavar="VERSION", type=int, help="set the IOS version")
iospatch_parser.add_argument("-s", "--slot", metavar="SLOT", type=int, iospatch_parser.add_argument("-s", "--slot", metavar="SLOT", type=int,
help="set the slot that this IOS will install to") help="set the slot that this IOS will install to")
@@ -178,19 +197,74 @@ 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.
# 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 (<filename>.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 "
"CID or by its index; by default, this will overwrite "
"the input file unless an output is specified")
tmd_remove_parser.set_defaults(func=handle_tmd_remove)
tmd_remove_parser.add_argument("input", metavar="IN", type=str, help="TMD file to remove a content record from")
tmd_remove_targets = tmd_remove_parser.add_mutually_exclusive_group(required=True)
tmd_remove_targets.add_argument("-i", "--index", metavar="INDEX", type=int,
help="index of the content record to remove")
tmd_remove_targets.add_argument("-c", "--cid", metavar="CID", type=str,
help="Content ID of the content record to remove")
tmd_remove_parser.add_argument("-o", "--output", metavar="OUT", type=str,
help="file to output the updated TMD to (optional)")
# Argument parser for the U8 subcommand. # Argument parser for the U8 subcommand.
u8_parser = subparsers.add_parser("u8", help="pack/unpack a U8 archive", u8_parser = subparsers.add_parser("u8", help="pack/unpack a U8 archive",
description="pack/unpack a U8 archive") description="pack/unpack a U8 archive")
u8_parser.set_defaults(func=handle_u8) u8_subparsers = u8_parser.add_subparsers(dest="subcommand", required=True)
u8_group = u8_parser.add_mutually_exclusive_group(required=True) # Pack U8 subcommand.
u8_group.add_argument("-p", "--pack", help="pack a directory to a U8 archive", action="store_true") u8_pack_parser = u8_subparsers.add_parser("pack", help="pack a folder into U8 archive",
u8_group.add_argument("-u", "--unpack", help="unpack a U8 archive to a directory", action="store_true") description="pack a folder into U8 archive")
u8_parser.add_argument("input", metavar="IN", type=str, help="input file") u8_pack_parser.set_defaults(func=handle_u8_pack)
u8_parser.add_argument("output", metavar="OUT", type=str, help="output file") u8_pack_parser.add_argument("input", metavar="IN", type=str, help="folder to pack")
u8_pack_parser.add_argument("output", metavar="OUT", type=str, help="output U8 archive")
# Unpack U8 subcommand.
u8_unpack_parser = u8_subparsers.add_parser("unpack", help="unpack a U8 archive into a folder",
description="unpack a U8 archive into a folder")
u8_unpack_parser.set_defaults(func=handle_u8_unpack)
u8_unpack_parser.add_argument("input", metavar="IN", type=str, help="U8 archive to unpack")
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",
@@ -207,6 +281,41 @@ if __name__ == "__main__":
"(optional, will default to \"Normal\" if not specified)") "(optional, will default to \"Normal\" if not specified)")
wad_add_parser.add_argument("-o", "--output", metavar="OUT", type=str, wad_add_parser.add_argument("-o", "--output", metavar="OUT", type=str,
help="file to output the updated WAD to (optional)") help="file to output the updated WAD to (optional)")
# Convert WAD subcommand.
wad_convert_parser = wad_subparsers.add_parser("convert", help="re-encrypt a WAD file with a different key",
description="re-encrypt a WAD file with a different key, making it "
"possible to use the WAD in a different environment; "
"this fakesigns the WAD by default")
wad_convert_parser.set_defaults(func=handle_wad_convert)
wad_convert_parser.add_argument("input", metavar="IN", type=str, help="WAD file to re-encrypt")
wad_convert_targets_lbl = wad_convert_parser.add_argument_group(title="target keys")
wad_convert_targets = wad_convert_targets_lbl.add_mutually_exclusive_group(required=True)
wad_convert_targets.add_argument("-d", "--dev", action="store_true",
help="re-encrypt the WAD with the development common key, allowing it to be "
"installed on development consoles")
wad_convert_targets.add_argument("-r", "--retail", action="store_true",
help="re-encrypt the WAD with the retail common key, allowing it to be installed "
"on retail consoles or inside of Dolphin")
wad_convert_targets.add_argument("-v", "--vwii", action="store_true",
help="re-encrypt the WAD with the vWii key, allowing it to theoretically be "
"installed from Wii U mode if a Wii U mode WAD installer is created")
wad_convert_parser.add_argument("-o", "--output", metavar="OUT", type=str,
help="file to output the new WAD to (optional, defaults to '<old_name>_<key>.wad')")
# Edit WAD subcommand.
wad_edit_parser = wad_subparsers.add_parser("edit", help="edit the properties of a WAD file",
description="edit the properties of a WAD file; by default, this will "
"overwrite the input file unless an output is specified")
wad_edit_parser.set_defaults(func=handle_wad_edit)
wad_edit_parser.add_argument("input", metavar="IN", type=str, help="WAD file to edit")
wad_edit_parser.add_argument("--tid", metavar="TID", type=str,
help="a new Title ID for this WAD (formatted as 4 ASCII characters)")
wad_edit_parser.add_argument("--ios", metavar="IOS", type=str,
help="a new IOS version for this WAD (formatted as the decimal IOS version, eg. 58)")
wad_edit_parser.add_argument("--type", metavar="TYPE", type=str,
help="a new title type for this WAD (valid options: System, Channel, SystemChannel, "
"GameChannel, DLC, HiddenChannel)")
wad_edit_parser.add_argument("-o", "--output", metavar="OUT", type=str,
help="file to output the updated WAD to (optional)")
# Pack WAD subcommand. # Pack WAD subcommand.
wad_pack_parser = wad_subparsers.add_parser("pack", help="pack a directory to a WAD file", wad_pack_parser = wad_subparsers.add_parser("pack", help="pack a directory to a WAD file",
description="pack a directory to a WAD file") description="pack a directory to a WAD file")