mirror of
https://git.citron-emu.org/citron/emulator
synced 2026-01-31 06:43:35 +00:00
Merge pull request 'feat/fix: Introduce Nx-Optimizer by MaxLastBreath Tool & Fix Corrupted Shader Alloc' (#102) from feat/fix/nx-optimizer/bad_alloc into main
Reviewed-on: https://git.citron-emu.org/Citron/Emulator/pulls/102
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
|
||||
#include <QHeaderView>
|
||||
#include <QMenu>
|
||||
#include <QProcess>
|
||||
#include <QDir>
|
||||
#include <QStandardItemModel>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
@@ -210,13 +212,21 @@ void ConfigurePerGameAddons::LoadConfiguration() {
|
||||
}
|
||||
|
||||
auto* const mod_item = new QStandardItem(full_name);
|
||||
mod_item->setCheckable(true);
|
||||
|
||||
// If it's a Tool, remove the checkbox entirely
|
||||
if (patch.version == "Tool") {
|
||||
mod_item->setCheckable(false);
|
||||
// Explicitly strip the checkable flag to prevent the UI from drawing a box
|
||||
mod_item->setFlags(mod_item->flags() & ~Qt::ItemIsUserCheckable);
|
||||
mod_item->setForeground(QBrush(QColor(0, 120, 215))); // Keep it blue to show it's special
|
||||
} else {
|
||||
mod_item->setCheckable(true);
|
||||
const auto patch_disabled = std::find(disabled.begin(), disabled.end(), patch.name) != disabled.end();
|
||||
mod_item->setCheckState(patch_disabled ? Qt::Unchecked : Qt::Checked);
|
||||
}
|
||||
|
||||
mod_item->setData(QString::fromStdString(patch.name), Qt::UserRole);
|
||||
|
||||
const auto patch_disabled = std::find(disabled.begin(), disabled.end(), patch.name) != disabled.end();
|
||||
mod_item->setCheckState(patch_disabled ? Qt::Unchecked : Qt::Checked);
|
||||
|
||||
QList<QStandardItem*> row;
|
||||
row << mod_item << new QStandardItem{QString::fromStdString(patch.version)};
|
||||
|
||||
@@ -236,33 +246,57 @@ void ConfigurePerGameAddons::OnContextMenu(const QPoint& pos) {
|
||||
QModelIndex index = tree_view->indexAt(pos);
|
||||
if (!index.isValid()) return;
|
||||
|
||||
// Get the item that was clicked
|
||||
QStandardItem* item = item_model->itemFromIndex(index);
|
||||
|
||||
// We only want to show the menu if the item has children (it's a folder/group)
|
||||
if (item->rowCount() == 0) return;
|
||||
|
||||
QMenu context_menu;
|
||||
|
||||
// Create "Check All" action
|
||||
QAction* check_all = context_menu.addAction(tr("Check All Mods in Folder"));
|
||||
connect(check_all, &QAction::triggered, this, [item] {
|
||||
for (int i = 0; i < item->rowCount(); ++i) {
|
||||
if (auto* child = item->child(i, 0)) {
|
||||
child->setCheckState(Qt::Checked);
|
||||
if (item->rowCount() > 0) {
|
||||
// --- Folder Logic ---
|
||||
QAction* check_all = context_menu.addAction(tr("Check All Mods in Folder"));
|
||||
connect(check_all, &QAction::triggered, this, [item] {
|
||||
for (int i = 0; i < item->rowCount(); ++i) {
|
||||
if (auto* child = item->child(i, 0)) child->setCheckState(Qt::Checked);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create "Uncheck All" action
|
||||
QAction* uncheck_all = context_menu.addAction(tr("Uncheck All Mods in Folder"));
|
||||
connect(uncheck_all, &QAction::triggered, this, [item] {
|
||||
for (int i = 0; i < item->rowCount(); ++i) {
|
||||
if (auto* child = item->child(i, 0)) {
|
||||
child->setCheckState(Qt::Unchecked);
|
||||
});
|
||||
QAction* uncheck_all = context_menu.addAction(tr("Uncheck All Mods in Folder"));
|
||||
connect(uncheck_all, &QAction::triggered, this, [item] {
|
||||
for (int i = 0; i < item->rowCount(); ++i) {
|
||||
if (auto* child = item->child(i, 0)) child->setCheckState(Qt::Unchecked);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// --- Individual Item Logic ---
|
||||
QModelIndex v_idx = index.siblingAtColumn(1);
|
||||
if (item_model->data(v_idx).toString() == QStringLiteral("Tool")) {
|
||||
QAction* launch = context_menu.addAction(tr("Launch Tool"));
|
||||
QString file_name = item->text();
|
||||
|
||||
connect(launch, &QAction::triggered, this, [this, file_name] {
|
||||
// 1. Check Global Safe Zone (ConfigDir)
|
||||
std::filesystem::path tool_path =
|
||||
Common::FS::GetCitronPath(Common::FS::CitronPath::ConfigDir) / "tools" / file_name.toStdString();
|
||||
|
||||
// 2. Fallback to Legacy/Game-specific folder
|
||||
if (!std::filesystem::exists(tool_path)) {
|
||||
tool_path = Common::FS::GetCitronPath(Common::FS::CitronPath::LoadDir) /
|
||||
fmt::format("{:016X}", title_id) / "tools" / file_name.toStdString();
|
||||
}
|
||||
|
||||
if (std::filesystem::exists(tool_path)) {
|
||||
QString program = QString::fromStdString(tool_path.string());
|
||||
QString working_dir = QString::fromStdString(tool_path.parent_path().string());
|
||||
|
||||
LOG_INFO(Frontend, "Launching tool: {} with working directory: {}",
|
||||
program.toStdString(), working_dir.toStdString());
|
||||
|
||||
// Start the process detached with an explicit working directory.
|
||||
// This prevents the emulator from "cleaning up" the tool's temporary files.
|
||||
QProcess::startDetached(program, {}, working_dir);
|
||||
} else {
|
||||
QMessageBox::critical(this, tr("Launch Error"),
|
||||
tr("The tool executable could not be found. Please redownload it."));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
context_menu.exec(tree_view->viewport()->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QFile>
|
||||
#include <QDir>
|
||||
#include <QMessageBox>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QListWidgetItem>
|
||||
#include <QInputDialog>
|
||||
#include <QProcess>
|
||||
#include <filesystem>
|
||||
#include <set>
|
||||
|
||||
#include "citron/mod_manager/mod_downloader_dialog.h"
|
||||
|
||||
// Generated header from uic
|
||||
#include "ui_mod_downloader_dialog.h"
|
||||
|
||||
#include "common/fs/path_util.h"
|
||||
#include "common/logging/log.h"
|
||||
|
||||
@@ -23,45 +22,31 @@ namespace ModManager {
|
||||
|
||||
ModDownloaderDialog::ModDownloaderDialog(const ModUpdateInfo& info, QWidget* parent)
|
||||
: QDialog(parent), mod_info(info), current_reply(nullptr) {
|
||||
|
||||
ui = new ::Ui::ModDownloaderDialog();
|
||||
ui->setupUi(this);
|
||||
|
||||
network_manager = new QNetworkAccessManager(this);
|
||||
|
||||
SetupModList();
|
||||
|
||||
connect(ui->buttonDownload, &QPushButton::clicked, this, &ModDownloaderDialog::OnDownloadClicked);
|
||||
connect(ui->buttonCancel, &QPushButton::clicked, this, &ModDownloaderDialog::OnCancelClicked);
|
||||
}
|
||||
|
||||
ModDownloaderDialog::~ModDownloaderDialog() {
|
||||
delete ui;
|
||||
}
|
||||
ModDownloaderDialog::~ModDownloaderDialog() { delete ui; }
|
||||
|
||||
void ModDownloaderDialog::SetupModList() {
|
||||
ui->treeWidget->setHeaderLabel(QStringLiteral("Version / Mod Name"));
|
||||
|
||||
for (auto const& [version, patches] : mod_info.version_patches) {
|
||||
QTreeWidgetItem* version_item = new QTreeWidgetItem(ui->treeWidget);
|
||||
version_item->setText(0, QStringLiteral("Update %1").arg(version));
|
||||
version_item->setCheckState(0, Qt::Unchecked);
|
||||
version_item->setFlags(version_item->flags() | Qt::ItemIsUserCheckable | Qt::ItemIsAutoTristate);
|
||||
|
||||
// Track names we've already added to this version to avoid duplicates in the UI
|
||||
std::set<QString> seen_names;
|
||||
|
||||
std::set<QString> seen;
|
||||
for (const auto& patch : patches) {
|
||||
if (seen_names.find(patch.name) != seen_names.end()) {
|
||||
continue; // Skip if we already listed this mod name
|
||||
}
|
||||
|
||||
if (seen.count(patch.name)) continue;
|
||||
QTreeWidgetItem* mod_item = new QTreeWidgetItem(version_item);
|
||||
mod_item->setText(0, patch.name);
|
||||
mod_item->setCheckState(0, Qt::Unchecked);
|
||||
mod_item->setFlags(mod_item->flags() | Qt::ItemIsUserCheckable);
|
||||
|
||||
seen_names.insert(patch.name);
|
||||
seen.insert(patch.name);
|
||||
}
|
||||
}
|
||||
ui->treeWidget->expandAll();
|
||||
@@ -69,34 +54,46 @@ void ModDownloaderDialog::SetupModList() {
|
||||
|
||||
void ModDownloaderDialog::OnDownloadClicked() {
|
||||
pending_downloads.clear();
|
||||
QString os_target;
|
||||
#ifdef _WIN32
|
||||
os_target = QStringLiteral("exe");
|
||||
#elif __APPLE__
|
||||
os_target = QStringLiteral("zip");
|
||||
#else
|
||||
os_target = QStringLiteral("AppImage");
|
||||
#endif
|
||||
|
||||
for (int i = 0; i < ui->treeWidget->topLevelItemCount(); ++i) {
|
||||
QTreeWidgetItem* version_node = ui->treeWidget->topLevelItem(i);
|
||||
QString version_str = version_node->text(0).replace(QStringLiteral("Update "), QStringLiteral(""));
|
||||
|
||||
QString v_str = version_node->text(0).replace(QStringLiteral("Update "), QStringLiteral(""));
|
||||
for (int j = 0; j < version_node->childCount(); ++j) {
|
||||
QTreeWidgetItem* mod_node = version_node->child(j);
|
||||
if (mod_node->checkState(0) == Qt::Checked) {
|
||||
QString mod_name = mod_node->text(0);
|
||||
const auto& patches = mod_info.version_patches[version_str];
|
||||
|
||||
// Find EVERY patch entry that matches this name
|
||||
// (e.g., grabs both the 'exefs' and 'cheats' entries for "30FPS")
|
||||
for (const auto& p : patches) {
|
||||
if (p.name == mod_name) {
|
||||
pending_downloads.push_back({p, version_str});
|
||||
if (mod_node->checkState(0) != Qt::Checked) continue;
|
||||
const auto& patches = mod_info.version_patches[v_str];
|
||||
for (auto p : patches) {
|
||||
if (p.name == mod_node->text(0)) {
|
||||
if (p.type == QStringLiteral("tool")) {
|
||||
QStringList filtered;
|
||||
for (const QString& f : p.files) {
|
||||
if (f.endsWith(os_target, Qt::CaseInsensitive)) filtered << f;
|
||||
}
|
||||
if (filtered.size() > 1) {
|
||||
bool ok;
|
||||
QString choice = QInputDialog::getItem(this, QStringLiteral("Select Architecture"),
|
||||
QStringLiteral("Choose your system type:"), filtered, 0, false, &ok);
|
||||
if (ok && !choice.isEmpty()) p.files = {choice};
|
||||
else continue;
|
||||
} else { p.files = filtered; }
|
||||
}
|
||||
pending_downloads.push_back({p, v_str});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pending_downloads.empty()) return;
|
||||
|
||||
ui->buttonDownload->setEnabled(false);
|
||||
ui->treeWidget->setEnabled(false);
|
||||
ui->progressBar->setVisible(true);
|
||||
|
||||
current_download_index = 0;
|
||||
current_file_index = 0;
|
||||
StartNextDownload();
|
||||
@@ -104,51 +101,53 @@ void ModDownloaderDialog::OnDownloadClicked() {
|
||||
|
||||
void ModDownloaderDialog::StartNextDownload() {
|
||||
if (current_download_index >= static_cast<int>(pending_downloads.size())) {
|
||||
QMessageBox::information(this, QStringLiteral("Success"), QStringLiteral("All selected mods have been installed."));
|
||||
QMessageBox::information(this, QStringLiteral("Success"), QStringLiteral("All items installed."));
|
||||
accept();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& task = pending_downloads[current_download_index];
|
||||
const ModPatch& patch = task.patch;
|
||||
QString version_str = task.version; // We use this to create the folder structure
|
||||
|
||||
if (current_file_index >= patch.files.size()) {
|
||||
if (current_file_index >= task.patch.files.size()) {
|
||||
current_download_index++;
|
||||
current_file_index = 0;
|
||||
StartNextDownload();
|
||||
return;
|
||||
}
|
||||
QString file_val = task.patch.files[current_file_index];
|
||||
QUrl url = (task.patch.type == QStringLiteral("tool")) ? QUrl(file_val) :
|
||||
QUrl(QStringLiteral("https://raw.githubusercontent.com/CollectingW/Citron-Mods/main/%1/%2")
|
||||
.arg(task.patch.rel_path).arg(file_val));
|
||||
|
||||
QString fileName = patch.files[current_file_index];
|
||||
QString urlBase = QStringLiteral("https://raw.githubusercontent.com/CollectingW/Citron-Mods/main/%1/%2");
|
||||
QString finalUrl = urlBase.arg(patch.rel_path).arg(fileName);
|
||||
QUrl url(finalUrl);
|
||||
QString fileName = file_val.contains(u'/') ? file_val.split(u'/').last() : file_val;
|
||||
current_reply = network_manager->get(QNetworkRequest(url));
|
||||
|
||||
LOG_INFO(Frontend, "Downloading: {}", url.toString().toStdString());
|
||||
|
||||
QNetworkRequest request(url);
|
||||
current_reply = network_manager->get(request);
|
||||
|
||||
connect(current_reply, &QNetworkReply::finished, this, [this, patch, version_str, fileName]() {
|
||||
connect(current_reply, &QNetworkReply::finished, this, [this, task, fileName]() {
|
||||
if (current_reply->error() == QNetworkReply::NoError) {
|
||||
std::filesystem::path load_dir = Common::FS::GetCitronPath(Common::FS::CitronPath::LoadDir);
|
||||
std::filesystem::path tid_path = load_dir / mod_info.title_id.toStdString();
|
||||
std::filesystem::path base = Common::FS::GetCitronPath(Common::FS::CitronPath::LoadDir);
|
||||
std::filesystem::path final_path = base / mod_info.title_id.toStdString();
|
||||
|
||||
// This creates the hierarchy: load / [TID] / [Version] / [ModName] / [exefs/romfs]
|
||||
std::filesystem::path final_path = tid_path / version_str.toStdString() /
|
||||
patch.name.toStdString() / patch.type.toStdString();
|
||||
if (task.patch.type == QStringLiteral("tool")) {
|
||||
final_path = Common::FS::GetCitronPath(Common::FS::CitronPath::ConfigDir) / "tools";
|
||||
} else {
|
||||
final_path /= task.version.toStdString();
|
||||
final_path /= task.patch.name.toStdString();
|
||||
final_path /= task.patch.type.toStdString();
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(final_path, ec);
|
||||
|
||||
QFile file(QString::fromStdString((final_path / fileName.toStdString()).string()));
|
||||
std::filesystem::create_directories(final_path);
|
||||
QString full_save_path = QString::fromStdString((final_path / fileName.toStdString()).string());
|
||||
QFile file(full_save_path);
|
||||
if (file.open(QIODevice::WriteOnly)) {
|
||||
file.write(current_reply->readAll());
|
||||
file.close();
|
||||
if (fileName.endsWith(QStringLiteral(".zip"))) {
|
||||
QProcess::execute(QStringLiteral("unzip"), {full_save_path, QStringLiteral("-d"), QString::fromStdString(final_path.string())});
|
||||
}
|
||||
#ifndef _WIN32
|
||||
std::filesystem::permissions(final_path / fileName.toStdString(),
|
||||
std::filesystem::perms::owner_exec | std::filesystem::perms::group_exec, std::filesystem::perm_options::add);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
current_reply->deleteLater();
|
||||
current_file_index++;
|
||||
ui->progressBar->setValue(((current_download_index + 1) * 100) / pending_downloads.size());
|
||||
@@ -156,9 +155,6 @@ void ModDownloaderDialog::StartNextDownload() {
|
||||
});
|
||||
}
|
||||
|
||||
void ModDownloaderDialog::OnCancelClicked() {
|
||||
if (current_reply) current_reply->abort();
|
||||
reject();
|
||||
}
|
||||
void ModDownloaderDialog::OnCancelClicked() { if (current_reply) current_reply->abort(); reject(); }
|
||||
|
||||
} // namespace ModManager
|
||||
|
||||
@@ -17,10 +17,17 @@ ModService::ModService(QObject* parent) : QObject(parent) {
|
||||
ModService::~ModService() = default;
|
||||
|
||||
void ModService::FetchAvailableMods(const QString& title_id) {
|
||||
QNetworkRequest request{QUrl(MANIFEST_URL)};
|
||||
const QStringList optimizer_supported = {
|
||||
QStringLiteral("01006BB00C6F0000"), QStringLiteral("0100F2C0115B6000"),
|
||||
QStringLiteral("01002B00111A2000"), QStringLiteral("01007EF00011E000"),
|
||||
QStringLiteral("0100F43008C44000"), QStringLiteral("0100A3D008C5C000"),
|
||||
QStringLiteral("01008F6008C5E000")
|
||||
};
|
||||
|
||||
QNetworkRequest request((QUrl(MANIFEST_URL)));
|
||||
QNetworkReply* reply = network_manager->get(request);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, title_id]() {
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, title_id, optimizer_supported]() {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
emit Error(reply->errorString());
|
||||
reply->deleteLater();
|
||||
@@ -29,47 +36,59 @@ void ModService::FetchAvailableMods(const QString& title_id) {
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
|
||||
QJsonObject root = doc.object();
|
||||
|
||||
// Convert title_id to uppercase to match your folder/manifest structure
|
||||
QString tid_upper = title_id.toUpper();
|
||||
if (!root.contains(tid_upper)) {
|
||||
emit Error(QStringLiteral("No mods found for this game in the repository."));
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
ModUpdateInfo info;
|
||||
info.title_id = title_id;
|
||||
|
||||
QJsonObject tid_obj = root.value(tid_upper).toObject();
|
||||
QJsonObject versions_obj = tid_obj.value(QStringLiteral("versions")).toObject();
|
||||
|
||||
// Loop through every version found for this game (e.g., 1.0.0, 2.0.0)
|
||||
for (auto it = versions_obj.begin(); it != versions_obj.end(); ++it) {
|
||||
QString v_name = it.key();
|
||||
QJsonObject v_data = it.value().toObject();
|
||||
QJsonArray patches_array = v_data.value(QStringLiteral("patches")).toArray();
|
||||
|
||||
std::vector<ModPatch> patches;
|
||||
for (const QJsonValue& val : patches_array) {
|
||||
QJsonObject p_obj = val.toObject();
|
||||
ModPatch patch;
|
||||
patch.name = p_obj.value(QStringLiteral("name")).toString();
|
||||
patch.type = p_obj.value(QStringLiteral("type")).toString();
|
||||
patch.rel_path = p_obj.value(QStringLiteral("rel_path")).toString();
|
||||
|
||||
QJsonArray files_arr = p_obj.value(QStringLiteral("files")).toArray();
|
||||
for (const QJsonValue& f : files_arr) {
|
||||
patch.files << f.toString();
|
||||
// 1. Process standard mods from manifest
|
||||
if (root.contains(tid_upper)) {
|
||||
QJsonObject tid_obj = root.value(tid_upper).toObject();
|
||||
QJsonObject versions_obj = tid_obj.value(QStringLiteral("versions")).toObject();
|
||||
for (auto it = versions_obj.begin(); it != versions_obj.end(); ++it) {
|
||||
QString v_name = it.key();
|
||||
QJsonObject v_data = it.value().toObject();
|
||||
QJsonArray patches_array = v_data.value(QStringLiteral("patches")).toArray();
|
||||
std::vector<ModPatch> patches;
|
||||
for (const QJsonValue& val : patches_array) {
|
||||
QJsonObject p_obj = val.toObject();
|
||||
ModPatch patch;
|
||||
patch.name = p_obj.value(QStringLiteral("name")).toString();
|
||||
patch.type = p_obj.value(QStringLiteral("type")).toString();
|
||||
patch.rel_path = p_obj.value(QStringLiteral("rel_path")).toString();
|
||||
QJsonArray files_arr = p_obj.value(QStringLiteral("files")).toArray();
|
||||
for (const QJsonValue& f : files_arr) patch.files << f.toString();
|
||||
patches.push_back(patch);
|
||||
}
|
||||
|
||||
patches.push_back(patch);
|
||||
info.version_patches[v_name] = patches;
|
||||
}
|
||||
// Add this version and its mods to the map
|
||||
info.version_patches[v_name] = patches;
|
||||
}
|
||||
|
||||
emit ModsAvailable(info);
|
||||
// 2. Also Fetch NX-Optimizer if supported
|
||||
if (optimizer_supported.contains(tid_upper)) {
|
||||
QNetworkRequest github_req(QUrl(QStringLiteral("https://api.github.com/repos/MaxLastBreath/nx-optimizer/releases/latest")));
|
||||
github_req.setRawHeader("Accept", "application/vnd.github.v3+json");
|
||||
github_req.setRawHeader("User-Agent", "Citron-Emulator");
|
||||
QNetworkReply* github_reply = network_manager->get(github_req);
|
||||
|
||||
connect(github_reply, &QNetworkReply::finished, this, [this, github_reply, info]() mutable {
|
||||
if (github_reply->error() == QNetworkReply::NoError) {
|
||||
QJsonObject release = QJsonDocument::fromJson(github_reply->readAll()).object();
|
||||
QJsonArray assets = release.value(QStringLiteral("assets")).toArray();
|
||||
ModPatch tool;
|
||||
tool.name = QStringLiteral("NX-Optimizer by MaxLastBreath");
|
||||
tool.type = QStringLiteral("tool");
|
||||
for (const QJsonValue& asset : assets) {
|
||||
tool.files << asset.toObject().value(QStringLiteral("browser_download_url")).toString();
|
||||
}
|
||||
info.version_patches[QStringLiteral("Global Tools")].push_back(tool);
|
||||
}
|
||||
emit ModsAvailable(info);
|
||||
github_reply->deleteLater();
|
||||
});
|
||||
} else {
|
||||
emit ModsAvailable(info);
|
||||
}
|
||||
reply->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
|
||||
#include <filesystem>
|
||||
#include "common/fs/path_util.h"
|
||||
#include "common/hex_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/settings.h"
|
||||
@@ -713,10 +715,60 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
|
||||
list += fmt::format("{}", dlc_match.back().title_id & 0x7FF);
|
||||
|
||||
const auto dlc_disabled = std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end();
|
||||
// FIX: Saved under base title_id so logic finds it
|
||||
out.push_back({.enabled = !dlc_disabled, .name = "DLC", .version = std::move(list), .type = PatchType::DLC, .program_id = title_id, .title_id = title_id});
|
||||
}
|
||||
|
||||
// Scan for Game-Specific Tools
|
||||
const auto load_root = fs_controller.GetModificationLoadRoot(title_id);
|
||||
if (load_root) {
|
||||
if (auto tools_dir = load_root->GetSubdirectory("tools")) {
|
||||
for (const auto& file : tools_dir->GetFiles()) {
|
||||
FileSys::Patch tool_patch;
|
||||
tool_patch.enabled = true;
|
||||
tool_patch.name = file->GetName();
|
||||
tool_patch.version = "Tool";
|
||||
tool_patch.type = PatchType::Mod;
|
||||
tool_patch.program_id = title_id;
|
||||
tool_patch.title_id = title_id;
|
||||
out.push_back(std::move(tool_patch));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scan for Global Tools (NX-Optimizer)
|
||||
const std::vector<u64> optimizer_supported_ids = {
|
||||
0x01006BB00C6F0000, 0x0100F2C0115B6000, 0x01002B00111A2000,
|
||||
0x01007EF00011E000, 0x0100F43008C44000, 0x0100A3D008C5C000, 0x01008F6008C5E000
|
||||
};
|
||||
|
||||
if (std::find(optimizer_supported_ids.begin(), optimizer_supported_ids.end(), title_id) != optimizer_supported_ids.end()) {
|
||||
auto global_tools_path = Common::FS::GetCitronPath(Common::FS::CitronPath::ConfigDir) / "tools";
|
||||
|
||||
if (std::filesystem::exists(global_tools_path)) {
|
||||
for (const auto& entry : std::filesystem::directory_iterator(global_tools_path)) {
|
||||
if (entry.is_regular_file()) {
|
||||
const auto extension = entry.path().extension().string();
|
||||
|
||||
// Only allow actual executables to show up in the UI
|
||||
bool is_tool = (extension == ".AppImage" || extension == ".exe");
|
||||
|
||||
if (!is_tool) {
|
||||
continue;
|
||||
}
|
||||
|
||||
FileSys::Patch global_tool;
|
||||
global_tool.enabled = true;
|
||||
global_tool.name = entry.path().filename().string();
|
||||
global_tool.version = "Tool";
|
||||
global_tool.type = PatchType::Mod;
|
||||
global_tool.program_id = title_id;
|
||||
global_tool.title_id = title_id;
|
||||
out.push_back(std::move(global_tool));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -761,7 +813,7 @@ PatchManager::Metadata PatchManager::GetControlMetadata() const {
|
||||
}
|
||||
}
|
||||
|
||||
// FIX: Only fetch the Update metadata if the user hasn't disabled it
|
||||
// Only fetch the Update metadata if the user hasn't disabled it
|
||||
const auto update_disabled = std::find(disabled_for_game.begin(), disabled_for_game.end(), "Update") != disabled_for_game.end();
|
||||
const auto update_tid = GetUpdateTitleID(title_id);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
@@ -692,10 +693,17 @@ void LoadPipelines(
|
||||
}
|
||||
u32 num_envs{};
|
||||
file.read(reinterpret_cast<char*>(&num_envs), sizeof(num_envs));
|
||||
|
||||
if (num_envs == 0 || num_envs > 64) {
|
||||
LOG_ERROR(Common_Filesystem, "Corrupted shader cache detected: num_envs={}", num_envs);
|
||||
throw std::ios_base::failure("Corrupted num_envs");
|
||||
}
|
||||
|
||||
std::vector<FileEnvironment> envs(num_envs);
|
||||
for (FileEnvironment& env : envs) {
|
||||
env.Deserialize(file);
|
||||
}
|
||||
|
||||
if (envs.front().ShaderStage() == Shader::Stage::Compute) {
|
||||
load_compute(file, std::move(envs.front()));
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user