15 Commits

Author SHA1 Message Date
cef85b4951 WIP U8 archive info and improved support for matching other files 2024-10-11 19:55:19 -04:00
079c7f9109 Automatically fakesign WAD when using add/remove/set 2024-10-11 13:52:24 -04:00
0a9733a8d3 Changed setting generation syntax, added commands to encrypt/decrypt setting file 2024-10-10 14:42:12 -04:00
676dbab4f1 Added command to generate a setting.txt from a serial number and region 2024-10-09 20:40:55 -04:00
97bc77b337 Added wad set command to replace content in a WAD 2024-09-19 14:41:23 -04:00
0ae9ac5060 Added work in progress cIOS build command, use with caution 2024-09-13 22:19:53 -04:00
2316e938b2 Removed randomly inserted (and broken) import; thanks PyCharm 2024-09-11 11:18:51 -04:00
e047b12114 Added wad remove command to remove contents from a WAD by index or CID 2024-09-11 11:13:54 -04:00
a35ba2e4b6 Adjusted WAD command syntax, added subcommand to add content to an existing WAD 2024-09-08 13:16:37 -04:00
4730f3512b Info cmd: Improved region output, added boot content index, installed title size now displayed for WADs 2024-09-04 14:37:21 -04:00
e34c10c3fa Added missing regions to nus scraper 2024-08-22 23:18:30 -04:00
55c237f5f7 Merge remote-tracking branch 'origin/main' 2024-08-22 22:16:47 -04:00
c51244e8e7 Rewrote scraping script around requests instead of downloading the whole TMD every time 2024-08-22 22:16:35 -04:00
dc94ca09c1 Fix uninstalling titles from EmuNAND 2024-08-19 10:39:44 -07:00
02fa6d09ac Updated for libWiiPy v0.5.1 dev, fixed ticket/tmd cert info for info command 2024-08-14 01:34:18 -04:00
8 changed files with 777 additions and 172 deletions

8
.gitignore vendored
View File

@@ -164,7 +164,7 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
# Allows me to keep TMD files in my repository folder for testing without accidentally publishing them
# Allows me to keep Wii files in my repository folder for testing without accidentally publishing them
*.tmd
*.tik
*.cert
@@ -173,8 +173,14 @@ cython_debug/
*.app
*.arc
*.ash
*.met
out_prod/
remakewad.pl
content.map
uid.sys
SYSCONF
setting.txt
ciosmaps.xml
# Also awful macOS files
*._*

View File

@@ -1,4 +1,4 @@
# "modules/title/emunand.py" from WiiPy by NinjaCheetah
# "modules/nand/emunand.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import pathlib
@@ -6,7 +6,7 @@ import libWiiPy
def handle_emunand_title(args):
emunand = libWiiPy.title.EmuNAND(args.emunand)
emunand = libWiiPy.nand.EmuNAND(args.emunand)
if args.skip_hash:
skip_hash = True
else:
@@ -47,7 +47,7 @@ def handle_emunand_title(args):
title.load_wad(open(pathlib.Path(input_str), "rb").read())
target_tid = title.tmd.title_id
else:
target_tid = args.install
target_tid = input_str
if len(target_tid) != 16:
raise ValueError("Invalid Title ID! Title IDs must be 16 characters long.")

100
modules/nand/setting.py Normal file
View 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
View 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")

View File

@@ -1,6 +1,7 @@
# "modules/title/info.py" from WiiPy by NinjaCheetah
# https://github.com/NinjaCheetah/WiiPy
import re
import pathlib
import binascii
import libWiiPy
@@ -18,19 +19,37 @@ def _print_tmd_info(tmd: libWiiPy.title.TMD):
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" IOS Version: N/A")
print(f" Required IOS: N/A")
else:
print(f" Required IOS: IOS{int(tmd.ios_tid[-2:], 16)} ({tmd.ios_tid})")
if tmd.issuer.decode().find("CP00000004") != 1:
if tmd.signature_issuer.find("CP00000004") != -1:
print(f" Certificate: CP00000004 (Retail)")
print(f" Certificate Issuer: Root-CA00000001")
elif tmd.issuer.decode().find("CP00000007") != 1:
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")
elif tmd.issuer.decode().find("CP10000000") != 1:
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")
print(f" Region: {tmd.get_title_region()}")
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)}")
@@ -39,6 +58,8 @@ def _print_tmd_info(tmd: libWiiPy.title.TMD):
# 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())
@@ -58,14 +79,14 @@ def _print_ticket_info(ticket: libWiiPy.title.Ticket):
else:
print(f" Title Version: {ticket.title_version}")
print(f" Ticket Version: {ticket.ticket_version}")
if ticket.signature_issuer.find("XS00000003") != 1:
if ticket.signature_issuer.find("XS00000003") != -1:
print(f" Certificate: XS00000003 (Retail)")
print(f" Certificate Issuer: Root-CA00000001")
elif ticket.signature_issuer.find("XS00000006") != 1:
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")
print(f" Certificate Issuer: Root-CA00000002 (Development)")
else:
print(f" Certificate Info: {ticket.signature_issuer}")
print(f" Certificate Info: {ticket.signature_issuer} (Unknown)")
match ticket.common_key_index:
case 0:
key = "Common"
@@ -89,6 +110,18 @@ def _print_wad_info(title: libWiiPy.title.Title):
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("")
@@ -96,6 +129,35 @@ def _print_wad_info(title: libWiiPy.title.Title):
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)
@@ -103,11 +165,11 @@ def handle_info(args):
if not input_path.exists():
raise FileNotFoundError(input_path)
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())
_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())
_print_ticket_info(tik)
@@ -115,5 +177,24 @@ def handle_info(args):
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:
raise TypeError("This does not appear to be a TMD, Ticket, or WAD! No info 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(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.")

