ps1: general STARTDAT lookup + warn on old base.pbp loader

- extract_startdat: locate STARTDAT at PSAR + p1 (the u32 at PSAR+0x0C)
  instead of popstation's p1 + 0x50000 absolute offset, which only worked
  for bases whose PSAR was aligned to 0x50000. Any PS1 classic can now be
  used as a base.
- _warn_old_base: print a warning when the base PBP is a 3.xx-era classic,
  since its POPS loader often fails to link on 6.60/6.61 CFW (error
  80020148 'game could not be started').
- README: document the 6.60/6.61 base preference and DEFLATE compression.
This commit is contained in:
2026-09-07 17:36:16 -04:00
parent e50cd02f2a
commit ea08f52d44
2 changed files with 57 additions and 9 deletions
+47 -4
View File
@@ -375,6 +375,46 @@ def extract_icon0(base_pbp: Path) -> bytes:
return read_pbp_section(base_pbp, header, idx)
def _sfo_entry(base_sfo: bytes, key: str) -> Optional[bytes]:
"""Return the raw data of an SFO entry, or ``None`` if absent."""
if len(base_sfo) < 20 or base_sfo[:4] != b"\x00PSF":
return None
_, _, key_table_off, data_table_off, nitems = struct.unpack("<IIIII", base_sfo[:20])
for i in range(nitems):
entry_off = 20 + i * 16
koff, _, _, max_len, doff = struct.unpack("<HHIII", base_sfo[entry_off:entry_off + 16])
kend = base_sfo.find(b"\x00", key_table_off + koff)
k = bytes(base_sfo[key_table_off + koff:kend]).decode("ascii", "replace")
if k == key:
return base_sfo[data_table_off + doff: data_table_off + doff + max_len]
return None
def _warn_old_base(base_pbp: Path) -> None:
"""Warn when the base PBP carries a very old POPS loader.
PS1 classics from the 3.xx firmware era ship a POPS loader that often
fails to link on 6.60/6.61 CFW (the PSP reports error 80020148), so
newer base PBPs are strongly preferred.
"""
try:
raw = _sfo_entry(extract_sfo(base_pbp), "PSP_SYSTEM_VER")
if not raw:
return
version = raw.rstrip(b"\x00").decode("ascii", "replace")
major = version.split(".")[0]
if major.isdigit() and int(major) < 6:
import sys
print(
f"warning: base PBP is a {version}-era PS1 classic; its POPS loader "
"may fail to load on 6.60/6.61 CFW (error 80020148). "
"Prefer a newer base.pbp.",
file=sys.stderr,
)
except Exception:
pass
def patch_sfo(base_sfo: bytes, title: str, game_id: str) -> bytes:
"""Patch ``TITLE`` and ``DISC_ID`` in a base PARAM.SFO in place.
@@ -481,16 +521,18 @@ def extract_data_psp(base_pbp: Path) -> bytes:
def extract_startdat(base_pbp: Path) -> bytes:
"""Extract the trailing STARTDAT block (boot.png + PGD) from a base PBP.
The STARTDAT offset is discovered the same way the reference writer does:
read the u32 at ``psar + 0x0C``, add ``0x50000``, and expect b"STARTDAT"
there. Everything from that marker to EOF is copied verbatim.
STARTDAT sits immediately after the disc data: at ``PSAR + p1`` where
``p1`` is the u32 at ``PSAR + 0x0C``. (The original popstation computes
``p1 + 0x50000`` as an absolute offset, which only works for bases whose
PSAR happens to be aligned to 0x50000; using ``PSAR + p1`` works for any
PS1 classic.)
"""
header = read_pbp_header(base_pbp)
psar_offset = header[SECTION_OFFSET["psar"] // 4] # header array index
with open(base_pbp, "rb") as fh:
fh.seek(psar_offset + 0x0C)
(x,) = struct.unpack("<I", fh.read(4))
marker_off = x + 0x50000
marker_off = psar_offset + x
fh.seek(marker_off)
if fh.read(8) != b"STARTDAT":
raise ConversionError(f"cannot find STARTDAT in {base_pbp} (not a valid PS1 base PBP?)")
@@ -825,6 +867,7 @@ def convert(
# Resources required from the base PBP.
data_psp = extract_data_psp(base_pbp)
startdat = extract_startdat(base_pbp)
_warn_old_base(base_pbp)
# Reuse the base PBP's SFO, patched with our title + game id (the
# popstation approach), rather than rebuilding it from scratch.