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 @@
+