View File

@@ -3,15 +3,83 @@
import os
import pathlib
from random import randint
import libWiiPy
def handle_wad(args):
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)
# 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():
@@ -91,8 +159,130 @@ def handle_wad(args):
print("WAD file packed!")
# Code for if the --unpack argument was passed.
elif args.unpack:
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.

View File

@@ -1,10 +1,11 @@
import os
import libWiiPy
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 = ["45", "4A", "4B", "50"]
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:
@@ -18,11 +19,12 @@ for tid in tid_high:
print(f"Scraping titles of type: {ttype}")
for title in range(0, 65536):
for region in regions:
try:
tmd = libWiiPy.title.download_tmd(f"{tid}{ttype}{title:04X}{region}")
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}")
except ValueError:
else:
print(f"Invalid TID: {tid}{ttype}{title:04X}{region}")
pass
request.close()
log.close()

129
wiipy.py
View File

@@ -6,7 +6,9 @@ from importlib.metadata import version
from modules.archive.ash import *
from modules.archive.u8 import *
from modules.title.emunand import *
from modules.nand.emunand 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 *
@@ -18,7 +20,7 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="A simple command line tool to manage file formats used by the Wii.")
parser.add_argument("--version", action="version",
version=f"WiiPy v1.3.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(title="subcommands", dest="subcommand", required=True)
# Argument parser for the ASH subcommand.
@@ -35,6 +37,22 @@ if __name__ == "__main__":
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")
@@ -64,8 +82,8 @@ if __name__ == "__main__":
fakesign_parser.add_argument("-o", "--output", metavar="OUT", type=str, help="output file (optional)")
# Argument parser for the info command.
info_parser = subparsers.add_parser("info", help="get information about a TMD, Ticket, or WAD",
description="get information about a TMD, Ticket, or WAD")
info_parser = 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")
@@ -130,6 +148,36 @@ if __name__ == "__main__":
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.
u8_parser = subparsers.add_parser("u8", help="pack/unpack a U8 archive",
description="pack/unpack a U8 archive")
@@ -143,19 +191,72 @@ if __name__ == "__main__":
# Argument parser for the WAD subcommand.
wad_parser = subparsers.add_parser("wad", help="pack/unpack a WAD file",
description="pack/unpack a WAD file")
wad_parser.set_defaults(func=handle_wad)
wad_group = wad_parser.add_mutually_exclusive_group(required=True)
wad_group.add_argument("-p", "--pack", help="pack a directory to a WAD file", action="store_true")
wad_group.add_argument("-u", "--unpack", help="unpack a WAD file to a directory", action="store_true")
wad_parser.add_argument("input", metavar="IN", type=str, help="input file")
wad_parser.add_argument("output", metavar="OUT", type=str, help="output file")
wad_pack_group = wad_parser.add_argument_group(title="packing options")
wad_pack_group.add_argument("-f", "--fakesign", help="fakesign the TMD and Ticket (trucha bug)",
wad_subparsers = wad_parser.add_subparsers(dest="subcommand", required=True)
# Add WAD subcommand.
wad_add_parser = wad_subparsers.add_parser("add", help="add decrypted content to a WAD file",
description="add decrypted content to a WAD file; by default, this "
"will overwrite the input file unless an output is specified")
wad_add_parser.set_defaults(func=handle_wad_add)
wad_add_parser.add_argument("input", metavar="IN", type=str, help="WAD file to add to")
wad_add_parser.add_argument("content", metavar="CONTENT", type=str, help="decrypted content to add")
wad_add_parser.add_argument("-c", "--cid", metavar="CID", type=str,
help="Content ID to assign the new content (optional, will be randomly assigned if "
"not specified)")
wad_add_parser.add_argument("-t", "--type", metavar="TYPE", type=str,
help="the type of the new content, can be \"Normal\", \"Shared\", or \"DLC\" "
"(optional, will default to \"Normal\" if not specified)")
wad_add_parser.add_argument("-o", "--output", metavar="OUT", type=str,
help="file to output the updated WAD to (optional)")
# 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")
wad_unpack_group = wad_parser.add_argument_group(title="unpacking options")
wad_unpack_group.add_argument("-s", "--skip-hash", help="skips validating the hashes of decrypted "
# 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.
args = parser.parse_args()
args.func(args)