mirror of
https://github.com/NinjaCheetah/WiiPy.git
synced 2025-04-26 13:21:01 -04:00
Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
dfb527388c | |||
46784f126e | |||
8e7489ec57 | |||
b123005bf5 | |||
5f751acabd | |||
71c0726c4f | |||
1410dd6c36 | |||
9a89f80247 | |||
a6388834e0 | |||
514deb6b6c | |||
9ad0f8412c | |||
4e6c7d2dd0 | |||
eeb3387701 | |||
ceff61930b | |||
3e9f452885 | |||
f3eb127aee | |||
fa4e9bf2f1 | |||
d6aa50697f | |||
f9739eab58 | |||
8a050ff8f6 |
19
.github/workflows/python-build.yaml
vendored
19
.github/workflows/python-build.yaml
vendored
@ -19,7 +19,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install ccache for Nuitka
|
||||
run: sudo apt update && sudo apt install -y ccache libicu70
|
||||
run: |
|
||||
sudo apt update && \
|
||||
sudo apt install -y ccache patchelf
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@ -36,7 +38,7 @@ jobs:
|
||||
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
|
||||
@ -63,7 +65,7 @@ jobs:
|
||||
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-x86_64-bin
|
||||
@ -90,7 +92,7 @@ jobs:
|
||||
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-arm64-bin
|
||||
@ -114,7 +116,12 @@ jobs:
|
||||
- name: Build Application
|
||||
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
|
||||
|
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!")
|
||||
|
@ -26,8 +26,15 @@ def handle_u8_unpack(args):
|
||||
|
||||
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(input_path.read_bytes(), str(output_path))
|
||||
libWiiPy.archive.extract_u8(u8_data, str(output_path))
|
||||
|
||||
print("U8 archive unpacked!")
|
||||
|
@ -1,11 +1,185 @@
|
||||
# "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:
|
||||
@ -45,7 +219,7 @@ def handle_emunand_title(args):
|
||||
input_str = args.uninstall
|
||||
if pathlib.Path(input_str).exists():
|
||||
title = libWiiPy.title.Title()
|
||||
title.load_wad(open(pathlib.Path(input_str), "rb").read())
|
||||
title.load_wad(pathlib.Path(input_str).read_bytes())
|
||||
target_tid = title.tmd.title_id
|
||||
else:
|
||||
target_tid = input_str
|
||||
@ -53,6 +227,6 @@ def handle_emunand_title(args):
|
||||
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)
|
||||
emunand.uninstall_title(target_tid.lower())
|
||||
|
||||
print("Title uninstalled from EmuNAND!")
|
||||
|
@ -1,21 +1,31 @@
|
||||
# "commands/title/info.py" from WiiPy by NinjaCheetah
|
||||
# https://github.com/NinjaCheetah/WiiPy
|
||||
|
||||
import pathlib
|
||||
import binascii
|
||||
import pathlib
|
||||
import re
|
||||
import libWiiPy
|
||||
from modules.core import fatal_error
|
||||
|
||||
|
||||
def _print_tmd_info(tmd: libWiiPy.title.TMD):
|
||||
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:
|
||||
print(f" Title ID: {tmd.title_id.upper()} ({str(bytes.fromhex(tmd.title_id[8:]).decode()).upper()})")
|
||||
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[:8] == "00000001":
|
||||
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}")
|
||||
@ -23,6 +33,8 @@ def _print_tmd_info(tmd: libWiiPy.title.TMD):
|
||||
# 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:
|
||||
@ -31,6 +43,9 @@ def _print_tmd_info(tmd: libWiiPy.title.TMD):
|
||||
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)")
|
||||
@ -57,6 +72,21 @@ def _print_tmd_info(tmd: libWiiPy.title.TMD):
|
||||
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")
|
||||
@ -71,16 +101,24 @@ def _print_tmd_info(tmd: libWiiPy.title.TMD):
|
||||
print(f" Content Hash: {content.content_hash.decode()}")
|
||||
|
||||
|
||||
def _print_ticket_info(ticket: libWiiPy.title.Ticket):
|
||||
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:
|
||||
print(f" Title ID: {ticket.title_id.decode().upper()} "
|
||||
f"({str(bytes.fromhex(ticket.title_id.decode()[8:]).decode()).upper()})")
|
||||
except UnicodeDecodeError:
|
||||
print(f" Title ID: {ticket.title_id.decode().upper()}")
|
||||
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()[:8] == "00000001":
|
||||
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:
|
||||
@ -92,6 +130,9 @@ def _print_ticket_info(ticket: libWiiPy.title.Ticket):
|
||||
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:
|
||||
@ -109,10 +150,35 @@ def _print_ticket_info(ticket: libWiiPy.title.Ticket):
|
||||
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")
|
||||
@ -134,10 +200,30 @@ def _print_wad_info(title: libWiiPy.title.Title):
|
||||
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)
|
||||
_print_ticket_info(title.ticket, ticket_cert)
|
||||
print("")
|
||||
_print_tmd_info(title.tmd)
|
||||
_print_tmd_info(title.tmd, tmd_cert)
|
||||
|
||||
|
||||
def handle_info(args):
|
||||
@ -146,17 +232,26 @@ def handle_info(args):
|
||||
if not input_path.exists():
|
||||
fatal_error(f"The specified input file \"{input_path}\" does not exist!")
|
||||
|
||||
if input_path.suffix.lower() == ".tmd":
|
||||
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())
|
||||
tmd.load(input_path.read_bytes())
|
||||
_print_tmd_info(tmd)
|
||||
elif input_path.suffix.lower() == ".tik":
|
||||
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())
|
||||
tik.load(input_path.read_bytes())
|
||||
_print_ticket_info(tik)
|
||||
elif input_path.suffix.lower() == ".wad":
|
||||
title = libWiiPy.title.Title()
|
||||
title.load_wad(open(input_path, "rb").read())
|
||||
title.load_wad(input_path.read_bytes())
|
||||
_print_wad_info(title)
|
||||
else:
|
||||
fatal_error("This does not appear to be a TMD, Ticket, or WAD! No information can be provided.")
|
||||
# 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.")
|
||||
|
@ -8,120 +8,6 @@ import libWiiPy
|
||||
from modules.core import fatal_error
|
||||
|
||||
|
||||
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:
|
||||
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("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:
|
||||
output_dir.joinpath("tmd." + str(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))
|
||||
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(" - 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:
|
||||
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(" - 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 = 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.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.
|
||||
pathlib.Path(wad_file).write_bytes(title.dump_wad())
|
||||
|
||||
print("Downloaded title with Title ID \"" + args.tid + "\"!")
|
||||
|
||||
|
||||
def handle_nus_content(args):
|
||||
tid = args.tid
|
||||
cid = args.cid
|
||||
@ -151,7 +37,7 @@ def handle_nus_content(args):
|
||||
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)
|
||||
@ -199,6 +85,122 @@ def handle_nus_content(args):
|
||||
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
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
import pathlib
|
||||
import libWiiPy
|
||||
from modules.core import fatal_error
|
||||
from modules.title import tmd_edit_ios, tmd_edit_tid, tmd_edit_type
|
||||
from modules.title import title_edit_ios, title_edit_tid, title_edit_type
|
||||
|
||||
|
||||
def handle_tmd_edit(args):
|
||||
@ -21,13 +21,16 @@ def handle_tmd_edit(args):
|
||||
edits_made = False
|
||||
# Go over every possible change, and apply them if they were specified.
|
||||
if args.tid is not None:
|
||||
tmd = tmd_edit_tid(tmd, args.tid)
|
||||
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:
|
||||
tmd = tmd_edit_ios(tmd, args.ios)
|
||||
new_ios_tid = title_edit_ios(args.ios)
|
||||
tmd.ios_tid = new_ios_tid
|
||||
edits_made = True
|
||||
if args.type is not None:
|
||||
tmd = tmd_edit_type(tmd, args.type)
|
||||
new_tid = title_edit_type(tmd.title_id, args.type)
|
||||
tmd.set_title_id(new_tid)
|
||||
edits_made = True
|
||||
|
||||
if not edits_made:
|
||||
|
@ -1,11 +1,12 @@
|
||||
# "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 tmd_edit_ios, tmd_edit_tid, tmd_edit_type
|
||||
from modules.title import title_edit_ios, title_edit_tid, title_edit_type
|
||||
|
||||
|
||||
def handle_wad_add(args):
|
||||
@ -80,7 +81,6 @@ def handle_wad_convert(args):
|
||||
else:
|
||||
fatal_error("No valid encryption target was specified!")
|
||||
|
||||
output_path = pathlib.Path(args.output)
|
||||
if args.output is None:
|
||||
match target:
|
||||
case "development":
|
||||
@ -91,6 +91,8 @@ def handle_wad_convert(args):
|
||||
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!")
|
||||
@ -151,13 +153,49 @@ def handle_wad_edit(args):
|
||||
edits_made = False
|
||||
# Go over every possible change, and apply them if they were specified.
|
||||
if args.tid is not None:
|
||||
title.tmd = tmd_edit_tid(title.tmd, args.tid)
|
||||
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:
|
||||
title.tmd = tmd_edit_ios(title.tmd, args.ios)
|
||||
new_ios_tid = title_edit_ios(args.ios)
|
||||
title.tmd.ios_tid = new_ios_tid
|
||||
edits_made = True
|
||||
if args.type is not None:
|
||||
title.tmd = tmd_edit_type(title.tmd, args.type)
|
||||
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:
|
||||
@ -216,7 +254,7 @@ def handle_wad_pack(args):
|
||||
title = libWiiPy.title.Title()
|
||||
title.load_tmd(tmd_file.read_bytes())
|
||||
title.load_ticket(ticket_file.read_bytes())
|
||||
title.wad.set_cert_data(cert_file.read_bytes())
|
||||
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])
|
||||
|
@ -2,11 +2,11 @@
|
||||
# https://github.com/NinjaCheetah/WiiPy
|
||||
|
||||
import binascii
|
||||
import libWiiPy
|
||||
import re
|
||||
from modules.core import fatal_error
|
||||
|
||||
|
||||
def tmd_edit_ios(tmd: libWiiPy.title.TMD, new_ios: str) -> libWiiPy.title.TMD:
|
||||
def title_edit_ios(new_ios: str) -> str:
|
||||
# Setting a new required IOS.
|
||||
try:
|
||||
new_ios = int(new_ios)
|
||||
@ -15,24 +15,23 @@ def tmd_edit_ios(tmd: libWiiPy.title.TMD, new_ios: str) -> libWiiPy.title.TMD:
|
||||
if new_ios < 3 or new_ios > 255:
|
||||
fatal_error("The specified IOS is not valid! The new IOS version should be between 3 and 255.")
|
||||
new_ios_tid = f"00000001{new_ios:08X}"
|
||||
tmd.ios_tid = new_ios_tid
|
||||
return tmd
|
||||
return new_ios_tid
|
||||
|
||||
|
||||
def tmd_edit_tid(tmd: libWiiPy.title.TMD, new_tid: str) -> libWiiPy.title.TMD:
|
||||
# Setting a new TID, only changing TID low since this expects a 4 character ASCII input.
|
||||
if len(new_tid) != 4:
|
||||
fatal_error(f"The specified Title ID is not valid! The new Title ID should be 4 characters long.")
|
||||
if not new_tid.isalnum():
|
||||
fatal_error(f"The specified Title ID is not valid! The new Title ID should be alphanumeric.")
|
||||
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 = tmd.title_id[:8]
|
||||
tid_high = tid[:8]
|
||||
new_tid = f"{tid_high}{str(binascii.hexlify(new_tid.encode()), 'ascii')}"
|
||||
tmd.set_title_id(new_tid)
|
||||
return tmd
|
||||
return new_tid
|
||||
|
||||
|
||||
def tmd_edit_type(tmd: libWiiPy.title.TMD, new_type: str) -> libWiiPy.title.TMD:
|
||||
def title_edit_type(tid: str, new_type: str) -> str:
|
||||
# Setting a new title type.
|
||||
new_tid_high = None
|
||||
match new_type:
|
||||
@ -51,7 +50,6 @@ def tmd_edit_type(tmd: libWiiPy.title.TMD, new_type: str) -> libWiiPy.title.TMD:
|
||||
case _:
|
||||
fatal_error("The specified type is not valid! The new type must be one of: System, Channel, "
|
||||
"SystemChannel, GameChannel, DLC, HiddenChannel.")
|
||||
tid_low = tmd.title_id[8:]
|
||||
tid_low = tid[8:]
|
||||
new_tid = f"{new_tid_high}{tid_low}"
|
||||
tmd.set_title_id(new_tid)
|
||||
return tmd
|
||||
return new_tid
|
||||
|
82
wiipy.py
82
wiipy.py
@ -5,7 +5,8 @@ import argparse
|
||||
from importlib.metadata import version
|
||||
|
||||
from commands.archive.ash import *
|
||||
#from commands.archive.theme 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 *
|
||||
@ -17,7 +18,7 @@ from commands.title.nus import *
|
||||
from commands.title.tmd import *
|
||||
from commands.title.wad import *
|
||||
|
||||
wiipy_ver = "1.4.0"
|
||||
wiipy_ver = "1.5.1"
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Main argument parser.
|
||||
@ -30,7 +31,7 @@ if __name__ == "__main__":
|
||||
# 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="emunand", dest="emunand", required=True)
|
||||
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 "
|
||||
@ -76,12 +77,31 @@ if __name__ == "__main__":
|
||||
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 to the target EmuNAND directory")
|
||||
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 "
|
||||
@ -127,6 +147,28 @@ if __name__ == "__main__":
|
||||
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",
|
||||
description="download from the NUS")
|
||||
@ -144,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")
|
||||
@ -197,19 +241,19 @@ if __name__ == "__main__":
|
||||
setting_gen_parser.add_argument("region", metavar="REGION", type=str,
|
||||
help="region of the console these settings are for (USA, EUR, JPN, or KOR)")
|
||||
|
||||
# # Argument parser for the theme subcommand.
|
||||
# theme_parser = subparsers.add_parser("theme", help="apply custom themes to the Wii Menu",
|
||||
# description="apply custom themes to the Wii Menu")
|
||||
# theme_subparsers = theme_parser.add_subparsers(dest="subcommand", required=True)
|
||||
# # MYM theme subcommand.
|
||||
# theme_mym_parser = theme_subparsers.add_parser("mym", help="apply an MYM theme to the Wii Menu",
|
||||
# description="apply an MYM theme to the Wii Menu")
|
||||
# theme_mym_parser.set_defaults(func=handle_apply_mym)
|
||||
# theme_mym_parser.add_argument("mym", metavar="MYM", type=str, help="MYM theme to apply")
|
||||
# theme_mym_parser.add_argument("base", metavar="BASE", type=str,
|
||||
# help="base Wii Menu assets to apply the theme to (000000xx.app)")
|
||||
# theme_mym_parser.add_argument("output", metavar="OUT", type=str,
|
||||
# help="path to output the finished theme to (<filename>.csm)")
|
||||
# Argument parser for the theme subcommand.
|
||||
theme_parser = subparsers.add_parser("theme", help="apply custom themes to the Wii Menu",
|
||||
description="apply custom themes to the Wii Menu")
|
||||
theme_subparsers = theme_parser.add_subparsers(dest="subcommand", required=True)
|
||||
# MYM theme subcommand.
|
||||
theme_mym_parser = theme_subparsers.add_parser("mym", help="apply an MYM theme to the Wii Menu",
|
||||
description="apply an MYM theme to the Wii Menu")
|
||||
theme_mym_parser.set_defaults(func=handle_apply_mym)
|
||||
theme_mym_parser.add_argument("mym", metavar="MYM", type=str, help="MYM theme to apply")
|
||||
theme_mym_parser.add_argument("base", metavar="BASE", type=str,
|
||||
help="base Wii Menu assets to apply the theme to (000000xx.app)")
|
||||
theme_mym_parser.add_argument("output", metavar="OUT", type=str,
|
||||
help="path to output the finished theme to (<filename>.csm)")
|
||||
|
||||
# Argument parser for the TMD subcommand.
|
||||
tmd_parser = subparsers.add_parser("tmd", help="edit a TMD file",
|
||||
@ -314,6 +358,8 @@ if __name__ == "__main__":
|
||||
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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user