From bfe6debcfd23152bb45353baba3c23dbdbdcb69f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 14 Apr 2026 11:55:13 -0600 Subject: [PATCH] Support auto-installing plugins from ZIPs with a deeper directory structure --- Common/UI/Notice.h | 4 ++++ Core/Loaders.cpp | 50 ++++++++++++++++++++++----------------- Core/Util/GameManager.cpp | 6 ++--- Core/Util/GameManager.h | 2 +- UI/InstallZipScreen.cpp | 20 +++++++++------- UI/InstallZipScreen.h | 3 ++- 6 files changed, 49 insertions(+), 36 deletions(-) diff --git a/Common/UI/Notice.h b/Common/UI/Notice.h index 1d8c96eeca..9ff4218154 100644 --- a/Common/UI/Notice.h +++ b/Common/UI/Notice.h @@ -30,6 +30,10 @@ public: void SetLevel(NoticeLevel level) { level_ = level; } + void SetLevelAndText(NoticeLevel level, std::string_view text) { + level_ = level; + text_ = text; + } void SetSquishy(bool squishy) { squishy_ = squishy; } diff --git a/Core/Loaders.cpp b/Core/Loaders.cpp index 5b5ab932e1..e3cd37f9f9 100644 --- a/Core/Loaders.cpp +++ b/Core/Loaders.cpp @@ -509,21 +509,22 @@ void DetectZipFileContents(zip_t *z, ZipFileInfo *info) { totalFileSize += stat.size; std::string fileName(fn); - std::string zippedName = fileName; // actually, lowercase-name - std::transform(zippedName.begin(), zippedName.end(), zippedName.begin(), - [](unsigned char c) { return asciitolower(c); }); // Not using std::tolower to avoid Turkish I->ı conversion. - // Ignore macos metadata stuff - if (startsWith(zippedName, "__macosx/")) { - continue; - } - if (endsWith(zippedName, "/")) { + if (endsWith(fileName, "/")) { // A directory. Not all zips bother including these. continue; } + std::string lowerCaseName = fileName; // actually, lowercase-name + std::transform(lowerCaseName.begin(), lowerCaseName.end(), lowerCaseName.begin(), + [](unsigned char c) { return asciitolower(c); }); // Not using std::tolower to avoid Turkish I->ı conversion. + // Ignore macos metadata stuff + if (startsWith(lowerCaseName, "__macosx/")) { + continue; + } + int prevSlashLocation = -1; - int slashCount = countSlashes(zippedName, &prevSlashLocation); - if (zippedName.find("eboot.pbp") != std::string::npos) { + int slashCount = countSlashes(lowerCaseName, &prevSlashLocation); + if (lowerCaseName.find("eboot.pbp") != std::string::npos) { if (slashCount >= 1 && (!isPSPMemstickGame || prevSlashLocation < stripChars + 1)) { stripChars = prevSlashLocation + 1; isPSPMemstickGame = true; @@ -531,11 +532,11 @@ void DetectZipFileContents(zip_t *z, ZipFileInfo *info) { INFO_LOG(Log::HLE, "Wrong number of slashes (%i) in '%s'", slashCount, fn); } // TODO: Extract icon and param.sfo from the pbp to be able to display it on the install screen. - } else if (endsWith(zippedName, ".iso") || endsWith(zippedName, ".cso") || endsWith(zippedName, ".chd")) { + } else if (endsWith(lowerCaseName, ".iso") || endsWith(lowerCaseName, ".cso") || endsWith(lowerCaseName, ".chd")) { if (slashCount <= 1) { // We only do this if the ISO file is in the root or one level down. isZippedISO = true; - INFO_LOG(Log::HLE, "ISO found in zip: %s", zippedName.c_str()); + INFO_LOG(Log::HLE, "ISO found in zip: %s", lowerCaseName.c_str()); if (info->isoFileIndex != -1) { INFO_LOG(Log::HLE, "More than one ISO file found in zip. Ignoring additional ones."); } else { @@ -543,27 +544,27 @@ void DetectZipFileContents(zip_t *z, ZipFileInfo *info) { info->contentName = fn; } } - } else if (zippedName.find("textures.ini") != std::string::npos) { - int slashLocation = (int)zippedName.find_last_of('/'); + } else if (lowerCaseName.find("textures.ini") != std::string::npos) { + int slashLocation = (int)lowerCaseName.find_last_of('/'); if (stripCharsTexturePack == -1 || slashLocation < stripCharsTexturePack + 1) { stripCharsTexturePack = slashLocation + 1; isTexturePack = true; info->textureIniIndex = i; } - } else if (endsWith(zippedName, ".ppdmp")) { + } else if (endsWith(lowerCaseName, ".ppdmp")) { isFrameDump = true; info->isoFileIndex = i; info->contentName = fn; - } else if (endsWith(zippedName, ".ppst")) { - int slashLocation = (int)zippedName.find_last_of('/'); + } else if (endsWith(lowerCaseName, ".ppst")) { + int slashLocation = (int)lowerCaseName.find_last_of('/'); if (stripChars == 0 || slashLocation < stripChars + 1) { stripChars = slashLocation + 1; } isSaveStates = true; info->gameTitle = fn; - } else if (endsWith(zippedName, "psp_game/sysdir/eboot.bin") || endsWith(zippedName, "psp_game/sysdir/boot.bin")) { + } else if (endsWith(lowerCaseName, "psp_game/sysdir/eboot.bin") || endsWith(lowerCaseName, "psp_game/sysdir/boot.bin")) { isExtractedISO = true; - } else if (endsWith(zippedName, "/param.sfo")) { + } else if (endsWith(lowerCaseName, "/param.sfo")) { // Get the game name so we can display it. std::string paramSFOContents; if (ZipExtractFileToMemory(z, i, ¶mSFOContents)) { @@ -581,13 +582,18 @@ void DetectZipFileContents(zip_t *z, ZipFileInfo *info) { } } } - } else if (endsWith(zippedName, "/icon0.png")) { + } else if (endsWith(lowerCaseName, "/icon0.png")) { hasIcon0PNG = true; - } else if (endsWith(zippedName, "/plugin.ini") && slashCount == 1) { + } else if (endsWith(lowerCaseName, "/plugin.ini") && slashCount >= 1) { + int slashLocation = (int)lowerCaseName.find_last_of('/'); + // Find previous slash to determine the root of the plugin, so we can display it properly. + int prevSlashLocation = (int)lowerCaseName.find_last_of('/', slashLocation - 1); + _dbg_assert_(prevSlashLocation != std::string::npos); + stripChars = prevSlashLocation + 1; hasPluginIni = true; ZipExtractFileToMemory(z, i, &info->iniContents); info->contentName = fileName.substr(0, fileName.find_last_of('/')); - } else if (endsWith(zippedName, ".prx") && slashCount == 1) { + } else if (endsWith(lowerCaseName, ".prx")) { hasPRX = true; } if (slashCount == 0) { diff --git a/Core/Util/GameManager.cpp b/Core/Util/GameManager.cpp index fd82a4f547..696250683e 100644 --- a/Core/Util/GameManager.cpp +++ b/Core/Util/GameManager.cpp @@ -175,7 +175,7 @@ void GameManager::Update() { } } -bool ZipCanExtractWithoutOverwrite(struct zip *z, const Path &destination, int maxOkFiles) { +bool ZipCanExtractWithoutOverwrite(struct zip *z, const Path &destination, int stripChars, int maxOkFiles) { int numFiles = zip_get_num_files(z); if (numFiles > maxOkFiles && maxOkFiles >= 0) { // Ignore the check, just assume we can't. @@ -260,7 +260,7 @@ void GameManager::InstallZipContents(ZipFileTask task) { } // Check for 7z. We don't support very many scenarios here yet, but we do support ISO install. - if (task.zipFileInfo->archiveType == ArchiveType::SevenZ) { + if (task.zipFileInfo && task.zipFileInfo->archiveType == ArchiveType::SevenZ) { Path destPath = task.destination; g_OSD.SetProgressBar("install", di->T("Installing..."), 0.0f, 0.0f, 0.0f, 0.1f); Path fn(task.zipFileInfo->isoFilename); @@ -662,7 +662,7 @@ bool GameManager::ExtractZipContents(struct zip *z, const Path &dest, const ZipF bool isDir = zippedName.empty() || zippedName.back() == '/'; if (!isDir && zippedName.find('/') != std::string::npos) { - outFilename = dest / zippedName.substr(0, zippedName.rfind('/')); + outFilename = dest / zippedName.substr(info.stripChars, zippedName.rfind('/') - info.stripChars); } else if (!isDir) { outFilename = dest; } diff --git a/Core/Util/GameManager.h b/Core/Util/GameManager.h index 665d112d3d..5b17e1c5fb 100644 --- a/Core/Util/GameManager.h +++ b/Core/Util/GameManager.h @@ -119,4 +119,4 @@ private: extern GameManager g_GameManager; -bool ZipCanExtractWithoutOverwrite(struct zip *z, const Path &destination, int maxOkFiles); +bool ZipCanExtractWithoutOverwrite(struct zip *z, const Path &destination, int stripChars, int maxOkFiles); diff --git a/UI/InstallZipScreen.cpp b/UI/InstallZipScreen.cpp index 2a1aba02c5..038ad52137 100644 --- a/UI/InstallZipScreen.cpp +++ b/UI/InstallZipScreen.cpp @@ -165,8 +165,6 @@ void InstallZipScreen::CreateContentViews(UI::ViewGroup *parent) { leftColumn->Add(new TextView(zipFileInfo_.contentName)); } - doneView_ = leftColumn->Add(new TextView("")); - if (zipFileInfo_.contents == ZipFileContents::ISO_FILE) { const bool isInDownloads = File::IsProbablyInDownloadsFolder(zipPath_); Path parent; @@ -205,7 +203,7 @@ void InstallZipScreen::CreateContentViews(UI::ViewGroup *parent) { leftColumn->Add(new TextView(GetFriendlyPath(zipPath_))); Path pluginsDir = GetSysDirectory(DIRECTORY_PLUGINS); ZipContainer zipFile = ZipOpenPath(zipPath_); - overwrite = !ZipCanExtractWithoutOverwrite(zipFile, pluginsDir, 50); + overwrite = !ZipCanExtractWithoutOverwrite(zipFile, pluginsDir, zipFileInfo_.stripChars, 50); ZipClose(zipFile); std::stringstream sstream(zipFileInfo_.iniContents); IniFile ini; @@ -236,7 +234,7 @@ void InstallZipScreen::CreateContentViews(UI::ViewGroup *parent) { Path savestateDir = GetSysDirectory(DIRECTORY_SAVESTATE); ZipContainer zipFile = ZipOpenPath(zipPath_); - overwrite = !ZipCanExtractWithoutOverwrite(zipFile, savestateDir, 50); + overwrite = !ZipCanExtractWithoutOverwrite(zipFile, savestateDir, zipFileInfo_.stripChars, 50); ZipClose(zipFile); destFolders_.push_back(savestateDir); @@ -253,7 +251,7 @@ void InstallZipScreen::CreateContentViews(UI::ViewGroup *parent) { Path savedataDir = GetSysDirectory(DIRECTORY_SAVEDATA); ZipContainer zipFile = ZipOpenPath(zipPath_); - overwrite = !ZipCanExtractWithoutOverwrite(zipFile, savedataDir, 50); + overwrite = !ZipCanExtractWithoutOverwrite(zipFile, savedataDir, zipFileInfo_.stripChars, 50); ZipClose(zipFile); destFolders_.push_back(savedataDir); @@ -307,7 +305,8 @@ void InstallZipScreen::CreateContentViews(UI::ViewGroup *parent) { break; } - doneView_ = leftColumn->Add(new TextView("")); + doneView_ = leftColumn->Add(new NoticeView(NoticeLevel::SUCCESS, "", "")); + doneView_->SetVisibility(Visibility::V_GONE); if (destFolders_.size() > 1) { leftColumn->Add(new TextView(iz->T("Install into folder"))); @@ -374,11 +373,14 @@ void InstallZipScreen::update() { } std::string err = g_GameManager.GetInstallError(); if (!err.empty()) { - if (doneView_) - doneView_->SetText(iz->T(err)); + if (doneView_) { + doneView_->SetLevelAndText(NoticeLevel::ERROR, iz->T(err)); + doneView_->SetVisibility(Visibility::V_VISIBLE); + } } else if (installStarted_) { if (doneView_) { - doneView_->SetText(iz->T("Installed!")); + doneView_->SetLevelAndText(NoticeLevel::SUCCESS, iz->T("Installed!")); + doneView_->SetVisibility(Visibility::V_VISIBLE); } if (playChoice_) { // Encourage the user to back out and play directly from the main screen, diff --git a/UI/InstallZipScreen.h b/UI/InstallZipScreen.h index ac1dfb32b8..29c2955976 100644 --- a/UI/InstallZipScreen.h +++ b/UI/InstallZipScreen.h @@ -24,6 +24,7 @@ #include "UI/BaseScreens.h" #include "UI/SimpleDialogScreen.h" +#include "Common/UI/Notice.h" class SavedataView; @@ -49,7 +50,7 @@ private: UI::Choice *installChoice_ = nullptr; UI::Choice *playChoice_ = nullptr; UI::Choice *backChoice_ = nullptr; - UI::TextView *doneView_ = nullptr; + NoticeView *doneView_ = nullptr; SavedataView *existingSaveView_ = nullptr; Path savedataToOverwrite_; Path zipPath_;