diff --git a/README.md b/README.md index 608f26f..23b95b9 100644 --- a/README.md +++ b/README.md @@ -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/GAME//EBOOT.PBP`. +- **Output** — a PSN-style folder `//` containing `EBOOT.PBP`, + `KEYS.BIN` and `DOCUMENT.DAT`, ready to copy to + `/PSP/GAME//`. Configure the folders in **Settings → General** (or `PSP_PS1_ROOTS` / `PSP_PS1_OUTPUT_DIR`). Current limits: single-disc games only (no multi-disc diff --git a/app/gui.py b/app/gui.py index 2058a4a..704ee26 100644 --- a/app/gui.py +++ b/app/gui.py @@ -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//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 "/\\") diff --git a/app/ps1_convert.py b/app/ps1_convert.py index fbc5f81..8ab58b6 100644 --- a/app/ps1_convert.py +++ b/app/ps1_convert.py @@ -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 ``//`` 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 //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}") diff --git a/app/transfers.py b/app/transfers.py index bcf458f..2a711c2 100644 --- a/app/transfers.py +++ b/app/transfers.py @@ -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):