Read patchsets from compat db

This commit is contained in:
Megamouse 2020-11-07 23:01:40 +01:00
parent f14d47bfe6
commit 68d411918d
6 changed files with 343 additions and 134 deletions

View file

@ -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<PKGEntry> 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<char*>(buf.get()), entry.name_size };
const std::string name{reinterpret_cast<char*>(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<char> 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<char> 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<double>& sync)

View file

@ -1,12 +1,13 @@
#pragma once
#pragma once
#include "Loader/PSF.h"
#include <sstream>
#include <iomanip>
// 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<double>& 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;
};

View file

@ -4,6 +4,7 @@
#include <QApplication>
#include <QMessageBox>
#include <QJsonArray>
#include <QJsonDocument>
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<std::string, compat_status>(sstr(key), status));
m_compat_database.emplace(std::pair<std::string, compat::status>(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);
}

View file

@ -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<pkg_changelog> changelogs;
std::vector<pkg_title> 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<pkg_package> packages;
};
struct status
{
int index;
QString date;
QString color;
QString text;
QString tooltip;
QString latest_version;
std::vector<pkg_patchset> patch_sets;
};
}
class game_compatibility : public QObject
{
Q_OBJECT
private:
const std::map<QString, compat_status> Status_Data =
const std::map<QString, compat::status> 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<gui_settings> m_gui_settings;
QString m_filepath;
downloader* m_downloader = nullptr;
std::map<std::string, compat_status> m_compat_database;
std::map<std::string, compat::status> 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();

View file

@ -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> gui_settings, std::shared_ptr<emu_settings> emu_settings, std::shared_ptr<persistent_settings> persistent_settings, QWidget *parent = nullptr);
explicit game_list_frame(std::shared_ptr<gui_settings> gui_settings, std::shared_ptr<emu_settings> emu_settings, std::shared_ptr<persistent_settings> 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<game_info> GetGameInfo() const;
// Returns the visible version string in the game list

View file

@ -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()));