ps1: bundle plain-ELF POPS loader + KEYS.BIN (iPoPS, MIT)

Fix 80020148 'game could not be started' on ARK-5: the base PBP's encrypted
~PSP loader fails to link on modern CFW. Use the bundled plain-ELF loader
(data/loader/DATA.PSP) as the DATA.PSP section instead, and write the 16-byte
NPDRM version key (data/loader/KEYS.BIN) alongside the EBOOT so CFW can
decrypt the STARTDAT PGD. Both assets are from iPoPS (MIT, attributed).

- load_data_psp: prefer the bundled ELF, fall back to the base ~PSP loader.
- load_keys_bin: return the bundled 16-byte key when present.
- convert: write KEYS.BIN next to the output EBOOT.
- README + data/loader/ATTRIBUTION.md: document the loader and its license.
This commit is contained in:
2026-09-07 17:44:54 -04:00
parent ea08f52d44
commit f61a77a31f
5 changed files with 89 additions and 19 deletions
+43 -7
View File
@@ -15,11 +15,13 @@ Format reference (offsets are byte-exact):
b"PSISOIMG0000", game id at +0x400, TOC at +0x800,
ISO block index at +0x4000, disc data at +0x100000.
A ``base.pbp`` (any PSN PS1 classic ``EBOOT.PBP``) is REQUIRED to supply two
blobs that we cannot synthesise:
A ``base.pbp`` (any PSN PS1 classic ``EBOOT.PBP``) supplies the blobs that we
cannot synthesise:
* DATA.PSP - the encrypted POPS loader (header section 8).
* STARTDAT - the trailing encrypted block (boot.png + PGD).
* DATA.PSP - fallback only: the encrypted POPS loader, used when the
bundled plain-ELF loader (``data/loader/DATA.PSP``, from
iPoPS/MIT) is unavailable.
Without a base.pbp the resulting EBOOT will not boot on real hardware; the
converter refuses to run without one.
@@ -82,6 +84,14 @@ INDEX_ENTRY_SIZE = 32 # offset u32 + length u32 + 6 unused u32
SYNC_PATTERN = b"\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00"
# Bundled plain-ELF POPS loader + NPDRM key (from iPoPS, MIT). Modern CFW
# (ARK/PRO/ME) loads this ELF directly as the DATA.PSP section, sidestepping
# the base PBP's encrypted ~PSP loader which frequently fails to link on
# 6.60/6.61 (error 80020148 "game could not be started").
_LOADER_DIR = Path(__file__).resolve().parent.parent / "data" / "loader"
BUNDLED_DATA_PSP = _LOADER_DIR / "DATA.PSP"
BUNDLED_KEYS_BIN = _LOADER_DIR / "KEYS.BIN"
# SFO field formats (PSP PARAM.SFO).
FMT_U32 = 0x0004
FMT_STRING = 0x0204
@@ -518,6 +528,25 @@ def extract_data_psp(base_pbp: Path) -> bytes:
return section[:prx_size]
def load_data_psp(base_pbp: Path) -> bytes:
"""Return the POPS loader (DATA.PSP) to embed.
Prefers the bundled plain-ELF loader (MIT, from iPoPS), which modern CFW
can load without ~PSP decryption. Falls back to the base PBP's encrypted
~PSP loader when the bundle is absent.
"""
if BUNDLED_DATA_PSP.is_file():
return BUNDLED_DATA_PSP.read_bytes()
return extract_data_psp(base_pbp)
def load_keys_bin() -> Optional[bytes]:
"""Return the bundled NPDRM version key (KEYS.BIN), if available."""
if BUNDLED_KEYS_BIN.is_file():
return BUNDLED_KEYS_BIN.read_bytes()
return None
def extract_startdat(base_pbp: Path) -> bytes:
"""Extract the trailing STARTDAT block (boot.png + PGD) from a base PBP.
@@ -865,7 +894,7 @@ def convert(
disc = DiscImage(None, source)
# Resources required from the base PBP.
data_psp = extract_data_psp(base_pbp)
data_psp = load_data_psp(base_pbp)
startdat = extract_startdat(base_pbp)
_warn_old_base(base_pbp)
@@ -933,6 +962,13 @@ def convert(
except OSError:
pass
raise
# Write the NPDRM version key next to the EBOOT (the popstation/iPoPS
# convention; ARK and other CFW read it to decrypt the STARTDAT PGD).
keys = load_keys_bin()
if keys is not None:
out_path.with_name("KEYS.BIN").write_bytes(keys)
return out_path
@@ -948,9 +984,9 @@ def convert_to_folder(
) -> Path:
"""Convert a PS1 disc into a game folder ready for /PSP/GAME.
Writes ``<out_dir>/<GAME_ID>/EBOOT.PBP`` — the same single-file output the
popstation tool produces. No KEYS.BIN or DOCUMENT.DAT is written:
popstation doesn't emit them and CFW (ARK-4 POPS) doesn't need them.
Writes ``<out_dir>/<GAME_ID>/EBOOT.PBP`` plus a ``KEYS.BIN`` (the NPDRM
version key modern CFW uses to decrypt the STARTDAT PGD). No
DOCUMENT.DAT is written; CFW (ARK POPS) doesn't need one.
"""
folder_name = re.sub(r"[^A-Za-z0-9]", "", game_id).upper() or "PS1GAME"
folder = Path(out_dir) / folder_name