PS1: match popstation PSAR header byte-for-byte; drop fake KEYS.BIN/DOCUMENT.DAT

This commit is contained in:
2026-09-07 16:20:39 -04:00
parent a9c371c221
commit 02b4adb1c1
2 changed files with 30 additions and 37 deletions
+4 -3
View File
@@ -190,9 +190,10 @@ PS1 discs (pending) and converted EBOOTs also show up in the **Library** tab
- **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. Copy one to `data/base.PBP` and
point **Settings → General → PS1 base.pbp** at it. point **Settings → General → PS1 base.pbp** at it.
- **Output** — a PSN-style folder `<out>/<Game ID>/` containing `EBOOT.PBP`, - **Output** — a folder `<out>/<Game ID>/` containing a single `EBOOT.PBP`
`KEYS.BIN` and `DOCUMENT.DAT`, ready to copy to (the popstation way: uncompressed level-0 PSAR, no `KEYS.BIN`/`DOCUMENT.DAT`),
`<PSP>/PSP/GAME/<Game ID>/`. ready to copy to `<PSP>/PSP/GAME/<Game ID>/`. Compatible with CFW POPS
(ARK-4).
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
+26 -34
View File
@@ -507,25 +507,35 @@ def build_toc(disc: "DiscImage") -> bytes:
def _build_psar_header(disc: "DiscImage", game_id: str, title: str) -> bytes: def _build_psar_header(disc: "DiscImage", game_id: str, title: str) -> bytes:
"""Build the fixed PSAR header region 0x400..0x4000. """Build the fixed PSAR header region 0x400..0x4000, byte-identical to the
popstation level-0 writer.
Layout mirrors the reference writer's data1/data2 templates: Layout (offsets relative to the PSAR start):
* game id at +0x000 (11 bytes, "_SLUS_00594" form) * 0x400 data1 (0xE20 bytes): game id at +0, TOC at +0x400, 0x10 at +0x7FE
* TOC at +0x400 (i.e. absolute PSAR +0x800) * 0x1220 p2 offset (u32) = padded ISO size + 0x100000 + 0x2D31
* title at data2 offset 8 (absolute PSAR +0x1228), 128 bytes max * 0x1224 data2 (0x2DDC bytes): FF 07 at +4, title at +8, 0x03 at +0x88
""" """
block = bytearray(PSAR_INDEX_OFFSET - PSAR_GAMEID_OFFSET) # 0x400..0x4000 data1_size = 0xE20
data2_size = 0x2DDC
p2_delta = 0x2D31
block = bytearray(data1_size + 4 + data2_size) # 0x3C00 = 0x400..0x4000
gid = _format_psar_game_id(game_id).encode("ascii")[:11] gid = _format_psar_game_id(game_id).encode("ascii")[:11]
block[: len(gid)] = gid block[: len(gid)] = gid
toc = build_toc(disc) toc = build_toc(disc)
toc_off = PSAR_TOC_OFFSET - PSAR_GAMEID_OFFSET # 0x400 block[0x400: 0x400 + len(toc)] = toc
block[toc_off: toc_off + len(toc)] = toc block[0x7FE] = 0x10
p2 = disc.padded_size() + PSAR_ISO_OFFSET + p2_delta
block[data1_size: data1_size + 4] = struct.pack("<I", p2)
data2 = data1_size + 4
block[data2 + 4: data2 + 6] = b"\xFF\x07"
title_bytes = title.encode("ascii", "replace")[:0x80] title_bytes = title.encode("ascii", "replace")[:0x80]
title_off = 0x1220 - PSAR_GAMEID_OFFSET + 8 # data2 template offset 8 block[data2 + 8: data2 + 8 + len(title_bytes)] = title_bytes
block[title_off: title_off + len(title_bytes)] = title_bytes block[data2 + 0x88] = 0x03
return bytes(block) return bytes(block)
@@ -744,17 +754,6 @@ def convert(
return out_path return out_path
KEYS_BIN_SIZE = 0x10 # 16-byte boot key
def _document_dat() -> bytes:
"""Minimal placeholder manual (DOCUMENT.DAT).
Blank is safe: POPS simply shows no manual when the file is empty.
"""
return b""
def convert_to_folder( def convert_to_folder(
source: Path, source: Path,
title: str, title: str,
@@ -764,12 +763,12 @@ def convert_to_folder(
progress_cb=None, progress_cb=None,
cancel_event=None, cancel_event=None,
) -> Path: ) -> Path:
"""Convert a PS1 disc into a PSN-style game folder ready for /PSP/GAME. """Convert a PS1 disc into a game folder ready for /PSP/GAME.
Writes ``<out_dir>/<GAME_ID>/`` containing ``EBOOT.PBP``, ``KEYS.BIN`` and Writes ``<out_dir>/<GAME_ID>/EBOOT.PBP`` — the same single-file output the
``DOCUMENT.DAT``. The PSAR is stored uncompressed/unencrypted (matching the popstation tool produces (uncompressed/unencrypted level-0 PSAR). No
popstation/PSXPackager level-0 path), so KEYS.BIN is a zero placeholder that KEYS.BIN or DOCUMENT.DAT is written: popstation doesn't emit them and CFW
keeps the folder structure identical to a real PSN classic. (ARK-4 POPS) doesn't need them.
""" """
folder_name = re.sub(r"[^A-Za-z0-9]", "", game_id).upper() or "PS1GAME" folder_name = re.sub(r"[^A-Za-z0-9]", "", game_id).upper() or "PS1GAME"
folder = Path(out_dir) / folder_name folder = Path(out_dir) / folder_name
@@ -777,13 +776,6 @@ def convert_to_folder(
convert(source, title, game_id, base_pbp, folder / "EBOOT.PBP", convert(source, title, game_id, base_pbp, folder / "EBOOT.PBP",
progress_cb=progress_cb, cancel_event=cancel_event) progress_cb=progress_cb, cancel_event=cancel_event)
keys_path = folder / "KEYS.BIN"
if not keys_path.exists():
keys_path.write_bytes(b"\x00" * KEYS_BIN_SIZE)
doc_path = folder / "DOCUMENT.DAT"
if not doc_path.exists():
doc_path.write_bytes(_document_dat())
return folder return folder
@@ -873,7 +865,7 @@ def main(argv: Optional[List[str]] = None) -> int:
ap.add_argument("--game-id", help="PS1 game id, e.g. SLUS00594") ap.add_argument("--game-id", help="PS1 game id, e.g. SLUS00594")
ap.add_argument("--base-pbp", help="path to a PSN PS1 classic EBOOT.PBP (required)") ap.add_argument("--base-pbp", help="path to a PSN PS1 classic EBOOT.PBP (required)")
ap.add_argument("--out", help="output EBOOT.PBP path (single file)") ap.add_argument("--out", help="output EBOOT.PBP path (single file)")
ap.add_argument("--out-dir", help="output folder: writes <dir>/<GAME_ID>/EBOOT.PBP (+KEYS.BIN, DOCUMENT.DAT)") ap.add_argument("--out-dir", help="output folder: writes <dir>/<GAME_ID>/EBOOT.PBP")
ap.add_argument("--test", action="store_true", help="run self-test and exit") ap.add_argument("--test", action="store_true", help="run self-test and exit")
args = ap.parse_args(argv) args = ap.parse_args(argv)