mirror of
https://github.com/NinjaCheetah/WiiPy.git
synced 2026-02-16 18:15:40 -05:00
Compare commits
15 Commits
v1.3.0
...
improved-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
cef85b4951
|
|||
|
079c7f9109
|
|||
|
0a9733a8d3
|
|||
|
676dbab4f1
|
|||
|
97bc77b337
|
|||
|
0ae9ac5060
|
|||
|
2316e938b2
|
|||
|
e047b12114
|
|||
|
a35ba2e4b6
|
|||
|
4730f3512b
|
|||
|
e34c10c3fa
|
|||
|
55c237f5f7
|
|||
|
c51244e8e7
|
|||
| dc94ca09c1 | |||
|
02fa6d09ac
|
8
.gitignore
vendored
8
.gitignore
vendored
@@ -164,7 +164,7 @@ 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
|
*.tik
|
||||||
*.cert
|
*.cert
|
||||||
@@ -173,8 +173,14 @@ cython_debug/
|
|||||||
*.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
|
||||||
*._*
|
*._*
|
||||||
|
|||||||
@@ -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
|
# https://github.com/NinjaCheetah/WiiPy
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
@@ -6,7 +6,7 @@ import libWiiPy
|
|||||||
|
|
||||||
|
|
||||||
def handle_emunand_title(args):
|
def handle_emunand_title(args):
|
||||||
emunand = libWiiPy.title.EmuNAND(args.emunand)
|
emunand = libWiiPy.nand.EmuNAND(args.emunand)
|
||||||
if args.skip_hash:
|
if args.skip_hash:
|
||||||
skip_hash = True
|
skip_hash = True
|
||||||
else:
|
else:
|
||||||
@@ -47,7 +47,7 @@ def handle_emunand_title(args):
|
|||||||
title.load_wad(open(pathlib.Path(input_str), "rb").read())
|
title.load_wad(open(pathlib.Path(input_str), "rb").read())
|
||||||
target_tid = title.tmd.title_id
|
target_tid = title.tmd.title_id
|
||||||
else:
|
else:
|
||||||
target_tid = args.install
|
target_tid = input_str
|
||||||
|
|
||||||
if len(target_tid) != 16:
|
if len(target_tid) != 16:
|
||||||
raise ValueError("Invalid Title ID! Title IDs must be 16 characters long.")
|
raise ValueError("Invalid Title ID! Title IDs must be 16 characters long.")
|
||||||
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")
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# "modules/title/info.py" from WiiPy by NinjaCheetah
|
# "modules/title/info.py" from WiiPy by NinjaCheetah
|
||||||
# https://github.com/NinjaCheetah/WiiPy
|
# https://github.com/NinjaCheetah/WiiPy
|
||||||
|
|
||||||
|
import re
|
||||||
import pathlib
|
import pathlib
|
||||||
import binascii
|
import binascii
|
||||||
import libWiiPy
|
import libWiiPy
|
||||||
@@ -18,19 +19,37 @@ def _print_tmd_info(tmd: libWiiPy.title.TMD):
|
|||||||
print(f" TMD Version: {tmd.tmd_version}")
|
print(f" TMD Version: {tmd.tmd_version}")
|
||||||
# IOSes just have an all-zero TID, so don't bothering showing that.
|
# IOSes just have an all-zero TID, so don't bothering showing that.
|
||||||
if tmd.ios_tid == "0000000000000000":
|
if tmd.ios_tid == "0000000000000000":
|
||||||
print(f" IOS Version: N/A")
|
print(f" Required IOS: N/A")
|
||||||
else:
|
else:
|
||||||
print(f" Required IOS: IOS{int(tmd.ios_tid[-2:], 16)} ({tmd.ios_tid})")
|
print(f" Required IOS: IOS{int(tmd.ios_tid[-2:], 16)} ({tmd.ios_tid})")
|
||||||
if tmd.issuer.decode().find("CP00000004") != 1:
|
if tmd.signature_issuer.find("CP00000004") != -1:
|
||||||
print(f" Certificate: CP00000004 (Retail)")
|
print(f" Certificate: CP00000004 (Retail)")
|
||||||
print(f" Certificate Issuer: Root-CA00000001")
|
print(f" Certificate Issuer: Root-CA00000001 (Retail)")
|
||||||
elif tmd.issuer.decode().find("CP00000007") != 1:
|
elif tmd.signature_issuer.find("CP00000007") != -1:
|
||||||
print(f" Certificate: CP00000007 (Development)")
|
print(f" Certificate: CP00000007 (Development)")
|
||||||
print(f" Certificate Issuer: Root-CA00000002")
|
print(f" Certificate Issuer: Root-CA00000002 (Development)")
|
||||||
elif tmd.issuer.decode().find("CP10000000") != 1:
|
elif tmd.signature_issuer.find("CP10000000") != -1:
|
||||||
print(f" Certificate: CP10000000 (Arcade)")
|
print(f" Certificate: CP10000000 (Arcade)")
|
||||||
print(f" Certificate Issuer: Root-CA10000000")
|
print(f" Certificate Issuer: Root-CA10000000 (Arcade)")
|
||||||
print(f" Region: {tmd.get_title_region()}")
|
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" Title Type: {tmd.get_title_type()}")
|
||||||
print(f" vWii Title: {bool(tmd.vwii)}")
|
print(f" vWii Title: {bool(tmd.vwii)}")
|
||||||
print(f" DVD Video Access: {tmd.get_access_right(tmd.AccessFlags.DVD_VIDEO)}")
|
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.
|
# Iterate over the content and print their details.
|
||||||
print("\nContent Info")
|
print("\nContent Info")
|
||||||
print(f" Total Contents: {tmd.num_contents}")
|
print(f" Total Contents: {tmd.num_contents}")
|
||||||
|
print(f" Boot Content Index: {tmd.boot_index}")
|
||||||
|
print(" Content Records:")
|
||||||
for content in tmd.content_records:
|
for content in tmd.content_records:
|
||||||
print(f" Content Index: {content.index}")
|
print(f" Content Index: {content.index}")
|
||||||
print(f" Content ID: " + f"{content.content_id:08X}".lower())
|
print(f" Content ID: " + f"{content.content_id:08X}".lower())
|
||||||
@@ -58,14 +79,14 @@ def _print_ticket_info(ticket: libWiiPy.title.Ticket):
|
|||||||
else:
|
else:
|
||||||
print(f" Title Version: {ticket.title_version}")
|
print(f" Title Version: {ticket.title_version}")
|
||||||
print(f" Ticket Version: {ticket.ticket_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: XS00000003 (Retail)")
|
||||||
print(f" Certificate Issuer: Root-CA00000001")
|
print(f" Certificate Issuer: Root-CA00000001 (Retail)")
|
||||||
elif ticket.signature_issuer.find("XS00000006") != 1:
|
elif ticket.signature_issuer.find("XS00000006") != -1:
|
||||||
print(f" Certificate: XS00000006 (Development)")
|
print(f" Certificate: XS00000006 (Development)")
|
||||||
print(f" Certificate Issuer: Root-CA00000002")
|
print(f" Certificate Issuer: Root-CA00000002 (Development)")
|
||||||
else:
|
else:
|
||||||
print(f" Certificate Info: {ticket.signature_issuer}")
|
print(f" Certificate Info: {ticket.signature_issuer} (Unknown)")
|
||||||
match ticket.common_key_index:
|
match ticket.common_key_index:
|
||||||
case 0:
|
case 0:
|
||||||
key = "Common"
|
key = "Common"
|
||||||
@@ -89,6 +110,18 @@ def _print_wad_info(title: libWiiPy.title.Title):
|
|||||||
print(f" WAD Type: boot2")
|
print(f" WAD Type: boot2")
|
||||||
case _:
|
case _:
|
||||||
print(f" WAD Type: Unknown ({title.wad.wad_type})")
|
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 Meta/Footer: {bool(title.wad.wad_meta_size)}")
|
||||||
print(f" Has CRL: {bool(title.wad.wad_crl_size)}")
|
print(f" Has CRL: {bool(title.wad.wad_crl_size)}")
|
||||||
print("")
|
print("")
|
||||||
@@ -96,6 +129,35 @@ def _print_wad_info(title: libWiiPy.title.Title):
|
|||||||
print("")
|
print("")
|
||||||
_print_tmd_info(title.tmd)
|
_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):
|
def handle_info(args):
|
||||||
input_path = pathlib.Path(args.input)
|
input_path = pathlib.Path(args.input)
|
||||||
@@ -103,11 +165,11 @@ def handle_info(args):
|
|||||||
if not input_path.exists():
|
if not input_path.exists():
|
||||||
raise FileNotFoundError(input_path)
|
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 = libWiiPy.title.TMD()
|
||||||
tmd.load(open(input_path, "rb").read())
|
tmd.load(open(input_path, "rb").read())
|
||||||
_print_tmd_info(tmd)
|
_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 = libWiiPy.title.Ticket()
|
||||||
tik.load(open(input_path, "rb").read())
|
tik.load(open(input_path, "rb").read())
|
||||||
_print_ticket_info(tik)
|
_print_ticket_info(tik)
|
||||||
@@ -115,5 +177,24 @@ def handle_info(args):
|
|||||||
title = libWiiPy.title.Title()
|
title = libWiiPy.title.Title()
|
||||||
title.load_wad(open(input_path, "rb").read())
|
title.load_wad(open(input_path, "rb").read())
|
||||||
_print_wad_info(title)
|
_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:
|
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.")
|
||||||
|
|||||||
@@ -3,15 +3,83 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
from random import randint
|
||||||
import libWiiPy
|
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)
|
input_path = pathlib.Path(args.input)
|
||||||
output_path = pathlib.Path(args.output)
|
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
|
# 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.
|
# errors than just a NotADirectoryError if the actual issue is that there's nothing at all.
|
||||||
if not input_path.exists():
|
if not input_path.exists():
|
||||||
@@ -91,8 +159,130 @@ def handle_wad(args):
|
|||||||
|
|
||||||
print("WAD file packed!")
|
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():
|
if not input_path.exists():
|
||||||
raise FileNotFoundError(input_path)
|
raise FileNotFoundError(input_path)
|
||||||
# Check if the output path already exists, and if it does, ensure that it is both a directory and empty.
|
# Check if the output path already exists, and if it does, ensure that it is both a directory and empty.
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
import libWiiPy
|
import requests
|
||||||
|
|
||||||
|
|
||||||
tid_high = ["00010000", "00010001", "00010005"]
|
tid_high = ["00010000", "00010001", "00010005"]
|
||||||
types = ["43", "44", "45", "46", "47", "48", "4A", "4C", "4D", "4E", "50", "51", "52", "53", "57", "58"]
|
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:
|
for tid in tid_high:
|
||||||
@@ -18,11 +19,12 @@ for tid in tid_high:
|
|||||||
print(f"Scraping titles of type: {ttype}")
|
print(f"Scraping titles of type: {ttype}")
|
||||||
for title in range(0, 65536):
|
for title in range(0, 65536):
|
||||||
for region in regions:
|
for region in regions:
|
||||||
try:
|
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)
|
||||||
tmd = libWiiPy.title.download_tmd(f"{tid}{ttype}{title:04X}{region}")
|
if request.status_code == 200:
|
||||||
print(f"Found valid TID: {tid}{ttype}{title:04X}{region}")
|
print(f"Found valid TID: {tid}{ttype}{title:04X}{region}")
|
||||||
log.write(f"{tid}{ttype}{title:02X}{region}")
|
log.write(f"{tid}{ttype}{title:02X}{region}")
|
||||||
except ValueError:
|
else:
|
||||||
print(f"Invalid TID: {tid}{ttype}{title:04X}{region}")
|
print(f"Invalid TID: {tid}{ttype}{title:04X}{region}")
|
||||||
pass
|
pass
|
||||||
|
request.close()
|
||||||
log.close()
|
log.close()
|
||||||
|
|||||||
129
wiipy.py
129
wiipy.py
@@ -6,7 +6,9 @@ from importlib.metadata import version
|
|||||||
|
|
||||||
from modules.archive.ash import *
|
from modules.archive.ash import *
|
||||||
from modules.archive.u8 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.fakesign import *
|
||||||
from modules.title.info import *
|
from modules.title.info import *
|
||||||
from modules.title.iospatcher import *
|
from modules.title.iospatcher import *
|
||||||
@@ -18,7 +20,7 @@ if __name__ == "__main__":
|
|||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="A simple command line tool to manage file formats used by the Wii.")
|
description="A simple command line tool to manage file formats used by the Wii.")
|
||||||
parser.add_argument("--version", action="version",
|
parser.add_argument("--version", action="version",
|
||||||
version=f"WiiPy v1.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)
|
subparsers = parser.add_subparsers(title="subcommands", dest="subcommand", required=True)
|
||||||
|
|
||||||
# Argument parser for the ASH subcommand.
|
# Argument parser for the ASH subcommand.
|
||||||
@@ -35,6 +37,22 @@ if __name__ == "__main__":
|
|||||||
ash_parser.add_argument("--dist-bits", metavar="DIST_BITS", type=int,
|
ash_parser.add_argument("--dist-bits", metavar="DIST_BITS", type=int,
|
||||||
help="number of bits in each distance tree leaf (default: 11)", default=11)
|
help="number of bits in each distance tree leaf (default: 11)", default=11)
|
||||||
|
|
||||||
|
# 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.
|
# Argument parser for the EmuNAND subcommand.
|
||||||
emunand_parser = subparsers.add_parser("emunand", help="manage Wii EmuNAND directories",
|
emunand_parser = subparsers.add_parser("emunand", help="manage Wii EmuNAND directories",
|
||||||
description="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)")
|
fakesign_parser.add_argument("-o", "--output", metavar="OUT", type=str, help="output file (optional)")
|
||||||
|
|
||||||
# Argument parser for the info command.
|
# Argument parser for the info command.
|
||||||
info_parser = subparsers.add_parser("info", help="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 TMD, Ticket, or WAD")
|
description="get information about a Wii file")
|
||||||
info_parser.set_defaults(func=handle_info)
|
info_parser.set_defaults(func=handle_info)
|
||||||
info_parser.add_argument("input", metavar="IN", type=str, help="input file")
|
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,
|
nus_tmd_parser.add_argument("-o", "--output", metavar="OUT", type=str,
|
||||||
help="path to download the TMD to (optional)")
|
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",
|
||||||
description="pack/unpack a U8 archive")
|
description="pack/unpack a U8 archive")
|
||||||
@@ -143,19 +191,72 @@ if __name__ == "__main__":
|
|||||||
# Argument parser for the WAD subcommand.
|
# Argument parser for the WAD subcommand.
|
||||||
wad_parser = subparsers.add_parser("wad", help="pack/unpack a WAD file",
|
wad_parser = subparsers.add_parser("wad", help="pack/unpack a WAD file",
|
||||||
description="pack/unpack a WAD file")
|
description="pack/unpack a WAD file")
|
||||||
wad_parser.set_defaults(func=handle_wad)
|
wad_subparsers = wad_parser.add_subparsers(dest="subcommand", required=True)
|
||||||
wad_group = wad_parser.add_mutually_exclusive_group(required=True)
|
# Add WAD subcommand.
|
||||||
wad_group.add_argument("-p", "--pack", help="pack a directory to a WAD file", action="store_true")
|
wad_add_parser = wad_subparsers.add_parser("add", help="add decrypted content to a WAD file",
|
||||||
wad_group.add_argument("-u", "--unpack", help="unpack a WAD file to a directory", action="store_true")
|
description="add decrypted content to a WAD file; by default, this "
|
||||||
wad_parser.add_argument("input", metavar="IN", type=str, help="input file")
|
"will overwrite the input file unless an output is specified")
|
||||||
wad_parser.add_argument("output", metavar="OUT", type=str, help="output file")
|
wad_add_parser.set_defaults(func=handle_wad_add)
|
||||||
wad_pack_group = wad_parser.add_argument_group(title="packing options")
|
wad_add_parser.add_argument("input", metavar="IN", type=str, help="WAD file to add to")
|
||||||
wad_pack_group.add_argument("-f", "--fakesign", help="fakesign the TMD and Ticket (trucha bug)",
|
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")
|
action="store_true")
|
||||||
wad_unpack_group = wad_parser.add_argument_group(title="unpacking options")
|
# Remove WAD subcommand.
|
||||||
wad_unpack_group.add_argument("-s", "--skip-hash", help="skips validating the hashes of decrypted "
|
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")
|
"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()
|
||||||
args.func(args)
|
args.func(args)
|
||||||
|
|||||||
Reference in New Issue
Block a user