Cheats/Patches

Adds the possibility of applying cheats/patches according to the specific game serial+version

The logic for adding modifications has not yet been implemented!

Interface based on issues/372 https://github.com/shadps4-emu/shadPS4/issues/372

[X]Front-end
[]Back-end

Create a synchronized fork of the cheats/patches repository
This commit is contained in:
DanielSvoboda 2024-08-20 11:18:00 -03:00
parent 1f416134e7
commit 10485af932
4 changed files with 269 additions and 2 deletions

View file

@ -110,7 +110,7 @@ add_subdirectory(externals)
include_directories(src)
if(ENABLE_QT_GUI)
find_package(Qt6 REQUIRED COMPONENTS Widgets Concurrent)
find_package(Qt6 REQUIRED COMPONENTS Widgets Concurrent Network)
qt_standard_project_setup()
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOMOC ON)
@ -639,7 +639,7 @@ else()
endif()
if (ENABLE_QT_GUI)
target_link_libraries(shadps4 PRIVATE Qt6::Widgets Qt6::Concurrent)
target_link_libraries(shadps4 PRIVATE Qt6::Widgets Qt6::Concurrent Qt6::Network)
endif()
if (WIN32)

View file

@ -74,6 +74,7 @@ static auto UserPaths = [] {
create_path(PathType::SysModuleDir, user_dir / SYSMODULES_DIR);
create_path(PathType::DownloadDir, user_dir / DOWNLOAD_DIR);
create_path(PathType::CapturesDir, user_dir / CAPTURES_DIR);
create_path(PathType::CheatsDir, user_dir / CHEATS_DIR);
return paths;
}();

View file

@ -20,6 +20,7 @@ enum class PathType {
SysModuleDir, // Where system modules are stored.
DownloadDir, // Where downloads/temp files are stored.
CapturesDir, // Where rdoc captures are stored.
CheatsDir, // Where cheats and patches are stored.
};
constexpr auto PORTABLE_DIR = "user";
@ -35,6 +36,7 @@ constexpr auto TEMPDATA_DIR = "temp";
constexpr auto SYSMODULES_DIR = "sys_modules";
constexpr auto DOWNLOAD_DIR = "download";
constexpr auto CAPTURES_DIR = "captures";
constexpr auto CHEATS_DIR = "cheats";
// Filenames
constexpr auto LOG_FILE = "shad_log.txt";

View file

