From e07e269bd766aa1fc29951b958db2063c1a4048e Mon Sep 17 00:00:00 2001 From: crueter Date: Fri, 6 Feb 2026 06:37:30 +0100 Subject: [PATCH] [desktop] Add mod importer from folder and zip (#3472) Closes #3125 Adds buttons to the addons page that imports a mod (or mods) from zip or folder. Currently known to work with mods that provide proper romfs/exefs things, unsure about cheats and such. Also works on mods that just stuff things into the root of the zip. TODO: - [ ] test folder more thoroughly - [ ] cheats - [ ] test all sorts of mod pack types Signed-off-by: crueter Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3472 Reviewed-by: Lizzie --- src/frontend_common/CMakeLists.txt | 3 +- src/frontend_common/mod_manager.cpp | 62 ++++++++ src/frontend_common/mod_manager.h | 21 +++ src/qt_common/CMakeLists.txt | 6 +- src/qt_common/abstract/frontend.cpp | 27 +++- src/qt_common/abstract/frontend.h | 8 +- src/qt_common/util/mod.cpp | 137 ++++++++++++++++++ src/qt_common/util/mod.h | 16 ++ src/yuzu/CMakeLists.txt | 1 + .../configuration/addon/mod_select_dialog.cpp | 66 +++++++++ .../configuration/addon/mod_select_dialog.h | 26 ++++ .../configuration/addon/mod_select_dialog.ui | 99 +++++++++++++ src/yuzu/configuration/configure_per_game.cpp | 2 +- src/yuzu/configuration/configure_per_game.h | 2 +- .../configure_per_game_addons.cpp | 75 +++++++++- .../configuration/configure_per_game_addons.h | 11 ++ .../configure_per_game_addons.ui | 18 ++- src/yuzu/main_window.cpp | 4 +- 18 files changed, 570 insertions(+), 14 deletions(-) create mode 100644 src/frontend_common/mod_manager.cpp create mode 100644 src/frontend_common/mod_manager.h create mode 100644 src/qt_common/util/mod.cpp create mode 100644 src/qt_common/util/mod.h create mode 100644 src/yuzu/configuration/addon/mod_select_dialog.cpp create mode 100644 src/yuzu/configuration/addon/mod_select_dialog.h create mode 100644 src/yuzu/configuration/addon/mod_select_dialog.ui diff --git a/src/frontend_common/CMakeLists.txt b/src/frontend_common/CMakeLists.txt index 2f75fede57..5c248ea337 100644 --- a/src/frontend_common/CMakeLists.txt +++ b/src/frontend_common/CMakeLists.txt @@ -13,7 +13,8 @@ add_library(frontend_common STATIC data_manager.h data_manager.cpp play_time_manager.cpp play_time_manager.h - settings_generator.h settings_generator.cpp) + settings_generator.h settings_generator.cpp + mod_manager.h mod_manager.cpp) if (ENABLE_UPDATE_CHECKER) target_link_libraries(frontend_common PRIVATE httplib::httplib) diff --git a/src/frontend_common/mod_manager.cpp b/src/frontend_common/mod_manager.cpp new file mode 100644 index 0000000000..6ac79d22e2 --- /dev/null +++ b/src/frontend_common/mod_manager.cpp @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include "common/fs/fs.h" +#include "common/fs/fs_types.h" +#include "common/logging/backend.h" +#include "frontend_common/data_manager.h" +#include "mod_manager.h" + +namespace FrontendCommon { + +// TODO: Handle cases where the folder appears to contain multiple mods. +std::vector GetModFolder(const std::string& root) { + std::vector paths; + + auto callback = [&paths](const std::filesystem::directory_entry& entry) -> bool { + const auto name = entry.path().filename().string(); + static constexpr const std::array valid_names = {"exefs", + "romfs" + "romfs_ext", + "cheats", "romfslite"}; + + if (std::ranges::find(valid_names, name) != valid_names.end()) { + paths.emplace_back(entry.path().parent_path()); + } + + return true; + }; + + Common::FS::IterateDirEntriesRecursively(root, callback, Common::FS::DirEntryFilter::Directory); + + return paths; +} + +ModInstallResult InstallMod(const std::filesystem::path& path, const u64 program_id, const bool copy) { + const auto program_id_string = fmt::format("{:016X}", program_id); + const auto mod_name = path.filename(); + const auto mod_dir = + DataManager::GetDataDir(DataManager::DataDir::Mods) / program_id_string / mod_name; + + // pre-emptively remove any existing mod here + std::filesystem::remove_all(mod_dir); + + // now copy + try { + std::filesystem::copy(path, mod_dir, std::filesystem::copy_options::recursive); + if (!copy) + std::filesystem::remove_all(path); + } catch (std::exception& e) { + LOG_ERROR(Frontend, "Mod install failed with message {}", e.what()); + return Failed; + } + + LOG_INFO(Frontend, "Copied mod from {} to {}", path.string(), mod_dir.string()); + + return Success; +} + +} // namespace FrontendCommon diff --git a/src/frontend_common/mod_manager.h b/src/frontend_common/mod_manager.h new file mode 100644 index 0000000000..57fdbc3533 --- /dev/null +++ b/src/frontend_common/mod_manager.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include "common/common_types.h" + +namespace FrontendCommon { + +enum ModInstallResult { + Cancelled, + Failed, + Success, +}; + +std::vector GetModFolder(const std::string& root); + +ModInstallResult InstallMod(const std::filesystem::path &path, const u64 program_id, const bool copy = true); +} diff --git a/src/qt_common/CMakeLists.txt b/src/qt_common/CMakeLists.txt index 073301b313..65f5b1ee2b 100644 --- a/src/qt_common/CMakeLists.txt +++ b/src/qt_common/CMakeLists.txt @@ -22,6 +22,8 @@ add_library(qt_common STATIC util/rom.h util/rom.cpp util/applet.h util/applet.cpp util/compress.h util/compress.cpp + util/fs.h util/fs.cpp + util/mod.h util/mod.cpp abstract/frontend.h abstract/frontend.cpp abstract/qt_progress_dialog.h abstract/qt_progress_dialog.cpp @@ -29,9 +31,7 @@ add_library(qt_common STATIC qt_string_lookup.h qt_compat.h - discord/discord.h - util/fs.h util/fs.cpp -) + discord/discord.h) if (UNIX) target_sources(qt_common PRIVATE gui_settings.cpp gui_settings.h) diff --git a/src/qt_common/abstract/frontend.cpp b/src/qt_common/abstract/frontend.cpp index a0ce943538..58f1fe6aa8 100644 --- a/src/qt_common/abstract/frontend.cpp +++ b/src/qt_common/abstract/frontend.cpp @@ -1,6 +1,7 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later +#include #include "frontend.h" #include "qt_common/qt_common.h" @@ -8,6 +9,9 @@ #include #endif +#include +#include + namespace QtCommon::Frontend { StandardButton ShowMessage( @@ -50,4 +54,25 @@ const QString GetExistingDirectory(const QString& caption, const QString& dir, #endif } +int Choice(const QString& title, const QString& caption, const QStringList& options) { + QMessageBox box(rootObject); + box.setText(caption); + box.setWindowTitle(title); + + for (const QString &opt : options) { + box.addButton(opt, QMessageBox::AcceptRole); + } + + box.addButton(QMessageBox::Cancel); + + box.exec(); + auto button = box.clickedButton(); + return options.indexOf(button->text()); +} + +const QString GetTextInput(const QString& title, const QString& caption, + const QString& defaultText) { + return QInputDialog::getText(rootObject, title, caption, QLineEdit::Normal, defaultText); +} + } // namespace QtCommon::Frontend diff --git a/src/qt_common/abstract/frontend.h b/src/qt_common/abstract/frontend.h index 0ef9ea97e4..d61eebfeb1 100644 --- a/src/qt_common/abstract/frontend.h +++ b/src/qt_common/abstract/frontend.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later #ifndef FRONTEND_H @@ -139,5 +139,11 @@ const QString GetExistingDirectory(const QString &caption = QString(), const QString &dir = QString(), Options options = Option::ShowDirsOnly); +int Choice(const QString& title = QString(), const QString& caption = QString(), + const QStringList& options = {}); + +const QString GetTextInput(const QString& title = QString(), const QString& caption = QString(), + const QString& defaultText = QString()); + } // namespace QtCommon::Frontend #endif // FRONTEND_H diff --git a/src/qt_common/util/mod.cpp b/src/qt_common/util/mod.cpp new file mode 100644 index 0000000000..1168bde2f6 --- /dev/null +++ b/src/qt_common/util/mod.cpp @@ -0,0 +1,137 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include "frontend_common/mod_manager.h" +#include "mod.h" +#include "qt_common/abstract/frontend.h" + +namespace QtCommon::Mod { +QStringList GetModFolders(const QString& root, const QString& fallbackName) { + namespace fs = std::filesystem; + + const auto std_root = root.toStdString(); + + auto paths = FrontendCommon::GetModFolder(std_root); + + // multi mod zip + if (paths.size() > 1) { + // We just have to assume it's properly formed here. + // If not, you're out of luck. + QStringList qpaths; + for (const fs::path& path : paths) { + qpaths << QString::fromStdString(path.string()); + } + + return qpaths; + } + // either frontend didn't detect any romfs/exefs, or is a single-mod zip + else { + fs::path std_path; + if (!paths.empty()) + std_path = paths[0]; + + QString default_name; + if (!fallbackName.isEmpty()) + default_name = fallbackName; + else if (!paths.empty()) + default_name = QString::fromStdString(std_path.filename().string()); + else + default_name = root.split(QLatin1Char('/')).last(); + + QString name = QtCommon::Frontend::GetTextInput( + tr("Mod Name"), tr("What should this mod be called?"), default_name); + + // if std_path is empty, frontend_common could not determine mod type and/or name. + // so we have to prompt the user and set up the structure ourselves + if (paths.empty()) { + // TODO: Carboxyl impl. + const QStringList choices = { + tr("RomFS"), + tr("ExeFS/Patch"), + tr("Cheat"), + }; + + int choice = QtCommon::Frontend::Choice( + tr("Mod Type"), + tr("Could not detect mod type automatically. Please manually " + "specify the type of mod you downloaded.\n\nMost mods are RomFS mods, but " + "patches " + "(.pchtxt) are typically ExeFS mods."), + choices); + + std::string to_make; + + switch (choice) { + case 0: + to_make = "romfs"; + break; + case 1: + to_make = "exefs"; + break; + case 2: + to_make = "cheats"; + break; + default: + return {}; + } + + // now make a temp directory... + const auto mod_dir = fs::temp_directory_path() / "eden" / "mod" / name.toStdString(); + const auto tmp = mod_dir / to_make; + fs::remove_all(mod_dir); + if (!fs::create_directories(tmp)) { + LOG_ERROR(Frontend, "Failed to create temporary directory {}", tmp.string()); + return {}; + } + + std_path = mod_dir; + + // ... and copy everything from the root to the temp dir + for (const auto& entry : fs::directory_iterator(root.toStdString())) { + const auto target = tmp / entry.path().filename(); + + fs::copy(entry.path(), target, + fs::copy_options::recursive | fs::copy_options::overwrite_existing); + } + } else { + // Rename the existing mod folder. + const auto new_path = std_path.parent_path() / name.toStdString(); + fs::rename(std_path, new_path); + std_path = new_path; + } + + return {QString::fromStdString(std_path.string())}; + } +} + +// TODO(crueter): Make this a common extract_to_tmp func +const QString ExtractMod(const QString& path) { + namespace fs = std::filesystem; + fs::path tmp{fs::temp_directory_path() / "eden" / "unzip_mod"}; + + fs::remove_all(tmp); + if (!fs::create_directories(tmp)) { + QtCommon::Frontend::Critical(tr("Mod Extract Failed"), + tr("Failed to create temporary directory %1") + .arg(QString::fromStdString(tmp.string()))); + return QString(); + } + + QString qCacheDir = QString::fromStdString(tmp.string()); + + QFile zip{path}; + + // TODO(crueter): use QtCompress + QStringList result = JlCompress::extractDir(&zip, qCacheDir); + if (result.isEmpty()) { + QtCommon::Frontend::Critical(tr("Mod Extract Failed"), + tr("Zip file %1 is empty").arg(path)); + return QString(); + } + + return qCacheDir; +} + +} // namespace QtCommon::Mod diff --git a/src/qt_common/util/mod.h b/src/qt_common/util/mod.h new file mode 100644 index 0000000000..ceda81ef92 --- /dev/null +++ b/src/qt_common/util/mod.h @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include "common/common_types.h" +#include "frontend_common/mod_manager.h" + +namespace QtCommon::Mod { + +QStringList GetModFolders(const QString &root, const QString &fallbackName); + +const QString ExtractMod(const QString &path); + +} diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 0d49e4a5e7..ba2b5b3927 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -238,6 +238,7 @@ add_executable(yuzu configuration/system/new_user_dialog.h configuration/system/new_user_dialog.cpp configuration/system/new_user_dialog.ui configuration/system/profile_avatar_dialog.h configuration/system/profile_avatar_dialog.cpp + configuration/addon/mod_select_dialog.h configuration/addon/mod_select_dialog.cpp configuration/addon/mod_select_dialog.ui ) set_target_properties(yuzu PROPERTIES OUTPUT_NAME "eden") diff --git a/src/yuzu/configuration/addon/mod_select_dialog.cpp b/src/yuzu/configuration/addon/mod_select_dialog.cpp new file mode 100644 index 0000000000..e6c361b94f --- /dev/null +++ b/src/yuzu/configuration/addon/mod_select_dialog.cpp @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include "mod_select_dialog.h" +#include "ui_mod_select_dialog.h" + +ModSelectDialog::ModSelectDialog(const QStringList& mods, QWidget* parent) + : QDialog(parent), ui(new Ui::ModSelectDialog) { + ui->setupUi(this); + + item_model = new QStandardItemModel(ui->treeView); + ui->treeView->setModel(item_model); + + // We must register all custom types with the Qt Automoc system so that we are able to use it + // with signals/slots. In this case, QList falls under the umbrella of custom types. + qRegisterMetaType>("QList"); + + for (const auto& mod : mods) { + const auto basename = QFileInfo(mod).fileName(); + + auto* const first_item = new QStandardItem; + first_item->setText(basename); + first_item->setData(mod); + + first_item->setCheckable(true); + first_item->setCheckState(Qt::Checked); + + item_model->appendRow(first_item); + } + + ui->treeView->expandAll(); + ui->treeView->resizeColumnToContents(0); + + int rows = item_model->rowCount(); + int height = + ui->treeView->contentsMargins().top() * 4 + ui->treeView->contentsMargins().bottom() * 4; + int width = 0; + + for (int i = 0; i < rows; ++i) { + height += ui->treeView->sizeHintForRow(i); + width = qMax(width, item_model->item(i)->sizeHint().width()); + } + + width += ui->treeView->contentsMargins().left() * 4 + ui->treeView->contentsMargins().right() * 4; + ui->treeView->setMinimumHeight(qMin(height, 600)); + ui->treeView->setMinimumWidth(qMin(width, 700)); + adjustSize(); + + connect(this, &QDialog::accepted, this, [this]() { + QStringList selected_mods; + + for (qsizetype i = 0; i < item_model->rowCount(); ++i) { + auto* const item = item_model->item(i); + if (item->checkState() == Qt::Checked) + selected_mods << item->data().toString(); + } + + emit modsSelected(selected_mods); + }); +} + +ModSelectDialog::~ModSelectDialog() { + delete ui; +} diff --git a/src/yuzu/configuration/addon/mod_select_dialog.h b/src/yuzu/configuration/addon/mod_select_dialog.h new file mode 100644 index 0000000000..d23c435e7a --- /dev/null +++ b/src/yuzu/configuration/addon/mod_select_dialog.h @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +namespace Ui { +class ModSelectDialog; +} + +class ModSelectDialog : public QDialog { + Q_OBJECT + +public: + explicit ModSelectDialog(const QStringList &mods, QWidget* parent = nullptr); + ~ModSelectDialog(); + +signals: + void modsSelected(const QStringList &mods); +private: + Ui::ModSelectDialog* ui; + + QStandardItemModel* item_model; +}; diff --git a/src/yuzu/configuration/addon/mod_select_dialog.ui b/src/yuzu/configuration/addon/mod_select_dialog.ui new file mode 100644 index 0000000000..6e196ecff0 --- /dev/null +++ b/src/yuzu/configuration/addon/mod_select_dialog.ui @@ -0,0 +1,99 @@ + + + ModSelectDialog + + + + 0 + 0 + 400 + 430 + + + + Dialog + + + + + + The specified folder or archive contains the following mods. Select which ones to install. + + + true + + + + + + + Qt::ContextMenuPolicy::NoContextMenu + + + QAbstractItemView::EditTrigger::NoEditTriggers + + + true + + + QAbstractItemView::ScrollMode::ScrollPerPixel + + + true + + + true + + + true + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + + + buttonBox + accepted() + ModSelectDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + ModSelectDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/yuzu/configuration/configure_per_game.cpp b/src/yuzu/configuration/configure_per_game.cpp index e49978414c..eddb24b952 100644 --- a/src/yuzu/configuration/configure_per_game.cpp +++ b/src/yuzu/configuration/configure_per_game.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project diff --git a/src/yuzu/configuration/configure_per_game.h b/src/yuzu/configuration/configure_per_game.h index e4d18f29b9..1e53bf0708 100644 --- a/src/yuzu/configuration/configure_per_game.h +++ b/src/yuzu/configuration/configure_per_game.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project diff --git a/src/yuzu/configuration/configure_per_game_addons.cpp b/src/yuzu/configuration/configure_per_game_addons.cpp index ee2db55a5d..3032e4c3d9 100644 --- a/src/yuzu/configuration/configure_per_game_addons.cpp +++ b/src/yuzu/configuration/configure_per_game_addons.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2016 Citra Emulator Project @@ -14,17 +14,21 @@ #include #include #include +#include #include "common/fs/fs.h" #include "common/fs/path_util.h" +#include "configuration/addon/mod_select_dialog.h" #include "core/core.h" #include "core/file_sys/patch_manager.h" -#include "core/file_sys/xts_archive.h" #include "core/loader/loader.h" +#include "frontend_common/mod_manager.h" +#include "qt_common/abstract/frontend.h" +#include "qt_common/config/uisettings.h" +#include "qt_common/util/mod.h" #include "ui_configure_per_game_addons.h" #include "yuzu/configuration/configure_input.h" #include "yuzu/configuration/configure_per_game_addons.h" -#include "qt_common/config/uisettings.h" ConfigurePerGameAddons::ConfigurePerGameAddons(Core::System& system_, QWidget* parent) : QWidget(parent), ui{std::make_unique()}, system{system_} { @@ -66,6 +70,9 @@ ConfigurePerGameAddons::ConfigurePerGameAddons(Core::System& system_, QWidget* p connect(item_model, &QStandardItemModel::itemChanged, [] { UISettings::values.is_game_list_reload_pending.exchange(true); }); + + connect(ui->folder, &QAbstractButton::clicked, this, &ConfigurePerGameAddons::InstallModFolder); + connect(ui->zip, &QAbstractButton::clicked, this, &ConfigurePerGameAddons::InstallModZip); } ConfigurePerGameAddons::~ConfigurePerGameAddons() = default; @@ -99,6 +106,68 @@ void ConfigurePerGameAddons::SetTitleId(u64 id) { this->title_id = id; } +void ConfigurePerGameAddons::InstallMods(const QStringList& mods) { + QStringList failed; + for (const auto& mod : mods) { + if (FrontendCommon::InstallMod(mod.toStdString(), title_id, true) == + FrontendCommon::Failed) { + failed << QFileInfo(mod).baseName(); + } + } + + if (failed.empty()) { + QtCommon::Frontend::Information(tr("Mod Install Succeeded"), + tr("Successfully installed all mods.")); + + item_model->removeRows(0, item_model->rowCount()); + list_items.clear(); + LoadConfiguration(); + + UISettings::values.is_game_list_reload_pending.exchange(true); + } else { + QtCommon::Frontend::Critical( + tr("Mod Install Failed"), + tr("Failed to install the following mods:\n\t%1\nCheck the log for details.") + .arg(failed.join(QStringLiteral("\n\t")))); + } +} + +void ConfigurePerGameAddons::InstallModPath(const QString& path) { + const auto mods = QtCommon::Mod::GetModFolders(path, {}); + + if (mods.size() > 1) { + ModSelectDialog* dialog = new ModSelectDialog(mods, this); + connect(dialog, &ModSelectDialog::modsSelected, this, &ConfigurePerGameAddons::InstallMods); + dialog->show(); + } else { + InstallMods(mods); + } +} + +void ConfigurePerGameAddons::InstallModFolder() { + const auto path = QtCommon::Frontend::GetExistingDirectory( + tr("Mod Folder"), QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); + if (path.isEmpty()) { + return; + } + + InstallModPath(path); +} + +void ConfigurePerGameAddons::InstallModZip() { + const auto path = QtCommon::Frontend::GetOpenFileName( + tr("Zipped Mod Location"), + QStandardPaths::writableLocation(QStandardPaths::DownloadLocation), + tr("Zipped Archives (*.zip)")); + if (path.isEmpty()) { + return; + } + + const QString extracted = QtCommon::Mod::ExtractMod(path); + if (!extracted.isEmpty()) + InstallModPath(extracted); +} + void ConfigurePerGameAddons::changeEvent(QEvent* event) { if (event->type() == QEvent::LanguageChange) { RetranslateUI(); diff --git a/src/yuzu/configuration/configure_per_game_addons.h b/src/yuzu/configuration/configure_per_game_addons.h index 32dc5dde62..8b698b5bec 100644 --- a/src/yuzu/configuration/configure_per_game_addons.h +++ b/src/yuzu/configuration/configure_per_game_addons.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2016 Citra Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -7,6 +10,7 @@ #include #include +#include #include "core/file_sys/vfs/vfs_types.h" @@ -38,6 +42,13 @@ public: void SetTitleId(u64 id); +public slots: + void InstallMods(const QStringList &mods); + void InstallModPath(const QString& path); + + void InstallModFolder(); + void InstallModZip(); + private: void changeEvent(QEvent* event) override; void RetranslateUI(); diff --git a/src/yuzu/configuration/configure_per_game_addons.ui b/src/yuzu/configuration/configure_per_game_addons.ui index f9cf6f2c31..632f9d422b 100644 --- a/src/yuzu/configuration/configure_per_game_addons.ui +++ b/src/yuzu/configuration/configure_per_game_addons.ui @@ -17,7 +17,21 @@ Add-Ons - + + + + Import Mod from ZIP + + + + + + + Import Mod from Folder + + + + true @@ -28,7 +42,7 @@ 0 0 380 - 280 + 249 diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp index 5fee35dc7f..8adcdb4cbe 100644 --- a/src/yuzu/main_window.cpp +++ b/src/yuzu/main_window.cpp @@ -85,6 +85,7 @@ #include "qt_common/util/meta.h" #include "qt_common/util/content.h" #include "qt_common/util/fs.h" +#include "qt_common/util/mod.h" // These are wrappers to avoid the calls to CreateDirectory and CreateFile because of the Windows // defines. @@ -3654,6 +3655,7 @@ void MainWindow::OpenPerGameConfiguration(u64 title_id, const std::string& file_ Settings::SetConfiguringGlobal(false); ConfigurePerGame dialog(this, title_id, file_name, vk_device_records, *QtCommon::system); dialog.LoadFromFile(v_file); + const auto result = dialog.exec(); if (result != QDialog::Accepted && !UISettings::values.configuration_applied) { @@ -3665,7 +3667,7 @@ void MainWindow::OpenPerGameConfiguration(u64 title_id, const std::string& file_ const auto reload = UISettings::values.is_game_list_reload_pending.exchange(false); if (reload) { - game_list->PopulateAsync(UISettings::values.game_dirs); + OnGameListRefresh(); } // Do not cause the global config to write local settings into the config file