diff --git a/README.md b/README.md index 50b6848..2642c53 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ Discs are matched by hash first (most precise), then serial, then filename. `openvgdb-covers.zip` offline cover bundles, and the legacy cover cache. - `data/base.PBP` — a PSN PS1 classic EBOOT (any game) used as the encrypted loader when converting PS1 discs to `EBOOT.PBP` (git-ignored, copyrighted). + Prefer a 6.60/6.61-era classic; 3.xx-era loaders can fail on modern CFW. - State dir (`~/.local/state/psp-preserve/` by default, or `$XDG_STATE_HOME/psp-preserve`): - `library.sqlite` — the catalog + library database. - `covers/` — downloaded cover cache. @@ -188,16 +189,20 @@ PS1 discs (pending) and converted EBOOTs also show up in the **Library** tab - **Serial detection** — reads `SYSTEM.CNF` to derive the game ID (e.g. `SLUS-00001`) and writes it into `PARAM.SFO` + the PSAR header. - **base.pbp** — conversion needs a PSN PS1 classic `EBOOT.PBP` (any game) to - supply Sony's encrypted `DATA.PSP` loader. Copy one to `data/base.PBP` and - point **Settings → General → PS1 base.pbp** at it. + supply Sony's encrypted `DATA.PSP` loader (POPS emulator) and `STARTDAT`. + Copy one to `data/base.PBP` and point **Settings → General → PS1 base.pbp** + at it. **Prefer a 6.60/6.61-era classic**: very old 3.xx-era classics ship a + POPS loader that fails to link on modern CFW (`80020148` "game could not be + started"). Any PS1 classic works structurally — the converter finds + `STARTDAT` at `PSAR + p1` regardless of PSAR alignment. - **Output** — a folder `//` containing a single `EBOOT.PBP` - (the popstation way: uncompressed level-0 PSAR, no `KEYS.BIN`/`DOCUMENT.DAT`), + (the popstation way: raw-DEFLATE-compressed PSAR, no `KEYS.BIN`/`DOCUMENT.DAT`), ready to copy to `/PSP/GAME//`. Compatible with CFW POPS - (ARK-4). + (ARK-4/ARK-5). Configure the folders in **Settings → General** (or `PSP_PS1_ROOTS` / `PSP_PS1_OUTPUT_DIR`). Current limits: single-disc games only (no multi-disc -PBP), and images are stored uncompressed (LZRC compression is on the roadmap). +PBP); disc data is DEFLATE-compressed (level 1 by default, `--complevel 0-9`). ## Store — archive.org and local folder diff --git a/app/ps1_convert.py b/app/ps1_convert.py index e001cd2..fe05fb4 100644 --- a/app/ps1_convert.py +++ b/app/ps1_convert.py @@ -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(" 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("