@ -3,20 +3,26 @@
#pragma once
#include <QCheckBox>
#include <QClipboard>
#include <QCoreApplication>
#include <QDesktopServices>
#include <QFile>
#include <QGroupBox>
#include <QHeaderView>
#include <QImage>
#include <QMenu>
#include <QMessageBox>
#include <QPixmap>
#include <QPushButton>
#include <QScrollArea>
#include <QStandardPaths>
#include <QTableWidget>
#include <QTextStream>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkReply>
#include "game_info.h"
#include "trophy_viewer.h"
@ -28,6 +34,7 @@
#include <shlguid.h>
#include <shobjidl.h>
#endif
#include "common/path_util.h"
class GuiContextMenus : public QObject {
Q_OBJECT
@ -46,11 +53,13 @@ public:
QMenu menu(widget);
QAction createShortcut("Create Shortcut", widget);
QAction openFolder("Open Game Folder", widget);
QAction openCheats("Cheats/Patches", widget);
QAction openSfoViewer("SFO Viewer", widget);
QAction openTrophyViewer("Trophy Viewer", widget);
menu.addAction(&openFolder);
menu.addAction(&createShortcut);
menu.addAction(&openCheats);
menu.addAction(&openSfoViewer);
menu.addAction(&openTrophyViewer);
@ -129,6 +138,261 @@ public:
}
}
if (selected == &openCheats) {
const auto& CHEATS_DIR = Common::FS::GetUserPath(Common::FS::PathType::CheatsDir);
QString CHEATS_DIR_QString = QString::fromStdString(CHEATS_DIR.string());
const QString NameCheatJson = QString::fromStdString(m_games[itemID].serial) + "_" +
QString::fromStdString(m_games[itemID].version) + ".json";
const QString cheatFilePath = CHEATS_DIR_QString + "/" + NameCheatJson;
QWidget* cheatWidget = new QWidget();
cheatWidget->setAttribute(Qt::WA_DeleteOnClose);
cheatWidget->setWindowTitle("Cheats/Patches");
cheatWidget->resize(700, 300);
QVBoxLayout* mainLayout = new QVBoxLayout(cheatWidget);
QHBoxLayout* horizontalLayout = new QHBoxLayout();
// GroupBox for game information (Left side)
QGroupBox* gameInfoGroupBox = new QGroupBox();
QVBoxLayout* leftLayout = new QVBoxLayout(gameInfoGroupBox);
leftLayout->setAlignment(Qt::AlignTop);
// Game image
QLabel* gameImage = new QLabel();
QPixmap pixmap(QString::fromStdString(m_games[itemID].icon_path));
gameImage->setPixmap(pixmap.scaled(250, 250, Qt::KeepAspectRatio));
gameImage->setAlignment(Qt::AlignCenter);
leftLayout->addWidget(gameImage, 0, Qt::AlignCenter);
// Game name
QLabel* gameName = new QLabel(QString::fromStdString(m_games[itemID].name));
gameName->setAlignment(Qt::AlignLeft);
gameName->setWordWrap(true);
leftLayout->addWidget(gameName);
// Game serial
QLabel* gameSerial =
new QLabel("Serial: " + QString::fromStdString(m_games[itemID].serial));
gameSerial->setAlignment(Qt::AlignLeft);
leftLayout->addWidget(gameSerial);
// Game version
QLabel* gameVersion =
new QLabel("Version: " + QString::fromStdString(m_games[itemID].version));
gameVersion->setAlignment(Qt::AlignLeft);
leftLayout->addWidget(gameVersion);
// Game size
QLabel* gameSize = new QLabel("Size: " + QString::fromStdString(m_games[itemID].size));
gameSize->setAlignment(Qt::AlignLeft);
leftLayout->addWidget(gameSize);
// Check for credits and add QLabel if exists
QFile file(cheatFilePath);
if (file.open(QIODevice::ReadOnly)) {
QByteArray jsonData = file.readAll();
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData);
QJsonObject jsonObject = jsonDoc.object();
if (jsonObject.contains("credits")) {
QJsonArray creditsArray = jsonObject["credits"].toArray();
if (!creditsArray.isEmpty()) {
QStringList creditsList;
for (const QJsonValue& value : creditsArray) {
creditsList.append(value.toString());
}
QString credits = creditsList.join(", ");
QLabel* creditsLabel = new QLabel("Author: " + credits);
creditsLabel->setAlignment(Qt::AlignLeft);
leftLayout->addWidget(creditsLabel);
}
}
}
gameInfoGroupBox->setLayout(leftLayout);
horizontalLayout->addWidget(gameInfoGroupBox);
QScrollArea* scrollArea = new QScrollArea();
scrollArea->setWidgetResizable(true);
// Right
QGroupBox* cheatsGroupBox = new QGroupBox();
QVBoxLayout* rightLayout = new QVBoxLayout(cheatsGroupBox);
QString checkBoxStyle = "QCheckBox { font-size: 19px; }";
QString buttonStyle = "QPushButton { font-size: 19px; }";
rightLayout->setAlignment(Qt::AlignTop);
// Function to add checkboxes and buttons to the layout
auto addMods = [=](const QJsonArray& modsArray) {
// Limpar widgets existentes
QLayoutItem* item;
while ((item = rightLayout->takeAt(0)) != nullptr) {
delete item->widget();
delete item;
}
for (const QJsonValue& modValue : modsArray) {
QJsonObject modObject = modValue.toObject();
QString modName = modObject["name"].toString();
QString modType = modObject["type"].toString();
if (modType == "checkbox") {
bool isEnabled = modObject.contains("is_enabled")
? modObject["is_enabled"].toBool()
: false;
QCheckBox* cheatCheckBox = new QCheckBox(modName);
cheatCheckBox->setStyleSheet(checkBoxStyle);
cheatCheckBox->setChecked(isEnabled);
rightLayout->addWidget(cheatCheckBox);
// Connect the toggled(bool) signal to handle state change
connect(cheatCheckBox, &QCheckBox::toggled, [=](bool checked) {
if (checked) {
// Implement action when checkbox is checked
} else {
// Implement action when checkbox is unchecked
}
});
} else if (modType == "button") {
QPushButton* cheatButton = new QPushButton(modName);
cheatButton->setStyleSheet(buttonStyle);
connect(cheatButton, &QPushButton::clicked, [=]() {
// Implementar a ação do botão !!!
});
rightLayout->addWidget(cheatButton);
}
}
};
QNetworkAccessManager* manager = new QNetworkAccessManager(cheatWidget);
auto loadCheats = [=](const QString& filePath) {
QFile file(filePath);
if (file.open(QIODevice::ReadOnly)) {
QByteArray jsonData = file.readAll();
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData);
QJsonObject jsonObject = jsonDoc.object();
QJsonArray modsArray = jsonObject["mods"].toArray();
addMods(modsArray);
}
};
loadCheats(cheatFilePath);
cheatsGroupBox->setLayout(rightLayout);
scrollArea->setWidget(cheatsGroupBox);
horizontalLayout->addWidget(scrollArea);
mainLayout->addLayout(horizontalLayout);
QHBoxLayout* buttonLayout = new QHBoxLayout();
QPushButton* checkUpdateButton = new QPushButton("Check Update");
QPushButton* cancelButton = new QPushButton("Cancel");
QPushButton* saveButton = new QPushButton("Save");
connect(checkUpdateButton, &QPushButton::clicked, [=]() {
if (QFile::exists(cheatFilePath)) {
QMessageBox::StandardButton reply;
reply = QMessageBox::question(cheatWidget, "File Exists",
"File already exists. Do you want to replace it?",
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::No) {
return;
}
}
// Cheats repository URL, replace with a synced fork of the repository
const QString url = "https://raw.githubusercontent.com/GoldHEN/"
"GoldHEN_Cheat_Repository/main/json/" +
NameCheatJson;
QNetworkRequest request(url);
QNetworkReply* reply = manager->get(request);
connect(reply, &QNetworkReply::finished, [=]() {
if (reply->error() == QNetworkReply::NoError) {
QByteArray jsonData = reply->readAll();
// Save the JSON file in the cheats folder
QFile cheatFile(cheatFilePath);
if (cheatFile.open(QIODevice::WriteOnly)) {
cheatFile.write(jsonData);
cheatFile.close();
}
// Reload and add new widgets
loadCheats(cheatFilePath);
} else {
QMessageBox::warning(
cheatWidget, "Cheats/Patches not found",
"No Cheats/Patches found for this game in this version.");
}
reply->deleteLater();
});
});
connect(cancelButton, &QPushButton::clicked, [=]() { cheatWidget->close(); });
connect(saveButton, &QPushButton::clicked, [=]() {
QJsonDocument jsonDoc;
QFile file(cheatFilePath);
if (file.open(QIODevice::ReadOnly)) {
jsonDoc = QJsonDocument::fromJson(file.readAll());
file.close();
}
QJsonObject json = jsonDoc.object();
QJsonArray modsArray = json["mods"].toArray();
QMap<QString, bool> modMap;
for (int i = 0; i < rightLayout->count(); ++i) {
QWidget* widget = rightLayout->itemAt(i)->widget();
if (QCheckBox* checkBox = qobject_cast<QCheckBox*>(widget)) {
modMap[checkBox->text()] = checkBox->isChecked();
}
// Buttons don't need state saving, so we ignore them.
}
for (auto it = modMap.begin(); it != modMap.end(); ++it) {
bool found = false;
for (int i = 0; i < modsArray.size(); ++i) {
QJsonObject mod = modsArray[i].toObject();
if (mod["name"].toString() == it.key()) {
mod["is_enabled"] = it.value();
modsArray[i] = mod;
found = true;
break;
}
}
if (!found) {
QJsonObject newMod;
newMod["name"] = it.key();
newMod["is_enabled"] = it.value();
modsArray.append(newMod);
}
}
json["mods"] = modsArray;
jsonDoc.setObject(json);
if (file.open(QIODevice::WriteOnly)) {
file.write(jsonDoc.toJson());
file.close();
QMessageBox::information(cheatWidget, "Save", "Settings saved.");
cheatWidget->close();
} else {
QMessageBox::warning(cheatWidget, "Error", "Could not open file for writing.");
}
});
buttonLayout->addWidget(checkUpdateButton);
buttonLayout->addWidget(cancelButton);
buttonLayout->addWidget(saveButton);
mainLayout->addLayout(buttonLayout);
cheatWidget->setLayout(mainLayout);
cheatWidget->show();
}
if (selected == &openTrophyViewer) {
QString trophyPath = QString::fromStdString(m_games[itemID].serial);
QString gameTrpPath = QString::fromStdString(m_games[itemID].path);