Add add-on (DLC) support (#1519)

Add add-on content support

Co-authored-by: nmzik <Nmzik@mail.ru>
This commit is contained in:
Felipe
2026-10-15 21:09:39 -03:00
committed by GitHub
parent c6b8210d7c
commit 4242ec5a64
18 changed files with 633 additions and 17 deletions
@@ -0,0 +1,29 @@
#ifndef ADDITIONAL_CONTENT_DIALOG_H
#define ADDITIONAL_CONTENT_DIALOG_H
#include <QDialog>
#include <QIcon>
#include <QString>
class Configuration;
class QLabel;
class QListWidget;
class QPushButton;
class AdditionalContentDialog final: public QDialog {
public:
AdditionalContentDialog(const Configuration& game, const QString& folder,
QWidget* parent = nullptr);
private:
void Scan();
QString m_title_id;
QString m_folder;
QIcon m_game_icon;
QLabel* m_status = nullptr;
QListWidget* m_list = nullptr;
QPushButton* m_open = nullptr;
};
#endif // ADDITIONAL_CONTENT_DIALOG_H
+1
View File
@@ -103,6 +103,7 @@ public:
QString firmwareVer; /* requiredSystemSoftwareVersion from sce_sys/param.json */
QString basedir; /* Game base directory */
QString game_path; /* Launcher-unique game path */
QString addons_dir; /* Global setting; not saved per game. */
bool custom_settings = false;
GameStatus game_status = GameStatus::Unknown;
QString game_comment;
@@ -6,6 +6,7 @@
#include <QStringList>
class QByteArray;
class QGroupBox;
class QLineEdit;
class QListWidget;
class QMoveEvent;
class QResizeEvent;
@@ -28,7 +29,8 @@ public:
static void WriteSettings(QSettings& s);
static void ReadSettings(QSettings& s);
void SetGlobalSettings(const QStringList& dirs);
void SetGlobalSettings(const QStringList& dirs, const QString& addons_dir);
[[nodiscard]] QString GetAddonsDirectory() const;
[[nodiscard]] QStringList GetGameDirectories() const;
signals:
@@ -41,6 +43,7 @@ private:
Configuration& m_info;
QGroupBox* m_game_dirs_group = nullptr;
QListWidget* m_game_dirs_list = nullptr;
QLineEdit* m_addons_dir_edit = nullptr;
QToolButton* m_remove_game_dir_button = nullptr;
bool m_global_settings = false;
@@ -85,6 +85,7 @@ private:
QString m_settings_file;
QString m_runtime_directory;
QStringList m_game_dirs;
QString m_addons_dir;
Configuration m_global_info;
QMap<QString, Configuration*> m_custom_infos;
CompatibilityDatabase* m_compatibility = nullptr;
@@ -0,0 +1,90 @@
#include "additionalContentDialog.h"
#include "common/additionalContent.h"
#include "configuration.h"
#include "gameContent.h"
#include <QDesktopServices>
#include <QDialogButtonBox>
#include <QDir>
#include <QLabel>
#include <QListWidget>
#include <QPixmap>
#include <QPushButton>
#include <QStyle>
#include <QUrl>
#include <QVBoxLayout>
AdditionalContentDialog::AdditionalContentDialog(const Configuration& game, const QString& folder,
QWidget* parent)
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()), m_folder(folder) {
setAttribute(Qt::WA_DeleteOnClose);
setWindowTitle(tr("Additional Content - %1").arg(game.name));
resize(720, 480);
QPixmap icon;
icon.loadFromData(GameContent::ReadFile(game.basedir, QStringLiteral("sce_sys/icon0.png"),
GameContent::MaxImageSize));
m_game_icon = icon.isNull() ? style()->standardIcon(QStyle::SP_FileIcon) : QIcon(icon);
auto* layout = new QVBoxLayout(this);
auto* name = new QLabel(tr("%1 · %2").arg(game.name, m_title_id), this);
name->setTextFormat(Qt::PlainText);
name->setWordWrap(true);
layout->addWidget(name);
m_list = new QListWidget(this);
m_list->setIconSize(QSize(64, 64));
m_list->setSpacing(4);
m_list->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
layout->addWidget(m_list, 1);
m_status = new QLabel(this);
m_status->setTextFormat(Qt::PlainText);
m_status->setWordWrap(true);
layout->addWidget(m_status);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
auto* refresh = buttons->addButton(tr("Refresh"), QDialogButtonBox::ActionRole);
m_open = buttons->addButton(tr("Open folder"), QDialogButtonBox::ActionRole);
layout->addWidget(buttons);
connect(refresh, &QPushButton::clicked, this, &AdditionalContentDialog::Scan);
connect(m_open, &QPushButton::clicked, this,
[this]() { QDesktopServices::openUrl(QUrl::fromLocalFile(m_folder)); });
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close);
Scan();
}
void AdditionalContentDialog::Scan() {
m_list->clear();
const bool folder_exists = !m_folder.isEmpty() && QDir(m_folder).exists();
m_open->setEnabled(folder_exists);
for (const auto& entry:
Common::FindAddContent(GameContent::ToPath(m_folder), m_title_id.toStdString())) {
QPixmap icon;
icon.loadFromData(GameContent::ReadFile(GameContent::FromPath(entry.mount_dir),
QStringLiteral("sce_sys/icon0.png"),
GameContent::MaxImageSize));
const auto title =
QString::fromStdString(entry.title.empty() ? entry.entitlement_label : entry.title);
auto* item = new QListWidgetItem(icon.isNull() ? m_game_icon : QIcon(icon),
tr("%1\nInstalled").arg(title), m_list);
item->setToolTip(QString::fromStdString(entry.content_id));
}
if (m_folder.isEmpty()) {
m_status->setText(tr("No add-ons folder is set. Open Global settings and set the "
"Add-ons folder to the location of your extracted add-on packages."));
} else if (!folder_exists) {
m_status->setText(tr("The add-ons folder does not exist:\n%1\nCheck the path in Global "
"settings.")
.arg(QDir::toNativeSeparators(m_folder)));
} else if (m_list->count() == 0) {
m_status->setText(
tr("No add-ons found for this game in:\n%1").arg(QDir::toNativeSeparators(m_folder)));
} else {
m_status->setText(tr("%n installed add-on(s)", nullptr, m_list->count()));
}
}
+30 -2
View File
@@ -15,6 +15,7 @@
#include <QFileInfo>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QLayout>
#include <QLineEdit>
#include <QListView>
@@ -347,6 +348,28 @@ void ConfigurationEditDialog::InitGameDirectories() {
button_layout->addStretch(1);
group_layout->addLayout(button_layout);
group_layout->addWidget(new QLabel(tr("Add-ons folder"), m_game_dirs_group));
auto* addons_layout = new QHBoxLayout;
addons_layout->setContentsMargins(0, 0, 0, 0);
addons_layout->setSpacing(4);
m_addons_dir_edit = new QLineEdit(m_game_dirs_group);
m_addons_dir_edit->setPlaceholderText(tr("Folder containing extracted add-on (DLC) packages"));
addons_layout->addWidget(m_addons_dir_edit, 1);
auto* addons_button = new QToolButton(m_game_dirs_group);
addons_button->setIcon(style()->standardIcon(QStyle::SP_DirOpenIcon));
addons_button->setToolTip(tr("Select add-ons folder"));
addons_button->setAutoRaise(true);
addons_layout->addWidget(addons_button);
group_layout->addLayout(addons_layout);
connect(addons_button, &QToolButton::clicked, this, [this]() {
const auto dir = QFileDialog::getExistingDirectory(
this, tr("Select add-ons folder"),
m_addons_dir_edit->text().isEmpty() ? QDir::homePath() : m_addons_dir_edit->text());
if (!dir.isEmpty()) {
m_addons_dir_edit->setText(QDir::toNativeSeparators(dir));
}
});
m_ui->gridLayout->addWidget(m_game_dirs_group, 1, 0, 1, 1);
m_game_dirs_group->setVisible(false);
@@ -359,9 +382,10 @@ void ConfigurationEditDialog::InitGameDirectories() {
update_game_directory_buttons();
}
void ConfigurationEditDialog::SetGlobalSettings(const QStringList& dirs) {
void ConfigurationEditDialog::SetGlobalSettings(const QStringList& dirs, const QString& addons_dir) {
m_global_settings = true;
auto* menu = new QMenu(m_ui->settings_button);
m_addons_dir_edit->setText(QDir::toNativeSeparators(addons_dir));
auto* menu = new QMenu(m_ui->settings_button);
menu->addAction(tr("Import..."), this, &ConfigurationEditDialog::ImportGameSettings);
menu->addAction(tr("Export..."), this, &ConfigurationEditDialog::ExportGameSettings);
m_ui->settings_button->setMenu(menu);
@@ -380,6 +404,10 @@ void ConfigurationEditDialog::SetGlobalSettings(const QStringList& dirs) {
resize(size().expandedTo(minimumSizeHint()));
}
QString ConfigurationEditDialog::GetAddonsDirectory() const {
return m_global_settings ? m_addons_dir_edit->text().trimmed() : QString();
}
QStringList ConfigurationEditDialog::GetGameDirectories() const {
QStringList dirs;
if (!m_global_settings) {
+24 -2
View File
@@ -1,5 +1,6 @@
#include "configurationListWidget.h"
#include "additionalContentDialog.h"
#include "cheatFile.h"
#include "common/archive.h"
#include "compatibilityDatabase.h"
@@ -73,6 +74,7 @@ constexpr char CONF_SECTION_NAME[] = "GameConfigurations";
constexpr char CONF_LAUNCHER[] = "Launcher";
constexpr char CONF_GAME_DIR[] = "game_dir";
constexpr char CONF_GAME_DIRS[] = "game_dirs";
constexpr char CONF_ADDONS_DIR[] = "addons_dir";
constexpr char CONF_GLOBAL[] = "GlobalConfiguration";
constexpr char SAVE_DATA_DIR[] = "_SaveData";
@@ -395,6 +397,7 @@ void ConfigurationListWidget::WriteSettings() {
s->beginGroup(CONF_LAUNCHER);
m_game_dirs = NormalizeGameDirectories(m_game_dirs);
s->setValue(CONF_GAME_DIRS, m_game_dirs);
s->setValue(CONF_ADDONS_DIR, m_addons_dir);
s->setValue("grid_view", m_ui->grid_view_button->isChecked());
s->setValue("grid_icon_width", m_ui->grid_size_slider->value());
s->remove(CONF_GAME_DIR);
@@ -444,6 +447,7 @@ void ConfigurationListWidget::ReadSettings() {
if (m_game_dirs.isEmpty()) {
m_game_dirs = NormalizeGameDirectories(SettingsStringList(s->value(CONF_GAME_DIR)));
}
m_addons_dir = NormalizeGameDirectory(s->value(CONF_ADDONS_DIR).toString());
s->endGroup();
s->beginGroup(CONF_GLOBAL);
@@ -508,6 +512,7 @@ ConfigurationListWidget::CreateConfiguration(const ConfigurationItem& item) cons
info->elf = custom->elf;
}
info->host_input_mapping = m_global_info.host_input_mapping;
info->addons_dir = m_addons_dir;
return info;
}
@@ -598,7 +603,13 @@ static GameMetadata GetGameMetadata(const QByteArray& param_data, const QString&
ret.title_name = title;
}
ret.title_id = GetJsonString(root, QStringLiteral("titleId"));
ret.title_id = GetJsonString(root, QStringLiteral("titleId"));
if (ret.title_id.isEmpty()) {
const auto content_id = GetJsonString(root, QStringLiteral("contentId"));
if (content_id.size() == 36 && content_id[6] == '-' && content_id[16] == '_') {
ret.title_id = content_id.mid(7, 9);
}
}
ret.gameVersion = GetJsonString(root, QStringLiteral("appVersion"));
if (ret.gameVersion.isEmpty()) {
ret.gameVersion = GetJsonString(root, QStringLiteral("contentVersion"));
@@ -887,7 +898,7 @@ void ConfigurationListWidget::edit_global_settings() {
ConfigurationEditDialog dlg(info, this);
dlg.setWindowTitle(tr("Global settings"));
dlg.SetGlobalSettings(m_game_dirs);
dlg.SetGlobalSettings(m_game_dirs, m_addons_dir);
connect(&dlg, &ConfigurationEditDialog::PreviewControllerColor, this,
&ConfigurationListWidget::PreviewControllerColor);
connect(&dlg, &ConfigurationEditDialog::ImportGameSettings, this,
@@ -901,6 +912,7 @@ void ConfigurationListWidget::edit_global_settings() {
const auto game_dirs = NormalizeGameDirectories(dlg.GetGameDirectories());
const bool game_dirs_changed = game_dirs != m_game_dirs;
m_game_dirs = game_dirs;
m_addons_dir = NormalizeGameDirectory(dlg.GetAddonsDirectory());
WriteSettings();
if (game_dirs_changed) {
ScanGameDirectory();
@@ -1246,6 +1258,16 @@ void ConfigurationListWidget::ShowContextMenu(QTreeWidgetItem* witem) {
});
action_patches->setVisible(item != nullptr &&
Cheats::IsSupportedTitleId(item->GetInfo().title_id));
QAction* action_addcont = menu.addAction(
style()->standardIcon(QStyle::SP_DirIcon), tr("Additional Content..."));
connect(action_addcont, &QAction::triggered, this,
[this, item = QPointer<ConfigurationItem>(item)]() {
if (item != nullptr) {
auto* dialog = new AdditionalContentDialog(item->GetInfo(), m_addons_dir, this);
dialog->show();
}
});
action_addcont->setEnabled(item != nullptr);
QAction* action_remove_save_data =
menu.addAction(style()->standardIcon(QStyle::SP_DialogDiscardButton),
tr("Remove save data..."), this, SLOT(remove_save_data()));
+3
View File
@@ -295,6 +295,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
game = QDir(info.basedir).filePath(info.elf);
}
args << "--game" << game;
if (!info.addons_dir.isEmpty()) {
args << "--addcont-dir" << info.addons_dir;
}
const auto patch_plan = Cheats::PlanPath(info.title_id);
if (QFileInfo::exists(patch_plan)) {