From 8e39c778a15b6315f395f8aa2f1203a64a8d2efb Mon Sep 17 00:00:00 2001 From: RipleyTom Date: Tue, 28 May 2019 21:14:26 +0200 Subject: [PATCH] Cheat engine --- rpcs3/rpcs3.vcxproj | 39 ++ rpcs3/rpcs3.vcxproj.filters | 21 + rpcs3/rpcs3qt/CMakeLists.txt | 1 + rpcs3/rpcs3qt/cheat_manager.cpp | 949 ++++++++++++++++++++++++++++++++ rpcs3/rpcs3qt/cheat_manager.h | 112 ++++ rpcs3/rpcs3qt/main_window.cpp | 7 + rpcs3/rpcs3qt/main_window.ui | 6 + 7 files changed, 1135 insertions(+) create mode 100644 rpcs3/rpcs3qt/cheat_manager.cpp create mode 100644 rpcs3/rpcs3qt/cheat_manager.h diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index 5d8d03e534..d05dfa56bc 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -372,6 +372,11 @@ true true + + true + true + true + true true @@ -562,6 +567,11 @@ true true + + true + true + true + true true @@ -772,6 +782,11 @@ true true + + true + true + true + true true @@ -962,6 +977,11 @@ true true + + true + true + true + true true @@ -1125,6 +1145,7 @@ + @@ -1600,6 +1621,24 @@ .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DQT_OPENGL_LIB -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_QML_LIB -DQT_NETWORK_LIB -DQT_CORE_LIB -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\minidx12\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtOpenGL" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtQml" "-I$(QTDIR)\include\QtNetwork" "-I$(QTDIR)\include\QtCore" "-I.\debug" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" + + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_OPENGL_LIB -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_QML_LIB -DQT_NETWORK_LIB -DQT_CORE_LIB -DNDEBUG -DQT_WINEXTRAS_LIB "-DBRANCH=$(BRANCH)" -D%(PreprocessorDefinitions) "-I.\..\3rdparty\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\minidx12\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtOpenGL" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtQml" "-I$(QTDIR)\include\QtNetwork" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DQT_OPENGL_LIB -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_QML_LIB -DQT_NETWORK_LIB -DQT_CORE_LIB -DQT_WINEXTRAS_LIB -D%(PreprocessorDefinitions) "-I$(VULKAN_SDK)\Include" "-I.\.." "-I.\..\3rdparty\minidx12\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtOpenGL" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtQml" "-I$(QTDIR)\include\QtNetwork" "-I$(QTDIR)\include\QtCore" "-I.\debug" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_OPENGL_LIB -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_QML_LIB -DQT_NETWORK_LIB -DQT_CORE_LIB -DNDEBUG -DQT_WINEXTRAS_LIB -D%(PreprocessorDefinitions) "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\minidx12\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtOpenGL" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtQml" "-I$(QTDIR)\include\QtNetwork" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DQT_OPENGL_LIB -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_QML_LIB -DQT_NETWORK_LIB -DQT_CORE_LIB -DQT_WINEXTRAS_LIB -D%(PreprocessorDefinitions) "-I$(VULKAN_SDK)\Include" "-I.\.." "-I.\..\3rdparty\minidx12\Include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtOpenGL" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtQml" "-I$(QTDIR)\include\QtNetwork" "-I$(QTDIR)\include\QtCore" "-I.\debug" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" + $(QTDIR)\bin\moc.exe;%(FullPath) diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index ebc8e7a629..f2c99cd1e9 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -117,6 +117,9 @@ {ccb05d09-d394-493b-8b08-bde054969da0} + + {73853473-9d11-4771-b8d8-d06ea25e2ead} + @@ -770,6 +773,21 @@ Gui + + Gui\cheat manager + + + Generated Files\Release - LLVM + + + Generated Files\Debug + + + Generated Files\Release + + + Generated Files\Debug - LLVM + @@ -1020,6 +1038,9 @@ Gui\update manager + + Gui\cheat manager + diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index cdb7f6f1d9..46dfef6ca4 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -4,6 +4,7 @@ breakpoint_handler.cpp breakpoint_list.cpp cg_disasm_window.cpp + cheat_manager.cpp custom_dialog.cpp debugger_frame.cpp debugger_list.cpp diff --git a/rpcs3/rpcs3qt/cheat_manager.cpp b/rpcs3/rpcs3qt/cheat_manager.cpp new file mode 100644 index 0000000000..5b805ca2f6 --- /dev/null +++ b/rpcs3/rpcs3qt/cheat_manager.cpp @@ -0,0 +1,949 @@ +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "cheat_manager.h" + +#include "Emu/System.h" +#include "Emu/Memory/vm.h" +#include "Emu/CPU/CPUThread.h" + +#include "Emu/IdManager.h" +#include "Emu/Cell/PPUAnalyser.h" + +#include "Utilities/StrUtil.h" + +LOG_CHANNEL(log_cheat); + +cheat_manager_dialog* cheat_manager_dialog::inst = nullptr; + +template <> +void fmt_class_string::format(std::string& out, u64 arg) +{ + format_enum(out, arg, [](cheat_type value) { + switch (value) + { + case cheat_type::unsigned_8_cheat: return "Unsigned 8 bits"; + case cheat_type::unsigned_16_cheat: return "Unsigned 16 bits"; + case cheat_type::unsigned_32_cheat: return "Unsigned 32 bits"; + case cheat_type::unsigned_64_cheat: return "Unsigned 64 bits"; + case cheat_type::signed_8_cheat: return "Signed 8 bits"; + case cheat_type::signed_16_cheat: return "Signed 16 bits"; + case cheat_type::signed_32_cheat: return "Signed 32 bits"; + case cheat_type::signed_64_cheat: return "Signed 64 bits"; + } + + return unknown; + }); +} + +namespace YAML +{ + template <> + struct convert + { + static bool decode(const Node& node, cheat_info& rhs) + { + if (node.size() != 3) + { + return false; + } + + rhs.description = node[0].as(); + u64 type64 = 0; + if (!cfg::try_to_enum_value(&type64, &fmt_class_string::format, node[1].Scalar())) + return false; + rhs.type = (cheat_type)type64; + rhs.red_script = node[2].as(); + return true; + } + }; +} // namespace YAML + +YAML::Emitter& operator<<(YAML::Emitter& out, const cheat_info& rhs) +{ + std::string type_formatted{}; + fmt_class_string::format(type_formatted, (u64)rhs.type); + + out << YAML::BeginSeq << rhs.description << type_formatted << rhs.red_script << YAML::EndSeq; + + return out; +} + +bool cheat_info::from_str(const std::string& cheat_line) +{ + auto cheat_vec = fmt::split(cheat_line, {"@@@"}, false); + + if (cheat_vec.size() != 5) + { + log_cheat.fatal("Failed to parse cheat line"); + return false; + } + + game = cheat_vec[0]; + description = cheat_vec[1]; + type = (cheat_type)std::stoul(cheat_vec[2]); + offset = std::stoul(cheat_vec[3]); + red_script = cheat_vec[4]; + + return true; +} + +std::string cheat_info::to_str() const +{ + std::string cheat_str = game + "@@@" + description + "@@@" + std::to_string((int)type) + "@@@" + std::to_string(offset) + "@@@" + red_script + "@@@"; + return cheat_str; +} + +cheat_engine::cheat_engine() +{ + try + { + YAML::Node yml_cheats = YAML::Load(fs::file{fs::get_config_dir() + cheats_filename, fs::read + fs::create}.to_string()); + + for (const auto& yml_cheat : yml_cheats) + { + const std::string& game_name = yml_cheat.first.Scalar(); + for (const auto& yml_offset : yml_cheat.second) + { + const u32 offset = yml_offset.first.as(); + cheat_info cheat = yml_offset.second.as(); + cheat.game = game_name; + cheat.offset = offset; + cheats[game_name][offset] = std::move(cheat); + } + } + } + catch (YAML::Exception& e) + { + log_cheat.error("Error parsing %s", cheats_filename); + } +} + +void cheat_engine::save() const +{ + fs::file cheat_file(fs::get_config_dir() + cheats_filename, fs::rewrite); + if (!cheat_file) + return; + + YAML::Emitter out; + + out << YAML::BeginMap; + for (const auto& game_entry : cheats) + { + out << game_entry.first; + out << YAML::BeginMap; + for (const auto& offset_entry : game_entry.second) + { + out << YAML::Hex << offset_entry.first; + out << offset_entry.second; + } + out << YAML::EndMap; + } + out << YAML::EndMap; + + cheat_file.write(out.c_str(), out.size()); +} + +void cheat_engine::import_cheats_from_str(const std::string& str_cheats) +{ + auto cheats_vec = fmt::split(str_cheats, {"^^^"}); + + for (auto& cheat_line : cheats_vec) + { + cheat_info new_cheat; + if (new_cheat.from_str(cheat_line)) + cheats[new_cheat.game][new_cheat.offset] = new_cheat; + } +} + +std::string cheat_engine::export_cheats_to_str() const +{ + std::string cheats_str; + + for (const auto& game : cheats) + { + for (const auto& offset : cheats.at(game.first)) + { + cheats_str += offset.second.to_str(); + cheats_str += "^^^"; + } + } + + return cheats_str; +} + +bool cheat_engine::exist(const std::string& name, const u32 offset) const +{ + if (cheats.count(name) && cheats.at(name).count(offset)) + return true; + + return false; +} + +void cheat_engine::add(const std::string& game, const std::string& description, const cheat_type type, const u32 offset, const std::string& red_script) +{ + cheats[game][offset] = cheat_info{game, description, type, offset, red_script}; +} + +cheat_info* cheat_engine::get(const std::string& game, const u32 offset) +{ + if (!exist(game, offset)) + return nullptr; + + return &cheats[game][offset]; +} + +bool cheat_engine::erase(const std::string& game, const u32 offset) +{ + if (!exist(game, offset)) + return false; + + cheats[game].erase(offset); + return true; +} + +bool cheat_engine::resolve_script(u32& final_offset, const u32 offset, const std::string& red_script) +{ + enum operand + { + operand_equal, + operand_add, + operand_sub + }; + + auto do_operation = [](const operand op, u32& param1, const u32 param2) -> u32 { + switch (op) + { + case operand_equal: return param1 = param2; + case operand_add: return param1 += param2; + case operand_sub: return param1 -= param2; + } + ASSERT(false); + }; + + operand cur_op = operand_equal; + u32 index = 0; + + while (index < red_script.size()) + { + if (red_script[index] >= '0' && red_script[index] <= '9') + { + std::string num_string; + for (; index < red_script.size(); index++) + { + if (red_script[index] < '0' || red_script[index] > '9') + break; + + num_string += red_script[index]; + } + + u32 num_value = std::stoul(num_string); + do_operation(cur_op, final_offset, num_value); + } + else + { + switch (red_script[index]) + { + case '$': + { + do_operation(cur_op, final_offset, offset); + index++; + break; + } + case '[': + { + // find corresponding ] + s32 found_close = 1; + std::string sub_script; + for (index++; index < red_script.size(); index++) + { + if (found_close == 0) + break; + + if (red_script[index] == ']') + found_close--; + else if (red_script[index] == '[') + found_close++; + + if (found_close != 0) + sub_script += red_script[index]; + } + + if (found_close) + return false; + + // Resolves content of [] + u32 res_addr = 0; + if (!resolve_script(res_addr, offset, sub_script)) + return false; + + // Tries to get value at resolved address + bool success; + u32 res_value = get_value(res_addr, success); + + if (!success) + return false; + + do_operation(cur_op, final_offset, res_value); + } + case '+': + cur_op = operand_add; + index++; + break; + case '-': + cur_op = operand_sub; + index++; + break; + case ' ': index++; break; + default: log_cheat.fatal("invalid character in redirection script"); return false; + } + } + } + + return true; +} + +template +std::vector cheat_engine::search(const T value, const std::vector& to_filter) +{ + std::vector results; + + be_t value_swapped = value; + + if (Emu.IsStopped()) + return {}; + + cpu_thread::suspend_all cpu_lock(nullptr); + + if (to_filter.size()) + { + for (const auto& off : to_filter) + { + if (vm::check_addr(off)) + { + if (*vm::get_super_ptr(off) == value_swapped) + results.push_back(off); + } + } + } + else + { + // Looks through mapped memory + for (u32 page_ind = (0x10000 / 4096); page_ind < (0xF0000000 / 4096); page_ind++) + { + if (vm::check_addr(page_ind * 4096)) + { + // Assumes the values are aligned + for (u32 index = 0; index < 4096; index += sizeof(T)) + { + if (*vm::get_super_ptr((page_ind * 4096) + index) == value_swapped) + results.push_back((page_ind * 4096) + index); + } + } + } + } + + return results; +} + +template +T cheat_engine::get_value(const u32 offset, bool& success) +{ + if (Emu.IsStopped()) + { + success = false; + return 0; + } + + cpu_thread::suspend_all cpu_lock(nullptr); + + if (!vm::check_addr(offset)) + { + success = false; + return 0; + } + + success = true; + + T ret_value = *vm::get_super_ptr(offset); + + return ret_value; +} + +template +bool cheat_engine::set_value(const u32 offset, const T value) +{ + if (Emu.IsStopped()) + return false; + + cpu_thread::suspend_all cpu_lock(nullptr); + + if (!vm::check_addr(offset)) + { + return false; + } + + *vm::get_super_ptr(offset) = value; + return true; +} + +bool cheat_engine::is_addr_safe(const u32 offset) +{ + if (Emu.IsStopped()) + return false; + + const auto ppum = g_fxo->get(); + if (!ppum) + { + log_cheat.fatal("Failed to get ppu_module"); + return false; + } + + std::vector> segs; + + for (const auto& seg : ppum->segs) + { + if ((seg.flags & 3)) + { + segs.push_back({seg.addr, seg.size}); + } + } + + if (!segs.size()) + { + log_cheat.fatal("Couldn't find a +rw-x section"); + return false; + } + + for (const auto& seg : segs) + { + if (offset >= seg.first && offset < (seg.first + seg.second)) + return true; + } + + return false; +} + +u32 cheat_engine::reverse_lookup(const u32 addr, const u32 max_offset, const u32 max_depth, const u32 cur_depth) +{ + u32 result; + + for (u32 index = 0; index <= max_offset; index += 4) + { + std::vector ptrs = search(addr - index, {}); + + log_cheat.fatal("Found %d pointer(s) for addr 0x%x [offset: %d cur_depth:%d]", ptrs.size(), addr, index, cur_depth); + + for (const auto& ptr : ptrs) + { + if (is_addr_safe(ptr)) + return ptr; + } + + // If depth has not been reached dig deeper + if (ptrs.size() && cur_depth < max_depth) + { + for (const auto& ptr : ptrs) + { + result = reverse_lookup(ptr, max_offset, max_depth, cur_depth + 1); + if (result) + return result; + } + } + } + + return 0; +} + +cheat_manager_dialog::cheat_manager_dialog(QWidget* parent) + : QDialog(parent) +{ + setWindowTitle(tr("Cheat Manager")); + setObjectName("cheat_manager"); + setMinimumSize(QSize(800, 400)); + + QVBoxLayout* main_layout = new QVBoxLayout(); + + tbl_cheats = new QTableWidget(this); + tbl_cheats->setSelectionMode(QAbstractItemView::SelectionMode::ExtendedSelection); + tbl_cheats->setSelectionBehavior(QAbstractItemView::SelectRows); + tbl_cheats->setContextMenuPolicy(Qt::CustomContextMenu); + tbl_cheats->setColumnCount(5); + tbl_cheats->setHorizontalHeaderLabels(QStringList() << tr("Game") << tr("Description") << tr("Type") << tr("Offset") << tr("Script")); + main_layout->addWidget(tbl_cheats); + + QHBoxLayout* btn_layout = new QHBoxLayout(); + QLabel* lbl_value_final = new QLabel(tr("Current Value:")); + edt_value_final = new QLineEdit(); + btn_apply = new QPushButton(tr("Apply"), this); + btn_apply->setEnabled(false); + btn_layout->addWidget(lbl_value_final); + btn_layout->addWidget(edt_value_final); + btn_layout->addWidget(btn_apply); + main_layout->addLayout(btn_layout); + + QGroupBox* grp_add_cheat = new QGroupBox(tr("Cheat Search")); + QVBoxLayout* grp_add_cheat_layout = new QVBoxLayout(); + QHBoxLayout* grp_add_cheat_sub_layout = new QHBoxLayout(); + QPushButton* btn_new_search = new QPushButton(tr("New Search")); + btn_filter_results = new QPushButton(tr("Filter Results")); + btn_filter_results->setEnabled(false); + edt_cheat_search_value = new QLineEdit(); + cbx_cheat_search_type = new QComboBox(); + + for (u64 i = 0; i <= (u64)cheat_type::signed_64_cheat; i++) + { + std::string type_formatted{}; + fmt_class_string::format(type_formatted, i); + cbx_cheat_search_type->insertItem(i, QString::fromStdString(type_formatted)); + } + cbx_cheat_search_type->setCurrentIndex((int)cheat_type::signed_32_cheat); + grp_add_cheat_sub_layout->addWidget(btn_new_search); + grp_add_cheat_sub_layout->addWidget(btn_filter_results); + grp_add_cheat_sub_layout->addWidget(edt_cheat_search_value); + grp_add_cheat_sub_layout->addWidget(cbx_cheat_search_type); + grp_add_cheat_layout->addLayout(grp_add_cheat_sub_layout); + lst_search = new QListWidget(this); + lst_search->setSelectionMode(QAbstractItemView::SelectionMode::SingleSelection); + lst_search->setSelectionBehavior(QAbstractItemView::SelectRows); + lst_search->setContextMenuPolicy(Qt::CustomContextMenu); + grp_add_cheat_layout->addWidget(lst_search); + grp_add_cheat->setLayout(grp_add_cheat_layout); + main_layout->addWidget(grp_add_cheat); + + setLayout(main_layout); + + // Edit/Manage UI + connect(tbl_cheats, &QTableWidget::itemClicked, [=](QTableWidgetItem* item) { + int row = tbl_cheats->currentRow(); + + if (row == -1) + return; + + cheat_info* cheat = g_cheat.get(tbl_cheats->item(row, 0)->text().toStdString(), tbl_cheats->item(row, 3)->data(Qt::UserRole).toUInt()); + if (cheat) + { + QString cur_value; + bool success; + + u32 final_offset; + if (cheat->red_script.size()) + { + final_offset = 0; + if (!cheat_engine::resolve_script(final_offset, cheat->offset, cheat->red_script)) + { + btn_apply->setEnabled(false); + edt_value_final->setText(tr("Failed to resolve redirection script")); + } + } + else + { + final_offset = cheat->offset; + } + + u64 result_value; + switch (cheat->type) + { + case cheat_type::unsigned_8_cheat: result_value = cheat_engine::get_value(final_offset, success); break; + case cheat_type::unsigned_16_cheat: result_value = cheat_engine::get_value(final_offset, success); break; + case cheat_type::unsigned_32_cheat: result_value = cheat_engine::get_value(final_offset, success); break; + case cheat_type::unsigned_64_cheat: result_value = cheat_engine::get_value(final_offset, success); break; + case cheat_type::signed_8_cheat: result_value = cheat_engine::get_value(final_offset, success); break; + case cheat_type::signed_16_cheat: result_value = cheat_engine::get_value(final_offset, success); break; + case cheat_type::signed_32_cheat: result_value = cheat_engine::get_value(final_offset, success); break; + case cheat_type::signed_64_cheat: result_value = cheat_engine::get_value(final_offset, success); break; + default: log_cheat.fatal("Unsupported cheat type"); return; + } + + if (success) + { + if (cheat->type >= cheat_type::signed_8_cheat && cheat->type <= cheat_type::signed_64_cheat) + cur_value = tr("%1").arg((s64)result_value); + else + cur_value = tr("%1").arg(result_value); + + btn_apply->setEnabled(true); + } + else + { + cur_value = tr("Failed to get the value from memory"); + btn_apply->setEnabled(false); + } + + edt_value_final->setText(cur_value); + } + else + { + log_cheat.fatal("Failed to retrieve cheat selected from internal cheat_engine"); + } + }); + + connect(tbl_cheats, &QTableWidget::cellChanged, [=](int row, int column) { + if (column != 1 && column != 4) + { + log_cheat.fatal("A column other than description and script was edited"); + return; + } + + cheat_info* cheat = g_cheat.get(tbl_cheats->item(row, 0)->text().toStdString(), tbl_cheats->item(row, 3)->data(Qt::UserRole).toUInt()); + if (!cheat) + { + log_cheat.fatal("Failed to retrieve cheat edited from internal cheat_engine"); + return; + } + + switch (column) + { + case 1: cheat->description = tbl_cheats->item(row, 1)->text().toStdString(); break; + case 4: cheat->red_script = tbl_cheats->item(row, 4)->text().toStdString(); break; + default: break; + } + + g_cheat.save(); + }); + + connect(tbl_cheats, &QTableWidget::customContextMenuRequested, [=](const QPoint& loc) { + QPoint globalPos = tbl_cheats->mapToGlobal(loc); + QMenu* menu = new QMenu(); + QAction* delete_cheats = new QAction(tr("Delete"), menu); + QAction* import_cheats = new QAction(tr("Import Cheats")); + QAction* export_cheats = new QAction(tr("Export Cheats")); + QAction* reverse_cheat = new QAction(tr("Reverse-Lookup Cheat")); + + connect(delete_cheats, &QAction::triggered, [=]() { + const auto selected = tbl_cheats->selectedItems(); + + std::set rows; + + for (const auto& sel : selected) + { + const int row = sel->row(); + + if (rows.count(row)) + continue; + + g_cheat.erase(tbl_cheats->item(row, 0)->text().toStdString(), tbl_cheats->item(row, 3)->data(Qt::UserRole).toUInt()); + rows.insert(row); + } + + update_cheat_list(); + }); + + connect(import_cheats, &QAction::triggered, [=]() { + QClipboard* clipboard = QGuiApplication::clipboard(); + g_cheat.import_cheats_from_str(clipboard->text().toStdString()); + update_cheat_list(); + }); + + connect(export_cheats, &QAction::triggered, [=]() { + const auto selected = tbl_cheats->selectedItems(); + + std::set rows; + std::string export_string; + + for (const auto& sel : selected) + { + const int row = sel->row(); + + if (rows.count(row)) + continue; + + cheat_info* cheat = g_cheat.get(tbl_cheats->item(row, 0)->text().toStdString(), tbl_cheats->item(row, 3)->data(Qt::UserRole).toUInt()); + if (cheat) + export_string += cheat->to_str() + "^^^"; + + rows.insert(row); + } + + QClipboard* clipboard = QGuiApplication::clipboard(); + clipboard->setText(QString::fromStdString(export_string)); + }); + + connect(reverse_cheat, &QAction::triggered, [=]() { + QTableWidgetItem* item = tbl_cheats->item(tbl_cheats->currentRow(), 3); + if (item) + { + u32 offset = item->data(Qt::UserRole).toUInt(); + u32 result = cheat_engine::reverse_lookup(offset, 32, 12); + + log_cheat.fatal("Result is 0x%x", result); + } + }); + + menu->addAction(delete_cheats); + menu->addSeparator(); + // menu->addAction(reverse_cheat); + // menu->addSeparator(); + menu->addAction(import_cheats); + menu->addAction(export_cheats); + menu->exec(globalPos); + }); + + connect(btn_apply, &QPushButton::clicked, [=](int action) { + int row = tbl_cheats->currentRow(); + cheat_info* cheat = g_cheat.get(tbl_cheats->item(row, 0)->text().toStdString(), tbl_cheats->item(row, 3)->data(Qt::UserRole).toUInt()); + + if (!cheat) + { + log_cheat.fatal("Failed to retrieve cheat selected from internal cheat_engine"); + return; + } + + std::pair results; + + u32 final_offset; + if (cheat->red_script.size()) + { + final_offset = 0; + if (!g_cheat.resolve_script(final_offset, cheat->offset, cheat->red_script)) + { + btn_apply->setEnabled(false); + edt_value_final->setText(tr("Failed to resolve redirection script")); + } + } + else + { + final_offset = cheat->offset; + } + + // TODO: better way to do this? + switch ((cheat_type)cbx_cheat_search_type->currentIndex()) + { + case cheat_type::unsigned_8_cheat: results = convert_and_set(final_offset); break; + case cheat_type::unsigned_16_cheat: results = convert_and_set(final_offset); break; + case cheat_type::unsigned_32_cheat: results = convert_and_set(final_offset); break; + case cheat_type::unsigned_64_cheat: results = convert_and_set(final_offset); break; + case cheat_type::signed_8_cheat: results = convert_and_set(final_offset); break; + case cheat_type::signed_16_cheat: results = convert_and_set(final_offset); break; + case cheat_type::signed_32_cheat: results = convert_and_set(final_offset); break; + case cheat_type::signed_64_cheat: results = convert_and_set(final_offset); break; + default: log_cheat.fatal("Unsupported cheat type"); return; + } + + if (!results.first) + { + QMessageBox::warning(this, tr("Error converting value"), tr("Couldn't convert the value you typed to the integer type of that cheat"), QMessageBox::Ok); + return; + } + + if (!results.second) + { + QMessageBox::warning(this, tr("Error applying value"), tr("Couldn't patch memory"), QMessageBox::Ok); + return; + } + }); + + // Search UI + connect(btn_new_search, &QPushButton::clicked, [=](int action) { + offsets_found.clear(); + do_the_search(); + }); + + connect(btn_filter_results, &QPushButton::clicked, [=](int action) { do_the_search(); }); + + connect(lst_search, &QListWidget::customContextMenuRequested, [=](const QPoint& loc) { + QPoint globalPos = lst_search->mapToGlobal(loc); + QListWidgetItem* item = lst_search->item(lst_search->currentRow()); + if (!item) + return; + + QMenu* menu = new QMenu(); + + QAction* add_to_cheat_list = new QAction(tr("Add to cheat list"), menu); + + u32 offset = offsets_found[lst_search->currentRow()]; + cheat_type type = (cheat_type)cbx_cheat_search_type->currentIndex(); + std::string name = Emu.GetTitle(); + + connect(add_to_cheat_list, &QAction::triggered, [=]() { + if (g_cheat.exist(name, offset)) + { + if (QMessageBox::question(this, tr("Cheat already exist"), tr("Do you want to overwrite the existing cheat?"), QMessageBox::Ok | QMessageBox::Cancel) != QMessageBox::Ok) + return; + } + + std::string comment; + if (!cheat_engine::is_addr_safe(offset)) + comment = "Unsafe"; + + g_cheat.add(name, comment, type, offset, ""); + update_cheat_list(); + }); + + menu->addAction(add_to_cheat_list); + menu->exec(globalPos); + }); + + update_cheat_list(); +} + +cheat_manager_dialog::~cheat_manager_dialog() +{ + inst = nullptr; +} + +cheat_manager_dialog* cheat_manager_dialog::get_dlg(QWidget* parent) +{ + if (inst == nullptr) + inst = new cheat_manager_dialog(parent); + + return inst; +} + +template +T cheat_manager_dialog::convert_from_QString(QString& str, bool& success) +{ + T result; + + if constexpr (std::is_same::value) + { + u16 result_16 = str.toUShort(&success); + + if (result_16 > 0xFF) + success = false; + + result = static_cast(result_16); + } + + if constexpr (std::is_same::value) + result = str.toUShort(&success); + + if constexpr (std::is_same::value) + result = str.toUInt(&success); + + if constexpr (std::is_same::value) + result = str.toULongLong(&success); + + if constexpr (std::is_same::value) + { + s16 result_16 = str.toShort(&success); + if (result_16 < -128 || result_16 > 127) + success = false; + + result = static_cast(result_16); + } + + if constexpr (std::is_same::value) + result = str.toShort(&success); + + if constexpr (std::is_same::value) + result = str.toInt(&success); + + if constexpr (std::is_same::value) + result = str.toLongLong(&success); + + return result; +} + +template +bool cheat_manager_dialog::convert_and_search() +{ + T value; + bool res_conv; + QString to_search = edt_cheat_search_value->text(); + + value = convert_from_QString(to_search, res_conv); + + if (!res_conv) + return false; + + offsets_found = cheat_engine::search(value, offsets_found); + return true; +} + +template +std::pair cheat_manager_dialog::convert_and_set(u32 offset) +{ + T value; + bool res_conv; + QString to_set = edt_value_final->text(); + + value = convert_from_QString(to_set, res_conv); + + if (!res_conv) + return {false, false}; + + return {true, cheat_engine::set_value(offset, value)}; +} + +void cheat_manager_dialog::do_the_search() +{ + bool res_conv = false; + QString qstr_to_search = edt_cheat_search_value->text(); + + // TODO: better way to do this? + switch ((cheat_type)cbx_cheat_search_type->currentIndex()) + { + case cheat_type::unsigned_8_cheat: res_conv = convert_and_search(); break; + case cheat_type::unsigned_16_cheat: res_conv = convert_and_search(); break; + case cheat_type::unsigned_32_cheat: res_conv = convert_and_search(); break; + case cheat_type::unsigned_64_cheat: res_conv = convert_and_search(); break; + case cheat_type::signed_8_cheat: res_conv = convert_and_search(); break; + case cheat_type::signed_16_cheat: res_conv = convert_and_search(); break; + case cheat_type::signed_32_cheat: res_conv = convert_and_search(); break; + case cheat_type::signed_64_cheat: res_conv = convert_and_search(); break; + default: log_cheat.fatal("Unsupported cheat type"); break; + } + + if (!res_conv) + { + QMessageBox::warning(this, tr("Error converting value"), tr("Couldn't convert the search value you typed to the integer type you selected"), QMessageBox::Ok); + return; + } + + lst_search->clear(); + for (u32 row = 0; row < offsets_found.size(); row++) + { + lst_search->insertItem(row, tr("0x%1").arg(offsets_found[row], 1, 16).toUpper()); + } + btn_filter_results->setEnabled(offsets_found.size()); +} + +void cheat_manager_dialog::update_cheat_list() +{ + u32 num_rows = 0; + for (const auto& name : g_cheat.cheats) + num_rows += g_cheat.cheats[name.first].size(); + + tbl_cheats->setRowCount(num_rows); + + u32 row = 0; + { + const QSignalBlocker blocker(tbl_cheats); + for (const auto& game : g_cheat.cheats) + { + for (const auto& offset : g_cheat.cheats[game.first]) + { + QTableWidgetItem* item_game = new QTableWidgetItem(QString::fromStdString(offset.second.game)); + item_game->setFlags(item_game->flags() & ~Qt::ItemIsEditable); + tbl_cheats->setItem(row, 0, item_game); + + tbl_cheats->setItem(row, 1, new QTableWidgetItem(QString::fromStdString(offset.second.description))); + + std::string type_formatted; + fmt_class_string::format(type_formatted, (u64)offset.second.type); + QTableWidgetItem* item_type = new QTableWidgetItem(QString::fromStdString(type_formatted)); + item_type->setFlags(item_type->flags() & ~Qt::ItemIsEditable); + tbl_cheats->setItem(row, 2, item_type); + + QTableWidgetItem* item_offset = new QTableWidgetItem(tr("0x%1").arg(offset.second.offset, 1, 16).toUpper()); + item_offset->setData(Qt::UserRole, QVariant(offset.second.offset)); + item_offset->setFlags(item_offset->flags() & ~Qt::ItemIsEditable); + tbl_cheats->setItem(row, 3, item_offset); + + tbl_cheats->setItem(row, 4, new QTableWidgetItem(QString::fromStdString(offset.second.red_script))); + + row++; + } + } + } + + g_cheat.save(); +} diff --git a/rpcs3/rpcs3qt/cheat_manager.h b/rpcs3/rpcs3qt/cheat_manager.h new file mode 100644 index 0000000000..6b45d6c819 --- /dev/null +++ b/rpcs3/rpcs3qt/cheat_manager.h @@ -0,0 +1,112 @@ +#pragma once + +#include "stdafx.h" +#include +#include +#include +#include +#include +#include + +enum class cheat_type : u8 +{ + unsigned_8_cheat, + unsigned_16_cheat, + unsigned_32_cheat, + unsigned_64_cheat, + signed_8_cheat, + signed_16_cheat, + signed_32_cheat, + signed_64_cheat, +}; + +struct cheat_info +{ + std::string game; + std::string description; + cheat_type type; + u32 offset; + std::string red_script; + + bool from_str(const std::string& cheat_line); + std::string to_str() const; +}; + +class cheat_engine +{ +public: + cheat_engine(); + + bool exist(const std::string& game, const u32 offset) const; + void add(const std::string& game, const std::string& description, const cheat_type type, const u32 offset, const std::string& red_script); + cheat_info* get(const std::string& game, const u32 offset); + bool erase(const std::string& game, const u32 offset); + + void import_cheats_from_str(const std::string& str_cheats); + std::string export_cheats_to_str() const; + void save() const; + + // Static functions to find/get/set values in ps3 memory + static bool resolve_script(u32& final_offset, const u32 offset, const std::string& red_script); + + template + static std::vector search(const T value, const std::vector& to_filter); + + template + static T get_value(const u32 offset, bool& success); + template + static bool set_value(const u32 offset, const T value); + + static bool is_addr_safe(const u32 offset); + static u32 reverse_lookup(const u32 addr, const u32 max_offset, const u32 max_depth, const u32 cur_depth = 0); + +public: + std::map> cheats; + +private: + const std::string cheats_filename = "/cheats.yml"; +}; + +class cheat_manager_dialog : public QDialog +{ + Q_OBJECT +public: + cheat_manager_dialog(QWidget* parent = nullptr); + ~cheat_manager_dialog(); + static cheat_manager_dialog* get_dlg(QWidget* parent = nullptr); + + cheat_manager_dialog(cheat_manager_dialog const&) = delete; + void operator=(cheat_manager_dialog const&) = delete; + +protected: + void update_cheat_list(); + void do_the_search(); + + template + T convert_from_QString(QString& str, bool& success); + + template + bool convert_and_search(); + template + std::pair convert_and_set(u32 offset); + +protected: + QTableWidget* tbl_cheats; + QListWidget* lst_search; + + QLineEdit* edt_value_final; + QPushButton* btn_apply; + + QLineEdit* edt_cheat_search_value; + QComboBox* cbx_cheat_search_type; + + QPushButton* btn_filter_results; + + u32 current_offset; + std::vector offsets_found; + + cheat_engine g_cheat; + +private: + static cheat_manager_dialog* inst; +}; diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 09fb6d3d74..68a5099d1e 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -29,6 +29,7 @@ #include "pad_settings_dialog.h" #include "progress_dialog.h" #include "skylander_dialog.h" +#include "cheat_manager.h" #include @@ -1319,6 +1320,12 @@ void main_window::CreateConnects() skylander_dialog* sky_diag = skylander_dialog::get_dlg(this); sky_diag->show(); }); + + connect(ui->actionManage_Cheats, &QAction::triggered, [=] + { + cheat_manager_dialog* cheat_manager = cheat_manager_dialog::get_dlg(this); + cheat_manager->show(); + }); connect(ui->actionManage_Users, &QAction::triggered, [=] { diff --git a/rpcs3/rpcs3qt/main_window.ui b/rpcs3/rpcs3qt/main_window.ui index 25d5fbdef4..29a59e9212 100644 --- a/rpcs3/rpcs3qt/main_window.ui +++ b/rpcs3/rpcs3qt/main_window.ui @@ -228,6 +228,7 @@ + @@ -1011,6 +1012,11 @@ Skylanders Portal + + + Cheats + +