mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-21 19:36:55 +02:00
Add add-on (DLC) support (#1519)
Add add-on content support Co-authored-by: nmzik <Nmzik@mail.ru>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
#include "common/additionalContent.h"
|
||||
|
||||
#include "common/stringUtils.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <regex>
|
||||
#include <system_error>
|
||||
|
||||
namespace Common {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string GetString(const nlohmann::json& object, const std::string& key) {
|
||||
const auto it = object.find(key);
|
||||
return it != object.end() && it->is_string() ? it->get<std::string>() : std::string {};
|
||||
}
|
||||
|
||||
std::string GetTitle(const nlohmann::json& param) {
|
||||
const auto localized = param.find("localizedParameters");
|
||||
if (localized != param.end() && localized->is_object()) {
|
||||
const auto preferred = localized->find(GetString(*localized, "defaultLanguage"));
|
||||
if (preferred != localized->end()) {
|
||||
if (auto title = GetString(*preferred, "titleName"); !title.empty()) {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
for (const auto& language: *localized) {
|
||||
if (auto title = GetString(language, "titleName"); !title.empty()) {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
}
|
||||
return GetString(param, "titleName");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<AddContentEntry> FindAddContent(const std::filesystem::path& dir,
|
||||
const std::string& title_id) {
|
||||
if (dir.empty() || title_id.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
const auto root = std::filesystem::absolute(dir, ec);
|
||||
if (ec) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Scan directories separately so an unreadable child does not discard its siblings.
|
||||
std::vector<std::filesystem::path> pending {root};
|
||||
std::vector<std::filesystem::path> params;
|
||||
while (!pending.empty()) {
|
||||
const auto current = std::move(pending.back());
|
||||
pending.pop_back();
|
||||
std::filesystem::directory_iterator it(
|
||||
current, std::filesystem::directory_options::skip_permission_denied, ec);
|
||||
const std::filesystem::directory_iterator end;
|
||||
for (; !ec && it != end; it.increment(ec)) {
|
||||
std::error_code entry_ec;
|
||||
if (it->is_directory(entry_ec) && !it->is_symlink(entry_ec) && !entry_ec) {
|
||||
pending.push_back(it->path());
|
||||
} else if (it->is_regular_file(entry_ec) && !entry_ec &&
|
||||
EqualNoCase(PathToString(it->path().filename()), "param.json") &&
|
||||
EqualNoCase(PathToString(it->path().parent_path().filename()), "sce_sys")) {
|
||||
params.push_back(it->path());
|
||||
}
|
||||
}
|
||||
}
|
||||
std::sort(params.begin(), params.end());
|
||||
|
||||
static const std::regex content_id_pattern(
|
||||
"^[A-Z]{2}[0-9]{4}-[A-Z]{4}[0-9]{5}_[0-9]{2}-[A-Z0-9]{16}$");
|
||||
std::map<std::string, AddContentEntry> by_label;
|
||||
for (const auto& file: params) {
|
||||
const auto size = std::filesystem::file_size(file, ec);
|
||||
if (ec || size > 1024 * 1024) {
|
||||
continue;
|
||||
}
|
||||
std::ifstream stream(file, std::ios::binary);
|
||||
if (!stream) {
|
||||
continue;
|
||||
}
|
||||
const auto param = nlohmann::json::parse(stream, nullptr, false);
|
||||
// applicationCategoryType is required for applications, not additional content.
|
||||
if (!param.is_object() || param.contains("applicationCategoryType")) {
|
||||
continue;
|
||||
}
|
||||
const auto content_id = GetString(param, "contentId");
|
||||
if (!std::regex_match(content_id, content_id_pattern) ||
|
||||
content_id.compare(7, 9, title_id) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto mount_dir = file.parent_path().parent_path();
|
||||
const auto label = content_id.substr(20);
|
||||
by_label.try_emplace(label,
|
||||
AddContentEntry {content_id, label, mount_dir, GetTitle(param)});
|
||||
}
|
||||
|
||||
std::vector<AddContentEntry> entries;
|
||||
entries.reserve(by_label.size());
|
||||
for (auto& [label, entry]: by_label) {
|
||||
entries.push_back(std::move(entry));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
} // namespace Common
|
||||
@@ -0,0 +1,23 @@
|
||||
#ifndef KYTY_COMMON_ADDITIONAL_CONTENT_H_
|
||||
#define KYTY_COMMON_ADDITIONAL_CONTENT_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Common {
|
||||
|
||||
struct AddContentEntry {
|
||||
std::string content_id;
|
||||
std::string entitlement_label;
|
||||
std::filesystem::path mount_dir;
|
||||
std::string title;
|
||||
};
|
||||
|
||||
// Finds packages for this title, deduplicated by entitlement label.
|
||||
std::vector<AddContentEntry> FindAddContent(const std::filesystem::path& dir,
|
||||
const std::string& title_id);
|
||||
|
||||
} // namespace Common
|
||||
|
||||
#endif /* KYTY_COMMON_ADDITIONAL_CONTENT_H_ */
|
||||
+11
-1
@@ -22,6 +22,7 @@
|
||||
#include "libs/controller.h"
|
||||
#include "libs/libs.h"
|
||||
#include "libs/network.h"
|
||||
#include "loader/additionalContent.h"
|
||||
#include "loader/runtimeLinker.h"
|
||||
#include "loader/systemContent.h"
|
||||
#include "loader/timer.h"
|
||||
@@ -201,7 +202,15 @@ void Run(const RunOptions& options) {
|
||||
|
||||
PrintSystemInfo();
|
||||
std::string title_id;
|
||||
if (Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id) && !title_id.empty()) {
|
||||
Loader::SystemContentParamSfoGetString("TITLE_ID", &title_id);
|
||||
if (title_id.empty()) {
|
||||
std::string content_id;
|
||||
if (Loader::SystemContentParamSfoGetString("CONTENT_ID", &content_id) &&
|
||||
content_id.size() == 36 && content_id[6] == '-' && content_id[16] == '_') {
|
||||
title_id = content_id.substr(7, 9);
|
||||
}
|
||||
}
|
||||
if (!title_id.empty()) {
|
||||
Log::WriteToConsoleAndLog(fmt::format("Title ID: {}\n", title_id));
|
||||
}
|
||||
|
||||
@@ -217,6 +226,7 @@ void Run(const RunOptions& options) {
|
||||
|
||||
MountSandboxDirs();
|
||||
|
||||
Loader::AddContentLoad(options.addcont_dir, title_id);
|
||||
auto* rt = Common::Singleton<Loader::RuntimeLinker>::Instance();
|
||||
Libs::InitAll(rt->Symbols());
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ struct RunOptions {
|
||||
std::filesystem::path app0_dir;
|
||||
std::filesystem::path elf;
|
||||
std::filesystem::path game_patch;
|
||||
std::filesystem::path addcont_dir;
|
||||
};
|
||||
|
||||
void Run(const RunOptions& options);
|
||||
|
||||
@@ -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
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -4,15 +4,21 @@
|
||||
#include "common/emulatorConfig.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/stringUtils.h"
|
||||
#include "kernel/fileSystem.h"
|
||||
#include "libs/errno.h"
|
||||
#include "libs/libs.h"
|
||||
#include "libs/systemService.h"
|
||||
#include "loader/additionalContent.h"
|
||||
#include "loader/symbolDatabase.h"
|
||||
#include "loader/systemContent.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
namespace Libs {
|
||||
|
||||
@@ -20,10 +26,21 @@ LIB_VERSION("AppContent", 1, "AppContentUtil", 1, 1);
|
||||
|
||||
namespace AppContent {
|
||||
|
||||
constexpr uint32_t APP_CONTENT_APPPARAM_SKU_FLAG_FULL = 3;
|
||||
constexpr int APP_CONTENT_ERROR_PARAMETER = -2133262334; /* 0x80D90002 */
|
||||
constexpr int APP_CONTENT_ERROR_NOT_MOUNTED = -2133262332; /* 0x80D90004 */
|
||||
constexpr int APP_CONTENT_ERROR_DRM_NO_ENTITLEMENT = -2133262329; /* 0x80D90007 */
|
||||
constexpr uint32_t APP_CONTENT_APPPARAM_SKU_FLAG_FULL = 3;
|
||||
constexpr int APP_CONTENT_ERROR_PARAMETER = -2133262334; /* 0x80D90002 */
|
||||
constexpr int APP_CONTENT_ERROR_BUSY = -2133262333; /* 0x80D90003 */
|
||||
constexpr int APP_CONTENT_ERROR_NOT_MOUNTED = -2133262332; /* 0x80D90004 */
|
||||
constexpr int APP_CONTENT_ERROR_NOT_FOUND = -2133262331; /* 0x80D90005 */
|
||||
constexpr int APP_CONTENT_ERROR_MOUNT_FULL = -2133262330; /* 0x80D90006 */
|
||||
constexpr size_t APP_CONTENT_ADDCONT_MOUNT_MAXNUM = 64;
|
||||
|
||||
struct AddcontMount {
|
||||
std::string label;
|
||||
std::string point;
|
||||
};
|
||||
|
||||
static std::mutex g_addcont_mutex;
|
||||
static std::vector<AddcontMount> g_addcont_mounts;
|
||||
|
||||
static std::string g_title_id;
|
||||
|
||||
@@ -174,13 +191,51 @@ int KYTY_SYSV_ABI AppContentAddcontMount(uint32_t servic
|
||||
service_label, reinterpret_cast<uint64_t>(entitlement_label),
|
||||
reinterpret_cast<uint64_t>(mount_point));
|
||||
|
||||
if (entitlement_label == nullptr || mount_point == nullptr) {
|
||||
if (service_label != 0 || entitlement_label == nullptr || mount_point == nullptr) {
|
||||
return APP_CONTENT_ERROR_PARAMETER;
|
||||
}
|
||||
|
||||
std::memset(mount_point->data, 0, sizeof(mount_point->data));
|
||||
|
||||
return APP_CONTENT_ERROR_DRM_NO_ENTITLEMENT;
|
||||
const auto* entry =
|
||||
Loader::AddContentFind(entitlement_label->data, sizeof(entitlement_label->data));
|
||||
if (entry == nullptr) {
|
||||
return APP_CONTENT_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
std::lock_guard lock(g_addcont_mutex);
|
||||
|
||||
for (const auto& m: g_addcont_mounts) {
|
||||
if (m.label == entry->entitlement_label) {
|
||||
return APP_CONTENT_ERROR_BUSY;
|
||||
}
|
||||
}
|
||||
|
||||
if (g_addcont_mounts.size() == APP_CONTENT_ADDCONT_MOUNT_MAXNUM) {
|
||||
return APP_CONTENT_ERROR_MOUNT_FULL;
|
||||
}
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::is_directory(entry->mount_dir, ec)) {
|
||||
return APP_CONTENT_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
uint32_t index = 0;
|
||||
while (
|
||||
std::any_of(g_addcont_mounts.begin(), g_addcont_mounts.end(), [&](const AddcontMount& m) {
|
||||
return m.point == "/addcont" + std::to_string(index) + "/";
|
||||
})) {
|
||||
index++;
|
||||
}
|
||||
|
||||
AddcontMount mount {entry->entitlement_label, "/addcont" + std::to_string(index) + "/"};
|
||||
LibKernel::FileSystem::Mount(entry->mount_dir, mount.point);
|
||||
LOGF("\t mounted %s -> %s\n", mount.point.c_str(),
|
||||
Common::PathToString(entry->mount_dir).c_str());
|
||||
|
||||
std::strncpy(mount_point->data, mount.point.c_str(), sizeof(mount_point->data) - 1);
|
||||
g_addcont_mounts.push_back(std::move(mount));
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
int KYTY_SYSV_ABI AppContentAddcontUnmount(const AppContentMountPoint* mount_point) {
|
||||
@@ -196,6 +251,19 @@ int KYTY_SYSV_ABI AppContentAddcontUnmount(const AppContentMountPoint* mount_poi
|
||||
return APP_CONTENT_ERROR_NOT_MOUNTED;
|
||||
}
|
||||
|
||||
const std::string point(mount_point->data,
|
||||
strnlen(mount_point->data, sizeof(mount_point->data)));
|
||||
|
||||
std::lock_guard lock(g_addcont_mutex);
|
||||
|
||||
const auto it = std::find_if(g_addcont_mounts.begin(), g_addcont_mounts.end(),
|
||||
[&](const AddcontMount& m) { return m.point == point; });
|
||||
if (it == g_addcont_mounts.end()) {
|
||||
return APP_CONTENT_ERROR_NOT_MOUNTED;
|
||||
}
|
||||
LibKernel::FileSystem::Umount(point);
|
||||
g_addcont_mounts.erase(it);
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
#include "common/abi.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "libs/libs.h"
|
||||
#include "loader/additionalContent.h"
|
||||
#include "loader/symbolDatabase.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cinttypes>
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
@@ -164,6 +167,16 @@ static constexpr NpEntitlementAccessAddcontEntitlementInfo NP_ENTITLEMENT_ACCESS
|
||||
{{{"_mtqu6"}, {}}, 3, 4}, // GTA V hash 0x9cd1bcad
|
||||
};
|
||||
|
||||
static NpEntitlementAccessAddcontEntitlementInfo
|
||||
MakeAddcontInfo(const Loader::AddContentEntry& entry) {
|
||||
NpEntitlementAccessAddcontEntitlementInfo info {};
|
||||
std::memcpy(info.entitlement_label.data, entry.entitlement_label.data(),
|
||||
entry.entitlement_label.size());
|
||||
info.package_type = 2; // Additional content with extra data.
|
||||
info.download_status = 4; // Installed.
|
||||
return info;
|
||||
}
|
||||
|
||||
static int KYTY_SYSV_ABI NpEntitlementAccessInitialize(
|
||||
const NpEntitlementAccessInitParam* init_param, NpEntitlementAccessBootParam* boot_param) {
|
||||
PRINT_NAME();
|
||||
@@ -404,19 +417,21 @@ static int KYTY_SYSV_ABI NpEntitlementAccessGetAddcontEntitlementInfoList(
|
||||
LOGF("\t list_num = %" PRIu32 "\n", list_num);
|
||||
LOGF("\t hit_num = 0x%016" PRIx64 "\n", reinterpret_cast<uint64_t>(hit_num));
|
||||
|
||||
if (hit_num == nullptr || (list == nullptr && list_num != 0)) {
|
||||
if (hit_num == nullptr) {
|
||||
return NP_ENTITLEMENT_ACCESS_ERROR_PARAMETER;
|
||||
}
|
||||
|
||||
*hit_num = static_cast<uint32_t>(sizeof(NP_ENTITLEMENT_ACCESS_ADDON_LIST) /
|
||||
sizeof(NP_ENTITLEMENT_ACCESS_ADDON_LIST[0]));
|
||||
constexpr auto legacy_count = std::size(NP_ENTITLEMENT_ACCESS_ADDON_LIST);
|
||||
const auto& entries = Loader::AddContentGetAll();
|
||||
*hit_num = static_cast<uint32_t>(legacy_count + entries.size());
|
||||
|
||||
if (list != nullptr && list_num != 0) {
|
||||
memset(list, 0, sizeof(*list) * list_num);
|
||||
|
||||
const auto copy_num = (list_num < *hit_num ? list_num : *hit_num);
|
||||
const auto copy_num = std::min(list_num, *hit_num);
|
||||
for (uint32_t i = 0; i < copy_num; i++) {
|
||||
list[i] = NP_ENTITLEMENT_ACCESS_ADDON_LIST[i];
|
||||
list[i] = i < legacy_count ? NP_ENTITLEMENT_ACCESS_ADDON_LIST[i]
|
||||
: MakeAddcontInfo(entries[i - legacy_count]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,6 +462,12 @@ static int KYTY_SYSV_ABI NpEntitlementAccessGetAddcontEntitlementInfo(
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto* entry =
|
||||
Loader::AddContentFind(entitlement_label->data, sizeof(entitlement_label->data))) {
|
||||
*info = MakeAddcontInfo(*entry);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return NP_ENTITLEMENT_ACCESS_ERROR_NO_ENTITLEMENT;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#include "loader/additionalContent.h"
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "common/stringUtils.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string_view>
|
||||
|
||||
namespace Loader {
|
||||
|
||||
static std::vector<AddContentEntry> g_add_content;
|
||||
|
||||
void AddContentLoad(const std::filesystem::path& dir, const std::string& title_id) {
|
||||
g_add_content = Common::FindAddContent(dir, title_id);
|
||||
for (const auto& entry: g_add_content) {
|
||||
LOGF("AddContent: registered %s -> %s\n", entry.content_id.c_str(),
|
||||
Common::PathToString(entry.mount_dir).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<AddContentEntry>& AddContentGetAll() {
|
||||
return g_add_content;
|
||||
}
|
||||
|
||||
const AddContentEntry* AddContentFind(const char* label, size_t label_size) {
|
||||
if (label == nullptr || label_size == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
const std::string_view value(label, std::find(label, label + label_size, '\0') - label);
|
||||
const auto it =
|
||||
std::find_if(g_add_content.begin(), g_add_content.end(),
|
||||
[&](const auto& entry) { return entry.entitlement_label == value; });
|
||||
return it == g_add_content.end() ? nullptr : &*it;
|
||||
}
|
||||
|
||||
} // namespace Loader
|
||||
@@ -0,0 +1,18 @@
|
||||
#ifndef EMULATOR_INCLUDE_EMULATOR_LOADER_ADDITIONAL_CONTENT_H_
|
||||
#define EMULATOR_INCLUDE_EMULATOR_LOADER_ADDITIONAL_CONTENT_H_
|
||||
|
||||
#include "common/additionalContent.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace Loader {
|
||||
|
||||
using AddContentEntry = Common::AddContentEntry;
|
||||
|
||||
void AddContentLoad(const std::filesystem::path& dir, const std::string& title_id);
|
||||
const std::vector<AddContentEntry>& AddContentGetAll();
|
||||
const AddContentEntry* AddContentFind(const char* label, size_t label_size);
|
||||
|
||||
} // namespace Loader
|
||||
|
||||
#endif /* EMULATOR_INCLUDE_EMULATOR_LOADER_ADDITIONAL_CONTENT_H_ */
|
||||
@@ -46,6 +46,7 @@ static void PrintUsage() {
|
||||
::printf("Options:\n");
|
||||
::printf(" --game <dir|elf|zar> Game directory, ELF, or ZArchive to load.\n");
|
||||
::printf(" --game-patch <json> ETAHen cheat file.\n");
|
||||
::printf(" --addcont-dir <dir> Add-ons (DLC) folder (default: none).\n");
|
||||
::printf(" --screen-width <num> Window width. Default: 1280.\n");
|
||||
::printf(" --screen-height <num> Window height. Default: 720.\n");
|
||||
::printf(
|
||||
@@ -326,6 +327,12 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
||||
return false;
|
||||
}
|
||||
options.game_patch = path;
|
||||
} else if (arg == "--addcont-dir") {
|
||||
if (!options.addcont_dir.empty()) {
|
||||
::printf("--addcont-dir can only be specified once\n");
|
||||
return false;
|
||||
}
|
||||
options.addcont_dir = Common::PathFromUtf8(Common::FixFilenameSlash(value));
|
||||
} else if (arg == "--screen-width") {
|
||||
if (!ParseUint32(value, options.config.screen_width) ||
|
||||
options.config.screen_width == 0) {
|
||||
|
||||
Reference in New Issue
Block a user