From 9fc954e5e51552fb8f486a89851b65013112677b Mon Sep 17 00:00:00 2001 From: phantomptr Date: Fri, 16 Oct 2026 10:58:12 -0700 Subject: [PATCH] Convert: make a game folder from an image or debug package (#469) Engine: POST /api/folder/build unpacks a game image (or, once the pkg reader lands, a debug-key .pkg) into /. It runs as a job with Write and Verify stages; every path is checked before anything is written (.., absolute, empty components, names this OS cannot hold, file/folder clashes, case clashes on a case-insensitive drive); data streams in 8 MiB chunks with cancel per chunk; the folder is written as .partial/ (marked so the output-folder sweep can tell it from a user's own) and renamed only after the read-back: every size and empty folder, and the SHA-256 of a seeded sample (or every file) re-read from a freshly opened source. An existing target, a plain folder source and a shortfall of free space (files + 64 MiB) are refused before the job starts. Client: a third "What to make" tile, A game folder. A folder source disables Make with "This is already a folder"; .pkg is accepted as a source; then is keep only; the finished card shows the folder and Show in folder. The queue, prefs, pipeline and stage list know the folder kind. Strings in all 21 locales. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS --- client/src-tauri/src/commands/ps5_engine.rs | 22 + client/src-tauri/src/lib.rs | 1 + client/src/api/fpkg.ts | 6 + client/src/i18n/locales/ar.ts | 13 + client/src/i18n/locales/bn.ts | 13 + client/src/i18n/locales/de.ts | 13 + client/src/i18n/locales/en.ts | 13 + client/src/i18n/locales/es.ts | 13 + client/src/i18n/locales/fa.ts | 13 + client/src/i18n/locales/fr.ts | 13 + client/src/i18n/locales/hi.ts | 13 + client/src/i18n/locales/hu.ts | 13 + client/src/i18n/locales/id.ts | 13 + client/src/i18n/locales/it.ts | 13 + client/src/i18n/locales/ja.ts | 13 + client/src/i18n/locales/ko.ts | 13 + client/src/i18n/locales/pl.ts | 13 + client/src/i18n/locales/pt-BR.ts | 13 + client/src/i18n/locales/ru.ts | 13 + client/src/i18n/locales/th.ts | 13 + client/src/i18n/locales/tr.ts | 13 + client/src/i18n/locales/vi.ts | 13 + client/src/i18n/locales/zh-CN.ts | 13 + client/src/i18n/locales/zh-TW.ts | 13 + client/src/lib/browserInvoke.ts | 8 + client/src/lib/convertQueueRunner.ts | 9 + client/src/screens/FpkgConvert/GameCard.tsx | 2 +- client/src/screens/FpkgConvert/MakeCard.tsx | 30 +- .../src/screens/FpkgConvert/OptionsCard.tsx | 13 +- client/src/screens/FpkgConvert/QueueCard.tsx | 2 +- .../src/screens/FpkgConvert/RunCard.test.tsx | 56 ++ client/src/screens/FpkgConvert/RunCard.tsx | 64 +- client/src/screens/FpkgConvert/cards.test.tsx | 40 + client/src/screens/FpkgConvert/index.tsx | 75 +- client/src/screens/FpkgConvert/stages.test.ts | 14 + client/src/screens/FpkgConvert/stages.ts | 4 + client/src/state/convertPrefs.test.ts | 10 + client/src/state/convertPrefs.ts | 12 +- client/src/state/convertQueue.test.ts | 15 +- client/src/state/convertQueue.ts | 18 +- client/src/state/fpkgConversion.test.ts | 24 + client/src/state/fpkgConversion.ts | 42 +- .../ps5upload-engine/src/folder_build.rs | 743 ++++++++++++++++++ .../crates/ps5upload-engine/src/fpkg_api.rs | 269 ++++++- .../ps5upload-engine/src/image_build.rs | 25 +- engine/crates/ps5upload-engine/src/lib.rs | 2 + 46 files changed, 1695 insertions(+), 84 deletions(-) create mode 100644 engine/crates/ps5upload-engine/src/folder_build.rs diff --git a/client/src-tauri/src/commands/ps5_engine.rs b/client/src-tauri/src/commands/ps5_engine.rs index f7fd0b46..bb92c89a 100644 --- a/client/src-tauri/src/commands/ps5_engine.rs +++ b/client/src-tauri/src/commands/ps5_engine.rs @@ -2409,6 +2409,28 @@ pub async fn exfat_build( .await } +/// Unpack a game image or a debug-key package into a plain folder; runs as an engine job. +#[tauri::command] +pub async fn folder_build( + source: String, + output_dir: Option, + name: Option, + full_verify: Option, +) -> Result { + let base = engine::url(); + let url = format!("{base}/api/folder/build"); + post_json( + &url, + &serde_json::json!({ + "source": source, + "output_dir": output_dir, + "name": name, + "full_verify": full_verify.unwrap_or(false), + }), + ) + .await +} + #[tauri::command] pub async fn job_status(job_id: String) -> Result { let base = engine::url(); diff --git a/client/src-tauri/src/lib.rs b/client/src-tauri/src/lib.rs index 51edf89b..d6d4672d 100644 --- a/client/src-tauri/src/lib.rs +++ b/client/src-tauri/src/lib.rs @@ -332,6 +332,7 @@ pub fn run() { commands::ps5_elfldr_ensure, commands::ffpfsc_compress, commands::exfat_build, + commands::folder_build, commands::fpkg_extract, commands::fpkg_extract_cleanup, commands::job_status, diff --git a/client/src/api/fpkg.ts b/client/src/api/fpkg.ts index 25b73f63..920e90e8 100644 --- a/client/src/api/fpkg.ts +++ b/client/src/api/fpkg.ts @@ -152,4 +152,10 @@ export const fpkg = { verifyChunkKib, fullVerify, }), + /** Unpack a game image (.exfat, .ffpkg, .ffpfs, .ffpfsc) or a package made with the debug + * key into a plain folder in the output folder, named after the source (or `name`). One + * job: written, read back (sizes, and SHA-256 of a sample or of every file), then given its + * name; the job's `dest` is the folder. */ + buildFolder: (source: string, outputDir?: string, name?: string, fullVerify = false) => + invoke<{ job_id: string }>("folder_build", { source, outputDir, name, fullVerify }), }; diff --git a/client/src/i18n/locales/ar.ts b/client/src/i18n/locales/ar.ts index 9ef1ca5c..f25ed28f 100644 --- a/client/src/i18n/locales/ar.ts +++ b/client/src/i18n/locales/ar.ts @@ -3987,6 +3987,19 @@ c2c_test_deep_bad: "لم يصل الإرسال التجريبي بحجم 4 KiB: "joberr.ava1_taken_over": "تولّى جهاز آخر هذا الإرسال؛ وهو يستمر هناك.", "joberr.ava1_shared_jobs_unsupported": "لا يزال جهاز آخر يشغّل هذا الإرسال. يتطلب توليه (أو مشاهدته) أثناء التشغيل المساعد الأحدث على كلا الجهازين: حدّثهما، أو أوقف الإرسال مؤقتًا على ذلك الجهاز أولًا.", "joberr.ava1_cannot_watch": "يمر هذا الإرسال عبر الجهاز الآخر، لذا يمكن توليه لكن لا يمكن مشاهدته.", +"fpkg.make.folder_title": "مجلد لعبة", +"fpkg.make.folder_body": "ملفات اللعبة كمجلد عادي على هذا الكمبيوتر، من صورة لعبة أو من حزمة أُنشئت بمفتاح التصحيح. عدّله أو احتفظ به أو ارفعه مثل أي مجلد.", +"fpkg.folder.what": "يُسمّى المجلد باسم الصورة أو الحزمة، داخل مجلد الإخراج. لا يُضغط شيء، ولا يصل شيء إلى PS5.", +"cq_ext_folder": "مجلد", +"fpkg.stage.write_folder": "كتابة المجلد", +"fpkg.stage.verify_folder": "إعادة قراءة المجلد", +"fpkg.folder.make": "إنشاء مجلد اللعبة", +"fpkg.folder.already": "هذا مجلد بالفعل", +"fpkg.folder.sourceOnly": "لا يمكن تحويل سوى صورة لعبة أو حزمة أُنشئت بمفتاح التصحيح إلى مجلد؛ فكّ ضغط الأرشيف أولًا.", +"fpkg.folder.done": "تمت كتابة مجلد اللعبة وإعادة قراءته.", +"cq_then_keep_folder": "الاحتفاظ بالمجلد", +"cq_folder_sources_only": "لا يمكن تحويل سوى صورة لعبة أو حزمة أُنشئت بمفتاح التصحيح إلى مجلد: تم تخطي {count}.", +"fpkg.run.what_folder": "مجلد لعبة", }; export default ar; diff --git a/client/src/i18n/locales/bn.ts b/client/src/i18n/locales/bn.ts index cbc46c5b..279497ec 100644 --- a/client/src/i18n/locales/bn.ts +++ b/client/src/i18n/locales/bn.ts @@ -3987,6 +3987,19 @@ c2c_test_deep_bad: "4 KiB-এর পরীক্ষামূলক পাঠা "joberr.ava1_taken_over": "অন্য একটি ডিভাইস এই পাঠানোর দায়িত্ব নিয়েছে; এটি সেখানে চলবে।", "joberr.ava1_shared_jobs_unsupported": "অন্য একটি ডিভাইস এখনও এই পাঠানো চালাচ্ছে। চলাকালীন এর দায়িত্ব নিতে (বা দেখতে) দুই কনসোলেই নতুন হেল্পার লাগবে: সেগুলো আপডেট করুন, বা আগে সেই ডিভাইসে পাঠানো বিরতি দিন।", "joberr.ava1_cannot_watch": "এই পাঠানো অন্য ডিভাইসের মাধ্যমে যায়, তাই দায়িত্ব নেওয়া যায় কিন্তু দেখা যায় না।", +"fpkg.make.folder_title": "একটি গেম ফোল্ডার", +"fpkg.make.folder_body": "গেমের ফাইলগুলো এই কম্পিউটারে একটি সাধারণ ফোল্ডার হিসেবে, একটি গেম ইমেজ বা ডিবাগ কী দিয়ে তৈরি প্যাকেজ থেকে। যেকোনো ফোল্ডারের মতো এটি সম্পাদনা করুন, রাখুন বা আপলোড করুন।", +"fpkg.folder.what": "ফোল্ডারটির নাম ইমেজ বা প্যাকেজের নামে হয়, আউটপুট ফোল্ডারে। কিছুই সংকুচিত হয় না, আর PS5-এ কিছুই পাঠানো হয় না।", +"cq_ext_folder": "ফোল্ডার", +"fpkg.stage.write_folder": "ফোল্ডার লিখুন", +"fpkg.stage.verify_folder": "ফোল্ডার আবার পড়ুন", +"fpkg.folder.make": "গেম ফোল্ডার বানান", +"fpkg.folder.already": "এটি আগে থেকেই একটি ফোল্ডার", +"fpkg.folder.sourceOnly": "শুধু গেম ইমেজ বা ডিবাগ কী দিয়ে তৈরি প্যাকেজই ফোল্ডার হতে পারে; আগে আর্কাইভটি আনপ্যাক করুন।", +"fpkg.folder.done": "গেম ফোল্ডার লেখা ও আবার পড়ে যাচাই করা হয়েছে।", +"cq_then_keep_folder": "ফোল্ডার রাখুন", +"cq_folder_sources_only": "শুধু গেম ইমেজ বা ডিবাগ কী দিয়ে তৈরি প্যাকেজই ফোল্ডার হতে পারে: {count}টি বাদ দেওয়া হয়েছে।", +"fpkg.run.what_folder": "একটি গেম ফোল্ডার", }; export default bn; diff --git a/client/src/i18n/locales/de.ts b/client/src/i18n/locales/de.ts index 783266bb..e3d5b58f 100644 --- a/client/src/i18n/locales/de.ts +++ b/client/src/i18n/locales/de.ts @@ -3995,6 +3995,19 @@ c2c_test_deep_bad: "Die 4-KiB-Testsendung ist nicht angekommen: {detail}", "joberr.ava1_taken_over": "Ein anderes Gerät hat diese Sendung übernommen; sie läuft dort weiter.", "joberr.ava1_shared_jobs_unsupported": "Ein anderes Gerät führt diese Sendung noch aus. Sie während des Laufs zu übernehmen (oder anzusehen) erfordert den neueren Helfer auf beiden Konsolen: Aktualisiere sie oder pausiere die Sendung zuerst auf jenem Gerät.", "joberr.ava1_cannot_watch": "Diese Sendung läuft über das andere Gerät, daher kann sie übernommen, aber nicht angesehen werden.", +"fpkg.make.folder_title": "Ein Spielordner", +"fpkg.make.folder_body": "Die Dateien des Spiels als normaler Ordner auf diesem Computer, aus einem Spiel-Image oder einem mit dem Debug-Schlüssel erstellten Paket. Bearbeite ihn, behalte ihn oder lade ihn wie jeden Ordner hoch.", +"fpkg.folder.what": "Der Ordner wird nach dem Image oder Paket benannt und landet im Ausgabeordner. Nichts wird komprimiert, und nichts gelangt zur PS5.", +"cq_ext_folder": "Ordner", +"fpkg.stage.write_folder": "Ordner schreiben", +"fpkg.stage.verify_folder": "Ordner zurücklesen", +"fpkg.folder.make": "Spielordner erstellen", +"fpkg.folder.already": "Das ist bereits ein Ordner", +"fpkg.folder.sourceOnly": "Nur ein Spiel-Image oder ein mit dem Debug-Schlüssel erstelltes Paket kann zu einem Ordner werden; entpacke zuerst ein Archiv.", +"fpkg.folder.done": "Spielordner geschrieben und zurückgelesen.", +"cq_then_keep_folder": "Ordner behalten", +"cq_folder_sources_only": "Nur ein Spiel-Image oder ein mit dem Debug-Schlüssel erstelltes Paket kann zu einem Ordner werden: {count} übersprungen.", +"fpkg.run.what_folder": "einen Spielordner", }; export default de; diff --git a/client/src/i18n/locales/en.ts b/client/src/i18n/locales/en.ts index 4f1bd268..ec30ec58 100644 --- a/client/src/i18n/locales/en.ts +++ b/client/src/i18n/locales/en.ts @@ -4310,6 +4310,19 @@ c2c_test_deep_bad: "The 4 KiB test send did not arrive: {detail}", "joberr.ava1_taken_over": "Another device took over this send; it continues there.", "joberr.ava1_shared_jobs_unsupported": "Another device is still running this send. Taking it over (or watching it) while it runs needs the newer helper on both consoles: update them, or pause the send on that device first.", "joberr.ava1_cannot_watch": "This send goes through the other device, so it can be taken over but not watched.", +"fpkg.make.folder_title": "A game folder", +"fpkg.make.folder_body": "The game's files as a plain folder on this computer, from a game image or a package made with the debug key. Edit it, keep it, or upload it like any folder.", +"fpkg.folder.what": "The folder is named after the image or package, in the output folder. Nothing is compressed, and nothing reaches the PS5.", +"cq_ext_folder": "folder", +"fpkg.stage.write_folder": "Write folder", +"fpkg.stage.verify_folder": "Read the folder back", +"fpkg.folder.make": "Make game folder", +"fpkg.folder.already": "This is already a folder", +"fpkg.folder.sourceOnly": "Only a game image or a package made with the debug key can become a folder; unpack an archive first.", +"fpkg.folder.done": "Game folder written and read back.", +"cq_then_keep_folder": "Keep the folder", +"cq_folder_sources_only": "Only a game image or a package made with the debug key can become a folder: {count} skipped.", +"fpkg.run.what_folder": "a game folder", }; export default en; diff --git a/client/src/i18n/locales/es.ts b/client/src/i18n/locales/es.ts index e7cb4fdb..360d9816 100644 --- a/client/src/i18n/locales/es.ts +++ b/client/src/i18n/locales/es.ts @@ -3937,6 +3937,19 @@ c2c_test_deep_bad: "El envío de prueba de 4 KiB no llegó: {detail}", "joberr.ava1_taken_over": "Otro dispositivo tomó el control de este envío; continúa allí.", "joberr.ava1_shared_jobs_unsupported": "Otro dispositivo sigue ejecutando este envío. Tomar el control (o verlo) mientras se ejecuta requiere el asistente más reciente en ambas consolas: actualízalos o pon el envío en pausa primero en ese dispositivo.", "joberr.ava1_cannot_watch": "Este envío pasa por el otro dispositivo, así que se puede tomar el control, pero no verlo.", +"fpkg.make.folder_title": "Una carpeta de juego", +"fpkg.make.folder_body": "Los archivos del juego como una carpeta normal en este equipo, a partir de una imagen de juego o de un paquete creado con la clave de depuración. Edítala, guárdala o súbela como cualquier carpeta.", +"fpkg.folder.what": "La carpeta lleva el nombre de la imagen o del paquete y se crea en la carpeta de salida. No se comprime nada y nada llega a la PS5.", +"cq_ext_folder": "carpeta", +"fpkg.stage.write_folder": "Escribir carpeta", +"fpkg.stage.verify_folder": "Releer la carpeta", +"fpkg.folder.make": "Crear carpeta del juego", +"fpkg.folder.already": "Esto ya es una carpeta", +"fpkg.folder.sourceOnly": "Solo una imagen de juego o un paquete creado con la clave de depuración puede convertirse en carpeta; descomprime antes el archivo comprimido.", +"fpkg.folder.done": "Carpeta del juego escrita y releída.", +"cq_then_keep_folder": "Conservar la carpeta", +"cq_folder_sources_only": "Solo una imagen de juego o un paquete creado con la clave de depuración puede convertirse en carpeta: {count} omitidos.", +"fpkg.run.what_folder": "una carpeta de juego", }; export default es; diff --git a/client/src/i18n/locales/fa.ts b/client/src/i18n/locales/fa.ts index 49042332..004b7566 100644 --- a/client/src/i18n/locales/fa.ts +++ b/client/src/i18n/locales/fa.ts @@ -4288,6 +4288,19 @@ c2c_test_deep_bad: "ارسال آزمایشی ۴ KiB نرسید: {detail}", "joberr.ava1_taken_over": "دستگاه دیگری این ارسال را در دست گرفت؛ آنجا ادامه می‌یابد.", "joberr.ava1_shared_jobs_unsupported": "دستگاه دیگری هنوز این ارسال را اجرا می‌کند. در دست گرفتن (یا تماشای) آن در حین اجرا به دستیار جدیدتر روی هر دو کنسول نیاز دارد: آن‌ها را به‌روز کنید یا ابتدا ارسال را روی آن دستگاه متوقف موقت کنید.", "joberr.ava1_cannot_watch": "این ارسال از طریق دستگاه دیگر می‌گذرد، پس می‌توان آن را در دست گرفت اما نمی‌توان تماشا کرد.", +"fpkg.make.folder_title": "یک پوشهٔ بازی", +"fpkg.make.folder_body": "فایل‌های بازی به‌صورت یک پوشهٔ معمولی روی این رایانه، از ایمیج بازی یا بسته‌ای که با کلید دیباگ ساخته شده است. مثل هر پوشهٔ دیگری ویرایشش کنید، نگهش دارید یا آپلودش کنید.", +"fpkg.folder.what": "نام پوشه از روی ایمیج یا بسته گذاشته می‌شود و در پوشهٔ خروجی ساخته می‌شود. هیچ چیزی فشرده نمی‌شود و چیزی به PS5 نمی‌رسد.", +"cq_ext_folder": "پوشه", +"fpkg.stage.write_folder": "نوشتن پوشه", +"fpkg.stage.verify_folder": "بازخوانی پوشه", +"fpkg.folder.make": "ساخت پوشهٔ بازی", +"fpkg.folder.already": "این از قبل یک پوشه است", +"fpkg.folder.sourceOnly": "فقط ایمیج بازی یا بسته‌ای که با کلید دیباگ ساخته شده می‌تواند پوشه شود؛ اول آرشیو را باز کنید.", +"fpkg.folder.done": "پوشهٔ بازی نوشته و بازخوانی شد.", +"cq_then_keep_folder": "نگه‌داشتن پوشه", +"cq_folder_sources_only": "فقط ایمیج بازی یا بسته‌ای که با کلید دیباگ ساخته شده می‌تواند پوشه شود: {count} مورد رد شد.", +"fpkg.run.what_folder": "یک پوشهٔ بازی", }; export default fa; diff --git a/client/src/i18n/locales/fr.ts b/client/src/i18n/locales/fr.ts index 4d771127..818a0ce8 100644 --- a/client/src/i18n/locales/fr.ts +++ b/client/src/i18n/locales/fr.ts @@ -4005,6 +4005,19 @@ c2c_test_deep_bad: "L’envoi test de 4 Kio n’est pas arrivé : {detail}", "joberr.ava1_taken_over": "Un autre appareil a repris la main sur cet envoi ; il continue là-bas.", "joberr.ava1_shared_jobs_unsupported": "Un autre appareil exécute encore cet envoi. En reprendre la main (ou le suivre) pendant qu’il tourne nécessite le nouvel assistant sur les deux consoles : mettez-les à jour, ou mettez d’abord l’envoi en pause sur cet appareil.", "joberr.ava1_cannot_watch": "Cet envoi passe par l’autre appareil : on peut en reprendre la main, mais pas le suivre.", +"fpkg.make.folder_title": "Un dossier de jeu", +"fpkg.make.folder_body": "Les fichiers du jeu sous forme de dossier ordinaire sur cet ordinateur, à partir d'une image de jeu ou d'un paquet créé avec la clé de débogage. Modifiez-le, gardez-le ou envoyez-le comme n'importe quel dossier.", +"fpkg.folder.what": "Le dossier porte le nom de l'image ou du paquet, dans le dossier de sortie. Rien n'est compressé et rien n'est envoyé à la PS5.", +"cq_ext_folder": "dossier", +"fpkg.stage.write_folder": "Écrire le dossier", +"fpkg.stage.verify_folder": "Relire le dossier", +"fpkg.folder.make": "Créer le dossier du jeu", +"fpkg.folder.already": "C'est déjà un dossier", +"fpkg.folder.sourceOnly": "Seule une image de jeu ou un paquet créé avec la clé de débogage peut devenir un dossier ; décompressez d'abord l'archive.", +"fpkg.folder.done": "Dossier du jeu écrit et relu.", +"cq_then_keep_folder": "Garder le dossier", +"cq_folder_sources_only": "Seule une image de jeu ou un paquet créé avec la clé de débogage peut devenir un dossier : {count} ignoré(s).", +"fpkg.run.what_folder": "un dossier de jeu", }; export default fr; diff --git a/client/src/i18n/locales/hi.ts b/client/src/i18n/locales/hi.ts index 2d82edc8..beedf033 100644 --- a/client/src/i18n/locales/hi.ts +++ b/client/src/i18n/locales/hi.ts @@ -4003,6 +4003,19 @@ c2c_test_deep_bad: "4 KiB का परीक्षण नहीं पहुँ "joberr.ava1_taken_over": "दूसरे डिवाइस ने यह भेजना अपने हाथ में ले लिया है; यह वहाँ जारी रहेगा।", "joberr.ava1_shared_jobs_unsupported": "दूसरा डिवाइस अभी भी यह भेजना चला रहा है। चलते समय इसे अपने हाथ में लेने (या देखने) के लिए दोनों कंसोल पर नया हेल्पर चाहिए: उन्हें अपडेट करें, या पहले उस डिवाइस पर भेजना रोकें।", "joberr.ava1_cannot_watch": "यह भेजना दूसरे डिवाइस से होकर जाता है, इसलिए इसे अपने हाथ में लिया जा सकता है पर देखा नहीं जा सकता।", +"fpkg.make.folder_title": "एक गेम फ़ोल्डर", +"fpkg.make.folder_body": "गेम की फ़ाइलें इस कंप्यूटर पर एक सामान्य फ़ोल्डर के रूप में, किसी गेम इमेज या डिबग कुंजी से बने पैकेज से। इसे किसी भी फ़ोल्डर की तरह संपादित करें, रखें या अपलोड करें।", +"fpkg.folder.what": "फ़ोल्डर का नाम इमेज या पैकेज के नाम पर रखा जाता है, आउटपुट फ़ोल्डर में। कुछ भी संपीड़ित नहीं होता और PS5 तक कुछ नहीं भेजा जाता।", +"cq_ext_folder": "फ़ोल्डर", +"fpkg.stage.write_folder": "फ़ोल्डर लिखें", +"fpkg.stage.verify_folder": "फ़ोल्डर दोबारा पढ़ें", +"fpkg.folder.make": "गेम फ़ोल्डर बनाएँ", +"fpkg.folder.already": "यह पहले से ही एक फ़ोल्डर है", +"fpkg.folder.sourceOnly": "केवल गेम इमेज या डिबग कुंजी से बना पैकेज ही फ़ोल्डर बन सकता है; पहले आर्काइव को अनपैक करें।", +"fpkg.folder.done": "गेम फ़ोल्डर लिखा गया और दोबारा पढ़कर जाँचा गया।", +"cq_then_keep_folder": "फ़ोल्डर रखें", +"cq_folder_sources_only": "केवल गेम इमेज या डिबग कुंजी से बना पैकेज ही फ़ोल्डर बन सकता है: {count} छोड़े गए।", +"fpkg.run.what_folder": "एक गेम फ़ोल्डर", }; export default hi; diff --git a/client/src/i18n/locales/hu.ts b/client/src/i18n/locales/hu.ts index 4a114c00..39210875 100644 --- a/client/src/i18n/locales/hu.ts +++ b/client/src/i18n/locales/hu.ts @@ -4302,6 +4302,19 @@ c2c_test_deep_bad: "A 4 KiB-os tesztküldés nem érkezett meg: {detail}", "joberr.ava1_taken_over": "Egy másik eszköz átvette ezt a küldést; ott folytatódik.", "joberr.ava1_shared_jobs_unsupported": "Egy másik eszköz még futtatja ezt a küldést. Futás közbeni átvételéhez (vagy figyeléséhez) mindkét konzolon az újabb segéd kell: frissítsd őket, vagy előbb szüneteltesd a küldést azon az eszközön.", "joberr.ava1_cannot_watch": "Ez a küldés a másik eszközön át megy, ezért átvehető, de nem figyelhető.", +"fpkg.make.folder_title": "Egy játékmappa", +"fpkg.make.folder_body": "A játék fájljai egyszerű mappaként ezen a számítógépen, játékképfájlból vagy a debug kulccsal készült csomagból. Szerkesztheted, megtarthatod vagy feltöltheted, mint bármely mappát.", +"fpkg.folder.what": "A mappa a képfájl vagy a csomag nevét kapja, és a kimeneti mappába kerül. Semmi sincs tömörítve, és semmi sem jut el a PS5-re.", +"cq_ext_folder": "mappa", +"fpkg.stage.write_folder": "Mappa írása", +"fpkg.stage.verify_folder": "Mappa visszaolvasása", +"fpkg.folder.make": "Játékmappa készítése", +"fpkg.folder.already": "Ez már egy mappa", +"fpkg.folder.sourceOnly": "Csak játékképfájlból vagy a debug kulccsal készült csomagból lehet mappa; előbb csomagold ki az archívumot.", +"fpkg.folder.done": "A játékmappa megírva és visszaolvasva.", +"cq_then_keep_folder": "Mappa megtartása", +"cq_folder_sources_only": "Csak játékképfájlból vagy a debug kulccsal készült csomagból lehet mappa: {count} kihagyva.", +"fpkg.run.what_folder": "egy játékmappát", }; export default hu; diff --git a/client/src/i18n/locales/id.ts b/client/src/i18n/locales/id.ts index 09c718be..88eb79cd 100644 --- a/client/src/i18n/locales/id.ts +++ b/client/src/i18n/locales/id.ts @@ -3996,6 +3996,19 @@ c2c_test_deep_bad: "Kiriman uji 4 KiB tidak tiba: {detail}", "joberr.ava1_taken_over": "Perangkat lain mengambil alih kiriman ini; kiriman berlanjut di sana.", "joberr.ava1_shared_jobs_unsupported": "Perangkat lain masih menjalankan kiriman ini. Mengambil alih (atau memantaunya) saat berjalan memerlukan helper terbaru di kedua konsol: perbarui, atau jeda dulu kiriman di perangkat itu.", "joberr.ava1_cannot_watch": "Kiriman ini lewat perangkat lain, jadi bisa diambil alih tetapi tidak bisa dipantau.", +"fpkg.make.folder_title": "Folder game", +"fpkg.make.folder_body": "File game sebagai folder biasa di komputer ini, dari image game atau paket yang dibuat dengan kunci debug. Edit, simpan, atau unggah seperti folder lainnya.", +"fpkg.folder.what": "Folder diberi nama sesuai image atau paket, di folder output. Tidak ada yang dikompresi, dan tidak ada yang dikirim ke PS5.", +"cq_ext_folder": "folder", +"fpkg.stage.write_folder": "Tulis folder", +"fpkg.stage.verify_folder": "Baca ulang folder", +"fpkg.folder.make": "Buat folder game", +"fpkg.folder.already": "Ini sudah berupa folder", +"fpkg.folder.sourceOnly": "Hanya image game atau paket yang dibuat dengan kunci debug yang bisa dijadikan folder; ekstrak arsip terlebih dahulu.", +"fpkg.folder.done": "Folder game ditulis dan dibaca ulang.", +"cq_then_keep_folder": "Simpan foldernya", +"cq_folder_sources_only": "Hanya image game atau paket yang dibuat dengan kunci debug yang bisa dijadikan folder: {count} dilewati.", +"fpkg.run.what_folder": "folder game", }; export default id; diff --git a/client/src/i18n/locales/it.ts b/client/src/i18n/locales/it.ts index fd61b5a2..231152c7 100644 --- a/client/src/i18n/locales/it.ts +++ b/client/src/i18n/locales/it.ts @@ -3937,6 +3937,19 @@ c2c_test_deep_bad: "L’invio di prova di 4 KiB non è arrivato: {detail}", "joberr.ava1_taken_over": "Un altro dispositivo ha preso il controllo di questo invio; continua lì.", "joberr.ava1_shared_jobs_unsupported": "Un altro dispositivo sta ancora eseguendo questo invio. Prenderne il controllo (o guardarlo) mentre è in corso richiede l’assistente più recente su entrambe le console: aggiornali, oppure metti prima in pausa l’invio su quel dispositivo.", "joberr.ava1_cannot_watch": "Questo invio passa dall’altro dispositivo, quindi se ne può prendere il controllo ma non guardarlo.", +"fpkg.make.folder_title": "Una cartella di gioco", +"fpkg.make.folder_body": "I file del gioco come una normale cartella su questo computer, da un'immagine di gioco o da un pacchetto creato con la chiave di debug. Modificala, conservala o caricala come qualsiasi cartella.", +"fpkg.folder.what": "La cartella prende il nome dall'immagine o dal pacchetto e viene creata nella cartella di destinazione. Nulla viene compresso e nulla arriva alla PS5.", +"cq_ext_folder": "cartella", +"fpkg.stage.write_folder": "Scrivi cartella", +"fpkg.stage.verify_folder": "Rileggi la cartella", +"fpkg.folder.make": "Crea cartella del gioco", +"fpkg.folder.already": "Questa è già una cartella", +"fpkg.folder.sourceOnly": "Solo un'immagine di gioco o un pacchetto creato con la chiave di debug può diventare una cartella; estrai prima l'archivio.", +"fpkg.folder.done": "Cartella del gioco scritta e riletta.", +"cq_then_keep_folder": "Conserva la cartella", +"cq_folder_sources_only": "Solo un'immagine di gioco o un pacchetto creato con la chiave di debug può diventare una cartella: {count} saltati.", +"fpkg.run.what_folder": "una cartella di gioco", }; export default it; diff --git a/client/src/i18n/locales/ja.ts b/client/src/i18n/locales/ja.ts index 5ec598d2..50460335 100644 --- a/client/src/i18n/locales/ja.ts +++ b/client/src/i18n/locales/ja.ts @@ -4008,6 +4008,19 @@ c2c_test_deep_bad: "4 KiB のテスト送信が届きませんでした: {detail "joberr.ava1_taken_over": "別のデバイスがこの送信を引き継ぎました。そちらで続行します。", "joberr.ava1_shared_jobs_unsupported": "別のデバイスがまだこの送信を実行しています。実行中に引き継ぐ(または見守る)には両方のコンソールに新しいヘルパーが必要です: 更新するか、先にそのデバイスで送信を一時停止してください。", "joberr.ava1_cannot_watch": "この送信は別のデバイスを経由しているため、引き継ぐことはできますが見守ることはできません。", +"fpkg.make.folder_title": "ゲームフォルダー", +"fpkg.make.folder_body": "ゲームのファイルを、このコンピューター上の普通のフォルダーとして取り出します。元はゲームイメージか、デバッグキーで作ったパッケージです。編集・保管したり、普通のフォルダーとしてアップロードしたりできます。", +"fpkg.folder.what": "フォルダーはイメージまたはパッケージの名前で出力フォルダーに作られます。圧縮はせず、PS5 には何も送りません。", +"cq_ext_folder": "フォルダー", +"fpkg.stage.write_folder": "フォルダーを書き込み", +"fpkg.stage.verify_folder": "フォルダーを読み戻し", +"fpkg.folder.make": "ゲームフォルダーを作成", +"fpkg.folder.already": "これはすでにフォルダーです", +"fpkg.folder.sourceOnly": "フォルダーにできるのは、ゲームイメージかデバッグキーで作ったパッケージだけです。アーカイブは先に展開してください。", +"fpkg.folder.done": "ゲームフォルダーを書き込み、読み戻して確認しました。", +"cq_then_keep_folder": "フォルダーを残す", +"cq_folder_sources_only": "フォルダーにできるのは、ゲームイメージかデバッグキーで作ったパッケージだけです:{count} 件をスキップしました。", +"fpkg.run.what_folder": "ゲームフォルダー", }; export default ja; diff --git a/client/src/i18n/locales/ko.ts b/client/src/i18n/locales/ko.ts index ffe0921d..1602e663 100644 --- a/client/src/i18n/locales/ko.ts +++ b/client/src/i18n/locales/ko.ts @@ -4006,6 +4006,19 @@ c2c_test_deep_bad: "4 KiB 테스트 전송이 도착하지 않았습니다: {det "joberr.ava1_taken_over": "다른 기기가 이 전송을 넘겨받았습니다. 그곳에서 계속됩니다.", "joberr.ava1_shared_jobs_unsupported": "다른 기기가 아직 이 전송을 실행 중입니다. 실행 중에 넘겨받거나 지켜보려면 두 콘솔 모두에 새 도우미가 필요합니다: 업데이트하거나 먼저 그 기기에서 전송을 일시 정지하세요.", "joberr.ava1_cannot_watch": "이 전송은 다른 기기를 거쳐 가므로 넘겨받을 수는 있지만 지켜볼 수는 없습니다.", +"fpkg.make.folder_title": "게임 폴더", +"fpkg.make.folder_body": "게임 이미지나 디버그 키로 만든 패키지에서 게임 파일을 이 컴퓨터의 일반 폴더로 꺼냅니다. 다른 폴더처럼 편집하거나 보관하거나 업로드할 수 있습니다.", +"fpkg.folder.what": "폴더는 이미지나 패키지 이름으로 출력 폴더에 만들어집니다. 압축하지 않으며 PS5로는 아무것도 보내지 않습니다.", +"cq_ext_folder": "폴더", +"fpkg.stage.write_folder": "폴더 쓰기", +"fpkg.stage.verify_folder": "폴더 다시 읽기", +"fpkg.folder.make": "게임 폴더 만들기", +"fpkg.folder.already": "이미 폴더입니다", +"fpkg.folder.sourceOnly": "게임 이미지나 디버그 키로 만든 패키지만 폴더로 만들 수 있습니다. 압축 파일은 먼저 풀어 주세요.", +"fpkg.folder.done": "게임 폴더를 쓰고 다시 읽어 확인했습니다.", +"cq_then_keep_folder": "폴더 보관", +"cq_folder_sources_only": "게임 이미지나 디버그 키로 만든 패키지만 폴더로 만들 수 있습니다: {count}개 건너뜀.", +"fpkg.run.what_folder": "게임 폴더", }; export default ko; diff --git a/client/src/i18n/locales/pl.ts b/client/src/i18n/locales/pl.ts index 661c789b..ace6ca4a 100644 --- a/client/src/i18n/locales/pl.ts +++ b/client/src/i18n/locales/pl.ts @@ -4242,6 +4242,19 @@ c2c_test_deep_bad: "Testowa wysyłka 4 KiB nie dotarła: {detail}", "joberr.ava1_taken_over": "Inne urządzenie przejęło tę wysyłkę; trwa ona tam.", "joberr.ava1_shared_jobs_unsupported": "Inne urządzenie nadal wykonuje tę wysyłkę. Przejęcie jej (lub obserwowanie) w trakcie wymaga nowszego pomocnika na obu konsolach: zaktualizuj je albo najpierw wstrzymaj wysyłkę na tamtym urządzeniu.", "joberr.ava1_cannot_watch": "Ta wysyłka idzie przez inne urządzenie, więc można ją przejąć, ale nie obserwować.", +"fpkg.make.folder_title": "Folder gry", +"fpkg.make.folder_body": "Pliki gry jako zwykły folder na tym komputerze, z obrazu gry lub pakietu zbudowanego kluczem debugowym. Edytuj go, zachowaj albo wyślij jak każdy folder.", +"fpkg.folder.what": "Folder otrzymuje nazwę obrazu lub pakietu i trafia do folderu wyjściowego. Nic nie jest kompresowane i nic nie trafia na PS5.", +"cq_ext_folder": "folder", +"fpkg.stage.write_folder": "Zapis folderu", +"fpkg.stage.verify_folder": "Odczyt kontrolny folderu", +"fpkg.folder.make": "Utwórz folder gry", +"fpkg.folder.already": "To już jest folder", +"fpkg.folder.sourceOnly": "Folderem może stać się tylko obraz gry lub pakiet zbudowany kluczem debugowym; najpierw rozpakuj archiwum.", +"fpkg.folder.done": "Folder gry zapisany i odczytany kontrolnie.", +"cq_then_keep_folder": "Zachowaj folder", +"cq_folder_sources_only": "Folderem może stać się tylko obraz gry lub pakiet zbudowany kluczem debugowym: pominięto {count}.", +"fpkg.run.what_folder": "folder gry", }; export default pl; diff --git a/client/src/i18n/locales/pt-BR.ts b/client/src/i18n/locales/pt-BR.ts index 9960abcf..43f56e9f 100644 --- a/client/src/i18n/locales/pt-BR.ts +++ b/client/src/i18n/locales/pt-BR.ts @@ -4004,6 +4004,19 @@ c2c_test_deep_bad: "O envio de teste de 4 KiB não chegou: {detail}", "joberr.ava1_taken_over": "Outro dispositivo assumiu este envio; ele continua lá.", "joberr.ava1_shared_jobs_unsupported": "Outro dispositivo ainda está executando este envio. Assumi-lo (ou acompanhá-lo) enquanto roda exige o auxiliar mais novo nos dois consoles: atualize-os ou pause o envio primeiro naquele dispositivo.", "joberr.ava1_cannot_watch": "Este envio passa pelo outro dispositivo, então pode ser assumido, mas não acompanhado.", +"fpkg.make.folder_title": "Uma pasta de jogo", +"fpkg.make.folder_body": "Os arquivos do jogo como uma pasta comum neste computador, a partir de uma imagem de jogo ou de um pacote feito com a chave de depuração. Edite, guarde ou envie como qualquer pasta.", +"fpkg.folder.what": "A pasta recebe o nome da imagem ou do pacote, na pasta de saída. Nada é comprimido e nada vai para o PS5.", +"cq_ext_folder": "pasta", +"fpkg.stage.write_folder": "Gravar pasta", +"fpkg.stage.verify_folder": "Reler a pasta", +"fpkg.folder.make": "Criar pasta do jogo", +"fpkg.folder.already": "Isto já é uma pasta", +"fpkg.folder.sourceOnly": "Só uma imagem de jogo ou um pacote feito com a chave de depuração pode virar pasta; extraia o arquivo compactado antes.", +"fpkg.folder.done": "Pasta do jogo gravada e relida.", +"cq_then_keep_folder": "Manter a pasta", +"cq_folder_sources_only": "Só uma imagem de jogo ou um pacote feito com a chave de depuração pode virar pasta: {count} ignorados.", +"fpkg.run.what_folder": "uma pasta de jogo", }; export default pt_BR; diff --git a/client/src/i18n/locales/ru.ts b/client/src/i18n/locales/ru.ts index 6282c221..7592f2f2 100644 --- a/client/src/i18n/locales/ru.ts +++ b/client/src/i18n/locales/ru.ts @@ -3937,6 +3937,19 @@ c2c_test_deep_bad: "Тестовая отправка 4 КиБ не дошла: "joberr.ava1_taken_over": "Другое устройство перехватило эту отправку; она продолжается там.", "joberr.ava1_shared_jobs_unsupported": "Другое устройство всё ещё выполняет эту отправку. Чтобы перехватить её (или наблюдать) во время работы, на обеих консолях нужен новый помощник: обновите их или сначала приостановите отправку на том устройстве.", "joberr.ava1_cannot_watch": "Эта отправка идёт через другое устройство, поэтому её можно перехватить, но не наблюдать.", +"fpkg.make.folder_title": "Папка игры", +"fpkg.make.folder_body": "Файлы игры обычной папкой на этом компьютере — из образа игры или пакета, собранного с отладочным ключом. Редактируйте её, храните или загружайте как любую папку.", +"fpkg.folder.what": "Папка получает имя образа или пакета и создаётся в папке вывода. Ничего не сжимается, и ничего не отправляется на PS5.", +"cq_ext_folder": "папка", +"fpkg.stage.write_folder": "Запись папки", +"fpkg.stage.verify_folder": "Проверка папки чтением", +"fpkg.folder.make": "Создать папку игры", +"fpkg.folder.already": "Это уже папка", +"fpkg.folder.sourceOnly": "Папкой может стать только образ игры или пакет, собранный с отладочным ключом; сначала распакуйте архив.", +"fpkg.folder.done": "Папка игры записана и проверена чтением.", +"cq_then_keep_folder": "Оставить папку", +"cq_folder_sources_only": "Папкой может стать только образ игры или пакет, собранный с отладочным ключом: пропущено {count}.", +"fpkg.run.what_folder": "папку игры", }; export default ru; diff --git a/client/src/i18n/locales/th.ts b/client/src/i18n/locales/th.ts index e90630c2..6fa68559 100644 --- a/client/src/i18n/locales/th.ts +++ b/client/src/i18n/locales/th.ts @@ -3996,6 +3996,19 @@ c2c_test_deep_bad: "การส่งทดสอบ 4 KiB ไปไม่ถ "joberr.ava1_taken_over": "อุปกรณ์อื่นรับช่วงการส่งนี้ไปแล้ว และจะทำต่อที่นั่น", "joberr.ava1_shared_jobs_unsupported": "อุปกรณ์อื่นยังทำการส่งนี้อยู่ การรับช่วง (หรือดู) ขณะทำงานต้องใช้ตัวช่วยเวอร์ชันใหม่บนทั้งสองคอนโซล: อัปเดต หรือหยุดการส่งชั่วคราวบนอุปกรณ์นั้นก่อน", "joberr.ava1_cannot_watch": "การส่งนี้วิ่งผ่านอุปกรณ์อื่น จึงรับช่วงต่อได้แต่ดูไม่ได้", +"fpkg.make.folder_title": "โฟลเดอร์เกม", +"fpkg.make.folder_body": "ไฟล์ของเกมในรูปโฟลเดอร์ธรรมดาบนคอมพิวเตอร์เครื่องนี้ จากอิมเมจเกมหรือแพ็กเกจที่สร้างด้วยคีย์ดีบัก แก้ไข เก็บไว้ หรืออัปโหลดได้เหมือนโฟลเดอร์ทั่วไป", +"fpkg.folder.what": "โฟลเดอร์จะตั้งชื่อตามอิมเมจหรือแพ็กเกจ ในโฟลเดอร์ปลายทาง ไม่มีการบีบอัด และไม่มีอะไรถูกส่งไปที่ PS5", +"cq_ext_folder": "โฟลเดอร์", +"fpkg.stage.write_folder": "เขียนโฟลเดอร์", +"fpkg.stage.verify_folder": "อ่านโฟลเดอร์กลับ", +"fpkg.folder.make": "สร้างโฟลเดอร์เกม", +"fpkg.folder.already": "นี่เป็นโฟลเดอร์อยู่แล้ว", +"fpkg.folder.sourceOnly": "เฉพาะอิมเมจเกมหรือแพ็กเกจที่สร้างด้วยคีย์ดีบักเท่านั้นที่ทำเป็นโฟลเดอร์ได้ แตกไฟล์บีบอัดก่อน", +"fpkg.folder.done": "เขียนโฟลเดอร์เกมและอ่านกลับตรวจสอบแล้ว", +"cq_then_keep_folder": "เก็บโฟลเดอร์ไว้", +"cq_folder_sources_only": "เฉพาะอิมเมจเกมหรือแพ็กเกจที่สร้างด้วยคีย์ดีบักเท่านั้นที่ทำเป็นโฟลเดอร์ได้: ข้าม {count} รายการ", +"fpkg.run.what_folder": "โฟลเดอร์เกม", }; export default th; diff --git a/client/src/i18n/locales/tr.ts b/client/src/i18n/locales/tr.ts index ad4b6458..e420b888 100644 --- a/client/src/i18n/locales/tr.ts +++ b/client/src/i18n/locales/tr.ts @@ -3994,6 +3994,19 @@ c2c_test_deep_bad: "4 KiB'lik test gönderimi ulaşmadı: {detail}", "joberr.ava1_taken_over": "Bu gönderimi başka bir cihaz devraldı; orada devam ediyor.", "joberr.ava1_shared_jobs_unsupported": "Bu gönderimi başka bir cihaz hâlâ çalıştırıyor. Çalışırken devralmak (veya izlemek) için her iki konsolda da yeni yardımcı gerekir: güncelleyin ya da önce gönderimi o cihazda duraklatın.", "joberr.ava1_cannot_watch": "Bu gönderim diğer cihaz üzerinden geçiyor, bu yüzden devralınabilir ama izlenemez.", +"fpkg.make.folder_title": "Bir oyun klasörü", +"fpkg.make.folder_body": "Oyunun dosyaları bu bilgisayarda düz bir klasör olarak; bir oyun kalıbından ya da hata ayıklama anahtarıyla yapılmış bir paketten. Düzenleyin, saklayın ya da her klasör gibi yükleyin.", +"fpkg.folder.what": "Klasör, kalıbın veya paketin adını alır ve çıktı klasörüne yazılır. Hiçbir şey sıkıştırılmaz ve PS5'e hiçbir şey gitmez.", +"cq_ext_folder": "klasör", +"fpkg.stage.write_folder": "Klasörü yaz", +"fpkg.stage.verify_folder": "Klasörü geri oku", +"fpkg.folder.make": "Oyun klasörü oluştur", +"fpkg.folder.already": "Bu zaten bir klasör", +"fpkg.folder.sourceOnly": "Yalnızca bir oyun kalıbı ya da hata ayıklama anahtarıyla yapılmış bir paket klasöre dönüştürülebilir; önce arşivi açın.", +"fpkg.folder.done": "Oyun klasörü yazıldı ve geri okundu.", +"cq_then_keep_folder": "Klasörü sakla", +"cq_folder_sources_only": "Yalnızca bir oyun kalıbı ya da hata ayıklama anahtarıyla yapılmış bir paket klasöre dönüştürülebilir: {count} atlandı.", +"fpkg.run.what_folder": "bir oyun klasörü", }; export default tr; diff --git a/client/src/i18n/locales/vi.ts b/client/src/i18n/locales/vi.ts index 53e94aa7..ac4cdc5a 100644 --- a/client/src/i18n/locales/vi.ts +++ b/client/src/i18n/locales/vi.ts @@ -3989,6 +3989,19 @@ c2c_test_deep_bad: "Lượt gửi thử 4 KiB không tới: {detail}", "joberr.ava1_taken_over": "Một thiết bị khác đã tiếp quản lượt gửi này; nó tiếp tục ở đó.", "joberr.ava1_shared_jobs_unsupported": "Một thiết bị khác vẫn đang chạy lượt gửi này. Tiếp quản (hoặc theo dõi) khi nó đang chạy cần trình trợ giúp mới trên cả hai máy: hãy cập nhật, hoặc tạm dừng lượt gửi trên thiết bị đó trước.", "joberr.ava1_cannot_watch": "Lượt gửi này đi qua thiết bị khác, nên có thể tiếp quản nhưng không thể theo dõi.", +"fpkg.make.folder_title": "Một thư mục game", +"fpkg.make.folder_body": "Các tệp của game dưới dạng thư mục thường trên máy tính này, từ ảnh game hoặc gói được tạo bằng khóa debug. Chỉnh sửa, giữ lại hoặc tải lên như mọi thư mục.", +"fpkg.folder.what": "Thư mục được đặt tên theo ảnh hoặc gói, trong thư mục đầu ra. Không có gì bị nén và không có gì được gửi tới PS5.", +"cq_ext_folder": "thư mục", +"fpkg.stage.write_folder": "Ghi thư mục", +"fpkg.stage.verify_folder": "Đọc lại thư mục", +"fpkg.folder.make": "Tạo thư mục game", +"fpkg.folder.already": "Đây đã là một thư mục", +"fpkg.folder.sourceOnly": "Chỉ ảnh game hoặc gói được tạo bằng khóa debug mới có thể thành thư mục; hãy giải nén tệp nén trước.", +"fpkg.folder.done": "Đã ghi và đọc lại thư mục game.", +"cq_then_keep_folder": "Giữ thư mục", +"cq_folder_sources_only": "Chỉ ảnh game hoặc gói được tạo bằng khóa debug mới có thể thành thư mục: đã bỏ qua {count}.", +"fpkg.run.what_folder": "một thư mục game", }; export default vi; diff --git a/client/src/i18n/locales/zh-CN.ts b/client/src/i18n/locales/zh-CN.ts index d9940cd4..7fcf86fb 100644 --- a/client/src/i18n/locales/zh-CN.ts +++ b/client/src/i18n/locales/zh-CN.ts @@ -3943,6 +3943,19 @@ c2c_test_deep_bad: "4 KiB 测试发送未到达:{detail}", "joberr.ava1_taken_over": "另一台设备已接管此发送,它会在那里继续。", "joberr.ava1_shared_jobs_unsupported": "另一台设备仍在运行此发送。在运行中接管(或查看)需要两台主机都使用新版助手:请更新助手,或先在那台设备上暂停发送。", "joberr.ava1_cannot_watch": "此发送经由另一台设备传输,因此可以接管,但无法查看。", +"fpkg.make.folder_title": "游戏文件夹", +"fpkg.make.folder_body": "把游戏镜像或用调试密钥制作的包里的游戏文件,还原成这台电脑上的普通文件夹。可以像任何文件夹一样编辑、保存或上传。", +"fpkg.folder.what": "文件夹以镜像或包的名称命名,放在输出文件夹中。不做任何压缩,也不会向 PS5 发送任何内容。", +"cq_ext_folder": "文件夹", +"fpkg.stage.write_folder": "写入文件夹", +"fpkg.stage.verify_folder": "回读文件夹", +"fpkg.folder.make": "制作游戏文件夹", +"fpkg.folder.already": "这已经是一个文件夹", +"fpkg.folder.sourceOnly": "只有游戏镜像或用调试密钥制作的包才能还原成文件夹;请先解压压缩包。", +"fpkg.folder.done": "游戏文件夹已写入并回读校验。", +"cq_then_keep_folder": "保留文件夹", +"cq_folder_sources_only": "只有游戏镜像或用调试密钥制作的包才能还原成文件夹:已跳过 {count} 个。", +"fpkg.run.what_folder": "游戏文件夹", }; export default zh_CN; diff --git a/client/src/i18n/locales/zh-TW.ts b/client/src/i18n/locales/zh-TW.ts index a058dea0..54753f19 100644 --- a/client/src/i18n/locales/zh-TW.ts +++ b/client/src/i18n/locales/zh-TW.ts @@ -3937,6 +3937,19 @@ c2c_test_deep_bad: "4 KiB 測試傳送未抵達:{detail}", "joberr.ava1_taken_over": "另一部裝置已接手此傳送,它會在那裡繼續。", "joberr.ava1_shared_jobs_unsupported": "另一部裝置仍在執行此傳送。在執行中接手(或查看)需要兩部主機都使用新版協助程式:請更新協助程式,或先在那部裝置上暫停傳送。", "joberr.ava1_cannot_watch": "此傳送經由另一部裝置傳輸,因此可以接手,但無法查看。", +"fpkg.make.folder_title": "遊戲資料夾", +"fpkg.make.folder_body": "把遊戲映像或以除錯金鑰製作的套件中的遊戲檔案,還原成這台電腦上的一般資料夾。可以像任何資料夾一樣編輯、保留或上傳。", +"fpkg.folder.what": "資料夾以映像或套件的名稱命名,放在輸出資料夾中。不做任何壓縮,也不會傳送任何內容到 PS5。", +"cq_ext_folder": "資料夾", +"fpkg.stage.write_folder": "寫入資料夾", +"fpkg.stage.verify_folder": "回讀資料夾", +"fpkg.folder.make": "製作遊戲資料夾", +"fpkg.folder.already": "這已經是資料夾", +"fpkg.folder.sourceOnly": "只有遊戲映像或以除錯金鑰製作的套件才能還原成資料夾;請先解壓縮封存檔。", +"fpkg.folder.done": "遊戲資料夾已寫入並回讀驗證。", +"cq_then_keep_folder": "保留資料夾", +"cq_folder_sources_only": "只有遊戲映像或以除錯金鑰製作的套件才能還原成資料夾:已略過 {count} 個。", +"fpkg.run.what_folder": "遊戲資料夾", }; export default zh_TW; diff --git a/client/src/lib/browserInvoke.ts b/client/src/lib/browserInvoke.ts index 5a9f1b37..41b034df 100644 --- a/client/src/lib/browserInvoke.ts +++ b/client/src/lib/browserInvoke.ts @@ -795,6 +795,14 @@ export async function browserInvoke( full_verify: args["fullVerify"] ?? false, }); } + case "folder_build": { + return postJson("/api/folder/build", { + source: args["source"], + output_dir: args["outputDir"], + name: args["name"], + full_verify: args["fullVerify"] ?? false, + }); + } case "job_status": { // TS caller: { jobId } (Tauri 2 camelCases job_id → jobId) const jobId = args["jobId"] as string; diff --git a/client/src/lib/convertQueueRunner.ts b/client/src/lib/convertQueueRunner.ts index 0a2ca915..863f07ee 100644 --- a/client/src/lib/convertQueueRunner.ts +++ b/client/src/lib/convertQueueRunner.ts @@ -22,6 +22,15 @@ function whenIdle(): Promise { async function build(item: ConvertItem) { await whenIdle(); const conv = useFpkgConversion.getState(); + if (item.build?.kind === "folder") { + // A plain folder from an image or package: kept here, nothing to hand on. + await conv.buildFolder(item.source, item.outputDir); + await whenIdle(); + const p = useFpkgConversion.getState().pipeline; + if (p.phase === "done") return { ok: true, packagePath: p.packagePath, installed: true }; + if (p.phase === "failed") return { ok: false, message: p.message }; + return { ok: false, message: "The build did not start." }; + } if (item.build?.kind === "image") { // A game image, in the format picked; sent on to the Upload queue when asked. const send = item.then !== "keep" && !!item.host && !!item.imageDest; diff --git a/client/src/screens/FpkgConvert/GameCard.tsx b/client/src/screens/FpkgConvert/GameCard.tsx index e99b6a78..4ac7ca98 100644 --- a/client/src/screens/FpkgConvert/GameCard.tsx +++ b/client/src/screens/FpkgConvert/GameCard.tsx @@ -105,7 +105,7 @@ export function GameCard(props: GameCardProps) { remote label={tr("fpkg.browseImage", undefined, "Image or archive…")} title={tr("fpkg.pickImage", undefined, "Choose a game image or archive")} - filters={[{ name: "Game image or archive", extensions: ["exfat", "ffpkg", "ffpfs", "ffpfsc", "zip", "7z", "rar"] }]} + filters={[{ name: "Game image, package or archive", extensions: ["exfat", "ffpkg", "ffpfs", "ffpfsc", "pkg", "zip", "7z", "rar"] }]} disabled={props.locked || props.checking} onMainClick={props.onBrowseImage} onPick={props.onRemotePick} diff --git a/client/src/screens/FpkgConvert/MakeCard.tsx b/client/src/screens/FpkgConvert/MakeCard.tsx index 6d0cb8b4..25913c52 100644 --- a/client/src/screens/FpkgConvert/MakeCard.tsx +++ b/client/src/screens/FpkgConvert/MakeCard.tsx @@ -1,14 +1,16 @@ -import { Package, HardDrive } from "lucide-react"; +import { Folder, Package, HardDrive } from "lucide-react"; import { Badge, Card } from "../../components"; import { ps5FakeGameUnplayableFirmware } from "../../lib/ps5Firmware"; +import type { MakeKind } from "../../state/convertPrefs"; import { useTr } from "../../state/lang"; /** - * ② What to make: a package the PS5 installs, or a game image ShadowMount+ mounts. The two - * are explained side by side, and the one that suits the game on the connected console is - * marked: a PS5 game on firmware above 11.60 installs as a package but will not play, so the - * image is the way; otherwise the package is the ordinary choice. + * ② What to make: a package the PS5 installs, a game image ShadowMount+ mounts, or (#469) the + * game's files back as a plain folder. They are explained side by side, and the one of the + * first two that suits the game on the connected console is marked: a PS5 game on firmware + * above 11.60 installs as a package but will not play, so the image is the way; otherwise the + * package is the ordinary choice. The folder is never marked: it is a way back, not a way in. */ export function MakeCard({ make, @@ -18,8 +20,8 @@ export function MakeCard({ checked, locked = false, }: { - make: "pkg" | "image"; - onMake: (make: "pkg" | "image") => void; + make: MakeKind; + onMake: (make: MakeKind) => void; /** The connected console's kernel string, for the firmware line. */ kernel: string | null | undefined; /** The checked game's content id (or title id); null before a game is checked. */ @@ -31,7 +33,7 @@ export function MakeCard({ const tr = useTr(); const unplayableOn = checked ? ps5FakeGameUnplayableFirmware(kernel, contentId) : null; const recommended: "pkg" | "image" | null = !checked ? null : unplayableOn ? "image" : "pkg"; - const tiles: { id: "pkg" | "image"; icon: typeof Package; title: string; body: string }[] = [ + const tiles: { id: MakeKind; icon: typeof Package; title: string; body: string }[] = [ { id: "pkg", icon: Package, @@ -52,12 +54,22 @@ export function MakeCard({ "One file on a PS5 drive that ShadowMount+ mounts and shows on the home screen. Nothing is installed, and it plays on firmware where a PS5 package will not.", ), }, + { + id: "folder", + icon: Folder, + title: tr("fpkg.make.folder_title", undefined, "A game folder"), + body: tr( + "fpkg.make.folder_body", + undefined, + "The game's files as a plain folder on this computer, from a game image or a package made with the debug key. Edit it, keep it, or upload it like any folder.", + ), + }, ]; return (
{tr("fpkg.card.make", undefined, "② What to make")}
-
+
{tiles.map((t) => { const on = make === t.id; const Icon = t.icon; diff --git a/client/src/screens/FpkgConvert/OptionsCard.tsx b/client/src/screens/FpkgConvert/OptionsCard.tsx index c372dadb..f20f2265 100644 --- a/client/src/screens/FpkgConvert/OptionsCard.tsx +++ b/client/src/screens/FpkgConvert/OptionsCard.tsx @@ -4,6 +4,7 @@ import type { FpkgCompression, FpkgEstimate, ImageFormat } from "../../api/fpkg"; import { Button, Card } from "../../components"; import { PLAYGO_LANGUAGES } from "../../lib/playgoLanguages"; +import type { MakeKind } from "../../state/convertPrefs"; import { useTr } from "../../state/lang"; import { CompressionTiles } from "./CompressionTiles"; import { FullVerifyOption, ImageChoice, type Lz4Choice } from "./ImageOptions"; @@ -32,7 +33,7 @@ export interface OptionsCardProps { language?: string; /** What Convert makes. Compression and language are a package's: an image carries the * folder's files as they are, and its own compression is chosen beside its format. */ - make?: "pkg" | "image"; + make?: MakeKind; /** The image's options, shown in place of the package's when `make` is "image". */ image?: { format: ImageFormat; @@ -107,6 +108,14 @@ export function OptionsCard(props: OptionsCardProps) { /> )}
+ ) : props.make === "folder" ? ( +
+ {tr( + "fpkg.folder.what", + undefined, + "The folder is named after the image or package, in the output folder. Nothing is compressed, and nothing reaches the PS5.", + )} +
) : (
@@ -120,7 +129,7 @@ export function OptionsCard(props: OptionsCardProps) { />
)} - {props.onLanguage && props.make !== "image" && ( + {props.onLanguage && props.make !== "image" && props.make !== "folder" && (
{ // Queueing still works: a folder of games can be queued as images. expect(button(out, "Add this game to the queue")?.disabled).toBe(false); }); + + it("a folder is only kept, and a folder source cannot become one", () => { + const ok = idle({ make: "folder" }, { canFolder: true }); + expect(options(ok, "After converting")).toEqual(["Keep the folder"]); + expect(button(ok, "Make game folder")?.disabled).toBe(false); + expect(ok).not.toContain("This is already a folder"); + const already = idle({ make: "folder" }, { canFolder: false, isFolderSource: true }); + expect(button(already, "Make game folder")?.disabled).toBe(true); + expect(already).toContain("This is already a folder"); + const archive = idle({ make: "folder" }, { canFolder: false }); + expect(button(archive, "Make game folder")?.disabled).toBe(true); + expect(archive).toContain("Only a game image or a package made with the debug key can become a folder"); + // Nothing is sent anywhere, so no console is asked for. + expect(idle({ make: "folder" }, { canFolder: true, canInstall: false })).not.toContain("Connect to a PS5"); + }); + }); + + it("a finished folder shows its path and Show in folder, nothing to install or delete", () => { + const out = html( + { ...done, mode: "folder", host: null, packagePath: "/out/Game", titleId: null, installMs: 0 }, + { sourceBytes: 1024 }, + ); + expect(out).toContain("Game folder written and read back."); + expect(out).toContain("/out/Game"); + expect(button(out, "Show in folder")).not.toBeNull(); + expect(button(out, "Stream & install")).toBeNull(); + expect(button(out, "Delete package")).toBeNull(); + expect(button(out, "View details")).toBeNull(); + expect(button(out, "Upload to PS5")).toBeNull(); + expect(out).not.toContain("% of"); + }); + + it("names a folder build's stages for a folder", () => { + const out = html({ + phase: "running", + mode: "folder", + source: "/g/Game.exfat", + host: null, + stage: "write", + stageDone: 1, + stageTotal: 2, + startedMs: 0, + stageStartedMs: 0, + stageMs: {}, + jobId: "j", + installTaskId: null, + taskId: null, + packagePath: null, + titleId: null, + copiedSource: null, + extractedSource: null, + }); + expect(out).toContain("Write folder"); + expect(out).toContain("Read the folder back"); + expect(out).not.toContain("Write package"); + expect(button(out, "Cancel")).not.toBeNull(); }); it("lists the stages while running, with Cancel during the build", () => { diff --git a/client/src/screens/FpkgConvert/RunCard.tsx b/client/src/screens/FpkgConvert/RunCard.tsx index dee97b00..4646321f 100644 --- a/client/src/screens/FpkgConvert/RunCard.tsx +++ b/client/src/screens/FpkgConvert/RunCard.tsx @@ -8,7 +8,8 @@ import { CheckCircle2, Circle, ListPlus, Loader2, XCircle } from "lucide-react"; import { Button, Card, Checkbox, ProgressBar, Toggle } from "../../components"; import type { ConvertThen } from "../../state/convertQueue"; -import { makesImage } from "../../state/fpkgConversion"; +import type { MakeKind } from "../../state/convertPrefs"; +import { makesFolder, makesImage } from "../../state/fpkgConversion"; import type { InstallMethod, Pipeline, PipelineStage } from "../../state/fpkgConversion"; import { useTr } from "../../state/lang"; import { formatBytes } from "../../lib/format"; @@ -76,6 +77,10 @@ export interface RunCardProps { onRun: () => void; /** The source is a game folder on this computer, so it can become an image here. */ canImage?: boolean; + /** The source is a game image or a package, so it can become a plain folder. */ + canFolder?: boolean; + /** The source is a folder already: there is nothing to unpack. */ + isFolderSource?: boolean; /** One line saying what the run makes, of which game, where (from ① to ③). */ summary?: string; /** Add the current game to the queue, with the same choice. */ @@ -136,8 +141,13 @@ export function RunCard(props: RunCardProps) { const rows = stageRows(p, installTask); // An image build plans, writes and verifies an image, not a package. const image = p.phase !== "idle" && makesImage(p.mode); + const folder = p.phase !== "idle" && makesFolder(p.mode); const label = (s: PipelineStage) => - image && s === "plan" + folder && s === "write" + ? tr("fpkg.stage.write_folder", undefined, "Write folder") + : folder && s === "verify" + ? tr("fpkg.stage.verify_folder", undefined, "Read the folder back") + : image && s === "plan" ? tr("fpkg.stage.plan_image", undefined, "Plan image") : image && s === "write" ? tr("fpkg.stage.write_image", undefined, "Write image") @@ -173,14 +183,18 @@ export function RunCard(props: RunCardProps) { if (p.phase === "idle") { const c = props.choice; const image = c.make === "image"; + const folder = c.make === "folder"; const packing = image && lz4Active(c.imageFormat, c.lz4); const sends = c.then !== "keep"; - const canRun = props.canConvert !== false && (!image || !!props.canImage); - const runLabel = image - ? c.imageCompress && !packing + const canRun = + props.canConvert !== false && (folder ? !!props.canFolder : !image || !!props.canImage); + const runLabel = folder + ? tr("fpkg.folder.make", undefined, "Make game folder") + : image + ? c.imageCompress && !packing ? tr("fpkg.image.make_compressed", undefined, "Make compressed image") - : tr("fpkg.image.make", undefined, "Make game image") - : !sends + : tr("fpkg.image.make", undefined, "Make game image") + : !sends ? tr("fpkg.convertOnly", undefined, "Convert only") : props.replaces ? tr("fpkg.convertReplace", undefined, "Convert & replace") @@ -219,6 +233,20 @@ export function RunCard(props: RunCardProps) { )}
)} + {folder && props.isFolderSource && ( +
+ {tr("fpkg.folder.already", undefined, "This is already a folder")} +
+ )} + {folder && !props.isFolderSource && !props.canFolder && props.canConvert !== false && ( +
+ {tr( + "fpkg.folder.sourceOnly", + undefined, + "Only a game image or a package made with the debug key can become a folder; unpack an archive first.", + )} +
+ )} {props.replaces && props.canInstall && !image && sends && (
{tr( @@ -228,7 +256,7 @@ export function RunCard(props: RunCardProps) { )}
)} - {!props.canInstall && ( + {!props.canInstall && !folder && (
{tr("fpkg.needConsole", undefined, "Connect to a PS5 to install.")}
@@ -375,11 +403,13 @@ export function RunCard(props: RunCardProps) { undefined, "Game image written and read back. Copy it to a PS5 drive (Upload) and ShadowMount+ will mount it.", ) - : tr("fpkg.done", undefined, "Package written")} + : folder + ? tr("fpkg.folder.done", undefined, "Game folder written and read back.") + : tr("fpkg.done", undefined, "Package written")}
{p.packageBytes > 0 && {prettyBytes(p.packageBytes)}} - {p.packageBytes > 0 && props.sourceBytes ? ( + {p.packageBytes > 0 && props.sourceBytes && !folder ? ( {" "} {tr( @@ -427,7 +457,7 @@ export function RunCard(props: RunCardProps) {
)} - {!p.deleted && !makesImage(p.mode) && installChoice(!installed)} + {!p.deleted && !makesImage(p.mode) && !folder && installChoice(!installed)} {!p.deleted && makesImage(p.mode) && ( {tr("fpkg.showFolder", undefined, "Show in folder")} )} - {!p.deleted && !makesImage(p.mode) && props.onViewPackage && ( + {!p.deleted && !makesImage(p.mode) && !folder && props.onViewPackage && ( )} - {!p.deleted && !makesImage(p.mode) && ( + {!p.deleted && !makesImage(p.mode) && !folder && (