diff --git a/AGENTS.md b/AGENTS.md index 98d13d5de4..4a761f7892 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -184,6 +184,11 @@ For string sanitation, we already have SanitizeString in StringUtils.cpp - add n ## Translated UI strings (assets/lang) +**When implementing new UI, translations come last, in their own commit after everything else is +done.** Write the English strings, get the feature built and working, commit that - then stop and ask +the user to check the English wording before translating anything. The English string is what all ~47 +languages get derived from, so rewording it afterwards means redoing the whole sweep. + One .ini file per language, keyed by section and key against `assets/lang/en_US.ini`. **Don't hand-edit the ~47 files**, and don't run the AI commands in `Tools/langtool` either - you can read the call site, which its fixed prompt can't. Do the translating yourself and let the tool do the file surgery. Run it diff --git a/Core/Util/PSARUnpack.cpp b/Core/Util/PSARUnpack.cpp index c0cf887409..ca3b27299d 100644 --- a/Core/Util/PSARUnpack.cpp +++ b/Core/Util/PSARUnpack.cpp @@ -489,6 +489,9 @@ public: PSARCompression entryCompression() const { return entryCompression_; } // Empty for a directory, or for an entry we couldn't decompress. const std::vector &entryData() const { return entryData_; } + // How far into the archive the next record starts, and where the records stop - i.e. progress. + u32 position() const { return pos_; } + size_t limit() const { return limit_; } private: // Decrypts one record into 'out'. Returns the decrypted size, or <= 0 on failure. @@ -726,6 +729,10 @@ bool UnpackPSAR(const u8 *psar, size_t psarSize, const Path &outputDir, const PS stats->entries++; stats->compressionCounts[(int)reader.entryCompression()]++; + if (options.progress && reader.limit() > 0) { + options.progress(std::min(1.0f, (float)reader.position() / (float)reader.limit())); + } + if (options.verbose) { INFO_LOG(Log::Loader, "PSAR entry '%s' (%s, %d bytes)", reader.entryName().c_str(), PSARCompressionToString(reader.entryCompression()), (int)reader.entryData().size()); diff --git a/Core/Util/PSARUnpack.h b/Core/Util/PSARUnpack.h index f604298170..620932c5d9 100644 --- a/Core/Util/PSARUnpack.h +++ b/Core/Util/PSARUnpack.h @@ -18,6 +18,7 @@ #pragma once #include +#include #include #include #include @@ -81,6 +82,9 @@ struct PSARUnpackOptions { bool listOnly = false; // Log a line per entry. Off by default - an updater holds well over a thousand of them. bool verbose = false; + // Called on the unpacking thread after every entry, with how far through the archive we are + // (0.0 to 1.0). Unpacking a full firmware takes a while, so a UI wants this. + std::function progress; }; struct PSARUnpackStats { diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 6ad1a9ac52..660e29392d 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -99,6 +99,8 @@ list(APPEND UISource UploadScreen.cpp CwCheatScreen.h CwCheatScreen.cpp + InstallUpdateScreen.h + InstallUpdateScreen.cpp InstallZipScreen.h InstallZipScreen.cpp JitCompareScreen.h diff --git a/UI/InstallUpdateScreen.cpp b/UI/InstallUpdateScreen.cpp new file mode 100644 index 0000000000..46c20454cd --- /dev/null +++ b/UI/InstallUpdateScreen.cpp @@ -0,0 +1,200 @@ +// Copyright (c) 2026- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#include "Common/Data/Text/I18n.h" +#include "Common/Data/Text/Parsers.h" +#include "Common/File/DirListing.h" +#include "Common/File/FileUtil.h" +#include "Common/Log.h" +#include "Common/StringUtils.h" +#include "Common/Thread/Promise.h" +#include "Common/Thread/ThreadManager.h" +#include "Common/UI/UI.h" +#include "Common/UI/View.h" +#include "Common/UI/ViewGroup.h" + +#include "Core/Config.h" +#include "Core/ConfigValues.h" +#include "Core/Util/PathUtil.h" + +#include "UI/InstallUpdateScreen.h" +#include "UI/MiscViews.h" + +// An updater carries one file list per hardware revision, and anything the chosen model's list +// doesn't name isn't part of its firmware. Unpacking the model we claim to be keeps flash0 +// consistent with what the emulator reports to games. +static PSPModelGeneration EmulatedModelGeneration() { + return g_Config.iPSPModel == PSP_MODEL_FAT ? PSPModelGeneration::PSP_1000 : PSPModelGeneration::PSP_2000; +} + +InstallUpdateScreen::InstallUpdateScreen(const Path &path, std::string_view title) + : UISimpleBaseDialogScreen(Path(), SimpleDialogFlags::ContentsCanScroll), path_(path), title_(title) { + destination_ = GetSysDirectory(DIRECTORY_NAND); + + File::FileInfo fileInfo; + if (File::GetFileInfo(path_, &fileInfo)) { + fileSize_ = fileInfo.size; + } + // There's no practical way to merge two firmwares, so an install replaces whatever is there. + overwrites_ = File::Exists(destination_ / "flash0"); +} + +std::string_view InstallUpdateScreen::GetTitle() const { + auto iz = GetI18NCategory(I18NCat::INSTALLZIP); + return iz->T("PSP firmware update"); +} + +void InstallUpdateScreen::CreateDialogViews(UI::ViewGroup *parent) { + using namespace UI; + + auto di = GetI18NCategory(I18NCat::DIALOG); + auto iz = GetI18NCategory(I18NCat::INSTALLZIP); + auto st = GetI18NCategory(I18NCat::STORE); // Borrow "Size" from here, like GameScreen does. + + LinearLayout *container = parent->Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(600, WRAP_CONTENT, 0.0f, UI::Gravity::G_HCENTER, Margins(10)))); + + container->Add(new TextView(iz->T("Install PSP firmware update?"), ALIGN_LEFT, false))->SetBig(true); + container->Add(new Spacer(8.0f)); + + if (!title_.empty()) { + // The updater's own title, which spells out the firmware version. + container->Add(new TextWithImage(ImageID("I_INFO"), title_)); + } + container->Add(new TextWithImage(ImageID("I_FILE"), GetFriendlyPath(path_))); + if (fileSize_ > 0) { + container->Add(new TextView(StringFromFormat("%s: %s", st->T_cstr("Size"), NiceSizeFormat(fileSize_).c_str()))); + } + + container->Add(new Spacer(12.0f)); + container->Add(new TextView(iz->T("Install into folder"))); + container->Add(new TextView(GetFriendlyPath(destination_)))->SetAlign(FLAG_WRAP_TEXT); + + if (overwrites_) { + container->Add(new NoticeView(NoticeLevel::WARN, di->T("Confirm Overwrite"), "")); + } + + container->Add(new Spacer(12.0f)); + + installChoice_ = container->Add(new Choice(iz->T("Install"), ImageID("I_FOLDER_UPLOAD"))); + installChoice_->OnClick.Add([this](UI::EventParams &e) { + StartInstall(); + }); + + progressBar_ = container->Add(new ProgressBar()); + progressBar_->SetVisibility(V_GONE); + + resultView_ = container->Add(new NoticeView(NoticeLevel::SUCCESS, "", "")); + resultView_->SetVisibility(V_GONE); + + // The screen can get recreated mid-install (a rotation, say), so pick the status back up. + RefreshStatus(); +} + +void InstallUpdateScreen::StartInstall() { + if (state_ && !state_->done) { + // Already running. A previous attempt that failed can be retried, though. + return; + } + + state_ = std::make_shared(); + reportedDone_ = false; + + PSARUnpackOptions options; + options.model = EmulatedModelGeneration(); + + INFO_LOG(Log::Loader, "Unpacking the updater %s into %s (model %s)", path_.c_str(), + destination_.c_str(), PSPModelGenerationToString(options.model)); + + g_threadManager.EnqueueTask(new IndependentTask(TaskType::IO_BLOCKING, TaskPriority::NORMAL, + [state = state_, path = path_, destination = destination_, options]() mutable { + options.progress = [state](float progress) { + state->progress = progress; + }; + state->success = UnpackUpdater(path, destination, options, &state->stats, &state->error); + if (state->success && state->stats.written == 0) { + // Nothing came out, so the archive had no file list for the model we asked for - + // old firmwares predate the later models. Not something to call a success. + state->success = false; + if (state->error.empty()) { + state->error = "The updater has no firmware for this PSP model"; + } + } + // Everything above is published by this store - see the atomic in InstallState. + state->done = true; + })); + + RefreshStatus(); +} + +void InstallUpdateScreen::RefreshStatus() { + using namespace UI; + + auto iz = GetI18NCategory(I18NCat::INSTALLZIP); + + const bool installing = state_ && !state_->done; + const bool succeeded = state_ && state_->done && state_->success; + + if (installChoice_) { + // There's no point installing the same firmware twice, but a failure can be retried. + installChoice_->SetEnabled(!installing && !succeeded); + } + if (progressBar_) { + progressBar_->SetVisibility(installing ? V_VISIBLE : V_GONE); + if (installing) { + progressBar_->SetProgress(state_->progress); + } + } + if (resultView_) { + if (!state_ || !state_->done) { + resultView_->SetVisibility(V_GONE); + } else if (state_->success) { + resultView_->SetLevelAndText(NoticeLevel::SUCCESS, iz->T("Installed!")); + resultView_->SetDetailsText(StringFromFormat("%s - %d files", state_->stats.firmwareVersion.c_str(), state_->stats.written)); + resultView_->SetVisibility(V_VISIBLE); + } else { + resultView_->SetLevelAndText(NoticeLevel::ERROR, iz->T("Installation failed")); + resultView_->SetDetailsText(state_->error); + resultView_->SetVisibility(V_VISIBLE); + } + } +} + +bool InstallUpdateScreen::key(const KeyInput &key) { + // Ignore key presses while installing, so the user can't escape out mid-write. + if (state_ && !state_->done) { + return false; + } + return UISimpleBaseDialogScreen::key(key); +} + +void InstallUpdateScreen::update() { + UISimpleBaseDialogScreen::update(); + + if (!state_) { + return; + } + if (state_->done && !reportedDone_) { + reportedDone_ = true; + if (state_->success) { + INFO_LOG(Log::Loader, "Installed firmware %s: %d files, %d failed", state_->stats.firmwareVersion.c_str(), + state_->stats.written, state_->stats.failed); + } else { + ERROR_LOG(Log::Loader, "Failed to install the updater: %s", state_->error.c_str()); + } + } + RefreshStatus(); +} diff --git a/UI/InstallUpdateScreen.h b/UI/InstallUpdateScreen.h new file mode 100644 index 0000000000..e4fe1c5b5a --- /dev/null +++ b/UI/InstallUpdateScreen.h @@ -0,0 +1,80 @@ +// Copyright (c) 2026- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#pragma once + +#include +#include +#include +#include + +#include "Common/File/Path.h" +#include "Common/UI/Notice.h" +#include "Common/UI/UIScreen.h" +#include "Common/UI/View.h" +#include "Common/UI/ViewGroup.h" + +#include "Core/Util/PSARUnpack.h" + +#include "UI/BaseScreens.h" +#include "UI/SimpleDialogScreen.h" + +// An official PSP firmware updater (a PSP/GAME/UPDATE/EBOOT.PBP, or the folder holding one). +// Running it isn't going to get anyone anywhere, but the firmware inside it is exactly what the +// emulated flash0/flash1 want, so offer to unpack it into the NAND directory instead. +class InstallUpdateScreen : public UISimpleBaseDialogScreen { +public: + // title is the updater's SFO title, which already carries the version ("PSP Update ver 6.61"). + InstallUpdateScreen(const Path &path, std::string_view title); + + void CreateDialogViews(UI::ViewGroup *parent) override; + void update() override; + bool key(const KeyInput &key) override; + + const char *tag() const override { return "InstallUpdate"; } + +protected: + std::string_view GetTitle() const override; + +private: + // The unpack runs on a worker thread, and the user can leave the screen while it's going, so + // the state it writes is shared with the task rather than owned by the screen. + struct InstallState { + std::atomic progress{}; + std::atomic done{}; + // Only valid to read once done is set. + bool success = false; + PSARUnpackStats stats; + std::string error; + }; + + void StartInstall(); + void RefreshStatus(); + + Path path_; + Path destination_; + std::string title_; + u64 fileSize_ = 0; + bool overwrites_ = false; + + std::shared_ptr state_; + bool reportedDone_ = false; + + UI::Choice *installChoice_ = nullptr; + UI::ProgressBar *progressBar_ = nullptr; + NoticeView *resultView_ = nullptr; +}; diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index a034919bd8..550d33c7df 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -49,6 +49,7 @@ #include "UI/RemoteISOScreen.h" #include "UI/DisplayLayoutScreen.h" #include "UI/SavedataScreen.h" +#include "UI/InstallUpdateScreen.h" #include "UI/InstallZipScreen.h" #include "UI/Background.h" #include "UI/GameBrowser.h" @@ -68,7 +69,9 @@ static void LaunchFile(ScreenManager *screenManager, Screen *currentScreen, cons screenManager->push(new InstallZipScreen(path)); } else { // Check if we already know that this game isn't playable. - auto info = g_gameInfoCache->GetInfo(nullptr, path, GameInfoFlags::FILE_TYPE); + // If coming from the main screen, the info will already be computed here since the icon is displayed etc. + // Otherwise, we probably technically should wait for it... + auto info = g_gameInfoCache->GetInfo(nullptr, path, GameInfoFlags::FILE_TYPE | GameInfoFlags::PARAM_SFO); switch (info->fileType) { case IdentifiedFileType::PSP_UMD_VIDEO_ISO: @@ -82,6 +85,20 @@ static void LaunchFile(ScreenManager *screenManager, Screen *currentScreen, cons screenManager->push(new SavedataPopupScreen(Path(), path, title)); return; } + case IdentifiedFileType::PSP_PBP: + case IdentifiedFileType::PSP_PBP_DIRECTORY: + { + // Check if it's an update file. If so, we'll offer to install it directly, + // instead of running it (which currently will not work). + if (info->id == "MSTKUPDATE") { + std::string title = info->GetTitle(); // includes the version. + // The unpacker wants the PBP itself, not the folder it happens to sit in. + const Path pbpPath = info->fileType == IdentifiedFileType::PSP_PBP ? path : path / "EBOOT.PBP"; + screenManager->push(new InstallUpdateScreen(pbpPath, title)); + return; + } + break; + } default: break; } diff --git a/UI/UI.vcxproj b/UI/UI.vcxproj index 5db23dac28..1c89ea0cd5 100644 --- a/UI/UI.vcxproj +++ b/UI/UI.vcxproj @@ -79,6 +79,7 @@ + @@ -136,6 +137,7 @@ + diff --git a/UI/UI.vcxproj.filters b/UI/UI.vcxproj.filters index 72f784c375..d48a5f6306 100644 --- a/UI/UI.vcxproj.filters +++ b/UI/UI.vcxproj.filters @@ -40,6 +40,9 @@ Screens + + Screens + Screens @@ -191,6 +194,9 @@ Screens + + Screens + Screens diff --git a/UWP/UI_UWP/UI_UWP.vcxproj b/UWP/UI_UWP/UI_UWP.vcxproj index 6d1e967e2b..32b48346fa 100644 --- a/UWP/UI_UWP/UI_UWP.vcxproj +++ b/UWP/UI_UWP/UI_UWP.vcxproj @@ -114,6 +114,7 @@ + @@ -172,6 +173,7 @@ + diff --git a/UWP/UI_UWP/UI_UWP.vcxproj.filters b/UWP/UI_UWP/UI_UWP.vcxproj.filters index 53b77646ca..4d0d433135 100644 --- a/UWP/UI_UWP/UI_UWP.vcxproj.filters +++ b/UWP/UI_UWP/UI_UWP.vcxproj.filters @@ -73,6 +73,9 @@ Screens + + Screens + Screens @@ -216,6 +219,9 @@ Screens + + Screens + Screens diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 02f45dcd91..be75a70f5f 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -984,6 +984,7 @@ LOCAL_SRC_FILES := \ $(SRC)/UI/BaseScreens.cpp \ $(SRC)/UI/Background.cpp \ $(SRC)/UI/CwCheatScreen.cpp \ + $(SRC)/UI/InstallUpdateScreen.cpp \ $(SRC)/UI/InstallZipScreen.cpp \ $(SRC)/UI/JitCompareScreen.cpp \ $(SRC)/UI/OnScreenDisplay.cpp \