Show and remove installed game updates from the game info screen

An installed update silently replaces what the game boots, so the info
pane now says when there is one - version, size and where it lives - and
the context menu offers to remove it again.

Removing takes the whole PSP/GAME/<DISC_ID> folder when the update is all
that's in it. When a digital game shares the folder, only PBOOT.PBP goes,
since deleting the folder would take the game with it and nothing records
what the install wrote. The confirmation names the exact path either way.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_018izZ1mGTWhz2RqeudqsDQR
This commit is contained in:
Henrik RydgårdandClaude Opus 5 committed 2026-09-08 11:15:15 -06:00
1 parent 3a70c6b149
commit f0dafdee10
5 files changed
+153 -2

No files matched your search

+53
View File
@@ -17,13 +17,18 @@
#include <algorithm>
#include <cstring>
#include <memory>
#include "Common/File/FileUtil.h"
#include "Common/File/Path.h"
#include "Common/Log.h"
#include "Common/StringUtils.h"
#include "Common/System/Request.h"
#include "Common/System/System.h"
#include "Core/ELF/ParamSFO.h"
#include "Core/ELF/PBPReader.h"
#include "Core/Loaders.h"
#include "Core/System.h"
#include "Core/Util/PkgUnpack.h"
extern "C" {
@@ -496,3 +501,51 @@ bool InstallPkg(PkgReader &reader, const Path &destDir, const std::function<void
}
return true;
}
bool FindInstalledGameUpdate(std::string_view discId, InstalledGameUpdate *update) {
if (discId.empty()) {
return false;
}
const Path folder = GetSysDirectory(DIRECTORY_GAME) / std::string(discId);
const Path pbootPath = folder / "PBOOT.PBP";
if (!File::Exists(pbootPath)) {
return false;
}
// The version we want is in the PBOOT's own PARAM.SFO, which isn't encrypted.
std::unique_ptr<FileLoader> loader(ConstructFileLoader(pbootPath));
if (!loader) {
return false;
}
PBPReader pbp(loader.get());
std::vector<u8> sfoData;
ParamSFOData sfo;
if (!pbp.IsValid() || !pbp.GetSubFile(PBP_PARAM_SFO, &sfoData) || !sfo.ReadSFO(sfoData)) {
WARN_LOG(Log::Loader, "'%s' doesn't look like a game update", pbootPath.c_str());
return false;
}
update->folder = folder;
update->pbootPath = pbootPath;
update->appVer = sfo.GetValueString("APP_VER");
update->discVersion = sfo.GetValueString("DISC_VERSION");
update->title = sfo.GetValueString("PBOOT_TITLE");
update->sharesFolderWithGame = File::Exists(folder / "EBOOT.PBP");
update->sizeOnDisk = update->sharesFolderWithGame
? (u64)std::max<s64>(0, File::GetFileSize(pbootPath))
: File::ComputeRecursiveDirectorySize(folder);
return true;
}
bool DeleteInstalledGameUpdate(const InstalledGameUpdate &update) {
const bool useTrash = System_GetPropertyBool(SYSPROP_HAS_TRASH_BIN);
// Only the PBOOT when the folder is a game in its own right - see the struct's comment.
const Path target = update.sharesFolderWithGame ? update.pbootPath : update.folder;
INFO_LOG(Log::Loader, "Removing game update '%s'", target.c_str());
if (useTrash) {
// TODO: No way to tell whether this succeeded.
System_MoveToTrash(target);
return true;
}
return update.sharesFolderWithGame ? File::Delete(target) : File::DeleteDirRecursively(target);
}
+23 -1
View File
@@ -22,9 +22,9 @@
#include <vector>
#include "Common/CommonTypes.h"
#include "Common/File/Path.h"
class FileLoader;
class Path;
// Reads .PKG files - the NPDRM container Sony distributed downloadable content in. We're only
// interested in one flavor: PSP *game updates*, which hold a patched EBOOT (PBOOT.PBP) plus the
@@ -106,3 +106,25 @@ u64 PkgInstalledSize(const PkgInfo &info);
// Unpacks the installable items into destDir, which should be the game folder itself
// (PSP/GAME/<DISC_ID>). progress is called with 0..1 as it goes, and may be null.
bool InstallPkg(PkgReader &reader, const Path &destDir, const std::function<void(float)> &progress, std::string *error);
// An update that has been installed, i.e. what's left over in PSP/GAME/<DISC_ID> afterwards.
struct InstalledGameUpdate {
Path folder;
Path pbootPath;
std::string appVer; // The update's version, from PBOOT.PBP's own PARAM.SFO.
std::string discVersion; // The disc version it was built against.
std::string title; // PBOOT_TITLE, when the update names itself ("Update 2.01").
u64 sizeOnDisk = 0; // How much deleting it would actually free.
// True when a game shares the folder - a digital title, whose EBOOT.PBP sits right next to the
// update. Deleting the whole folder would take the game with it.
bool sharesFolderWithGame = false;
};
// Looks for an update installed for discId. Cheap enough to call while building a UI - a stat and
// a small read, plus a directory walk for the size.
bool FindInstalledGameUpdate(std::string_view discId, InstalledGameUpdate *update);
// Removes an installed update, to the trash where there is one. Takes the whole folder when the
// update is all that's in it, and only PBOOT.PBP when a game shares the folder - that stops the
// update from being used, and it's the only part we can still identify after the fact.
bool DeleteInstalledGameUpdate(const InstalledGameUpdate &update);
+62
View File
@@ -262,6 +262,15 @@ static bool FileTypeIsPlayable(IdentifiedFileType fileType) {
}
}
void GameScreen::RefreshInstalledUpdate() {
hasInstalledUpdate_ = false;
// Homebrew reuses real disc IDs often enough that we'd get false positives.
if (isHomebrew_ || !(knownFlags_ & GameInfoFlags::PARAM_SFO)) {
return;
}
hasInstalledUpdate_ = FindInstalledGameUpdate(info_->id, &installedUpdate_);
}
void GameScreen::CreateContentViews(UI::ViewGroup *parent) {
if (!info_) {
// Shouldn't happen
@@ -505,6 +514,24 @@ void GameScreen::CreateContentViews(UI::ViewGroup *parent) {
}
}
// An installed game update replaces the disc's executable, so it's worth saying so here -
// otherwise there's nothing in the UI to explain why a patched game is running.
RefreshInstalledUpdate();
if (hasInstalledUpdate_) {
infoLayout->Add(new ItemHeader(ga->T("Game update")));
std::string updateLine = installedUpdate_.title;
if (!installedUpdate_.appVer.empty()) {
const std::string version = ApplySafeSubstitutions(ga->T("Version %1"), installedUpdate_.appVer);
updateLine = updateLine.empty() ? version : updateLine + " - " + version;
}
if (updateLine.empty()) {
updateLine = ga->T("Installed");
}
updateLine += " - " + NiceSizeFormat(installedUpdate_.sizeOnDisk);
infoLayout->Add(new TextView(updateLine, ALIGN_LEFT, true))->SetBullet(true);
infoLayout->Add(new TextView(GetFriendlyPath(installedUpdate_.folder), ALIGN_LEFT | FLAG_WRAP_TEXT, true))->SetBullet(true);
}
// Show plugin info_, if any. Later might add checkboxes.
auto plugins = HLEPlugins::FindPlugins(info_->id, g_Config.sLanguageIni);
if (!plugins.empty()) {
@@ -625,6 +652,12 @@ void GameScreen::CreateContextMenu(UI::ViewGroup *parent) {
});
}
RefreshInstalledUpdate();
if (!inGame_ && hasInstalledUpdate_) {
Choice *btnDeleteUpdate = parent->Add(new Choice(ga->T("Delete Game Update"), ImageID("I_TRASHCAN")));
btnDeleteUpdate->OnClick.Handle(this, &GameScreen::OnDeleteGameUpdate);
}
// Don't want to be able to delete the game while it's running.
if (!inGame_) {
Choice *deleteChoice = parent->Add(new Choice(ga->T("Delete Game"), ImageID("I_WARNING")));
@@ -632,6 +665,35 @@ void GameScreen::CreateContextMenu(UI::ViewGroup *parent) {
}
}
void GameScreen::OnDeleteGameUpdate(UI::EventParams &e) {
if (!hasInstalledUpdate_) {
return;
}
auto di = GetI18NCategory(I18NCat::DIALOG);
auto ga = GetI18NCategory(I18NCat::GAME);
std::string prompt(ga->T("DeleteConfirmGameUpdate", "Do you really want to remove the installed update?\nThe game will go back to running the version on the disc."));
prompt += "\n\n";
// Say exactly what disappears - for a digital game the folder holds the game itself, so only
// the update's executable goes.
prompt += GetFriendlyPath(installedUpdate_.sharesFolderWithGame ? installedUpdate_.pbootPath : installedUpdate_.folder);
const bool trashAvailable = System_GetPropertyBool(SYSPROP_HAS_TRASH_BIN);
const InstalledGameUpdate update = installedUpdate_;
screenManager()->push(
new UI::MessagePopupScreen(ga->T("Delete Game Update"), prompt, trashAvailable ? di->T("Move to trash") : di->T("Delete"), di->T("Cancel"),
[this, update](bool yes) {
if (!yes) {
return;
}
if (!DeleteInstalledGameUpdate(update)) {
auto er = GetI18NCategory(I18NCat::ERRORS);
g_OSD.Show(OSDType::MESSAGE_ERROR, er->T("Failed to delete the game update"));
}
RecreateViews();
}));
}
void GameScreen::OnCreateConfig(UI::EventParams &e) {
if (!info_->Ready(GameInfoFlags::PARAM_SFO)) {
return;
+10
View File
@@ -23,6 +23,7 @@
#include "Common/UI/UIScreen.h"
#include "Common/File/Path.h"
#include "UI/GameInfoCache.h"
#include "Core/Util/PkgUnpack.h"
#include "UI/SimpleDialogScreen.h"
@@ -58,6 +59,11 @@ private:
void OnCreateConfig(UI::EventParams &e);
void OnDeleteConfig(UI::EventParams &e);
void OnSetBackground(UI::EventParams &e);
void OnDeleteGameUpdate(UI::EventParams &e);
// Checks whether a game update is installed for this game. Both the info pane and the context
// menu need the answer, and either can be built first.
void RefreshInstalledUpdate();
std::string CRC32string;
@@ -69,6 +75,10 @@ private:
bool knownHasCRC_ = false;
// A game update installed in PSP/GAME/<DISC_ID>, which is what actually runs when there is one.
InstalledGameUpdate installedUpdate_;
bool hasInstalledUpdate_ = false;
std::shared_ptr<GameInfo> info_;
mutable std::string titleCache_;
};
+5 -1
View File
@@ -167,7 +167,7 @@ patch file are silently dropped, with no warning that anything was skipped.
## How PPSSPP handles them
Three pieces, added 2026-08-21:
Four pieces:
- **`Core/Util/PkgUnpack.cpp`** reads a package: header, item table, both PARAM.SFOs, and the
decryption. `PkgReader::Open()` gives you a `PkgInfo` with the disc ID, disc version and patch
@@ -177,6 +177,10 @@ Three pieces, added 2026-08-21:
`InstallZipScreen` - it shows what the update patches, what it'll take up on disk, and where it's
going. The size is exact rather than an estimate: package contents aren't compressed, so summing
the item table is the answer. `GameManager::InstallPkgOnThread()` does the work.
- **`GameScreen`** shows an installed update in the info pane, and offers "Delete Game Update" in
its context menu. Deleting takes the whole `PSP/GAME/<DISC_ID>` folder when the update is all
that's in it, and only `PBOOT.PBP` when a digital game shares the folder - nothing records what
an install wrote, so the executable is the only part still identifiable afterwards.
- **`FindGameUpdatePBOOT()` in `Core/PSPLoaders.cpp`** is the boot-time half. Starting a disc looks
for `ms0:/PSP/GAME/<DISC_ID>/PBOOT.PBP`, and boots that instead of `disc0:/PSP_GAME/SYSDIR/EBOOT.BIN`
if it's there, leaving the disc mounted.