Qt: patch creator

This commit is contained in:
Megamouse 2021-09-02 20:48:56 +02:00
parent 0debcfed0a
commit 3c0681ad6d
15 changed files with 846 additions and 13 deletions

View File

@ -176,6 +176,14 @@ bool patch_engine::load(patch_map& patches_map, const std::string& path, std::st
continue;
}
if (main_key.empty())
{
append_log_message(log_messages, "Error: Skipping empty key");
patch_log.error("Skipping empty key (file: %s)", path);
is_valid = false;
continue;
}
// Skip Anchors
if (main_key == patch_key::anchors)
{
@ -223,10 +231,18 @@ bool patch_engine::load(patch_map& patches_map, const std::string& path, std::st
{
const std::string& title = game_node.first.Scalar();
if (title.empty())
{
append_log_message(log_messages, fmt::format("Error: Empty game title (key: %s, file: %s)", main_key, path));
patch_log.error("Empty game title (key: %s, file: %s)", main_key, path);
is_valid = false;
continue;
}
if (const auto yml_type = game_node.second.Type(); yml_type != YAML::NodeType::Map)
{
append_log_message(log_messages, fmt::format("Error: Skipping %s: expected Map, found %s (patch: %s, key: %s)", title, yml_type, description, main_key));
patch_log.error("Skipping %s: expected Map, found %s (patch: %s, key: %s, file: %s)", title, yml_type, description, main_key, path);
append_log_message(log_messages, fmt::format("Error: Skipping game %s: expected Map, found %s (patch: %s, key: %s)", title, yml_type, description, main_key));
patch_log.error("Skipping game %s: expected Map, found %s (patch: %s, key: %s, file: %s)", title, yml_type, description, main_key, path);
is_valid = false;
continue;
}
@ -237,7 +253,14 @@ bool patch_engine::load(patch_map& patches_map, const std::string& path, std::st
{
const std::string& serial = serial_node.first.Scalar();
if (serial == patch_key::all)
if (serial.empty())
{
append_log_message(log_messages, fmt::format("Error: Using empty serial (title: %s, patch: %s, key: %s)", title, description, main_key));
patch_log.error("Using empty serial (title: %s, patch: %s, key: %s, file: %s)", title, description, main_key, path);
is_valid = false;
continue;
}
else if (serial == patch_key::all)
{
if (!title_is_all_key)
{
@ -363,11 +386,11 @@ bool patch_engine::load(patch_map& patches_map, const std::string& path, std::st
return is_valid;
}
patch_type patch_engine::get_patch_type(YAML::Node node)
patch_type patch_engine::get_patch_type(const std::string& text)
{
u64 type_val = 0;
if (!node || !node.IsScalar() || !cfg::try_to_enum_value(&type_val, &fmt_class_string<patch_type>::format, node.Scalar()))
if (!cfg::try_to_enum_value(&type_val, &fmt_class_string<patch_type>::format, text))
{
return patch_type::invalid;
}
@ -375,6 +398,16 @@ patch_type patch_engine::get_patch_type(YAML::Node node)
return static_cast<patch_type>(type_val);
}
patch_type patch_engine::get_patch_type(YAML::Node node)
{
if (!node || !node.IsScalar())
{
return patch_type::invalid;
}
return get_patch_type(node.Scalar());
}
bool patch_engine::add_patch_data(YAML::Node node, patch_info& info, u32 modifier, const YAML::Node& root, std::stringstream* log_messages)
{
if (!node || !node.IsSequence())

View File

@ -121,6 +121,9 @@ public:
// Read and add a patch node to the patch info
static bool read_patch_node(patch_info& info, YAML::Node node, const YAML::Node& root, std::stringstream* log_messages = nullptr);
// Get the patch type from a string
static patch_type get_patch_type(const std::string& text);
// Get the patch type of a patch node
static patch_type get_patch_type(YAML::Node node);

View File

@ -291,6 +291,9 @@
<ClCompile Include="QTGeneratedFiles\Debug\moc_pad_settings_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Debug\moc_patch_creator_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Debug\moc_patch_manager_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild>
</ClCompile>
@ -480,6 +483,9 @@
<ClCompile Include="QTGeneratedFiles\Release\moc_pad_settings_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Release\moc_patch_creator_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Release\moc_patch_manager_dialog.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</ExcludedFromBuild>
</ClCompile>
@ -572,6 +578,7 @@
<ClCompile Include="rpcs3qt\microphone_creator.cpp" />
<ClCompile Include="rpcs3qt\osk_dialog_frame.cpp" />
<ClCompile Include="rpcs3qt\pad_led_settings_dialog.cpp" />
<ClCompile Include="rpcs3qt\patch_creator_dialog.cpp" />
<ClCompile Include="rpcs3qt\patch_manager_dialog.cpp" />
<ClCompile Include="rpcs3qt\pkg_install_dialog.cpp" />
<ClCompile Include="rpcs3qt\persistent_settings.cpp" />
@ -1046,6 +1053,16 @@
</CustomBuild>
<ClInclude Include="rpcs3qt\movie_item.h" />
<ClInclude Include="rpcs3qt\numbered_widget_item.h" />
<CustomBuild Include="rpcs3qt\patch_creator_dialog.h">
<Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Moc%27ing %(Identity)...</Message>
<Outputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">.\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">"$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\flatbuffers\include" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\XAudio2Redist\include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\debug" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent"</Command>
<Message Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Moc%27ing %(Identity)...</Message>
<Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\flatbuffers\include" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\XAudio2Redist\include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent"</Command>
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(QTDIR)\bin\moc.exe;%(FullPath)</AdditionalInputs>
</CustomBuild>
<ClInclude Include="rpcs3qt\richtext_item_delegate.h" />
<ClInclude Include="rpcs3qt\stylesheets.h" />
<CustomBuild Include="rpcs3qt\skylander_dialog.h">
@ -1398,6 +1415,16 @@
<ItemGroup>
<None Include="QTGeneratedFiles\Debug\pkg_install_dialog.moc" />
<None Include="QTGeneratedFiles\Release\pkg_install_dialog.moc" />
<CustomBuild Include="rpcs3qt\patch_creator_dialog.ui">
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">$(QTDIR)\bin\uic.exe;%(AdditionalInputs)</AdditionalInputs>
<Message Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Uic%27ing %(Identity)...</Message>
<Outputs Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">.\QTGeneratedFiles\ui_%(Filename).h;%(Outputs)</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">"$(QTDIR)\bin\uic.exe" -o ".\QTGeneratedFiles\ui_%(Filename).h" "%(FullPath)"</Command>
<AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(QTDIR)\bin\uic.exe;%(AdditionalInputs)</AdditionalInputs>
<Message Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Uic%27ing %(Identity)...</Message>
<Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\QTGeneratedFiles\ui_%(Filename).h;%(Outputs)</Outputs>
<Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">"$(QTDIR)\bin\uic.exe" -o ".\QTGeneratedFiles\ui_%(Filename).h" "%(FullPath)"</Command>
</CustomBuild>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets" />

View File

@ -747,6 +747,15 @@
<ClCompile Include="rpcs3qt\game_list.cpp">
<Filter>Gui\game list</Filter>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Debug\moc_patch_creator_dialog.cpp">
<Filter>Generated Files\Debug</Filter>
</ClCompile>
<ClCompile Include="rpcs3qt\patch_creator_dialog.cpp">
<Filter>Gui\patch manager</Filter>
</ClCompile>
<ClCompile Include="QTGeneratedFiles\Release\moc_patch_creator_dialog.cpp">
<Filter>Generated Files\Release</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Input\ds4_pad_handler.h">
@ -1093,6 +1102,12 @@
<CustomBuild Include="rpcs3qt\log_viewer.h">
<Filter>Gui\log</Filter>
</CustomBuild>
<CustomBuild Include="rpcs3qt\patch_creator_dialog.ui">
<Filter>Form Files</Filter>
</CustomBuild>
<CustomBuild Include="rpcs3qt\patch_creator_dialog.h">
<Filter>Gui\patch manager</Filter>
</CustomBuild>
</ItemGroup>
<ItemGroup>
<Image Include="rpcs3.ico" />

View File

@ -44,6 +44,7 @@ set(SRC_FILES
osk_dialog_frame.cpp
pad_led_settings_dialog.cpp
pad_settings_dialog.cpp
patch_creator_dialog.cpp
patch_manager_dialog.cpp
persistent_settings.cpp
pkg_install_dialog.cpp
@ -80,6 +81,8 @@ set(UI_FILES
main_window.ui
pad_led_settings_dialog.ui
pad_settings_dialog.ui
patch_creator_dialog.ui
patch_manager_dialog.ui
settings_dialog.ui
welcome_dialog.ui
)

View File

@ -2211,7 +2211,7 @@ bool game_list_frame::eventFilter(QObject *object, QEvent *event)
return true;
}
}
else
else if (!key_event->isAutoRepeat())
{
if (key_event->key() == Qt::Key_Enter || key_event->key() == Qt::Key_Return)
{

View File

@ -683,7 +683,7 @@ bool log_frame::eventFilter(QObject* object, QEvent* event)
if (event->type() == QEvent::KeyPress)
{
QKeyEvent* e = static_cast<QKeyEvent*>(event);
if (e->modifiers() == Qt::ControlModifier && e->key() == Qt::Key_F)
if (e && !e->isAutoRepeat() && e->modifiers() == Qt::ControlModifier && e->key() == Qt::Key_F)
{
if (m_find_dialog && m_find_dialog->isVisible())
m_find_dialog->close();

View File

@ -182,7 +182,7 @@ bool log_viewer::eventFilter(QObject* object, QEvent* event)
if (event->type() == QEvent::KeyPress)
{
QKeyEvent* e = static_cast<QKeyEvent*>(event);
if (e && e->modifiers() == Qt::ControlModifier && e->key() == Qt::Key_F)
if (e && !e->isAutoRepeat() && e->modifiers() == Qt::ControlModifier && e->key() == Qt::Key_F)
{
if (m_find_dialog && m_find_dialog->isVisible())
m_find_dialog->close();

View File

@ -23,6 +23,7 @@
#include "skylander_dialog.h"
#include "cheat_manager.h"
#include "patch_manager_dialog.h"
#include "patch_creator_dialog.h"
#include "pkg_install_dialog.h"
#include "category.h"
#include "gui_settings.h"
@ -2111,6 +2112,12 @@ void main_window::CreateConnects()
patch_manager.exec();
});
connect(ui->patchCreatorAct, &QAction::triggered, this, [this]
{
patch_creator_dialog patch_creator(this);
patch_creator.exec();
});
connect(ui->actionManage_Users, &QAction::triggered, this, [this]
{
user_manager_dialog user_manager(m_gui_settings, m_persistent_settings, this);

View File

@ -255,6 +255,7 @@
<addaction name="toolsmemory_viewerAct"/>
<addaction name="toolsRsxDebuggerAct"/>
<addaction name="toolsStringSearchAct"/>
<addaction name="patchCreatorAct"/>
<addaction name="separator"/>
<addaction name="toolsDecryptSprxLibsAct"/>
<addaction name="separator"/>
@ -1169,6 +1170,11 @@
<string>Boot VSH/XMB</string>
</property>
</action>
<action name="patchCreatorAct">
<property name="text">
<string>Patch Creator</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources>

View File

@ -0,0 +1,484 @@
#include "ui_patch_creator_dialog.h"
#include "patch_creator_dialog.h"
#include "table_item_delegate.h"
#include "qt_utils.h"
#include "Utilities/Config.h"
#include <QCompleter>
#include <QFontDatabase>
#include <QMenu>
#include <QMessageBox>
#include <QFileDialog>
#include <QMenuBar>
LOG_CHANNEL(patch_log, "PAT");
constexpr auto qstr = QString::fromStdString;
inline std::string sstr(const QString& _in) { return _in.toStdString(); }
Q_DECLARE_METATYPE(patch_type)
enum patch_column : int
{
type = 0,
offset,
value,
comment
};
patch_creator_dialog::patch_creator_dialog(QWidget* parent)
: QDialog(parent)
, ui(new Ui::patch_creator_dialog)
, mMonoFont(QFontDatabase::systemFont(QFontDatabase::FixedFont))
, mValidColor(gui::utils::get_label_color("log_level_success"))
, mInvalidColor(gui::utils::get_label_color("log_level_error"))
{
ui->setupUi(this);
ui->patchEdit->setFont(mMonoFont);
ui->addPatchOffsetEdit->setFont(mMonoFont);
ui->addPatchValueEdit->setFont(mMonoFont);
ui->instructionTable->setFont(mMonoFont);
ui->instructionTable->setItemDelegate(new table_item_delegate(this, false));
ui->instructionTable->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::Fixed);
ui->instructionTable->installEventFilter(this);
ui->versionMinorSpinBox->setValue(0);
ui->versionMajorSpinBox->setValue(0);
QMenuBar* menu_bar = new QMenuBar(this);
QMenu* file_menu = menu_bar->addMenu(tr("File"));
QAction* export_act = file_menu->addAction(tr("&Export Patch"));
export_act->setShortcut(QKeySequence("Ctrl+E"));
export_act->installEventFilter(this);
layout()->setMenuBar(menu_bar);
connect(export_act, &QAction::triggered, this, &patch_creator_dialog::export_patch);
connect(ui->hashEdit, &QLineEdit::textChanged, this, &patch_creator_dialog::generate_yml);
connect(ui->authorEdit, &QLineEdit::textChanged, this, &patch_creator_dialog::generate_yml);
connect(ui->patchNameEdit, &QLineEdit::textChanged, this, &patch_creator_dialog::generate_yml);
connect(ui->gameEdit, &QLineEdit::textChanged, this, &patch_creator_dialog::generate_yml);
connect(ui->gameVersionEdit, &QLineEdit::textChanged, this, &patch_creator_dialog::generate_yml);
connect(ui->serialEdit, &QLineEdit::textChanged, this, &patch_creator_dialog::generate_yml);
connect(ui->notesEdit, &QLineEdit::textChanged, this, &patch_creator_dialog::generate_yml);
connect(ui->groupEdit, &QLineEdit::textChanged, this, &patch_creator_dialog::generate_yml);
connect(ui->versionMinorSpinBox, &QSpinBox::textChanged, this, &patch_creator_dialog::generate_yml);
connect(ui->versionMajorSpinBox, &QSpinBox::textChanged, this, &patch_creator_dialog::generate_yml);
connect(ui->instructionTable, &QTableWidget::itemChanged, this, [this](QTableWidgetItem*){ generate_yml(); });
connect(ui->instructionTable, &QTableWidget::customContextMenuRequested, this, &patch_creator_dialog::show_table_menu);
connect(ui->addPatchButton, &QAbstractButton::clicked, this, [this]() { add_instruction(ui->instructionTable->rowCount()); });
init_patch_type_bombo_box(ui->addPatchTypeComboBox, patch_type::be32, false);
generate_yml();
}
patch_creator_dialog::~patch_creator_dialog()
{
}
void patch_creator_dialog::init_patch_type_bombo_box(QComboBox* combo_box, patch_type set_type, bool searchable)
{
if (!combo_box) return;
combo_box->clear();
QStringList types;
for (const std::string& type : cfg::try_to_enum_list(&fmt_class_string<patch_type>::format))
{
if (const patch_type t = patch_engine::get_patch_type(type); t != patch_type::invalid)
{
types << qstr(type);
combo_box->addItem(types.last(), QVariant::fromValue<patch_type>(t));
if (t == set_type)
{
combo_box->setCurrentText(types.last());
}
}
}
if (searchable)
{
QCompleter* completer = new QCompleter(types, combo_box);
completer->setCaseSensitivity(Qt::CaseInsensitive);
completer->setCompletionMode(QCompleter::PopupCompletion);
completer->setFilterMode(Qt::MatchContains);
combo_box->setCompleter(completer);
combo_box->setEditable(true);
combo_box->setInsertPolicy(QComboBox::NoInsert);
}
}
QComboBox* patch_creator_dialog::create_patch_type_bombo_box(patch_type set_type)
{
QComboBox* combo_box = new QComboBox;
init_patch_type_bombo_box(combo_box, set_type, true);
connect(combo_box, &QComboBox::currentTextChanged, this, &patch_creator_dialog::generate_yml);
return combo_box;
}
void patch_creator_dialog::show_table_menu(const QPoint& pos)
{
QMenu menu;
QModelIndexList selection = ui->instructionTable->selectionModel()->selectedRows();
if (selection.isEmpty())
{
QAction* act_add_instruction = menu.addAction(tr("&Add Instruction"));
connect(act_add_instruction, &QAction::triggered, [this]()
{
add_instruction(ui->instructionTable->rowCount());
});
}
else
{
if (selection.count() == 1)
{
QAction* act_add_instruction_above = menu.addAction(tr("&Add Instruction Above"));
connect(act_add_instruction_above, &QAction::triggered, [this, row = selection.first().row()]()
{
add_instruction(row);
});
QAction* act_add_instruction_below = menu.addAction(tr("&Add Instruction Below"));
connect(act_add_instruction_below, &QAction::triggered, [this, row = selection.first().row()]()
{
add_instruction(row + 1);
});
}
const bool can_move_up = can_move_instructions(selection, move_direction::up);
const bool can_move_down = can_move_instructions(selection, move_direction::down);
if (can_move_up)
{
menu.addSeparator();
QAction* act_move_instruction_up = menu.addAction(tr("&Move Instruction(s) Up"));
connect(act_move_instruction_up, &QAction::triggered, [this, &selection]()
{
move_instructions(selection.first().row(), selection.count(), 1, move_direction::up);
});
}
if (can_move_down)
{
if (!can_move_up)
menu.addSeparator();
QAction* act_move_instruction_down = menu.addAction(tr("&Move Instruction(s) Down"));
connect(act_move_instruction_down, &QAction::triggered, [this, &selection]()
{
move_instructions(selection.first().row(), selection.count(), 1, move_direction::down);
});
}
menu.addSeparator();
QAction* act_remove_instruction = menu.addAction(tr("&Remove Instruction(s)"));
connect(act_remove_instruction, &QAction::triggered, [this]()
{
remove_instructions();
});
}
menu.addSeparator();
QAction* act_clear_table = menu.addAction(tr("&Clear Table"));
connect(act_clear_table, &QAction::triggered, [this]()
{
patch_log.notice("Patch Creator: Clearing instruction table...");
ui->instructionTable->clearContents();
ui->instructionTable->setRowCount(0);
generate_yml();
});
menu.exec(ui->instructionTable->viewport()->mapToGlobal(pos));
}
void patch_creator_dialog::add_instruction(int row)
{
const QString type = ui->addPatchTypeComboBox->currentText();
const QString offset = ui->addPatchOffsetEdit->text();
const QString value = ui->addPatchValueEdit->text();
const QString comment = ui->addPatchCommentEdit->text();
const patch_type t = patch_engine::get_patch_type(type.toStdString());
QComboBox* combo_box = create_patch_type_bombo_box(t);
ui->instructionTable->insertRow(std::max(0, std::min(row, ui->instructionTable->rowCount())));
ui->instructionTable->setCellWidget(row, patch_column::type, combo_box);
ui->instructionTable->setItem(row, patch_column::offset, new QTableWidgetItem(offset));
ui->instructionTable->setItem(row, patch_column::value, new QTableWidgetItem(value));
ui->instructionTable->setItem(row, patch_column::comment, new QTableWidgetItem(comment));
patch_log.notice("Patch Creator: Inserted instruction [ %s, %s, %s ] at row %d", sstr(combo_box->currentText()), sstr(offset), sstr(value), row);
generate_yml();
}
void patch_creator_dialog::remove_instructions()
{
QModelIndexList selection(ui->instructionTable->selectionModel()->selectedRows());
if (selection.empty())
return;
std::sort(selection.rbegin(), selection.rend());
for (const QModelIndex& index : selection)
{
patch_log.notice("Patch Creator: Removing instruction in row %d...", index.row());
ui->instructionTable->removeRow(index.row());
}
generate_yml();
}
void patch_creator_dialog::move_instructions(int src_row, int rows_to_move, int distance, move_direction dir)
{
patch_log.notice("Patch Creator: Moving %d instruction(s) from row %d %s by %d...", rows_to_move, src_row, dir == move_direction::up ? "up" : "down", distance);
if (src_row < 0 || src_row >= ui->instructionTable->rowCount() || distance < 1)
return;
rows_to_move = std::max(0, std::min(rows_to_move, ui->instructionTable->rowCount() - src_row));
if (rows_to_move < 1)
return;
const int dst_row = std::max(0, std::min(ui->instructionTable->rowCount() - rows_to_move, dir == move_direction::up ? src_row - distance : src_row + distance));
if (dir == move_direction::up ? dst_row >= src_row : dst_row <= src_row)
return;
const int friends_to_relocate = std::abs(dst_row - src_row);
const int friends_src_row = dir == move_direction::up ? dst_row : src_row + rows_to_move;
const int friends_dst_row = dir == move_direction::up ? dst_row + rows_to_move : src_row;
std::vector<patch_type> moving_types(rows_to_move);
std::vector<std::vector<QTableWidgetItem*>> moving_rows(rows_to_move);
std::vector<patch_type> friend_types(friends_to_relocate);
std::vector<std::vector<QTableWidgetItem*>> friend_rows(friends_to_relocate);
const auto get_row_type = [this](int i) -> patch_type
{
if (const QComboBox* type_item = qobject_cast<QComboBox*>(ui->instructionTable->cellWidget(i, patch_column::type)))
return type_item->currentData().value<patch_type>();
return patch_type::invalid;
};
const auto set_row_type_widget = [this](int i, patch_type type) -> void
{
ui->instructionTable->setCellWidget(i, patch_column::type, create_patch_type_bombo_box(type));
};
for (int i = 0; i < rows_to_move; i++)
{
moving_types[i] = get_row_type(src_row + i);
moving_rows[i].push_back(ui->instructionTable->takeItem(src_row + i, patch_column::type));
moving_rows[i].push_back(ui->instructionTable->takeItem(src_row + i, patch_column::offset));
moving_rows[i].push_back(ui->instructionTable->takeItem(src_row + i, patch_column::value));
moving_rows[i].push_back(ui->instructionTable->takeItem(src_row + i, patch_column::comment));
}
for (int i = 0; i < friends_to_relocate; i++)
{
friend_types[i] = get_row_type(friends_src_row + i);
friend_rows[i].push_back(ui->instructionTable->takeItem(friends_src_row + i, patch_column::type));
friend_rows[i].push_back(ui->instructionTable->takeItem(friends_src_row + i, patch_column::offset));
friend_rows[i].push_back(ui->instructionTable->takeItem(friends_src_row + i, patch_column::value));
friend_rows[i].push_back(ui->instructionTable->takeItem(friends_src_row + i, patch_column::comment));
}
for (int i = 0; i < rows_to_move; i++)
{
int item_index = 0;
ui->instructionTable->setCellWidget(dst_row + i, patch_column::type, create_patch_type_bombo_box(moving_types[i]));
ui->instructionTable->setItem(dst_row + i, patch_column::type, moving_rows[i][item_index++]);
ui->instructionTable->setItem(dst_row + i, patch_column::offset, moving_rows[i][item_index++]);
ui->instructionTable->setItem(dst_row + i, patch_column::value, moving_rows[i][item_index++]);
ui->instructionTable->setItem(dst_row + i, patch_column::comment, moving_rows[i][item_index++]);
}
for (int i = 0; i < friends_to_relocate; i++)
{
int item_index = 0;
ui->instructionTable->setCellWidget(friends_dst_row + i, patch_column::type, create_patch_type_bombo_box(friend_types[i]));
ui->instructionTable->setItem(friends_dst_row + i, patch_column::type, friend_rows[i][item_index++]);
ui->instructionTable->setItem(friends_dst_row + i, patch_column::offset, friend_rows[i][item_index++]);
ui->instructionTable->setItem(friends_dst_row + i, patch_column::value, friend_rows[i][item_index++]);
ui->instructionTable->setItem(friends_dst_row + i, patch_column::comment, friend_rows[i][item_index++]);
}
ui->instructionTable->clearSelection();
ui->instructionTable->setRangeSelected(QTableWidgetSelectionRange(dst_row, 0, dst_row + rows_to_move - 1, ui->instructionTable->columnCount() - 1), true);
generate_yml();
}
bool patch_creator_dialog::can_move_instructions(QModelIndexList& selection, move_direction dir)
{
if (selection.isEmpty())
return false;
std::sort(selection.begin(), selection.end());
// Check if there are any gaps in the selection
for (int i = 1, row = selection.first().row(); i < selection.count(); i++)
{
if (++row != selection[i].row())
return false;
}
if (dir == move_direction::up)
return selection.first().row() > 0;
return selection.last().row() < ui->instructionTable->rowCount() - 1;
}
void patch_creator_dialog::validate()
{
patch_engine::patch_map patches;
const std::string content = ui->patchEdit->toPlainText().toStdString();
const bool is_valid = patch_engine::load(patches, "From Patch Creator", content, true);
if (is_valid != m_valid)
{
QPalette palette = ui->validLabel->palette();
if (is_valid)
{
ui->validLabel->setText(tr("Valid Patch"));
palette.setColor(ui->validLabel->foregroundRole(), mValidColor);
patch_log.success("Patch Creator: Validation successful!");
}
else
{
ui->validLabel->setText(tr("Validation Failed"));
palette.setColor(ui->validLabel->foregroundRole(), mInvalidColor);
patch_log.error("Patch Creator: Validation failed!");
}
ui->validLabel->setPalette(palette);
m_valid = is_valid;
}
}
void patch_creator_dialog::export_patch()
{
if (!m_valid)
{
QMessageBox::information(this, tr("Patch invalid!"), tr("The patch validation failed.\nThe export of invalid patches is not allowed."));
return;
}
const QString file_path = QFileDialog::getSaveFileName(this, tr("Select Patch File"), qstr(patch_engine::get_patches_path()), tr("patch.yml files (*.yml);;All files (*.*)"));
if (file_path.isEmpty())
{
return;
}
if (QFile patch_file(file_path); patch_file.open(QIODevice::WriteOnly | QIODevice::Truncate))
{
patch_file.write(ui->patchEdit->toPlainText().toUtf8());
patch_file.close();
patch_log.success("Exported patch to file '%s'", sstr(file_path));
}
else
{
patch_log.fatal("Failed to export patch to file '%s'", sstr(file_path));
}
}
void patch_creator_dialog::generate_yml(const QString& /*text*/)
{
QString patch;
patch.append(QString("%0: %1\n").arg(qstr(patch_key::version)).arg(qstr(patch_engine_version)));
patch.append("\n");
patch.append(QString("%0:\n").arg(ui->hashEdit->text()));
patch.append(QString(" \"%0\":\n").arg(ui->patchNameEdit->text()));
patch.append(QString(" %0:\n").arg(qstr(patch_key::games)));
patch.append(QString(" \"%0\":\n").arg(ui->gameEdit->text()));
patch.append(QString(" %0: [ %1 ]\n").arg(ui->serialEdit->text()).arg(ui->gameVersionEdit->text()));
patch.append(QString(" %0: \"%1\"\n").arg(qstr(patch_key::author)).arg(ui->authorEdit->text()));
patch.append(QString(" %0: %1.%2\n").arg(qstr(patch_key::patch_version)).arg(ui->versionMajorSpinBox->text()).arg(ui->versionMinorSpinBox->text()));
patch.append(QString(" %0: \"%1\"\n").arg(qstr(patch_key::group)).arg(ui->groupEdit->text()));
patch.append(QString(" %0: \"%1\"\n").arg(qstr(patch_key::notes)).arg(ui->notesEdit->text()));
patch.append(QString(" %0:\n").arg(qstr(patch_key::patch)));
for (int i = 0; i < ui->instructionTable->rowCount(); i++)
{
const QComboBox* type_item = qobject_cast<QComboBox*>(ui->instructionTable->cellWidget(i, patch_column::type));
const QTableWidgetItem* offset_item = ui->instructionTable->item(i, patch_column::offset);
const QTableWidgetItem* value_item = ui->instructionTable->item(i, patch_column::value);
const QTableWidgetItem* comment_item = ui->instructionTable->item(i, patch_column::comment);
const QString type = type_item ? type_item->currentText() : "";
const QString offset = offset_item ? offset_item->text() : "";
const QString value = value_item ? value_item->text() : "";
const QString comment = comment_item ? comment_item->text() : "";
if (patch_engine::get_patch_type(type.toStdString()) == patch_type::invalid)
{
ui->patchEdit->setText(tr("Instruction %0: Type '%1' is invalid!").arg(i + 1).arg(type));
return;
}
patch.append(QString(" - [ %0, %1, %2 ]%3\n").arg(type).arg(offset).arg(value).arg(comment.isEmpty() ? QStringLiteral("") : QString(" # %0").arg(comment)));
}
ui->patchEdit->setText(patch);
validate();
}
bool patch_creator_dialog::eventFilter(QObject* object, QEvent* event)
{
if (object != ui->instructionTable)
{
return QDialog::eventFilter(object, event);
}
if (event->type() == QEvent::KeyPress)
{
if (QKeyEvent* key_event = static_cast<QKeyEvent*>(event))
{
if (key_event->modifiers() == Qt::AltModifier)
{
switch (key_event->key())
{
case Qt::Key_Up:
{
QModelIndexList selection = ui->instructionTable->selectionModel()->selectedRows();
if (can_move_instructions(selection, move_direction::up))
move_instructions(selection.first().row(), selection.count(), 1, move_direction::up);
return true;
}
case Qt::Key_Down:
{
QModelIndexList selection = ui->instructionTable->selectionModel()->selectedRows();
if (can_move_instructions(selection, move_direction::down))
move_instructions(selection.first().row(), selection.count(), 1, move_direction::down);
return true;
}
default:
break;
}
}
else if (!key_event->isAutoRepeat())
{
switch (key_event->key())
{
case Qt::Key_Delete:
{
remove_instructions();
return true;
}
default:
break;
}
}
}
}
return QDialog::eventFilter(object, event);
}

View File

@ -0,0 +1,51 @@
#pragma once
#include "Utilities/bin_patch.h"
#include <QDialog>
#include <QComboBox>
#include <QKeyEvent>
namespace Ui
{
class patch_creator_dialog;
}
class patch_creator_dialog : public QDialog
{
Q_OBJECT
public:
explicit patch_creator_dialog(QWidget* parent = nullptr);
~patch_creator_dialog();
private:
Ui::patch_creator_dialog* ui;
QFont mMonoFont;
QColor mValidColor;
QColor mInvalidColor;
bool m_valid = true; // Will be invalidated immediately
enum class move_direction
{
up,
down
};
void add_instruction(int row);
void remove_instructions();
void move_instructions(int src_row, int rows_to_move, int distance, move_direction dir);
bool can_move_instructions(QModelIndexList& selection, move_direction dir);
static void init_patch_type_bombo_box(QComboBox* combo_box, patch_type set_type, bool searchable);
QComboBox* create_patch_type_bombo_box(patch_type set_type);
private Q_SLOTS:
void show_table_menu(const QPoint& pos);
void validate();
void generate_yml(const QString& text = {});
void export_patch();
protected:
bool eventFilter(QObject* object, QEvent* event) override;
};

View File

@ -0,0 +1,204 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>patch_creator_dialog</class>
<widget class="QDialog" name="patch_creator_dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1005</width>
<height>804</height>
</rect>
</property>
<property name="windowTitle">
<string>Patch Creator</string>
</property>
<layout class="QVBoxLayout" name="dialogLayout">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QWidget" name="verticalLayoutWidget">
<layout class="QVBoxLayout" name="leftLayout">
<item>
<widget class="QTableWidget" name="instructionTable">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Type</string>
</property>
</column>
<column>
<property name="text">
<string>Offset</string>
</property>
</column>
<column>
<property name="text">
<string>Value</string>
</property>
</column>
<column>
<property name="text">
<string>Comment</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="addPatchLayout" stretch="1,1,1,1,0">
<item>
<widget class="QComboBox" name="addPatchTypeComboBox"/>
</item>
<item>
<widget class="QLineEdit" name="addPatchOffsetEdit">
<property name="placeholderText">
<string>Offset</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="addPatchValueEdit">
<property name="placeholderText">
<string>Value</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="addPatchCommentEdit">
<property name="placeholderText">
<string>Comment</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="addPatchButton">
<property name="text">
<string>Add</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="verticalLayoutWidget_2">
<layout class="QVBoxLayout" name="rightLayout">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLineEdit" name="hashEdit">
<property name="placeholderText">
<string>Hash</string>
</property>
</widget>
</item>
<item row="5" column="1">
<layout class="QHBoxLayout" name="versionLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Patch Version</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="versionMajorSpinBox"/>
</item>
<item>
<widget class="QSpinBox" name="versionMinorSpinBox"/>
</item>
</layout>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="notesEdit">
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>Notes</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="authorEdit">
<property name="placeholderText">
<string>Author</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="groupEdit">
<property name="placeholderText">
<string>Group</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="patchNameEdit">
<property name="placeholderText">
<string>Patch Name</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLineEdit" name="gameEdit">
<property name="placeholderText">
<string>Game</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLineEdit" name="serialEdit">
<property name="placeholderText">
<string>Title ID</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLineEdit" name="gameVersionEdit">
<property name="placeholderText">
<string>Game Versions</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="validLabel">
<property name="text">
<string>Valid Patch</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTextEdit" name="patchEdit">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -442,7 +442,7 @@ void save_manager_dialog::OnEntriesRemove()
if (QMessageBox::question(this, tr("Delete Confirmation"), tr("Are you sure you want to delete these %n items?", "", selection.size()), QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes)
{
std::sort(selection.rbegin(), selection.rend());
for (QModelIndex index : selection)
for (const QModelIndex& index : selection)
{
QTableWidgetItem* item = m_list->item(index.row(), 1);
if (!item)

View File

@ -935,7 +935,7 @@ bool trophy_manager_dialog::eventFilter(QObject *object, QEvent *event)
{
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->modifiers() & Qt::ControlModifier && (is_trophy_table || is_game_table))
if (keyEvent && keyEvent->modifiers() == Qt::ControlModifier && (is_trophy_table || is_game_table))
{
if (keyEvent->key() == Qt::Key_Plus)
{