mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-23 20:36:58 +02:00
loader: add support for etaHEN cheats
This commit is contained in:
@@ -889,7 +889,7 @@ 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)..."));
|
QAction* action_patches = menu.addAction(tr("Cheats (experimental)..."));
|
||||||
connect(action_patches, &QAction::triggered, this, [this, item]() {
|
connect(action_patches, &QAction::triggered, this, [this, item]() {
|
||||||
if (item != nullptr) {
|
if (item != nullptr) {
|
||||||
auto* dialog = new PatchesDialog(item->GetInfo(), this);
|
auto* dialog = new PatchesDialog(item->GetInfo(), this);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
PatchesDialog::PatchesDialog(const Configuration& game, QWidget* parent)
|
PatchesDialog::PatchesDialog(const Configuration& game, QWidget* parent)
|
||||||
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()) {
|
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()) {
|
||||||
setAttribute(Qt::WA_DeleteOnClose);
|
setAttribute(Qt::WA_DeleteOnClose);
|
||||||
setWindowTitle(tr("Patches (Experimental) - %1").arg(game.name));
|
setWindowTitle(tr("Cheats (Experimental) - %1").arg(game.name));
|
||||||
resize(640, 480);
|
resize(640, 480);
|
||||||
|
|
||||||
auto* layout = new QVBoxLayout(this);
|
auto* layout = new QVBoxLayout(this);
|
||||||
@@ -54,46 +54,50 @@ QString PatchesDialog::PatchPlanPath(const QString& title_id) {
|
|||||||
void PatchesDialog::Load() {
|
void PatchesDialog::Load() {
|
||||||
QFile file(PatchPlanPath(m_title_id));
|
QFile file(PatchPlanPath(m_title_id));
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
m_status->setText(tr("No local patch file: %1").arg(file.fileName()));
|
m_status->setText(tr("No local cheat file: %1").arg(file.fileName()));
|
||||||
m_apply->setEnabled(false);
|
m_apply->setEnabled(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto document = QJsonDocument::fromJson(file.readAll());
|
const auto document = QJsonDocument::fromJson(file.readAll());
|
||||||
if (document.isNull() || !document.isObject()) {
|
if (document.isNull() || !document.isObject()) {
|
||||||
m_status->setText(tr("Invalid patch JSON in %1.").arg(file.fileName()));
|
m_status->setText(tr("Invalid cheat JSON in %1.").arg(file.fileName()));
|
||||||
m_apply->setEnabled(false);
|
m_apply->setEnabled(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto root = document.object();
|
const auto root = document.object();
|
||||||
const auto patches = root.value(QStringLiteral("patches")).toArray();
|
if (!root.value(QStringLiteral("mods")).isArray()) {
|
||||||
for (const auto& value: patches) {
|
m_status->setText(tr("Cheat JSON has no \"mods\" array in %1.").arg(file.fileName()));
|
||||||
const auto patch = value.toObject();
|
m_apply->setEnabled(false);
|
||||||
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());
|
|
||||||
if (patches.isEmpty()) {
|
|
||||||
// shadPS4/ETAHen cheat files use "mods"; Kyty only reads a "patches" array.
|
|
||||||
if (root.contains(QStringLiteral("mods"))) {
|
|
||||||
m_status->setText(
|
|
||||||
tr("This file uses a \"mods\" list. Kyty expects a top-level \"patches\" "
|
|
||||||
"array in %1.")
|
|
||||||
.arg(file.fileName()));
|
|
||||||
} else if (!root.contains(QStringLiteral("patches"))) {
|
|
||||||
m_status->setText(
|
|
||||||
tr("No \"patches\" array in %1.").arg(file.fileName()));
|
|
||||||
} else {
|
|
||||||
m_status->setText(tr("Loaded 0 patch(es) from %1.").arg(file.fileName()));
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_status->setText(tr("Loaded %1 patch(es) from %2.").arg(patches.size()).arg(file.fileName()));
|
const auto mods = root.value(QStringLiteral("mods")).toArray();
|
||||||
|
for (const auto& value: mods) {
|
||||||
|
if (!value.isObject()) {
|
||||||
|
m_status->setText(tr("Invalid cheat entry in %1.").arg(file.fileName()));
|
||||||
|
m_apply->setEnabled(false);
|
||||||
|
m_patches->clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto mod = value.toObject();
|
||||||
|
const auto name = mod.value(QStringLiteral("name"));
|
||||||
|
const auto enabled = mod.value(QStringLiteral("enabled"));
|
||||||
|
if (!name.isString() || name.toString().isEmpty() ||
|
||||||
|
(!enabled.isUndefined() && !enabled.isBool())) {
|
||||||
|
m_status->setText(tr("Invalid cheat entry in %1.").arg(file.fileName()));
|
||||||
|
m_apply->setEnabled(false);
|
||||||
|
m_patches->clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto* item = new QListWidgetItem(name.toString(), m_patches);
|
||||||
|
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
||||||
|
item->setCheckState(enabled.toBool(true) ? Qt::Checked : Qt::Unchecked);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_apply->setEnabled(!mods.isEmpty());
|
||||||
|
m_status->setText(tr("Loaded %1 cheat(s) from %2.").arg(mods.size()).arg(file.fileName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void PatchesDialog::Save() {
|
void PatchesDialog::Save() {
|
||||||
@@ -105,20 +109,28 @@ void PatchesDialog::Save() {
|
|||||||
|
|
||||||
auto document = QJsonDocument::fromJson(input.readAll());
|
auto document = QJsonDocument::fromJson(input.readAll());
|
||||||
input.close();
|
input.close();
|
||||||
auto root = document.object();
|
const auto changed_message = tr("Cheat file changed; reload the dialog.");
|
||||||
auto patches = root.value(QStringLiteral("patches")).toArray();
|
if (!document.isObject()) {
|
||||||
for (int index = 0; index < patches.size(); index++) {
|
m_status->setText(changed_message);
|
||||||
auto patch = patches[index].toObject();
|
return;
|
||||||
patch.insert(QStringLiteral("enabled"),
|
|
||||||
m_patches->item(index)->checkState() == Qt::Checked);
|
|
||||||
patches[index] = patch;
|
|
||||||
}
|
}
|
||||||
root.insert(QStringLiteral("patches"), patches);
|
auto root = document.object();
|
||||||
|
auto mods = root.value(QStringLiteral("mods")).toArray();
|
||||||
|
if (mods.size() != m_patches->count()) {
|
||||||
|
m_status->setText(changed_message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int index = 0; index < mods.size(); index++) {
|
||||||
|
auto mod = mods[index].toObject();
|
||||||
|
mod.insert(QStringLiteral("enabled"), m_patches->item(index)->checkState() == Qt::Checked);
|
||||||
|
mods[index] = mod;
|
||||||
|
}
|
||||||
|
root.insert(QStringLiteral("mods"), mods);
|
||||||
document.setObject(root);
|
document.setObject(root);
|
||||||
|
|
||||||
QSaveFile output(path);
|
QSaveFile output(path);
|
||||||
if (output.open(QIODevice::WriteOnly) && output.write(document.toJson()) >= 0 &&
|
if (output.open(QIODevice::WriteOnly) && output.write(document.toJson()) >= 0 &&
|
||||||
output.commit()) {
|
output.commit()) {
|
||||||
m_status->setText(tr("Patch selection saved."));
|
m_status->setText(tr("Cheat selection saved."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+211
-139
@@ -8,207 +8,235 @@
|
|||||||
#include "loader/systemContent.h"
|
#include "loader/systemContent.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <charconv>
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <memory>
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <utility>
|
||||||
#include <system_error>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace Loader::GamePatch {
|
namespace Loader::GamePatch {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
constexpr uint64_t kPageSize = 0x4000;
|
||||||
|
|
||||||
struct Write {
|
struct Write {
|
||||||
std::string patch_name;
|
uint64_t source_address = 0;
|
||||||
uint64_t address = 0;
|
uint64_t target_address = 0;
|
||||||
std::vector<uint8_t> expected;
|
std::vector<uint8_t> off;
|
||||||
std::vector<uint8_t> replacement;
|
std::vector<uint8_t> on;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Plan {
|
struct Plan {
|
||||||
std::string title_id;
|
std::string title_id;
|
||||||
std::string game_version;
|
std::string version;
|
||||||
std::string process;
|
std::string process;
|
||||||
std::vector<Write> writes;
|
std::vector<Write> writes;
|
||||||
std::vector<std::string> patch_names;
|
std::vector<std::string> mod_names;
|
||||||
|
std::vector<uint64_t> cave_pages;
|
||||||
};
|
};
|
||||||
|
|
||||||
using Json = nlohmann::json;
|
using Json = nlohmann::json;
|
||||||
|
|
||||||
|
std::unique_ptr<Plan> g_pending_plan;
|
||||||
|
std::vector<uint64_t> g_applied_cave_pages;
|
||||||
|
|
||||||
bool Fail(std::string* error, std::string message) {
|
bool Fail(std::string* error, std::string message) {
|
||||||
*error = std::move(message);
|
*error = std::move(message);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Json::string_t* StringField(const Json& object, const char* name) {
|
bool ParseBytes(const std::string& text, std::vector<uint8_t>* bytes) {
|
||||||
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) {
|
if (text.empty() || (text.size() % 2) != 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes->resize(text.size() / 2);
|
bytes->resize(text.size() / 2);
|
||||||
for (size_t index = 0; index < bytes->size(); index++) {
|
for (size_t index = 0; index < bytes->size(); index++) {
|
||||||
unsigned int value = 0;
|
(*bytes)[index] = static_cast<uint8_t>(std::stoul(text.substr(index * 2, 2), nullptr, 16));
|
||||||
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ReadJson(const std::filesystem::path& path, Json* root, std::string* error) {
|
bool LoadPlan(const std::filesystem::path& path, Plan* plan, std::string* error) {
|
||||||
std::ifstream file(path, std::ios::binary);
|
std::ifstream file(path, std::ios::binary);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return Fail(error, "could not read patch plan");
|
return Fail(error, "could not read cheat file");
|
||||||
}
|
}
|
||||||
|
|
||||||
*root = Json::parse(file, nullptr, false);
|
const auto root = Json::parse(file, nullptr, false);
|
||||||
return root->is_object() || Fail(error, "patch plan is not valid JSON");
|
if (!root.is_object() || !root.contains("id") || !root.contains("version") ||
|
||||||
}
|
!root.contains("process") || !root.contains("mods")) {
|
||||||
|
return Fail(error, "expected a GoldHEN-style mods JSON file");
|
||||||
bool LoadPlan(const std::filesystem::path& path, Plan* plan, std::string* error) {
|
|
||||||
Json root;
|
|
||||||
if (!ReadJson(path, &root, error)) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
plan->title_id = root["id"].get<std::string>();
|
||||||
|
plan->version = root["version"].get<std::string>();
|
||||||
|
plan->process = root["process"].get<std::string>();
|
||||||
|
|
||||||
const auto* title_id = StringField(root, "title_id");
|
for (const auto& mod: root["mods"]) {
|
||||||
const auto* game_version = StringField(root, "game_version");
|
if (!mod.value("enabled", true)) {
|
||||||
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
const auto name = mod["name"].get<std::string>();
|
||||||
const auto* name = StringField(patch_json, "name");
|
plan->mod_names.push_back(name);
|
||||||
const auto* writes = ArrayField(patch_json, "writes");
|
for (const auto& entry: mod["memory"]) {
|
||||||
if (name == nullptr || writes == nullptr) {
|
Write write;
|
||||||
return Fail(error, "invalid patch entry");
|
write.source_address = std::stoull(entry["offset"].get<std::string>(), nullptr, 16);
|
||||||
}
|
if (!ParseBytes(entry["off"].get<std::string>(), &write.off) ||
|
||||||
|
!ParseBytes(entry["on"].get<std::string>(), &write.on) ||
|
||||||
plan->patch_names.push_back(*name);
|
write.off.size() != write.on.size()) {
|
||||||
for (const auto& write_json: *writes) {
|
return Fail(error, "invalid mod memory bytes");
|
||||||
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));
|
plan->writes.push_back(std::move(write));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ValidateTarget(const Plan& plan, const Program* program, std::string* error) {
|
bool ValidateGame(const Plan& plan, const Program* main_program, std::string* error) {
|
||||||
if (program == nullptr || program->elf == nullptr || program->base_vaddr == 0) {
|
if (main_program == nullptr || main_program->base_vaddr == 0) {
|
||||||
return Fail(error, "main executable is not loaded");
|
return Fail(error, "main executable is not loaded");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string title_id;
|
std::string title_id;
|
||||||
std::string game_version;
|
std::string version;
|
||||||
if (!SystemContentParamSfoGetString("TITLE_ID", &title_id) ||
|
if (!SystemContentParamSfoGetString("TITLE_ID", &title_id) ||
|
||||||
!SystemContentParamSfoGetString("APP_VER", &game_version)) {
|
!SystemContentParamSfoGetString("APP_VER", &version) ||
|
||||||
return Fail(error, "game metadata is incomplete");
|
!Common::EqualNoCase(main_program->file_name.filename().string(), plan.process) ||
|
||||||
}
|
!Common::EqualNoCase(title_id, plan.title_id) || version != plan.version) {
|
||||||
if (!Common::EqualNoCase(program->file_name.filename().string(), plan.process) ||
|
return Fail(error, "cheat file does not match the loaded game");
|
||||||
!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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ResolveWrite(const Program& program, Write* write, std::string* error) {
|
void Translate(const Program& program, uint64_t source_base, uint64_t source_address,
|
||||||
const auto* ehdr = program.elf->GetEhdr();
|
uint64_t* target_address) {
|
||||||
const auto* phdr = program.elf->GetPhdr();
|
if (source_address >= source_base) {
|
||||||
|
*target_address = program.base_vaddr + source_address - source_base;
|
||||||
uint64_t match = 0;
|
return;
|
||||||
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 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
*target_address = program.base_vaddr - (source_base - source_address);
|
||||||
::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) {
|
bool IsInsideProgram(const Program& program, uint64_t address, size_t size) {
|
||||||
|
return size != 0 && address >= program.base_vaddr &&
|
||||||
|
address + size <= program.base_vaddr + program.mapped_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsZero(const std::vector<uint8_t>& bytes) {
|
||||||
|
return std::all_of(bytes.begin(), bytes.end(), [](uint8_t byte) { return byte == 0; });
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TranslateAndValidate(Plan* plan, const Program& program, uint64_t source_base) {
|
||||||
for (auto& write: plan->writes) {
|
for (auto& write: plan->writes) {
|
||||||
if (!ResolveWrite(program, &write, error)) {
|
Translate(program, source_base, write.source_address, &write.target_address);
|
||||||
|
if (!IsZero(write.off) &&
|
||||||
|
(!IsInsideProgram(program, write.target_address, write.off.size()) ||
|
||||||
|
std::memcmp(reinterpret_cast<const void*>(write.target_address), write.off.data(),
|
||||||
|
write.off.size()) != 0)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ApplyWrites(Plan* plan, std::string* error) {
|
bool Matches(Plan* plan, const Program& program, uint64_t* source_base) {
|
||||||
for (auto& write: plan->writes) {
|
const auto anchor = std::find_if(plan->writes.begin(), plan->writes.end(),
|
||||||
const auto size = write.replacement.size();
|
[](const Write& write) { return !IsZero(write.off); });
|
||||||
std::memcpy(reinterpret_cast<void*>(write.address), write.replacement.data(), size);
|
if (anchor == plan->writes.end() || program.mapped_size < anchor->off.size()) {
|
||||||
if (!Common::VirtualMemory::FlushInstructionCache(write.address, size)) {
|
return false;
|
||||||
return Fail(error, "could not finalize patch");
|
}
|
||||||
|
|
||||||
|
const auto* ehdr = program.elf->GetEhdr();
|
||||||
|
const auto* phdr = program.elf->GetPhdr();
|
||||||
|
for (Elf64_Half index = 0; index < ehdr->e_phnum; index++) {
|
||||||
|
const auto& segment = phdr[index];
|
||||||
|
if ((segment.p_type != PT_LOAD && segment.p_type != PT_OS_RELRO) ||
|
||||||
|
segment.p_filesz < anchor->off.size()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* begin = reinterpret_cast<const uint8_t*>(program.base_vaddr + segment.p_vaddr);
|
||||||
|
const auto* end = begin + segment.p_filesz;
|
||||||
|
for (auto* current = begin; current < end;) {
|
||||||
|
const auto* found = std::search(current, end, anchor->off.begin(), anchor->off.end());
|
||||||
|
if (found == end) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const auto rva = static_cast<uint64_t>(found - begin) + segment.p_vaddr;
|
||||||
|
if (anchor->source_address >= rva) {
|
||||||
|
*source_base = anchor->source_address - rva;
|
||||||
|
if (TranslateAndValidate(plan, program, *source_base)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = found + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReleaseCaves(std::vector<uint64_t>* cave_pages) {
|
||||||
|
for (auto page = cave_pages->rbegin(); page != cave_pages->rend(); ++page) {
|
||||||
|
EXIT_IF(!Libs::LibKernel::Memory::FreeGuestMemory(*page, kPageSize));
|
||||||
|
}
|
||||||
|
cave_pages->clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AllocateCave(Plan* plan, const Program& program, std::string* error) {
|
||||||
|
std::vector<uint64_t> pages;
|
||||||
|
for (const auto& write: plan->writes) {
|
||||||
|
if (IsInsideProgram(program, write.target_address, write.off.size())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!IsZero(write.off)) {
|
||||||
|
return Fail(error, "non-zero write is outside the matched module");
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto write_end = write.target_address + write.off.size();
|
||||||
|
for (auto page = write.target_address & ~(kPageSize - 1); page < write_end;
|
||||||
|
page += kPageSize) {
|
||||||
|
if (std::find(pages.begin(), pages.end(), page) == pages.end()) {
|
||||||
|
pages.push_back(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto page: pages) {
|
||||||
|
const auto allocated = Libs::LibKernel::Memory::AllocateRuntimeMemory(
|
||||||
|
page, kPageSize, Common::VirtualMemory::Mode::ExecuteReadWrite, "game_cheat_code_cave",
|
||||||
|
true);
|
||||||
|
if (allocated != page) {
|
||||||
|
if (allocated != 0) {
|
||||||
|
Libs::LibKernel::Memory::FreeGuestMemory(allocated, kPageSize);
|
||||||
|
}
|
||||||
|
ReleaseCaves(&plan->cave_pages);
|
||||||
|
return Fail(error, "could not allocate code-cave page");
|
||||||
|
}
|
||||||
|
plan->cave_pages.push_back(page);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ApplyWrites(Plan* plan, const Program& program, std::string* error) {
|
||||||
|
if (!AllocateCave(plan, program, error)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int external = 1; external >= 0; external--) {
|
||||||
|
for (const auto& write: plan->writes) {
|
||||||
|
if (static_cast<int>(
|
||||||
|
!IsInsideProgram(program, write.target_address, write.on.size())) != external) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::memcpy(reinterpret_cast<void*>(write.target_address), write.on.data(),
|
||||||
|
write.on.size());
|
||||||
|
if (!Common::VirtualMemory::FlushInstructionCache(write.target_address,
|
||||||
|
write.on.size())) {
|
||||||
|
return Fail(error, "could not finalize cheat write");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -216,19 +244,63 @@ bool ApplyWrites(Plan* plan, std::string* error) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool Apply(const std::filesystem::path& plan_path, Program* program) {
|
bool Apply(const std::filesystem::path& plan_path, Program* main_program,
|
||||||
|
const std::vector<Program*>& programs) {
|
||||||
Plan plan;
|
Plan plan;
|
||||||
std::string error;
|
std::string error;
|
||||||
if (LoadPlan(plan_path, &plan, &error) && ValidateTarget(plan, program, &error) &&
|
if (!LoadPlan(plan_path, &plan, &error) || !ValidateGame(plan, main_program, &error)) {
|
||||||
PrepareWrites(&plan, *program, &error) && ApplyWrites(&plan, &error)) {
|
::printf("Game cheat error: %s\n", error.c_str());
|
||||||
for (const auto& name: plan.patch_names) {
|
return false;
|
||||||
::printf("Successfully applied patch: %s\n", name.c_str());
|
}
|
||||||
}
|
if (plan.writes.empty()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
::printf("Game patch error: %s\n", error.c_str());
|
g_pending_plan = std::make_unique<Plan>(std::move(plan));
|
||||||
return false;
|
for (auto* program: programs) {
|
||||||
|
if (!ApplyPending(program)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (g_pending_plan == nullptr) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
::printf("Game cheat: waiting for the matching module\n");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ApplyPending(Program* program) {
|
||||||
|
uint64_t source_base = 0;
|
||||||
|
if (g_pending_plan == nullptr || program == nullptr || program->elf == nullptr ||
|
||||||
|
!Matches(g_pending_plan.get(), *program, &source_base)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
::printf("Game cheat: matched %s with source base 0x%llx\n",
|
||||||
|
program->file_name.filename().string().c_str(),
|
||||||
|
static_cast<unsigned long long>(source_base));
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
if (!ApplyWrites(g_pending_plan.get(), *program, &error)) {
|
||||||
|
ReleaseCaves(&g_pending_plan->cave_pages);
|
||||||
|
g_pending_plan.reset();
|
||||||
|
::printf("Game cheat error: %s\n", error.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& name: g_pending_plan->mod_names) {
|
||||||
|
::printf("Successfully applied cheat: %s\n", name.c_str());
|
||||||
|
}
|
||||||
|
g_applied_cave_pages = std::move(g_pending_plan->cave_pages);
|
||||||
|
g_pending_plan.reset();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Clear() {
|
||||||
|
if (g_pending_plan != nullptr) {
|
||||||
|
ReleaseCaves(&g_pending_plan->cave_pages);
|
||||||
|
}
|
||||||
|
g_pending_plan.reset();
|
||||||
|
ReleaseCaves(&g_applied_cave_pages);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Loader::GamePatch
|
} // namespace Loader::GamePatch
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "common/common.h"
|
#include "common/common.h"
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace Loader {
|
namespace Loader {
|
||||||
|
|
||||||
@@ -11,7 +12,10 @@ struct Program;
|
|||||||
|
|
||||||
namespace GamePatch {
|
namespace GamePatch {
|
||||||
|
|
||||||
bool Apply(const std::filesystem::path& plan_path, Program* program);
|
bool Apply(const std::filesystem::path& plan_path, Program* main_program,
|
||||||
|
const std::vector<Program*>& programs);
|
||||||
|
bool ApplyPending(Program* program);
|
||||||
|
void Clear();
|
||||||
|
|
||||||
} // namespace GamePatch
|
} // namespace GamePatch
|
||||||
|
|
||||||
|
|||||||
@@ -1289,6 +1289,9 @@ void RuntimeLinker::RelocateProgram(Program* program) {
|
|||||||
EXIT_IF(std::find(m_programs.begin(), m_programs.end(), program) == m_programs.end());
|
EXIT_IF(std::find(m_programs.begin(), m_programs.end(), program) == m_programs.end());
|
||||||
|
|
||||||
Relocate(program);
|
Relocate(program);
|
||||||
|
if (!GamePatch::ApplyPending(program)) {
|
||||||
|
EXIT("Failed to apply pending game cheat\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void RuntimeLinker::UnloadProgram(Program* program) {
|
void RuntimeLinker::UnloadProgram(Program* program) {
|
||||||
@@ -1412,7 +1415,10 @@ void RuntimeLinker::Execute(const std::filesystem::path& game_patch) {
|
|||||||
RelocateAll();
|
RelocateAll();
|
||||||
|
|
||||||
if (!game_patch.empty()) {
|
if (!game_patch.empty()) {
|
||||||
GamePatch::Apply(game_patch, m_programs.empty() ? nullptr : m_programs.front());
|
if (!GamePatch::Apply(game_patch, m_programs.empty() ? nullptr : m_programs.front(),
|
||||||
|
m_programs)) {
|
||||||
|
EXIT("Failed to apply game cheat\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
StartAllModules();
|
StartAllModules();
|
||||||
|
|
||||||
@@ -1436,6 +1442,7 @@ void RuntimeLinker::Clear() {
|
|||||||
// EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
|
// EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
|
||||||
|
|
||||||
Common::LockGuard lock(m_mutex);
|
Common::LockGuard lock(m_mutex);
|
||||||
|
GamePatch::Clear();
|
||||||
|
|
||||||
for (auto* p: m_programs) {
|
for (auto* p: m_programs) {
|
||||||
DeleteProgram(p);
|
DeleteProgram(p);
|
||||||
|
|||||||
+3
-3
@@ -43,15 +43,15 @@ 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(
|
::printf(" --game-patch <json> ETAHen cheat file.\n");
|
||||||
" --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(
|
::printf(
|
||||||
" --user-name <name> Local user name (1-16 bytes). Default: Kyty.\n");
|
" --user-name <name> Local user name (1-16 bytes). Default: Kyty.\n");
|
||||||
::printf(" --user-id <num> Local user ID. Default: %d.\n",
|
::printf(" --user-id <num> Local user ID. Default: %d.\n",
|
||||||
Config::DEFAULT_USER_ID);
|
Config::DEFAULT_USER_ID);
|
||||||
::printf(" --present-mode <value> Fifo, Mailbox, or Immediate. Default: Fifo.\n");
|
::printf(
|
||||||
|
" --present-mode <value> Fifo, Mailbox, or Immediate. Default: Fifo.\n");
|
||||||
::printf(" --fullscreen Run in borderless desktop fullscreen.\n");
|
::printf(" --fullscreen Run in borderless desktop fullscreen.\n");
|
||||||
::printf(" --vblank-frequency <num> Virtual vblank frequency. Default: 60.\n");
|
::printf(" --vblank-frequency <num> Virtual vblank frequency. Default: 60.\n");
|
||||||
::printf(" --console-language <0-29> Console language. Default: 1 (English US).\n");
|
::printf(" --console-language <0-29> Console language. Default: 1 (English US).\n");
|
||||||
|
|||||||
Reference in New Issue
Block a user