From 68d411918dc5a483578d8077bd15e6127b2b18fe Mon Sep 17 00:00:00 2001 From: Megamouse Date: Sat, 7 Nov 2020 23:01:40 +0100 Subject: [PATCH] Read patchsets from compat db --- rpcs3/Crypto/unpkg.cpp | 248 +++++++++++++++------------ rpcs3/Crypto/unpkg.h | 8 +- rpcs3/rpcs3qt/game_compatibility.cpp | 66 ++++++- rpcs3/rpcs3qt/game_compatibility.h | 91 ++++++++-- rpcs3/rpcs3qt/game_list_frame.h | 6 +- rpcs3/rpcs3qt/main_window.cpp | 58 ++++++- 6 files changed, 343 insertions(+), 134 deletions(-) diff --git a/rpcs3/Crypto/unpkg.cpp b/rpcs3/Crypto/unpkg.cpp index 354e210deb..a422fc0655 100644 --- a/rpcs3/Crypto/unpkg.cpp +++ b/rpcs3/Crypto/unpkg.cpp @@ -30,6 +30,18 @@ package_reader::package_reader(const std::string& path) } m_is_valid = read_metadata(); + + if (!m_is_valid) + { + return; + } + + const bool param_sfo_found = read_param_sfo(); + + if (!param_sfo_found) + { + pkg_log.notice("PKG does not contain a PARAM.SFO"); + } } package_reader::~package_reader() @@ -476,12 +488,11 @@ bool package_reader::decrypt_data() return true; } -// TODO: maybe also check if VERSION matches -package_error package_reader::check_target_app_version() +bool package_reader::read_param_sfo() { if (!decrypt_data()) { - return package_error::other; + return false; } std::vector entries(header.file_count); @@ -500,7 +511,7 @@ package_error package_reader::check_target_app_version() decrypt(entry.name_offset, entry.name_size, is_psp ? PKG_AES_KEY2 : dec_key.data()); - const std::string name{ reinterpret_cast(buf.get()), entry.name_size }; + const std::string name{reinterpret_cast(buf.get()), entry.name_size}; // We're looking for the PARAM.SFO file, if there is any if (name != "PARAM.SFO") @@ -518,130 +529,143 @@ package_error package_reader::check_target_app_version() if (decrypt(entry.file_offset + pos, block_size, is_psp ? PKG_AES_KEY2 : dec_key.data()) != block_size) { pkg_log.error("Failed to decrypt PARAM.SFO file"); - return package_error::other; + return false; } if (tmp.write(buf.get(), block_size) != block_size) { pkg_log.error("Failed to write to temporary PARAM.SFO file"); - return package_error::other; + return false; } } tmp.seek(0); - const auto psf = psf::load_object(tmp); + m_psf = psf::load_object(tmp); - const auto category = psf::get_string(psf, "CATEGORY", ""); - const auto title_id = psf::get_string(psf, "TITLE_ID", ""); - const auto app_ver = psf::get_string(psf, "APP_VER", ""); - const auto target_app_ver = psf::get_string(psf, "TARGET_APP_VER", ""); - - if (category != "GD") - { - // We allow anything that isn't an update for now - return package_error::no_error; - } - - if (title_id.empty()) - { - // Let's allow packages without ID for now - return package_error::no_error; - } - - if (app_ver.empty()) - { - if (!target_app_ver.empty()) - { - // Let's see if this case exists - pkg_log.fatal("Trying to install an unversioned patch with a target app version (%s). Please contact a developer!", target_app_ver); - } - - // This is probably not a version dependant patch, so we may install the package - return package_error::no_error; - } - - const fs::file installed_sfo_file(Emu.GetHddDir() + "game/" + std::string(title_id) + "/PARAM.SFO"); - if (!installed_sfo_file) - { - if (!target_app_ver.empty()) - { - // We are unable to compare anything with the target app version - pkg_log.error("A target app version is required (%s), but no PARAM.SFO was found for %s", target_app_ver, title_id); - return package_error::app_version; - } - - // There is nothing we need to compare, so we may install the package - return package_error::no_error; - } - - const auto installed_psf = psf::load_object(installed_sfo_file); - - const auto installed_title_id = psf::get_string(installed_psf, "TITLE_ID", ""); - const auto installed_app_ver = psf::get_string(installed_psf, "APP_VER", ""); - - if (title_id != installed_title_id || installed_app_ver.empty()) - { - // Let's allow this package for now - return package_error::no_error; - } - - std::add_pointer_t ev0, ev1; - const double old_version = std::strtod(installed_app_ver.data(), &ev0); - - if (installed_app_ver.data() + installed_app_ver.size() != ev0) - { - pkg_log.error("Failed to convert the installed app version to double (%s)", installed_app_ver); - return package_error::other; - } - - if (target_app_ver.empty()) - { - // This is most likely the first patch. Let's make sure its version is high enough for the installed game. - - const double new_version = std::strtod(app_ver.data(), &ev1); - - if (app_ver.data() + app_ver.size() != ev1) - { - pkg_log.error("Failed to convert the package's app version to double (%s)", app_ver); - return package_error::other; - } - - if (new_version >= old_version) - { - // Yay! The patch has a higher or equal version than the installed game. - return package_error::no_error; - } - - pkg_log.error("The new app version (%s) is smaller than the installed app version (%s)", app_ver, installed_app_ver); - return package_error::app_version; - } - - // Check if the installed app version matches the target app version - - const double target_version = std::strtod(target_app_ver.data(), &ev1); - - if (target_app_ver.data() + target_app_ver.size() != ev1) - { - pkg_log.error("Failed to convert the package's target app version to double (%s)", target_app_ver); - return package_error::other; - } - - if (target_version == old_version) - { - // Yay! This patch is for the installed game version. - return package_error::no_error; - } - - pkg_log.error("The installed app version (%s) does not match the target app version (%s)", installed_app_ver, target_app_ver); - return package_error::app_version; + return true; } + else + { + pkg_log.error("Failed to create temporary PARAM.SFO file"); + return false; + } + } - pkg_log.error("Failed to create temporary PARAM.SFO file"); + return false; +} + +// TODO: maybe also check if VERSION matches +package_error package_reader::check_target_app_version() +{ + if (!m_is_valid) + { return package_error::other; } - return package_error::no_error; + const auto category = psf::get_string(m_psf, "CATEGORY", ""); + const auto title_id = psf::get_string(m_psf, "TITLE_ID", ""); + const auto app_ver = psf::get_string(m_psf, "APP_VER", ""); + const auto target_app_ver = psf::get_string(m_psf, "TARGET_APP_VER", ""); + + if (category != "GD") + { + // We allow anything that isn't an update for now + return package_error::no_error; + } + + if (title_id.empty()) + { + // Let's allow packages without ID for now + return package_error::no_error; + } + + if (app_ver.empty()) + { + if (!target_app_ver.empty()) + { + // Let's see if this case exists + pkg_log.fatal("Trying to install an unversioned patch with a target app version (%s). Please contact a developer!", target_app_ver); + } + + // This is probably not a version dependant patch, so we may install the package + return package_error::no_error; + } + + const fs::file installed_sfo_file(Emu.GetHddDir() + "game/" + std::string(title_id) + "/PARAM.SFO"); + if (!installed_sfo_file) + { + if (!target_app_ver.empty()) + { + // We are unable to compare anything with the target app version + pkg_log.error("A target app version is required (%s), but no PARAM.SFO was found for %s", target_app_ver, title_id); + return package_error::app_version; + } + + // There is nothing we need to compare, so we may install the package + return package_error::no_error; + } + + const auto installed_psf = psf::load_object(installed_sfo_file); + + const auto installed_title_id = psf::get_string(installed_psf, "TITLE_ID", ""); + const auto installed_app_ver = psf::get_string(installed_psf, "APP_VER", ""); + + if (title_id != installed_title_id || installed_app_ver.empty()) + { + // Let's allow this package for now + return package_error::no_error; + } + + std::add_pointer_t ev0, ev1; + const double old_version = std::strtod(installed_app_ver.data(), &ev0); + + if (installed_app_ver.data() + installed_app_ver.size() != ev0) + { + pkg_log.error("Failed to convert the installed app version to double (%s)", installed_app_ver); + return package_error::other; + } + + if (target_app_ver.empty()) + { + // This is most likely the first patch. Let's make sure its version is high enough for the installed game. + + const double new_version = std::strtod(app_ver.data(), &ev1); + + if (app_ver.data() + app_ver.size() != ev1) + { + pkg_log.error("Failed to convert the package's app version to double (%s)", app_ver); + return package_error::other; + } + + if (new_version >= old_version) + { + // Yay! The patch has a higher or equal version than the installed game. + return package_error::no_error; + } + + pkg_log.error("The new app version (%s) is smaller than the installed app version (%s)", app_ver, installed_app_ver); + return package_error::app_version; + } + + // Check if the installed app version matches the target app version + + const double target_version = std::strtod(target_app_ver.data(), &ev1); + + if (target_app_ver.data() + target_app_ver.size() != ev1) + { + pkg_log.error("Failed to convert the package's target app version to double (%s)", target_app_ver); + return package_error::other; + } + + if (target_version == old_version) + { + // Yay! This patch is for the installed game version. + return package_error::no_error; + } + + pkg_log.error("The installed app version (%s) does not match the target app version (%s)", installed_app_ver, target_app_ver); + return package_error::app_version; } bool package_reader::extract_data(atomic_t& sync) diff --git a/rpcs3/Crypto/unpkg.h b/rpcs3/Crypto/unpkg.h index 6b4bd1a8aa..06ca7e17b8 100644 --- a/rpcs3/Crypto/unpkg.h +++ b/rpcs3/Crypto/unpkg.h @@ -1,12 +1,13 @@ -#pragma once +#pragma once +#include "Loader/PSF.h" #include #include // Constants enum { - PKG_HEADER_SIZE = 0xC0, //sizeof(pkg_header) + sizeof(pkg_unk_checksum) + PKG_HEADER_SIZE = 0xC0, // sizeof(pkg_header) + sizeof(pkg_unk_checksum) PKG_HEADER_SIZE2 = 0x280, }; @@ -303,10 +304,12 @@ public: package_error check_target_app_version(); bool extract_data(atomic_t& sync); + psf::registry get_psf() const { return m_psf; } private: bool read_header(); bool read_metadata(); + bool read_param_sfo(); bool decrypt_data(); void archive_seek(const s64 new_offset, const fs::seek_mode damode = fs::seek_set); u64 archive_read(void* data_ptr, const u64 num_bytes); @@ -327,4 +330,5 @@ private: PKGHeader header{}; PKGMetaData metadata{}; + psf::registry m_psf; }; diff --git a/rpcs3/rpcs3qt/game_compatibility.cpp b/rpcs3/rpcs3qt/game_compatibility.cpp index be9feb30b3..a10f226a41 100644 --- a/rpcs3/rpcs3qt/game_compatibility.cpp +++ b/rpcs3/rpcs3qt/game_compatibility.cpp @@ -4,6 +4,7 @@ #include #include +#include #include LOG_CHANNEL(compat_log, "Compat"); @@ -112,7 +113,7 @@ bool game_compatibility::ReadJSON(const QJsonObject& json_data, bool after_downl QJsonObject json_result = json_results[key].toObject(); // Retrieve compatibility information from json - compat_status status = Status_Data.at(json_result.value("status").toString("NoResult")); + compat::status status = Status_Data.at(json_result.value("status").toString("NoResult")); // Add date if possible status.date = json_result.value("date").toString(); @@ -120,8 +121,63 @@ bool game_compatibility::ReadJSON(const QJsonObject& json_data, bool after_downl // Add latest version if possible status.latest_version = json_result.value("update").toString(); + // Add patchsets if possible + if (const QJsonValue patchsets_value = json_result.value("patchsets"); patchsets_value.isArray()) + { + for (const QJsonValue& patch_set : patchsets_value.toArray()) + { + compat::pkg_patchset set; + set.tag_id = patch_set["tag_id"].toString().toStdString(); + set.popup = patch_set["popup"].toBool(); + set.signoff = patch_set["signoff"].toBool(); + set.popup_delay = patch_set["popup_delay"].toInt(); + set.min_system_ver = patch_set["min_system_ver"].toString().toStdString(); + + if (const QJsonValue packages_value = patch_set["packages"]; packages_value.isArray()) + { + for (const QJsonValue& package : packages_value.toArray()) + { + compat::pkg_package pkg; + pkg.version = package["version"].toString().toStdString(); + pkg.size = package["size"].toInt(); + pkg.sha1sum = package["sha1sum"].toString().toStdString(); + pkg.ps3_system_ver = package["ps3_system_ver"].toString().toStdString(); + pkg.drm_type = package["drm_type"].toString().toStdString(); + + if (const QJsonValue changelogs_value = package["changelogs"]; changelogs_value.isArray()) + { + for (const QJsonValue& changelog : changelogs_value.toArray()) + { + compat::pkg_changelog chl; + chl.type = changelog["type"].toString().toStdString(); + chl.content = changelog["content"].toString().toStdString(); + + pkg.changelogs.push_back(std::move(chl)); + } + } + + if (const QJsonValue titles_value = package["titles"]; titles_value.isArray()) + { + for (const QJsonValue& title : titles_value.toArray()) + { + compat::pkg_title ttl; + ttl.type = title["type"].toString().toStdString(); + ttl.title = title["title"].toString().toStdString(); + + pkg.titles.push_back(std::move(ttl)); + } + } + + set.packages.push_back(std::move(pkg)); + } + } + + status.patch_sets.push_back(std::move(set)); + } + } + // Add status to map - m_compat_database.emplace(std::pair(sstr(key), status)); + m_compat_database.emplace(std::pair(sstr(key), status)); } return true; @@ -146,7 +202,7 @@ void game_compatibility::RequestCompatibility(bool online) return; } - QByteArray data = file.readAll(); + const QByteArray data = file.readAll(); file.close(); compat_log.notice("Finished reading database from file: %s", sstr(m_filepath)); @@ -166,7 +222,7 @@ void game_compatibility::RequestCompatibility(bool online) Q_EMIT DownloadStarted(); } -compat_status game_compatibility::GetCompatibility(const std::string& title_id) +compat::status game_compatibility::GetCompatibility(const std::string& title_id) { if (m_compat_database.empty()) { @@ -180,7 +236,7 @@ compat_status game_compatibility::GetCompatibility(const std::string& title_id) return Status_Data.at("NoResult"); } -compat_status game_compatibility::GetStatusData(const QString& status) +compat::status game_compatibility::GetStatusData(const QString& status) { return Status_Data.at(status); } diff --git a/rpcs3/rpcs3qt/game_compatibility.h b/rpcs3/rpcs3qt/game_compatibility.h index d31d4d87af..67e0eebd11 100644 --- a/rpcs3/rpcs3qt/game_compatibility.h +++ b/rpcs3/rpcs3qt/game_compatibility.h @@ -8,22 +8,89 @@ class downloader; class gui_settings; -struct compat_status +namespace compat { - int index; - QString date; - QString color; - QString text; - QString tooltip; - QString latest_version; -}; + struct pkg_title + { + std::string type; // TITLE or TITLE_08 etc. (system languages) + std::string title; // The Last of Arse + }; + + struct pkg_changelog + { + std::string type; // paramhip or paramhip_08 etc. (system languages) + std::string content; // "This system software update improves system performance." + }; + + struct pkg_package + { + std::string version; // 01.04 + int size = 0; + std::string sha1sum; // a5c83b88394ea3ae99974caedd38690981a80f3e + std::string ps3_system_ver; // 04.4000 + std::string drm_type; // local or mbind etc. + std::vector changelogs; + std::vector titles; + + std::string get_changelog(const std::string& type) const + { + if (auto it = std::find_if(changelogs.begin(), changelogs.end(), [type](const pkg_changelog& cl) { return cl.type == type; }); + it != changelogs.end()) + { + return it->content; + } + if (auto it = std::find_if(changelogs.begin(), changelogs.end(), [](const pkg_changelog& cl) { return cl.type == "paramhip"; }); + it != changelogs.end()) + { + return it->content; + } + return ""; + } + + std::string get_title(const std::string& type) const + { + if (auto it = std::find_if(titles.begin(), titles.end(), [type](const pkg_title& t) { return t.type == type; }); + it != titles.end()) + { + return it->title; + } + if (auto it = std::find_if(titles.begin(), titles.end(), [](const pkg_title& t) { return t.type == "TITLE"; }); + it != titles.end()) + { + return it->title; + } + return ""; + } + }; + + struct pkg_patchset + { + std::string tag_id; // BLES01269_T7 + bool popup = false; + bool signoff = false; + int popup_delay = 0; + std::string min_system_ver; // 03.60 + std::vector packages; + }; + + struct status + { + int index; + QString date; + QString color; + QString text; + QString tooltip; + QString latest_version; + std::vector patch_sets; + }; +} class game_compatibility : public QObject { Q_OBJECT private: - const std::map Status_Data = + const std::map Status_Data = { { "Playable", { 0, "", "#1ebc61", tr("Playable"), tr("Games that can be properly played from start to finish") } }, { "Ingame", { 1, "", "#f9b32f", tr("Ingame"), tr("Games that either can't be finished, have serious glitches or have insufficient performance") } }, @@ -37,7 +104,7 @@ private: std::shared_ptr m_gui_settings; QString m_filepath; downloader* m_downloader = nullptr; - std::map m_compat_database; + std::map m_compat_database; /** Creates new map from the database */ bool ReadJSON(const QJsonObject& json_data, bool after_download); @@ -50,10 +117,10 @@ public: void RequestCompatibility(bool online = false); /** Returns the compatibility status for the requested title */ - compat_status GetCompatibility(const std::string& title_id); + compat::status GetCompatibility(const std::string& title_id); /** Returns the data for the requested status */ - compat_status GetStatusData(const QString& status); + compat::status GetStatusData(const QString& status); Q_SIGNALS: void DownloadStarted(); diff --git a/rpcs3/rpcs3qt/game_list_frame.h b/rpcs3/rpcs3qt/game_list_frame.h index feac201e1a..725963ead8 100644 --- a/rpcs3/rpcs3qt/game_list_frame.h +++ b/rpcs3/rpcs3qt/game_list_frame.h @@ -25,7 +25,7 @@ struct gui_game_info { GameInfo info; QString localized_category; - compat_status compat; + compat::status compat; QPixmap icon; QPixmap pxmap; bool hasCustomConfig; @@ -40,7 +40,7 @@ class game_list_frame : public custom_dock_widget Q_OBJECT public: - explicit game_list_frame(std::shared_ptr gui_settings, std::shared_ptr emu_settings, std::shared_ptr persistent_settings, QWidget *parent = nullptr); + explicit game_list_frame(std::shared_ptr gui_settings, std::shared_ptr emu_settings, std::shared_ptr persistent_settings, QWidget* parent = nullptr); ~game_list_frame(); /** Fix columns with width smaller than the minimal section size */ @@ -69,6 +69,8 @@ public: void SetShowHidden(bool show); + game_compatibility* GetGameCompatibility() const { return m_game_compat; }; + QList GetGameInfo() const; // Returns the visible version string in the game list diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 3f5686035a..250950e416 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -511,7 +511,63 @@ void main_window::InstallPackages(QStringList file_paths) else if (file_paths.count() == 1) { // This can currently only happen by drag and drop. - if (QMessageBox::question(this, tr("PKG Decrypter / Installer"), tr("Install package: %1?").arg(file_paths.front()), + const QString file_path = file_paths.front(); + package_reader reader(file_path.toStdString()); + psf::registry psf = reader.get_psf(); + const std::string title_id(psf::get_string(psf, "TITLE_ID")); + + // TODO: localization of title and changelog + std::string title_key = "TITLE"; + std::string changelog_key = "paramhip"; + //std::string cat(psf::get_string(psf, "CATEGORY")); + QString version = qstr(std::string(psf::get_string(psf, "APP_VER"))); + QString title = qstr(std::string(psf::get_string(psf, title_key))); // Let's read this from the psf first + QString changelog; + + if (game_compatibility* compat = m_game_list_frame ? m_game_list_frame->GetGameCompatibility() : nullptr) + { + compat::status info = compat->GetCompatibility(title_id); + if (!info.patch_sets.empty()) + { + // We currently only handle the first patch set + for (const auto& package : info.patch_sets.front().packages) + { + if (sstr(version) == package.version) + { + if (const std::string localized_title = package.get_title(title_key); !localized_title.empty()) + { + title = qstr(localized_title); + } + + if (const std::string localized_changelog = package.get_changelog(changelog_key); !localized_changelog.empty()) + { + changelog = qstr(localized_changelog); + } + + break; + } + } + } + } + + if (!changelog.isEmpty()) + { + changelog = tr("\n\nChangelog:\n%0").arg(changelog); + } + + if (!version.isEmpty()) + { + version = tr("\nVersion %0").arg(version); + } + + if (title.isEmpty()) + { + QFileInfo file_info(file_path); + title = file_info.fileName(); + } + + if (QMessageBox::question(this, tr("PKG Decrypter / Installer"), tr("Do you want to install this package?\n\n%0%1%2") + .arg(title).arg(version).arg(changelog), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) { gui_log.notice("PKG: Cancelled installation from drop. File: %s", sstr(file_paths.front()));