mirror of
https://github.com/NinjaCheetah/WiiPy.git
synced 2026-02-16 18:15:40 -05:00
Compare commits
45 Commits
v1.2.0
...
improved-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
cef85b4951
|
|||
|
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
|
|||
|
1f82aa61c7
|
|||
|
125ba4ea69
|
|||
|
7c4906f0db
|
|||
|
2066f8b4a2
|
10
.github/workflows/python-build.yaml
vendored
10
.github/workflows/python-build.yaml
vendored
@@ -31,10 +31,10 @@ jobs:
|
|||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
- name: Build Application
|
- name: Build Application
|
||||||
run: |
|
run: |
|
||||||
nuitka3 --show-progress --assume-yes-for-downloads --onefile wiipy.py
|
python -m nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py -o wiipy
|
||||||
- name: Prepare Package for Upload
|
- name: Prepare Package for Upload
|
||||||
run: |
|
run: |
|
||||||
mv wiipy.bin ~/wiipy
|
mv wiipy ~/wiipy
|
||||||
cd ~
|
cd ~
|
||||||
tar cvf WiiPy.tar wiipy
|
tar cvf WiiPy.tar wiipy
|
||||||
- name: Upload Application
|
- name: Upload Application
|
||||||
@@ -59,10 +59,10 @@ jobs:
|
|||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
- name: Build Application
|
- name: Build Application
|
||||||
run: |
|
run: |
|
||||||
nuitka3 --show-progress --assume-yes-for-downloads --onefile wiipy.py
|
python -m nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py -o wiipy
|
||||||
- name: Prepare Package for Upload
|
- name: Prepare Package for Upload
|
||||||
run: |
|
run: |
|
||||||
mv wiipy.bin ~/wiipy
|
mv wiipy ~/wiipy
|
||||||
cd ~
|
cd ~
|
||||||
tar cvf WiiPy.tar wiipy
|
tar cvf WiiPy.tar wiipy
|
||||||
- name: Upload Application
|
- name: Upload Application
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
- name: Build Application
|
- name: Build Application
|
||||||
run: |
|
run: |
|
||||||
nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py
|
python -m nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py
|
||||||
- name: Upload Application
|
- name: Upload Application
|
||||||
uses: actions/upload-artifact@v4.3.0
|
uses: actions/upload-artifact@v4.3.0
|
||||||
with:
|
with:
|
||||||
|
|||||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -25,6 +25,7 @@ share/python-wheels/
|
|||||||
*.build/
|
*.build/
|
||||||
*.dist/
|
*.dist/
|
||||||
*.onefile-build/
|
*.onefile-build/
|
||||||
|
wiipy
|
||||||
.installed.cfg
|
.installed.cfg
|
||||||
*.egg
|
*.egg
|
||||||
MANIFEST
|
MANIFEST
|
||||||
@@ -163,14 +164,23 @@ cython_debug/
|
|||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
.idea/
|
.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
|
*.tmd
|
||||||
|
*.tik
|
||||||
|
*.cert
|
||||||
|
*.footer
|
||||||
*.wad
|
*.wad
|
||||||
*.app
|
*.app
|
||||||
*.arc
|
*.arc
|
||||||
*.ash
|
*.ash
|
||||||
|
*.met
|
||||||
out_prod/
|
out_prod/
|
||||||
remakewad.pl
|
remakewad.pl
|
||||||
|
content.map
|
||||||
|
uid.sys
|
||||||
|
SYSCONF
|
||||||
|
setting.txt
|
||||||
|
ciosmaps.xml
|
||||||
|
|
||||||
# Also awful macOS files
|
# Also awful macOS files
|
||||||
*._*
|
*._*
|
||||||
|
|||||||
13
Makefile
Normal file
13
Makefile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
CC=python -m nuitka
|
||||||
|
|
||||||
|
linux:
|
||||||
|
$(CC) --show-progress --assume-yes-for-downloads --onefile wiipy.py -o wiipy
|
||||||
|
|
||||||
|
linux-install:
|
||||||
|
install wiipy /usr/bin/
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm wiipy
|
||||||
|
rm -rd wiipy.build
|
||||||
|
rm -rd wiipy.dist
|
||||||
|
rm -rd wiipy.onefile-build
|
||||||
25
README.md
25
README.md
@@ -5,22 +5,25 @@ 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").
|
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
|
## 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:
|
To make sure you have libWiiPy and all of its dependencies, you can simply run:
|
||||||
```shell
|
```shell
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
Basic usage for WiiPy is very simple.
|
Basic usage for WiiPy is very simple.
|
||||||
```shell
|
```shell
|
||||||
python3 wiipy.py <subcommand>
|
python3 wiipy.py <subcommand>
|
||||||
```
|
```
|
||||||
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
|
## 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 you've installed Nuitka first (it's included in `requirements.txt`).
|
||||||
@@ -28,3 +31,19 @@ If you'd like to compile WiiPy from Python into something a little more native,
|
|||||||
python -m nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py
|
python -m nuitka --show-progress --assume-yes-for-downloads --onefile wiipy.py
|
||||||
```
|
```
|
||||||
On macOS and Linux, this will give you a binary named `wiipy.bin` in the same directory as the Python file. On Windows, you'll get a binary named `wiipy.exe` instead.
|
On macOS and Linux, this will give you a binary named `wiipy.bin` in the same directory as the Python file. On Windows, you'll get a binary named `wiipy.exe` instead.
|
||||||
|
|
||||||
|
### For Linux Users:
|
||||||
|
A Makefile has been included to both build and install WiiPy on Linux. This will install `wiipy` to `/usr/bin/`.
|
||||||
|
|
||||||
|
First, use make to build WiiPy (this automates the step above):
|
||||||
|
```shell
|
||||||
|
make linux
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, run the install command with `sudo` (or your favorite alternative):
|
||||||
|
```shell
|
||||||
|
sudo make linux-install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# "ash.py" from WiiPy by NinjaCheetah
|
# "modules/archive/ash.py" from WiiPy by NinjaCheetah
|
||||||
# https://github.com/NinjaCheetah/WiiPy
|
# https://github.com/NinjaCheetah/WiiPy
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# "u8.py" from WiiPy by NinjaCheetah
|
# "modules/archive/u8.py" from WiiPy by NinjaCheetah
|
||||||
# https://github.com/NinjaCheetah/WiiPy
|
# https://github.com/NinjaCheetah/WiiPy
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
57
modules/nand/emunand.py
Normal file
57
modules/nand/emunand.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# "modules/nand/emunand.py" from WiiPy by NinjaCheetah
|
||||||
|
# https://github.com/NinjaCheetah/WiiPy
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import libWiiPy
|
||||||
|
|
||||||
|
|
||||||
|
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():
|
||||||
|
raise FileNotFoundError(input_path)
|
||||||
|
|
||||||
|
if input_path.is_dir():
|
||||||
|
wad_files = list(input_path.glob("*.[wW][aA][dD]"))
|
||||||
|
if not wad_files:
|
||||||
|
raise FileNotFoundError("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(open(pathlib.Path(input_str), "rb").read())
|
||||||
|
target_tid = title.tmd.title_id
|
||||||
|
else:
|
||||||
|
target_tid = input_str
|
||||||
|
|
||||||
|
if len(target_tid) != 16:
|
||||||
|
raise ValueError("Invalid Title ID! Title IDs must be 16 characters long.")
|
||||||
|
|
||||||
|
emunand.uninstall_title(target_tid)
|
||||||
|
|
||||||
|
print("Title uninstalled from EmuNAND!")
|
||||||
100
modules/nand/setting.py
Normal file
100
modules/nand/setting.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# "modules/nand/setting.py" from WiiPy by NinjaCheetah
|
||||||
|
# https://github.com/NinjaCheetah/WiiPy
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import libWiiPy
|
||||||
|
|
||||||
|
|
||||||
|
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():
|
||||||
|
raise FileNotFoundError(input_path)
|
||||||
|
|
||||||
|
# Load and decrypt the provided file.
|
||||||
|
setting = libWiiPy.nand.SettingTxt()
|
||||||
|
setting.load(open(input_path, "rb").read())
|
||||||
|
# Write out the decrypted data.
|
||||||
|
open(output_path, "w").write(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():
|
||||||
|
raise FileNotFoundError(input_path)
|
||||||
|
|
||||||
|
# Load and encrypt the provided file.
|
||||||
|
setting = libWiiPy.nand.SettingTxt()
|
||||||
|
setting.load_decrypted(open(input_path, "r").read())
|
||||||
|
# Write out the encrypted data.
|
||||||
|
open(output_path, "wb").write(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:
|
||||||
|
raise ValueError("The provided Serial Number is not valid!")
|
||||||
|
try:
|
||||||
|
int(args.serno[-9:])
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError("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:
|
||||||
|
raise ValueError("The provided region is not valid!")
|
||||||
|
if len(prefix) == 2 and args.region != "USA":
|
||||||
|
raise ValueError("The provided region does not match the provided Serial Number!")
|
||||||
|
elif len(prefix) == 3 and args.region == "USA":
|
||||||
|
raise ValueError("The provided region does not match the provided Serial Number!")
|
||||||
|
# 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}!")
|
||||||
125
modules/title/ciosbuild.py
Normal file
125
modules/title/ciosbuild.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# "modules/title/ciosbuild.py" from WiiPy by NinjaCheetah
|
||||||
|
# https://github.com/NinjaCheetah/WiiPy
|
||||||
|
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import pathlib
|
||||||
|
import libWiiPy
|
||||||
|
|
||||||
|
|
||||||
|
def build_cios(args):
|
||||||
|
base_path = pathlib.Path(args.base)
|
||||||
|
map_path = pathlib.Path(args.map)
|
||||||
|
if args.modules:
|
||||||
|
modules_path = pathlib.Path(args.modules)
|
||||||
|
else:
|
||||||
|
modules_path = pathlib.Path(os.getcwd())
|
||||||
|
output_path = pathlib.Path(args.output)
|
||||||
|
|
||||||
|
if not base_path.exists():
|
||||||
|
raise FileNotFoundError(base_path)
|
||||||
|
if not map_path.exists():
|
||||||
|
raise FileNotFoundError(map_path)
|
||||||
|
if not modules_path.exists():
|
||||||
|
raise FileNotFoundError(modules_path)
|
||||||
|
|
||||||
|
title = libWiiPy.title.Title()
|
||||||
|
title.load_wad(open(base_path, 'rb').read())
|
||||||
|
|
||||||
|
cios_tree = ET.parse(map_path)
|
||||||
|
cios_root = cios_tree.getroot()
|
||||||
|
|
||||||
|
# Iterate over all <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:
|
||||||
|
raise ValueError("The target cIOS could not be found in the provided map!")
|
||||||
|
|
||||||
|
# Iterate over all bases in the target cIOS to find a base that matches the provided WAD. If one is found, ensure
|
||||||
|
# that the version of the base in the map matches the version of the IOS WAD.
|
||||||
|
target_base = None
|
||||||
|
provided_base = int(title.tmd.title_id[-2:], 16)
|
||||||
|
for child in target_cios:
|
||||||
|
base = int(child.get("ios"))
|
||||||
|
if base == provided_base:
|
||||||
|
target_base = child
|
||||||
|
break
|
||||||
|
if target_base is None:
|
||||||
|
raise ValueError("The provided base IOS doesn't match any bases found in the provided map!")
|
||||||
|
base_version = int(target_base.get("version"))
|
||||||
|
if title.tmd.title_version != base_version:
|
||||||
|
raise ValueError("The provided base IOS does not match the required version for this base!")
|
||||||
|
|
||||||
|
# We're ready to begin building the cIOS now. Find all the <content> tags that have <patch> tags, and then apply
|
||||||
|
# the patches listed in them to the content.
|
||||||
|
for content in target_base.findall("content"):
|
||||||
|
patches = content.findall("patch")
|
||||||
|
if patches:
|
||||||
|
cid = int(content.get("id"), 16)
|
||||||
|
dec_content = title.get_content_by_cid(cid)
|
||||||
|
content_index = title.content.get_index_from_cid(cid)
|
||||||
|
with io.BytesIO(dec_content) as content_data:
|
||||||
|
for patch in patches:
|
||||||
|
# Read patch info from the map. This requires some conversion since ciosmap files seem to use a
|
||||||
|
# comma-separated list of bytes.
|
||||||
|
offset = int(patch.get("offset"), 16)
|
||||||
|
original_data = b''
|
||||||
|
original_data_map = patch.get("originalbytes").split(",")
|
||||||
|
for byte in original_data_map:
|
||||||
|
original_data += bytes.fromhex(byte[2:])
|
||||||
|
new_data = b''
|
||||||
|
new_data_map = patch.get("newbytes").split(",")
|
||||||
|
for byte in new_data_map:
|
||||||
|
new_data += bytes.fromhex(byte[2:])
|
||||||
|
# Seek to the target offset and apply the patches. One last sanity check to ensure this
|
||||||
|
# original data exists.
|
||||||
|
if original_data in dec_content:
|
||||||
|
content_data.seek(offset)
|
||||||
|
content_data.write(new_data)
|
||||||
|
else:
|
||||||
|
raise Exception("An error occurred while patching! Please make sure your base IOS is valid.")
|
||||||
|
content_data.seek(0x0)
|
||||||
|
dec_content = content_data.read()
|
||||||
|
# Set the content in the title to the newly-patched content, and set the type to normal.
|
||||||
|
title.set_content(dec_content, content_index, content_type=libWiiPy.title.ContentType.NORMAL)
|
||||||
|
|
||||||
|
# Next phase of cIOS building is to add the required extra modules to the end.
|
||||||
|
for content in target_base.findall("content"):
|
||||||
|
target_module = content.get("module")
|
||||||
|
if target_module is not None:
|
||||||
|
# The cIOS map supplies a Content ID to use for each additional module.
|
||||||
|
cid = int(content.get("id")[-2:], 16)
|
||||||
|
target_path = modules_path.joinpath(target_module + ".app")
|
||||||
|
if target_path.exists():
|
||||||
|
new_module = open(target_path, "rb").read()
|
||||||
|
title.add_content(new_module, cid, libWiiPy.title.ContentType.NORMAL)
|
||||||
|
else:
|
||||||
|
raise Exception(f"A required module \"{target_module}.app\" could not be found!")
|
||||||
|
|
||||||
|
# Last cIOS building step, we need to set the slot and version.
|
||||||
|
slot = args.slot
|
||||||
|
if 3 <= slot <= 255:
|
||||||
|
tid = title.tmd.title_id[:-2] + f"{slot:02X}"
|
||||||
|
title.set_title_id(tid)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"The provided slot \"{slot}\" is not valid!")
|
||||||
|
try:
|
||||||
|
title.set_title_version(args.version)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(f"The provided version \"{args.version}\" is not valid!")
|
||||||
|
|
||||||
|
# Ensure the WAD is fakesigned.
|
||||||
|
title.fakesign()
|
||||||
|
|
||||||
|
# Write the new cIOS to the specified output path.
|
||||||
|
out_file = open(output_path, "wb")
|
||||||
|
out_file.write(title.dump_wad())
|
||||||
|
out_file.close()
|
||||||
|
|
||||||
|
print("success")
|
||||||
37
modules/title/fakesign.py
Normal file
37
modules/title/fakesign.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# "modules/title/fakesign.py" from WiiPy by NinjaCheetah
|
||||||
|
# https://github.com/NinjaCheetah/WiiPy
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import libWiiPy
|
||||||
|
|
||||||
|
|
||||||
|
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():
|
||||||
|
raise FileNotFoundError(input_path)
|
||||||
|
|
||||||
|
if input_path.suffix.lower() == ".tmd":
|
||||||
|
tmd = libWiiPy.title.TMD()
|
||||||
|
tmd.load(open(input_path, "rb").read())
|
||||||
|
tmd.fakesign()
|
||||||
|
open(output_path, "wb").write(tmd.dump())
|
||||||
|
print("TMD fakesigned successfully!")
|
||||||
|
elif input_path.suffix.lower() == ".tik":
|
||||||
|
tik = libWiiPy.title.Ticket()
|
||||||
|
tik.load(open(input_path, "rb").read())
|
||||||
|
tik.fakesign()
|
||||||
|
open(output_path, "wb").write(tik.dump())
|
||||||
|
print("Ticket fakesigned successfully!")
|
||||||
|
elif input_path.suffix.lower() == ".wad":
|
||||||
|
title = libWiiPy.title.Title()
|
||||||
|
title.load_wad(open(input_path, "rb").read())
|
||||||
|
title.fakesign()
|
||||||
|
open(output_path, "wb").write(title.dump_wad())
|
||||||
|
print("WAD fakesigned successfully!")
|
||||||
|
else:
|
||||||
|
raise TypeError("This does not appear to be a TMD, Ticket, or WAD! Cannot fakesign.")
|
||||||
200
modules/title/info.py
Normal file
200
modules/title/info.py
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
# "modules/title/info.py" from WiiPy by NinjaCheetah
|
||||||
|
# https://github.com/NinjaCheetah/WiiPy
|
||||||
|
|
||||||
|
import re
|
||||||
|
import pathlib
|
||||||
|
import binascii
|
||||||
|
import libWiiPy
|
||||||
|
|
||||||
|
|
||||||
|
def _print_tmd_info(tmd: libWiiPy.title.TMD):
|
||||||
|
# Get all important keys from the TMD and print them out nicely.
|
||||||
|
print("Title Info")
|
||||||
|
print(f" Title ID: {tmd.title_id}")
|
||||||
|
# This type of version number really only applies to the System Menu and IOS.
|
||||||
|
if tmd.title_id[:8] == "00000001":
|
||||||
|
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")
|
||||||
|
else:
|
||||||
|
print(f" Required IOS: IOS{int(tmd.ios_tid[-2:], 16)} ({tmd.ios_tid})")
|
||||||
|
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("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.title_id[:8] == "00000001":
|
||||||
|
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)}")
|
||||||
|
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):
|
||||||
|
# Get all important keys from the TMD and print them out nicely.
|
||||||
|
print(f"Ticket Info")
|
||||||
|
print(f" Title ID: {ticket.title_id.decode()}")
|
||||||
|
# This type of version number really only applies to the System Menu and IOS.
|
||||||
|
if ticket.title_id.decode()[:8] == "00000001":
|
||||||
|
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)")
|
||||||
|
else:
|
||||||
|
print(f" Certificate Info: {ticket.signature_issuer} (Unknown)")
|
||||||
|
match ticket.common_key_index:
|
||||||
|
case 0:
|
||||||
|
key = "Common"
|
||||||
|
case 1:
|
||||||
|
key = "Korean"
|
||||||
|
case 2:
|
||||||
|
key = "vWii"
|
||||||
|
case _:
|
||||||
|
key = "Unknown (Likely Common)"
|
||||||
|
print(f" Common 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()}")
|
||||||
|
|
||||||
|
|
||||||
|
def _print_wad_info(title: libWiiPy.title.Title):
|
||||||
|
print(f"WAD Info")
|
||||||
|
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)}")
|
||||||
|
print("")
|
||||||
|
_print_ticket_info(title.ticket)
|
||||||
|
print("")
|
||||||
|
_print_tmd_info(title.tmd)
|
||||||
|
|
||||||
|
def _print_u8_info(u8_archive: libWiiPy.archive.U8Archive):
|
||||||
|
# Build the file structure of the U8 archive and print it out.
|
||||||
|
print(f"U8 Info")
|
||||||
|
# This variable stores the path of the directory we're currently processing.
|
||||||
|
current_dir = pathlib.Path()
|
||||||
|
# This variable stores the order of directory nodes leading to the current working directory, to make sure that
|
||||||
|
# things get where they belong.
|
||||||
|
parent_dirs = [0]
|
||||||
|
for node in range(len(u8_archive.u8_node_list)):
|
||||||
|
# Code for a directory node (excluding the root node since that already exists).
|
||||||
|
if u8_archive.u8_node_list[node].type == 1 and u8_archive.u8_node_list[node].name_offset != 0:
|
||||||
|
if u8_archive.u8_node_list[node].data_offset == parent_dirs[-1]:
|
||||||
|
current_dir = current_dir.joinpath(u8_archive.file_name_list[node])
|
||||||
|
print(("│" * (len(parent_dirs) - 1) + "├┬ ") + str(current_dir.name) + "/")
|
||||||
|
parent_dirs.append(node)
|
||||||
|
else:
|
||||||
|
# Go up until we're back at the correct level.
|
||||||
|
while u8_archive.u8_node_list[node].data_offset != parent_dirs[-1]:
|
||||||
|
parent_dirs.pop()
|
||||||
|
parent_dirs.append(node)
|
||||||
|
current_dir = pathlib.Path()
|
||||||
|
# Rebuild current working directory, and make sure all directories in the path exist.
|
||||||
|
for directory in parent_dirs:
|
||||||
|
current_dir = current_dir.joinpath(u8_archive.file_name_list[directory])
|
||||||
|
#print(("│" * (len(parent_dirs) - 1) + "┬ ") + str(current_dir.name))
|
||||||
|
# Code for a file node.
|
||||||
|
elif u8_archive.u8_node_list[node].type == 0:
|
||||||
|
print(("│" * (len(parent_dirs) - 1) + "├ ") + u8_archive.file_name_list[node])
|
||||||
|
|
||||||
|
|
||||||
|
def handle_info(args):
|
||||||
|
input_path = pathlib.Path(args.input)
|
||||||
|
|
||||||
|
if not input_path.exists():
|
||||||
|
raise FileNotFoundError(input_path)
|
||||||
|
|
||||||
|
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(open(input_path, "rb").read())
|
||||||
|
_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(open(input_path, "rb").read())
|
||||||
|
_print_ticket_info(tik)
|
||||||
|
elif input_path.suffix.lower() == ".wad":
|
||||||
|
title = libWiiPy.title.Title()
|
||||||
|
title.load_wad(open(input_path, "rb").read())
|
||||||
|
_print_wad_info(title)
|
||||||
|
elif input_path.suffix.lower() == ".arc":
|
||||||
|
u8_archive = libWiiPy.archive.U8Archive()
|
||||||
|
u8_archive.load(open(input_path, "rb").read())
|
||||||
|
_print_u8_info(u8_archive)
|
||||||
|
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(open(input_path, "rb").read())
|
||||||
|
_print_wad_info(title)
|
||||||
|
return
|
||||||
|
# This is the length of a normal magic number, WADs just require a little more checking.
|
||||||
|
magic_number = open(input_path, "rb").read(4)
|
||||||
|
# U8 archives have an annoying number of possible extensions, so this is definitely necessary.
|
||||||
|
if magic_number == b'\x55\xAA\x38\x2D':
|
||||||
|
u8_archive = libWiiPy.archive.U8Archive()
|
||||||
|
u8_archive.load(open(input_path, "rb").read())
|
||||||
|
_print_u8_info(u8_archive)
|
||||||
|
return
|
||||||
|
raise TypeError("This does not appear to be a supported file type! No info can be provided.")
|
||||||
129
modules/title/iospatcher.py
Normal file
129
modules/title/iospatcher.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# "modules/title/iospatcher.py" from WiiPy by NinjaCheetah
|
||||||
|
# https://github.com/NinjaCheetah/WiiPy
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import libWiiPy
|
||||||
|
|
||||||
|
|
||||||
|
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 not input_path.exists():
|
||||||
|
raise FileNotFoundError(input_path)
|
||||||
|
|
||||||
|
title = libWiiPy.title.Title()
|
||||||
|
title.load_wad(open(input_path, "rb").read())
|
||||||
|
|
||||||
|
tid = title.tmd.title_id
|
||||||
|
if tid[:8] != "00000001" or tid[8:] == "00000001" or tid[8:] == "00000002":
|
||||||
|
raise ValueError("This WAD does not appear to contain an IOS! Patching cannot continue.")
|
||||||
|
|
||||||
|
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:
|
||||||
|
raise ValueError("No patches were applied! Please select 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
|
||||||
|
if args.output is not None:
|
||||||
|
output_path = pathlib.Path(args.output)
|
||||||
|
output_file = open(output_path, "wb")
|
||||||
|
output_file.write(ios_patcher.title.dump_wad())
|
||||||
|
output_file.close()
|
||||||
|
else:
|
||||||
|
output_file = open(input_path, "wb")
|
||||||
|
output_file.write(ios_patcher.title.dump_wad())
|
||||||
|
output_file.close()
|
||||||
|
|
||||||
|
print("IOS successfully patched!")
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# "nus.py" from WiiPy by NinjaCheetah
|
# "modules/title/nus.py" from WiiPy by NinjaCheetah
|
||||||
# https://github.com/NinjaCheetah/WiiPy
|
# https://github.com/NinjaCheetah/WiiPy
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -106,15 +106,13 @@ def handle_nus_title(args):
|
|||||||
title.content.content_list = content_list
|
title.content.content_list = content_list
|
||||||
|
|
||||||
# Try to decrypt the contents for this title if a ticket was available.
|
# Try to decrypt the contents for this title if a ticket was available.
|
||||||
if can_decrypt is True and output_dir is not None:
|
if output_dir is not None:
|
||||||
|
if can_decrypt is True:
|
||||||
for content in range(len(title.tmd.content_records)):
|
for content in range(len(title.tmd.content_records)):
|
||||||
print(" - Decrypting content " + str(content + 1) + " of " + str(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) + ")...")
|
" (Content ID: " + str(title.tmd.content_records[content].content_id) + ")...")
|
||||||
dec_content = title.get_content_by_index(content)
|
dec_content = title.get_content_by_index(content)
|
||||||
content_file_name = hex(title.tmd.content_records[content].content_id)[2:]
|
content_file_name = f"{title.tmd.content_records[content].content_id:08X}".lower() + ".app"
|
||||||
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 = open(output_dir.joinpath(content_file_name), "wb")
|
||||||
dec_content_out.write(dec_content)
|
dec_content_out.write(dec_content)
|
||||||
dec_content_out.close()
|
dec_content_out.close()
|
||||||
@@ -158,9 +156,7 @@ def handle_nus_content(args):
|
|||||||
|
|
||||||
# Use the supplied output path if one was specified, otherwise generate one using the Content ID.
|
# Use the supplied output path if one was specified, otherwise generate one using the Content ID.
|
||||||
if out is None:
|
if out is None:
|
||||||
content_file_name = hex(content_id)[2:]
|
content_file_name = f"{content_id:08X}".lower()
|
||||||
while len(content_file_name) < 8:
|
|
||||||
content_file_name = "0" + content_file_name
|
|
||||||
output_path = pathlib.Path(content_file_name)
|
output_path = pathlib.Path(content_file_name)
|
||||||
else:
|
else:
|
||||||
output_path = pathlib.Path(out)
|
output_path = pathlib.Path(out)
|
||||||
@@ -222,3 +218,37 @@ def handle_nus_content(args):
|
|||||||
file.close()
|
file.close()
|
||||||
|
|
||||||
print("Downloaded content with Content ID \"" + cid + "\"!")
|
print("Downloaded content with Content ID \"" + cid + "\"!")
|
||||||
|
|
||||||
|
|
||||||
|
def handle_nus_tmd(args):
|
||||||
|
tid = args.tid
|
||||||
|
version = args.version
|
||||||
|
out = args.output
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Use the supplied output path if one was specified, otherwise generate one using the Title ID.
|
||||||
|
if out is None:
|
||||||
|
output_path = pathlib.Path(tid + ".tmd")
|
||||||
|
else:
|
||||||
|
output_path = pathlib.Path(out)
|
||||||
|
|
||||||
|
# Try to download the TMD, and catch the ValueError libWiiPy will throw if it can't be found.
|
||||||
|
print("Downloading TMD for title " + tid + "...")
|
||||||
|
try:
|
||||||
|
tmd_data = libWiiPy.title.download_tmd(tid, version)
|
||||||
|
except ValueError:
|
||||||
|
print("The Title ID or version you specified could not be found!")
|
||||||
|
return
|
||||||
|
|
||||||
|
file = open(output_path, "wb")
|
||||||
|
file.write(tmd_data)
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
print("Downloaded TMD for title \"" + tid + "\"!")
|
||||||
334
modules/title/wad.py
Normal file
334
modules/title/wad.py
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
# "modules/title/wad.py" from WiiPy by NinjaCheetah
|
||||||
|
# https://github.com/NinjaCheetah/WiiPy
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
from random import randint
|
||||||
|
import libWiiPy
|
||||||
|
|
||||||
|
|
||||||
|
def handle_wad_add(args):
|
||||||
|
input_path = pathlib.Path(args.input)
|
||||||
|
content_path = pathlib.Path(args.content)
|
||||||
|
if args.output is not None:
|
||||||
|
output_path = pathlib.Path(args.output)
|
||||||
|
else:
|
||||||
|
output_path = pathlib.Path(args.input)
|
||||||
|
|
||||||
|
if not input_path.exists():
|
||||||
|
raise FileNotFoundError(input_path)
|
||||||
|
if not content_path.exists():
|
||||||
|
raise FileNotFoundError(content_path)
|
||||||
|
|
||||||
|
wad_file = open(input_path, 'rb')
|
||||||
|
title = libWiiPy.title.Title()
|
||||||
|
title.load_wad(wad_file.read())
|
||||||
|
wad_file.close()
|
||||||
|
|
||||||
|
content_file = open(content_path, 'rb')
|
||||||
|
content_data = content_file.read()
|
||||||
|
content_file.close()
|
||||||
|
|
||||||
|
# Prepare the CID so it's ready when we go to add this content to the WAD.
|
||||||
|
# We need to both validate that this is a real CID, and also that it isn't already taken by another content.
|
||||||
|
if args.cid is not None:
|
||||||
|
if len(args.cid) != 8:
|
||||||
|
raise ValueError("The provided Content ID is invalid!")
|
||||||
|
target_cid = int(args.cid, 16)
|
||||||
|
for record in title.content.content_records:
|
||||||
|
if target_cid == record.content_id:
|
||||||
|
raise ValueError("The provided Content ID is already being used by this title!")
|
||||||
|
print(f"Using provided Content ID \"{target_cid:08X}\".")
|
||||||
|
# If we weren't given a CID, then we need to randomly assign one, and ensure it isn't being used.
|
||||||
|
else:
|
||||||
|
used_cids = []
|
||||||
|
for record in title.content.content_records:
|
||||||
|
used_cids.append(record.content_id)
|
||||||
|
target_cid = randint(0, 0x000000FF)
|
||||||
|
while target_cid in used_cids:
|
||||||
|
target_cid = randint(0, 0x000000FF)
|
||||||
|
print(f"Using randomly assigned Content ID \"{target_cid:08X}\" since none were provided.")
|
||||||
|
|
||||||
|
# Get the type of the new content.
|
||||||
|
if args.type is not None:
|
||||||
|
match str.lower(args.type):
|
||||||
|
case "normal":
|
||||||
|
target_type = libWiiPy.title.ContentType.NORMAL
|
||||||
|
case "shared":
|
||||||
|
target_type = libWiiPy.title.ContentType.SHARED
|
||||||
|
case "dlc":
|
||||||
|
target_type = libWiiPy.title.ContentType.DLC
|
||||||
|
case _:
|
||||||
|
raise ValueError("The provided content type is invalid!")
|
||||||
|
else:
|
||||||
|
target_type = libWiiPy.title.ContentType.NORMAL
|
||||||
|
|
||||||
|
# Call add_content to add our new content with the set parameters.
|
||||||
|
title.add_content(content_data, target_cid, target_type)
|
||||||
|
|
||||||
|
# Auto fakesign because we've edited the title.
|
||||||
|
title.fakesign()
|
||||||
|
|
||||||
|
out_file = open(output_path, 'wb')
|
||||||
|
out_file.write(title.dump_wad())
|
||||||
|
out_file.close()
|
||||||
|
|
||||||
|
print(f"Successfully added new content with Content ID \"{target_cid:08X}\" and type \"{target_type.name}\"!")
|
||||||
|
|
||||||
|
|
||||||
|
def handle_wad_pack(args):
|
||||||
|
input_path = pathlib.Path(args.input)
|
||||||
|
output_path = pathlib.Path(args.output)
|
||||||
|
|
||||||
|
# Make sure input path both exists and is a directory. Separate checks because this provides more relevant
|
||||||
|
# errors than just a NotADirectoryError if the actual issue is that there's nothing at all.
|
||||||
|
if not input_path.exists():
|
||||||
|
raise FileNotFoundError(input_path)
|
||||||
|
if not input_path.is_dir():
|
||||||
|
raise NotADirectoryError(input_path)
|
||||||
|
|
||||||
|
# Get a list of all files ending in .tmd, and then make sure that that list has *only* 1 entry. More than 1
|
||||||
|
# means we can't pack a WAD because we couldn't really tell which TMD is intended for this WAD.
|
||||||
|
tmd_list = list(input_path.glob('*.[tT][mM][dD]'))
|
||||||
|
if len(tmd_list) > 1:
|
||||||
|
raise FileExistsError("More than one TMD file was found! Only one TMD can be packed into a WAD.")
|
||||||
|
elif len(tmd_list) == 0:
|
||||||
|
raise FileNotFoundError("No TMD file found! Cannot pack WAD.")
|
||||||
|
else:
|
||||||
|
tmd_file = tmd_list[0]
|
||||||
|
|
||||||
|
# Repeat the same process as above for all .tik files.
|
||||||
|
ticket_list = list(input_path.glob('*.[tT][iI][kK]'))
|
||||||
|
if len(ticket_list) > 1:
|
||||||
|
raise FileExistsError("More than one Ticket file was found! Only one Ticket can be packed into a WAD.")
|
||||||
|
elif len(ticket_list) == 0:
|
||||||
|
raise FileNotFoundError("No Ticket file found! Cannot pack WAD.")
|
||||||
|
else:
|
||||||
|
ticket_file = ticket_list[0]
|
||||||
|
|
||||||
|
# And one more time for all .cert files.
|
||||||
|
cert_list = list(input_path.glob('*.[cC][eE][rR][tT]'))
|
||||||
|
if len(cert_list) > 1:
|
||||||
|
raise FileExistsError("More than one certificate file was found! Only one certificate can be packed into a "
|
||||||
|
"WAD.")
|
||||||
|
elif len(cert_list) == 0:
|
||||||
|
raise FileNotFoundError("No certificate file found! Cannot pack WAD.")
|
||||||
|
else:
|
||||||
|
cert_file = cert_list[0]
|
||||||
|
|
||||||
|
# Make sure that there's at least one content to pack.
|
||||||
|
content_files = list(input_path.glob("*.[aA][pP][pP]"))
|
||||||
|
if not content_files:
|
||||||
|
raise FileNotFoundError("No contents found! Cannot pack WAD.")
|
||||||
|
|
||||||
|
# Semi-hacky sorting method, but it works. Should maybe be changed eventually.
|
||||||
|
content_files_ordered = []
|
||||||
|
for index in range(len(content_files)):
|
||||||
|
content_files_ordered.append(None)
|
||||||
|
for content_file in content_files:
|
||||||
|
content_index = int(content_file.stem, 16)
|
||||||
|
content_files_ordered[content_index] = content_file
|
||||||
|
|
||||||
|
# Open the output file, and load all the component files that we've now verified we have into a libWiiPy Title()
|
||||||
|
# object.
|
||||||
|
with open(output_path, "wb") as output_path:
|
||||||
|
title = libWiiPy.title.Title()
|
||||||
|
|
||||||
|
title.load_tmd(open(tmd_file, "rb").read())
|
||||||
|
title.load_ticket(open(ticket_file, "rb").read())
|
||||||
|
title.wad.set_cert_data(open(cert_file, "rb").read())
|
||||||
|
# Footers are not super common and are not required, so we don't care about one existing until we get to
|
||||||
|
# the step where we'd pack it.
|
||||||
|
footer_file = list(input_path.glob("*.[fF][oO][oO][tT][eE][rR]"))[0]
|
||||||
|
if footer_file.exists():
|
||||||
|
title.wad.set_meta_data(open(footer_file, "rb").read())
|
||||||
|
# Method to ensure that the title's content records match between the TMD() and ContentRegion() objects.
|
||||||
|
title.load_content_records()
|
||||||
|
|
||||||
|
# Iterate over every file in the content_files list, and set them in the Title().
|
||||||
|
for record in title.content.content_records:
|
||||||
|
index = title.content.content_records.index(record)
|
||||||
|
dec_content = open(content_files_ordered[index], "rb").read()
|
||||||
|
title.set_content(dec_content, index)
|
||||||
|
|
||||||
|
# Fakesign the TMD and Ticket using the trucha bug, if enabled. This is built-in in libWiiPy v0.4.1+.
|
||||||
|
if args.fakesign:
|
||||||
|
title.fakesign()
|
||||||
|
|
||||||
|
output_path.write(title.dump_wad())
|
||||||
|
|
||||||
|
print("WAD file packed!")
|
||||||
|
|
||||||
|
|
||||||
|
def handle_wad_remove(args):
|
||||||
|
input_path = pathlib.Path(args.input)
|
||||||
|
if args.output is not None:
|
||||||
|
output_path = pathlib.Path(args.output)
|
||||||
|
else:
|
||||||
|
output_path = pathlib.Path(args.input)
|
||||||
|
|
||||||
|
if not input_path.exists():
|
||||||
|
raise FileNotFoundError(input_path)
|
||||||
|
|
||||||
|
wad_file = open(input_path, 'rb')
|
||||||
|
title = libWiiPy.title.Title()
|
||||||
|
title.load_wad(wad_file.read())
|
||||||
|
wad_file.close()
|
||||||
|
|
||||||
|
if args.index is not None:
|
||||||
|
# List indices in the title, and ensure that the target content index exists.
|
||||||
|
valid_indices = []
|
||||||
|
for record in title.content.content_records:
|
||||||
|
valid_indices.append(record.index)
|
||||||
|
if args.index not in valid_indices:
|
||||||
|
raise ValueError("The provided content index could not be found in this title!")
|
||||||
|
title.content.remove_content_by_index(args.index)
|
||||||
|
# Auto fakesign because we've edited the title.
|
||||||
|
title.fakesign()
|
||||||
|
out_file = open(output_path, 'wb')
|
||||||
|
out_file.write(title.dump_wad())
|
||||||
|
out_file.close()
|
||||||
|
print(f"Removed content at content index {args.index}!")
|
||||||
|
|
||||||
|
elif args.cid is not None:
|
||||||
|
if len(args.cid) != 8:
|
||||||
|
raise ValueError("The provided Content ID is invalid!")
|
||||||
|
target_cid = int(args.cid, 16)
|
||||||
|
# List Contents IDs in the title, and ensure that the target Content ID exists.
|
||||||
|
valid_ids = []
|
||||||
|
for record in title.content.content_records:
|
||||||
|
valid_ids.append(record.content_id)
|
||||||
|
if target_cid not in valid_ids:
|
||||||
|
raise ValueError("The provided Content ID could not be found in this title!")
|
||||||
|
title.content.remove_content_by_cid(target_cid)
|
||||||
|
# Auto fakesign because we've edited the title.
|
||||||
|
title.fakesign()
|
||||||
|
out_file = open(output_path, 'wb')
|
||||||
|
out_file.write(title.dump_wad())
|
||||||
|
out_file.close()
|
||||||
|
print(f"Removed content with Content ID \"{target_cid:08X}\"!")
|
||||||
|
|
||||||
|
|
||||||
|
def handle_wad_set(args):
|
||||||
|
input_path = pathlib.Path(args.input)
|
||||||
|
content_path = pathlib.Path(args.content)
|
||||||
|
if args.output is not None:
|
||||||
|
output_path = pathlib.Path(args.output)
|
||||||
|
else:
|
||||||
|
output_path = pathlib.Path(args.input)
|
||||||
|
|
||||||
|
if not input_path.exists():
|
||||||
|
raise FileNotFoundError(input_path)
|
||||||
|
if not content_path.exists():
|
||||||
|
raise FileNotFoundError(content_path)
|
||||||
|
|
||||||
|
title = libWiiPy.title.Title()
|
||||||
|
title.load_wad(open(input_path, "rb").read())
|
||||||
|
|
||||||
|
content_data = open(content_path, "rb").read()
|
||||||
|
|
||||||
|
# Get the new type of the content, if one was specified.
|
||||||
|
if args.type is not None:
|
||||||
|
match str.lower(args.type):
|
||||||
|
case "normal":
|
||||||
|
target_type = libWiiPy.title.ContentType.NORMAL
|
||||||
|
case "shared":
|
||||||
|
target_type = libWiiPy.title.ContentType.SHARED
|
||||||
|
case "dlc":
|
||||||
|
target_type = libWiiPy.title.ContentType.DLC
|
||||||
|
case _:
|
||||||
|
raise ValueError("The provided content type is invalid!")
|
||||||
|
else:
|
||||||
|
target_type = None
|
||||||
|
|
||||||
|
if args.index is not None:
|
||||||
|
# If we're replacing based on the index, then make sure the specified index exists.
|
||||||
|
existing_indices = []
|
||||||
|
for record in title.content.content_records:
|
||||||
|
existing_indices.append(record.index)
|
||||||
|
if args.index not in existing_indices:
|
||||||
|
raise ValueError("The provided index could not be found in this title!")
|
||||||
|
if target_type:
|
||||||
|
title.set_content(content_data, args.index, content_type=target_type)
|
||||||
|
else:
|
||||||
|
title.set_content(content_data, args.index)
|
||||||
|
# Auto fakesign because we've edited the title.
|
||||||
|
title.fakesign()
|
||||||
|
open(output_path, "wb").write(title.dump_wad())
|
||||||
|
print(f"Replaced content at content index {args.index}!")
|
||||||
|
|
||||||
|
|
||||||
|
elif args.cid is not None:
|
||||||
|
# If we're replacing based on the CID, then make sure the specified CID is valid and exists.
|
||||||
|
if len(args.cid) != 8:
|
||||||
|
raise ValueError("The provided Content ID is invalid!")
|
||||||
|
target_cid = int(args.cid, 16)
|
||||||
|
existing_cids = []
|
||||||
|
for record in title.content.content_records:
|
||||||
|
existing_cids.append(record.content_id)
|
||||||
|
if target_cid not in existing_cids:
|
||||||
|
raise ValueError("The provided Content ID could not be found in this title!")
|
||||||
|
target_index = title.content.get_index_from_cid(target_cid)
|
||||||
|
if target_type:
|
||||||
|
title.set_content(content_data, target_index, content_type=target_type)
|
||||||
|
else:
|
||||||
|
title.set_content(content_data, target_index)
|
||||||
|
# Auto fakesign because we've edited the title.
|
||||||
|
title.fakesign()
|
||||||
|
open(output_path, "wb").write(title.dump_wad())
|
||||||
|
print(f"Replaced content with Content ID \"{target_cid:08X}\"!")
|
||||||
|
|
||||||
|
|
||||||
|
def handle_wad_unpack(args):
|
||||||
|
input_path = pathlib.Path(args.input)
|
||||||
|
output_path = pathlib.Path(args.output)
|
||||||
|
|
||||||
|
if not input_path.exists():
|
||||||
|
raise FileNotFoundError(input_path)
|
||||||
|
# Check if the output path already exists, and if it does, ensure that it is both a directory and empty.
|
||||||
|
if output_path.exists():
|
||||||
|
# if output_path.is_dir() and next(os.scandir(output_path), None):
|
||||||
|
# raise ValueError("Output folder is not empty!")
|
||||||
|
if output_path.is_file():
|
||||||
|
raise ValueError("A file already exists with the provided directory name!")
|
||||||
|
else:
|
||||||
|
os.mkdir(output_path)
|
||||||
|
|
||||||
|
# Step through each component of a WAD and dump it to a file.
|
||||||
|
with open(args.input, "rb") as wad_file:
|
||||||
|
title = libWiiPy.title.Title()
|
||||||
|
title.load_wad(wad_file.read())
|
||||||
|
|
||||||
|
cert_name = title.tmd.title_id + ".cert"
|
||||||
|
cert_out = open(output_path.joinpath(cert_name), "wb")
|
||||||
|
cert_out.write(title.wad.get_cert_data())
|
||||||
|
cert_out.close()
|
||||||
|
|
||||||
|
tmd_name = title.tmd.title_id + ".tmd"
|
||||||
|
tmd_out = open(output_path.joinpath(tmd_name), "wb")
|
||||||
|
tmd_out.write(title.wad.get_tmd_data())
|
||||||
|
tmd_out.close()
|
||||||
|
|
||||||
|
ticket_name = title.tmd.title_id + ".tik"
|
||||||
|
ticket_out = open(output_path.joinpath(ticket_name), "wb")
|
||||||
|
ticket_out.write(title.wad.get_ticket_data())
|
||||||
|
ticket_out.close()
|
||||||
|
|
||||||
|
meta_name = title.tmd.title_id + ".footer"
|
||||||
|
meta_out = open(output_path.joinpath(meta_name), "wb")
|
||||||
|
meta_out.write(title.wad.get_meta_data())
|
||||||
|
meta_out.close()
|
||||||
|
|
||||||
|
# Skip validating hashes if -s/--skip-hash was passed.
|
||||||
|
if args.skip_hash:
|
||||||
|
skip_hash = True
|
||||||
|
else:
|
||||||
|
skip_hash = False
|
||||||
|
|
||||||
|
for content_file in range(0, title.tmd.num_contents):
|
||||||
|
content_file_name = f"{content_file:08X}".lower() + ".app"
|
||||||
|
content_out = open(output_path.joinpath(content_file_name), "wb")
|
||||||
|
content_out.write(title.get_content_by_index(content_file, skip_hash))
|
||||||
|
content_out.close()
|
||||||
|
|
||||||
|
print("WAD file unpacked!")
|
||||||
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!")
|
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
git+https://github.com/NinjaCheetah/libWiiPy
|
git+https://github.com/NinjaCheetah/libWiiPy
|
||||||
nuitka
|
nuitka==2.3.11
|
||||||
|
|||||||
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()
|
||||||
234
wiipy.py
234
wiipy.py
@@ -4,30 +4,109 @@
|
|||||||
import argparse
|
import argparse
|
||||||
from importlib.metadata import version
|
from importlib.metadata import version
|
||||||
|
|
||||||
from modules.wad import *
|
from modules.archive.ash import *
|
||||||
from modules.nus import *
|
from modules.archive.u8 import *
|
||||||
from modules.u8 import *
|
from modules.nand.emunand import *
|
||||||
from modules.ash import *
|
from modules.nand.setting import *
|
||||||
|
from modules.title.ciosbuild import *
|
||||||
|
from modules.title.fakesign import *
|
||||||
|
from modules.title.info import *
|
||||||
|
from modules.title.iospatcher import *
|
||||||
|
from modules.title.nus import *
|
||||||
|
from modules.title.wad import *
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Main argument parser.
|
# Main argument parser.
|
||||||
parser = argparse.ArgumentParser(
|
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",
|
parser.add_argument("--version", action="version",
|
||||||
version=f"WiiPy v1.2.0, based on libWiiPy v{version('libWiiPy')} (from branch \'main\')")
|
version=f"WiiPy v1.4.0, based on libWiiPy v{version('libWiiPy')} (from branch \'main\')")
|
||||||
subparsers = parser.add_subparsers(dest="subcommand", required=True)
|
subparsers = parser.add_subparsers(title="subcommands", dest="subcommand", required=True)
|
||||||
|
|
||||||
# Argument parser for the WAD subcommand.
|
# Argument parser for the ASH subcommand.
|
||||||
wad_parser = subparsers.add_parser("wad", help="pack/unpack a WAD file",
|
ash_parser = subparsers.add_parser("ash", help="compress/decompress an ASH file",
|
||||||
description="pack/unpack a WAD file")
|
description="compress/decompress an ASH file")
|
||||||
wad_parser.set_defaults(func=handle_wad)
|
ash_parser.set_defaults(func=handle_ash)
|
||||||
wad_group = wad_parser.add_mutually_exclusive_group(required=True)
|
ash_group = ash_parser.add_mutually_exclusive_group(required=True)
|
||||||
wad_group.add_argument("-p", "--pack", help="pack a directory to a WAD file", action="store_true")
|
ash_group.add_argument("-c", "--compress", help="compress a file into an ASH file", action="store_true")
|
||||||
wad_group.add_argument("-u", "--unpack", help="unpack a WAD file to a directory", action="store_true")
|
ash_group.add_argument("-d", "--decompress", help="decompress an ASH file", action="store_true")
|
||||||
wad_parser.add_argument("input", metavar="IN", type=str, help="input file")
|
ash_parser.add_argument("input", metavar="IN", type=str, help="input file")
|
||||||
wad_parser.add_argument("output", metavar="OUT", type=str, help="output file")
|
ash_parser.add_argument("output", metavar="OUT", type=str, help="output file")
|
||||||
wad_parser.add_argument("--fakesign", help="fakesign the TMD and Ticket (trucha bug)",
|
ash_parser.add_argument("--sym-bits", metavar="SYM_BITS", type=int,
|
||||||
action="store_true")
|
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)
|
||||||
|
|
||||||
|
# Argument parser for the cIOS command
|
||||||
|
cios_parser = subparsers.add_parser("cios", help="build a cIOS from a base IOS and provided map",
|
||||||
|
description="build a cIOS from a base IOS and provided map")
|
||||||
|
cios_parser.set_defaults(func=build_cios)
|
||||||
|
cios_parser.add_argument("base", metavar="BASE", type=str, help="base IOS WAD")
|
||||||
|
cios_parser.add_argument("map", metavar="MAP", type=str, help="cIOS map file")
|
||||||
|
cios_parser.add_argument("output", metavar="OUT", type=str, help="file to output the cIOS to")
|
||||||
|
cios_parser.add_argument("-c", "--cios-ver", metavar="CIOS", type=str,
|
||||||
|
help="cIOS version from the map to build", required=True)
|
||||||
|
cios_parser.add_argument("-m", "--modules", metavar="MODULES", type=str,
|
||||||
|
help="directory to look for cIOS modules in (optional, defaults to current directory)")
|
||||||
|
cios_parser.add_argument("-s", "--slot", metavar="SLOT", type=int,
|
||||||
|
help="slot that this cIOS will install to (optional, defaults to 249)", default=249)
|
||||||
|
cios_parser.add_argument("-v", "--version", metavar="VERSION", type=int,
|
||||||
|
help="version that this cIOS will be (optional, defaults to 65535)", default=65535)
|
||||||
|
|
||||||
|
# Argument parser for the EmuNAND subcommand.
|
||||||
|
emunand_parser = subparsers.add_parser("emunand", help="manage Wii EmuNAND directories",
|
||||||
|
description="manage Wii EmuNAND directories")
|
||||||
|
emunand_subparsers = emunand_parser.add_subparsers(title="emunand", dest="emunand", required=True)
|
||||||
|
# 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 to 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 Wii file",
|
||||||
|
description="get information about a Wii file")
|
||||||
|
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 check")
|
||||||
|
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 NUS subcommand.
|
# Argument parser for the NUS subcommand.
|
||||||
nus_parser = subparsers.add_parser("nus", help="download data from the NUS",
|
nus_parser = subparsers.add_parser("nus", help="download data from the NUS",
|
||||||
@@ -60,6 +139,44 @@ if __name__ == "__main__":
|
|||||||
nus_content_parser.add_argument("-o", "--output", metavar="OUT", type=str,
|
nus_content_parser.add_argument("-o", "--output", metavar="OUT", type=str,
|
||||||
help="path to download the content to (optional)")
|
help="path to download the content to (optional)")
|
||||||
nus_content_parser.add_argument("-d", "--decrypt", action="store_true", help="decrypt this content")
|
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 U8 subcommand.
|
# Argument parser for the U8 subcommand.
|
||||||
u8_parser = subparsers.add_parser("u8", help="pack/unpack a U8 archive",
|
u8_parser = subparsers.add_parser("u8", help="pack/unpack a U8 archive",
|
||||||
@@ -71,19 +188,74 @@ if __name__ == "__main__":
|
|||||||
u8_parser.add_argument("input", metavar="IN", type=str, help="input file")
|
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_parser.add_argument("output", metavar="OUT", type=str, help="output file")
|
||||||
|
|
||||||
# Argument parser for the ASH subcommand.
|
# Argument parser for the WAD subcommand.
|
||||||
ash_parser = subparsers.add_parser("ash", help="compress/decompress an ASH file",
|
wad_parser = subparsers.add_parser("wad", help="pack/unpack a WAD file",
|
||||||
description="compress/decompress an ASH file")
|
description="pack/unpack a WAD file")
|
||||||
ash_parser.set_defaults(func=handle_ash)
|
wad_subparsers = wad_parser.add_subparsers(dest="subcommand", required=True)
|
||||||
ash_group = ash_parser.add_mutually_exclusive_group(required=True)
|
# Add WAD subcommand.
|
||||||
ash_group.add_argument("-c", "--compress", help="compress a file into an ASH file", action="store_true")
|
wad_add_parser = wad_subparsers.add_parser("add", help="add decrypted content to a WAD file",
|
||||||
ash_group.add_argument("-d", "--decompress", help="decompress an ASH file", action="store_true")
|
description="add decrypted content to a WAD file; by default, this "
|
||||||
ash_parser.add_argument("input", metavar="IN", type=str, help="input file")
|
"will overwrite the input file unless an output is specified")
|
||||||
ash_parser.add_argument("output", metavar="OUT", type=str, help="output file")
|
wad_add_parser.set_defaults(func=handle_wad_add)
|
||||||
ash_parser.add_argument("--sym-bits", metavar="SYM_BITS", type=int,
|
wad_add_parser.add_argument("input", metavar="IN", type=str, help="WAD file to add to")
|
||||||
help="number of bits in each symbol tree leaf (default: 9)", default=9)
|
wad_add_parser.add_argument("content", metavar="CONTENT", type=str, help="decrypted content to add")
|
||||||
ash_parser.add_argument("--dist-bits", metavar="DIST_BITS", type=int,
|
wad_add_parser.add_argument("-c", "--cid", metavar="CID", type=str,
|
||||||
help="number of bits in each distance tree leaf (default: 11)", default=11)
|
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)")
|
||||||
|
# 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")
|
||||||
|
|
||||||
|
|
||||||
# Parse all the args, and call the appropriate function with all of those args if a valid subcommand was passed.
|
# Parse all the args, and call the appropriate function with all of those args if a valid subcommand was passed.
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|||||||
Reference in New Issue
Block a user