mirror of
https://github.com/NinjaCheetah/libWiiPy.git
synced 2026-03-05 00:25:29 -05:00
Compare commits
18 Commits
v0.5.2
...
f98a3703a4
| Author | SHA1 | Date | |
|---|---|---|---|
|
f98a3703a4
|
|||
|
1e6952c2b2
|
|||
|
944fb896b5
|
|||
|
3d4d3dc99e
|
|||
|
62f99165c7
|
|||
|
e227f4e2be
|
|||
|
da16259938
|
|||
|
1cce0f14ee
|
|||
|
c86b44f35c
|
|||
|
1ff4ecdf68
|
|||
|
302bd842d1
|
|||
|
c5a007e1f5
|
|||
|
e96f6d9f13
|
|||
|
57b2ed63d4
|
|||
|
855200bb98
|
|||
| cfd105ba81 | |||
|
ed7e928ad8
|
|||
|
6b18254edc
|
@@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
# libWiiPy
|
||||
libWiiPy is a modern Python 3 library for handling the various files and formats found on the Wii. It aims to be simple to use, well maintained, and offer as many features as reasonably possible in one library, so that a newly-written Python program could do 100% of its Wii-related work with just one library. It also aims to be fully cross-platform, so that any tools written with it can also be cross-platform.
|
||||
|
||||
@@ -8,7 +8,7 @@ libWiiPy is inspired by [libWiiSharp](https://github.com/TheShadowEevee/libWiiSh
|
||||
# Features
|
||||
This list will expand as libWiiPy is developed, but these features are currently available:
|
||||
- TMD and Ticket parsing/editing (`.tmd`, `.tik`)
|
||||
- Title parsing/editing, including content encryption/decryption
|
||||
- Title parsing/editing, including content encryption/decryption (both retail and development)
|
||||
- WAD file parsing/editing (`.wad`)
|
||||
- Downloading titles from the NUS
|
||||
- Packing and unpacking U8 archives (`.app`, `.arc`)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 82 KiB |
BIN
docs/source/banner_old.png
Normal file
BIN
docs/source/banner_old.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -6,6 +6,7 @@
|
||||
:maxdepth: 4
|
||||
|
||||
libWiiPy.archive
|
||||
libWiiPy.media
|
||||
libWiiPy.nand
|
||||
libWiiPy.title
|
||||
```
|
||||
|
||||
11
docs/source/libWiiPy.media.md
Normal file
11
docs/source/libWiiPy.media.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# libWiiPy.media package
|
||||
|
||||
## Submodules
|
||||
|
||||
### libWiiPy.media.banner module
|
||||
```{eval-rst}
|
||||
.. automodule:: libWiiPy.media.banner
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "libWiiPy"
|
||||
version = "0.5.2"
|
||||
version = "0.6.0"
|
||||
authors = [
|
||||
{ name="NinjaCheetah", email="ninjacheetah@ncxprogramming.com" },
|
||||
{ name="Lillian Skinner", email="lillian@randommeaninglesscharacters.com" }
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
#
|
||||
# These are the essential submodules from libWiiPy that you'd probably want imported by default.
|
||||
|
||||
__all__ = ["archive", "nand", "title"]
|
||||
__all__ = ["archive", "media", "nand", "title"]
|
||||
|
||||
from . import archive
|
||||
from . import media
|
||||
from . import nand
|
||||
from . import title
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
#
|
||||
# See https://wiibrew.org/wiki/U8_archive for details about the U8 archive format.
|
||||
|
||||
import binascii
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
import pathlib
|
||||
from enum import IntEnum as _IntEnum
|
||||
from dataclasses import dataclass as _dataclass
|
||||
from typing import List
|
||||
from typing import List, Tuple
|
||||
from ..shared import _align_value, _pad_bytes
|
||||
|
||||
|
||||
@@ -36,13 +39,25 @@ class _U8Node:
|
||||
|
||||
|
||||
class U8Archive:
|
||||
def __init__(self):
|
||||
"""
|
||||
A U8 object that allows for managing the contents of a U8 archive.
|
||||
"""
|
||||
A U8 object that allows for parsing and editing the contents of a U8 archive.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
"""
|
||||
Attributes
|
||||
----------
|
||||
u8_node_list : List[_U8Node]
|
||||
A list of U8Node objects representing the nodes of the U8 archive.
|
||||
file_name_list : List[str]
|
||||
A list of the names of the files in the U8 archive.
|
||||
file_data_list : List[bytes]
|
||||
A list of file data for the files in the U8 archive; corresponds with file_name_list.
|
||||
header_size : int
|
||||
The size of the U8 archive header.
|
||||
data_offset : int
|
||||
The offset of the data region of the U8 archive.
|
||||
imet_header: IMETHeader
|
||||
The IMET header of the U8 archive, if one exists. Otherwise, an empty IMETHeader object.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.u8_magic = b''
|
||||
self.u8_node_list: List[_U8Node] = [] # All the nodes in the header of a U8 file.
|
||||
self.file_name_list: List[str] = []
|
||||
@@ -51,6 +66,7 @@ class U8Archive:
|
||||
self.header_size: int = 0
|
||||
self.data_offset: int = 0
|
||||
self.root_node: _U8Node = _U8Node(0, 0, 0, 0)
|
||||
self.imet_header: IMETHeader = IMETHeader()
|
||||
|
||||
def load(self, u8_data: bytes) -> None:
|
||||
"""
|
||||
@@ -66,7 +82,34 @@ class U8Archive:
|
||||
u8_data.seek(0x0)
|
||||
self.u8_magic = u8_data.read(4)
|
||||
if self.u8_magic != b'\x55\xAA\x38\x2D':
|
||||
raise TypeError("This is not a valid U8 archive!")
|
||||
# Check for an IMET header, if the file doesn't start with the proper magic number. The header magic
|
||||
# may be at either 0x40 or 0x80 depending on whether this title has a build tag at the start or not.
|
||||
u8_data.seek(0x40)
|
||||
self.u8_magic = u8_data.read(4)
|
||||
if self.u8_magic == b'\x49\x4D\x45\x54':
|
||||
# IMET with no build tag means the U8 archive should start at 0x600.
|
||||
u8_data.seek(0x600)
|
||||
self.u8_magic = u8_data.read(4)
|
||||
if self.u8_magic != b'\x55\xAA\x38\x2D':
|
||||
raise TypeError("This is not a valid U8 archive!")
|
||||
# Parse the IMET header, then continue parsing the U8 archive.
|
||||
u8_data.seek(0x0)
|
||||
self.imet_header.load(u8_data.read(0x600))
|
||||
else:
|
||||
# This check will pass if the IMET comes after a build tag.
|
||||
u8_data.seek(0x80)
|
||||
self.u8_magic = u8_data.read(4)
|
||||
if self.u8_magic == b'\x49\x4D\x45\x54':
|
||||
# IMET with a build tag means the U8 archive should start at 0x640.
|
||||
u8_data.seek(0x640)
|
||||
self.u8_magic = u8_data.read(4)
|
||||
if self.u8_magic != b'\x55\xAA\x38\x2D':
|
||||
raise TypeError("This is not a valid U8 archive!")
|
||||
# Parse the IMET header, then continue parsing the U8 archive.
|
||||
u8_data.seek(0x40)
|
||||
self.imet_header.load(u8_data.read(0x600))
|
||||
else:
|
||||
raise TypeError("This is not a valid U8 archive!")
|
||||
# Offset of the root node, which will always be 0x20.
|
||||
self.root_node_offset = int.from_bytes(u8_data.read(4))
|
||||
# The size of the U8 header.
|
||||
@@ -215,7 +258,7 @@ def extract_u8(u8_data, output_folder) -> None:
|
||||
open(current_dir.joinpath(u8_archive.file_name_list[node]), "wb").write(u8_archive.file_data_list[node])
|
||||
# Handle an invalid node type.
|
||||
elif u8_archive.u8_node_list[node].type != 0 and u8_archive.u8_node_list[node].type != 1:
|
||||
raise ValueError("A node with an invalid type (" + str(u8_archive.u8_node_list[node].type) + ") was found!")
|
||||
raise ValueError(f"A node with an invalid type ({str(u8_archive.u8_node_list[node].type)}) was found!")
|
||||
|
||||
|
||||
def _pack_u8_dir(u8_archive: U8Archive, current_path, node_count, parent_node):
|
||||
@@ -257,14 +300,23 @@ def _pack_u8_dir(u8_archive: U8Archive, current_path, node_count, parent_node):
|
||||
return u8_archive, node_count
|
||||
|
||||
|
||||
def pack_u8(input_path) -> bytes:
|
||||
def pack_u8(input_path, generate_imet=False, imet_titles:List[str]=None) -> bytes:
|
||||
"""
|
||||
Packs the provided file or folder into a new U8 archive, and returns the raw file data for it.
|
||||
|
||||
To generate an IMET header for this U8 archive, the archive must contain the required banner files "icon.bin",
|
||||
"banner.bin", and "sound.bin", because the sizes of these files are stored in the header.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input_path
|
||||
The path to the input file or folder.
|
||||
generate_imet : bool, optional
|
||||
Whether an IMET header should be generated for this U8 archive or not. IMET headers are only used for channel
|
||||
banners (00000000.app), and required banner files must exist to generate this header. Defaults to False.
|
||||
imet_titles : List[str], optional
|
||||
A list of the channel title in different languages for the IMET header. If only one item is provided, that
|
||||
item will be used for all entries in the header. Defaults to None, and is only used when generate_imet is True.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -283,8 +335,221 @@ def pack_u8(input_path) -> bytes:
|
||||
# subdirectory and file. Discard node_count and name_offset since we don't care about them here, as they're
|
||||
# really only necessary for the directory recursion.
|
||||
u8_archive, _ = _pack_u8_dir(u8_archive, input_path, node_count=1, parent_node=0)
|
||||
if generate_imet:
|
||||
print("gen imet")
|
||||
return u8_archive.dump()
|
||||
elif input_path.is_file():
|
||||
raise ValueError("This does not appear to be a directory.")
|
||||
else:
|
||||
raise FileNotFoundError("Input directory: \"" + str(input_path) + "\" does not exist!")
|
||||
raise FileNotFoundError(f"Input directory: \"{input_path}\" does not exist!")
|
||||
|
||||
|
||||
class IMETHeader:
|
||||
"""
|
||||
An IMETHeader object that allows for parsing, editing, and generating an IMET header. These headers precede the
|
||||
data of a channel banner (00000000.app), and are used to store metadata about the banner and verify its data.
|
||||
|
||||
An IMET header is always 1,536 (0x600) bytes long.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
magic : str
|
||||
Magic number for the header, should be "IMD5".
|
||||
header_size : int
|
||||
Length of the M
|
||||
imet_version : int
|
||||
Version of the IMET header. Normally always 3.
|
||||
sizes : List[int]
|
||||
The file sizes of icon.bin, banner.bin, and sound.bin.
|
||||
flag1 : int
|
||||
Unknown.
|
||||
channel_names : List[str]
|
||||
The name of the channel this header is for in Japanese, English, German, French, Spanish, Italian, Dutch,
|
||||
Simplified Chinese, Traditional Chinese, and Korean, in that order.
|
||||
md5_hash : bytes
|
||||
MD5 sum of the entire header, with this field being all zeros during the hashing.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.magic: str = "" # Should always be "IMET"
|
||||
self.header_size: int = 0 # Always 1536? I assumed this would mean something, but it's just the header length.
|
||||
self.imet_version: int = 0 # Always 3?
|
||||
self.sizes: List[int] = [] # Should only have 3 items
|
||||
self.flag1: int = 0 # Unknown
|
||||
self.channel_names: List[str] = [] # Should have 10 items
|
||||
self.md5_hash: bytes = b''
|
||||
|
||||
class LocalizedTitles(_IntEnum):
|
||||
TITLE_JAPANESE = 0
|
||||
TITLE_ENGLISH = 1
|
||||
TITLE_GERMAN = 2
|
||||
TITLE_FRENCH = 3
|
||||
TITLE_SPANISH = 4
|
||||
TITLE_ITALIAN = 5
|
||||
TITLE_DUTCH = 6
|
||||
TITLE_CHINESE_SIMPLIFIED = 7
|
||||
TITLE_CHINESE_TRADITIONAL = 8
|
||||
TITLE_KOREAN = 9
|
||||
|
||||
def load(self, imet_data: bytes) -> None:
|
||||
"""
|
||||
Loads the raw data of an IMET header.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
imet_data : bytes
|
||||
The data for the IMET header to load.
|
||||
"""
|
||||
with io.BytesIO(imet_data) as data:
|
||||
data.seek(0x40)
|
||||
self.magic = str(data.read(4).decode())
|
||||
self.header_size = int.from_bytes(data.read(4))
|
||||
self.imet_version = int.from_bytes(data.read(4))
|
||||
self.sizes = []
|
||||
for _ in range(0, 3):
|
||||
self.sizes.append(int.from_bytes(data.read(4)))
|
||||
self.flag1 = int.from_bytes(data.read(4))
|
||||
self.channel_names = []
|
||||
for _ in range(0, 10):
|
||||
# Read the translated channel name from the header, then drop all trailing null bytes. The encoding
|
||||
# used here is UTF-16 Big Endian.
|
||||
new_channel_name = data.read(84)
|
||||
self.channel_names.append(str(new_channel_name.decode('utf-16-be')).replace('\x00', ''))
|
||||
data.seek(data.tell() + 588)
|
||||
self.md5_hash = binascii.hexlify(data.read(16))
|
||||
|
||||
def dump(self) -> bytes:
|
||||
"""
|
||||
Dump the IMETHeader back into raw bytes.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytes
|
||||
The IMET header as bytes.
|
||||
"""
|
||||
imet_data = b''
|
||||
# 64 bytes of padding.
|
||||
imet_data += b'\x00' * 64
|
||||
# "IMET" magic number.
|
||||
imet_data += str.encode("IMET")
|
||||
# IMET header size. TODO: check if this is actually always 1536
|
||||
imet_data += int.to_bytes(1536, 4)
|
||||
# IMET header version.
|
||||
imet_data += int.to_bytes(self.imet_version, 4)
|
||||
# Banner component sizes.
|
||||
for size in self.sizes:
|
||||
imet_data += int.to_bytes(size, 4)
|
||||
# flag1.
|
||||
imet_data += int.to_bytes(self.flag1, 4)
|
||||
# Channel names.
|
||||
for channel_name in self.channel_names:
|
||||
new_channel_name = channel_name.encode('utf-16-be')
|
||||
while len(new_channel_name) < 84:
|
||||
new_channel_name += b'\x00'
|
||||
imet_data += new_channel_name
|
||||
# 588 (WHY??) bytes of padding.
|
||||
imet_data += b'\x00' * 588
|
||||
# MD5 hash. To calculate the real one, we need to write all zeros to it first, then hash the entire header with
|
||||
# the zero hash. After that we'll replace this hash with the calculated one.
|
||||
imet_data += b'\x00' * 16
|
||||
imet_hash = hashlib.md5(imet_data)
|
||||
imet_data = imet_data[:-16] + imet_hash.digest()
|
||||
return imet_data
|
||||
|
||||
def create(self, sizes: List[int], channel_names: Tuple[int, str] | List[Tuple[int, str]]) -> None:
|
||||
"""
|
||||
Create a new IMET header, specifying the sizes of the banner components and one or more localized channel names.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sizes : List[int]
|
||||
The size in bytes of icon.bin, banner.bin, and sound.bin, in that order.
|
||||
channel_names : Tuple(int, str), List[Tuple[int, str]]
|
||||
A pair or list of pairs of the target language and channel name for that language. Target languages are
|
||||
defined in LocalizedTitles.
|
||||
|
||||
See Also
|
||||
--------
|
||||
libWiiPy.archive.u8.IMETHeader.LocalizedTitles
|
||||
"""
|
||||
# Begin by setting the constant values before we parse the input.
|
||||
self.magic = "IMET"
|
||||
self.header_size = 1536
|
||||
self.imet_version = 3
|
||||
self.flag1 = 0 # Still not really sure about this one.
|
||||
# Validate the number of entries, then set the provided file sizes.
|
||||
if len(sizes) != 3:
|
||||
raise ValueError("You must supply 3 file sizes to generate an IMET header!")
|
||||
self.sizes = sizes
|
||||
# Now we can parse the channel names. This functions the same as setting them later, so just calling
|
||||
# set_channel_names() is the most practical.
|
||||
self.channel_names = ["" for _ in range(0, 10)]
|
||||
self.set_channel_names(channel_names)
|
||||
|
||||
def get_channel_names(self, target_languages: int | List[int]) -> str | List[str]:
|
||||
"""
|
||||
Get one or more channel names from the IMET header based on the specified languages.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
target_languages : int, List[int, str]
|
||||
One or more target languages. Target languages are defined in LocalizedTitles.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str, List[str]
|
||||
The channel name for the specified language, or a list of channel names in the same order as the specified
|
||||
languages.
|
||||
|
||||
See Also
|
||||
--------
|
||||
libWiiPy.archive.u8.IMETHeader.LocalizedTitles
|
||||
"""
|
||||
# Flatten single instance of LocalizedTitles being passed to a proper int.
|
||||
if isinstance(target_languages, self.LocalizedTitles):
|
||||
target_languages = int(target_languages)
|
||||
# If only one channel name was requested.
|
||||
if type(target_languages) == int:
|
||||
if target_languages not in self.LocalizedTitles:
|
||||
raise ValueError(f"The specified language is not valid!")
|
||||
return self.channel_names[target_languages]
|
||||
# If multiple channel names were requested.
|
||||
else:
|
||||
channel_names = []
|
||||
for lang in target_languages:
|
||||
if lang not in self.LocalizedTitles:
|
||||
raise ValueError(f"The specified language at index {target_languages.index(lang)} is not valid!")
|
||||
channel_names.append(self.channel_names[lang])
|
||||
return channel_names
|
||||
|
||||
def set_channel_names(self, channel_names: Tuple[int, str] | List[Tuple[int, str]]) -> None:
|
||||
"""
|
||||
Specify one or more new channel names to set in the IMET header.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
channel_names : Tuple(int, str), List[Tuple[int, str]]
|
||||
A pair or list of pairs of the target language and channel name for that language. Target languages are
|
||||
defined in LocalizedTitles.
|
||||
|
||||
See Also
|
||||
--------
|
||||
libWiiPy.archive.u8.IMETHeader.LocalizedTitles
|
||||
"""
|
||||
# If only one channel name was provided.
|
||||
if type(channel_names) == tuple:
|
||||
if channel_names[0] not in self.LocalizedTitles:
|
||||
raise ValueError(f"The target language \"{channel_names[0]}\" is not valid!")
|
||||
if len(channel_names[1]) > 42:
|
||||
raise ValueError(f"The channel name \"{channel_names[1]}\" is too long! Channel names cannot exceed "
|
||||
f"42 characters!")
|
||||
self.channel_names[channel_names[0]] = channel_names[1]
|
||||
# If a list of channel names was provided.
|
||||
else:
|
||||
for name in channel_names:
|
||||
if name[0] not in self.LocalizedTitles:
|
||||
raise ValueError(f"The target language \"{name[0]}\" for the name at index {channel_names.index(name)} "
|
||||
f"is not valid!")
|
||||
if len(name[1]) > 42:
|
||||
raise ValueError(f"The channel name \"{name[1]}\" at index {channel_names.index(name)} is too long! "
|
||||
f"Channel names cannot exceed 42 characters!")
|
||||
self.channel_names[name[0]] = name[1]
|
||||
|
||||
4
src/libWiiPy/media/__init__.py
Normal file
4
src/libWiiPy/media/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# "media/__init__.py" from libWiiPy by NinjaCheetah & Contributors
|
||||
# https://github.com/NinjaCheetah/libWiiPy
|
||||
|
||||
from .banner import *
|
||||
31
src/libWiiPy/media/banner.py
Normal file
31
src/libWiiPy/media/banner.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# "title/banner.py" from libWiiPy by NinjaCheetah & Contributors
|
||||
# https://github.com/NinjaCheetah/libWiiPy
|
||||
#
|
||||
# See https://wiibrew.org/wiki/Opening.bnr for details about the Wii's banner format
|
||||
|
||||
from dataclasses import dataclass as _dataclass
|
||||
|
||||
|
||||
@_dataclass
|
||||
class IMD5Header:
|
||||
"""
|
||||
An IMD5Header object that contains the properties of an IMD5 header. These headers precede the data of banner.bin
|
||||
and icon.bin inside the banner (00000000.app) of a channel, and are used to verify the data of those files.
|
||||
|
||||
An IMD5 header is always 32 bytes long.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
magic : str
|
||||
Magic number for the header, should be "IMD5".
|
||||
file_size : int
|
||||
The size of the file this header precedes.
|
||||
zeros : int
|
||||
8 bytes of zero padding.
|
||||
md5_hash : bytes
|
||||
The MD5 hash of the file this header precedes.
|
||||
"""
|
||||
magic: str # Should always be "IMD5"
|
||||
file_size: int
|
||||
zeros: int
|
||||
md5_hash: bytes
|
||||
@@ -6,7 +6,11 @@
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
from dataclasses import dataclass as _dataclass
|
||||
from typing import List
|
||||
from ..title.ticket import Ticket
|
||||
from ..title.title import Title
|
||||
from ..title.tmd import TMD
|
||||
from ..title.content import SharedContentMap as _SharedContentMap
|
||||
from .sys import UidSys as _UidSys
|
||||
|
||||
@@ -73,7 +77,7 @@ class EmuNAND:
|
||||
# Tickets are installed as <tid_lower>.tik in /ticket/<tid_upper>/
|
||||
ticket_dir = self.ticket_dir.joinpath(tid_upper)
|
||||
ticket_dir.mkdir(exist_ok=True)
|
||||
open(ticket_dir.joinpath(tid_lower + ".tik"), "wb").write(title.wad.get_ticket_data())
|
||||
ticket_dir.joinpath(f"{tid_lower}.tik").write_bytes(title.ticket.dump())
|
||||
|
||||
# The TMD and normal contents are installed to /title/<tid_upper>/<tid_lower>/content/, with the tmd being named
|
||||
# title.tmd and the contents being named <cid>.app.
|
||||
@@ -85,11 +89,11 @@ class EmuNAND:
|
||||
if content_dir.exists():
|
||||
shutil.rmtree(content_dir) # Clear the content directory so old contents aren't left behind.
|
||||
content_dir.mkdir(exist_ok=True)
|
||||
open(content_dir.joinpath("title.tmd"), "wb").write(title.wad.get_tmd_data())
|
||||
content_dir.joinpath("title.tmd").write_bytes(title.tmd.dump())
|
||||
for content_file in range(0, title.tmd.num_contents):
|
||||
if title.tmd.content_records[content_file].content_type == 1:
|
||||
content_file_name = f"{title.tmd.content_records[content_file].content_id:08X}".lower()
|
||||
open(content_dir.joinpath(content_file_name + ".app"), "wb").write(
|
||||
content_dir.joinpath(f"{content_file_name}.app").write_bytes(
|
||||
title.get_content_by_index(content_file, skip_hash=skip_hash))
|
||||
title_dir.joinpath("data").mkdir(exist_ok=True) # Empty directory used for save data for the title.
|
||||
|
||||
@@ -98,16 +102,16 @@ class EmuNAND:
|
||||
content_map = _SharedContentMap()
|
||||
existing_hashes = []
|
||||
if content_map_path.exists():
|
||||
content_map.load(open(content_map_path, "rb").read())
|
||||
content_map.load(content_map_path.read_bytes())
|
||||
for record in content_map.shared_records:
|
||||
existing_hashes.append(record.content_hash)
|
||||
for content_file in range(0, title.tmd.num_contents):
|
||||
if title.tmd.content_records[content_file].content_type == 32769:
|
||||
if title.tmd.content_records[content_file].content_hash not in existing_hashes:
|
||||
content_file_name = content_map.add_content(title.tmd.content_records[content_file].content_hash)
|
||||
open(self.shared1_dir.joinpath(content_file_name + ".app"), "wb").write(
|
||||
self.shared1_dir.joinpath(f"{content_file_name}.app").write_bytes(
|
||||
title.get_content_by_index(content_file, skip_hash=skip_hash))
|
||||
open(self.shared1_dir.joinpath("content.map"), "wb").write(content_map.dump())
|
||||
self.shared1_dir.joinpath("content.map").write_bytes(content_map.dump())
|
||||
|
||||
# The "footer" or meta file is installed as title.met in /meta/<tid_upper>/<tid_lower>/. Only write this if meta
|
||||
# is not nothing.
|
||||
@@ -117,7 +121,7 @@ class EmuNAND:
|
||||
meta_dir.mkdir(exist_ok=True)
|
||||
meta_dir = meta_dir.joinpath(tid_lower)
|
||||
meta_dir.mkdir(exist_ok=True)
|
||||
open(meta_dir.joinpath("title.met"), "wb").write(title.wad.get_meta_data())
|
||||
meta_dir.joinpath("title.met").write_bytes(title.wad.get_meta_data())
|
||||
|
||||
# Ensure we have a uid.sys file created.
|
||||
uid_sys_path = self.sys_dir.joinpath("uid.sys")
|
||||
@@ -159,3 +163,96 @@ class EmuNAND:
|
||||
# On the off chance this title has a meta entry, delete that too.
|
||||
if self.meta_dir.joinpath(tid_upper).joinpath(tid_lower).joinpath("title.met").exists():
|
||||
shutil.rmtree(self.meta_dir.joinpath(tid_upper).joinpath(tid_lower))
|
||||
|
||||
@_dataclass
|
||||
class InstalledTitles:
|
||||
"""
|
||||
An InstalledTitles object that is used to track a title type and any titles that belong to that type that are
|
||||
installed to an EmuNAND.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
type : str
|
||||
The type (Title ID high) of the installed titles.
|
||||
titles : List[str]
|
||||
The Title ID low of each installed title.
|
||||
"""
|
||||
type: str
|
||||
titles: List[str]
|
||||
|
||||
def get_installed_titles(self) -> List[InstalledTitles]:
|
||||
"""
|
||||
Scans for installed titles and returns a list of InstalledTitles objects, which each contain a title type
|
||||
(Title ID high) and a list of Title ID lows that are installed under it.
|
||||
|
||||
Returns
|
||||
-------
|
||||
List[InstalledTitles]
|
||||
The titles installed to the EmuNAND.
|
||||
"""
|
||||
# Scan for TID highs present.
|
||||
tid_highs = [d for d in self.title_dir.iterdir() if d.is_dir()]
|
||||
# Iterate through each one, verify that every TID low directory contains a TMD, and then add it to the list.
|
||||
installed_titles = []
|
||||
for high in tid_highs:
|
||||
tid_lows = [d for d in high.iterdir() if d.is_dir()]
|
||||
valid_lows = []
|
||||
for low in tid_lows:
|
||||
if low.joinpath("content", "title.tmd").exists():
|
||||
valid_lows.append(low.name)
|
||||
installed_titles.append(self.InstalledTitles(high.name, valid_lows))
|
||||
return installed_titles
|
||||
|
||||
def get_title_tmd(self, tid: str) -> TMD:
|
||||
"""
|
||||
Gets the TMD for a title installed to the EmuNAND, and returns it as a TMD objects. Returns an error if the
|
||||
TMD for the specified Title ID does not exist.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tid : str
|
||||
The Title ID of the Title to get the TMD for.
|
||||
|
||||
Returns
|
||||
-------
|
||||
TMD
|
||||
The TMD for the Title.
|
||||
"""
|
||||
# Validate the TID, then build a path to the TMD file to verify that it exists.
|
||||
if len(tid) != 16:
|
||||
raise ValueError(f"Title ID \"{tid}\" is not a valid!")
|
||||
tid_high = tid[:8].lower()
|
||||
tid_low = tid[8:].lower()
|
||||
tmd_path = self.title_dir.joinpath(tid_high, tid_low, "content", "title.tmd")
|
||||
if not tmd_path.exists():
|
||||
raise FileNotFoundError(f"Title with Title ID {tid} does not appear to be installed!")
|
||||
tmd = TMD()
|
||||
tmd.load(tmd_path.read_bytes())
|
||||
return tmd
|
||||
|
||||
def get_title_ticket(self, tid: str) -> Ticket:
|
||||
"""
|
||||
Gets the Ticket for a title installed to the EmuNAND, and returns it as a Ticket object. Returns an error if
|
||||
the Ticket for the specified Title ID does not exist.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tid : str
|
||||
The Title ID of the Title to get the Ticket for.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Ticket
|
||||
The Ticket for the Title.
|
||||
"""
|
||||
# Validate the TID, then build a path to the Ticket files to verify that it exists.
|
||||
if len(tid) != 16:
|
||||
raise ValueError(f"Title ID \"{tid}\" is not a valid!")
|
||||
tid_high = tid[:8].lower()
|
||||
tid_low = tid[8:].lower()
|
||||
ticket_path = self.ticket_dir.joinpath(tid_high, f"{tid_low}.tik")
|
||||
if not ticket_path.exists():
|
||||
raise FileNotFoundError(f"No Ticket exists for the title with Title ID {tid}!")
|
||||
ticket = Ticket()
|
||||
ticket.load(ticket_path.read_bytes())
|
||||
return ticket
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import requests
|
||||
import hashlib
|
||||
from typing import List
|
||||
from urllib.parse import urlparse as _urlparse
|
||||
from .title import Title
|
||||
from .tmd import TMD
|
||||
from .ticket import Ticket
|
||||
@@ -13,7 +14,8 @@ from .ticket import Ticket
|
||||
_nus_endpoint = ["http://nus.cdn.shop.wii.com/ccs/download/", "http://ccs.cdn.wup.shop.nintendo.net/ccs/download/"]
|
||||
|
||||
|
||||
def download_title(title_id: str, title_version: int = None, wiiu_endpoint: bool = False) -> Title:
|
||||
def download_title(title_id: str, title_version: int = None, wiiu_endpoint: bool = False,
|
||||
endpoint_override: str = None) -> Title:
|
||||
"""
|
||||
Download an entire title and all of its contents, then load the downloaded components into a Title object for
|
||||
further use. This method is NOT recommended for general use, as it has absolutely no verbosity. It is instead
|
||||
@@ -23,10 +25,13 @@ def download_title(title_id: str, title_version: int = None, wiiu_endpoint: bool
|
||||
----------
|
||||
title_id : str
|
||||
The Title ID of the title to download.
|
||||
title_version : int, option
|
||||
title_version : int, optional
|
||||
The version of the title to download. Defaults to latest if not set.
|
||||
wiiu_endpoint : bool, option
|
||||
wiiu_endpoint : bool, optional
|
||||
Whether the Wii U endpoint for the NUS should be used or not. This increases download speeds. Defaults to False.
|
||||
endpoint_override: str, optional
|
||||
A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
|
||||
set entirely overrides the "wiiu_endpoint" parameter.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -36,17 +41,18 @@ def download_title(title_id: str, title_version: int = None, wiiu_endpoint: bool
|
||||
# First, create the new title.
|
||||
title = Title()
|
||||
# Download and load the TMD, Ticket, and certs.
|
||||
title.load_tmd(download_tmd(title_id, title_version, wiiu_endpoint))
|
||||
title.load_ticket(download_ticket(title_id, wiiu_endpoint))
|
||||
title.wad.set_cert_data(download_cert(wiiu_endpoint))
|
||||
title.load_tmd(download_tmd(title_id, title_version, wiiu_endpoint, endpoint_override))
|
||||
title.load_ticket(download_ticket(title_id, wiiu_endpoint, endpoint_override))
|
||||
title.wad.set_cert_data(download_cert(wiiu_endpoint, endpoint_override))
|
||||
# Download all contents
|
||||
title.load_content_records()
|
||||
title.content.content_list = download_contents(title_id, title.tmd, wiiu_endpoint)
|
||||
title.content.content_list = download_contents(title_id, title.tmd, wiiu_endpoint, endpoint_override)
|
||||
# Return the completed title.
|
||||
return title
|
||||
|
||||
|
||||
def download_tmd(title_id: str, title_version: int = None, wiiu_endpoint: bool = False) -> bytes:
|
||||
def download_tmd(title_id: str, title_version: int = None, wiiu_endpoint: bool = False,
|
||||
endpoint_override: str = None) -> bytes:
|
||||
"""
|
||||
Downloads the TMD of the Title specified in the object. Will download the latest version by default, or another
|
||||
version if it was manually specified in the object.
|
||||
@@ -59,6 +65,9 @@ def download_tmd(title_id: str, title_version: int = None, wiiu_endpoint: bool =
|
||||
The version of the TMD to download. Defaults to latest if not set.
|
||||
wiiu_endpoint : bool, option
|
||||
Whether the Wii U endpoint for the NUS should be used or not. This increases download speeds. Defaults to False.
|
||||
endpoint_override: str, optional
|
||||
A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
|
||||
set entirely overrides the "wiiu_endpoint" parameter.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -67,10 +76,14 @@ def download_tmd(title_id: str, title_version: int = None, wiiu_endpoint: bool =
|
||||
"""
|
||||
# Build the download URL. The structure is download/<TID>/tmd for latest and download/<TID>/tmd.<version> for
|
||||
# when a specific version is requested.
|
||||
if wiiu_endpoint is False:
|
||||
tmd_url = _nus_endpoint[0] + title_id + "/tmd"
|
||||
if endpoint_override is not None:
|
||||
endpoint_url = _validate_endpoint(endpoint_override)
|
||||
else:
|
||||
tmd_url = _nus_endpoint[1] + title_id + "/tmd"
|
||||
if wiiu_endpoint:
|
||||
endpoint_url = _nus_endpoint[1]
|
||||
else:
|
||||
endpoint_url = _nus_endpoint[0]
|
||||
tmd_url = endpoint_url + title_id + "/tmd"
|
||||
# Add the version to the URL if one was specified.
|
||||
if title_version is not None:
|
||||
tmd_url += "." + str(title_version)
|
||||
@@ -89,7 +102,7 @@ def download_tmd(title_id: str, title_version: int = None, wiiu_endpoint: bool =
|
||||
return tmd
|
||||
|
||||
|
||||
def download_ticket(title_id: str, wiiu_endpoint: bool = False) -> bytes:
|
||||
def download_ticket(title_id: str, wiiu_endpoint: bool = False, endpoint_override: str = None) -> bytes:
|
||||
"""
|
||||
Downloads the Ticket of the Title specified in the object. This will only work if the Title ID specified is for
|
||||
a free title.
|
||||
@@ -100,6 +113,9 @@ def download_ticket(title_id: str, wiiu_endpoint: bool = False) -> bytes:
|
||||
The Title ID of the title to download the Ticket for.
|
||||
wiiu_endpoint : bool, option
|
||||
Whether the Wii U endpoint for the NUS should be used or not. This increases download speeds. Defaults to False.
|
||||
endpoint_override: str, optional
|
||||
A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
|
||||
set entirely overrides the "wiiu_endpoint" parameter.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -108,10 +124,14 @@ def download_ticket(title_id: str, wiiu_endpoint: bool = False) -> bytes:
|
||||
"""
|
||||
# Build the download URL. The structure is download/<TID>/cetk, and cetk will only exist if this is a free
|
||||
# title.
|
||||
if wiiu_endpoint is False:
|
||||
ticket_url = _nus_endpoint[0] + title_id + "/cetk"
|
||||
if endpoint_override is not None:
|
||||
endpoint_url = _validate_endpoint(endpoint_override)
|
||||
else:
|
||||
ticket_url = _nus_endpoint[1] + title_id + "/cetk"
|
||||
if wiiu_endpoint:
|
||||
endpoint_url = _nus_endpoint[1]
|
||||
else:
|
||||
endpoint_url = _nus_endpoint[0]
|
||||
ticket_url = endpoint_url + title_id + "/cetk"
|
||||
# Make the request.
|
||||
ticket_request = requests.get(url=ticket_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
|
||||
if ticket_request.status_code != 200:
|
||||
@@ -126,7 +146,7 @@ def download_ticket(title_id: str, wiiu_endpoint: bool = False) -> bytes:
|
||||
return ticket
|
||||
|
||||
|
||||
def download_cert(wiiu_endpoint: bool = False) -> bytes:
|
||||
def download_cert(wiiu_endpoint: bool = False, endpoint_override: str = None) -> bytes:
|
||||
"""
|
||||
Downloads the signing certificate used by all WADs. This uses System Menu 4.3U as the source.
|
||||
|
||||
@@ -134,6 +154,9 @@ def download_cert(wiiu_endpoint: bool = False) -> bytes:
|
||||
----------
|
||||
wiiu_endpoint : bool, option
|
||||
Whether the Wii U endpoint for the NUS should be used or not. This increases download speeds. Defaults to False.
|
||||
endpoint_override: str, optional
|
||||
A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
|
||||
set entirely overrides the "wiiu_endpoint" parameter.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -141,12 +164,15 @@ def download_cert(wiiu_endpoint: bool = False) -> bytes:
|
||||
The cert file.
|
||||
"""
|
||||
# Download the TMD and cetk for the System Menu 4.3U.
|
||||
if wiiu_endpoint is False:
|
||||
tmd_url = _nus_endpoint[0] + "0000000100000002/tmd.513"
|
||||
cetk_url = _nus_endpoint[0] + "0000000100000002/cetk"
|
||||
if endpoint_override is not None:
|
||||
endpoint_url = _validate_endpoint(endpoint_override)
|
||||
else:
|
||||
tmd_url = _nus_endpoint[1] + "0000000100000002/tmd.513"
|
||||
cetk_url = _nus_endpoint[1] + "0000000100000002/cetk"
|
||||
if wiiu_endpoint:
|
||||
endpoint_url = _nus_endpoint[1]
|
||||
else:
|
||||
endpoint_url = _nus_endpoint[0]
|
||||
tmd_url = endpoint_url + "0000000100000002/tmd.513"
|
||||
cetk_url = endpoint_url + "0000000100000002/cetk"
|
||||
tmd = requests.get(url=tmd_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True).content
|
||||
cetk = requests.get(url=cetk_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True).content
|
||||
# Assemble the certificate.
|
||||
@@ -163,7 +189,8 @@ def download_cert(wiiu_endpoint: bool = False) -> bytes:
|
||||
return cert
|
||||
|
||||
|
||||
def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False) -> bytes:
|
||||
def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False,
|
||||
endpoint_override: str = None) -> bytes:
|
||||
"""
|
||||
Downloads a specified content for the title specified in the object.
|
||||
|
||||
@@ -175,6 +202,9 @@ def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False
|
||||
The Content ID of the content you wish to download.
|
||||
wiiu_endpoint : bool, option
|
||||
Whether the Wii U endpoint for the NUS should be used or not. This increases download speeds. Defaults to False.
|
||||
endpoint_override: str, optional
|
||||
A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
|
||||
set entirely overrides the "wiiu_endpoint" parameter.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -185,10 +215,14 @@ def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False
|
||||
content_id_hex = hex(content_id)[2:]
|
||||
if len(content_id_hex) < 2:
|
||||
content_id_hex = "0" + content_id_hex
|
||||
if wiiu_endpoint is False:
|
||||
content_url = _nus_endpoint[0] + title_id + "/000000" + content_id_hex
|
||||
if endpoint_override is not None:
|
||||
endpoint_url = _validate_endpoint(endpoint_override)
|
||||
else:
|
||||
content_url = _nus_endpoint[1] + title_id + "/000000" + content_id_hex
|
||||
if wiiu_endpoint:
|
||||
endpoint_url = _nus_endpoint[1]
|
||||
else:
|
||||
endpoint_url = _nus_endpoint[0]
|
||||
content_url = endpoint_url + title_id + "/000000" + content_id_hex
|
||||
# Make the request.
|
||||
content_request = requests.get(url=content_url, headers={'User-Agent': 'wii libnup/1.0'}, stream=True)
|
||||
if content_request.status_code != 200:
|
||||
@@ -199,7 +233,8 @@ def download_content(title_id: str, content_id: int, wiiu_endpoint: bool = False
|
||||
return content_data
|
||||
|
||||
|
||||
def download_contents(title_id: str, tmd: TMD, wiiu_endpoint: bool = False) -> List[bytes]:
|
||||
def download_contents(title_id: str, tmd: TMD, wiiu_endpoint: bool = False,
|
||||
endpoint_override: str = None) -> List[bytes]:
|
||||
"""
|
||||
Downloads all the contents for the title specified in the object. This requires a TMD to already be available
|
||||
so that the content records can be accessed.
|
||||
@@ -212,6 +247,9 @@ def download_contents(title_id: str, tmd: TMD, wiiu_endpoint: bool = False) -> L
|
||||
The TMD that matches the title that the contents being downloaded are from.
|
||||
wiiu_endpoint : bool, option
|
||||
Whether the Wii U endpoint for the NUS should be used or not. This increases download speeds. Defaults to False.
|
||||
endpoint_override: str, optional
|
||||
A custom endpoint URL to use instead of the standard Wii or Wii U endpoints. Defaults to no override, and if
|
||||
set entirely overrides the "wiiu_endpoint" parameter.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -228,6 +266,29 @@ def download_contents(title_id: str, tmd: TMD, wiiu_endpoint: bool = False) -> L
|
||||
content_list = []
|
||||
for content_id in content_ids:
|
||||
# Call self.download_content() for each Content ID.
|
||||
content = download_content(title_id, content_id, wiiu_endpoint)
|
||||
content = download_content(title_id, content_id, wiiu_endpoint, endpoint_override)
|
||||
content_list.append(content)
|
||||
return content_list
|
||||
|
||||
|
||||
def _validate_endpoint(endpoint: str) -> str:
|
||||
"""
|
||||
Validate the provided NUS endpoint URL and append the required path if necessary.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
endpoint: str
|
||||
The NUS endpoint URL to validate.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The validated NUS endpoint with the proper path.
|
||||
"""
|
||||
# Find the root of the URL and then assemble the correct URL based on that.
|
||||
new_url = _urlparse(endpoint)
|
||||
if new_url.netloc == "":
|
||||
endpoint_url = "http://" + new_url.path + "/ccs/download/"
|
||||
else:
|
||||
endpoint_url = "http://" + new_url.netloc + "/ccs/download/"
|
||||
return endpoint_url
|
||||
|
||||
@@ -23,12 +23,11 @@ class _TitleLimit:
|
||||
Attributes
|
||||
----------
|
||||
limit_type : int
|
||||
The type of play limit applied.
|
||||
The type of play limit applied. 0 and 3 are none, 1 is a time limit, and 4 is a launch count limit.
|
||||
maximum_usage : int
|
||||
The maximum value for the type of play limit applied.
|
||||
"""
|
||||
# The type of play limit applied.
|
||||
# 0 = None, 1 = Time Limit, 3 = None, 4 = Launch Count
|
||||
limit_type: int
|
||||
# The maximum value of the limit applied.
|
||||
maximum_usage: int
|
||||
|
||||
@@ -58,9 +58,9 @@ class Title:
|
||||
self.content.load(self.wad.get_content_data(), self.tmd.content_records)
|
||||
# Ensure that the Title IDs of the TMD and Ticket match before doing anything else. If they don't, throw an
|
||||
# error because clearly something strange has gone on with the WAD and editing it probably won't work.
|
||||
if self.tmd.title_id != str(self.ticket.title_id.decode()):
|
||||
raise ValueError("The Title IDs of the TMD and Ticket in this WAD do not match. This WAD appears to be "
|
||||
"invalid.")
|
||||
#if self.tmd.title_id != str(self.ticket.title_id.decode()):
|
||||
# raise ValueError("The Title IDs of the TMD and Ticket in this WAD do not match. This WAD appears to be "
|
||||
# "invalid.")
|
||||
|
||||
def dump_wad(self) -> bytes:
|
||||
"""
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import io
|
||||
import binascii
|
||||
import hashlib
|
||||
import math
|
||||
import struct
|
||||
from typing import List
|
||||
from enum import IntEnum as _IntEnum
|
||||
@@ -390,14 +391,71 @@ class TMD:
|
||||
raise IndexError("Invalid content record! TMD lists '" + str(self.num_contents - 1) +
|
||||
"' contents but index was '" + str(record) + "'!")
|
||||
|
||||
def get_content_size(self, absolute=False, dlc=False) -> int:
|
||||
"""
|
||||
Gets the installed size of the content listed in the TMD, in bytes. This does not include the size of hash tree
|
||||
content, so the size of disc titles will not be calculated. The "absolute" option determines whether shared
|
||||
content sizes should be included in the total size or not. This option defaults to False. The "dlc" option
|
||||
determines whether DLC content sizes should be included in the total size or not. This option also defaults to
|
||||
False.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
absolute: bool, optional
|
||||
Whether shared contents should be included in the total size or not. Defaults to False.
|
||||
dlc: bool, optional
|
||||
Whether DLC contents should be included in the total size or not. Defaults to False.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The installed size of the content, in bytes.
|
||||
"""
|
||||
title_size = 0
|
||||
for record in self.content_records:
|
||||
if record.content_type == 0x8001:
|
||||
if absolute:
|
||||
title_size += record.content_size
|
||||
elif record.content_type == 0x4001:
|
||||
if dlc:
|
||||
title_size += record.content_size
|
||||
elif record.content_type != 3:
|
||||
title_size += record.content_size
|
||||
return title_size
|
||||
|
||||
def get_content_size_blocks(self, absolute=False, dlc=False) -> int:
|
||||
"""
|
||||
Gets the installed size of the content listed in the TMD, in the Wii's displayed "blocks" format. The
|
||||
"absolute" option determines whether shared content sizes should be included in the total size or not. This
|
||||
option defaults to False. The "dlc" option determines whether DLC content sizes should be included in the total
|
||||
size or not. This option also defaults to False.
|
||||
|
||||
1 Wii block is equal to 128KiB, and if any amount of a block is used, the entire block is considered used.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
absolute : bool, optional
|
||||
Whether shared contents should be included in the total size or not. Defaults to False.
|
||||
dlc: bool, optional
|
||||
Whether DLC contents should be included in the total size or not. Defaults to False.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The installed size of the content, in blocks.
|
||||
"""
|
||||
title_size_bytes = self.get_content_size(absolute, dlc)
|
||||
blocks = math.ceil(title_size_bytes / 131072)
|
||||
return blocks
|
||||
|
||||
class AccessFlags(_IntEnum):
|
||||
AHB = 0
|
||||
DVD_VIDEO = 1
|
||||
|
||||
def get_access_right(self, flag: int) -> bool:
|
||||
"""
|
||||
Gets whether an access rights flag is enabled or not. This is done by checking the specified bit. Possible flags
|
||||
and their corresponding bits are defined in the AccessFlags enum.
|
||||
Gets whether the specified access rights flag is enabled or not. This is done by checking the specified bit.
|
||||
Possible flags and their corresponding bits are defined in AccessFlags.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -408,6 +466,10 @@ class TMD:
|
||||
-------
|
||||
bool
|
||||
True if the flag is enabled, False otherwise.
|
||||
|
||||
See Also
|
||||
--------
|
||||
libWiiPy.title.tmd.TMD.AccessFlags
|
||||
"""
|
||||
return bool(self.access_rights & _bitmask(flag))
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# "types.py" from libWiiPy by NinjaCheetah & Contributors
|
||||
# https://github.com/NinjaCheetah/libWiiPy
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user