mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
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) <[email protected]>
This commit is contained in:
1 parent
5089caf2a0
commit
3a70c6b149
18 files changed
+568
-17
No files matched your search
@@ -219,6 +219,8 @@ IdentifiedFileType Identify_File(FileLoader *fileLoader, std::string *errorStrin
|
||||
return IdentifiedFileType::ARCHIVE_RAR;
|
||||
} else if (!memcmp(&id, "\x37\x7A\xBC\xAF", 4)) {
|
||||
return IdentifiedFileType::ARCHIVE_7Z;
|
||||
} else if (!memcmp(&id, "\x7F""PKG", 4)) {
|
||||
return IdentifiedFileType::PSP_PKG;
|
||||
}
|
||||
|
||||
// "~PSP" is an encrypted PRX. The module loader decrypts those on the way in, so as far as
|
||||
@@ -279,6 +281,10 @@ IdentifiedFileType Identify_File(FileLoader *fileLoader, std::string *errorStrin
|
||||
return IdentifiedFileType::ARCHIVE_RAR;
|
||||
} else if (extension == ".7z") {
|
||||
return IdentifiedFileType::ARCHIVE_7Z;
|
||||
} else if (extension == ".pkg") {
|
||||
// Magic didn't match, but the name says what it was meant to be - report it as a PKG so
|
||||
// the install screen can explain what's wrong with it.
|
||||
return IdentifiedFileType::PSP_PKG;
|
||||
}
|
||||
return IdentifiedFileType::UNKNOWN;
|
||||
}
|
||||
@@ -673,6 +679,7 @@ const char *IdentifiedFileTypeToString(IdentifiedFileType type) {
|
||||
case IdentifiedFileType::UNKNOWN_ISO: return "UNKNOWN_ISO";
|
||||
case IdentifiedFileType::ARCHIVE_RAR: return "ARCHIVE_RAR";
|
||||
case IdentifiedFileType::ARCHIVE_ZIP: return "ARCHIVE_ZIP";
|
||||
case IdentifiedFileType::PSP_PKG: return "PSP_PKG";
|
||||
case IdentifiedFileType::ARCHIVE_7Z: return "ARCHIVE_7Z";
|
||||
case IdentifiedFileType::PSP_PS1_PBP: return "PSP_PS1_PBP";
|
||||
case IdentifiedFileType::PSX_ISO: return "PSX_ISO";
|
||||
|
||||
@@ -40,6 +40,8 @@ enum class IdentifiedFileType {
|
||||
ARCHIVE_RAR,
|
||||
ARCHIVE_ZIP,
|
||||
ARCHIVE_7Z,
|
||||
// A .pkg game update - a container we install from rather than boot. See Core/Util/PkgUnpack.h.
|
||||
PSP_PKG,
|
||||
PSP_PS1_PBP,
|
||||
PSX_ISO,
|
||||
PS2_ISO,
|
||||
|
||||
+76
-2
@@ -260,8 +260,82 @@ static const char * const altBootNames[] = {
|
||||
//"disc0:/PSP_GAME/SYSDIR/ss.RAW",//Code Geass: Lost Colors chinese version
|
||||
};
|
||||
|
||||
// A game update installed from a .pkg (see Core/Util/PkgUnpack.h) lands in PSP/GAME/<DISC_ID>/,
|
||||
// with the patched executable as PBOOT.PBP. The PSP 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
|
||||
// everything else.
|
||||
|
||||
static bool ReadPBPParamSFO(const std::string &path, ParamSFOData *sfo) {
|
||||
const int fd = pspFileSystem.OpenFile(path, FILEACCESS_READ);
|
||||
if (fd < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
// A PBP starts with its magic, a version, and eight little-endian subfile offsets. PARAM.SFO
|
||||
// is the first subfile, so it runs from its own offset to ICON0.PNG's.
|
||||
u8 header[0x28];
|
||||
if (pspFileSystem.ReadFile(fd, header, sizeof(header)) == sizeof(header) && !memcmp(header, "\0PBP", 4)) {
|
||||
u32_le sfoOffset, iconOffset;
|
||||
memcpy(&sfoOffset, header + 0x08, sizeof(sfoOffset));
|
||||
memcpy(&iconOffset, header + 0x0C, sizeof(iconOffset));
|
||||
const u32 sfoSize = iconOffset - sfoOffset;
|
||||
if (sfoOffset >= sizeof(header) && iconOffset > sfoOffset && sfoSize <= 64 * 1024) {
|
||||
std::vector<u8> sfoData(sfoSize);
|
||||
if (pspFileSystem.SeekFile(fd, sfoOffset, FILEMOVE_BEGIN) >= 0 &&
|
||||
pspFileSystem.ReadFile(fd, sfoData.data(), sfoSize) == sfoSize) {
|
||||
success = sfo->ReadSFO(sfoData);
|
||||
}
|
||||
}
|
||||
}
|
||||
pspFileSystem.CloseFile(fd);
|
||||
return success;
|
||||
}
|
||||
|
||||
// Returns the path of the update to boot, or an empty string to boot the disc normally.
|
||||
static std::string FindGameUpdatePBOOT(const std::string &discId, const std::string &discVersion) {
|
||||
if (discId.empty()) {
|
||||
return std::string();
|
||||
}
|
||||
const std::string path = "ms0:/PSP/GAME/" + discId + "/PBOOT.PBP";
|
||||
if (!pspFileSystem.GetFileInfo(path).exists) {
|
||||
return std::string();
|
||||
}
|
||||
|
||||
// Check what the update claims to patch before handing it the boot.
|
||||
ParamSFOData sfo;
|
||||
if (!ReadPBPParamSFO(path, &sfo)) {
|
||||
WARN_LOG(Log::Loader, "Ignoring '%s': couldn't read its PARAM.SFO", path.c_str());
|
||||
return std::string();
|
||||
}
|
||||
|
||||
const std::string updateDiscId = sfo.GetValueString("DISC_ID");
|
||||
if (updateDiscId != discId) {
|
||||
WARN_LOG(Log::Loader, "Ignoring '%s': it's an update for %s, not %s", path.c_str(), updateDiscId.c_str(), discId.c_str());
|
||||
return std::string();
|
||||
}
|
||||
|
||||
// The disc version is advisory here. An update is built against one specific revision of a
|
||||
// disc, but refusing to run one on a slightly different dump is a worse failure than letting
|
||||
// the user find out - they went and installed it on purpose.
|
||||
const std::string updateDiscVersion = sfo.GetValueString("DISC_VERSION");
|
||||
if (!updateDiscVersion.empty() && !discVersion.empty() && updateDiscVersion != discVersion) {
|
||||
WARN_LOG(Log::Loader, "Game update '%s' is for disc version %s, but this disc is %s. Booting it anyway.",
|
||||
path.c_str(), updateDiscVersion.c_str(), discVersion.c_str());
|
||||
}
|
||||
|
||||
NOTICE_LOG(Log::Loader, "Booting game update '%s' (app version %s) instead of the disc's executable",
|
||||
path.c_str(), sfo.GetValueString("APP_VER").c_str());
|
||||
return path;
|
||||
}
|
||||
|
||||
bool Load_PSP_ISO(FileLoader *fileLoader, std::string *error_string) {
|
||||
std::string bootpath("disc0:/PSP_GAME/SYSDIR/EBOOT.BIN");
|
||||
const std::string id = g_paramSFO.GetValueString("DISC_ID");
|
||||
|
||||
// An installed game update replaces the disc's executable - see FindGameUpdatePBOOT above.
|
||||
std::string bootpath = FindGameUpdatePBOOT(id, g_paramSFO.GetValueString("DISC_VERSION"));
|
||||
if (bootpath.empty()) {
|
||||
bootpath = "disc0:/PSP_GAME/SYSDIR/EBOOT.BIN";
|
||||
|
||||
// Bypass Chinese translation patches, see comment above.
|
||||
for (size_t i = 0; i < ARRAY_SIZE(altBootNames); i++) {
|
||||
@@ -273,7 +347,6 @@ bool Load_PSP_ISO(FileLoader *fileLoader, std::string *error_string) {
|
||||
}
|
||||
|
||||
// Bypass another more dangerous one where the file is in USRDIR - this could collide with files in some game.
|
||||
std::string id = g_paramSFO.GetValueString("DISC_ID");
|
||||
if (id == "NPJH50624" && pspFileSystem.GetFileInfo("disc0:/PSP_GAME/USRDIR/PAKFILE2.BIN").exists) {
|
||||
bootpath = "disc0:/PSP_GAME/USRDIR/PAKFILE2.BIN";
|
||||
}
|
||||
@@ -299,6 +372,7 @@ bool Load_PSP_ISO(FileLoader *fileLoader, std::string *error_string) {
|
||||
// try unencrypted Boot.BIN
|
||||
bootpath = "disc0:/PSP_GAME/SYSDIR/BOOT.BIN";
|
||||
}
|
||||
}
|
||||
|
||||
// Fail early with a clearer message for some types of ISOs.
|
||||
if (!pspFileSystem.GetFileInfo(bootpath).exists) {
|
||||
|
||||
@@ -255,6 +255,7 @@ static void GetBootError(IdentifiedFileType type, std::string *errorString) {
|
||||
break;
|
||||
|
||||
case IdentifiedFileType::ARCHIVE_7Z: *errorString = "7z file detected (Require 7-Zip)"; break;
|
||||
case IdentifiedFileType::PSP_PKG: *errorString = "PKG game updates need to be installed, not booted."; break;
|
||||
case IdentifiedFileType::PSX_ISO: *errorString = "PSX game image detected."; break;
|
||||
case IdentifiedFileType::PS2_ISO: *errorString = "PS2 game image detected."; break;
|
||||
case IdentifiedFileType::PS3_ISO: *errorString = "PS2 game image detected."; break;
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
#include "Core/FileSystems/ISOFileSystem.h"
|
||||
#include "Core/Util/GameManager.h"
|
||||
#include "Core/Util/PathUtil.h"
|
||||
#include "Core/Util/PkgUnpack.h"
|
||||
#include "Core/Util/RecentFiles.h"
|
||||
#include "Common/Data/Text/I18n.h"
|
||||
|
||||
@@ -902,6 +903,70 @@ bool GameManager::InstallZipOnThread(ZipFileTask task) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Installing a game update from a .pkg. Unlike a zip there's nothing to guess about - the
|
||||
// package says which disc it patches, and that decides the destination folder.
|
||||
void GameManager::InstallPkgContents(Path pkgPath, bool deleteAfter) {
|
||||
SetCurrentThreadName("InstallPkgContents");
|
||||
|
||||
AndroidJNIThreadContext context; // Destructor detaches.
|
||||
|
||||
auto di = GetI18NCategory(I18NCat::DIALOG);
|
||||
auto iz = GetI18NCategory(I18NCat::INSTALLZIP);
|
||||
|
||||
g_OSD.SetProgressBar("install", di->T("Installing..."), 0.0f, 1.0f, 0.0f, 0.1f);
|
||||
|
||||
bool success = false;
|
||||
std::string error;
|
||||
std::unique_ptr<FileLoader> loader(ConstructFileLoader(pkgPath));
|
||||
PkgReader reader;
|
||||
if (!loader || !reader.Open(loader.get(), &error)) {
|
||||
ERROR_LOG(Log::HLE, "PKG install failed: %s", error.c_str());
|
||||
SetInstallError(iz->T("This PKG file isn't a PSP game update"));
|
||||
} else if (!reader.Info().isGameUpdate) {
|
||||
SetInstallError(iz->T("This PKG file isn't a PSP game update"));
|
||||
} else {
|
||||
const Path destination = GetSysDirectory(DIRECTORY_GAME) / reader.Info().discId;
|
||||
success = InstallPkg(reader, destination, [this](float progress) {
|
||||
installProgress_ = progress;
|
||||
auto di = GetI18NCategory(I18NCat::DIALOG);
|
||||
g_OSD.SetProgressBar("install", di->T("Installing..."), 0.0f, 1.0f, installProgress_, 0.1f);
|
||||
}, &error);
|
||||
if (!success) {
|
||||
ERROR_LOG(Log::HLE, "PKG install failed: %s", error.c_str());
|
||||
SetInstallError(iz->T("Failed to install the game update"));
|
||||
}
|
||||
}
|
||||
|
||||
// Close the package before anything tries to delete it.
|
||||
loader.reset();
|
||||
|
||||
if (deleteAfter && success) {
|
||||
if (System_GetPropertyBool(SYSPROP_HAS_TRASH_BIN)) {
|
||||
System_MoveToTrash(pkgPath);
|
||||
} else {
|
||||
File::Delete(pkgPath);
|
||||
}
|
||||
}
|
||||
|
||||
g_OSD.RemoveProgressBar("install", success, 0.5f);
|
||||
installProgress_ = 1.0f;
|
||||
InstallDone();
|
||||
if (success) {
|
||||
ResetInstallError();
|
||||
}
|
||||
}
|
||||
|
||||
bool GameManager::InstallPkgOnThread(const Path &pkgPath, bool deleteAfter) {
|
||||
if (InstallInProgress() || installDonePending_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
installThread_ = std::thread([this, pkgPath, deleteAfter]() {
|
||||
InstallPkgContents(pkgPath, deleteAfter);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GameManager::UninstallGameOnThread(const std::string &name) {
|
||||
if (name.empty()) {
|
||||
ERROR_LOG(Log::HLE, "Cannot uninstall an empty-named game");
|
||||
|
||||
@@ -85,6 +85,10 @@ public:
|
||||
// Only returns false if there's already an installation in progress.
|
||||
bool InstallZipOnThread(ZipFileTask task);
|
||||
|
||||
// Installs a game update from a .pkg into PSP/GAME/<DISC_ID>. Same contract as the above -
|
||||
// only returns false if something else is already installing. See Core/Util/PkgUnpack.h.
|
||||
bool InstallPkgOnThread(const Path &pkgPath, bool deleteAfter);
|
||||
|
||||
// Separate kind of functionality from InstallZipOnThread, so doesn't re-use the task struct.
|
||||
bool UninstallGameOnThread(const std::string &name);
|
||||
|
||||
@@ -94,6 +98,7 @@ public:
|
||||
|
||||
private:
|
||||
void InstallZipContents(ZipFileTask task);
|
||||
void InstallPkgContents(Path pkgPath, bool deleteAfter);
|
||||
bool InstallMemstickZip(const Path &zipFile, const Path &dest, const ZipFileInfo &info);
|
||||
bool InstallZippedISO(struct zip *z, int isoFileIndex, const Path &destDir);
|
||||
void UninstallGame(const std::string &name);
|
||||
|
||||
@@ -103,6 +103,8 @@ list(APPEND UISource
|
||||
InstallUpdateScreen.cpp
|
||||
InstallZipScreen.h
|
||||
InstallZipScreen.cpp
|
||||
InstallPkgScreen.h
|
||||
InstallPkgScreen.cpp
|
||||
JitCompareScreen.h
|
||||
JitCompareScreen.cpp
|
||||
MemStickScreen.h
|
||||
|
||||
+4
-2
@@ -239,6 +239,7 @@ void GameButton::Draw(UIContext &dc) {
|
||||
case IdentifiedFileType::ARCHIVE_ZIP: imageIcon = ImageID("I_ARCHIVE_ZIP"); drawBackground = false; break;
|
||||
case IdentifiedFileType::ARCHIVE_7Z: imageIcon = ImageID("I_ARCHIVE_7Z"); drawBackground = false; break;
|
||||
case IdentifiedFileType::ARCHIVE_RAR: imageIcon = ImageID("I_ARCHIVE_RAR"); drawBackground = false; break;
|
||||
case IdentifiedFileType::PSP_PKG: imageIcon = ImageID("I_FOLDER_UPLOAD"); drawBackground = false; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
@@ -880,11 +881,12 @@ void GameBrowser::Refresh() {
|
||||
}
|
||||
}
|
||||
|
||||
// Put RAR/ZIP files at the end to get them out of the way.
|
||||
// Put RAR/ZIP files at the end to get them out of the way. Game update packages go here
|
||||
// too - they're not something to boot, they're something to install.
|
||||
// We do support unpacking some of them automatically.
|
||||
if (browseFlags_ & BrowseFlags::ARCHIVES) {
|
||||
fileInfo.clear();
|
||||
path_.GetListing(fileInfo, "zip:rar:r00:r01:7z:");
|
||||
path_.GetListing(fileInfo, "zip:rar:r00:r01:7z:pkg:");
|
||||
if (!fileInfo.empty()) {
|
||||
for (size_t i = 0; i < fileInfo.size(); i++) {
|
||||
if (!fileInfo[i].isDirectory) {
|
||||
|
||||
@@ -153,6 +153,7 @@ bool GameInfo::Delete() {
|
||||
case IdentifiedFileType::ARCHIVE_RAR:
|
||||
case IdentifiedFileType::ARCHIVE_ZIP:
|
||||
case IdentifiedFileType::ARCHIVE_7Z:
|
||||
case IdentifiedFileType::PSP_PKG:
|
||||
case IdentifiedFileType::UNKNOWN:
|
||||
case IdentifiedFileType::PSP_UMD_VIDEO_ISO:
|
||||
case IdentifiedFileType::PPSSPP_GE_DUMP:
|
||||
@@ -918,6 +919,7 @@ handleELF:
|
||||
}
|
||||
|
||||
case IdentifiedFileType::ARCHIVE_ZIP:
|
||||
case IdentifiedFileType::PSP_PKG:
|
||||
info_->SetTitle(info_->GetFilePath().GetFilename());
|
||||
info_->icon.dataLoaded = true;
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
// 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 ¶ms) {
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// 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 "Common/File/Path.h"
|
||||
#include "Common/UI/Notice.h"
|
||||
#include "Common/UI/UIScreen.h"
|
||||
#include "Common/UI/View.h"
|
||||
|
||||
#include "Core/Util/PkgUnpack.h"
|
||||
#include "UI/BaseScreens.h"
|
||||
#include "UI/SimpleDialogScreen.h"
|
||||
|
||||
// Offers to install a PSP game update from a .pkg file, the way InstallZipScreen does for zips.
|
||||
// The package goes into PSP/GAME/<DISC_ID>, and from then on booting that disc runs the update -
|
||||
// see FindGameUpdatePBOOT() in Core/PSPLoaders.cpp.
|
||||
class InstallPkgScreen : public UITwoPaneBaseDialogScreen {
|
||||
public:
|
||||
InstallPkgScreen(const Path &pkgPath);
|
||||
|
||||
void update() override;
|
||||
bool key(const KeyInput &key) override;
|
||||
|
||||
const char *tag() const override { return "InstallPkg"; }
|
||||
|
||||
protected:
|
||||
void CreateSettingsViews(UI::ViewGroup *parent) override;
|
||||
void CreateContentViews(UI::ViewGroup *parent) override;
|
||||
std::string_view GetTitle() const override;
|
||||
ViewLayoutMode LayoutMode() const override {
|
||||
return ViewLayoutMode::ApplyInsets;
|
||||
}
|
||||
|
||||
private:
|
||||
void OnInstall(UI::EventParams ¶ms);
|
||||
|
||||
Path pkgPath_;
|
||||
Path destination_;
|
||||
|
||||
PkgInfo pkgInfo_;
|
||||
std::string pkgError_; // Why the package can't be installed, if it can't.
|
||||
bool canInstall_ = false;
|
||||
u64 installSize_ = 0;
|
||||
s64 freeSpace_ = -1; // Negative if we couldn't find out.
|
||||
bool alreadyInstalled_ = false;
|
||||
|
||||
UI::Choice *installChoice_ = nullptr;
|
||||
NoticeView *doneView_ = nullptr;
|
||||
bool installStarted_ = false;
|
||||
bool deletePkgFile_ = false;
|
||||
};
|
||||
@@ -51,6 +51,7 @@
|
||||
#include "UI/SavedataScreen.h"
|
||||
#include "UI/InstallUpdateScreen.h"
|
||||
#include "UI/InstallZipScreen.h"
|
||||
#include "UI/InstallPkgScreen.h"
|
||||
#include "UI/Background.h"
|
||||
#include "UI/GameBrowser.h"
|
||||
#include "Core/Config.h"
|
||||
@@ -67,6 +68,9 @@ static void LaunchFile(ScreenManager *screenManager, Screen *currentScreen, cons
|
||||
if (extension == ".zip" || extension == ".7z") {
|
||||
// If is a zip file, we have a screen for that.
|
||||
screenManager->push(new InstallZipScreen(path));
|
||||
} else if (extension == ".pkg") {
|
||||
// A game update package - not something to boot, something to install.
|
||||
screenManager->push(new InstallPkgScreen(path));
|
||||
} else {
|
||||
// Check if we already know that this game isn't playable.
|
||||
// If coming from the main screen, the info will already be computed here since the icon is displayed etc.
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
<ClCompile Include="TiltAnalogSettingsScreen.cpp" />
|
||||
<ClCompile Include="TouchControlLayoutScreen.cpp" />
|
||||
<ClCompile Include="TouchControlVisibilityScreen.cpp" />
|
||||
<ClCompile Include="InstallPkgScreen.cpp" />
|
||||
<ClCompile Include="InstallUpdateScreen.cpp" />
|
||||
<ClCompile Include="InstallZipScreen.cpp" />
|
||||
<ClCompile Include="Theme.cpp" />
|
||||
@@ -137,6 +138,7 @@
|
||||
<ClInclude Include="TiltAnalogSettingsScreen.h" />
|
||||
<ClInclude Include="TouchControlLayoutScreen.h" />
|
||||
<ClInclude Include="TouchControlVisibilityScreen.h" />
|
||||
<ClInclude Include="InstallPkgScreen.h" />
|
||||
<ClInclude Include="InstallUpdateScreen.h" />
|
||||
<ClInclude Include="InstallZipScreen.h" />
|
||||
<ClInclude Include="Theme.h" />
|
||||
|
||||
@@ -40,6 +40,9 @@
|
||||
<ClCompile Include="TiltAnalogSettingsScreen.cpp">
|
||||
<Filter>Screens</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="InstallPkgScreen.cpp">
|
||||
<Filter>Screens</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="InstallUpdateScreen.cpp">
|
||||
<Filter>Screens</Filter>
|
||||
</ClCompile>
|
||||
@@ -194,6 +197,9 @@
|
||||
<ClInclude Include="TiltAnalogSettingsScreen.h">
|
||||
<Filter>Screens</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="InstallPkgScreen.h">
|
||||
<Filter>Screens</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="InstallUpdateScreen.h">
|
||||
<Filter>Screens</Filter>
|
||||
</ClInclude>
|
||||
|
||||
@@ -114,6 +114,7 @@
|
||||
<ClInclude Include="..\..\UI\ImDebugger\ImJitViewer.h" />
|
||||
<ClInclude Include="..\..\UI\ImDebugger\ImMemView.h" />
|
||||
<ClInclude Include="..\..\UI\ImDebugger\ImStructViewer.h" />
|
||||
<ClInclude Include="..\..\UI\InstallPkgScreen.h" />
|
||||
<ClInclude Include="..\..\UI\InstallUpdateScreen.h" />
|
||||
<ClInclude Include="..\..\UI\InstallZipScreen.h" />
|
||||
<ClInclude Include="..\..\UI\JitCompareScreen.h" />
|
||||
@@ -173,6 +174,7 @@
|
||||
<ClCompile Include="..\..\UI\ImDebugger\ImJitViewer.cpp" />
|
||||
<ClCompile Include="..\..\UI\ImDebugger\ImMemView.cpp" />
|
||||
<ClCompile Include="..\..\UI\ImDebugger\ImStructViewer.cpp" />
|
||||
<ClCompile Include="..\..\UI\InstallPkgScreen.cpp" />
|
||||
<ClCompile Include="..\..\UI\InstallUpdateScreen.cpp" />
|
||||
<ClCompile Include="..\..\UI\InstallZipScreen.cpp" />
|
||||
<ClCompile Include="..\..\UI\JitCompareScreen.cpp" />
|
||||
|
||||
@@ -73,6 +73,9 @@
|
||||
<ClCompile Include="..\..\UI\IAPScreen.cpp">
|
||||
<Filter>Screens</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\UI\InstallPkgScreen.cpp">
|
||||
<Filter>Screens</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\UI\InstallUpdateScreen.cpp">
|
||||
<Filter>Screens</Filter>
|
||||
</ClCompile>
|
||||
@@ -219,6 +222,9 @@
|
||||
<ClInclude Include="..\..\UI\IAPScreen.h">
|
||||
<Filter>Screens</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\..\UI\InstallPkgScreen.h">
|
||||
<Filter>Screens</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\..\UI\InstallUpdateScreen.h">
|
||||
<Filter>Screens</Filter>
|
||||
</ClInclude>
|
||||
|
||||
@@ -989,6 +989,7 @@ LOCAL_SRC_FILES := \
|
||||
$(SRC)/UI/BaseScreens.cpp \
|
||||
$(SRC)/UI/Background.cpp \
|
||||
$(SRC)/UI/CwCheatScreen.cpp \
|
||||
$(SRC)/UI/InstallPkgScreen.cpp \
|
||||
$(SRC)/UI/InstallUpdateScreen.cpp \
|
||||
$(SRC)/UI/InstallZipScreen.cpp \
|
||||
$(SRC)/UI/JitCompareScreen.cpp \
|
||||
|
||||
+124
-13
@@ -6,8 +6,9 @@ EBOOT (`PBOOT.PBP`) plus whatever data files the patch replaces, and installing
|
||||
`ms0:/PSP/GAME/<DISC_ID>/`. The patched EBOOT then runs with the original UMD (or the original
|
||||
PSN game) still supplying everything it doesn't override.
|
||||
|
||||
`Tools/pkg.py` is a working parser/extractor. PPSSPP itself cannot yet read or use these - see
|
||||
"What PPSSPP is missing" at the end.
|
||||
This document is what was learned decoding some update packages on 2026-08-21. `Tools/pkg.py` is a
|
||||
working parser/extractor built from it; PPSSPP reads and installs them itself now - see "How PPSSPP
|
||||
handles them" and the verification sections at the end.
|
||||
|
||||
Format reference: <https://www.psdevwiki.com/ps3/PKG_files>, cross-checked against
|
||||
[pkg2zip](https://github.com/mmozeiko/pkg2zip).
|
||||
@@ -164,17 +165,127 @@ recognises `USRDIR/CONTENT/EBOOT.PBP` (a full PSN game, which it converts to an
|
||||
`PSP-KEY.EDAT` and `CONTENT.DAT`, and `continue`s past everything else - `PBOOT.PBP` and every
|
||||
patch file are silently dropped, with no warning that anything was skipped.
|
||||
|
||||
## What PPSSPP is missing
|
||||
## How PPSSPP handles them
|
||||
|
||||
`grep -r PBOOT` over the tree returns nothing, so both halves are unwritten:
|
||||
Three pieces, added 2026-08-21:
|
||||
|
||||
1. **Reading and installing a `.pkg`** - unpack it into `ms0:/PSP/GAME/<DISC_ID>/`, next to the
|
||||
existing ZIP/ISO install paths in `Core/Util/GameManager.cpp`.
|
||||
2. **Booting the patch** - when starting a game, notice
|
||||
`ms0:/PSP/GAME/<DISC_ID>/PBOOT.PBP` whose inner SFO matches the disc's `DISC_ID` and
|
||||
`DISC_VERSION`, and boot that instead with the ISO mounted on `disc0:`. The mounting already
|
||||
exists (see `--mount` above); what's missing is the decision.
|
||||
- **`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
|
||||
version; `InstallPkg()` writes the payload out. Sits next to `PSARUnpack.cpp`, and like it needs
|
||||
nothing but the AES already in `ext/libkirk`.
|
||||
- **`UI/InstallPkgScreen.cpp`** is what opening a `.pkg` gets you, the same way a `.zip` gets
|
||||
`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.
|
||||
- **`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.
|
||||
|
||||
Only the UMD half of this is proven. The digital NP\* titles patch a PSN `EBOOT.PBP` /
|
||||
NPUMDIMG rather than a UMD, and `ISO.BIN.EDAT` most likely re-keys it - that path has not been
|
||||
tested.
|
||||
`PPSSPPHeadless --install-pkg=DIR <file.pkg>` does an install without the UI, which is how the
|
||||
above got tested. It prints what the package is and installs into DIR exactly (the app picks
|
||||
`PSP/GAME/<DISC_ID>` itself).
|
||||
|
||||
### Install layout
|
||||
|
||||
The package's own PS3-style wrapping is stripped: `USRDIR/CONTENT/<x>` and `USRDIR/<x>` both become
|
||||
`<x>` in the game folder, and the `USRDIR` and `USRDIR/CONTENT` directory entries are dropped
|
||||
rather than created. The root-level files - `PARAM.SFO`, `PS3LOGO.DAT`, `ICON0.PNG`, `PIC0.PNG`,
|
||||
`PIC2.PNG` - are store metadata and are **not** installed. Writing that `PARAM.SFO` in particular
|
||||
would be actively wrong: a folder holding a `PARAM.SFO` and no `EBOOT.PBP` is what PPSSPP
|
||||
identifies as *save data*, so the update would show up in the savedata list.
|
||||
|
||||
### Which updates get used
|
||||
|
||||
The update's `DISC_ID` has to match the disc's, or it's ignored with a warning - that's what keeps
|
||||
an update from being applied to the wrong game.
|
||||
|
||||
`DISC_VERSION` is advisory. An update is built against one specific disc revision, and PPSSPP logs
|
||||
a warning when they differ, but still boots it: refusing outright is a worse failure mode than
|
||||
letting the user find out, since they installed it deliberately. This is not what a real PSP does.
|
||||
It comes up in practice - the LittleBigPlanet v2.05 update targets disc version 1.00, and the
|
||||
common European dump is 1.01, and it works.
|
||||
|
||||
There's no setting to turn this off. Anyone who has a `PBOOT.PBP` sitting in a game folder either
|
||||
installed it here or copied it off a real memory stick, and in both cases booting it is what they
|
||||
were after.
|
||||
|
||||
### Verified
|
||||
|
||||
- All packages parse and install through the C++ path, byte-identical to `Tools/pkg.py` - each
|
||||
one reports its disc ID and versions and writes its payload without an error.
|
||||
- Hatsune Miku Project DIVA 2nd (ULJM05681): install the v1.01 update, boot the UMD, PPSSPP boots
|
||||
`PBOOT.PBP` and the game reads its CPKs off `disc0:`.
|
||||
- LittleBigPlanet (UCES01264): install the v2.05 update, boot the v1.01 UMD, and the patched game
|
||||
opens `ms0:/PSP/GAME/UCES01264/PATCH.ARC` alongside the disc's own `lbp_archive.arc` - the update
|
||||
is actually in use, not just booted.
|
||||
- `python3 test.py -g --graphics=software`: no failures, so restructuring the disc boot
|
||||
path didn't disturb anything.
|
||||
- Truncated and item-table-corrupted packages are refused with a message rather than crashing.
|
||||
Corruption *inside* file data still installs - nothing here verifies the package CMAC, same as
|
||||
every other PKG tool.
|
||||
- The browser listing, install screen and install itself were checked by hand in the app.
|
||||
|
||||
## Digital (NP\*) titles
|
||||
|
||||
About half of the packages patch a digital title rather than a UMD, and that half is tested too.
|
||||
|
||||
The one that settles the question is **Super Robot Taisen Operation Extend (NPJH50521)**, because
|
||||
it's a real NPUMDIMG `EBOOT.PBP` rather than a decrypted ISO dump - `NPDRM: PSAR ID: 4d55504e`,
|
||||
mounted on `disc0:` by the NPDRM block device, with the game's own 560 MB EBOOT sitting in the same
|
||||
folder as the update. All eight of its update revisions were installed and booted in turn, and each
|
||||
one loads a distinguishably different executable:
|
||||
|
||||
```
|
||||
disc executable .text 0x419b1c
|
||||
v1.01 0x41fb8c v1.02 0x4233dc v1.03 0x42952c v1.04 0x42a53c
|
||||
v1.05 0x42a61c v1.06 0x42a59c v1.07 0x42b48c v1.08 0x42b7dc
|
||||
```
|
||||
|
||||
So **`ISO.BIN.EDAT` does not re-key the PBOOT**, which was the open worry: a digital title's patched
|
||||
executable is encrypted exactly like a UMD one, and needs nothing PPSSPP doesn't already have.
|
||||
|
||||
**`DISC_VERSION` being advisory matters far more than expected.** Most of the pairs hit a mismatch,
|
||||
because the dumps in circulation are later disc revisions than the updates were built against.
|
||||
Refusing outright would make most of them unusable. Elminage Original was the one clean
|
||||
exact-match case, disc 1.01 against an update for 1.01, and it boots without a warning.
|
||||
|
||||
### Known limitation: PGD-wrapped `.sprx` modules don't load
|
||||
|
||||
Package payloads are full of `\0PSPEDAT` files. That's fine for
|
||||
*data*: `sceNpDrmEdataSetupKey()` in `Core/HLE/scePspNpDrm_user.cpp` wraps an open file descriptor
|
||||
with the `0x04100002`/`0x04100001` ioctl pair, and the game reads plaintext.
|
||||
|
||||
A few packages ship `\0PSPEDAT` **executables** - `.sprx` modules - and that path is not
|
||||
implemented. God Eater 2 (NPJH50832, 45 of them) installs cleanly, boots its `PBOOT.PBP`, and then
|
||||
loops forever on:
|
||||
|
||||
```
|
||||
E Loader: SCE_KERNEL_ERROR_UNSUPPORTED_PRX_TYPE=sceKernelLoadModuleNpDrm(
|
||||
ms0:/PSP/GAME/NPJH50832/system.sprx, 00000000, 00000000): failed to load
|
||||
E sceModule: Wrong magic number 50535000
|
||||
```
|
||||
|
||||
Shiren 4 Plus (NPJH50698, one `.sprx` holding the whole game behind a 62 KB loader) fails
|
||||
identically. Tales of the World Radiant Mythology 3 (NPJH50353, two) doesn't reach its modules
|
||||
within 100 seconds of headless boot, so it gets past startup - it presumably hits the same wall
|
||||
whenever it does load them.
|
||||
|
||||
`sceKernelLoadModuleNpDrm()` in `Core/HLE/sceKernelModule.cpp` is a one-line forward to
|
||||
`sceKernelLoadModule()`, which sees something that isn't ELF magic and gives up. Fixing it means
|
||||
unwrapping the EDAT before handing the bytes to the ELF loader, using the licensee key the game has
|
||||
already set via `sceNpDrmSetLicenseeKey` - the same material the data path uses.
|
||||
|
||||
This is a pre-existing emulator gap rather than something the installer gets wrong: until now
|
||||
nothing put such a file on the memory stick, so it was unreachable. The unpatched God Eater 2 never
|
||||
calls `sceKernelLoadModuleNpDrm` at all - the update is what introduces the modules - so for that
|
||||
title the update is currently better not installed, since the unpatched game boots. It's also why a
|
||||
"God Eater 2 DLC Update v1.40 *Decrypted for PPSSPP Emulator*" folder circulates: the `PBOOT.PBP` in
|
||||
it is byte-for-byte what `InstallPkg()` writes, and the only difference is that its `.sprx` files
|
||||
have been unwrapped to plain ELF by hand.
|
||||
|
||||
### Still not tested
|
||||
|
||||
- Nothing here plays past a title screen. The runs are 25-second headless boots, so "the update is
|
||||
in use" means the patched executable is what loaded and ran - not that a patched *asset* was read.
|
||||
LittleBigPlanet's `PATCH.ARC` covers that for a UMD title; there's no equivalent observation for a
|
||||
digital one yet.
|
||||
Reference in new issue
Block a user