PS1: output PSN-style game folders (EBOOT.PBP + KEYS.BIN + DOCUMENT.DAT)

This commit is contained in:
2026-09-07 16:20:39 -04:00
parent 4d5c48cb94
commit a9c371c221
4 changed files with 77 additions and 27 deletions
+7 -7
View File
@@ -4124,13 +4124,13 @@ class MainWindow(QMainWindow):
if system == "videos":
dest_dir = psp_root / "ISO" / "VIDEO"
elif system == "ps1":
# PS1 EBOOTs install to /PSP/GAME/<folder>/EBOOT.PBP.
folder = item.get("serial") or item.get("name") or "PS1"
folder = "".join(c for c in folder if c not in '/\\:*?"<>|').strip() or "PS1"
dest_dir = psp_root / "PSP" / "GAME" / folder
log.info("copying ps1 eboot to psp: %s -> %s", item["path"], dest_dir / "EBOOT.PBP")
self.queue.enqueue(transfers.FileCopySource(item["path"]), dest_dir,
{"dest_name": "EBOOT.PBP"}, item.get("name") or folder, kind="psp-install")
# PS1 EBOOTs install as a whole game folder under /PSP/GAME.
src_folder = Path(item["path"]).parent
dest_dir = psp_root / "PSP" / "GAME"
log.info("copying ps1 game to psp: %s -> %s", src_folder, dest_dir / src_folder.name)
self.queue.enqueue(transfers.TreeCopySource(str(src_folder)), dest_dir,
{"dest_name": src_folder.name}, item.get("name") or src_folder.name,
kind="psp-install")
return True
else:
cat = "".join(c for c in (self._iso_category or "").strip() if c not in "/\\")
+59 -11
View File
@@ -744,6 +744,49 @@ def convert(
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(
source: Path,
title: str,
game_id: str,
base_pbp: Path,
out_dir: Path,
progress_cb=None,
cancel_event=None,
) -> Path:
"""Convert a PS1 disc into a PSN-style game folder ready for /PSP/GAME.
Writes ``<out_dir>/<GAME_ID>/`` containing ``EBOOT.PBP``, ``KEYS.BIN`` and
``DOCUMENT.DAT``. The PSAR is stored uncompressed/unencrypted (matching the
popstation/PSXPackager level-0 path), so KEYS.BIN is a zero placeholder that
keeps the folder structure identical to a real PSN classic.
"""
folder_name = re.sub(r"[^A-Za-z0-9]", "", game_id).upper() or "PS1GAME"
folder = Path(out_dir) / folder_name
folder.mkdir(parents=True, exist_ok=True)
convert(source, title, game_id, base_pbp, folder / "EBOOT.PBP",
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
# --------------------------------------------------------------------------
# Self-test / CLI
# --------------------------------------------------------------------------
@@ -829,7 +872,8 @@ def main(argv: Optional[List[str]] = None) -> int:
ap.add_argument("--title", help="game title shown on the PSP")
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("--out", help="output EBOOT.PBP path")
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("--test", action="store_true", help="run self-test and exit")
args = ap.parse_args(argv)
@@ -837,18 +881,22 @@ def main(argv: Optional[List[str]] = None) -> int:
_self_test()
return 0
if not all([args.source, args.title, args.game_id, args.base_pbp, args.out]):
ap.error("--source --title --game-id --base-pbp and --out are all required (or use --test)")
if not all([args.source, args.title, args.game_id, args.base_pbp]) or (not args.out and not args.out_dir):
ap.error("--source --title --game-id --base-pbp and --out (or --out-dir) are all required (or use --test)")
try:
convert(
Path(args.source),
args.title,
args.game_id,
Path(args.base_pbp),
Path(args.out),
)
print(f"wrote {args.out}")
if args.out_dir:
folder = convert_to_folder(
Path(args.source), args.title, args.game_id,
Path(args.base_pbp), Path(args.out_dir),
)
print(f"wrote {folder}")
else:
convert(
Path(args.source), args.title, args.game_id,
Path(args.base_pbp), Path(args.out),
)
print(f"wrote {args.out}")
return 0
except ConversionError as exc:
print(f"error: {exc}")
+8 -7
View File
@@ -227,10 +227,11 @@ class Ps1ConvertSource(Source):
if not game_id:
raise RuntimeError("missing game_id for PS1 conversion")
safe = re.sub(r'[<>:"/\\|?*]', " ", title).strip() or "PS1 game"
out_path = dest_dir / f"{safe} ({game_id}).pbp"
if out_path.exists():
raise FileExistsError(str(out_path))
folder_name = re.sub(r"[^A-Za-z0-9]", "", game_id).upper() or "PS1GAME"
folder = dest_dir / folder_name
eboot = folder / "EBOOT.PBP"
if eboot.exists():
raise FileExistsError(str(eboot))
def cb(done, total):
if progress_cb is not None and total:
@@ -239,13 +240,13 @@ class Ps1ConvertSource(Source):
from . import ps1_convert
try:
ps1_convert.convert(
source, title, game_id, self.base_pbp, out_path,
ps1_convert.convert_to_folder(
source, title, game_id, self.base_pbp, dest_dir,
progress_cb=cb, cancel_event=cancel_event,
)
except ps1_convert.Cancelled:
raise TransferCancelled() from None
return out_path
return folder
class TreeCopySource(Source):