Boot original Wii firmware through boot0, boot1, boot2 and IOS on an emulated ARM Starlet. Model the required IPC, memory, SD, USB and Bluetooth hardware behavior, including IOS reload into IOS58, and add focused tests, launch utilities and architecture documentation.
122 lines
4.2 KiB
Python
122 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate a LetterBomb tree directly in Dolphin's virtual SD folder."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import hmac
|
|
import shutil
|
|
import zipfile
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path, PurePosixPath
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
DEFAULT_SOURCE = REPO_ROOT / ".starlet_check" / "letterbomb-web" / "public"
|
|
|
|
|
|
def parse_mac(text: str) -> bytes:
|
|
compact = text.replace(":", "").replace("-", "")
|
|
if len(compact) != 12:
|
|
raise argparse.ArgumentTypeError("MAC must contain exactly 12 hexadecimal digits")
|
|
try:
|
|
return bytes.fromhex(compact)
|
|
except ValueError as error:
|
|
raise argparse.ArgumentTypeError("MAC contains a non-hexadecimal character") from error
|
|
|
|
|
|
def safe_extract(archive: zipfile.ZipFile, destination: Path) -> None:
|
|
for member in archive.infolist():
|
|
relative = PurePosixPath(member.filename)
|
|
if relative.is_absolute() or ".." in relative.parts:
|
|
raise RuntimeError(f"Unsafe archive member: {member.filename!r}")
|
|
target = destination.joinpath(*relative.parts)
|
|
if member.is_dir():
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
continue
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
with archive.open(member) as source, target.open("wb") as output:
|
|
shutil.copyfileobj(source, output)
|
|
|
|
|
|
def generate(template: bytes, mac: bytes, message_time: datetime) -> tuple[bytes, Path]:
|
|
payload = bytearray(template)
|
|
key = hashlib.sha1(mac + b"uyy").digest()
|
|
payload[8:16] = key[:8]
|
|
payload[176:196] = bytes(20)
|
|
|
|
epoch = datetime(2000, 1, 1)
|
|
timestamp = int((message_time - epoch).total_seconds())
|
|
payload[124:128] = timestamp.to_bytes(4, "big")
|
|
payload[128:138] = f"{timestamp:010d}".encode("ascii")
|
|
payload[176:196] = hmac.new(key[8:], payload, hashlib.sha1).digest()
|
|
|
|
relative_path = Path(
|
|
"private",
|
|
"wii",
|
|
"title",
|
|
"HAEA",
|
|
key[:4].hex().upper(),
|
|
key[4:8].hex().upper(),
|
|
f"{message_time.year:04d}",
|
|
f"{message_time.month - 1:02d}",
|
|
f"{message_time.day:02d}",
|
|
f"{message_time.hour:02d}",
|
|
f"{message_time.minute:02d}",
|
|
"HABA_#1",
|
|
"txt",
|
|
f"{timestamp:08X}.000",
|
|
)
|
|
return bytes(payload), relative_path
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--mac", required=True, type=parse_mac)
|
|
parser.add_argument("--region", required=True, choices=("E", "U", "J", "K"))
|
|
parser.add_argument("--output", required=True, type=Path)
|
|
parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE)
|
|
parser.add_argument(
|
|
"--date",
|
|
help="message date as YYYY-MM-DD; defaults to yesterday in local Wii time",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
source = args.source.resolve()
|
|
output = args.output.resolve()
|
|
template_path = source / f"template{args.region}.bin"
|
|
bundle_path = source / "hackmii.zip"
|
|
if not template_path.is_file() or not bundle_path.is_file():
|
|
raise RuntimeError(f"LetterBomb assets are missing from {source}")
|
|
|
|
if args.date:
|
|
message_time = datetime.strptime(args.date, "%Y-%m-%d")
|
|
else:
|
|
message_time = datetime.now().replace(second=0, microsecond=0) - timedelta(days=1)
|
|
|
|
payload, relative_path = generate(template_path.read_bytes(), args.mac, message_time)
|
|
output.mkdir(parents=True, exist_ok=True)
|
|
target = output / relative_path
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_bytes(payload)
|
|
with zipfile.ZipFile(bundle_path) as archive:
|
|
safe_extract(archive, output)
|
|
|
|
mac_text = ":".join(f"{byte:02X}" for byte in args.mac)
|
|
(output / "LETTERBOMB-INFO.txt").write_text(
|
|
"LetterBomb virtual Wii test card\n"
|
|
f"System Menu: 4.3{args.region}\n"
|
|
f"Emulated Wi-Fi MAC: {mac_text}\n"
|
|
f"Message date: {message_time:%Y-%m-%d %H:%M}\n"
|
|
f"Message file: {relative_path.as_posix()}\n"
|
|
"Close Dolphin before changing files in this folder.\n",
|
|
encoding="utf-8",
|
|
)
|
|
print(f"LetterBomb generated at {target}")
|
|
print(f"Bundled HackMii Installer extracted to {output}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|