PS1: output PSN-style game folders (EBOOT.PBP + KEYS.BIN + DOCUMENT.DAT)
This commit is contained in:
@@ -190,8 +190,9 @@ 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
|
||||
supply Sony's encrypted `DATA.PSP` loader. Copy one to `data/base.PBP` and
|
||||
point **Settings → General → PS1 base.pbp** at it.
|
||||
- **Output** — `Title (Game ID).pbp` in the **PS1 output directory**. To run on
|
||||
a PSP, copy each one to `<PSP>/PSP/GAME/<Game ID>/EBOOT.PBP`.
|
||||
- **Output** — a PSN-style folder `<out>/<Game ID>/` containing `EBOOT.PBP`,
|
||||
`KEYS.BIN` and `DOCUMENT.DAT`, ready to copy to
|
||||
`<PSP>/PSP/GAME/<Game ID>/`.
|
||||
|
||||
Configure the folders in **Settings → General** (or `PSP_PS1_ROOTS` /
|
||||
`PSP_PS1_OUTPUT_DIR`). Current limits: single-disc games only (no multi-disc
|
||||
|
||||
+7
-7
@@ -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
@@ -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
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user