diff options
| author | crueter <crueter@eden-emu.dev> | 2026-02-06 06:37:30 +0100 |
|---|---|---|
| committer | crueter <crueter@eden-emu.dev> | 2026-02-06 06:37:30 +0100 |
| commit | e07e269bd766aa1fc29951b958db2063c1a4048e (patch) | |
| tree | c106879286f3bddaf4e5655210913b6e773facbf /src/yuzu/configuration | |
| parent | 08232ce64275eaf388eba7005f4dae28fc18a0b7 (diff) | |
[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 <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3472
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Diffstat (limited to 'src/yuzu/configuration')
| -rw-r--r-- | src/yuzu/configuration/addon/mod_select_dialog.cpp | 66 | ||||
| -rw-r--r-- | src/yuzu/configuration/addon/mod_select_dialog.h | 26 | ||||
| -rw-r--r-- | src/yuzu/configuration/addon/mod_select_dialog.ui | 99 | ||||
| -rw-r--r-- | src/yuzu/configuration/configure_per_game.cpp | 2 | ||||
| -rw-r--r-- | src/yuzu/configuration/configure_per_game.h | 2 | ||||
| -rw-r--r-- | src/yuzu/configuration/configure_per_game_addons.cpp | 75 | ||||
| -rw-r--r-- | src/yuzu/configuration/configure_per_game_addons.h | 11 | ||||
| -rw-r--r-- | src/yuzu/configuration/configure_per_game_addons.ui | 18 |
8 files changed, 292 insertions, 7 deletions
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 <QFileInfo> +#include <qnamespace.h> +#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<QStandardItem*>>("QList<QStandardItem*>"); + + 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 <QDialog> +#include <QStandardItemModel> + +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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ModSelectDialog</class> + <widget class="QDialog" name="ModSelectDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>430</height> + </rect> + </property> + <property name="windowTitle"> + <string>Dialog</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>The specified folder or archive contains the following mods. Select which ones to install.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QTreeView" name="treeView"> + <property name="contextMenuPolicy"> + <enum>Qt::ContextMenuPolicy::NoContextMenu</enum> + </property> + <property name="editTriggers"> + <set>QAbstractItemView::EditTrigger::NoEditTriggers</set> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="verticalScrollMode"> + <enum>QAbstractItemView::ScrollMode::ScrollPerPixel</enum> + </property> + <property name="uniformRowHeights"> + <bool>true</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <property name="headerHidden"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Orientation::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>ModSelectDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>ModSelectDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> 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 <QString> #include <QTimer> #include <QTreeView> +#include <qstandardpaths.h> #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<Ui::ConfigurePerGameAddons>()}, 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 <vector> #include <QList> +#include <QWidget> #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 @@ <string>Add-Ons</string> </property> <layout class="QGridLayout" name="gridLayout"> - <item row="0" column="0"> + <item row="1" column="0"> + <widget class="QPushButton" name="zip"> + <property name="text"> + <string>Import Mod from ZIP</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QPushButton" name="folder"> + <property name="text"> + <string>Import Mod from Folder</string> + </property> + </widget> + </item> + <item row="0" column="0" colspan="2"> <widget class="QScrollArea" name="scrollArea"> <property name="widgetResizable"> <bool>true</bool> @@ -28,7 +42,7 @@ <x>0</x> <y>0</y> <width>380</width> - <height>280</height> + <height>249</height> </rect> </property> </widget> |
