mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-28 22:06:59 +01:00
addcont: add user add-ons with --addcont and a launcher list
This commit is contained in:
@@ -109,4 +109,23 @@ std::vector<AddContentEntry> FindAddContent(const std::filesystem::path& dir,
|
|||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<AddContentEntry> ParseAddContent(std::string_view value) {
|
||||||
|
static const std::regex label_pattern("^[A-Z0-9]{16}$");
|
||||||
|
const auto eq = value.find('=');
|
||||||
|
const auto label = value.substr(0, eq);
|
||||||
|
if (!std::regex_match(label.begin(), label.end(), label_pattern)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddContentEntry entry {{}, std::string(label), {}, {}};
|
||||||
|
if (eq != std::string_view::npos) {
|
||||||
|
entry.mount_dir = PathFromUtf8(FixFilenameSlash(value.substr(eq + 1)));
|
||||||
|
std::error_code ec;
|
||||||
|
if (!std::filesystem::is_directory(entry.mount_dir, ec)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Common
|
} // namespace Common
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
#define KYTY_COMMON_ADDITIONAL_CONTENT_H_
|
#define KYTY_COMMON_ADDITIONAL_CONTENT_H_
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace Common {
|
namespace Common {
|
||||||
@@ -10,7 +12,7 @@ namespace Common {
|
|||||||
struct AddContentEntry {
|
struct AddContentEntry {
|
||||||
std::string content_id;
|
std::string content_id;
|
||||||
std::string entitlement_label;
|
std::string entitlement_label;
|
||||||
std::filesystem::path mount_dir;
|
std::filesystem::path mount_dir; // Empty for an add-on without extra data.
|
||||||
std::string title;
|
std::string title;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -18,6 +20,9 @@ struct AddContentEntry {
|
|||||||
std::vector<AddContentEntry> FindAddContent(const std::filesystem::path& dir,
|
std::vector<AddContentEntry> FindAddContent(const std::filesystem::path& dir,
|
||||||
const std::string& title_id);
|
const std::string& title_id);
|
||||||
|
|
||||||
|
// Parses a user add-on: "LABEL" without extra data, or "LABEL=DIR" with its data in DIR.
|
||||||
|
std::optional<AddContentEntry> ParseAddContent(std::string_view value);
|
||||||
|
|
||||||
} // namespace Common
|
} // namespace Common
|
||||||
|
|
||||||
#endif /* KYTY_COMMON_ADDITIONAL_CONTENT_H_ */
|
#endif /* KYTY_COMMON_ADDITIONAL_CONTENT_H_ */
|
||||||
|
|||||||
+1
-1
@@ -188,7 +188,7 @@ void Run(const RunOptions& options) {
|
|||||||
|
|
||||||
MountSandboxDirs();
|
MountSandboxDirs();
|
||||||
|
|
||||||
Loader::AddContentLoad(options.addcont_dir, title_id);
|
Loader::AddContentLoad(options.addcont_dir, options.addcont, title_id);
|
||||||
auto* rt = Common::Singleton<Loader::RuntimeLinker>::Instance();
|
auto* rt = Common::Singleton<Loader::RuntimeLinker>::Instance();
|
||||||
Libs::InitAll(rt->Symbols());
|
Libs::InitAll(rt->Symbols());
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#ifndef EMULATOR_INCLUDE_EMULATOR_EMULATOR_H_
|
#ifndef EMULATOR_INCLUDE_EMULATOR_EMULATOR_H_
|
||||||
#define EMULATOR_INCLUDE_EMULATOR_EMULATOR_H_
|
#define EMULATOR_INCLUDE_EMULATOR_EMULATOR_H_
|
||||||
|
|
||||||
|
#include "common/additionalContent.h"
|
||||||
#include "common/emulatorConfig.h"
|
#include "common/emulatorConfig.h"
|
||||||
#include "common/stringUtils.h"
|
#include "common/stringUtils.h"
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ struct RunOptions {
|
|||||||
std::filesystem::path elf;
|
std::filesystem::path elf;
|
||||||
std::filesystem::path game_patch;
|
std::filesystem::path game_patch;
|
||||||
std::filesystem::path addcont_dir;
|
std::filesystem::path addcont_dir;
|
||||||
|
std::vector<Common::AddContentEntry> addcont;
|
||||||
};
|
};
|
||||||
|
|
||||||
void Run(const RunOptions& options);
|
void Run(const RunOptions& options);
|
||||||
|
|||||||
@@ -4,26 +4,39 @@
|
|||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
class Configuration;
|
class Configuration;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
|
class QLineEdit;
|
||||||
class QListWidget;
|
class QListWidget;
|
||||||
class QPushButton;
|
class QPushButton;
|
||||||
|
|
||||||
class AdditionalContentDialog final: public QDialog {
|
class AdditionalContentDialog final: public QDialog {
|
||||||
public:
|
public:
|
||||||
AdditionalContentDialog(const Configuration& game, const QString& folder,
|
// entries are the game's user add-ons, "LABEL" or "LABEL=DIR"; save receives every edit.
|
||||||
|
AdditionalContentDialog(const Configuration& game, const QString& folder, QStringList entries,
|
||||||
|
std::function<void(const QStringList&)> save,
|
||||||
QWidget* parent = nullptr);
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void Scan();
|
void Scan();
|
||||||
|
void Add();
|
||||||
|
void Remove();
|
||||||
|
|
||||||
QString m_title_id;
|
QString m_title_id;
|
||||||
QString m_folder;
|
QString m_folder;
|
||||||
|
QStringList m_entries;
|
||||||
|
std::function<void(const QStringList&)> m_save;
|
||||||
QIcon m_game_icon;
|
QIcon m_game_icon;
|
||||||
QLabel* m_status = nullptr;
|
QLabel* m_status = nullptr;
|
||||||
QListWidget* m_list = nullptr;
|
QListWidget* m_list = nullptr;
|
||||||
QPushButton* m_open = nullptr;
|
QPushButton* m_open = nullptr;
|
||||||
|
QPushButton* m_remove = nullptr;
|
||||||
|
QLineEdit* m_label_edit = nullptr;
|
||||||
|
QLineEdit* m_dir_edit = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // ADDITIONAL_CONTENT_DIALOG_H
|
#endif // ADDITIONAL_CONTENT_DIALOG_H
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ public:
|
|||||||
QString basedir; /* Game base directory */
|
QString basedir; /* Game base directory */
|
||||||
QString game_path; /* Launcher-unique game path */
|
QString game_path; /* Launcher-unique game path */
|
||||||
QString addons_dir; /* Global setting; not saved per game. */
|
QString addons_dir; /* Global setting; not saved per game. */
|
||||||
|
QStringList addcont; /* User add-ons ("LABEL" or "LABEL=DIR"), saved by title ID. */
|
||||||
bool custom_settings = false;
|
bool custom_settings = false;
|
||||||
GameStatus game_status = GameStatus::Unknown;
|
GameStatus game_status = GameStatus::Unknown;
|
||||||
QString game_comment;
|
QString game_comment;
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ private:
|
|||||||
QString m_runtime_directory;
|
QString m_runtime_directory;
|
||||||
QStringList m_game_dirs;
|
QStringList m_game_dirs;
|
||||||
QString m_addons_dir;
|
QString m_addons_dir;
|
||||||
|
QMap<QString, QStringList> m_addcont; // User add-ons by title ID.
|
||||||
Configuration m_global_info;
|
Configuration m_global_info;
|
||||||
QMap<QString, Configuration*> m_custom_infos;
|
QMap<QString, Configuration*> m_custom_infos;
|
||||||
CompatibilityDatabase* m_compatibility = nullptr;
|
CompatibilityDatabase* m_compatibility = nullptr;
|
||||||
|
|||||||
@@ -7,7 +7,10 @@
|
|||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QHBoxLayout>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QLineEdit>
|
||||||
#include <QListWidget>
|
#include <QListWidget>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
@@ -16,8 +19,11 @@
|
|||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
AdditionalContentDialog::AdditionalContentDialog(const Configuration& game, const QString& folder,
|
AdditionalContentDialog::AdditionalContentDialog(const Configuration& game, const QString& folder,
|
||||||
|
QStringList entries,
|
||||||
|
std::function<void(const QStringList&)> save,
|
||||||
QWidget* parent)
|
QWidget* parent)
|
||||||
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()), m_folder(folder) {
|
: QDialog(parent), m_title_id(game.title_id.trimmed().toUpper()), m_folder(folder),
|
||||||
|
m_entries(std::move(entries)), m_save(std::move(save)) {
|
||||||
setAttribute(Qt::WA_DeleteOnClose);
|
setAttribute(Qt::WA_DeleteOnClose);
|
||||||
setWindowTitle(tr("Additional Content - %1").arg(game.name));
|
setWindowTitle(tr("Additional Content - %1").arg(game.name));
|
||||||
resize(720, 480);
|
resize(720, 480);
|
||||||
@@ -39,6 +45,28 @@ AdditionalContentDialog::AdditionalContentDialog(const Configuration& game, cons
|
|||||||
m_list->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
m_list->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||||
layout->addWidget(m_list, 1);
|
layout->addWidget(m_list, 1);
|
||||||
|
|
||||||
|
auto* add_layout = new QHBoxLayout;
|
||||||
|
m_label_edit = new QLineEdit(this);
|
||||||
|
m_label_edit->setPlaceholderText(tr("Label or content ID"));
|
||||||
|
m_dir_edit = new QLineEdit(this);
|
||||||
|
m_dir_edit->setPlaceholderText(tr("Add-on data folder (empty for a license only)"));
|
||||||
|
auto* browse = new QPushButton(tr("Browse..."), this);
|
||||||
|
auto* add = new QPushButton(tr("Add"), this);
|
||||||
|
add->setDefault(true);
|
||||||
|
add_layout->addWidget(m_label_edit, 1);
|
||||||
|
add_layout->addWidget(m_dir_edit, 2);
|
||||||
|
add_layout->addWidget(browse);
|
||||||
|
add_layout->addWidget(add);
|
||||||
|
layout->addLayout(add_layout);
|
||||||
|
connect(browse, &QPushButton::clicked, this, [this]() {
|
||||||
|
const auto dir =
|
||||||
|
QFileDialog::getExistingDirectory(this, tr("Add-on data folder"), m_dir_edit->text());
|
||||||
|
if (!dir.isEmpty()) {
|
||||||
|
m_dir_edit->setText(QDir::toNativeSeparators(dir));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect(add, &QPushButton::clicked, this, &AdditionalContentDialog::Add);
|
||||||
|
|
||||||
m_status = new QLabel(this);
|
m_status = new QLabel(this);
|
||||||
m_status->setTextFormat(Qt::PlainText);
|
m_status->setTextFormat(Qt::PlainText);
|
||||||
m_status->setWordWrap(true);
|
m_status->setWordWrap(true);
|
||||||
@@ -47,10 +75,15 @@ AdditionalContentDialog::AdditionalContentDialog(const Configuration& game, cons
|
|||||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
|
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
|
||||||
auto* refresh = buttons->addButton(tr("Refresh"), QDialogButtonBox::ActionRole);
|
auto* refresh = buttons->addButton(tr("Refresh"), QDialogButtonBox::ActionRole);
|
||||||
m_open = buttons->addButton(tr("Open folder"), QDialogButtonBox::ActionRole);
|
m_open = buttons->addButton(tr("Open folder"), QDialogButtonBox::ActionRole);
|
||||||
|
m_remove = buttons->addButton(tr("Remove"), QDialogButtonBox::ActionRole);
|
||||||
layout->addWidget(buttons);
|
layout->addWidget(buttons);
|
||||||
connect(refresh, &QPushButton::clicked, this, &AdditionalContentDialog::Scan);
|
connect(refresh, &QPushButton::clicked, this, &AdditionalContentDialog::Scan);
|
||||||
connect(m_open, &QPushButton::clicked, this,
|
connect(m_open, &QPushButton::clicked, this,
|
||||||
[this]() { QDesktopServices::openUrl(QUrl::fromLocalFile(m_folder)); });
|
[this]() { QDesktopServices::openUrl(QUrl::fromLocalFile(m_folder)); });
|
||||||
|
connect(m_remove, &QPushButton::clicked, this, &AdditionalContentDialog::Remove);
|
||||||
|
connect(m_list, &QListWidget::currentItemChanged, this, [this](const QListWidgetItem* item) {
|
||||||
|
m_remove->setEnabled(item != nullptr && item->data(Qt::UserRole).isValid());
|
||||||
|
});
|
||||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close);
|
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close);
|
||||||
|
|
||||||
Scan();
|
Scan();
|
||||||
@@ -73,18 +106,54 @@ void AdditionalContentDialog::Scan() {
|
|||||||
tr("%1\nInstalled").arg(title), m_list);
|
tr("%1\nInstalled").arg(title), m_list);
|
||||||
item->setToolTip(QString::fromStdString(entry.content_id));
|
item->setToolTip(QString::fromStdString(entry.content_id));
|
||||||
}
|
}
|
||||||
|
for (const auto& entry: m_entries) {
|
||||||
|
const auto label = entry.section('=', 0, 0);
|
||||||
|
const auto dir = entry.section('=', 1);
|
||||||
|
auto text = tr("%1\nLicense only").arg(label);
|
||||||
|
if (!dir.isEmpty()) {
|
||||||
|
text = (QDir(dir).exists() ? tr("%1\nData: %2") : tr("%1\nData folder not found: %2"))
|
||||||
|
.arg(label, QDir::toNativeSeparators(dir));
|
||||||
|
}
|
||||||
|
auto* item = new QListWidgetItem(m_game_icon, text, m_list);
|
||||||
|
item->setData(Qt::UserRole, entry);
|
||||||
|
}
|
||||||
|
m_remove->setEnabled(false);
|
||||||
|
|
||||||
if (m_folder.isEmpty()) {
|
if (m_list->count() != 0) {
|
||||||
|
m_status->setText(tr("%n add-on(s)", nullptr, m_list->count()));
|
||||||
|
} else if (m_folder.isEmpty()) {
|
||||||
m_status->setText(tr("No add-ons folder is set. Open Global settings and set the "
|
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."));
|
"Add-ons folder to the location of your extracted add-on packages."));
|
||||||
} else if (!folder_exists) {
|
} else if (!folder_exists) {
|
||||||
m_status->setText(tr("The add-ons folder does not exist:\n%1\nCheck the path in Global "
|
m_status->setText(tr("The add-ons folder does not exist:\n%1\nCheck the path in Global "
|
||||||
"settings.")
|
"settings.")
|
||||||
.arg(QDir::toNativeSeparators(m_folder)));
|
.arg(QDir::toNativeSeparators(m_folder)));
|
||||||
} else if (m_list->count() == 0) {
|
} else {
|
||||||
m_status->setText(
|
m_status->setText(
|
||||||
tr("No add-ons found for this game in:\n%1").arg(QDir::toNativeSeparators(m_folder)));
|
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()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AdditionalContentDialog::Add() {
|
||||||
|
// A content ID ends with its entitlement label.
|
||||||
|
const auto label = m_label_edit->text().trimmed().section('-', -1);
|
||||||
|
const auto dir = m_dir_edit->text().trimmed();
|
||||||
|
const auto entry = dir.isEmpty() ? label : label + '=' + QDir(dir).absolutePath();
|
||||||
|
if (!Common::ParseAddContent(entry.toStdString())) {
|
||||||
|
m_status->setText(tr("Enter a 16-character entitlement label (A-Z, 0-9) and an existing "
|
||||||
|
"data folder, or no folder for a license only."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_entries.removeIf([&](const QString& e) { return e.section('=', 0, 0) == label; });
|
||||||
|
m_entries.append(entry);
|
||||||
|
m_save(m_entries);
|
||||||
|
m_label_edit->clear();
|
||||||
|
m_dir_edit->clear();
|
||||||
|
Scan();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AdditionalContentDialog::Remove() {
|
||||||
|
m_entries.removeOne(m_list->currentItem()->data(Qt::UserRole).toString());
|
||||||
|
m_save(m_entries);
|
||||||
|
Scan();
|
||||||
|
}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ constexpr char CONF_LAUNCHER[] = "Launcher";
|
|||||||
constexpr char CONF_GAME_DIR[] = "game_dir";
|
constexpr char CONF_GAME_DIR[] = "game_dir";
|
||||||
constexpr char CONF_GAME_DIRS[] = "game_dirs";
|
constexpr char CONF_GAME_DIRS[] = "game_dirs";
|
||||||
constexpr char CONF_ADDONS_DIR[] = "addons_dir";
|
constexpr char CONF_ADDONS_DIR[] = "addons_dir";
|
||||||
|
constexpr char CONF_ADDCONT[] = "AdditionalContent";
|
||||||
constexpr char CONF_GLOBAL[] = "GlobalConfiguration";
|
constexpr char CONF_GLOBAL[] = "GlobalConfiguration";
|
||||||
constexpr char SAVE_DATA_DIR[] = "_SaveData";
|
constexpr char SAVE_DATA_DIR[] = "_SaveData";
|
||||||
|
|
||||||
@@ -348,6 +349,13 @@ void ConfigurationListWidget::WriteSettings() {
|
|||||||
s->remove(CONF_GAME_DIR);
|
s->remove(CONF_GAME_DIR);
|
||||||
s->endGroup();
|
s->endGroup();
|
||||||
|
|
||||||
|
s->remove(CONF_ADDCONT);
|
||||||
|
s->beginGroup(CONF_ADDCONT);
|
||||||
|
for (auto it = m_addcont.constBegin(); it != m_addcont.constEnd(); ++it) {
|
||||||
|
s->setValue(it.key(), it.value());
|
||||||
|
}
|
||||||
|
s->endGroup();
|
||||||
|
|
||||||
s->remove(CONF_GLOBAL);
|
s->remove(CONF_GLOBAL);
|
||||||
s->beginGroup(CONF_GLOBAL);
|
s->beginGroup(CONF_GLOBAL);
|
||||||
m_global_info.WriteSettings(s.get());
|
m_global_info.WriteSettings(s.get());
|
||||||
@@ -395,6 +403,13 @@ void ConfigurationListWidget::ReadSettings() {
|
|||||||
m_addons_dir = NormalizeGameDirectory(s->value(CONF_ADDONS_DIR).toString());
|
m_addons_dir = NormalizeGameDirectory(s->value(CONF_ADDONS_DIR).toString());
|
||||||
s->endGroup();
|
s->endGroup();
|
||||||
|
|
||||||
|
m_addcont.clear();
|
||||||
|
s->beginGroup(CONF_ADDCONT);
|
||||||
|
for (const auto& title_id: s->childKeys()) {
|
||||||
|
m_addcont.insert(title_id, SettingsStringList(s->value(title_id)));
|
||||||
|
}
|
||||||
|
s->endGroup();
|
||||||
|
|
||||||
s->beginGroup(CONF_GLOBAL);
|
s->beginGroup(CONF_GLOBAL);
|
||||||
if (!s->childKeys().isEmpty()) {
|
if (!s->childKeys().isEmpty()) {
|
||||||
m_global_info.ReadSettings(s.get());
|
m_global_info.ReadSettings(s.get());
|
||||||
@@ -458,6 +473,7 @@ ConfigurationListWidget::CreateConfiguration(const ConfigurationItem& item) cons
|
|||||||
}
|
}
|
||||||
info->host_input_mapping = m_global_info.host_input_mapping;
|
info->host_input_mapping = m_global_info.host_input_mapping;
|
||||||
info->addons_dir = m_addons_dir;
|
info->addons_dir = m_addons_dir;
|
||||||
|
info->addcont = m_addcont.value(item.GetInfo().title_id.trimmed().toUpper());
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1273,11 +1289,22 @@ void ConfigurationListWidget::ShowContextMenu(QTreeWidgetItem* witem) {
|
|||||||
connect(action_addcont, &QAction::triggered, this,
|
connect(action_addcont, &QAction::triggered, this,
|
||||||
[this, item = QPointer<ConfigurationItem>(item)]() {
|
[this, item = QPointer<ConfigurationItem>(item)]() {
|
||||||
if (item != nullptr) {
|
if (item != nullptr) {
|
||||||
auto* dialog = new AdditionalContentDialog(item->GetInfo(), m_addons_dir, this);
|
const auto title_id = item->GetInfo().title_id.trimmed().toUpper();
|
||||||
|
auto* dialog = new AdditionalContentDialog(
|
||||||
|
item->GetInfo(), m_addons_dir, m_addcont.value(title_id),
|
||||||
|
[this, title_id](const QStringList& entries) {
|
||||||
|
if (entries.isEmpty()) {
|
||||||
|
m_addcont.remove(title_id);
|
||||||
|
} else {
|
||||||
|
m_addcont.insert(title_id, entries);
|
||||||
|
}
|
||||||
|
WriteSettings();
|
||||||
|
},
|
||||||
|
this);
|
||||||
dialog->show();
|
dialog->show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
action_addcont->setEnabled(item != nullptr);
|
action_addcont->setEnabled(item != nullptr && !item->GetInfo().title_id.trimmed().isEmpty());
|
||||||
QAction* action_compress =
|
QAction* action_compress =
|
||||||
menu.addAction(tr("Compress to ZAR..."), this, SLOT(compress_game()));
|
menu.addAction(tr("Compress to ZAR..."), this, SLOT(compress_game()));
|
||||||
QAction* action_remove_save_data =
|
QAction* action_remove_save_data =
|
||||||
|
|||||||
@@ -305,6 +305,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
|
|||||||
if (!info.addons_dir.isEmpty()) {
|
if (!info.addons_dir.isEmpty()) {
|
||||||
args << "--addcont-dir" << info.addons_dir;
|
args << "--addcont-dir" << info.addons_dir;
|
||||||
}
|
}
|
||||||
|
for (const auto& entry: info.addcont) {
|
||||||
|
args << "--addcont" << entry;
|
||||||
|
}
|
||||||
|
|
||||||
const auto patch_plan = Cheats::PlanPath(info.title_id);
|
const auto patch_plan = Cheats::PlanPath(info.title_id);
|
||||||
if (QFileInfo::exists(patch_plan)) {
|
if (QFileInfo::exists(patch_plan)) {
|
||||||
|
|||||||
@@ -172,8 +172,9 @@ MakeAddcontInfo(const Loader::AddContentEntry& entry) {
|
|||||||
NpEntitlementAccessAddcontEntitlementInfo info {};
|
NpEntitlementAccessAddcontEntitlementInfo info {};
|
||||||
std::memcpy(info.entitlement_label.data, entry.entitlement_label.data(),
|
std::memcpy(info.entitlement_label.data, entry.entitlement_label.data(),
|
||||||
entry.entitlement_label.size());
|
entry.entitlement_label.size());
|
||||||
info.package_type = 2; // Additional content with extra data.
|
const bool extra_data = !entry.mount_dir.empty();
|
||||||
info.download_status = 4; // Installed.
|
info.package_type = extra_data ? 2 : 3; // PSAC (with extra data) or PSAL (license only).
|
||||||
|
info.download_status = extra_data ? 4 : 0; // INSTALLED or NO_EXTRA_DATA.
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,24 @@ namespace Loader {
|
|||||||
|
|
||||||
static std::vector<AddContentEntry> g_add_content;
|
static std::vector<AddContentEntry> g_add_content;
|
||||||
|
|
||||||
void AddContentLoad(const std::filesystem::path& dir, const std::string& title_id) {
|
static auto FindLabel(std::string_view label) {
|
||||||
|
return std::find_if(g_add_content.begin(), g_add_content.end(),
|
||||||
|
[&](const auto& entry) { return entry.entitlement_label == label; });
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddContentLoad(const std::filesystem::path& dir, const std::vector<AddContentEntry>& user,
|
||||||
|
const std::string& title_id) {
|
||||||
g_add_content = Common::FindAddContent(dir, title_id);
|
g_add_content = Common::FindAddContent(dir, title_id);
|
||||||
|
for (const auto& entry: user) {
|
||||||
|
const auto it = FindLabel(entry.entitlement_label);
|
||||||
|
if (it == g_add_content.end()) {
|
||||||
|
g_add_content.push_back(entry);
|
||||||
|
} else {
|
||||||
|
*it = entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const auto& entry: g_add_content) {
|
for (const auto& entry: g_add_content) {
|
||||||
LOGF("AddContent: registered %s -> %s\n", entry.content_id.c_str(),
|
LOGF("AddContent: registered %s -> %s\n", entry.entitlement_label.c_str(),
|
||||||
Common::PathToString(entry.mount_dir).c_str());
|
Common::PathToString(entry.mount_dir).c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,10 +40,7 @@ const AddContentEntry* AddContentFind(const char* label, size_t label_size) {
|
|||||||
if (label == nullptr || label_size == 0) {
|
if (label == nullptr || label_size == 0) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
const std::string_view value(label, std::find(label, label + label_size, '\0') - label);
|
const auto it = FindLabel({label, std::find(label, label + label_size, '\0')});
|
||||||
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;
|
return it == g_add_content.end() ? nullptr : &*it;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ namespace Loader {
|
|||||||
|
|
||||||
using AddContentEntry = Common::AddContentEntry;
|
using AddContentEntry = Common::AddContentEntry;
|
||||||
|
|
||||||
void AddContentLoad(const std::filesystem::path& dir, const std::string& title_id);
|
// User entries replace detected packages with the same label.
|
||||||
|
void AddContentLoad(const std::filesystem::path& dir, const std::vector<AddContentEntry>& user,
|
||||||
|
const std::string& title_id);
|
||||||
const std::vector<AddContentEntry>& AddContentGetAll();
|
const std::vector<AddContentEntry>& AddContentGetAll();
|
||||||
const AddContentEntry* AddContentFind(const char* label, size_t label_size);
|
const AddContentEntry* AddContentFind(const char* label, size_t label_size);
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ static void PrintUsage() {
|
|||||||
::printf(" --game <dir|elf|zar> Game directory, ELF, or ZArchive to load.\n");
|
::printf(" --game <dir|elf|zar> Game directory, ELF, or ZArchive to load.\n");
|
||||||
::printf(" --game-patch <json> ETAHen cheat file.\n");
|
::printf(" --game-patch <json> ETAHen cheat file.\n");
|
||||||
::printf(" --addcont-dir <dir> Add-ons (DLC) folder (default: none).\n");
|
::printf(" --addcont-dir <dir> Add-ons (DLC) folder (default: none).\n");
|
||||||
|
::printf(
|
||||||
|
" --addcont <label>[=<dir>] Add-on (DLC) by entitlement label with its data in\n"
|
||||||
|
" <dir>, or license only without <dir>. Repeatable.\n");
|
||||||
::printf(" --screen-width <num> Window width. Default: 1280.\n");
|
::printf(" --screen-width <num> Window width. Default: 1280.\n");
|
||||||
::printf(" --screen-height <num> Window height. Default: 720.\n");
|
::printf(" --screen-height <num> Window height. Default: 720.\n");
|
||||||
::printf(
|
::printf(
|
||||||
@@ -303,6 +306,14 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
options.addcont_dir = Common::PathFromUtf8(Common::FixFilenameSlash(value));
|
options.addcont_dir = Common::PathFromUtf8(Common::FixFilenameSlash(value));
|
||||||
|
} else if (arg == "--addcont") {
|
||||||
|
auto entry = Common::ParseAddContent(value);
|
||||||
|
if (!entry) {
|
||||||
|
::printf("--addcont needs <16-character A-Z/0-9 label>[=<existing folder>]: %s\n",
|
||||||
|
value.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
options.addcont.push_back(std::move(*entry));
|
||||||
} else if (arg == "--screen-width") {
|
} else if (arg == "--screen-width") {
|
||||||
if (!ParseUint32(value, options.config.screen_width) ||
|
if (!ParseUint32(value, options.config.screen_width) ||
|
||||||
options.config.screen_width == 0) {
|
options.config.screen_width == 0) {
|
||||||
|
|||||||
@@ -506,9 +506,9 @@ void TestAddContent(const std::filesystem::path& root) {
|
|||||||
write(folder / "duplicate" / "sce_sys" / "param.json", param("PPSA11111", label(1)));
|
write(folder / "duplicate" / "sce_sys" / "param.json", param("PPSA11111", label(1)));
|
||||||
write(folder / label(0) / "payload.txt", "DLC payload");
|
write(folder / label(0) / "payload.txt", "DLC payload");
|
||||||
|
|
||||||
Loader::AddContentLoad(folder, "");
|
Loader::AddContentLoad(folder, {}, "");
|
||||||
Check(Loader::AddContentGetAll().empty(), "unknown title never exposes unrelated add-ons");
|
Check(Loader::AddContentGetAll().empty(), "unknown title never exposes unrelated add-ons");
|
||||||
Loader::AddContentLoad(folder, "PPSA11111");
|
Loader::AddContentLoad(folder, {}, "PPSA11111");
|
||||||
Check(Loader::AddContentGetAll().size() == 65,
|
Check(Loader::AddContentGetAll().size() == 65,
|
||||||
"scan filters title before deduplication and ignores invalid/non-package metadata");
|
"scan filters title before deduplication and ignores invalid/non-package metadata");
|
||||||
|
|
||||||
@@ -603,7 +603,39 @@ void TestAddContent(const std::filesystem::path& root) {
|
|||||||
for (size_t i = 1; i < mounts.size(); ++i) {
|
for (size_t i = 1; i < mounts.size(); ++i) {
|
||||||
Check(unmount(&mounts[i]) == OK, "unmount fixture");
|
Check(unmount(&mounts[i]) == OK, "unmount fixture");
|
||||||
}
|
}
|
||||||
Loader::AddContentLoad(folder / "missing", "PPSA11111");
|
|
||||||
|
const auto user_dir = root / "user-add-on";
|
||||||
|
write(user_dir / "payload.txt", "user payload");
|
||||||
|
const auto user_arg = label(0) + "=" + Common::PathToString(user_dir);
|
||||||
|
Check(!Common::ParseAddContent("SHORT") && !Common::ParseAddContent("lowercase0000000") &&
|
||||||
|
!Common::ParseAddContent("LICENSE0000000001") &&
|
||||||
|
!Common::ParseAddContent(user_arg + "/missing"),
|
||||||
|
"user add-on needs a 16-character A-Z/0-9 label and an existing folder");
|
||||||
|
const auto licensed = Common::ParseAddContent("LICENSE000000001");
|
||||||
|
const auto replaced = Common::ParseAddContent(user_arg);
|
||||||
|
const auto duplicate =
|
||||||
|
Common::ParseAddContent("LICENSE000000001=" + Common::PathToString(user_dir));
|
||||||
|
Check(licensed && replaced && duplicate, "user add-ons parse with and without extra data");
|
||||||
|
Loader::AddContentLoad(folder, {*duplicate, *licensed, *replaced}, "PPSA11111");
|
||||||
|
Check(list(0, nullptr, 0, &count) == OK && count == 69,
|
||||||
|
"user add-ons replace detected labels and append new ones");
|
||||||
|
Label user {};
|
||||||
|
std::memcpy(user.data, "LICENSE000000001", 17);
|
||||||
|
Check(get(0, &user, &single) == OK && single.package_type == 3 && single.download_status == 0 &&
|
||||||
|
mount(0, &user, &point) == static_cast<int>(0x80d90005u),
|
||||||
|
"last entry of a label wins and a license-only add-on has no extra data to mount");
|
||||||
|
Check(mount(0, &info[3].label, &point) == OK, "user add-on mounts its extra data");
|
||||||
|
const std::string user_file = std::string(point.data) + "/payload.txt";
|
||||||
|
const int user_fd = FileSystem::KernelOpen(user_file.c_str(), 0, 0);
|
||||||
|
std::array<char, 12> user_payload {};
|
||||||
|
Check(user_fd >= 3 &&
|
||||||
|
FileSystem::KernelRead(user_fd, user_payload.data(), user_payload.size()) ==
|
||||||
|
user_payload.size() &&
|
||||||
|
std::string_view(user_payload.data(), user_payload.size()) == "user payload" &&
|
||||||
|
FileSystem::KernelClose(user_fd) == OK && unmount(&point) == OK,
|
||||||
|
"user add-on replaces the detected folder of its label");
|
||||||
|
|
||||||
|
Loader::AddContentLoad(folder / "missing", {}, "PPSA11111");
|
||||||
Check(list(0, info.data(), 4, &count) == OK && count == 3,
|
Check(list(0, info.data(), 4, &count) == OK && count == 3,
|
||||||
"missing add-ons folder retains GTA V entitlements");
|
"missing add-ons folder retains GTA V entitlements");
|
||||||
for (size_t i = 0; i < existing.size(); ++i) {
|
for (size_t i = 0; i < existing.size(); ++i) {
|
||||||
|
|||||||
Reference in New Issue
Block a user