mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-28 22:06:59 +01:00
add remote cheats launcher
This commit is contained in:
@@ -0,0 +1,26 @@
|
|||||||
|
#ifndef CHEAT_FILE_H
|
||||||
|
#define CHEAT_FILE_H
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace Cheats {
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsSupportedTitleId(const QString& title_id);
|
||||||
|
[[nodiscard]] QString PlanPath(const QString& title_id);
|
||||||
|
|
||||||
|
// Local files retain their existing defaults; remote imports receive additional validation.
|
||||||
|
[[nodiscard]] QString ParseDocument(const QByteArray& data, QJsonDocument* document);
|
||||||
|
[[nodiscard]] QString ValidateImport(const QJsonDocument& document, const QString& title_id,
|
||||||
|
const QString& version, const QString& process);
|
||||||
|
[[nodiscard]] QJsonDocument WithSelection(const QJsonDocument& document,
|
||||||
|
const QList<bool>& enabled);
|
||||||
|
// Refuse to replace a file changed since it was loaded; empty data also permits creation.
|
||||||
|
[[nodiscard]] QString SaveDocument(const QString& path, const QJsonDocument& document,
|
||||||
|
const QByteArray& expected_data);
|
||||||
|
|
||||||
|
} // namespace Cheats
|
||||||
|
|
||||||
|
#endif // CHEAT_FILE_H
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#ifndef CHEAT_REPOSITORY_H
|
||||||
|
#define CHEAT_REPOSITORY_H
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QList>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
class QNetworkReply;
|
||||||
|
|
||||||
|
namespace Cheats {
|
||||||
|
|
||||||
|
struct RemoteFile {
|
||||||
|
QString name;
|
||||||
|
QString title;
|
||||||
|
QString version;
|
||||||
|
QString format;
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsSupported() const;
|
||||||
|
[[nodiscard]] QUrl Url() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CatalogResult {
|
||||||
|
QList<RemoteFile> files;
|
||||||
|
QString error;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] CatalogResult ParseIndex(const QByteArray& data, const QString& format,
|
||||||
|
const QString& title_id);
|
||||||
|
|
||||||
|
class CheatRepository final: public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit CheatRepository(QObject* parent = nullptr);
|
||||||
|
void Load(const QString& title_id);
|
||||||
|
void Fetch(const RemoteFile& file);
|
||||||
|
void CancelFetch();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void CatalogLoaded(QList<Cheats::RemoteFile> files, QString error);
|
||||||
|
void FileLoaded(QByteArray data, QString error);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QNetworkReply* Request(const QUrl& url, qint64 limit,
|
||||||
|
std::function<void(QByteArray, QString)> complete);
|
||||||
|
|
||||||
|
QNetworkAccessManager m_network;
|
||||||
|
QList<QNetworkReply*> m_catalog_replies;
|
||||||
|
QPointer<QNetworkReply> m_file_reply;
|
||||||
|
QList<RemoteFile> m_files;
|
||||||
|
QStringList m_errors;
|
||||||
|
int m_catalog_generation = 0;
|
||||||
|
int m_fetch_generation = 0;
|
||||||
|
int m_pending = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Cheats
|
||||||
|
|
||||||
|
#endif // CHEAT_REPOSITORY_H
|
||||||
@@ -1,30 +1,51 @@
|
|||||||
#ifndef PATCHES_DIALOG_H
|
#ifndef PATCHES_DIALOG_H
|
||||||
#define PATCHES_DIALOG_H
|
#define PATCHES_DIALOG_H
|
||||||
|
|
||||||
|
#include "cheatRepository.h"
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
|
#include <QJsonDocument>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
class Configuration;
|
class Configuration;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
class QListWidget;
|
class QListWidget;
|
||||||
class QPushButton;
|
class QPushButton;
|
||||||
|
class QTabWidget;
|
||||||
|
class QTreeWidget;
|
||||||
|
|
||||||
class PatchesDialog final: public QDialog {
|
class PatchesDialog final: public QDialog {
|
||||||
public:
|
public:
|
||||||
explicit PatchesDialog(const Configuration& game, QWidget* parent = nullptr);
|
explicit PatchesDialog(const Configuration& game, QWidget* parent = nullptr);
|
||||||
~PatchesDialog() override = default;
|
~PatchesDialog() override = default;
|
||||||
|
|
||||||
[[nodiscard]] static bool IsSupportedTitleId(const QString& title_id);
|
|
||||||
[[nodiscard]] static QString PatchPlanPath(const QString& title_id);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void Load();
|
void Load();
|
||||||
void Save();
|
void Save();
|
||||||
|
void LoadRemote();
|
||||||
|
void PreviewRemote();
|
||||||
|
void ImportRemote();
|
||||||
|
|
||||||
QString m_title_id;
|
QString m_title_id;
|
||||||
QListWidget* m_patches = nullptr;
|
QString m_version;
|
||||||
QLabel* m_status = nullptr;
|
QString m_process;
|
||||||
QPushButton* m_apply = nullptr;
|
QByteArray m_local_data;
|
||||||
|
QJsonDocument m_local_document;
|
||||||
|
QJsonDocument m_remote_document;
|
||||||
|
bool m_local_readable = false;
|
||||||
|
Cheats::CheatRepository* m_repository = nullptr;
|
||||||
|
QList<Cheats::RemoteFile> m_remote_files;
|
||||||
|
QTabWidget* m_tabs = nullptr;
|
||||||
|
QTreeWidget* m_files = nullptr;
|
||||||
|
QListWidget* m_preview = nullptr;
|
||||||
|
QLabel* m_remote_status = nullptr;
|
||||||
|
QLabel* m_preview_status = nullptr;
|
||||||
|
QPushButton* m_refresh = nullptr;
|
||||||
|
QPushButton* m_import = nullptr;
|
||||||
|
QListWidget* m_patches = nullptr;
|
||||||
|
QLabel* m_status = nullptr;
|
||||||
|
QPushButton* m_apply = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // PATCHES_DIALOG_H
|
#endif // PATCHES_DIALOG_H
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
#include "cheatFile.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QSaveFile>
|
||||||
|
|
||||||
|
namespace Cheats {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
QString Tr(const char* text) {
|
||||||
|
return QCoreApplication::translate("Cheats", text);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ValidateDocument(const QJsonDocument& document) {
|
||||||
|
if (!document.isObject()) {
|
||||||
|
return Tr("Invalid cheat JSON.");
|
||||||
|
}
|
||||||
|
const auto mods = document.object().value(QStringLiteral("mods"));
|
||||||
|
if (!mods.isArray()) {
|
||||||
|
return Tr("Cheat JSON has no \"mods\" array.");
|
||||||
|
}
|
||||||
|
for (const auto& value: mods.toArray()) {
|
||||||
|
const auto mod = value.toObject();
|
||||||
|
const auto name = mod.value(QStringLiteral("name"));
|
||||||
|
const auto enabled = mod.value(QStringLiteral("enabled"));
|
||||||
|
if (!value.isObject() || !name.isString() || name.toString().isEmpty() ||
|
||||||
|
(!enabled.isUndefined() && !enabled.isBool())) {
|
||||||
|
return Tr("Invalid cheat entry.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool IsSupportedTitleId(const QString& title_id) {
|
||||||
|
static const QRegularExpression pattern(QStringLiteral("\\APPSA[0-9]{5}\\z"));
|
||||||
|
return pattern.match(title_id.trimmed().toUpper()).hasMatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PlanPath(const QString& title_id) {
|
||||||
|
if (!IsSupportedTitleId(title_id)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return QDir(QCoreApplication::applicationDirPath())
|
||||||
|
.filePath(QStringLiteral("_Patches/%1.json").arg(title_id.trimmed().toUpper()));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ParseDocument(const QByteArray& data, QJsonDocument* document) {
|
||||||
|
const auto parsed = QJsonDocument::fromJson(data);
|
||||||
|
const auto error = ValidateDocument(parsed);
|
||||||
|
*document = error.isEmpty() ? parsed : QJsonDocument();
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ValidateImport(const QJsonDocument& document, const QString& title_id,
|
||||||
|
const QString& version, const QString& process) {
|
||||||
|
const auto parse_error = ValidateDocument(document);
|
||||||
|
if (!parse_error.isEmpty()) {
|
||||||
|
return parse_error;
|
||||||
|
}
|
||||||
|
const auto root = document.object();
|
||||||
|
if (!IsSupportedTitleId(title_id) ||
|
||||||
|
root.value(QStringLiteral("id"))
|
||||||
|
.toString()
|
||||||
|
.compare(title_id.trimmed(), Qt::CaseInsensitive) != 0) {
|
||||||
|
return Tr("The cheat file does not match this game's title ID.");
|
||||||
|
}
|
||||||
|
if (version.isEmpty() || root.value(QStringLiteral("version")).toString() != version) {
|
||||||
|
return Tr("The cheat file does not match this game's version (%1).").arg(version);
|
||||||
|
}
|
||||||
|
if (process.isEmpty() ||
|
||||||
|
root.value(QStringLiteral("process")).toString().compare(process, Qt::CaseInsensitive) !=
|
||||||
|
0) {
|
||||||
|
return Tr("The cheat file does not match this game's executable (%1).").arg(process);
|
||||||
|
}
|
||||||
|
static const QRegularExpression offset_pattern(
|
||||||
|
QStringLiteral("\\A(?:0[xX])?[0-9a-fA-F]{1,16}\\z"));
|
||||||
|
static const QRegularExpression bytes_pattern(QStringLiteral("\\A(?:[0-9a-fA-F]{2})+\\z"));
|
||||||
|
const auto mods = root.value(QStringLiteral("mods")).toArray();
|
||||||
|
if (mods.isEmpty()) {
|
||||||
|
return Tr("The cheat file contains no cheats.");
|
||||||
|
}
|
||||||
|
if (root.contains(QStringLiteral("master"))) {
|
||||||
|
return Tr("This JSON uses master codes, which are not supported yet.");
|
||||||
|
}
|
||||||
|
for (const auto& value: mods) {
|
||||||
|
const auto mod = value.toObject();
|
||||||
|
const auto memory = mod.value(QStringLiteral("memory"));
|
||||||
|
if (mod.contains(QStringLiteral("module_name"))) {
|
||||||
|
return Tr("This JSON uses module-specific cheats, which are not supported yet.");
|
||||||
|
}
|
||||||
|
if (!memory.isArray() || memory.toArray().isEmpty()) {
|
||||||
|
return Tr("Unsupported memory entries in cheat \"%1\".")
|
||||||
|
.arg(mod.value(QStringLiteral("name")).toString());
|
||||||
|
}
|
||||||
|
for (const auto& entry: memory.toArray()) {
|
||||||
|
const auto write = entry.toObject();
|
||||||
|
const auto offset = write.value(QStringLiteral("offset")).toString();
|
||||||
|
const auto off = write.value(QStringLiteral("off")).toString();
|
||||||
|
const auto on = write.value(QStringLiteral("on")).toString();
|
||||||
|
const auto absolute = write.value(QStringLiteral("absolute"));
|
||||||
|
const auto expected = write.value(QStringLiteral("expected"));
|
||||||
|
if ((!absolute.isUndefined() && (!absolute.isBool() || absolute.toBool())) ||
|
||||||
|
(!expected.isUndefined() &&
|
||||||
|
(!expected.isString() ||
|
||||||
|
expected.toString().compare(off, Qt::CaseInsensitive) != 0)) ||
|
||||||
|
write.contains(QStringLiteral("section")) ||
|
||||||
|
write.contains(QStringLiteral("sectionName")) ||
|
||||||
|
write.contains(QStringLiteral("sectionProtection")) ||
|
||||||
|
write.contains(QStringLiteral("logicalOffset")) ||
|
||||||
|
write.contains(QStringLiteral("sectionOffset"))) {
|
||||||
|
return Tr("This JSON uses memory addressing features that are not supported yet.");
|
||||||
|
}
|
||||||
|
if (!entry.isObject() || !offset_pattern.match(offset).hasMatch() ||
|
||||||
|
!bytes_pattern.match(off).hasMatch() || !bytes_pattern.match(on).hasMatch() ||
|
||||||
|
off.size() != on.size()) {
|
||||||
|
return Tr("Unsupported memory entries in cheat \"%1\".")
|
||||||
|
.arg(mod.value(QStringLiteral("name")).toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonDocument WithSelection(const QJsonDocument& document, const QList<bool>& enabled) {
|
||||||
|
auto root = document.object();
|
||||||
|
auto mods = root.value(QStringLiteral("mods")).toArray();
|
||||||
|
if (mods.size() != enabled.size()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
for (qsizetype index = 0; index < mods.size(); ++index) {
|
||||||
|
auto mod = mods[index].toObject();
|
||||||
|
mod.insert(QStringLiteral("enabled"), enabled[index]);
|
||||||
|
mods[index] = mod;
|
||||||
|
}
|
||||||
|
root.insert(QStringLiteral("mods"), mods);
|
||||||
|
return QJsonDocument(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SaveDocument(const QString& path, const QJsonDocument& document,
|
||||||
|
const QByteArray& expected_data) {
|
||||||
|
if (path.isEmpty() || !document.isObject()) {
|
||||||
|
return Tr("Invalid cheat file.");
|
||||||
|
}
|
||||||
|
QFile input(path);
|
||||||
|
if (input.exists()) {
|
||||||
|
if (!input.open(QIODevice::ReadOnly)) {
|
||||||
|
return Tr("Could not read cheat file: %1").arg(input.errorString());
|
||||||
|
}
|
||||||
|
const auto current = input.readAll();
|
||||||
|
if (input.error() != QFile::NoError) {
|
||||||
|
return Tr("Could not read cheat file: %1").arg(input.errorString());
|
||||||
|
}
|
||||||
|
if (current != expected_data) {
|
||||||
|
return Tr("Cheat file changed; reload the dialog.");
|
||||||
|
}
|
||||||
|
} else if (!expected_data.isEmpty()) {
|
||||||
|
return Tr("Cheat file changed; reload the dialog.");
|
||||||
|
}
|
||||||
|
input.close();
|
||||||
|
if (!QDir().mkpath(QFileInfo(path).absolutePath())) {
|
||||||
|
return Tr("Could not create the local cheats directory.");
|
||||||
|
}
|
||||||
|
QSaveFile output(path);
|
||||||
|
const auto data = document.toJson();
|
||||||
|
if (!output.open(QIODevice::WriteOnly) || output.write(data) != data.size() ||
|
||||||
|
!output.commit()) {
|
||||||
|
return Tr("Could not save cheat file: %1").arg(output.errorString());
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Cheats
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
#include "cheatRepository.h"
|
||||||
|
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QNetworkRequest>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace Cheats {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr qint64 kIndexLimit = 2 * 1024 * 1024;
|
||||||
|
constexpr qint64 kFileLimit = 8 * 1024 * 1024;
|
||||||
|
|
||||||
|
const QString kBaseUrl = QStringLiteral(
|
||||||
|
"https://raw.githubusercontent.com/TeeKay87/HEN-Cheats-Collection/master/cheats/");
|
||||||
|
|
||||||
|
bool IsFormat(const QString& format) {
|
||||||
|
return format == QStringLiteral("json") || format == QStringLiteral("mc4") ||
|
||||||
|
format == QStringLiteral("shn");
|
||||||
|
}
|
||||||
|
|
||||||
|
QRegularExpressionMatch MatchName(const QString& name) {
|
||||||
|
static const QRegularExpression pattern(QStringLiteral(
|
||||||
|
"\\A([A-Z]{4}[0-9]{5})_([0-9]+(?:\\.[0-9]+)+)_[A-Za-z0-9_.-]+\\.(json|mc4|shn)\\z"));
|
||||||
|
return pattern.match(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool RemoteFile::IsSupported() const {
|
||||||
|
return format == QStringLiteral("json") && !Url().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
QUrl RemoteFile::Url() const {
|
||||||
|
const auto match = MatchName(name);
|
||||||
|
if (!IsFormat(format) || !match.hasMatch() || match.captured(3) != format) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return QUrl(kBaseUrl + format + QLatin1Char('/') + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
CatalogResult ParseIndex(const QByteArray& data, const QString& format, const QString& title_id) {
|
||||||
|
CatalogResult result;
|
||||||
|
if (!IsFormat(format) || data.isEmpty() || data.size() > kIndexLimit) {
|
||||||
|
result.error = CheatRepository::tr("Invalid cheat index.");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto id = title_id.trimmed().toUpper();
|
||||||
|
QSet<QString> seen;
|
||||||
|
int invalid = 0;
|
||||||
|
auto text = QString::fromUtf8(data);
|
||||||
|
if (text.startsWith(QChar(0xfeff))) {
|
||||||
|
text.remove(0, 1);
|
||||||
|
}
|
||||||
|
for (const auto& line: text.split(QLatin1Char('\n'))) {
|
||||||
|
const auto trimmed = line.trimmed();
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto separator = trimmed.indexOf(QLatin1Char('='));
|
||||||
|
const auto name = trimmed.left(separator);
|
||||||
|
const auto match = MatchName(name);
|
||||||
|
if (separator < 0 || !match.hasMatch() || match.captured(3) != format) {
|
||||||
|
invalid++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (match.captured(1) != id || seen.contains(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.insert(name);
|
||||||
|
result.files.append(
|
||||||
|
{name, trimmed.mid(separator + 1).trimmed(), match.captured(2), format});
|
||||||
|
}
|
||||||
|
if (invalid != 0) {
|
||||||
|
result.error = CheatRepository::tr("Skipped %1 invalid cheat index entries.").arg(invalid);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
CheatRepository::CheatRepository(QObject* parent): QObject(parent), m_network(this) {}
|
||||||
|
|
||||||
|
QNetworkReply* CheatRepository::Request(const QUrl& url, qint64 limit,
|
||||||
|
std::function<void(QByteArray, QString)> complete) {
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
|
||||||
|
QNetworkRequest::SameOriginRedirectPolicy);
|
||||||
|
request.setRawHeader("User-Agent", "Kyty-Launcher");
|
||||||
|
request.setTransferTimeout(15000);
|
||||||
|
auto* reply = m_network.get(request);
|
||||||
|
reply->setReadBufferSize(limit + 1);
|
||||||
|
connect(reply, &QNetworkReply::readyRead, this, [reply, limit]() {
|
||||||
|
if (reply->bytesAvailable() > limit) {
|
||||||
|
reply->setProperty("too_large", true);
|
||||||
|
reply->abort();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
QTimer::singleShot(30000, reply, [reply]() {
|
||||||
|
if (!reply->isFinished()) {
|
||||||
|
reply->setProperty("timed_out", true);
|
||||||
|
reply->abort();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(reply, &QNetworkReply::finished, this,
|
||||||
|
[this, reply, limit, complete = std::move(complete)]() {
|
||||||
|
QString error;
|
||||||
|
if (reply->property("too_large").toBool() || reply->bytesAvailable() > limit) {
|
||||||
|
error = tr("Cheat download exceeds the size limit.");
|
||||||
|
} else if (reply->property("timed_out").toBool()) {
|
||||||
|
error = tr("Cheat download timed out.");
|
||||||
|
} else if (reply->error() != QNetworkReply::NoError) {
|
||||||
|
error = reply->errorString();
|
||||||
|
} else if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() !=
|
||||||
|
200) {
|
||||||
|
error = tr("Unexpected response from the cheat repository.");
|
||||||
|
}
|
||||||
|
const auto data = error.isEmpty() ? reply->readAll() : QByteArray();
|
||||||
|
m_catalog_replies.removeAll(reply);
|
||||||
|
if (m_file_reply == reply) {
|
||||||
|
m_file_reply.clear();
|
||||||
|
}
|
||||||
|
reply->deleteLater();
|
||||||
|
complete(data, error);
|
||||||
|
});
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CheatRepository::Load(const QString& title_id) {
|
||||||
|
const auto generation = ++m_catalog_generation;
|
||||||
|
for (auto* reply: std::exchange(m_catalog_replies, {})) {
|
||||||
|
reply->abort();
|
||||||
|
}
|
||||||
|
CancelFetch();
|
||||||
|
m_files.clear();
|
||||||
|
m_errors.clear();
|
||||||
|
m_pending = 3;
|
||||||
|
for (const auto& format:
|
||||||
|
{QStringLiteral("json"), QStringLiteral("mc4"), QStringLiteral("shn")}) {
|
||||||
|
auto* reply =
|
||||||
|
Request(QUrl(kBaseUrl + format + QStringLiteral(".txt")), kIndexLimit,
|
||||||
|
[this, generation, format, title_id](const QByteArray& data, QString error) {
|
||||||
|
if (generation != m_catalog_generation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (error.isEmpty()) {
|
||||||
|
auto result = ParseIndex(data, format, title_id);
|
||||||
|
m_files.append(result.files);
|
||||||
|
error = result.error;
|
||||||
|
}
|
||||||
|
if (!error.isEmpty()) {
|
||||||
|
m_errors.append(format.toUpper() + QStringLiteral(": ") + error);
|
||||||
|
}
|
||||||
|
if (--m_pending == 0) {
|
||||||
|
std::sort(m_files.begin(), m_files.end(),
|
||||||
|
[](const RemoteFile& left, const RemoteFile& right) {
|
||||||
|
if (left.version != right.version) {
|
||||||
|
return left.version > right.version;
|
||||||
|
}
|
||||||
|
if (left.format != right.format) {
|
||||||
|
return left.format < right.format;
|
||||||
|
}
|
||||||
|
return left.name < right.name;
|
||||||
|
});
|
||||||
|
emit CatalogLoaded(m_files, m_errors.join(QLatin1Char('\n')));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
m_catalog_replies.append(reply);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CheatRepository::Fetch(const RemoteFile& file) {
|
||||||
|
CancelFetch();
|
||||||
|
if (!file.IsSupported()) {
|
||||||
|
emit FileLoaded({}, tr("Only JSON cheat files are supported."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto generation = m_fetch_generation;
|
||||||
|
m_file_reply = Request(file.Url(), kFileLimit,
|
||||||
|
[this, generation](const QByteArray& data, const QString& error) {
|
||||||
|
if (generation == m_fetch_generation) {
|
||||||
|
emit FileLoaded(data, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void CheatRepository::CancelFetch() {
|
||||||
|
m_fetch_generation++;
|
||||||
|
if (m_file_reply != nullptr) {
|
||||||
|
auto* reply = m_file_reply.data();
|
||||||
|
m_file_reply.clear();
|
||||||
|
reply->abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Cheats
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "configurationListWidget.h"
|
#include "configurationListWidget.h"
|
||||||
|
|
||||||
|
#include "cheatFile.h"
|
||||||
#include "common/archive.h"
|
#include "common/archive.h"
|
||||||
#include "compatibilityDatabase.h"
|
#include "compatibilityDatabase.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
@@ -1015,7 +1016,7 @@ void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
action_patches->setVisible(item != nullptr &&
|
action_patches->setVisible(item != nullptr &&
|
||||||
PatchesDialog::IsSupportedTitleId(item->GetInfo().title_id));
|
Cheats::IsSupportedTitleId(item->GetInfo().title_id));
|
||||||
QAction* action_remove_save_data =
|
QAction* action_remove_save_data =
|
||||||
menu.addAction(style()->standardIcon(QStyle::SP_DialogDiscardButton),
|
menu.addAction(style()->standardIcon(QStyle::SP_DialogDiscardButton),
|
||||||
tr("Remove save data..."), this, SLOT(remove_save_data()));
|
tr("Remove save data..."), this, SLOT(remove_save_data()));
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#include "mainDialog.h"
|
#include "mainDialog.h"
|
||||||
|
|
||||||
|
#include "cheatFile.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "configurationItem.h"
|
#include "configurationItem.h"
|
||||||
#include "configurationListWidget.h"
|
#include "configurationListWidget.h"
|
||||||
#include "controllerLightbar.h"
|
#include "controllerLightbar.h"
|
||||||
#include "gameContent.h"
|
#include "gameContent.h"
|
||||||
#include "patchesDialog.h"
|
|
||||||
#include "updateChecker.h"
|
#include "updateChecker.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
@@ -285,7 +285,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
|
|||||||
}
|
}
|
||||||
args << "--game" << game;
|
args << "--game" << game;
|
||||||
|
|
||||||
const auto patch_plan = PatchesDialog::PatchPlanPath(info.title_id);
|
const auto patch_plan = Cheats::PlanPath(info.title_id);
|
||||||
if (QFileInfo::exists(patch_plan)) {
|
if (QFileInfo::exists(patch_plan)) {
|
||||||
args << "--game-patch" << patch_plan;
|
args << "--game-patch" << patch_plan;
|
||||||
}
|
}
|
||||||
|
|||||||
+270
-101
@@ -1,140 +1,309 @@
|
|||||||
#include "patchesDialog.h"
|
#include "patchesDialog.h"
|
||||||
|
|
||||||
|
#include "cheatFile.h"
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
|
||||||
#include <QCoreApplication>
|
|
||||||
#include <QDebug>
|
|
||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
#include <QDir>
|
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QHeaderView>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QListWidget>
|
#include <QListWidget>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QSaveFile>
|
#include <QSplitter>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QTabWidget>
|
||||||
|
#include <QTreeWidget>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void ShowMods(QListWidget* list, const QJsonDocument& document, bool checkable) {
|
||||||
|
list->clear();
|
||||||
|
for (const auto& value: document.object().value(QStringLiteral("mods")).toArray()) {
|
||||||
|
const auto mod = value.toObject();
|
||||||
|
auto* item = new QListWidgetItem(mod.value(QStringLiteral("name")).toString(), list);
|
||||||
|
if (checkable) {
|
||||||
|
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
||||||
|
item->setCheckState(mod.value(QStringLiteral("enabled")).toBool(true) ? Qt::Checked
|
||||||
|
: Qt::Unchecked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QLabel* StatusLabel(QWidget* parent) {
|
||||||
|
auto* label = new QLabel(parent);
|
||||||
|
label->setWordWrap(true);
|
||||||
|
label->setTextFormat(Qt::PlainText);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
PatchesDialog::PatchesDialog(const Configuration& game, QWidget* parent)
|
PatchesDialog::PatchesDialog(const Configuration& game, QWidget* parent)
|
||||||
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()) {
|
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()), m_version(game.gameVersion),
|
||||||
|
m_process(QFileInfo(game.elf.isEmpty() ? QStringLiteral("eboot.bin") : game.elf).fileName()) {
|
||||||
setAttribute(Qt::WA_DeleteOnClose);
|
setAttribute(Qt::WA_DeleteOnClose);
|
||||||
setWindowTitle(tr("Cheats (experimental) - %1").arg(game.name));
|
setWindowTitle(tr("Cheats (experimental) - %1").arg(game.name));
|
||||||
resize(640, 480);
|
resize(800, 640);
|
||||||
|
|
||||||
auto* layout = new QVBoxLayout(this);
|
auto* layout = new QVBoxLayout(this);
|
||||||
m_patches = new QListWidget(this);
|
auto* game_label = StatusLabel(this);
|
||||||
m_status = new QLabel(this);
|
game_label->setText(tr("%1 · Installed version: %2")
|
||||||
m_apply = new QPushButton(tr("Apply selection"), this);
|
.arg(m_title_id, m_version.isEmpty() ? tr("Unknown") : m_version));
|
||||||
auto* close = new QPushButton(tr("Close"), this);
|
layout->addWidget(game_label);
|
||||||
|
m_tabs = new QTabWidget(this);
|
||||||
|
layout->addWidget(m_tabs);
|
||||||
|
|
||||||
m_status->setWordWrap(true);
|
auto* remote = new QWidget(m_tabs);
|
||||||
layout->addWidget(m_patches);
|
auto* remote_layout = new QVBoxLayout(remote);
|
||||||
layout->addWidget(m_status);
|
m_tabs->addTab(remote, tr("Remote"));
|
||||||
|
auto* credits = new QLabel(
|
||||||
|
tr("Cheats by <a href=\"https://github.com/TeeKay87/HEN-Cheats-Collection\">TeeKay87 / HEN "
|
||||||
|
"Cheats Collection</a> and its contributors."),
|
||||||
|
remote);
|
||||||
|
credits->setOpenExternalLinks(true);
|
||||||
|
credits->setWordWrap(true);
|
||||||
|
remote_layout->addWidget(credits);
|
||||||
|
auto* support = StatusLabel(remote);
|
||||||
|
support->setText(tr("Only JSON cheats are supported. MC4 and SHN files are shown disabled. "
|
||||||
|
"Select a JSON file to preview its cheats."));
|
||||||
|
remote_layout->addWidget(support);
|
||||||
|
auto* split = new QSplitter(Qt::Vertical, remote);
|
||||||
|
m_files = new QTreeWidget(split);
|
||||||
|
m_files->setHeaderLabels({tr("Version"), tr("Format"), tr("File")});
|
||||||
|
m_files->setRootIsDecorated(false);
|
||||||
|
m_files->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||||
|
m_files->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
||||||
|
m_files->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
|
||||||
|
m_files->header()->setSectionResizeMode(2, QHeaderView::Stretch);
|
||||||
|
m_preview = new QListWidget(split);
|
||||||
|
remote_layout->addWidget(split, 1);
|
||||||
|
m_remote_status = StatusLabel(remote);
|
||||||
|
m_preview_status = StatusLabel(remote);
|
||||||
|
remote_layout->addWidget(m_remote_status);
|
||||||
|
remote_layout->addWidget(m_preview_status);
|
||||||
|
auto* remote_buttons = new QDialogButtonBox(remote);
|
||||||
|
m_refresh = remote_buttons->addButton(tr("Refresh"), QDialogButtonBox::ActionRole);
|
||||||
|
m_import = remote_buttons->addButton(tr("Import to Local"), QDialogButtonBox::ActionRole);
|
||||||
|
m_import->setEnabled(false);
|
||||||
|
remote_layout->addWidget(remote_buttons);
|
||||||
|
|
||||||
auto* buttons = new QDialogButtonBox(this);
|
auto* local = new QWidget(m_tabs);
|
||||||
buttons->addButton(m_apply, QDialogButtonBox::ActionRole);
|
auto* local_layout = new QVBoxLayout(local);
|
||||||
buttons->addButton(close, QDialogButtonBox::RejectRole);
|
m_tabs->addTab(local, tr("Local"));
|
||||||
|
m_patches = new QListWidget(local);
|
||||||
|
m_status = StatusLabel(local);
|
||||||
|
local_layout->addWidget(m_patches, 1);
|
||||||
|
local_layout->addWidget(m_status);
|
||||||
|
auto* local_buttons = new QDialogButtonBox(local);
|
||||||
|
auto* reload = local_buttons->addButton(tr("Reload"), QDialogButtonBox::ActionRole);
|
||||||
|
m_apply = local_buttons->addButton(tr("Apply selection"), QDialogButtonBox::ActionRole);
|
||||||
|
local_layout->addWidget(local_buttons);
|
||||||
|
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
|
||||||
layout->addWidget(buttons);
|
layout->addWidget(buttons);
|
||||||
|
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close);
|
||||||
|
connect(reload, &QPushButton::clicked, this, &PatchesDialog::Load);
|
||||||
connect(m_apply, &QPushButton::clicked, this, &PatchesDialog::Save);
|
connect(m_apply, &QPushButton::clicked, this, &PatchesDialog::Save);
|
||||||
connect(close, &QPushButton::clicked, this, &QDialog::close);
|
connect(m_refresh, &QPushButton::clicked, this, &PatchesDialog::LoadRemote);
|
||||||
|
connect(m_import, &QPushButton::clicked, this, &PatchesDialog::ImportRemote);
|
||||||
|
connect(m_files, &QTreeWidget::currentItemChanged, this, &PatchesDialog::PreviewRemote);
|
||||||
|
|
||||||
|
m_repository = new Cheats::CheatRepository(this);
|
||||||
|
connect(
|
||||||
|
m_repository, &Cheats::CheatRepository::CatalogLoaded, this,
|
||||||
|
[this](QList<Cheats::RemoteFile> files, const QString& error) {
|
||||||
|
std::stable_sort(files.begin(), files.end(), [this](const auto& a, const auto& b) {
|
||||||
|
return (a.version == m_version) > (b.version == m_version);
|
||||||
|
});
|
||||||
|
m_remote_files = std::move(files);
|
||||||
|
for (qsizetype index = 0; index < m_remote_files.size(); ++index) {
|
||||||
|
const auto& file = m_remote_files[index];
|
||||||
|
auto* item = new QTreeWidgetItem(
|
||||||
|
m_files, {file.version == m_version ? tr("%1 (installed)").arg(file.version)
|
||||||
|
: file.version,
|
||||||
|
file.format.toUpper(), file.name});
|
||||||
|
item->setData(0, Qt::UserRole, index);
|
||||||
|
for (int column = 0; column < 3; ++column) {
|
||||||
|
item->setToolTip(column, file.IsSupported()
|
||||||
|
? file.title
|
||||||
|
: tr("%1 — %2 is not supported.")
|
||||||
|
.arg(file.title, file.format.toUpper()));
|
||||||
|
}
|
||||||
|
if (!file.IsSupported()) {
|
||||||
|
item->setFlags(item->flags() & ~(Qt::ItemIsEnabled | Qt::ItemIsSelectable));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto status =
|
||||||
|
m_remote_files.isEmpty() && error.isEmpty()
|
||||||
|
? tr("No remote cheats found for %1.").arg(m_title_id)
|
||||||
|
: tr("%1 remote file(s) for %2.").arg(m_remote_files.size()).arg(m_title_id);
|
||||||
|
if (!error.isEmpty()) {
|
||||||
|
status = tr("%1\nCould not load the complete collection: %2\nUse Refresh to retry.")
|
||||||
|
.arg(status, error);
|
||||||
|
}
|
||||||
|
m_remote_status->setText(status);
|
||||||
|
m_refresh->setEnabled(true);
|
||||||
|
});
|
||||||
|
connect(
|
||||||
|
m_repository, &Cheats::CheatRepository::FileLoaded, this,
|
||||||
|
[this](const QByteArray& data, QString error) {
|
||||||
|
if (error.isEmpty()) {
|
||||||
|
error = Cheats::ParseDocument(data, &m_remote_document);
|
||||||
|
}
|
||||||
|
if (!error.isEmpty()) {
|
||||||
|
m_preview_status->setText(
|
||||||
|
tr("Could not preview this file: %1\nSelect another file or refresh to retry.")
|
||||||
|
.arg(error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ShowMods(m_preview, m_remote_document, false);
|
||||||
|
error = Cheats::ValidateImport(m_remote_document, m_title_id, m_version, m_process);
|
||||||
|
const auto root = m_remote_document.object();
|
||||||
|
QStringList creators;
|
||||||
|
for (const auto& value: root.value(QStringLiteral("credits")).toArray()) {
|
||||||
|
if (value.isString()) {
|
||||||
|
creators.append(value.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const auto metadata =
|
||||||
|
tr("%1\nCredits: %2 · Version: %3")
|
||||||
|
.arg(root.value(QStringLiteral("name")).toString(),
|
||||||
|
creators.isEmpty() ? root.value(QStringLiteral("author")).toString()
|
||||||
|
: creators.join(QStringLiteral(", ")),
|
||||||
|
root.value(QStringLiteral("version")).toString());
|
||||||
|
m_preview_status->setText(
|
||||||
|
metadata + QStringLiteral("\n\n") +
|
||||||
|
(error.isEmpty() ? tr("Import to Local, then select the cheats to enable. Changes "
|
||||||
|
"take effect on the next game launch.")
|
||||||
|
: error));
|
||||||
|
m_import->setEnabled(error.isEmpty());
|
||||||
|
});
|
||||||
Load();
|
Load();
|
||||||
}
|
LoadRemote();
|
||||||
|
|
||||||
bool PatchesDialog::IsSupportedTitleId(const QString& title_id) {
|
|
||||||
return title_id.trimmed().toUpper().startsWith(QStringLiteral("PPSA"));
|
|
||||||
}
|
|
||||||
|
|
||||||
QString PatchesDialog::PatchPlanPath(const QString& title_id) {
|
|
||||||
return QDir(QCoreApplication::applicationDirPath())
|
|
||||||
.filePath(QStringLiteral("_Patches/%1.json").arg(title_id.trimmed().toUpper()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PatchesDialog::Load() {
|
void PatchesDialog::Load() {
|
||||||
QFile file(PatchPlanPath(m_title_id));
|
m_patches->clear();
|
||||||
|
m_local_data.clear();
|
||||||
|
m_local_document = {};
|
||||||
|
m_local_readable = false;
|
||||||
|
m_apply->setEnabled(false);
|
||||||
|
const auto path = Cheats::PlanPath(m_title_id);
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
m_status->setText(tr("Unsupported title ID."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QFile file(path);
|
||||||
|
if (!file.exists()) {
|
||||||
|
m_local_readable = true;
|
||||||
|
m_status->setText(tr("No local cheat file: %1").arg(path));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
m_status->setText(tr("No local cheat file: %1").arg(file.fileName()));
|
m_status->setText(tr("Could not read cheat file: %1").arg(file.errorString()));
|
||||||
m_apply->setEnabled(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
m_local_data = file.readAll();
|
||||||
const auto document = QJsonDocument::fromJson(file.readAll());
|
if (file.error() != QFile::NoError) {
|
||||||
if (document.isNull() || !document.isObject()) {
|
m_status->setText(tr("Could not read cheat file: %1").arg(file.errorString()));
|
||||||
m_status->setText(tr("Invalid cheat JSON in %1.").arg(file.fileName()));
|
|
||||||
m_apply->setEnabled(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
m_local_readable = true;
|
||||||
const auto root = document.object();
|
const auto error = Cheats::ParseDocument(m_local_data, &m_local_document);
|
||||||
if (!root.value(QStringLiteral("mods")).isArray()) {
|
if (!error.isEmpty()) {
|
||||||
m_status->setText(tr("Cheat JSON has no \"mods\" array in %1.").arg(file.fileName()));
|
m_status->setText(tr("%1\n%2").arg(error, path));
|
||||||
m_apply->setEnabled(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
ShowMods(m_patches, m_local_document, true);
|
||||||
const auto mods = root.value(QStringLiteral("mods")).toArray();
|
m_apply->setEnabled(m_patches->count() > 0);
|
||||||
for (const auto& value: mods) {
|
m_status->setText(tr("Loaded %1 cheat(s) from %2.").arg(m_patches->count()).arg(path));
|
||||||
if (!value.isObject()) {
|
|
||||||
m_status->setText(tr("Invalid cheat entry in %1.").arg(file.fileName()));
|
|
||||||
m_apply->setEnabled(false);
|
|
||||||
m_patches->clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto mod = value.toObject();
|
|
||||||
const auto name = mod.value(QStringLiteral("name"));
|
|
||||||
const auto enabled = mod.value(QStringLiteral("enabled"));
|
|
||||||
if (!name.isString() || name.toString().isEmpty() ||
|
|
||||||
(!enabled.isUndefined() && !enabled.isBool())) {
|
|
||||||
m_status->setText(tr("Invalid cheat entry in %1.").arg(file.fileName()));
|
|
||||||
m_apply->setEnabled(false);
|
|
||||||
m_patches->clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto* item = new QListWidgetItem(name.toString(), m_patches);
|
|
||||||
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
|
||||||
item->setCheckState(enabled.toBool(true) ? Qt::Checked : Qt::Unchecked);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_apply->setEnabled(!mods.isEmpty());
|
|
||||||
m_status->setText(tr("Loaded %1 cheat(s) from %2.").arg(mods.size()).arg(file.fileName()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PatchesDialog::Save() {
|
void PatchesDialog::Save() {
|
||||||
const auto path = PatchPlanPath(m_title_id);
|
QList<bool> enabled;
|
||||||
QFile input(path);
|
for (int index = 0; index < m_patches->count(); ++index) {
|
||||||
if (!input.open(QIODevice::ReadOnly)) {
|
enabled.append(m_patches->item(index)->checkState() == Qt::Checked);
|
||||||
|
}
|
||||||
|
const auto document = Cheats::WithSelection(m_local_document, enabled);
|
||||||
|
const auto error = Cheats::SaveDocument(Cheats::PlanPath(m_title_id), document, m_local_data);
|
||||||
|
if (!error.isEmpty()) {
|
||||||
|
m_status->setText(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
m_local_document = document;
|
||||||
auto document = QJsonDocument::fromJson(input.readAll());
|
m_local_data = document.toJson();
|
||||||
input.close();
|
m_status->setText(tr("Cheat selection saved. Changes take effect on the next game launch."));
|
||||||
const auto changed_message = tr("Cheat file changed; reload the dialog.");
|
}
|
||||||
if (!document.isObject()) {
|
|
||||||
m_status->setText(changed_message);
|
void PatchesDialog::LoadRemote() {
|
||||||
return;
|
m_repository->CancelFetch();
|
||||||
}
|
m_files->clear();
|
||||||
auto root = document.object();
|
m_remote_files.clear();
|
||||||
auto mods = root.value(QStringLiteral("mods")).toArray();
|
m_preview->clear();
|
||||||
if (mods.size() != m_patches->count()) {
|
m_remote_document = {};
|
||||||
m_status->setText(changed_message);
|
m_import->setEnabled(false);
|
||||||
return;
|
m_refresh->setEnabled(false);
|
||||||
}
|
m_preview_status->clear();
|
||||||
for (int index = 0; index < mods.size(); index++) {
|
m_remote_status->setText(tr("Loading remote cheats…"));
|
||||||
auto mod = mods[index].toObject();
|
m_repository->Load(m_title_id);
|
||||||
mod.insert(QStringLiteral("enabled"), m_patches->item(index)->checkState() == Qt::Checked);
|
}
|
||||||
mods[index] = mod;
|
|
||||||
}
|
void PatchesDialog::PreviewRemote() {
|
||||||
root.insert(QStringLiteral("mods"), mods);
|
m_repository->CancelFetch();
|
||||||
document.setObject(root);
|
m_preview->clear();
|
||||||
|
m_remote_document = {};
|
||||||
QSaveFile output(path);
|
m_import->setEnabled(false);
|
||||||
if (!output.open(QIODevice::WriteOnly) || output.write(document.toJson()) < 0 ||
|
m_preview_status->clear();
|
||||||
!output.commit()) {
|
const auto* item = m_files->currentItem();
|
||||||
qWarning() << "Could not save cheat file:" << output.errorString();
|
if (item == nullptr) {
|
||||||
m_status->setText(tr("Could not save cheat file: %1").arg(output.errorString()));
|
return;
|
||||||
return;
|
}
|
||||||
}
|
const auto index = item->data(0, Qt::UserRole).toInt();
|
||||||
m_status->setText(tr("Cheat selection saved."));
|
if (index < 0 || index >= m_remote_files.size() || !m_remote_files[index].IsSupported()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_preview_status->setText(tr("Loading cheat preview…"));
|
||||||
|
m_repository->Fetch(m_remote_files[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PatchesDialog::ImportRemote() {
|
||||||
|
const auto error = Cheats::ValidateImport(m_remote_document, m_title_id, m_version, m_process);
|
||||||
|
if (!error.isEmpty()) {
|
||||||
|
m_preview_status->setText(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!m_local_readable) {
|
||||||
|
m_preview_status->setText(
|
||||||
|
tr("Could not read the local cheat file. Open Local and reload it before importing."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto path = Cheats::PlanPath(m_title_id);
|
||||||
|
if (QFile::exists(path) &&
|
||||||
|
QMessageBox::question(this, tr("Replace local cheats?"),
|
||||||
|
tr("Replace the local cheat file and selections for %1? Imported "
|
||||||
|
"cheats will start unchecked.")
|
||||||
|
.arg(m_title_id),
|
||||||
|
QMessageBox::Yes | QMessageBox::No,
|
||||||
|
QMessageBox::No) != QMessageBox::Yes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto mods = m_remote_document.object().value(QStringLiteral("mods")).toArray();
|
||||||
|
const auto document = Cheats::WithSelection(m_remote_document, QList<bool>(mods.size(), false));
|
||||||
|
const auto save_error = Cheats::SaveDocument(path, document, m_local_data);
|
||||||
|
if (!save_error.isEmpty()) {
|
||||||
|
m_preview_status->setText(save_error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Load();
|
||||||
|
m_status->setText(
|
||||||
|
tr("Imported %1 cheat(s). Select cheats and apply your selection for the next game launch.")
|
||||||
|
.arg(mods.size()));
|
||||||
|
m_tabs->setCurrentIndex(1);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user