Files
dolphin/Tools/Generate-LetterBomb.py
Yaya48 a77c156bc8 IOS: add Starlet LLE milestone through HBC
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.
2026-08-25 17:11:49 +02:00

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()