mirror of
https://github.com/NinjaCheetah/WiiPy.git
synced 2025-04-26 13:21:01 -04:00
Compare commits
83 Commits
Author | SHA1 | Date | |
---|---|---|---|
dfb527388c | |||
46784f126e | |||
8e7489ec57 | |||
b123005bf5 | |||
5f751acabd | |||
71c0726c4f | |||
1410dd6c36 | |||
9a89f80247 | |||
a6388834e0 | |||
514deb6b6c | |||
9ad0f8412c | |||
4e6c7d2dd0 | |||
eeb3387701 | |||
ceff61930b | |||
3e9f452885 | |||
f3eb127aee | |||
fa4e9bf2f1 | |||
d6aa50697f | |||
f9739eab58 | |||
8a050ff8f6 | |||
76c78e6d85 | |||
554bbfb7cb | |||
42fc37de65 | |||
71450b5204 | |||
2733b70e18 | |||
6336791be0 | |||
19dc956d25 | |||
3de4dbd6e6 | |||
ec7cb1063f | |||
33197c36f1 | |||
b77587771c | |||
4941cf8b94 | |||
73a877090b | |||
530afd4189 | |||
a4c06cae36 | |||
bcdb4fed21 | |||
55e38caf46 | |||
2342657e70 | |||
af9b639a6b | |||
1612d2ecb9 | |||
31635a8015 | |||
cde90c474d | |||
4636deeb0a | |||
079c7f9109 | |||
0a9733a8d3 | |||
676dbab4f1 | |||
97bc77b337 | |||
0ae9ac5060 | |||
2316e938b2 | |||
e047b12114 | |||
a35ba2e4b6 | |||
4730f3512b | |||
e34c10c3fa | |||
55c237f5f7 | |||
c51244e8e7 | |||
dc94ca09c1 | |||
02fa6d09ac | |||
4c700266cb | |||
fa6ba28dbe | |||
3062a739d6 | |||
bc1b6623bb | |||
d9c5940307 | |||
f09806e002 | |||
a863da98c7 | |||
1b7c9afb53 | |||
fc8bef61a7 | |||
26b49ddff6 | |||
b183336d95 | |||
bb3c2737e5 | |||
304539b120 | |||
6af035068c | |||
8ec2a0d186 | |||
15e99af267 | |||
744d738a8c | |||
9db9e3ad6f | |||
4e2f7b14e7 | |||
5d8b9e7c08 | |||
b1de3aa9c1 | |||
6ddb97eb6c | |||
b82b6f3873 | |||
3115105343 | |||
b2de9eb7dc | |||
a5afdc6d6a |
80
.github/workflows/python-build.yaml
vendored
80
.github/workflows/python-build.yaml
vendored
@ -1,5 +1,4 @@
|
||||
# This workflow will install Python dependencies, run tests and lint with a single version of Python
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
|
||||
# This workflow will install Python dependencies and then build WiiPy for all platforms
|
||||
|
||||
name: Python application
|
||||
|
||||
@ -20,56 +19,83 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install ccache for Nuitka
|
||||
run: sudo apt update && sudo apt install -y ccache libicu70
|
||||
- name: Set up Python 3.11
|
||||
run: |
|
||||
sudo apt update && \
|
||||
sudo apt install -y ccache patchelf
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.12"
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
- name: Build Application
|
||||
run: |
|
||||
python -m nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py
|
||||
run: make all
|
||||
- name: Prepare Package for Upload
|
||||
run: |
|
||||
mv wiipy.bin ~/wiipy
|
||||
mv wiipy ~/wiipy
|
||||
cd ~
|
||||
tar cvf WiiPy.tar wiipy
|
||||
- name: Upload Application
|
||||
uses: actions/upload-artifact@v4.3.0
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
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:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.11
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.12"
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
- name: Build Application
|
||||
run: |
|
||||
python -m nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py
|
||||
run: ARCH_FLAGS=--macos-target-arch=x86_64 make all
|
||||
- name: Prepare Package for Upload
|
||||
run: |
|
||||
mv wiipy.bin ~/wiipy
|
||||
mv wiipy ~/wiipy
|
||||
cd ~
|
||||
tar cvf WiiPy.tar wiipy
|
||||
- name: Upload Application
|
||||
uses: actions/upload-artifact@v4.3.0
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: ~/WiiPy.tar
|
||||
name: WiiPy-macos-bin
|
||||
name: WiiPy-macOS-x86_64-bin
|
||||
|
||||
build-macos-arm64:
|
||||
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
- name: Build Application
|
||||
run: ARCH_FLAGS=--macos-target-arch=arm64 make all
|
||||
- name: Prepare Package for Upload
|
||||
run: |
|
||||
mv wiipy ~/wiipy
|
||||
cd ~
|
||||
tar cvf WiiPy.tar wiipy
|
||||
- name: Upload Application
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: ~/WiiPy.tar
|
||||
name: WiiPy-macOS-arm64-bin
|
||||
|
||||
build-windows:
|
||||
|
||||
@ -79,19 +105,23 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Enable Developer Command Prompt
|
||||
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
|
||||
with:
|
||||
python-version: "3.11"
|
||||
python-version: "3.12"
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
- name: Build Application
|
||||
run: |
|
||||
python -m nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py
|
||||
run: .\Build.ps1
|
||||
- name: Upload Application
|
||||
uses: actions/upload-artifact@v4.3.0
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: D:\a\WiiPy\WiiPy\WiiPy.dist
|
||||
name: WiiPy-Windows-bin
|
||||
- name: Upload Onefile Application
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: D:\a\WiiPy\WiiPy\wiipy.exe
|
||||
name: WiiPy-windows-bin
|
||||
name: WiiPy-Windows-onefile-bin
|
||||
|
12
.gitignore
vendored
12
.gitignore
vendored
@ -25,6 +25,7 @@ share/python-wheels/
|
||||
*.build/
|
||||
*.dist/
|
||||
*.onefile-build/
|
||||
wiipy
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
@ -163,14 +164,23 @@ cython_debug/
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
.idea/
|
||||
|
||||
# Allows me to keep TMD files in my repository folder for testing without accidentally publishing them
|
||||
# Allows me to keep Wii files in my repository folder for testing without accidentally publishing them
|
||||
*.tmd
|
||||
*.tik
|
||||
*.cert
|
||||
*.footer
|
||||
*.wad
|
||||
*.app
|
||||
*.arc
|
||||
*.ash
|
||||
*.met
|
||||
out_prod/
|
||||
remakewad.pl
|
||||
content.map
|
||||
uid.sys
|
||||
SYSCONF
|
||||
setting.txt
|
||||
ciosmaps.xml
|
||||
|
||||
# Also awful macOS files
|
||||
*._*
|
||||
|
32
Build.ps1
Normal file
32
Build.ps1
Normal 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
|
||||
}
|
||||
}
|
14
Makefile
Normal file
14
Makefile
Normal file
@ -0,0 +1,14 @@
|
||||
CC=python -m nuitka
|
||||
ARCH_FLAGS?=
|
||||
|
||||
all:
|
||||
$(CC) --show-progress --assume-yes-for-downloads --onefile wiipy.py --onefile-tempdir-spec="{CACHE_DIR}/NinjaCheetah/WiiPy" $(ARCH_FLAGS) -o wiipy
|
||||
|
||||
install:
|
||||
install wiipy /usr/bin/
|
||||
|
||||
clean:
|
||||
rm wiipy
|
||||
rm -rd wiipy.build
|
||||
rm -rd wiipy.dist
|
||||
rm -rd wiipy.onefile-build
|
36
README.md
36
README.md
@ -5,26 +5,48 @@ WiiPy is cross-platform, and supports macOS, Windows, and Linux, both because it
|
||||
|
||||
To see what features are supported, I would recommend checking out the list of features that libWiiPy offers, which can be found [here]("https://github.com/NinjaCheetah/libWiiPy?tab=readme-ov-file#features").
|
||||
|
||||
|
||||
## Requirements
|
||||
libWiiPy has been tested on both Python 3.11 and 3.12, and works as expected in both places. libWiiPy *should* support Python 3.10, however this is not verified. WiiPy only relies on libWiiPy, so it supports the same Python versions.
|
||||
libWiiPy has been tested on both Python 3.11 and 3.12, and works as expected in both places. WiiPy relies only on libWiiPy, so generally any version supported by libWiiPy should be supported by WiiPy.
|
||||
|
||||
To make sure you have libWiiPy and all of its dependencies, you can simply run:
|
||||
```shell
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
|
||||
## Usage
|
||||
Basic usage for WiiPy is very simple.
|
||||
```shell
|
||||
python3 wiipy.py <subcommand>
|
||||
python3 wiipy.py <command>
|
||||
```
|
||||
You can use `--help` to see a list of all subcommands, or use `<subcommand> --help` to see usage instructions for that subcommand.
|
||||
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`.
|
||||
|
||||
Available subcommands will expand as support for more features are added into libWiiPy. WiiPy is designed around libWiiPy's `main` branch, so any features that have been merged into main are likely to be supported here within a short period of time. This also means that any updates to the library will be addressed here quickly, so breaking changes in libWiiPy shouldn't cause issues.
|
||||
|
||||
Available subcommands will expand as support for more features are added into libWiiPy. WiiPy is designed around libWiiPy's `main` branch, so any features that have been merged into main are likely to be supported here within a short period of time. This also means that any breaking changes to the library will be handled here quickly, so incompatibilities shouldn't happen.
|
||||
|
||||
## 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`).
|
||||
|
||||
|
||||
### 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
|
||||
python -m nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py
|
||||
make all
|
||||
```
|
||||
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.
|
||||
|
||||
Optionally, you can install WiiPy so that it's available system-wide. This will install it into `/usr/bin/`.
|
||||
```shell
|
||||
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
|
||||
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
33
commands/archive/ash.py
Normal 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!")
|
41
commands/archive/lz77.py
Normal file
41
commands/archive/lz77.py
Normal file
@ -0,0 +1,41 @@
|
||||
# "commands/archive/lz77.py" from WiiPy by NinjaCheetah
|
||||
# https://github.com/NinjaCheetah/WiiPy
|
||||
|
||||
import pathlib
|
||||
import libWiiPy
|
||||
from modules.core import fatal_error
|
||||
|
||||
|
||||
def handle_lz77_compress(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 + ".lz77")
|
||||
|
||||
if not input_path.exists():
|
||||
fatal_error(f"The specified file \"{input_path}\" does not exist!")
|
||||
|
||||
lz77_data = input_path.read_bytes()
|
||||
data = libWiiPy.archive.compress_lz77(lz77_data)
|
||||
output_path.write_bytes(data)
|
||||
|
||||
print("LZ77 file compressed!")
|
||||
|
||||
|
||||
def handle_lz77_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 + ".out")
|
||||
|
||||
if not input_path.exists():
|
||||
fatal_error(f"The specified file \"{input_path}\" does not exist!")
|
||||
|
||||
lz77_data = input_path.read_bytes()
|
||||
data = libWiiPy.archive.decompress_lz77(lz77_data)
|
||||
output_path.write_bytes(data)
|
||||
|
||||
print("LZ77 file decompressed!")
|
||||
|
65
commands/archive/theme.py
Normal file
65
commands/archive/theme.py
Normal 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}\"!")
|
40
commands/archive/u8.py
Normal file
40
commands/archive/u8.py
Normal file
@ -0,0 +1,40 @@
|
||||
# "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!")
|
||||
|
||||
u8_data = input_path.read_bytes()
|
||||
# U8 archives are sometimes compressed. In the event that the provided data is LZ77 data, assume it's a compressed
|
||||
# U8 archive and decompress it before continuing. Standard checks will then catch it if it was something else.
|
||||
if u8_data[0:4] == b'LZ77':
|
||||
u8_data = libWiiPy.archive.decompress_lz77(u8_data)
|
||||
|
||||
# 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!")
|
232
commands/nand/emunand.py
Normal file
232
commands/nand/emunand.py
Normal file
@ -0,0 +1,232 @@
|
||||
# "commands/nand/emunand.py" from WiiPy by NinjaCheetah
|
||||
# https://github.com/NinjaCheetah/WiiPy
|
||||
|
||||
import math
|
||||
import pathlib
|
||||
import libWiiPy
|
||||
from modules.core import fatal_error
|
||||
|
||||
|
||||
def handle_emunand_info(args):
|
||||
emunand = libWiiPy.nand.EmuNAND(args.emunand)
|
||||
# Basic info.
|
||||
print(f"EmuNAND Info")
|
||||
print(f" Path: {str(emunand.emunand_root.absolute())}")
|
||||
is_vwii = False
|
||||
try:
|
||||
tmd = emunand.get_title_tmd("0000000100000002")
|
||||
is_vwii = bool(tmd.vwii)
|
||||
print(f" System Menu Version: {libWiiPy.title.title_ver_dec_to_standard(tmd.title_version, '0000000100000002',
|
||||
vwii=is_vwii)}")
|
||||
except FileNotFoundError:
|
||||
print(f" System Menu Version: None")
|
||||
settings_path = emunand.title_dir.joinpath("00000001", "00000002", "data", "setting.txt")
|
||||
if settings_path.exists():
|
||||
settings = libWiiPy.nand.SettingTxt()
|
||||
settings.load(settings_path.read_bytes())
|
||||
print(f" System Region: {settings.area}")
|
||||
else:
|
||||
print(f" System Region: N/A")
|
||||
if is_vwii:
|
||||
print(f" Type: vWii")
|
||||
else:
|
||||
print(f" Type: Wii")
|
||||
categories = emunand.get_installed_titles()
|
||||
installed_count = 0
|
||||
for category in categories:
|
||||
if category.type != "00010000":
|
||||
for _ in category.titles:
|
||||
installed_count += 1
|
||||
print(f" Installed Titles: {installed_count}")
|
||||
total_size = sum(file.stat().st_size for file in emunand.emunand_root.rglob('*'))
|
||||
total_size_blocks = math.ceil(total_size / 131072)
|
||||
print(f" Space Used: {total_size_blocks} blocks ({round(total_size / 1048576, 2)} MB)")
|
||||
print("")
|
||||
|
||||
installed_ioses = []
|
||||
installed_titles = []
|
||||
disc_titles = []
|
||||
for category in categories:
|
||||
if category.type == "00000001":
|
||||
ioses = []
|
||||
for title in category.titles:
|
||||
if title != "00000002":
|
||||
ioses.append(int(title, 16))
|
||||
ioses.sort()
|
||||
installed_ioses = [f"00000001{i:08X}".upper() for i in ioses]
|
||||
elif category.type != "00010000":
|
||||
for title in category.titles:
|
||||
installed_titles.append(f"{category.type}{title}".upper())
|
||||
elif category.type == "00010000":
|
||||
for title in category.titles:
|
||||
if title != "48415A41":
|
||||
disc_titles.append(f"{category.type}{title}".upper())
|
||||
|
||||
print(f"System Titles:")
|
||||
for ios in installed_ioses:
|
||||
if ios[8:] in ["00000100", "00000101", "00000200", "00000201"]:
|
||||
if ios[8:] == "00000100":
|
||||
print(f" BC ({ios.upper()})")
|
||||
elif ios[8:] == "00000101":
|
||||
print(f" MIOS ({ios.upper()})")
|
||||
elif ios[8:] == "00000200":
|
||||
print(f" BC-NAND ({ios.upper()})")
|
||||
elif ios[8:] == "00000201":
|
||||
print(f" BC-WFS ({ios.upper()})")
|
||||
tmd = emunand.get_title_tmd(ios)
|
||||
print(f" Version: {tmd.title_version}")
|
||||
else:
|
||||
print(f" IOS{int(ios[-2:], 16)} ({ios.upper()})")
|
||||
tmd = emunand.get_title_tmd(ios)
|
||||
print(f" Version: {tmd.title_version} ({tmd.title_version_converted})")
|
||||
print("")
|
||||
|
||||
print(f"Installed Titles:")
|
||||
missing_ioses = []
|
||||
for title in installed_titles:
|
||||
ascii_tid = ""
|
||||
try:
|
||||
ascii_tid = (bytes.fromhex(title[8:].replace("00", "30"))).decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
if ascii_tid.isalnum():
|
||||
print(f" {title.upper()} ({ascii_tid})")
|
||||
else:
|
||||
print(f" {title.upper()}")
|
||||
tmd = emunand.get_title_tmd(f"{title}")
|
||||
print(f" Version: {tmd.title_version}")
|
||||
print(f" Required IOS: IOS{int(tmd.ios_tid[-2:], 16)} ({tmd.ios_tid.upper()})", end="", flush=True)
|
||||
if tmd.ios_tid.upper() not in installed_ioses:
|
||||
print(" *")
|
||||
if tmd.ios_tid not in missing_ioses:
|
||||
missing_ioses.append(tmd.ios_tid)
|
||||
else:
|
||||
print("")
|
||||
print("")
|
||||
|
||||
if disc_titles:
|
||||
print(f"Save data was found for the following disc titles:")
|
||||
for disc in disc_titles:
|
||||
ascii_tid = ""
|
||||
try:
|
||||
ascii_tid = (bytes.fromhex(disc[8:].replace("00", "30"))).decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
if ascii_tid.isalnum():
|
||||
print(f" {disc.upper()} ({ascii_tid})")
|
||||
else:
|
||||
print(f" {disc.upper()}")
|
||||
print("")
|
||||
if missing_ioses:
|
||||
print(f"Some titles installed are missing their required IOS. These missing IOSes are marked with \"*\" in the "
|
||||
f"title list above. If these IOSes are not installed, the titles requiring them will not launch. The "
|
||||
f"IOSes required but not installed are:")
|
||||
for missing in missing_ioses:
|
||||
print(f" IOS{int(missing[-2:], 16)} ({missing.upper()})")
|
||||
print("Missing IOSes can be automatically installed using the install-missing command.")
|
||||
|
||||
|
||||
def handle_emunand_install_missing(args):
|
||||
# Get an index of all installed titles, and check their required IOSes. Then compare the required IOSes with the
|
||||
# installed IOSes, and build a list of IOSes we need to obtain.
|
||||
emunand = libWiiPy.nand.EmuNAND(args.emunand)
|
||||
if args.vwii:
|
||||
is_vwii = True
|
||||
else:
|
||||
# Try and detect a vWii System Menu, if one is installed, so that we get vWii IOSes if they're needed.
|
||||
try:
|
||||
tmd = emunand.get_title_tmd("0000000100000002")
|
||||
is_vwii = bool(tmd.vwii)
|
||||
except FileNotFoundError:
|
||||
is_vwii = False
|
||||
categories = emunand.get_installed_titles()
|
||||
installed_ioses = []
|
||||
installed_titles = []
|
||||
for category in categories:
|
||||
if category.type == "00000001":
|
||||
for title in category.titles:
|
||||
if title == "00000002":
|
||||
installed_titles.append(f"{category.type}{title}")
|
||||
else:
|
||||
installed_ioses.append(f"{category.type}{title}")
|
||||
elif category.type != "00010000":
|
||||
for title in category.titles:
|
||||
installed_titles.append(f"{category.type}{title}")
|
||||
missing = []
|
||||
for title in installed_titles:
|
||||
tmd = emunand.get_title_tmd(title)
|
||||
if tmd.ios_tid.upper() not in installed_ioses:
|
||||
if int(tmd.ios_tid[8:], 16) not in missing:
|
||||
missing.append(int(tmd.ios_tid[8:], 16))
|
||||
missing.sort()
|
||||
if is_vwii:
|
||||
missing_ioses = [f"00000007{i:08X}" for i in missing]
|
||||
else:
|
||||
missing_ioses = [f"00000001{i:08X}" for i in missing]
|
||||
if not missing_ioses:
|
||||
print(f"All necessary IOSes are already installed!")
|
||||
return
|
||||
print(f"Missing IOSes:")
|
||||
for ios in missing_ioses:
|
||||
print(f" IOS{int(ios[-2:], 16)} ({ios.upper()})")
|
||||
print("")
|
||||
# Download and then install each missing IOS to the EmuNAND.
|
||||
for ios in missing_ioses:
|
||||
print(f"Downloading IOS{int(ios[-2:], 16)} ({ios.upper()})...")
|
||||
title = libWiiPy.title.download_title(ios)
|
||||
print(f" Installing IOS{int(ios[-2:], 16)} ({ios.upper()}) v{title.tmd.title_version}...")
|
||||
emunand.install_title(title)
|
||||
print(f" Installed IOS{int(ios[-2:], 16)} ({ios.upper()}) v{title.tmd.title_version}!")
|
||||
print(f"\nAll missing IOSes have been installed!")
|
||||
|
||||
|
||||
def handle_emunand_title(args):
|
||||
emunand = libWiiPy.nand.EmuNAND(args.emunand)
|
||||
if args.skip_hash:
|
||||
skip_hash = True
|
||||
else:
|
||||
skip_hash = False
|
||||
|
||||
# Code for if the --install argument was passed.
|
||||
if args.install:
|
||||
input_path = pathlib.Path(args.install)
|
||||
|
||||
if not input_path.exists():
|
||||
fatal_error("The specified WAD file does not exist!")
|
||||
|
||||
if input_path.is_dir():
|
||||
wad_files = list(input_path.glob("*.[wW][aA][dD]"))
|
||||
if not wad_files:
|
||||
fatal_error("No WAD files were found in the provided input directory!")
|
||||
wad_count = 0
|
||||
for wad in wad_files:
|
||||
title = libWiiPy.title.Title()
|
||||
title.load_wad(open(wad, "rb").read())
|
||||
try:
|
||||
emunand.install_title(title, skip_hash=skip_hash)
|
||||
wad_count += 1
|
||||
except ValueError:
|
||||
print(f"WAD {wad} could not be installed!")
|
||||
print(f"Successfully installed {wad_count} WAD(s) to EmuNAND!")
|
||||
else:
|
||||
title = libWiiPy.title.Title()
|
||||
title.load_wad(open(input_path, "rb").read())
|
||||
emunand.install_title(title, skip_hash=skip_hash)
|
||||
print("Successfully installed WAD to EmuNAND!")
|
||||
|
||||
# Code for if the --uninstall argument was passed.
|
||||
elif args.uninstall:
|
||||
input_str = args.uninstall
|
||||
if pathlib.Path(input_str).exists():
|
||||
title = libWiiPy.title.Title()
|
||||
title.load_wad(pathlib.Path(input_str).read_bytes())
|
||||
target_tid = title.tmd.title_id
|
||||
else:
|
||||
target_tid = input_str
|
||||
|
||||
if len(target_tid) != 16:
|
||||
fatal_error("The provided Title ID is invalid! Title IDs must be 16 characters long.")
|
||||
|
||||
emunand.uninstall_title(target_tid.lower())
|
||||
|
||||
print("Title uninstalled from EmuNAND!")
|
103
commands/nand/setting.py
Normal file
103
commands/nand/setting.py
Normal file
@ -0,0 +1,103 @@
|
||||
# "commands/nand/setting.py" from WiiPy by NinjaCheetah
|
||||
# https://github.com/NinjaCheetah/WiiPy
|
||||
|
||||
import pathlib
|
||||
import libWiiPy
|
||||
from modules.core import fatal_error
|
||||
|
||||
|
||||
def handle_setting_decrypt(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.stem + "_dec" + input_path.suffix)
|
||||
|
||||
if not input_path.exists():
|
||||
fatal_error("The specified setting file does not exist!")
|
||||
|
||||
# Load and decrypt the provided file.
|
||||
setting = libWiiPy.nand.SettingTxt()
|
||||
setting.load(input_path.read_bytes())
|
||||
# Write out the decrypted data.
|
||||
output_path.write_text(setting.dump_decrypted())
|
||||
print("Successfully decrypted setting.txt!")
|
||||
|
||||
|
||||
def handle_setting_encrypt(args):
|
||||
input_path = pathlib.Path(args.input)
|
||||
if args.output is not None:
|
||||
output_path = pathlib.Path(args.output)
|
||||
else:
|
||||
output_path = pathlib.Path("setting.txt")
|
||||
|
||||
if not input_path.exists():
|
||||
fatal_error("The specified setting file does not exist!")
|
||||
|
||||
# Load and encrypt the provided file.
|
||||
setting = libWiiPy.nand.SettingTxt()
|
||||
setting.load_decrypted(input_path.read_text())
|
||||
# Write out the encrypted data.
|
||||
output_path.write_bytes(setting.dump())
|
||||
print("Successfully encrypted setting.txt!")
|
||||
|
||||
|
||||
def handle_setting_gen(args):
|
||||
# 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:
|
||||
fatal_error("The provided Serial Number is not valid!")
|
||||
try:
|
||||
int(args.serno[-9:])
|
||||
except ValueError:
|
||||
fatal_error("The provided Serial Number is not valid!")
|
||||
prefix = args.serno[:-9]
|
||||
# Detect the console revision based on the SN.
|
||||
match prefix[0].upper():
|
||||
case "L":
|
||||
revision = "RVL-001"
|
||||
case "K":
|
||||
revision = "RVL-101"
|
||||
case "H":
|
||||
revision = "RVL-201"
|
||||
case _:
|
||||
revision = "RVL-001"
|
||||
# Validate the region, and then validate the SN based on the region. USA has a two-letter prefix for a total length
|
||||
# of 11 characters, while other regions have a three-letter prefix for a total length of 12 characters.
|
||||
valid_regions = ["USA", "EUR", "JPN", "KOR"]
|
||||
if args.region not in valid_regions:
|
||||
fatal_error(f"The provided region \"{args.region}\" is not valid!")
|
||||
if len(prefix) == 2 and args.region != "USA":
|
||||
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":
|
||||
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.
|
||||
video = ""
|
||||
game = ""
|
||||
match args.region:
|
||||
case "USA":
|
||||
video = "NTSC"
|
||||
game = "US"
|
||||
case "EUR":
|
||||
video = "PAL"
|
||||
game = "EU"
|
||||
case "JPN":
|
||||
video = "NTSC"
|
||||
game = "JP"
|
||||
case "KOR":
|
||||
video = "NTSC"
|
||||
game = "KR"
|
||||
# Create a new SettingTxt object and load the settings into it.
|
||||
setting = libWiiPy.nand.SettingTxt()
|
||||
setting.area = args.region
|
||||
setting.model = f"{revision}({args.region})"
|
||||
setting.dvd = 0
|
||||
setting.mpch = "0x7FFE"
|
||||
setting.code = prefix
|
||||
setting.serial_number = args.serno[-9:]
|
||||
setting.video = video
|
||||
setting.game = game
|
||||
# Write out the setting.txt file.
|
||||
open("setting.txt", "wb").write(setting.dump())
|
||||
print(f"Successfully created setting.txt for console with serial number {args.serno}!")
|
145
commands/title/ciosbuild.py
Normal file
145
commands/title/ciosbuild.py
Normal file
@ -0,0 +1,145 @@
|
||||
# "commands/title/ciosbuild.py" from WiiPy by NinjaCheetah
|
||||
# https://github.com/NinjaCheetah/WiiPy
|
||||
|
||||
import io
|
||||
import os
|
||||
import xml.etree.ElementTree as ET
|
||||
import pathlib
|
||||
import libWiiPy
|
||||
from modules.core import fatal_error
|
||||
|
||||
|
||||
def build_cios(args):
|
||||
base_path = pathlib.Path(args.base)
|
||||
map_path = pathlib.Path(args.map)
|
||||
if args.modules:
|
||||
modules_path = pathlib.Path(args.modules)
|
||||
else:
|
||||
modules_path = pathlib.Path(os.getcwd())
|
||||
output_path = pathlib.Path(args.output)
|
||||
|
||||
if not base_path.exists():
|
||||
fatal_error(f"The specified base IOS file \"{base_path}\" does not exist!")
|
||||
if not map_path.exists():
|
||||
fatal_error(f"The specified cIOS map file \"{map_path}\" does not exist!")
|
||||
if not modules_path.exists():
|
||||
fatal_error(f"The specified cIOS modules directory \"{modules_path}\" does not exist!")
|
||||
|
||||
title = libWiiPy.title.Title()
|
||||
title.load_wad(open(base_path, 'rb').read())
|
||||
|
||||
cios_tree = ET.parse(map_path)
|
||||
cios_root = cios_tree.getroot()
|
||||
|
||||
# Iterate over all <ciosgroup> tags to find the cIOS that was requested, and return an error if it doesn't match
|
||||
# any of the groups in the provided map.
|
||||
target_cios = None
|
||||
for child in cios_root:
|
||||
cios = child.get("name")
|
||||
if args.cios_ver == cios:
|
||||
target_cios = child
|
||||
break
|
||||
if target_cios is None:
|
||||
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
|
||||
# that the version of the base in the map matches the version of the IOS WAD.
|
||||
target_base = None
|
||||
provided_base = int(title.tmd.title_id[-2:], 16)
|
||||
for child in target_cios:
|
||||
base = int(child.get("ios"))
|
||||
if base == provided_base:
|
||||
target_base = child
|
||||
break
|
||||
if target_base is None:
|
||||
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"))
|
||||
if title.tmd.title_version != base_version:
|
||||
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
|
||||
# the patches listed in them to the content.
|
||||
print("Patching existing modules...")
|
||||
for content in target_base.findall("content"):
|
||||
patches = content.findall("patch")
|
||||
if patches:
|
||||
cid = int(content.get("id"), 16)
|
||||
dec_content = title.get_content_by_cid(cid)
|
||||
content_index = title.content.get_index_from_cid(cid)
|
||||
with io.BytesIO(dec_content) as content_data:
|
||||
for patch in patches:
|
||||
# Read patch info from the map. This requires some conversion since ciosmap files seem to use a
|
||||
# comma-separated list of bytes.
|
||||
offset = int(patch.get("offset"), 16)
|
||||
original_data = b''
|
||||
original_data_map = patch.get("originalbytes").split(",")
|
||||
for byte in original_data_map:
|
||||
original_data += bytes.fromhex(byte[2:])
|
||||
new_data = b''
|
||||
new_data_map = patch.get("newbytes").split(",")
|
||||
for byte in new_data_map:
|
||||
new_data += bytes.fromhex(byte[2:])
|
||||
# Seek to the target offset and apply the patches. One last sanity check to ensure this
|
||||
# original data exists.
|
||||
if original_data in dec_content:
|
||||
content_data.seek(offset)
|
||||
content_data.write(new_data)
|
||||
else:
|
||||
fatal_error("An error occurred while patching! Please make sure your base IOS is valid.")
|
||||
content_data.seek(0x0)
|
||||
dec_content = content_data.read()
|
||||
# Set the content in the title to the newly-patched content, and set the type to normal.
|
||||
title.set_content(dec_content, content_index, content_type=libWiiPy.title.ContentType.NORMAL)
|
||||
|
||||
# Next phase of cIOS building is to add the required extra modules.
|
||||
print("Adding required additional modules...")
|
||||
for content in target_base.findall("content"):
|
||||
target_module = content.get("module")
|
||||
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.
|
||||
cid = int(content.get("id"), 16)
|
||||
target_path = modules_path.joinpath(target_module + ".app")
|
||||
if not target_path.exists():
|
||||
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)
|
||||
else:
|
||||
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.
|
||||
slot = args.slot
|
||||
if 3 <= slot <= 255:
|
||||
tid = title.tmd.title_id[:-2] + f"{slot:02X}"
|
||||
title.set_title_id(tid)
|
||||
else:
|
||||
fatal_error(f"The specified slot \"{slot}\" is not valid!")
|
||||
try:
|
||||
title.set_title_version(args.version)
|
||||
except ValueError:
|
||||
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.
|
||||
title.fakesign()
|
||||
|
||||
# Write the new cIOS to the specified output path.
|
||||
output_path.write_bytes(title.dump_wad())
|
||||
|
||||
print(f"Successfully built cIOS \"{args.cios_ver}\"!")
|
38
commands/title/fakesign.py
Normal file
38
commands/title/fakesign.py
Normal file
@ -0,0 +1,38 @@
|
||||
# "commands/title/fakesign.py" from WiiPy by NinjaCheetah
|
||||
# https://github.com/NinjaCheetah/WiiPy
|
||||
|
||||
import pathlib
|
||||
import libWiiPy
|
||||
from modules.core import fatal_error
|
||||
|
||||
|
||||
def handle_fakesign(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 input file \"{input_path}\" does not exist!")
|
||||
|
||||
if input_path.suffix.lower() == ".tmd":
|
||||
tmd = libWiiPy.title.TMD()
|
||||
tmd.load(input_path.read_bytes())
|
||||
tmd.fakesign()
|
||||
output_path.write_bytes(tmd.dump())
|
||||
print("TMD fakesigned successfully!")
|
||||
elif input_path.suffix.lower() == ".tik":
|
||||
tik = libWiiPy.title.Ticket()
|
||||
tik.load(input_path.read_bytes())
|
||||
tik.fakesign()
|
||||
output_path.write_bytes(tik.dump())
|
||||
print("Ticket fakesigned successfully!")
|
||||
elif input_path.suffix.lower() == ".wad":
|
||||
title = libWiiPy.title.Title()
|
||||
title.load_wad(input_path.read_bytes())
|
||||
title.fakesign()
|
||||
output_path.write_bytes(title.dump_wad())
|
||||
print("WAD fakesigned successfully!")
|
||||
else:
|
||||
fatal_error("The provided file does not appear to be a TMD, Ticket, or WAD and cannot be fakesigned!")
|
257
commands/title/info.py
Normal file
257
commands/title/info.py
Normal file
@ -0,0 +1,257 @@
|
||||
# "commands/title/info.py" from WiiPy by NinjaCheetah
|
||||
# https://github.com/NinjaCheetah/WiiPy
|
||||
|
||||
import binascii
|
||||
import pathlib
|
||||
import re
|
||||
import libWiiPy
|
||||
from modules.core import fatal_error
|
||||
|
||||
|
||||
def _print_tmd_info(tmd: libWiiPy.title.TMD, signing_cert=None):
|
||||
# Get all important keys from the TMD and print them out nicely.
|
||||
print("Title Info")
|
||||
ascii_tid = ""
|
||||
try:
|
||||
ascii_tid = (bytes.fromhex(tmd.title_id[8:].replace("00", "30"))).decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
pattern = r"^[a-z0-9!@#$%^&*]{4}$"
|
||||
if re.fullmatch(pattern, ascii_tid, re.IGNORECASE):
|
||||
print(f" Title ID: {tmd.title_id.upper()} ({ascii_tid})")
|
||||
else:
|
||||
print(f" Title ID: {tmd.title_id.upper()}")
|
||||
# This type of version number really only applies to the System Menu and IOS.
|
||||
if tmd.title_id.startswith("00000001"):
|
||||
if tmd.title_id == "0000000100000001":
|
||||
print(f" Title Version: {tmd.title_version} (boot2v{tmd.title_version})")
|
||||
else:
|
||||
print(f" Title Version: {tmd.title_version} ({tmd.title_version_converted})")
|
||||
else:
|
||||
print(f" Title Version: {tmd.title_version}")
|
||||
print(f" TMD Version: {tmd.tmd_version}")
|
||||
# IOSes just have an all-zero TID, so don't bothering showing that.
|
||||
if tmd.ios_tid == "0000000000000000":
|
||||
print(f" Required IOS: N/A")
|
||||
elif tmd.title_id == "0000000100000001":
|
||||
pass
|
||||
else:
|
||||
print(f" Required IOS: IOS{int(tmd.ios_tid[-2:], 16)} ({tmd.ios_tid.upper()})")
|
||||
if tmd.signature_issuer.find("CP00000004") != -1:
|
||||
print(f" Certificate: CP00000004 (Retail)")
|
||||
print(f" Certificate Issuer: Root-CA00000001 (Retail)")
|
||||
elif tmd.signature_issuer.find("CP00000007") != -1:
|
||||
print(f" Certificate: CP00000007 (Development)")
|
||||
print(f" Certificate Issuer: Root-CA00000002 (Development)")
|
||||
elif tmd.signature_issuer.find("CP00000005") != -1:
|
||||
print(f" Certificate: CP00000005 (Development/Unknown)")
|
||||
print(f" Certificate Issuer: Root-CA00000002 (Development)")
|
||||
elif tmd.signature_issuer.find("CP10000000") != -1:
|
||||
print(f" Certificate: CP10000000 (Arcade)")
|
||||
print(f" Certificate Issuer: Root-CA10000000 (Arcade)")
|
||||
else:
|
||||
print(f" Certificate Info: {tmd.signature_issuer} (Unknown)")
|
||||
if tmd.title_id == "0000000100000002":
|
||||
match tmd.title_version_converted[-1:]:
|
||||
case "U":
|
||||
region = "USA"
|
||||
case "E":
|
||||
region = "EUR"
|
||||
case "J":
|
||||
region = "JPN"
|
||||
case "K":
|
||||
region = "KOR"
|
||||
case _:
|
||||
region = "None"
|
||||
elif tmd.get_title_type() == "System":
|
||||
region = "None"
|
||||
else:
|
||||
region = tmd.get_title_region()
|
||||
print(f" Region: {region}")
|
||||
print(f" Title Type: {tmd.get_title_type()}")
|
||||
print(f" vWii Title: {bool(tmd.vwii)}")
|
||||
print(f" DVD Video Access: {tmd.get_access_right(tmd.AccessFlags.DVD_VIDEO)}")
|
||||
print(f" AHB Access: {tmd.get_access_right(tmd.AccessFlags.AHB)}")
|
||||
if signing_cert is not None:
|
||||
try:
|
||||
if libWiiPy.title.verify_tmd_sig(signing_cert, tmd):
|
||||
signing_str = "Valid (Unmodified TMD)"
|
||||
elif tmd.get_is_fakesigned():
|
||||
signing_str = "Fakesigned"
|
||||
else:
|
||||
signing_str = "Invalid (Modified TMD)"
|
||||
except ValueError:
|
||||
if tmd.get_is_fakesigned():
|
||||
signing_str = "Fakesigned"
|
||||
else:
|
||||
signing_str = "Invalid (Modified TMD)"
|
||||
print(f" Signature: {signing_str}")
|
||||
else:
|
||||
print(f" Fakesigned: {tmd.get_is_fakesigned()}")
|
||||
# Iterate over the content and print their details.
|
||||
print("\nContent Info")
|
||||
print(f" Total Contents: {tmd.num_contents}")
|
||||
print(f" Boot Content Index: {tmd.boot_index}")
|
||||
print(" Content Records:")
|
||||
for content in tmd.content_records:
|
||||
print(f" Content Index: {content.index}")
|
||||
print(f" Content ID: " + f"{content.content_id:08X}".lower())
|
||||
print(f" Content Type: {tmd.get_content_type(content.index)}")
|
||||
print(f" Content Size: {content.content_size} bytes")
|
||||
print(f" Content Hash: {content.content_hash.decode()}")
|
||||
|
||||
|
||||
def _print_ticket_info(ticket: libWiiPy.title.Ticket, signing_cert=None):
|
||||
# Get all important keys from the TMD and print them out nicely.
|
||||
print(f"Ticket Info")
|
||||
ascii_tid = ""
|
||||
try:
|
||||
ascii_tid = str(bytes.fromhex(binascii.hexlify(ticket.title_id).decode()[8:].replace("00", "30")).decode("ascii"))
|
||||
except UnicodeDecodeError or binascii.Error:
|
||||
pass
|
||||
pattern = r"^[a-z0-9!@#$%^&*]{4}$"
|
||||
if re.fullmatch(pattern, ascii_tid, re.IGNORECASE):
|
||||
print(f" Title ID: {binascii.hexlify(ticket.title_id).decode().upper()} ({ascii_tid})")
|
||||
else:
|
||||
print(f" Title ID: {binascii.hexlify(ticket.title_id).decode().upper()}")
|
||||
# This type of version number really only applies to the System Menu and IOS.
|
||||
if ticket.title_id.decode().startswith("00000001"):
|
||||
if ticket.title_id.decode() == "0000000100000001":
|
||||
print(f" Title Version: {ticket.title_version} (boot2v{ticket.title_version})")
|
||||
else:
|
||||
print(f" Title Version: {ticket.title_version} "
|
||||
f"({libWiiPy.title.title_ver_dec_to_standard(ticket.title_version, ticket.title_id.decode())})")
|
||||
else:
|
||||
print(f" Title Version: {ticket.title_version}")
|
||||
print(f" Ticket Version: {ticket.ticket_version}")
|
||||
if ticket.signature_issuer.find("XS00000003") != -1:
|
||||
print(f" Certificate: XS00000003 (Retail)")
|
||||
print(f" Certificate Issuer: Root-CA00000001 (Retail)")
|
||||
elif ticket.signature_issuer.find("XS00000006") != -1:
|
||||
print(f" Certificate: XS00000006 (Development)")
|
||||
print(f" Certificate Issuer: Root-CA00000002 (Development)")
|
||||
elif ticket.signature_issuer.find("XS00000004") != -1:
|
||||
print(f" Certificate: XS00000004 (Development/Unknown)")
|
||||
print(f" Certificate Issuer: Root-CA00000002 (Development)")
|
||||
else:
|
||||
print(f" Certificate Info: {ticket.signature_issuer} (Unknown)")
|
||||
match ticket.common_key_index:
|
||||
case 0:
|
||||
if ticket.is_dev:
|
||||
key = "Common (Development)"
|
||||
else:
|
||||
key = "Common (Retail)"
|
||||
case 1:
|
||||
key = "Korean"
|
||||
case 2:
|
||||
key = "vWii"
|
||||
case _:
|
||||
key = "Unknown (Likely Common)"
|
||||
print(f" Decryption Key: {key}")
|
||||
print(f" Title Key (Encrypted): {binascii.hexlify(ticket.title_key_enc).decode()}")
|
||||
print(f" Title Key (Decrypted): {binascii.hexlify(ticket.get_title_key()).decode()}")
|
||||
if signing_cert is not None:
|
||||
try:
|
||||
if libWiiPy.title.verify_ticket_sig(signing_cert, ticket):
|
||||
signing_str = "Valid (Unmodified Ticket)"
|
||||
elif ticket.get_is_fakesigned():
|
||||
signing_str = "Fakesigned"
|
||||
else:
|
||||
signing_str = "Invalid (Modified Ticket)"
|
||||
except ValueError:
|
||||
if ticket.get_is_fakesigned():
|
||||
signing_str = "Fakesigned"
|
||||
else:
|
||||
signing_str = "Invalid (Modified Ticket)"
|
||||
print(f" Signature: {signing_str}")
|
||||
else:
|
||||
print(f" Fakesigned: {ticket.get_is_fakesigned()}")
|
||||
|
||||
|
||||
def _print_wad_info(title: libWiiPy.title.Title):
|
||||
print(f"WAD Info")
|
||||
banner_data = title.get_content_by_index(0)
|
||||
banner_u8 = libWiiPy.archive.U8Archive()
|
||||
try:
|
||||
banner_u8.load(banner_data)
|
||||
if banner_u8.imet_header.magic != "":
|
||||
channel_title = banner_u8.imet_header.get_channel_names(banner_u8.imet_header.LocalizedTitles.TITLE_ENGLISH)
|
||||
print(f" Channel Name: {channel_title}")
|
||||
except TypeError:
|
||||
pass
|
||||
match title.wad.wad_type:
|
||||
case "Is":
|
||||
print(f" WAD Type: Standard Installable")
|
||||
case "ib":
|
||||
print(f" WAD Type: boot2")
|
||||
case _:
|
||||
print(f" WAD Type: Unknown ({title.wad.wad_type})")
|
||||
min_size_blocks = title.get_title_size_blocks()
|
||||
max_size_blocks = title.get_title_size_blocks(absolute=True)
|
||||
if min_size_blocks == max_size_blocks:
|
||||
print(f" Installed Size: {min_size_blocks} blocks")
|
||||
else:
|
||||
print(f" Installed Size: {min_size_blocks}-{max_size_blocks} blocks")
|
||||
min_size = round(title.get_title_size() / 1048576, 2)
|
||||
max_size = round(title.get_title_size(absolute=True) / 1048576, 2)
|
||||
if min_size == max_size:
|
||||
print(f" Installed Size (MB): {min_size} MB")
|
||||
else:
|
||||
print(f" Installed Size (MB): {min_size}-{max_size} MB")
|
||||
print(f" Has Meta/Footer: {bool(title.wad.wad_meta_size)}")
|
||||
print(f" Has CRL: {bool(title.wad.wad_crl_size)}")
|
||||
tmd_cert = None
|
||||
ticket_cert = None
|
||||
try:
|
||||
tmd_cert = title.cert_chain.tmd_cert
|
||||
ticket_cert = title.cert_chain.ticket_cert
|
||||
if title.get_is_signed():
|
||||
signing_str = "Legitimate (Unmodified TMD + Ticket)"
|
||||
elif title.get_is_fakesigned():
|
||||
signing_str = "Fakesigned"
|
||||
elif (libWiiPy.title.verify_tmd_sig(tmd_cert, title.tmd)
|
||||
and not libWiiPy.title.verify_ticket_sig(ticket_cert, title.ticket)):
|
||||
signing_str = "Piratelegit (Unmodified TMD, Modified Ticket)"
|
||||
elif (not libWiiPy.title.verify_tmd_sig(tmd_cert, title.tmd)
|
||||
and libWiiPy.title.verify_ticket_sig(ticket_cert, title.ticket)):
|
||||
signing_str = "Edited (Modified TMD, Unmodified Ticket)"
|
||||
else:
|
||||
signing_str = "Illegitimate (Modified TMD + Ticket)"
|
||||
except ValueError:
|
||||
signing_str = "Illegitimate (Modified TMD + Ticket)"
|
||||
print(f" Signing Status: {signing_str}")
|
||||
print("")
|
||||
_print_ticket_info(title.ticket, ticket_cert)
|
||||
print("")
|
||||
_print_tmd_info(title.tmd, tmd_cert)
|
||||
|
||||
|
||||
def handle_info(args):
|
||||
input_path = pathlib.Path(args.input)
|
||||
|
||||
if not input_path.exists():
|
||||
fatal_error(f"The specified input file \"{input_path}\" does not exist!")
|
||||
|
||||
if (input_path.suffix.lower() == ".tmd" or input_path.name == "tmd.bin" or
|
||||
re.match("tmd.?[0-9]*", input_path.name)):
|
||||
tmd = libWiiPy.title.TMD()
|
||||
tmd.load(input_path.read_bytes())
|
||||
_print_tmd_info(tmd)
|
||||
elif input_path.suffix.lower() == ".tik" or input_path.name == "ticket.bin" or input_path.name == "cetk":
|
||||
tik = libWiiPy.title.Ticket()
|
||||
tik.load(input_path.read_bytes())
|
||||
_print_ticket_info(tik)
|
||||
elif input_path.suffix.lower() == ".wad":
|
||||
title = libWiiPy.title.Title()
|
||||
title.load_wad(input_path.read_bytes())
|
||||
_print_wad_info(title)
|
||||
else:
|
||||
# Try file types that have a matchable magic number if we can't tell the easy way.
|
||||
magic_number = open(input_path, "rb").read(8)
|
||||
if magic_number == b'\x00\x00\x00\x20\x49\x73\x00\x00' or magic_number == b'\x00\x00\x00\x20\x69\x62\x00\x00':
|
||||
title = libWiiPy.title.Title()
|
||||
title.load_wad(input_path.read_bytes())
|
||||
_print_wad_info(title)
|
||||
return
|
||||
else:
|
||||
fatal_error("This does not appear to be a supported file type! No info can be provided.")
|
127
commands/title/iospatcher.py
Normal file
127
commands/title/iospatcher.py
Normal file
@ -0,0 +1,127 @@
|
||||
# "commands/title/iospatcher.py" from WiiPy by NinjaCheetah
|
||||
# https://github.com/NinjaCheetah/WiiPy
|
||||
|
||||
import pathlib
|
||||
import libWiiPy
|
||||
from modules.core import fatal_error
|
||||
|
||||
|
||||
def _patch_fakesigning(ios_patcher: libWiiPy.title.IOSPatcher) -> int:
|
||||
print("Applying fakesigning patch... ", end="", flush=True)
|
||||
count = ios_patcher.patch_fakesigning()
|
||||
if count == 1:
|
||||
print(f"{count} patch applied")
|
||||
else:
|
||||
print(f"{count} patches applied")
|
||||
return count
|
||||
|
||||
|
||||
def _patch_es_identify(ios_patcher: libWiiPy.title.IOSPatcher) -> int:
|
||||
print("Applying ES_Identify access patch... ", end="", flush=True)
|
||||
count = ios_patcher.patch_es_identify()
|
||||
if count == 1:
|
||||
print(f"{count} patch applied")
|
||||
else:
|
||||
print(f"{count} patches applied")
|
||||
return count
|
||||
|
||||
|
||||
def _patch_nand_access(ios_patcher: libWiiPy.title.IOSPatcher) -> int:
|
||||
print("Applying /dev/flash access patch... ", end="", flush=True)
|
||||
count = ios_patcher.patch_nand_access()
|
||||
if count == 1:
|
||||
print(f"{count} patch applied")
|
||||
else:
|
||||
print(f"{count} patches applied")
|
||||
return count
|
||||
|
||||
|
||||
def _patch_version_downgrading(ios_patcher: libWiiPy.title.IOSPatcher) -> int:
|
||||
print("Applying version downgrading patch... ", end="", flush=True)
|
||||
count = ios_patcher.patch_version_downgrading()
|
||||
if count == 1:
|
||||
print(f"{count} patch applied")
|
||||
else:
|
||||
print(f"{count} patches applied")
|
||||
return count
|
||||
|
||||
|
||||
def _patch_drive_inquiry(ios_patcher: libWiiPy.title.IOSPatcher) -> int:
|
||||
print("\n/!\\ WARNING! /!\\\n"
|
||||
"This drive inquiry patch is experimental, and may introduce unexpected side effects on some consoles.\n")
|
||||
print("Applying drive inquiry patch... ", end="", flush=True)
|
||||
count = ios_patcher.patch_drive_inquiry()
|
||||
if count == 1:
|
||||
print(f"{count} patch applied")
|
||||
else:
|
||||
print(f"{count} patches applied")
|
||||
return count
|
||||
|
||||
|
||||
def handle_iospatch(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 IOS file \"{input_path}\" does not exist!")
|
||||
|
||||
title = libWiiPy.title.Title()
|
||||
title.load_wad(input_path.read_bytes())
|
||||
|
||||
tid = title.tmd.title_id
|
||||
if tid[:8] != "00000001" or tid[8:] == "00000001" or tid[8:] == "00000002":
|
||||
fatal_error(f"The provided WAD does not appear to contain an IOS! No patches can be applied.")
|
||||
|
||||
patch_count = 0
|
||||
|
||||
if args.version is not None:
|
||||
title.set_title_version(args.version)
|
||||
print(f"Title version set to {args.version}!")
|
||||
|
||||
if args.slot is not None:
|
||||
slot = args.slot
|
||||
if 3 <= slot <= 255:
|
||||
tid = title.tmd.title_id[:-2] + f"{slot:02X}"
|
||||
title.set_title_id(tid)
|
||||
print(f"IOS slot set to {slot}!")
|
||||
|
||||
ios_patcher = libWiiPy.title.IOSPatcher()
|
||||
ios_patcher.load(title)
|
||||
|
||||
if args.all is True:
|
||||
patch_count += _patch_fakesigning(ios_patcher)
|
||||
patch_count += _patch_es_identify(ios_patcher)
|
||||
patch_count += _patch_nand_access(ios_patcher)
|
||||
patch_count += _patch_version_downgrading(ios_patcher)
|
||||
else:
|
||||
if args.fakesigning is True:
|
||||
patch_count += _patch_fakesigning(ios_patcher)
|
||||
if args.es_identify is True:
|
||||
patch_count += _patch_es_identify(ios_patcher)
|
||||
if args.nand_access is True:
|
||||
patch_count += _patch_nand_access(ios_patcher)
|
||||
if args.version_downgrading is True:
|
||||
patch_count += _patch_version_downgrading(ios_patcher)
|
||||
if args.drive_inquiry is True:
|
||||
patch_count += _patch_drive_inquiry(ios_patcher)
|
||||
|
||||
print(f"\nTotal patches applied: {patch_count}")
|
||||
|
||||
if patch_count == 0 and args.version is None and args.slot is None:
|
||||
fatal_error("No patches were applied! Please specify patches to apply, and ensure that selected patches are "
|
||||
"compatible with this IOS.")
|
||||
|
||||
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.
|
||||
if args.no_shared:
|
||||
ios_patcher.title.content.content_records[ios_patcher.es_module_index].content_type = 1
|
||||
if ios_patcher.dip_module_index != -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
|
||||
output_path.write_bytes(ios_patcher.title.dump_wad())
|
||||
|
||||
print("IOS successfully patched!")
|
@ -1,149 +1,17 @@
|
||||
# "nus.py" from WiiPy by NinjaCheetah
|
||||
# "commands/title/nus.py" from WiiPy by NinjaCheetah
|
||||
# https://github.com/NinjaCheetah/WiiPy
|
||||
|
||||
import os
|
||||
import hashlib
|
||||
import pathlib
|
||||
import binascii
|
||||
import libWiiPy
|
||||
|
||||
|
||||
def handle_nus_title(args):
|
||||
title_version = None
|
||||
wad_file = None
|
||||
output_dir = None
|
||||
can_decrypt = False
|
||||
tid = args.tid
|
||||
if args.wii:
|
||||
wiiu_nus_enabled = False
|
||||
else:
|
||||
wiiu_nus_enabled = True
|
||||
|
||||
# Check if --version was passed, because it'll be None if it wasn't.
|
||||
if args.version is not None:
|
||||
try:
|
||||
title_version = int(args.version)
|
||||
except ValueError:
|
||||
print("Enter a valid integer for the Title Version.")
|
||||
return
|
||||
|
||||
# If --wad was passed, check to make sure the path is okay.
|
||||
if args.wad is not None:
|
||||
wad_file = pathlib.Path(args.wad)
|
||||
if wad_file.suffix != ".wad":
|
||||
wad_file = wad_file.with_suffix(".wad")
|
||||
|
||||
# If --output was passed, make sure the directory either doesn't exist or is empty.
|
||||
if args.output is not None:
|
||||
output_dir = pathlib.Path(args.output)
|
||||
if output_dir.exists():
|
||||
if output_dir.is_dir() and next(os.scandir(output_dir), None):
|
||||
raise ValueError("Output folder is not empty!")
|
||||
elif output_dir.is_file():
|
||||
raise ValueError("A file already exists with the provided directory name!")
|
||||
else:
|
||||
os.mkdir(output_dir)
|
||||
|
||||
# Download the title from the NUS. This is done "manually" (as opposed to using download_title()) so that we can
|
||||
# provide verbose output.
|
||||
title = libWiiPy.title.Title()
|
||||
|
||||
# Announce the title being downloaded, and the version if applicable.
|
||||
if title_version is not None:
|
||||
print("Downloading title " + tid + " v" + str(title_version) + ", please wait...")
|
||||
else:
|
||||
print("Downloading title " + tid + " vLatest, please wait...")
|
||||
print(" - Downloading and parsing TMD...")
|
||||
# Download a specific TMD version if a version was specified, otherwise just download the latest TMD.
|
||||
if title_version is not None:
|
||||
title.load_tmd(libWiiPy.title.download_tmd(tid, title_version, wiiu_endpoint=wiiu_nus_enabled))
|
||||
else:
|
||||
title.load_tmd(libWiiPy.title.download_tmd(tid, wiiu_endpoint=wiiu_nus_enabled))
|
||||
title_version = title.tmd.title_version
|
||||
# Write out the TMD to a file.
|
||||
if output_dir is not None:
|
||||
tmd_out = open(output_dir.joinpath("tmd." + str(title_version)), "wb")
|
||||
tmd_out.write(title.tmd.dump())
|
||||
tmd_out.close()
|
||||
|
||||
# Download the ticket, if we can.
|
||||
print(" - Downloading and parsing Ticket...")
|
||||
try:
|
||||
title.load_ticket(libWiiPy.title.download_ticket(tid, wiiu_endpoint=wiiu_nus_enabled))
|
||||
can_decrypt = True
|
||||
if output_dir is not None:
|
||||
ticket_out = open(output_dir.joinpath("tik"), "wb")
|
||||
ticket_out.write(title.ticket.dump())
|
||||
ticket_out.close()
|
||||
except ValueError:
|
||||
# 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.
|
||||
print(" - No Ticket is available!")
|
||||
if wad_file is not None and output_dir is None:
|
||||
print("--wad was passed, but this title cannot be packed into a WAD!")
|
||||
return
|
||||
|
||||
# Load the content records from the TMD, and begin iterating over the records.
|
||||
title.load_content_records()
|
||||
content_list = []
|
||||
for content in range(len(title.tmd.content_records)):
|
||||
# Generate the content file name by converting the Content ID to hex and then removing the 0x.
|
||||
content_file_name = hex(title.tmd.content_records[content].content_id)[2:]
|
||||
while len(content_file_name) < 8:
|
||||
content_file_name = "0" + content_file_name
|
||||
print(" - Downloading content " + str(content + 1) + " of " +
|
||||
str(len(title.tmd.content_records)) + " (Content ID: " +
|
||||
str(title.tmd.content_records[content].content_id) + ", Size: " +
|
||||
str(title.tmd.content_records[content].content_size) + " bytes)...")
|
||||
content_list.append(libWiiPy.title.download_content(tid, title.tmd.content_records[content].content_id,
|
||||
wiiu_endpoint=wiiu_nus_enabled))
|
||||
print(" - Done!")
|
||||
# If we're supposed to be outputting to a folder, then write these files out.
|
||||
if output_dir is not None:
|
||||
enc_content_out = open(output_dir.joinpath(content_file_name), "wb")
|
||||
enc_content_out.write(content_list[content])
|
||||
enc_content_out.close()
|
||||
title.content.content_list = content_list
|
||||
|
||||
# Try to decrypt the contents for this title if a ticket was available.
|
||||
if output_dir is not None:
|
||||
if can_decrypt is True:
|
||||
for content in range(len(title.tmd.content_records)):
|
||||
print(" - Decrypting content " + str(content + 1) + " of " + str(len(title.tmd.content_records)) +
|
||||
" (Content ID: " + str(title.tmd.content_records[content].content_id) + ")...")
|
||||
dec_content = title.get_content_by_index(content)
|
||||
content_file_name = hex(title.tmd.content_records[content].content_id)[2:]
|
||||
while len(content_file_name) < 8:
|
||||
content_file_name = "0" + content_file_name
|
||||
content_file_name = content_file_name + ".app"
|
||||
dec_content_out = open(output_dir.joinpath(content_file_name), "wb")
|
||||
dec_content_out.write(dec_content)
|
||||
dec_content_out.close()
|
||||
else:
|
||||
print("Title has no Ticket, so content will not be decrypted!")
|
||||
|
||||
# If --wad was passed, pack a WAD and output that.
|
||||
if wad_file is not None:
|
||||
# Get the WAD certificate chain.
|
||||
print(" - Building certificate...")
|
||||
title.wad.set_cert_data(libWiiPy.title.download_cert(wiiu_endpoint=wiiu_nus_enabled))
|
||||
# Ensure that the path ends in .wad, and add that if it doesn't.
|
||||
print("Packing WAD...")
|
||||
if wad_file.suffix != ".wad":
|
||||
wad_file = wad_file.with_suffix(".wad")
|
||||
# Have libWiiPy dump the WAD, and write that data out.
|
||||
file = open(wad_file, "wb")
|
||||
file.write(title.dump_wad())
|
||||
file.close()
|
||||
|
||||
print("Downloaded title with Title ID \"" + args.tid + "\"!")
|
||||
from modules.core import fatal_error
|
||||
|
||||
|
||||
def handle_nus_content(args):
|
||||
tid = args.tid
|
||||
cid = args.cid
|
||||
version = args.version
|
||||
out = args.output
|
||||
if args.decrypt:
|
||||
decrypt_content = True
|
||||
else:
|
||||
@ -151,45 +19,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
|
||||
# actual integer that the hex Content ID translates to.
|
||||
content_id = None
|
||||
try:
|
||||
content_id = int.from_bytes(binascii.unhexlify(cid))
|
||||
except binascii.Error:
|
||||
print("Invalid Content ID! Content ID must be in format \"000000xx\"!")
|
||||
return
|
||||
fatal_error("The provided Content ID is invalid! The Content ID must be in the format \"000000xx\"!")
|
||||
|
||||
# Use the supplied output path if one was specified, otherwise generate one using the Content ID.
|
||||
if out is None:
|
||||
content_file_name = hex(content_id)[2:]
|
||||
while len(content_file_name) < 8:
|
||||
content_file_name = "0" + content_file_name
|
||||
if args.output is None:
|
||||
content_file_name = f"{content_id:08X}".lower()
|
||||
output_path = pathlib.Path(content_file_name)
|
||||
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.
|
||||
print("Downloading content with Content ID " + cid + "...")
|
||||
print(f"Downloading content with Content ID {cid}...")
|
||||
content_data = None
|
||||
try:
|
||||
content_data = libWiiPy.title.download_content(tid, content_id)
|
||||
except ValueError:
|
||||
print("The Title ID or Content ID you specified could not be found!")
|
||||
return
|
||||
fatal_error("The specified Title ID or Content ID could not be found!")
|
||||
|
||||
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")
|
||||
tmd = libWiiPy.title.TMD()
|
||||
tmd.load(libWiiPy.title.download_tmd(tid, version))
|
||||
# Try to get a Ticket for the title, if a common one is available.
|
||||
ticket = None
|
||||
try:
|
||||
ticket = libWiiPy.title.Ticket()
|
||||
ticket.load(libWiiPy.title.download_ticket(tid, wiiu_endpoint=True))
|
||||
except ValueError:
|
||||
print("No Ticket is available! Content cannot be decrypted!")
|
||||
return
|
||||
fatal_error("No Ticket is available! Content cannot be decrypted.")
|
||||
|
||||
content_hash = 'gggggggggggggggggggggggggggggggggggggggg'
|
||||
content_size = 0
|
||||
@ -203,23 +68,168 @@ def handle_nus_content(args):
|
||||
# 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.
|
||||
if content_hash == 'gggggggggggggggggggggggggggggggggggggggg':
|
||||
print("Content was not found in the TMD from the specified version! Content cannot be decrypted!")
|
||||
return
|
||||
fatal_error("Content was not found in the TMD for the specified version! Content cannot be decrypted.")
|
||||
|
||||
# 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.
|
||||
content_dec = libWiiPy.title.decrypt_content(content_data, ticket.get_title_key(), content_index, content_size)
|
||||
content_dec_hash = hashlib.sha1(content_dec).hexdigest()
|
||||
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) +
|
||||
"Actual hash is: {}".format(content_dec_hash))
|
||||
file = open(output_path, "wb")
|
||||
file.write(content_dec)
|
||||
file.close()
|
||||
output_path.write_bytes(content_dec)
|
||||
else:
|
||||
file = open(output_path, "wb")
|
||||
file.write(content_data)
|
||||
file.close()
|
||||
output_path.write_bytes(content_data)
|
||||
|
||||
print("Downloaded content with Content ID \"" + cid + "\"!")
|
||||
print(f"Downloaded content with Content ID \"{cid}\"!")
|
||||
|
||||
|
||||
def handle_nus_title(args):
|
||||
title_version = None
|
||||
wad_file = None
|
||||
output_dir = None
|
||||
can_decrypt = False
|
||||
tid = args.tid
|
||||
wiiu_nus_enabled = False if args.wii else True
|
||||
endpoint_override = args.endpoint if args.endpoint else None
|
||||
|
||||
# Check if --version was passed, because it'll be None if it wasn't.
|
||||
if args.version is not None:
|
||||
try:
|
||||
title_version = int(args.version)
|
||||
except ValueError:
|
||||
fatal_error("The specified Title Version must be a valid integer!")
|
||||
|
||||
# If --wad was passed, check to make sure the path is okay.
|
||||
if args.wad is not None:
|
||||
wad_file = pathlib.Path(args.wad)
|
||||
if wad_file.suffix != ".wad":
|
||||
wad_file = wad_file.with_suffix(".wad")
|
||||
|
||||
# If --output was passed, make sure the directory either doesn't exist or is empty.
|
||||
if args.output is not None:
|
||||
output_dir = pathlib.Path(args.output)
|
||||
if output_dir.exists():
|
||||
if output_dir.is_file():
|
||||
fatal_error("A file already exists with the provided directory name!")
|
||||
else:
|
||||
output_dir.mkdir()
|
||||
|
||||
# Download the title from the NUS. This is done "manually" (as opposed to using download_title()) so that we can
|
||||
# provide verbose output.
|
||||
title = libWiiPy.title.Title()
|
||||
|
||||
# Announce the title being downloaded, and the version if applicable.
|
||||
if title_version is not None:
|
||||
print(f"Downloading title {tid} v{title_version}, please wait...")
|
||||
else:
|
||||
print(f"Downloading title {tid} vLatest, please wait...")
|
||||
print(" - Downloading and parsing TMD...")
|
||||
# Download a specific TMD version if a version was specified, otherwise just download the latest TMD.
|
||||
if title_version is not None:
|
||||
title.load_tmd(libWiiPy.title.download_tmd(tid, title_version, wiiu_endpoint=wiiu_nus_enabled,
|
||||
endpoint_override=endpoint_override))
|
||||
else:
|
||||
title.load_tmd(libWiiPy.title.download_tmd(tid, wiiu_endpoint=wiiu_nus_enabled,
|
||||
endpoint_override=endpoint_override))
|
||||
title_version = title.tmd.title_version
|
||||
# Write out the TMD to a file.
|
||||
if output_dir is not None:
|
||||
output_dir.joinpath(f"tmd.{title_version}").write_bytes(title.tmd.dump())
|
||||
|
||||
# Download the ticket, if we can.
|
||||
print(" - Downloading and parsing Ticket...")
|
||||
try:
|
||||
title.load_ticket(libWiiPy.title.download_ticket(tid, wiiu_endpoint=wiiu_nus_enabled,
|
||||
endpoint_override=endpoint_override))
|
||||
can_decrypt = True
|
||||
if output_dir is not None:
|
||||
output_dir.joinpath("tik").write_bytes(title.ticket.dump())
|
||||
except ValueError:
|
||||
# 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.
|
||||
print(" - No Ticket is available!")
|
||||
if wad_file is not None and output_dir is None:
|
||||
fatal_error("--wad was passed, but this title has no common ticket and cannot be packed into a WAD!")
|
||||
|
||||
# Load the content records from the TMD, and begin iterating over the records.
|
||||
title.load_content_records()
|
||||
content_list = []
|
||||
for content in range(len(title.tmd.content_records)):
|
||||
# Generate the content file name by converting the Content ID to hex and then removing the 0x.
|
||||
content_file_name = hex(title.tmd.content_records[content].content_id)[2:]
|
||||
while len(content_file_name) < 8:
|
||||
content_file_name = "0" + content_file_name
|
||||
print(f" - Downloading content {content + 1} of {len(title.tmd.content_records)} "
|
||||
f"(Content ID: {title.tmd.content_records[content].content_id}, "
|
||||
f"Size: {title.tmd.content_records[content].content_size} bytes)...")
|
||||
content_list.append(libWiiPy.title.download_content(tid, title.tmd.content_records[content].content_id,
|
||||
wiiu_endpoint=wiiu_nus_enabled,
|
||||
endpoint_override=endpoint_override))
|
||||
print(" - Done!")
|
||||
# If we're supposed to be outputting to a folder, then write these files out.
|
||||
if output_dir is not None:
|
||||
output_dir.joinpath(content_file_name).write_bytes(content_list[content])
|
||||
title.content.content_list = content_list
|
||||
|
||||
# Try to decrypt the contents for this title if a ticket was available.
|
||||
if output_dir is not None:
|
||||
if can_decrypt is True:
|
||||
for content in range(len(title.tmd.content_records)):
|
||||
print(f" - Decrypting content {content + 1} of {len(title.tmd.content_records)} "
|
||||
f"(Content ID: {title.tmd.content_records[content].content_id})...")
|
||||
dec_content = title.get_content_by_index(content)
|
||||
content_file_name = f"{title.tmd.content_records[content].content_id:08X}".lower() + ".app"
|
||||
output_dir.joinpath(content_file_name).write_bytes(dec_content)
|
||||
else:
|
||||
print("Title has no Ticket, so content will not be decrypted!")
|
||||
|
||||
# If --wad was passed, pack a WAD and output that.
|
||||
if wad_file is not None:
|
||||
# Get the WAD certificate chain.
|
||||
print(" - Building certificate...")
|
||||
title.load_cert_chain(libWiiPy.title.download_cert_chain(wiiu_endpoint=wiiu_nus_enabled,
|
||||
endpoint_override=endpoint_override))
|
||||
# Ensure that the path ends in .wad, and add that if it doesn't.
|
||||
print("Packing WAD...")
|
||||
if wad_file.suffix != ".wad":
|
||||
wad_file = wad_file.with_suffix(".wad")
|
||||
# Have libWiiPy dump the WAD, and write that data out.
|
||||
pathlib.Path(wad_file).write_bytes(title.dump_wad())
|
||||
|
||||
print(f"Downloaded title with Title ID \"{args.tid}\"!")
|
||||
|
||||
|
||||
def handle_nus_tmd(args):
|
||||
tid = args.tid
|
||||
|
||||
# Check if --version was passed, because it'll be None if it wasn't.
|
||||
version = None
|
||||
if args.version is not None:
|
||||
try:
|
||||
version = int(args.version)
|
||||
except ValueError:
|
||||
fatal_error("The specified TMD version must be a valid integer!")
|
||||
|
||||
# Use the supplied output path if one was specified, otherwise generate one using the Title ID. If a version has
|
||||
# been specified, append the version to the end of the path as well.
|
||||
if args.output is None:
|
||||
if version is not None:
|
||||
output_path = pathlib.Path(f"{tid}.tmd.{version}")
|
||||
else:
|
||||
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.
|
||||
print(f"Downloading TMD for title {tid}...")
|
||||
tmd_data = None
|
||||
try:
|
||||
tmd_data = libWiiPy.title.download_tmd(tid, version)
|
||||
except ValueError:
|
||||
fatal_error("The specified Title ID or version could not be found!")
|
||||
|
||||
output_path.write_bytes(tmd_data)
|
||||
|
||||
print(f"Downloaded TMD for title \"{tid}\"!")
|
85
commands/title/tmd.py
Normal file
85
commands/title/tmd.py
Normal file
@ -0,0 +1,85 @@
|
||||
# "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 title_edit_ios, title_edit_tid, title_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:
|
||||
new_tid = title_edit_tid(tmd.title_id, args.tid)
|
||||
tmd.set_title_id(new_tid)
|
||||
edits_made = True
|
||||
if args.ios is not None:
|
||||
new_ios_tid = title_edit_ios(args.ios)
|
||||
tmd.ios_tid = new_ios_tid
|
||||
edits_made = True
|
||||
if args.type is not None:
|
||||
new_tid = title_edit_type(tmd.title_id, args.type)
|
||||
tmd.set_title_id(new_tid)
|
||||
edits_made = True
|
||||
|
||||
if not edits_made:
|
||||
fatal_error("You must specify at least one change to make!")
|
||||
|
||||
# Fakesign the title since any changes have already invalidated the signature.
|
||||
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}\"!")
|
440
commands/title/wad.py
Normal file
440
commands/title/wad.py
Normal file
@ -0,0 +1,440 @@
|
||||
# "commands/title/wad.py" from WiiPy by NinjaCheetah
|
||||
# https://github.com/NinjaCheetah/WiiPy
|
||||
|
||||
import io
|
||||
import pathlib
|
||||
from random import randint
|
||||
import libWiiPy
|
||||
from modules.core import fatal_error
|
||||
from modules.title import title_edit_ios, title_edit_tid, title_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!")
|
||||
|
||||
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!")
|
||||
else:
|
||||
output_path = pathlib.Path(args.output)
|
||||
|
||||
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:
|
||||
new_tid = title_edit_tid(title.tmd.title_id, args.tid)
|
||||
title.set_title_id(new_tid)
|
||||
edits_made = True
|
||||
if args.ios is not None:
|
||||
new_ios_tid = title_edit_ios(args.ios)
|
||||
title.tmd.ios_tid = new_ios_tid
|
||||
edits_made = True
|
||||
if args.type is not None:
|
||||
new_tid = title_edit_type(title.tmd.title_id, args.type)
|
||||
title.set_title_id(new_tid)
|
||||
edits_made = True
|
||||
if args.channel_name is not None:
|
||||
# Assess if this is actually a channel, because a channel name can't be set otherwise.
|
||||
banner_data = title.get_content_by_index(0)
|
||||
with io.BytesIO(banner_data) as data:
|
||||
data.seek(0x40)
|
||||
magic = data.read(4)
|
||||
if magic != b'\x49\x4D\x45\x54':
|
||||
data.seek(0x80)
|
||||
magic = data.read(4)
|
||||
if magic != b'\x49\x4D\x45\x54':
|
||||
fatal_error(f"This WAD file doesn't contain a Channel, so a new Channel name cannot be set!")
|
||||
target = 0x40
|
||||
else:
|
||||
target = 0x0
|
||||
# Read out the IMET header data, load it, edit it, then dump it back to bytes and directly write it over
|
||||
# the old header data, since libWiiPy doesn't offer a cleaner solution currently.
|
||||
data.seek(target)
|
||||
imet_data = data.read(0x600)
|
||||
imet_header = libWiiPy.archive.IMETHeader()
|
||||
imet_header.load(imet_data)
|
||||
target_languages = list(imet_header.LocalizedTitles)
|
||||
try:
|
||||
for target_language in target_languages:
|
||||
imet_header.set_channel_names((target_language, args.channel_name))
|
||||
except ValueError:
|
||||
fatal_error(f"The specified Channel name is not valid! Channel names must be no longer than 40 "
|
||||
f"characters.")
|
||||
imet_data = imet_header.dump()
|
||||
data.seek(target)
|
||||
data.write(imet_data)
|
||||
data.seek(0x0)
|
||||
title.set_content(data.read(), 0)
|
||||
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.load_cert_chain(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!")
|
@ -1,38 +0,0 @@
|
||||
# "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!")
|
9
modules/core.py
Normal file
9
modules/core.py
Normal 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)
|
55
modules/title.py
Normal file
55
modules/title.py
Normal file
@ -0,0 +1,55 @@
|
||||
# "modules/title.py" from WiiPy by NinjaCheetah
|
||||
# https://github.com/NinjaCheetah/WiiPy
|
||||
|
||||
import binascii
|
||||
import re
|
||||
from modules.core import fatal_error
|
||||
|
||||
|
||||
def title_edit_ios(new_ios: str) -> str:
|
||||
# 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}"
|
||||
return new_ios_tid
|
||||
|
||||
|
||||
def title_edit_tid(tid: str, new_tid: str) -> str:
|
||||
# Setting a new TID, only changing TID low since this expects a 4 character input with letters, numbers, and some
|
||||
# symbols.
|
||||
pattern = r"^[a-z0-9!@#$%^&*]{4}$"
|
||||
if not re.fullmatch(pattern, new_tid, re.IGNORECASE):
|
||||
fatal_error(f"The specified Title ID is not valid! The new Title ID should be 4 characters and only include "
|
||||
f"letters, numbers, and the special characters \"!@#$%&*\".")
|
||||
# Get the current TID high, because we want to preserve the title type while only changing the TID low.
|
||||
tid_high = tid[:8]
|
||||
new_tid = f"{tid_high}{str(binascii.hexlify(new_tid.encode()), 'ascii')}"
|
||||
return new_tid
|
||||
|
||||
|
||||
def title_edit_type(tid: str, new_type: str) -> str:
|
||||
# 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 = tid[8:]
|
||||
new_tid = f"{new_tid_high}{tid_low}"
|
||||
return new_tid
|
@ -1,37 +0,0 @@
|
||||
# "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!")
|
138
modules/wad.py
138
modules/wad.py
@ -1,138 +0,0 @@
|
||||
# "wad.py" from WiiPy by NinjaCheetah
|
||||
# https://github.com/NinjaCheetah/WiiPy
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import hashlib
|
||||
import binascii
|
||||
import libWiiPy
|
||||
|
||||
|
||||
def handle_wad(args):
|
||||
input_path = pathlib.Path(args.input)
|
||||
output_path = pathlib.Path(args.output)
|
||||
|
||||
# Code for if the --pack argument was passed.
|
||||
if args.pack:
|
||||
# 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('*.tmd'))
|
||||
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('*.tik'))
|
||||
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('*.cert'))
|
||||
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("*.app"))
|
||||
if not content_files:
|
||||
raise FileNotFoundError("No contents 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.
|
||||
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("*.footer"))[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()
|
||||
|
||||
# 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()
|
||||
|
||||
# Iterate over every file in the content_files list, and attempt to load it into the Title().
|
||||
for index in range(len(title.content.content_records)):
|
||||
for content in range(len(content_files)):
|
||||
dec_content = open(content_files[content], "rb").read()
|
||||
try:
|
||||
# Attempt to load the content into the correct index.
|
||||
title.load_content(dec_content, index)
|
||||
break
|
||||
except ValueError:
|
||||
# Wasn't the right content, so try again.
|
||||
pass
|
||||
|
||||
output_path.write(title.dump_wad())
|
||||
|
||||
print("WAD file packed!")
|
||||
|
||||
# Code for if the --unpack argument was passed.
|
||||
elif args.unpack:
|
||||
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!")
|
||||
elif 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()
|
||||
|
||||
for content_file in range(0, title.tmd.num_contents):
|
||||
content_file_name = "000000" + str(binascii.hexlify(content_file.to_bytes()).decode()) + ".app"
|
||||
content_out = open(output_path.joinpath(content_file_name), "wb")
|
||||
content_out.write(title.get_content_by_index(content_file))
|
||||
content_out.close()
|
||||
|
||||
print("WAD file unpacked!")
|
27
scripts/content-checker.py
Normal file
27
scripts/content-checker.py
Normal file
@ -0,0 +1,27 @@
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
import libWiiPy
|
||||
|
||||
target_hash = sys.argv[1].lower().encode()
|
||||
print(target_hash)
|
||||
|
||||
for content in range(3, 81):
|
||||
try:
|
||||
tmd = libWiiPy.title.download_tmd(f"00000007000000{content:02X}")
|
||||
open(f"00000007000000{content:02X}.tmd", "wb").write(tmd)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
workdir = pathlib.Path(".")
|
||||
|
||||
tmd_files = list(workdir.glob("*.tmd"))
|
||||
|
||||
for tmd in tmd_files:
|
||||
new_tmd = libWiiPy.title.TMD()
|
||||
new_tmd.load(open(tmd, "rb").read())
|
||||
hash_list = []
|
||||
for content in new_tmd.content_records:
|
||||
hash_list.append(content.content_hash)
|
||||
if target_hash in hash_list:
|
||||
print(f"Found match in {tmd}\n")
|
30
scripts/nus-scraper.py
Normal file
30
scripts/nus-scraper.py
Normal file
@ -0,0 +1,30 @@
|
||||
import os
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
tid_high = ["00010000", "00010001", "00010005"]
|
||||
types = ["43", "44", "45", "46", "47", "48", "4A", "4C", "4D", "4E", "50", "51", "52", "53", "57", "58"]
|
||||
regions = ["41", "42", "43", "44", "45", "46", "49", "4A", "4B", "4C", "4D", "4E", "50", "51", "53", "54", "55", "57", "58"]
|
||||
|
||||
|
||||
for tid in tid_high:
|
||||
print(f"Starting scrape for TID high {tid}...")
|
||||
|
||||
if os.path.exists(f"{tid}.log"):
|
||||
os.remove(f"{tid}.log")
|
||||
log = open(f"{tid}.log", "a")
|
||||
|
||||
for ttype in types:
|
||||
print(f"Scraping titles of type: {ttype}")
|
||||
for title in range(0, 65536):
|
||||
for region in regions:
|
||||
request = requests.get(url=f"http://ccs.cdn.wup.shop.nintendo.net/ccs/download/{tid}{ttype}{title:04X}{region}/tmd", headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
|
||||
if request.status_code == 200:
|
||||
print(f"Found valid TID: {tid}{ttype}{title:04X}{region}")
|
||||
log.write(f"{tid}{ttype}{title:02X}{region}")
|
||||
else:
|
||||
print(f"Invalid TID: {tid}{ttype}{title:04X}{region}")
|
||||
pass
|
||||
request.close()
|
||||
log.close()
|
403
wiipy.py
403
wiipy.py
@ -4,30 +4,170 @@
|
||||
import argparse
|
||||
from importlib.metadata import version
|
||||
|
||||
from modules.wad import *
|
||||
from modules.nus import *
|
||||
from modules.u8 import *
|
||||
from modules.ash import *
|
||||
from commands.archive.ash import *
|
||||
from commands.archive.lz77 import *
|
||||
from commands.archive.theme import *
|
||||
from commands.archive.u8 import *
|
||||
from commands.nand.emunand import *
|
||||
from commands.nand.setting import *
|
||||
from commands.title.ciosbuild import *
|
||||
from commands.title.fakesign import *
|
||||
from commands.title.info import *
|
||||
from commands.title.iospatcher import *
|
||||
from commands.title.nus import *
|
||||
from commands.title.tmd import *
|
||||
from commands.title.wad import *
|
||||
|
||||
wiipy_ver = "1.5.1"
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Main argument parser.
|
||||
parser = argparse.ArgumentParser(
|
||||
description="WiiPy is 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",
|
||||
version=f"WiiPy v1.2.1, based on libWiiPy v{version('libWiiPy')} (from branch \'main\')")
|
||||
subparsers = parser.add_subparsers(dest="subcommand", required=True)
|
||||
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)
|
||||
|
||||
# Argument parser for the WAD subcommand.
|
||||
wad_parser = subparsers.add_parser("wad", help="pack/unpack a WAD file",
|
||||
description="pack/unpack a WAD file")
|
||||
wad_parser.set_defaults(func=handle_wad)
|
||||
wad_group = wad_parser.add_mutually_exclusive_group(required=True)
|
||||
wad_group.add_argument("-p", "--pack", help="pack a directory to a WAD file", action="store_true")
|
||||
wad_group.add_argument("-u", "--unpack", help="unpack a WAD file to a directory", action="store_true")
|
||||
wad_parser.add_argument("input", metavar="IN", type=str, help="input file")
|
||||
wad_parser.add_argument("output", metavar="OUT", type=str, help="output file")
|
||||
wad_parser.add_argument("--fakesign", help="fakesign the TMD and Ticket (trucha bug)",
|
||||
action="store_true")
|
||||
# Argument parser for the ASH subcommand.
|
||||
ash_parser = subparsers.add_parser("ash", help="compress/decompress an ASH file",
|
||||
description="compress/decompress an ASH file")
|
||||
ash_subparsers = ash_parser.add_subparsers(title="ash", dest="ash", required=True)
|
||||
# ASH compress parser.
|
||||
ash_compress_parser = ash_subparsers.add_parser("compress", help="compress a file into an ASH file",
|
||||
description="compress a file into an ASH file; by default, this "
|
||||
"will output to <input file>.ash")
|
||||
ash_compress_parser.set_defaults(func=handle_ash_compress)
|
||||
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)
|
||||
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)
|
||||
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
|
||||
cios_parser = subparsers.add_parser("cios", help="build a cIOS from a base IOS and provided map",
|
||||
description="build a cIOS from a base IOS and provided map")
|
||||
cios_parser.set_defaults(func=build_cios)
|
||||
cios_parser.add_argument("base", metavar="BASE", type=str, help="base IOS WAD")
|
||||
cios_parser.add_argument("map", metavar="MAP", type=str, help="cIOS map file")
|
||||
cios_parser.add_argument("output", metavar="OUT", type=str, help="file to output the cIOS to")
|
||||
cios_parser.add_argument("-c", "--cios-ver", metavar="CIOS", type=str,
|
||||
help="cIOS version from the map to build", required=True)
|
||||
cios_parser.add_argument("-m", "--modules", metavar="MODULES", type=str,
|
||||
help="directory to look for cIOS commands in (optional, defaults to current directory)")
|
||||
cios_parser.add_argument("-s", "--slot", metavar="SLOT", type=int,
|
||||
help="slot that this cIOS will install to (optional, defaults to 249)", default=249)
|
||||
cios_parser.add_argument("-v", "--version", metavar="VERSION", type=int,
|
||||
help="version that this cIOS will be (optional, defaults to 65535)", default=65535)
|
||||
|
||||
# Argument parser for the EmuNAND subcommand.
|
||||
emunand_parser = subparsers.add_parser("emunand", help="manage Wii EmuNAND directories",
|
||||
description="manage Wii EmuNAND directories")
|
||||
emunand_subparsers = emunand_parser.add_subparsers(title="emunand", dest="emunand", required=True)
|
||||
# Info EmuNAND subcommand.
|
||||
emunand_info_parser = emunand_subparsers.add_parser("info", help="show info about an EmuNAND",
|
||||
description="show info about an EmuNAND")
|
||||
emunand_info_parser.set_defaults(func=handle_emunand_info)
|
||||
emunand_info_parser.add_argument("emunand", metavar="EMUNAND", type=str,
|
||||
help="path of the EmuNAND directory")
|
||||
# Install-Missing EmuNAND command.
|
||||
emunand_install_missing_parser = emunand_subparsers.add_parser("install-missing",
|
||||
help="install missing IOSes to an EmuNAND",
|
||||
description="install missing IOSes to an EmuNAND by "
|
||||
"checking installed titles and finding "
|
||||
"their required IOSes, then downloading "
|
||||
"and installing any that are missing")
|
||||
emunand_install_missing_parser.set_defaults(func=handle_emunand_install_missing)
|
||||
emunand_install_missing_parser.add_argument("emunand", metavar="EMUNAND", type=str,
|
||||
help="path of the EmuNAND directory")
|
||||
emunand_install_missing_parser.add_argument("--vwii", action="store_true",
|
||||
help="override the automatic vWii detection based on the installed "
|
||||
"System Menu and use vWii IOSes")
|
||||
# Title EmuNAND subcommand.
|
||||
emunand_title_parser = emunand_subparsers.add_parser("title", help="manage titles on an EmuNAND",
|
||||
description="manage titles on an EmuNAND")
|
||||
emunand_title_parser.set_defaults(func=handle_emunand_title)
|
||||
emunand_title_parser.add_argument("emunand", metavar="EMUNAND", type=str,
|
||||
help="path of the target EmuNAND directory")
|
||||
emunand_title_install_group = emunand_title_parser.add_mutually_exclusive_group(required=True)
|
||||
emunand_title_install_group.add_argument("--install", metavar="WAD", type=str,
|
||||
help="install the target WAD(s) to an EmuNAND (can be a single file or a "
|
||||
"folder of WADs)")
|
||||
emunand_title_install_group.add_argument("--uninstall", metavar="TID", type=str,
|
||||
help="uninstall a title with the provided Title ID from an EmuNAND (also"
|
||||
"accepts a WAD file to read the TID from)")
|
||||
emunand_title_parser.add_argument("-s", "--skip-hash", help="skips validating the hashes of decrypted "
|
||||
"content (install only)", action="store_true")
|
||||
|
||||
# Argument parser for the fakesign subcommand.
|
||||
fakesign_parser = subparsers.add_parser("fakesign", help="fakesign a TMD, Ticket, or WAD (trucha bug)",
|
||||
description="fakesign a TMD, Ticket, or WAD (trucha bug); by default, this "
|
||||
"will overwrite the input file if no output file is specified")
|
||||
fakesign_parser.set_defaults(func=handle_fakesign)
|
||||
fakesign_parser.add_argument("input", metavar="IN", type=str, help="input file")
|
||||
fakesign_parser.add_argument("-o", "--output", metavar="OUT", type=str, help="output file (optional)")
|
||||
|
||||
# Argument parser for the info command.
|
||||
info_parser = subparsers.add_parser("info", help="get information about a TMD, Ticket, or WAD",
|
||||
description="get information about a TMD, Ticket, or WAD")
|
||||
info_parser.set_defaults(func=handle_info)
|
||||
info_parser.add_argument("input", metavar="IN", type=str, help="input file")
|
||||
|
||||
# Argument parser for the iospatch command.
|
||||
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 "
|
||||
"overwrite the input file in place unless you use -o/--output")
|
||||
iospatch_parser.set_defaults(func=handle_iospatch)
|
||||
iospatch_parser.add_argument("input", metavar="IN", type=str, help="input file")
|
||||
iospatch_parser.add_argument("-o", "--output", metavar="OUT", type=str, help="output file (optional)")
|
||||
iospatch_parser.add_argument("-fs", "--fakesigning", action="store_true", help="patch in fakesigning support")
|
||||
iospatch_parser.add_argument("-ei", "--es-identify", action="store_true", help="patch in ES_Identify access")
|
||||
iospatch_parser.add_argument("-na", "--nand-access", action="store_true", help="patch in /dev/flash access")
|
||||
iospatch_parser.add_argument("-vd", "--version-downgrading", action="store_true",
|
||||
help="patch in version downgrading support")
|
||||
iospatch_parser.add_argument("-di", "--drive-inquiry", action="store_true",
|
||||
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("-s", "--slot", metavar="SLOT", type=int,
|
||||
help="set the slot that this IOS will install to")
|
||||
iospatch_parser.add_argument("-a", "--all", action="store_true", help="apply all patches (overrides other options)")
|
||||
iospatch_parser.add_argument("-ns", "--no-shared", action="store_true",
|
||||
help="set all patched content to be non-shared")
|
||||
|
||||
# Argument parser for the LZ77 subcommand.
|
||||
lz77_parser = subparsers.add_parser("lz77", help="compress/decompress data using LZ77 compression",
|
||||
description="compress/decompress data using LZ77 compression")
|
||||
lz77_subparsers = lz77_parser.add_subparsers(title="lz77", dest="lz77", required=True)
|
||||
# LZ77 compress parser.
|
||||
lz77_compress_parser = lz77_subparsers.add_parser("compress", help="compress a file with LZ77 compression",
|
||||
description="compress a file with LZ77 compression; by default, "
|
||||
"this will output to <input file>.lz77")
|
||||
lz77_compress_parser.set_defaults(func=handle_lz77_compress)
|
||||
lz77_compress_parser.add_argument("input", metavar="IN", type=str, help="file to compress")
|
||||
lz77_compress_parser.add_argument("-o", "--output", metavar="OUT", type=str,
|
||||
help="file to output the compressed data to (optional)")
|
||||
# LZ77 decompress parser.
|
||||
lz77_decompress_parser = lz77_subparsers.add_parser("decompress", help="decompress an LZ77-compressed file",
|
||||
description="decompress an LZ77-compressed file; by default, "
|
||||
"this will output to <input file>.out")
|
||||
lz77_decompress_parser.set_defaults(func=handle_lz77_decompress)
|
||||
lz77_decompress_parser.add_argument("input", metavar="IN", type=str,
|
||||
help="LZ77-compressed file to decompress")
|
||||
lz77_decompress_parser.add_argument("-o", "--output", metavar="OUT", type=str,
|
||||
help="file to output the decompressed data to (optional)")
|
||||
|
||||
# Argument parser for the NUS subcommand.
|
||||
nus_parser = subparsers.add_parser("nus", help="download data from the NUS",
|
||||
@ -46,8 +186,10 @@ if __name__ == "__main__":
|
||||
help="download the title to a folder")
|
||||
nus_title_out_group.add_argument("-w", "--wad", metavar="WAD", type=str,
|
||||
help="pack a wad with the provided name")
|
||||
nus_title_parser.add_argument("--wii", help="use original Wii NUS instead of the Wii U servers",
|
||||
nus_title_parser.add_argument("--wii", help="use the original Wii NUS endpoint instead of the Wii U endpoint",
|
||||
action="store_true")
|
||||
nus_title_parser.add_argument("-e", "--endpoint", metavar="ENDPOINT", type=str,
|
||||
help="use the specified NUS endpoint instead of the official one")
|
||||
# Content NUS subcommand.
|
||||
nus_content_parser = nus_subparsers.add_parser("content", help="download a specific content from the NUS",
|
||||
description="download a specific content from the NUS")
|
||||
@ -60,30 +202,215 @@ if __name__ == "__main__":
|
||||
nus_content_parser.add_argument("-o", "--output", metavar="OUT", type=str,
|
||||
help="path to download the content to (optional)")
|
||||
nus_content_parser.add_argument("-d", "--decrypt", action="store_true", help="decrypt this content")
|
||||
# TMD NUS subcommand.
|
||||
nus_tmd_parser = nus_subparsers.add_parser("tmd", help="download a tmd from the NUS",
|
||||
description="download a tmd from the NUS")
|
||||
nus_tmd_parser.set_defaults(func=handle_nus_tmd)
|
||||
nus_tmd_parser.add_argument("tid", metavar="TID", type=str, help="Title ID the TMD is for")
|
||||
nus_tmd_parser.add_argument("-v", "--version", metavar="VERSION", type=int, help="version of the TMD to download")
|
||||
nus_tmd_parser.add_argument("-o", "--output", metavar="OUT", type=str,
|
||||
help="path to download the TMD to (optional)")
|
||||
|
||||
# Argument parser for the setting subcommand.
|
||||
setting_parser = subparsers.add_parser("setting", help="manage setting.txt",
|
||||
description="manage setting.txt")
|
||||
setting_subparsers = setting_parser.add_subparsers(dest="subcommand", required=True)
|
||||
# Decrypt setting.txt subcommand.
|
||||
setting_dec_parser = setting_subparsers.add_parser("decrypt", help="decrypt setting.txt",
|
||||
description="decrypt setting.txt; by default, this will output "
|
||||
"to setting_dec.txt")
|
||||
setting_dec_parser.set_defaults(func=handle_setting_decrypt)
|
||||
setting_dec_parser.add_argument("input", metavar="IN", type=str, help="encrypted setting.txt file to decrypt")
|
||||
setting_dec_parser.add_argument("-o", "--output", metavar="OUT", type=str,
|
||||
help="path to output the decrypted file to (optional)")
|
||||
# Encrypt setting.txt subcommand.
|
||||
setting_enc_parser = setting_subparsers.add_parser("encrypt", help="encrypt setting.txt",
|
||||
description="encrypt setting.txt; by default, this will output "
|
||||
"to setting.txt")
|
||||
setting_enc_parser.set_defaults(func=handle_setting_encrypt)
|
||||
setting_enc_parser.add_argument("input", metavar="IN", type=str, help="decrypted setting.txt file to encrypt")
|
||||
setting_enc_parser.add_argument("-o", "--output", metavar="OUT", type=str,
|
||||
help="path to output the encrypted file to (optional)")
|
||||
# Generate setting.txt subcommand.
|
||||
setting_gen_parser = setting_subparsers.add_parser("gen",
|
||||
help="generate a new setting.txt based on the provided values",
|
||||
description="generate a new setting.txt based on the provided values")
|
||||
setting_gen_parser.set_defaults(func=handle_setting_gen)
|
||||
setting_gen_parser.add_argument("serno", metavar="SERNO", type=str,
|
||||
help="serial number of the console these settings are for")
|
||||
setting_gen_parser.add_argument("region", metavar="REGION", type=str,
|
||||
help="region of the console these settings are for (USA, EUR, JPN, or KOR)")
|
||||
|
||||
# Argument parser for the theme subcommand.
|
||||
theme_parser = subparsers.add_parser("theme", help="apply custom themes to the Wii Menu",
|
||||
description="apply custom themes to the Wii Menu")
|
||||
theme_subparsers = theme_parser.add_subparsers(dest="subcommand", required=True)
|
||||
# MYM theme subcommand.
|
||||
theme_mym_parser = theme_subparsers.add_parser("mym", help="apply an MYM theme to the Wii Menu",
|
||||
description="apply an MYM theme to the Wii Menu")
|
||||
theme_mym_parser.set_defaults(func=handle_apply_mym)
|
||||
theme_mym_parser.add_argument("mym", metavar="MYM", type=str, help="MYM theme to apply")
|
||||
theme_mym_parser.add_argument("base", metavar="BASE", type=str,
|
||||
help="base Wii Menu assets to apply the theme to (000000xx.app)")
|
||||
theme_mym_parser.add_argument("output", metavar="OUT", type=str,
|
||||
help="path to output the finished theme to (<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.
|
||||
u8_parser = subparsers.add_parser("u8", help="pack/unpack a U8 archive",
|
||||
description="pack/unpack a U8 archive")
|
||||
u8_parser.set_defaults(func=handle_u8)
|
||||
u8_group = u8_parser.add_mutually_exclusive_group(required=True)
|
||||
u8_group.add_argument("-p", "--pack", help="pack a directory to a U8 archive", action="store_true")
|
||||
u8_group.add_argument("-u", "--unpack", help="unpack a U8 archive to a directory", action="store_true")
|
||||
u8_parser.add_argument("input", metavar="IN", type=str, help="input file")
|
||||
u8_parser.add_argument("output", metavar="OUT", type=str, help="output file")
|
||||
u8_subparsers = u8_parser.add_subparsers(dest="subcommand", required=True)
|
||||
# Pack U8 subcommand.
|
||||
u8_pack_parser = u8_subparsers.add_parser("pack", help="pack a folder into U8 archive",
|
||||
description="pack a folder into U8 archive")
|
||||
u8_pack_parser.set_defaults(func=handle_u8_pack)
|
||||
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.
|
||||
wad_parser = subparsers.add_parser("wad", help="pack/unpack/edit a WAD file",
|
||||
description="pack/unpack/edit a WAD file")
|
||||
wad_subparsers = wad_parser.add_subparsers(dest="subcommand", required=True)
|
||||
# Add WAD subcommand.
|
||||
wad_add_parser = wad_subparsers.add_parser("add", help="add decrypted content to a WAD file",
|
||||
description="add decrypted content to a WAD file; by default, this "
|
||||
"will overwrite the input file unless an output is specified")
|
||||
wad_add_parser.set_defaults(func=handle_wad_add)
|
||||
wad_add_parser.add_argument("input", metavar="IN", type=str, help="WAD file to add to")
|
||||
wad_add_parser.add_argument("content", metavar="CONTENT", type=str, help="decrypted content to add")
|
||||
wad_add_parser.add_argument("-c", "--cid", metavar="CID", type=str,
|
||||
help="Content ID to assign the new content (optional, will be randomly assigned if "
|
||||
"not specified)")
|
||||
wad_add_parser.add_argument("-t", "--type", metavar="TYPE", type=str,
|
||||
help="the type of the new content, can be \"Normal\", \"Shared\", or \"DLC\" "
|
||||
"(optional, will default to \"Normal\" if not specified)")
|
||||
wad_add_parser.add_argument("-o", "--output", metavar="OUT", type=str,
|
||||
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("--channel-name", metavar="CHANNEL", type=str,
|
||||
help="a new Channel name for this WAD, if it contains a channel")
|
||||
wad_edit_parser.add_argument("-o", "--output", metavar="OUT", type=str,
|
||||
help="file to output the updated WAD to (optional)")
|
||||
# Pack WAD subcommand.
|
||||
wad_pack_parser = wad_subparsers.add_parser("pack", help="pack a directory to a WAD file",
|
||||
description="pack a directory to a WAD file")
|
||||
wad_pack_parser.set_defaults(func=handle_wad_pack)
|
||||
wad_pack_parser.add_argument("input", metavar="IN", type=str, help="input directory")
|
||||
wad_pack_parser.add_argument("output", metavar="OUT", type=str, help="WAD file to pack")
|
||||
wad_pack_parser.add_argument("-f", "--fakesign", help="fakesign the TMD and Ticket (trucha bug)",
|
||||
action="store_true")
|
||||
# Remove WAD subcommand.
|
||||
wad_remove_parser = wad_subparsers.add_parser("remove", help="remove content from a WAD file",
|
||||
description="remove content from a WAD file, either by its CID or"
|
||||
"by its index; by default, this will overwrite the input "
|
||||
"file unless an output is specified")
|
||||
wad_remove_parser.set_defaults(func=handle_wad_remove)
|
||||
wad_remove_parser.add_argument("input", metavar="IN", type=str, help="WAD file to remove content from")
|
||||
wad_remove_targets = wad_remove_parser.add_mutually_exclusive_group(required=True)
|
||||
wad_remove_targets.add_argument("-i", "--index", metavar="INDEX", type=int,
|
||||
help="index of the content to remove")
|
||||
wad_remove_targets.add_argument("-c", "--cid", metavar="CID", type=str,
|
||||
help="Content ID of the content to remove")
|
||||
wad_remove_parser.add_argument("-o", "--output", metavar="OUT", type=str,
|
||||
help="file to output the updated WAD to (optional)")
|
||||
# Set WAD subcommand.
|
||||
wad_set_parser = wad_subparsers.add_parser("set", help="set content in a WAD file",
|
||||
description="replace existing content in a WAD file with new decrypted "
|
||||
"data; by default, this will overwrite the input file "
|
||||
"unless an output is specified")
|
||||
wad_set_parser.set_defaults(func=handle_wad_set)
|
||||
wad_set_parser.add_argument("input", metavar="IN", type=str, help="WAD file to replace content in")
|
||||
wad_set_parser.add_argument("content", metavar="CONTENT", type=str, help="new decrypted content")
|
||||
wad_set_targets = wad_set_parser.add_mutually_exclusive_group(required=True)
|
||||
wad_set_targets.add_argument("-i", "--index", metavar="INDEX", type=int,
|
||||
help="index of the content to replace")
|
||||
wad_set_targets.add_argument("-c", "--cid", metavar="CID", type=str,
|
||||
help="Content ID of the content to replace")
|
||||
wad_set_parser.add_argument("-o", "--output", metavar="OUT", type=str,
|
||||
help="file to output the updated WAD to (optional)")
|
||||
wad_set_parser.add_argument("-t", "--type", metavar="TYPE", type=str,
|
||||
help="specifies a new type for the content, can be \"Normal\", \"Shared\", or \"DLC\" "
|
||||
"(optional)")
|
||||
# Unpack WAD subcommand.
|
||||
wad_unpack_parser = wad_subparsers.add_parser("unpack", help="unpack a WAD file to a directory",
|
||||
description="unpack a WAD file to a directory")
|
||||
wad_unpack_parser.set_defaults(func=handle_wad_unpack)
|
||||
wad_unpack_parser.add_argument("input", metavar="IN", type=str, help="WAD file to unpack")
|
||||
wad_unpack_parser.add_argument("output", metavar="OUT", type=str, help="output directory")
|
||||
wad_unpack_parser.add_argument("-s", "--skip-hash", help="skips validating the hashes of decrypted "
|
||||
"content", action="store_true")
|
||||
|
||||
# Argument parser for the ASH subcommand.
|
||||
ash_parser = subparsers.add_parser("ash", help="compress/decompress an ASH file",
|
||||
description="compress/decompress an ASH file")
|
||||
ash_parser.set_defaults(func=handle_ash)
|
||||
ash_group = ash_parser.add_mutually_exclusive_group(required=True)
|
||||
ash_group.add_argument("-c", "--compress", help="compress a file into an ASH file", action="store_true")
|
||||
ash_group.add_argument("-d", "--decompress", help="decompress an ASH file", action="store_true")
|
||||
ash_parser.add_argument("input", metavar="IN", type=str, help="input file")
|
||||
ash_parser.add_argument("output", metavar="OUT", type=str, help="output file")
|
||||
ash_parser.add_argument("--sym-bits", metavar="SYM_BITS", type=int,
|
||||
help="number of bits in each symbol tree leaf (default: 9)", default=9)
|
||||
ash_parser.add_argument("--dist-bits", metavar="DIST_BITS", type=int,
|
||||
help="number of bits in each distance tree leaf (default: 11)", default=11)
|
||||
|
||||
# Parse all the args, and call the appropriate function with all of those args if a valid subcommand was passed.
|
||||
args = parser.parse_args()
|
||||
|
Loading…
x
Reference in New Issue
Block a user