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
|
- **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** — `Title (Game ID).pbp` in the **PS1 output directory**. To run on
|
- **Output** — a PSN-style folder `<out>/<Game ID>/` containing `EBOOT.PBP`,
|
||||||
a PSP, copy each one to `<PSP>/PSP/GAME/<Game ID>/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` /
|
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
|
||||||
|
|||||||
+7
-7
@@ -4124,13 +4124,13 @@ class MainWindow(QMainWindow):
|
|||||||
if system == "videos":
|
if system == "videos":
|
||||||
dest_dir = psp_root / "ISO" / "VIDEO"
|
dest_dir = psp_root / "ISO" / "VIDEO"
|
||||||
elif system == "ps1":
|
elif system == "ps1":
|
||||||
# PS1 EBOOTs install to /PSP/GAME/<folder>/EBOOT.PBP.
|
# PS1 EBOOTs install as a whole game folder under /PSP/GAME.
|
||||||
folder = item.get("serial") or item.get("name") or "PS1"
|
src_folder = Path(item["path"]).parent
|
||||||
folder = "".join(c for c in folder if c not in '/\\:*?"<>|').strip() or "PS1"
|
dest_dir = psp_root / "PSP" / "GAME"
|
||||||
dest_dir = psp_root / "PSP" / "GAME" / folder
|
log.info("copying ps1 game to psp: %s -> %s", src_folder, dest_dir / src_folder.name)
|
||||||
log.info("copying ps1 eboot to psp: %s -> %s", item["path"], dest_dir / "EBOOT.PBP")
|
self.queue.enqueue(transfers.TreeCopySource(str(src_folder)), dest_dir,
|
||||||
self.queue.enqueue(transfers.FileCopySource(item["path"]), dest_dir,
|
{"dest_name": src_folder.name}, item.get("name") or src_folder.name,
|
||||||
{"dest_name": "EBOOT.PBP"}, item.get("name") or folder, kind="psp-install")
|
kind="psp-install")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
cat = "".join(c for c in (self._iso_category or "").strip() if c not in "/\\")
|
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
|
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
|
# 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("--title", help="game title shown on the PSP")
|
||||||
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")
|
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")
|
ap.add_argument("--test", action="store_true", help="run self-test and exit")
|
||||||
args = ap.parse_args(argv)
|
args = ap.parse_args(argv)
|
||||||
|
|
||||||
@@ -837,18 +881,22 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|||||||
_self_test()
|
_self_test()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
if not all([args.source, args.title, args.game_id, args.base_pbp, args.out]):
|
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 are all required (or use --test)")
|
ap.error("--source --title --game-id --base-pbp and --out (or --out-dir) are all required (or use --test)")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
convert(
|
if args.out_dir:
|
||||||
Path(args.source),
|
folder = convert_to_folder(
|
||||||
args.title,
|
Path(args.source), args.title, args.game_id,
|
||||||
args.game_id,
|
Path(args.base_pbp), Path(args.out_dir),
|
||||||
Path(args.base_pbp),
|
)
|
||||||
Path(args.out),
|
print(f"wrote {folder}")
|
||||||
)
|
else:
|
||||||
print(f"wrote {args.out}")
|
convert(
|
||||||
|
Path(args.source), args.title, args.game_id,
|
||||||
|
Path(args.base_pbp), Path(args.out),
|
||||||
|
)
|
||||||
|
print(f"wrote {args.out}")
|
||||||
return 0
|
return 0
|
||||||
except ConversionError as exc:
|
except ConversionError as exc:
|
||||||
print(f"error: {exc}")
|
print(f"error: {exc}")
|
||||||
|
|||||||
+8
-7
@@ -227,10 +227,11 @@ class Ps1ConvertSource(Source):
|
|||||||
if not game_id:
|
if not game_id:
|
||||||
raise RuntimeError("missing game_id for PS1 conversion")
|
raise RuntimeError("missing game_id for PS1 conversion")
|
||||||
|
|
||||||
safe = re.sub(r'[<>:"/\\|?*]', " ", title).strip() or "PS1 game"
|
folder_name = re.sub(r"[^A-Za-z0-9]", "", game_id).upper() or "PS1GAME"
|
||||||
out_path = dest_dir / f"{safe} ({game_id}).pbp"
|
folder = dest_dir / folder_name
|
||||||
if out_path.exists():
|
eboot = folder / "EBOOT.PBP"
|
||||||
raise FileExistsError(str(out_path))
|
if eboot.exists():
|
||||||
|
raise FileExistsError(str(eboot))
|
||||||
|
|
||||||
def cb(done, total):
|
def cb(done, total):
|
||||||
if progress_cb is not None and total:
|
if progress_cb is not None and total:
|
||||||
@@ -239,13 +240,13 @@ class Ps1ConvertSource(Source):
|
|||||||
from . import ps1_convert
|
from . import ps1_convert
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ps1_convert.convert(
|
ps1_convert.convert_to_folder(
|
||||||
source, title, game_id, self.base_pbp, out_path,
|
source, title, game_id, self.base_pbp, dest_dir,
|
||||||
progress_cb=cb, cancel_event=cancel_event,
|
progress_cb=cb, cancel_event=cancel_event,
|
||||||
)
|
)
|
||||||
except ps1_convert.Cancelled:
|
except ps1_convert.Cancelled:
|
||||||
raise TransferCancelled() from None
|
raise TransferCancelled() from None
|
||||||
return out_path
|
return folder
|
||||||
|
|
||||||
|
|
||||||
class TreeCopySource(Source):
|
class TreeCopySource(Source):
|
||||||
|
|||||||
Reference in New Issue
Block a user