Files
dusk/tools/makedolphiniso.py
T

138 lines
4.4 KiB
Python

#!/usr/bin/env python3
"""
Builds GameCube / Wii disc images (NTSC-J, NTSC-U, PAL) using xorrisofs /
mkisofs + Swiss-GC system-area headers.
The Swiss-GC headers contain a working GCN apploader that boots any DOL via
the El Torito catalog — no custom PPC assembly needed.
Headers are cached in <script_dir>/../buildtools/iso/ and downloaded from the
Swiss-GC repository on first use.
Disc layout (produced by xorrisofs):
sectors 0-15 system area (boot.bin + bi2 + apploader, from Swiss header)
sector 16 ISO 9660 PVD
sector 17 El Torito Boot Record
... directory records, file data
Dusk.dol (El Torito boot image — loaded by apploader)
dusk.dsk (asset archive — found at runtime via ISO 9660)
"""
import argparse
import os
import shutil
import struct
import subprocess
import sys
import urllib.request
SWISS_BASE = (
"https://raw.githubusercontent.com/emukidid/swiss-gc/"
"6f78e41940d2699b69ec62636d57411ae2857f1e/buildtools/iso/"
)
REGIONS = [
('j', 'J', 'NTSC-J', 0),
('u', 'E', 'NTSC-U', 1),
('e', 'P', 'PAL', 2),
]
def _buildtools_iso_dir():
here = os.path.dirname(os.path.abspath(__file__))
return os.path.join(here, '..', 'buildtools', 'iso')
def _get_hdr(region_suffix):
"""Return the raw bytes of the Swiss system-area header for *region_suffix*
('j', 'u', or 'e'), downloading it if necessary."""
cache_dir = _buildtools_iso_dir()
os.makedirs(cache_dir, exist_ok=True)
path = os.path.join(cache_dir, f'eltorito-{region_suffix}.hdr')
if not os.path.exists(path):
url = SWISS_BASE + f'eltorito-{region_suffix}.hdr'
print(f'Downloading {url} ...', flush=True)
with urllib.request.urlopen(url) as resp:
data = resp.read()
with open(path, 'wb') as f:
f.write(data)
with open(path, 'rb') as f:
return f.read()
def _patch_hdr(hdr_data, game_id, title):
"""Patch game ID (bytes 0-5) and title (bytes 0x20-0x3FF) into the
system-area header. Everything else (magic, bi2, apploader) is unchanged."""
hdr = bytearray(hdr_data)
hdr[0:6] = game_id.encode('ascii')
tb = title.encode('ascii')[:0x3E0]
hdr[0x20:0x20 + len(tb)] = tb
# zero remaining title bytes so stale Swiss title doesn't bleed through
hdr[0x20 + len(tb):0x400] = bytes(0x3E0 - len(tb))
return bytes(hdr)
def _find_mkisofs():
for tool in ('xorrisofs', 'mkisofs'):
if shutil.which(tool):
return tool
sys.exit('Error: neither xorrisofs nor mkisofs found. '
'Install xorriso (apt install xorriso) or mkisofs.')
def _build_iso(platform, game_id, title, region_suffix, dol_path, dsk_path,
out_path):
hdr_data = _patch_hdr(_get_hdr(region_suffix), game_id, title)
# Write patched header to a temp file alongside the output so we have a
# stable path (avoids tempfile cleanup races on some systems).
hdr_tmp = out_path + '.hdr.tmp'
try:
with open(hdr_tmp, 'wb') as f:
f.write(hdr_data)
mkisofs = _find_mkisofs()
dol_name = os.path.basename(dol_path)
cmd = [
mkisofs,
'-R', '-J',
'-G', hdr_tmp,
'-no-emul-boot',
'-eltorito-platform', 'PPC',
'-b', dol_name,
'-o', out_path,
dol_path,
dsk_path,
]
subprocess.run(cmd, check=True)
finally:
if os.path.exists(hdr_tmp):
os.remove(hdr_tmp)
size = os.path.getsize(out_path)
print(f'Created {out_path} ({size:,} bytes)')
def main():
ap = argparse.ArgumentParser(
description='Build GCN/Wii disc images from a DOL and asset file.')
ap.add_argument('platform', choices=['GCN', 'WII'])
ap.add_argument('dol', help='Path to Dusk.dol')
ap.add_argument('dsk', help='Path to dusk.dsk asset archive')
ap.add_argument('title', help='Game title string')
ap.add_argument('build_dir', help='Output directory')
args = ap.parse_args()
id_prefix = 'G' if args.platform == 'GCN' else 'R'
for region_suffix, region_char, region_name, _ in REGIONS:
game_id = f'{id_prefix}DK{region_char}01'
out_path = os.path.join(args.build_dir, f'Dusk-{region_name}.iso')
_build_iso(args.platform, game_id, args.title,
region_suffix, args.dol, args.dsk, out_path)
if __name__ == '__main__':
main()