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:
@@ -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.
|
`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
|
- `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).
|
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`):
|
- State dir (`~/.local/state/psp-preserve/` by default, or `$XDG_STATE_HOME/psp-preserve`):
|
||||||
- `library.sqlite` — the catalog + library database.
|
- `library.sqlite` — the catalog + library database.
|
||||||
- `covers/` — downloaded cover cache.
|
- `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.
|
- **Serial detection** — reads `SYSTEM.CNF` to derive the game ID (e.g.
|
||||||
`SLUS-00001`) and writes it into `PARAM.SFO` + the PSAR header.
|
`SLUS-00001`) and writes it into `PARAM.SFO` + the PSAR header.
|
||||||
- **base.pbp** — conversion needs a PSN PS1 classic `EBOOT.PBP` (any game) to
|
- **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
|
supply Sony's encrypted `DATA.PSP` loader (POPS emulator) and `STARTDAT`.
|
||||||
point **Settings → General → PS1 base.pbp** at it.
|
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 `<out>/<Game ID>/` containing a single `EBOOT.PBP`
|
- **Output** — a folder `<out>/<Game ID>/` 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>/PSP/GAME/<Game ID>/`. Compatible with CFW POPS
|
ready to copy to `<PSP>/PSP/GAME/<Game ID>/`. Compatible with CFW POPS
|
||||||
(ARK-4).
|
(ARK-4/ARK-5).
|
||||||
|
|
||||||
Configure the folders in **Settings → General** (or `PSP_PS1_ROOTS` /
|
Configure the folders in **Settings → General** (or `PSP_PS1_ROOTS` /
|
||||||
`PSP_PS1_OUTPUT_DIR`). Current limits: single-disc games only (no multi-disc
|
`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
|
## Store — archive.org and local folder
|
||||||
|
|
||||||
|
|||||||
+47
-4
@@ -375,6 +375,46 @@ def extract_icon0(base_pbp: Path) -> bytes:
|
|||||||
return read_pbp_section(base_pbp, header, idx)
|
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:
|
def patch_sfo(base_sfo: bytes, title: str, game_id: str) -> bytes:
|
||||||
"""Patch ``TITLE`` and ``DISC_ID`` in a base PARAM.SFO in place.
|
"""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:
|
def extract_startdat(base_pbp: Path) -> bytes:
|
||||||
"""Extract the trailing STARTDAT block (boot.png + PGD) from a base PBP.
|
"""Extract the trailing STARTDAT block (boot.png + PGD) from a base PBP.
|
||||||
|
|
||||||
The STARTDAT offset is discovered the same way the reference writer does:
|
STARTDAT sits immediately after the disc data: at ``PSAR + p1`` where
|
||||||
read the u32 at ``psar + 0x0C``, add ``0x50000``, and expect b"STARTDAT"
|
``p1`` is the u32 at ``PSAR + 0x0C``. (The original popstation computes
|
||||||
there. Everything from that marker to EOF is copied verbatim.
|
``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)
|
header = read_pbp_header(base_pbp)
|
||||||
psar_offset = header[SECTION_OFFSET["psar"] // 4] # header array index
|
psar_offset = header[SECTION_OFFSET["psar"] // 4] # header array index
|
||||||
with open(base_pbp, "rb") as fh:
|
with open(base_pbp, "rb") as fh:
|
||||||
fh.seek(psar_offset + 0x0C)
|
fh.seek(psar_offset + 0x0C)
|
||||||
(x,) = struct.unpack("<I", fh.read(4))
|
(x,) = struct.unpack("<I", fh.read(4))
|
||||||
marker_off = x + 0x50000
|
marker_off = psar_offset + x
|
||||||
fh.seek(marker_off)
|
fh.seek(marker_off)
|
||||||
if fh.read(8) != b"STARTDAT":
|
if fh.read(8) != b"STARTDAT":
|
||||||
raise ConversionError(f"cannot find STARTDAT in {base_pbp} (not a valid PS1 base PBP?)")
|
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.
|
# Resources required from the base PBP.
|
||||||
data_psp = extract_data_psp(base_pbp)
|
data_psp = extract_data_psp(base_pbp)
|
||||||
startdat = extract_startdat(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
|
# Reuse the base PBP's SFO, patched with our title + game id (the
|
||||||
# popstation approach), rather than rebuilding it from scratch.
|
# popstation approach), rather than rebuilding it from scratch.
|
||||||
|
|||||||
Reference in New Issue
Block a user