Files
ppsspp/UI/InstallPkgScreen.cpp
T
Henrik RydgårdandClaude Opus 5 3a70c6b149 Install PKG game updates, and boot them
Opening a .pkg now offers to install it, the way a .zip does - see the new
InstallPkgScreen, which shows what the update patches, what it'll take up
on disk (exact, since package contents aren't compressed) and where it's
going. The package's PS3-style USRDIR/CONTENT wrapping is stripped so the
files land where the PSP expects them, in PSP/GAME/<DISC_ID>.

Booting a disc then looks for PSP/GAME/<DISC_ID>/PBOOT.PBP and boots that
instead of the disc's own EBOOT, leaving the disc mounted - so the update
overrides the files it ships and the disc supplies the rest. The update's
DISC_ID has to match; a DISC_VERSION mismatch only warns, since updates do
get used with slightly different dumps in practice.

Verified against the whole corpus: every one installs, and the
digital NP* update/base-image pairs that could be assembled all boot the
patch rather than the disc's executable. That includes Super Robot Taisen
Operation Extend from a real NPUMDIMG EBOOT.PBP, which settles that
ISO.BIN.EDAT does not re-key the PBOOT - a digital title's patched EBOOT is
encrypted exactly like a UMD one. On the UMD side, the patched
LittleBigPlanet reads PATCH.ARC out of the install alongside the disc's own
archive.

The DISC_VERSION warning turns out to be load-bearing: many of the pairs
mismatch, because the dumps in circulation are later disc revisions than the
updates were built against. docs/pkg_notes.md has the numbers, and the one
thing that doesn't work - PGD-wrapped .sprx modules, which
sceKernelLoadModuleNpDrm can't load.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 11:15:15 -06:00

194 lines
6.7 KiB
C++

// 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 <memory>
#include "Common/Data/Text/I18n.h"
#include "Common/Data/Text/Parsers.h"
#include "Common/File/DiskFree.h"
#include "Common/File/FileUtil.h"
#include "Common/StringUtils.h"
#include "Common/System/Request.h"
#include "Common/System/System.h"
#include "Common/UI/Context.h"
#include "Common/UI/UI.h"
#include "Common/UI/View.h"
#include "Common/UI/ViewGroup.h"
#include "Core/Loaders.h"
#include "Core/System.h"
#include "Core/Util/GameManager.h"
#include "Core/Util/PathUtil.h"
#include "UI/InstallPkgScreen.h"
InstallPkgScreen::InstallPkgScreen(const Path &pkgPath)
: UITwoPaneBaseDialogScreen(Path(), TwoPaneFlags::SettingsToTheRight | TwoPaneFlags::ContentsCanScroll), pkgPath_(pkgPath) {
g_GameManager.ResetInstallError();
// Reading the package is cheap - the item table and two small PARAM.SFOs. We only keep the
// info; the install re-opens the file on its own thread.
std::unique_ptr<FileLoader> loader(ConstructFileLoader(pkgPath_));
PkgReader reader;
if (!loader || !reader.Open(loader.get(), &pkgError_)) {
return;
}
pkgInfo_ = reader.Info();
if (!pkgInfo_.isGameUpdate) {
// Everything we can read is a game update; anything else got rejected above with a better
// message than this.
pkgError_ = "This PKG file isn't a PSP game update";
return;
}
canInstall_ = true;
installSize_ = PkgInstalledSize(pkgInfo_);
destination_ = GetSysDirectory(DIRECTORY_GAME) / pkgInfo_.discId;
alreadyInstalled_ = File::Exists(destination_ / "PBOOT.PBP");
int64_t space = 0;
if (free_disk_space(GetSysDirectory(DIRECTORY_GAME), space)) {
freeSpace_ = space;
}
}
std::string_view InstallPkgScreen::GetTitle() const {
auto iz = GetI18NCategory(I18NCat::INSTALLZIP);
return iz->T("Game update");
}
void InstallPkgScreen::CreateSettingsViews(UI::ViewGroup *parent) {
using namespace UI;
auto di = GetI18NCategory(I18NCat::DIALOG);
auto iz = GetI18NCategory(I18NCat::INSTALLZIP);
installChoice_ = nullptr;
if (canInstall_) {
installChoice_ = parent->Add(new Choice(iz->T("Install"), ImageID("I_FOLDER_UPLOAD")));
installChoice_->OnClick.Handle(this, &InstallPkgScreen::OnInstall);
}
if (System_GetPropertyBool(SYSPROP_CAN_SHOW_FILE)) {
parent->Add(new Spacer(12.0f));
parent->Add(new Choice(di->T("Show in folder")))->OnClick.Add([this](UI::EventParams &) {
System_ShowFileInFolder(pkgPath_);
});
}
if (canInstall_) {
parent->Add(new Spacer(12.0f));
parent->Add(new CheckBox(&deletePkgFile_, iz->T("Delete PKG file")));
}
}
void InstallPkgScreen::CreateContentViews(UI::ViewGroup *parent) {
using namespace UI;
auto di = GetI18NCategory(I18NCat::DIALOG);
auto iz = GetI18NCategory(I18NCat::INSTALLZIP);
auto er = GetI18NCategory(I18NCat::ERRORS);
LinearLayout *leftColumn = parent->Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Margins(8))));
if (!canInstall_) {
leftColumn->Add(new TextView(GetFriendlyPath(pkgPath_)));
leftColumn->Add(new NoticeView(NoticeLevel::ERROR, iz->T(pkgError_.empty() ? "This PKG file isn't a PSP game update" : pkgError_), ""));
doneView_ = leftColumn->Add(new NoticeView(NoticeLevel::SUCCESS, "", ""));
doneView_->SetVisibility(Visibility::V_GONE);
return;
}
leftColumn->Add(new TextView(iz->T("Install game update?")))->SetBig(true);
leftColumn->Add(new TextView(pkgPath_.GetFilename()));
if (!pkgInfo_.title.empty()) {
leftColumn->Add(new TextView(pkgInfo_.title));
}
if (!pkgInfo_.pbootTitle.empty()) {
leftColumn->Add(new TextView(pkgInfo_.pbootTitle));
}
leftColumn->Add(new Spacer(8.0f));
// What it patches, and to what. The disc version is the one the update was built against.
leftColumn->Add(new TextView(ApplySafeSubstitutions("%1: %2 (%3)", iz->T("Game"), pkgInfo_.discId, pkgInfo_.discVersion)));
if (!pkgInfo_.appVer.empty()) {
leftColumn->Add(new TextView(ApplySafeSubstitutions("%1: %2", iz->T("Update version"), pkgInfo_.appVer)));
}
if (!pkgInfo_.systemVer.empty()) {
leftColumn->Add(new TextView(ApplySafeSubstitutions("%1: %2", iz->T("Requires firmware"), pkgInfo_.systemVer)));
}
// Package contents aren't compressed, so this is what it'll actually take up.
leftColumn->Add(new TextView(ApplySafeSubstitutions("%1: %2", iz->T("Space needed"), NiceSizeFormat(installSize_))));
if (freeSpace_ >= 0) {
leftColumn->Add(new TextView(ApplySafeSubstitutions("%1: %2", iz->T("Free space"), NiceSizeFormat((u64)freeSpace_))));
}
leftColumn->Add(new Spacer(8.0f));
leftColumn->Add(new TextView(iz->T("Install into folder")));
leftColumn->Add(new TextView(GetFriendlyPath(destination_)))->SetAlign(FLAG_WRAP_TEXT);
doneView_ = leftColumn->Add(new NoticeView(NoticeLevel::SUCCESS, "", ""));
doneView_->SetVisibility(Visibility::V_GONE);
if (freeSpace_ >= 0 && (u64)freeSpace_ < installSize_) {
leftColumn->Add(new NoticeView(NoticeLevel::ERROR, er->T("Not enough free space"), ""));
}
if (alreadyInstalled_) {
leftColumn->Add(new NoticeView(NoticeLevel::WARN, di->T("Confirm Overwrite"), iz->T("An update for this game is already installed")));
}
}
bool InstallPkgScreen::key(const KeyInput &key) {
// Ignore key presses while installing, so the user can't escape mid-write.
if (g_GameManager.GetState() == GameManagerState::IDLE) {
return UIDialogScreen::key(key);
}
return false;
}
void InstallPkgScreen::OnInstall(UI::EventParams &params) {
if (!canInstall_) {
return;
}
if (g_GameManager.InstallPkgOnThread(pkgPath_, deletePkgFile_)) {
installStarted_ = true;
if (installChoice_) {
installChoice_->SetEnabled(false);
}
}
}
void InstallPkgScreen::update() {
auto iz = GetI18NCategory(I18NCat::INSTALLZIP);
using namespace UI;
if (g_GameManager.GetState() == GameManagerState::IDLE && doneView_) {
const std::string err = g_GameManager.GetInstallError();
if (!err.empty()) {
doneView_->SetLevelAndText(NoticeLevel::ERROR, iz->T(err));
doneView_->SetVisibility(Visibility::V_VISIBLE);
} else if (installStarted_) {
doneView_->SetLevelAndText(NoticeLevel::SUCCESS, iz->T("Installed!"));
doneView_->SetVisibility(Visibility::V_VISIBLE);
}
}
UIBaseDialogScreen::update();
}