mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-28 05:46:56 +01:00
+6
-5
@@ -159,13 +159,14 @@ static void LoadElf(const std::filesystem::path& elf, bool dbg_print_reloc = fal
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void Execute() {
|
static void Execute(const std::filesystem::path& game_patch) {
|
||||||
|
auto patch_path = game_patch;
|
||||||
Common::Thread guest_thread(
|
Common::Thread guest_thread(
|
||||||
[](void* /*unused*/) {
|
[](void* param) {
|
||||||
auto* rt = Common::Singleton<Loader::RuntimeLinker>::Instance();
|
auto* rt = Common::Singleton<Loader::RuntimeLinker>::Instance();
|
||||||
rt->Execute();
|
rt->Execute(*static_cast<const std::filesystem::path*>(param));
|
||||||
},
|
},
|
||||||
nullptr);
|
&patch_path);
|
||||||
Libs::Graphics::WindowRun();
|
Libs::Graphics::WindowRun();
|
||||||
std::quick_exit(0);
|
std::quick_exit(0);
|
||||||
}
|
}
|
||||||
@@ -207,7 +208,7 @@ void Run(const RunOptions& options) {
|
|||||||
|
|
||||||
LoadElf(options.elf);
|
LoadElf(options.elf);
|
||||||
|
|
||||||
Execute();
|
Execute(options.game_patch);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Emulator
|
} // namespace Emulator
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ struct RunOptions {
|
|||||||
Config::ConfigOptions config;
|
Config::ConfigOptions config;
|
||||||
std::filesystem::path app0_dir;
|
std::filesystem::path app0_dir;
|
||||||
std::filesystem::path elf;
|
std::filesystem::path elf;
|
||||||
|
std::filesystem::path game_patch;
|
||||||
};
|
};
|
||||||
|
|
||||||
void Run(const RunOptions& options);
|
void Run(const RunOptions& options);
|
||||||
|
|||||||
@@ -183,6 +183,11 @@
|
|||||||
<string>Serial</string>
|
<string>Serial</string>
|
||||||
</property>
|
</property>
|
||||||
</column>
|
</column>
|
||||||
|
<column>
|
||||||
|
<property name="text">
|
||||||
|
<string>Game Version</string>
|
||||||
|
</property>
|
||||||
|
</column>
|
||||||
<column>
|
<column>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Firmware Version</string>
|
<string>Firmware Version</string>
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ public:
|
|||||||
|
|
||||||
QString name;
|
QString name;
|
||||||
QString title_id; /* Serial / title id from sce_sys/param.json */
|
QString title_id; /* Serial / title id from sce_sys/param.json */
|
||||||
|
QString gameVersion; /* appVersion / contentVersion from sce_sys/param.json */
|
||||||
QString firmwareVer; /* requiredSystemSoftwareVersion from sce_sys/param.json */
|
QString firmwareVer; /* requiredSystemSoftwareVersion from sce_sys/param.json */
|
||||||
QString basedir; /* Game base directory */
|
QString basedir; /* Game base directory */
|
||||||
QString game_path; /* Launcher-unique game path */
|
QString game_path; /* Launcher-unique game path */
|
||||||
@@ -117,6 +118,7 @@ public:
|
|||||||
void CopyFrom(const Configuration& other) {
|
void CopyFrom(const Configuration& other) {
|
||||||
name = other.name;
|
name = other.name;
|
||||||
title_id = other.title_id;
|
title_id = other.title_id;
|
||||||
|
gameVersion = other.gameVersion;
|
||||||
firmwareVer = other.firmwareVer;
|
firmwareVer = other.firmwareVer;
|
||||||
basedir = other.basedir;
|
basedir = other.basedir;
|
||||||
game_path = other.game_path;
|
game_path = other.game_path;
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#ifndef PATCHES_DIALOG_H
|
||||||
|
#define PATCHES_DIALOG_H
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class Configuration;
|
||||||
|
class QLabel;
|
||||||
|
class QListWidget;
|
||||||
|
class QPushButton;
|
||||||
|
|
||||||
|
class PatchesDialog final: public QDialog {
|
||||||
|
public:
|
||||||
|
explicit PatchesDialog(const Configuration& game, QWidget* parent = nullptr);
|
||||||
|
~PatchesDialog() override = default;
|
||||||
|
|
||||||
|
KYTY_QT_CLASS_NO_COPY(PatchesDialog);
|
||||||
|
|
||||||
|
[[nodiscard]] static bool IsSupportedTitleId(const QString& title_id);
|
||||||
|
[[nodiscard]] static QString PatchPlanPath(const QString& title_id);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Load();
|
||||||
|
void Save();
|
||||||
|
|
||||||
|
QString m_title_id;
|
||||||
|
QListWidget* m_patches = nullptr;
|
||||||
|
QLabel* m_status = nullptr;
|
||||||
|
QPushButton* m_apply = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // PATCHES_DIALOG_H
|
||||||
@@ -24,6 +24,7 @@ namespace {
|
|||||||
enum Column {
|
enum Column {
|
||||||
NameColumn,
|
NameColumn,
|
||||||
SerialColumn,
|
SerialColumn,
|
||||||
|
GameVersionColumn,
|
||||||
FirmwareVersionColumn,
|
FirmwareVersionColumn,
|
||||||
PathColumn,
|
PathColumn,
|
||||||
StatusColumn,
|
StatusColumn,
|
||||||
@@ -85,6 +86,9 @@ QString GetDisplayText(const Configuration& info) {
|
|||||||
if (!info.title_id.isEmpty()) {
|
if (!info.title_id.isEmpty()) {
|
||||||
lines.append(QStringLiteral("Serial ID: %1").arg(info.title_id));
|
lines.append(QStringLiteral("Serial ID: %1").arg(info.title_id));
|
||||||
}
|
}
|
||||||
|
if (!info.gameVersion.isEmpty()) {
|
||||||
|
lines.append(QStringLiteral("Game version: %1").arg(info.gameVersion));
|
||||||
|
}
|
||||||
if (!info.firmwareVer.isEmpty()) {
|
if (!info.firmwareVer.isEmpty()) {
|
||||||
lines.append(QStringLiteral("Firmware version: %1").arg(info.firmwareVer));
|
lines.append(QStringLiteral("Firmware version: %1").arg(info.firmwareVer));
|
||||||
}
|
}
|
||||||
@@ -169,6 +173,8 @@ void ConfigurationItem::Update() {
|
|||||||
|
|
||||||
setText(NameColumn, m_info->name);
|
setText(NameColumn, m_info->name);
|
||||||
setText(SerialColumn, m_info->title_id);
|
setText(SerialColumn, m_info->title_id);
|
||||||
|
setText(GameVersionColumn,
|
||||||
|
m_info->gameVersion.isEmpty() ? QStringLiteral("\u2014") : m_info->gameVersion);
|
||||||
setText(FirmwareVersionColumn,
|
setText(FirmwareVersionColumn,
|
||||||
m_info->firmwareVer.isEmpty() ? QStringLiteral("\u2014") : m_info->firmwareVer);
|
m_info->firmwareVer.isEmpty() ? QStringLiteral("\u2014") : m_info->firmwareVer);
|
||||||
setText(PathColumn, path);
|
setText(PathColumn, path);
|
||||||
@@ -198,9 +204,13 @@ bool ConfigurationItem::operator<(const QTreeWidgetItem& other) const {
|
|||||||
case StatusColumn:
|
case StatusColumn:
|
||||||
return GetStatusText(m_info->game_status) <
|
return GetStatusText(m_info->game_status) <
|
||||||
GetStatusText(other_item->m_info->game_status);
|
GetStatusText(other_item->m_info->game_status);
|
||||||
|
case GameVersionColumn:
|
||||||
case FirmwareVersionColumn: {
|
case FirmwareVersionColumn: {
|
||||||
const auto& version = m_info->firmwareVer;
|
const auto& version = column == GameVersionColumn ? m_info->gameVersion
|
||||||
const auto& other_version = other_item->m_info->firmwareVer;
|
: m_info->firmwareVer;
|
||||||
|
const auto& other_version = column == GameVersionColumn
|
||||||
|
? other_item->m_info->gameVersion
|
||||||
|
: other_item->m_info->firmwareVer;
|
||||||
if (version.isEmpty() || other_version.isEmpty()) {
|
if (version.isEmpty() || other_version.isEmpty()) {
|
||||||
return version.isEmpty() && !other_version.isEmpty();
|
return version.isEmpty() && !other_version.isEmpty();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "configurationListWidget.h"
|
#include "configurationListWidget.h"
|
||||||
|
|
||||||
|
#include "patchesDialog.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "compatibilityDatabase.h"
|
#include "compatibilityDatabase.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
@@ -55,10 +56,11 @@ constexpr char SAVE_DATA_DIR[] = "_SaveData";
|
|||||||
|
|
||||||
constexpr int GAME_NAME_COLUMN = 0;
|
constexpr int GAME_NAME_COLUMN = 0;
|
||||||
constexpr int GAME_SERIAL_COLUMN = 1;
|
constexpr int GAME_SERIAL_COLUMN = 1;
|
||||||
constexpr int GAME_FIRMWARE_VERSION_COLUMN = 2;
|
constexpr int GAME_VERSION_COLUMN = 2;
|
||||||
constexpr int GAME_PATH_COLUMN = 3;
|
constexpr int GAME_FIRMWARE_VERSION_COLUMN = 3;
|
||||||
constexpr int GAME_STATUS_COLUMN = 4;
|
constexpr int GAME_PATH_COLUMN = 4;
|
||||||
constexpr int GAME_COMMENT_COLUMN = 5;
|
constexpr int GAME_STATUS_COLUMN = 5;
|
||||||
|
constexpr int GAME_COMMENT_COLUMN = 6;
|
||||||
|
|
||||||
static QString NormalizeGameDirectory(const QString& dir) {
|
static QString NormalizeGameDirectory(const QString& dir) {
|
||||||
const auto trimmed = dir.trimmed();
|
const auto trimmed = dir.trimmed();
|
||||||
@@ -239,6 +241,7 @@ ConfigurationListWidget::ConfigurationListWidget(QWidget* parent)
|
|||||||
m_ui->cfgs_list->setSortingEnabled(true);
|
m_ui->cfgs_list->setSortingEnabled(true);
|
||||||
m_ui->cfgs_list->setColumnWidth(GAME_NAME_COLUMN, 320);
|
m_ui->cfgs_list->setColumnWidth(GAME_NAME_COLUMN, 320);
|
||||||
m_ui->cfgs_list->setColumnWidth(GAME_SERIAL_COLUMN, 110);
|
m_ui->cfgs_list->setColumnWidth(GAME_SERIAL_COLUMN, 110);
|
||||||
|
m_ui->cfgs_list->setColumnWidth(GAME_VERSION_COLUMN, 120);
|
||||||
m_ui->cfgs_list->setColumnWidth(GAME_FIRMWARE_VERSION_COLUMN, 150);
|
m_ui->cfgs_list->setColumnWidth(GAME_FIRMWARE_VERSION_COLUMN, 150);
|
||||||
m_ui->cfgs_list->setColumnWidth(GAME_PATH_COLUMN, 320);
|
m_ui->cfgs_list->setColumnWidth(GAME_PATH_COLUMN, 320);
|
||||||
m_ui->cfgs_list->setColumnWidth(GAME_STATUS_COLUMN, 150);
|
m_ui->cfgs_list->setColumnWidth(GAME_STATUS_COLUMN, 150);
|
||||||
@@ -403,6 +406,7 @@ static Configuration* CloneConfiguration(const Configuration& source) {
|
|||||||
struct GameMetadata {
|
struct GameMetadata {
|
||||||
QString title_name;
|
QString title_name;
|
||||||
QString title_id;
|
QString title_id;
|
||||||
|
QString gameVersion;
|
||||||
QString firmwareVer;
|
QString firmwareVer;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -488,6 +492,10 @@ static GameMetadata GetGameMetadata(const QString& param_file, const QString& fa
|
|||||||
}
|
}
|
||||||
|
|
||||||
ret.title_id = GetJsonString(root, QStringLiteral("titleId"));
|
ret.title_id = GetJsonString(root, QStringLiteral("titleId"));
|
||||||
|
ret.gameVersion = GetJsonString(root, QStringLiteral("appVersion"));
|
||||||
|
if (ret.gameVersion.isEmpty()) {
|
||||||
|
ret.gameVersion = GetJsonString(root, QStringLiteral("contentVersion"));
|
||||||
|
}
|
||||||
ret.firmwareVer = GetFirmwareVersion(root);
|
ret.firmwareVer = GetFirmwareVersion(root);
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
@@ -501,6 +509,7 @@ static void SetGameFiles(Configuration& info, const QString& game_dir, const QSt
|
|||||||
info.basedir = game.absolutePath();
|
info.basedir = game.absolutePath();
|
||||||
info.name = metadata.title_name;
|
info.name = metadata.title_name;
|
||||||
info.title_id = metadata.title_id;
|
info.title_id = metadata.title_id;
|
||||||
|
info.gameVersion = metadata.gameVersion;
|
||||||
info.firmwareVer = metadata.firmwareVer;
|
info.firmwareVer = metadata.firmwareVer;
|
||||||
|
|
||||||
if (info.name.isEmpty()) {
|
if (info.name.isEmpty()) {
|
||||||
@@ -866,6 +875,15 @@ void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
|
|||||||
style()->standardIcon(QStyle::SP_FileDialogContentsView), tr("View trophies..."));
|
style()->standardIcon(QStyle::SP_FileDialogContentsView), tr("View trophies..."));
|
||||||
connect(action_view_trophies, &QAction::triggered, this,
|
connect(action_view_trophies, &QAction::triggered, this,
|
||||||
&ConfigurationListWidget::ViewTrophies);
|
&ConfigurationListWidget::ViewTrophies);
|
||||||
|
QAction* action_patches = menu.addAction(tr("Patches (experimental)..."));
|
||||||
|
connect(action_patches, &QAction::triggered, this, [this, item]() {
|
||||||
|
if (item != nullptr) {
|
||||||
|
auto* dialog = new PatchesDialog(item->GetInfo(), this);
|
||||||
|
dialog->show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
action_patches->setVisible(item != nullptr &&
|
||||||
|
PatchesDialog::IsSupportedTitleId(item->GetInfo().title_id));
|
||||||
QAction* action_remove_save_data =
|
QAction* action_remove_save_data =
|
||||||
menu.addAction(style()->standardIcon(QStyle::SP_DialogDiscardButton),
|
menu.addAction(style()->standardIcon(QStyle::SP_DialogDiscardButton),
|
||||||
tr("Remove save data..."), this, SLOT(remove_save_data()));
|
tr("Remove save data..."), this, SLOT(remove_save_data()));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "configurationItem.h"
|
#include "configurationItem.h"
|
||||||
#include "configurationListWidget.h"
|
#include "configurationListWidget.h"
|
||||||
|
#include "patchesDialog.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
@@ -228,6 +229,11 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
|
|||||||
}
|
}
|
||||||
args << "--game" << game;
|
args << "--game" << game;
|
||||||
|
|
||||||
|
const auto patch_plan = PatchesDialog::PatchPlanPath(info.title_id);
|
||||||
|
if (QFileInfo::exists(patch_plan)) {
|
||||||
|
args << "--game-patch" << patch_plan;
|
||||||
|
}
|
||||||
|
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
#include "patchesDialog.h"
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QListWidget>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QSaveFile>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
PatchesDialog::PatchesDialog(const Configuration& game, QWidget* parent)
|
||||||
|
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()) {
|
||||||
|
setAttribute(Qt::WA_DeleteOnClose);
|
||||||
|
setWindowTitle(tr("Patches (Experimental) - %1").arg(game.name));
|
||||||
|
resize(640, 480);
|
||||||
|
|
||||||
|
auto* layout = new QVBoxLayout(this);
|
||||||
|
m_patches = new QListWidget(this);
|
||||||
|
m_status = new QLabel(this);
|
||||||
|
m_apply = new QPushButton(tr("Apply selection"), this);
|
||||||
|
auto* close = new QPushButton(tr("Close"), this);
|
||||||
|
|
||||||
|
m_status->setWordWrap(true);
|
||||||
|
layout->addWidget(m_patches);
|
||||||
|
layout->addWidget(m_status);
|
||||||
|
|
||||||
|
auto* buttons = new QDialogButtonBox(this);
|
||||||
|
buttons->addButton(m_apply, QDialogButtonBox::ActionRole);
|
||||||
|
buttons->addButton(close, QDialogButtonBox::RejectRole);
|
||||||
|
layout->addWidget(buttons);
|
||||||
|
|
||||||
|
connect(m_apply, &QPushButton::clicked, this, &PatchesDialog::Save);
|
||||||
|
connect(close, &QPushButton::clicked, this, &QDialog::close);
|
||||||
|
|
||||||
|
Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PatchesDialog::IsSupportedTitleId(const QString& title_id) {
|
||||||
|
return title_id.trimmed().toUpper().startsWith(QStringLiteral("PPSA"));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PatchesDialog::PatchPlanPath(const QString& title_id) {
|
||||||
|
return QDir(QCoreApplication::applicationDirPath())
|
||||||
|
.filePath(QStringLiteral("_Patches/%1.json").arg(title_id.trimmed().toUpper()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PatchesDialog::Load() {
|
||||||
|
QFile file(PatchPlanPath(m_title_id));
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
m_status->setText(tr("No local patch file: %1").arg(file.fileName()));
|
||||||
|
m_apply->setEnabled(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto patches = QJsonDocument::fromJson(file.readAll())
|
||||||
|
.object()
|
||||||
|
.value(QStringLiteral("patches"))
|
||||||
|
.toArray();
|
||||||
|
for (const auto& value: patches) {
|
||||||
|
const auto patch = value.toObject();
|
||||||
|
auto* item = new QListWidgetItem(patch.value(QStringLiteral("name")).toString(), m_patches);
|
||||||
|
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
||||||
|
item->setCheckState(patch.value(QStringLiteral("enabled")).toBool(true) ? Qt::Checked
|
||||||
|
: Qt::Unchecked);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_apply->setEnabled(!patches.isEmpty());
|
||||||
|
m_status->setText(tr("Loaded %1 patch(es) from %2.").arg(patches.size()).arg(file.fileName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PatchesDialog::Save() {
|
||||||
|
const auto path = PatchPlanPath(m_title_id);
|
||||||
|
QFile input(path);
|
||||||
|
if (!input.open(QIODevice::ReadOnly)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto document = QJsonDocument::fromJson(input.readAll());
|
||||||
|
input.close();
|
||||||
|
auto root = document.object();
|
||||||
|
auto patches = root.value(QStringLiteral("patches")).toArray();
|
||||||
|
for (int index = 0; index < patches.size(); index++) {
|
||||||
|
auto patch = patches[index].toObject();
|
||||||
|
patch.insert(QStringLiteral("enabled"),
|
||||||
|
m_patches->item(index)->checkState() == Qt::Checked);
|
||||||
|
patches[index] = patch;
|
||||||
|
}
|
||||||
|
root.insert(QStringLiteral("patches"), patches);
|
||||||
|
document.setObject(root);
|
||||||
|
|
||||||
|
QSaveFile output(path);
|
||||||
|
if (output.open(QIODevice::WriteOnly) && output.write(document.toJson()) >= 0 &&
|
||||||
|
output.commit()) {
|
||||||
|
m_status->setText(tr("Patch selection saved."));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
#include "loader/gamePatch.h"
|
||||||
|
|
||||||
|
#include "common/stringUtils.h"
|
||||||
|
#include "common/virtualMemory.h"
|
||||||
|
#include "loader/elf.h"
|
||||||
|
#include "loader/runtimeLinker.h"
|
||||||
|
#include "loader/systemContent.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <charconv>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <fstream>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <system_error>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace Loader::GamePatch {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct Write {
|
||||||
|
std::string patch_name;
|
||||||
|
uint64_t address = 0;
|
||||||
|
std::vector<uint8_t> expected;
|
||||||
|
std::vector<uint8_t> replacement;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Plan {
|
||||||
|
std::string title_id;
|
||||||
|
std::string game_version;
|
||||||
|
std::string process;
|
||||||
|
std::vector<Write> writes;
|
||||||
|
std::vector<std::string> patch_names;
|
||||||
|
};
|
||||||
|
|
||||||
|
using Json = nlohmann::json;
|
||||||
|
|
||||||
|
bool Fail(std::string* error, std::string message) {
|
||||||
|
*error = std::move(message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Json::string_t* StringField(const Json& object, const char* name) {
|
||||||
|
const auto value = object.find(name);
|
||||||
|
return value == object.end() ? nullptr : value->get_ptr<const Json::string_t*>();
|
||||||
|
}
|
||||||
|
|
||||||
|
const Json::array_t* ArrayField(const Json& object, const char* name) {
|
||||||
|
const auto value = object.find(name);
|
||||||
|
return value == object.end() ? nullptr : value->get_ptr<const Json::array_t*>();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ParseBytes(std::string_view text, std::vector<uint8_t>* bytes) {
|
||||||
|
if (text.empty() || (text.size() % 2) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes->resize(text.size() / 2);
|
||||||
|
for (size_t index = 0; index < bytes->size(); index++) {
|
||||||
|
unsigned int value = 0;
|
||||||
|
const char* begin = text.data() + index * 2;
|
||||||
|
const auto [end, error] = std::from_chars(begin, begin + 2, value, 16);
|
||||||
|
if (error != std::errc {} || end != begin + 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
(*bytes)[index] = static_cast<uint8_t>(value);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReadJson(const std::filesystem::path& path, Json* root, std::string* error) {
|
||||||
|
std::ifstream file(path, std::ios::binary);
|
||||||
|
if (!file) {
|
||||||
|
return Fail(error, "could not read patch plan");
|
||||||
|
}
|
||||||
|
|
||||||
|
*root = Json::parse(file, nullptr, false);
|
||||||
|
return root->is_object() || Fail(error, "patch plan is not valid JSON");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LoadPlan(const std::filesystem::path& path, Plan* plan, std::string* error) {
|
||||||
|
Json root;
|
||||||
|
if (!ReadJson(path, &root, error)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* title_id = StringField(root, "title_id");
|
||||||
|
const auto* game_version = StringField(root, "game_version");
|
||||||
|
const auto* process = StringField(root, "process");
|
||||||
|
const auto* patches = ArrayField(root, "patches");
|
||||||
|
if (title_id == nullptr || game_version == nullptr || process == nullptr ||
|
||||||
|
patches == nullptr) {
|
||||||
|
return Fail(error, "invalid patch plan");
|
||||||
|
}
|
||||||
|
|
||||||
|
plan->title_id = *title_id;
|
||||||
|
plan->game_version = *game_version;
|
||||||
|
plan->process = *process;
|
||||||
|
|
||||||
|
for (const auto& patch_json: *patches) {
|
||||||
|
const auto enabled = patch_json.find("enabled");
|
||||||
|
if (enabled != patch_json.end() && enabled->is_boolean() && !enabled->get<bool>()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* name = StringField(patch_json, "name");
|
||||||
|
const auto* writes = ArrayField(patch_json, "writes");
|
||||||
|
if (name == nullptr || writes == nullptr) {
|
||||||
|
return Fail(error, "invalid patch entry");
|
||||||
|
}
|
||||||
|
|
||||||
|
plan->patch_names.push_back(*name);
|
||||||
|
for (const auto& write_json: *writes) {
|
||||||
|
const auto* expected = StringField(write_json, "expected");
|
||||||
|
const auto* replacement = StringField(write_json, "replacement");
|
||||||
|
if (expected == nullptr || replacement == nullptr) {
|
||||||
|
return Fail(error, "invalid patch write");
|
||||||
|
}
|
||||||
|
|
||||||
|
Write write {
|
||||||
|
.patch_name = *name,
|
||||||
|
};
|
||||||
|
if (!ParseBytes(*expected, &write.expected) ||
|
||||||
|
!ParseBytes(*replacement, &write.replacement) ||
|
||||||
|
write.expected.size() != write.replacement.size()) {
|
||||||
|
return Fail(error, "invalid patch bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
plan->writes.push_back(std::move(write));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ValidateTarget(const Plan& plan, const Program* program, std::string* error) {
|
||||||
|
if (program == nullptr || program->elf == nullptr || program->base_vaddr == 0) {
|
||||||
|
return Fail(error, "main executable is not loaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string title_id;
|
||||||
|
std::string game_version;
|
||||||
|
if (!SystemContentParamSfoGetString("TITLE_ID", &title_id) ||
|
||||||
|
!SystemContentParamSfoGetString("APP_VER", &game_version)) {
|
||||||
|
return Fail(error, "game metadata is incomplete");
|
||||||
|
}
|
||||||
|
if (!Common::EqualNoCase(program->file_name.filename().string(), plan.process) ||
|
||||||
|
!Common::EqualNoCase(title_id, plan.title_id) || game_version != plan.game_version) {
|
||||||
|
return Fail(error, "patch plan does not match the loaded game");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Common::VirtualMemory::Mode ReadableMode(Elf64_Word flags) {
|
||||||
|
const bool executable = (flags & PF_X) != 0;
|
||||||
|
const bool writable = (flags & PF_W) != 0;
|
||||||
|
if (executable && writable) {
|
||||||
|
return Common::VirtualMemory::Mode::ExecuteReadWrite;
|
||||||
|
}
|
||||||
|
if (executable) {
|
||||||
|
return Common::VirtualMemory::Mode::ExecuteRead;
|
||||||
|
}
|
||||||
|
return writable ? Common::VirtualMemory::Mode::ReadWrite : Common::VirtualMemory::Mode::Read;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ResolveWrite(const Program& program, Write* write, std::string* error) {
|
||||||
|
const auto* ehdr = program.elf->GetEhdr();
|
||||||
|
const auto* phdr = program.elf->GetPhdr();
|
||||||
|
|
||||||
|
uint64_t match = 0;
|
||||||
|
size_t match_count = 0;
|
||||||
|
for (Elf64_Half index = 0; index < ehdr->e_phnum; index++) {
|
||||||
|
const auto& segment = phdr[index];
|
||||||
|
const bool loaded = segment.p_type == PT_LOAD || segment.p_type == PT_OS_RELRO;
|
||||||
|
if (!loaded || segment.p_filesz < write->expected.size()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto segment_address = program.base_vaddr + segment.p_vaddr;
|
||||||
|
const bool add_read = (segment.p_flags & PF_R) == 0;
|
||||||
|
Common::VirtualMemory::Mode old_mode {};
|
||||||
|
if (add_read && !Common::VirtualMemory::Protect(segment_address, segment.p_memsz,
|
||||||
|
ReadableMode(segment.p_flags), &old_mode)) {
|
||||||
|
return Fail(error, "could not read a loaded executable segment");
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* begin = reinterpret_cast<const uint8_t*>(segment_address);
|
||||||
|
const auto* end = begin + segment.p_filesz;
|
||||||
|
for (auto* current = begin; current < end;) {
|
||||||
|
const auto* found =
|
||||||
|
std::search(current, end, write->expected.begin(), write->expected.end());
|
||||||
|
if (found == end) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto address = reinterpret_cast<uint64_t>(found);
|
||||||
|
if (match == 0) {
|
||||||
|
match = address;
|
||||||
|
}
|
||||||
|
match_count++;
|
||||||
|
current = found + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (add_read &&
|
||||||
|
!Common::VirtualMemory::Protect(segment_address, segment.p_memsz, old_mode)) {
|
||||||
|
return Fail(error, "could not restore executable segment protection");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::printf("Game patch: found %zu entries for '%s'\n", match_count,
|
||||||
|
write->patch_name.c_str());
|
||||||
|
if (match == 0) {
|
||||||
|
return Fail(error, "original bytes not found for '" + write->patch_name + "'");
|
||||||
|
}
|
||||||
|
write->address = match;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PrepareWrites(Plan* plan, const Program& program, std::string* error) {
|
||||||
|
for (auto& write: plan->writes) {
|
||||||
|
if (!ResolveWrite(program, &write, error)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ApplyWrites(Plan* plan, std::string* error) {
|
||||||
|
for (auto& write: plan->writes) {
|
||||||
|
Common::VirtualMemory::Mode old_mode {};
|
||||||
|
const auto size = write.replacement.size();
|
||||||
|
if (!Common::VirtualMemory::Protect(
|
||||||
|
write.address, size, Common::VirtualMemory::Mode::ExecuteReadWrite, &old_mode)) {
|
||||||
|
return Fail(error, "could not make patch memory writable");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::memcpy(reinterpret_cast<void*>(write.address), write.replacement.data(), size);
|
||||||
|
if (!Common::VirtualMemory::Protect(write.address, size, old_mode) ||
|
||||||
|
!Common::VirtualMemory::FlushInstructionCache(write.address, size)) {
|
||||||
|
return Fail(error, "could not finalize patch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool Apply(const std::filesystem::path& plan_path, Program* program) {
|
||||||
|
Plan plan;
|
||||||
|
std::string error;
|
||||||
|
if (LoadPlan(plan_path, &plan, &error) && ValidateTarget(plan, program, &error) &&
|
||||||
|
PrepareWrites(&plan, *program, &error) && ApplyWrites(&plan, &error)) {
|
||||||
|
for (const auto& name: plan.patch_names) {
|
||||||
|
::printf("Successfully applied patch: %s\n", name.c_str());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
::printf("Game patch error: %s\n", error.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Loader::GamePatch
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#ifndef KYTY_LOADER_GAME_PATCH_H_
|
||||||
|
#define KYTY_LOADER_GAME_PATCH_H_
|
||||||
|
|
||||||
|
#include "common/common.h"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
namespace Loader {
|
||||||
|
|
||||||
|
struct Program;
|
||||||
|
|
||||||
|
namespace GamePatch {
|
||||||
|
|
||||||
|
bool Apply(const std::filesystem::path& plan_path, Program* program);
|
||||||
|
|
||||||
|
} // namespace GamePatch
|
||||||
|
|
||||||
|
} // namespace Loader
|
||||||
|
|
||||||
|
#endif // KYTY_LOADER_GAME_PATCH_H_
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "kernel/memory.h"
|
#include "kernel/memory.h"
|
||||||
#include "kernel/pthread.h"
|
#include "kernel/pthread.h"
|
||||||
#include "loader/elf.h"
|
#include "loader/elf.h"
|
||||||
|
#include "loader/gamePatch.h"
|
||||||
#include "loader/jit.h"
|
#include "loader/jit.h"
|
||||||
#include "loader/symbolDatabase.h"
|
#include "loader/symbolDatabase.h"
|
||||||
#include "loader/x64InstructionEmulator.h"
|
#include "loader/x64InstructionEmulator.h"
|
||||||
@@ -1326,7 +1327,7 @@ void RuntimeLinker::SaveProgram(Program* program, const std::filesystem::path& e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void RuntimeLinker::Execute() {
|
void RuntimeLinker::Execute(const std::filesystem::path& game_patch) {
|
||||||
KYTY_PROFILER_THREAD("Thread_Main");
|
KYTY_PROFILER_THREAD("Thread_Main");
|
||||||
|
|
||||||
Libs::LibKernel::PthreadInitSelfForMainThread();
|
Libs::LibKernel::PthreadInitSelfForMainThread();
|
||||||
@@ -1346,6 +1347,9 @@ void RuntimeLinker::Execute() {
|
|||||||
|
|
||||||
PreloadAdjacentPrograms();
|
PreloadAdjacentPrograms();
|
||||||
RelocateAll();
|
RelocateAll();
|
||||||
|
if (!game_patch.empty()) {
|
||||||
|
GamePatch::Apply(game_patch, m_programs.empty() ? nullptr : m_programs.front());
|
||||||
|
}
|
||||||
StartAllModules();
|
StartAllModules();
|
||||||
|
|
||||||
LOGF_COLOR(Log::Color::BrightYellow, "---\n--- Execute: %s\n---\n", "Main");
|
LOGF_COLOR(Log::Color::BrightYellow, "---\n--- Execute: %s\n---\n", "Main");
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ public:
|
|||||||
void RelocateAll();
|
void RelocateAll();
|
||||||
void RelocateProgram(Program* program);
|
void RelocateProgram(Program* program);
|
||||||
|
|
||||||
void Execute();
|
void Execute(const std::filesystem::path& game_patch = {});
|
||||||
int StartModule(Program* program, size_t args, const void* argp, module_func_t func);
|
int StartModule(Program* program, size_t args, const void* argp, module_func_t func);
|
||||||
int StopModule(Program* program, size_t args, const void* argp, module_func_t func);
|
int StopModule(Program* program, size_t args, const void* argp, module_func_t func);
|
||||||
void StartAllModules();
|
void StartAllModules();
|
||||||
|
|||||||
+15
-1
@@ -42,6 +42,8 @@ static void PrintUsage() {
|
|||||||
::printf("kyty_emulator --game <dir|elf> [options]\n\n");
|
::printf("kyty_emulator --game <dir|elf> [options]\n\n");
|
||||||
::printf("Options:\n");
|
::printf("Options:\n");
|
||||||
::printf(" --game <dir|elf> Game directory or ELF to load.\n");
|
::printf(" --game <dir|elf> Game directory or ELF to load.\n");
|
||||||
|
::printf(
|
||||||
|
" --game-patch <json> Validated patch plan to apply before entry.\n");
|
||||||
::printf(" --screen-width <num> Window width. Default: 1280.\n");
|
::printf(" --screen-width <num> Window width. Default: 1280.\n");
|
||||||
::printf(" --screen-height <num> Window height. Default: 720.\n");
|
::printf(" --screen-height <num> Window height. Default: 720.\n");
|
||||||
::printf(" --vblank-frequency <num> Virtual vblank frequency. Default: 60.\n");
|
::printf(" --vblank-frequency <num> Virtual vblank frequency. Default: 60.\n");
|
||||||
@@ -59,7 +61,8 @@ static void PrintUsage() {
|
|||||||
::printf(" --spirv-debug-printf <true|false> Enable SPIR-V debug printf.\n");
|
::printf(" --spirv-debug-printf <true|false> Enable SPIR-V debug printf.\n");
|
||||||
::printf(" --ngg-rectlist-draw <true|false> Draw rect-list auto draws using the NGG "
|
::printf(" --ngg-rectlist-draw <true|false> Draw rect-list auto draws using the NGG "
|
||||||
"4-vertex path.\n");
|
"4-vertex path.\n");
|
||||||
::printf(" --readback-linear-images <true|false> Read back writable linear images on submit.\n");
|
::printf(
|
||||||
|
" --readback-linear-images <true|false> Read back writable linear images on submit.\n");
|
||||||
::printf(" --rd Enable RenderDoc capture.\n");
|
::printf(" --rd Enable RenderDoc capture.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +150,17 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
|||||||
::printf("--game must point to an existing directory or ELF: %s\n", value.c_str());
|
::printf("--game must point to an existing directory or ELF: %s\n", value.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
} else if (arg == "--game-patch") {
|
||||||
|
if (!options.game_patch.empty()) {
|
||||||
|
::printf("--game-patch can only be specified once\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
value = Common::FixFilenameSlash(value);
|
||||||
|
if (!Common::File::IsFileExisting(value)) {
|
||||||
|
::printf("--game-patch must point to an existing file: %s\n", value.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
options.game_patch = value;
|
||||||
} else if (arg == "--screen-width") {
|
} else if (arg == "--screen-width") {
|
||||||
options.config.screen_width = static_cast<uint32_t>(Common::ToInt32(value));
|
options.config.screen_width = static_cast<uint32_t>(Common::ToInt32(value));
|
||||||
} else if (arg == "--screen-height") {
|
} else if (arg == "--screen-height") {
|
||||||
|
|||||||
Reference in New Issue
Block a user