[qt_common] reorg + checkstate abstraction (#2735)

no diff. in functionality, just confirm builds on ubuntu 24.04/debian 12

Signed-off-by: crueter <crueter@eden-emu.dev>

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2735
This commit is contained in:
crueter 2025-10-15 05:05:03 +02:00
parent 5f9dba40a0
commit fff8e2026f
No known key found for this signature in database
GPG key ID: 425ACD2D4830EBC6
77 changed files with 174 additions and 129 deletions

View file

@ -0,0 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "qt_common/util/applet.h"

View file

@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef QT_APPLET_UTIL_H
#define QT_APPLET_UTIL_H
// TODO
namespace QtCommon::Applets {
}
#endif // QT_APPLET_UTIL_H

View file

@ -0,0 +1,354 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "compress.h"
#include "quazipfileinfo.h"
#include <QDirIterator>
/** This is a modified version of JlCompress **/
namespace QtCommon::Compress {
bool compressDir(QString fileCompressed,
QString dir,
const JlCompress::Options &options,
QtCommon::QtProgressCallback callback)
{
// Create zip
QuaZip zip(fileCompressed);
QDir().mkpath(QFileInfo(fileCompressed).absolutePath());
if (!zip.open(QuaZip::mdCreate)) {
QFile::remove(fileCompressed);
return false;
}
// See how big the overall fs structure is
// good approx. of total progress
// TODO(crueter): QDirListing impl... or fs::recursive_dir_iterator
QDirIterator iter(dir,
QDir::NoDotAndDotDot | QDir::Hidden | QDir::Files,
QDirIterator::Subdirectories);
std::size_t total = 0;
while (iter.hasNext()) {
total += iter.nextFileInfo().size();
}
std::size_t progress = 0;
callback(total, progress);
// Add the files and subdirectories
if (!compressSubDir(&zip, dir, dir, options, total, progress, callback)) {
QFile::remove(fileCompressed);
return false;
}
// Close zip
zip.close();
if (zip.getZipError() != 0) {
QFile::remove(fileCompressed);
return false;
}
return true;
}
bool compressSubDir(QuaZip *zip,
QString dir,
QString origDir,
const JlCompress::Options &options,
std::size_t total,
std::size_t &progress,
QtProgressCallback callback)
{
// zip: object where to add the file
// dir: current real directory
// origDir: original real directory
// (path(dir)-path(origDir)) = path inside the zip object
if (!zip)
return false;
if (zip->getMode() != QuaZip::mdCreate && zip->getMode() != QuaZip::mdAppend
&& zip->getMode() != QuaZip::mdAdd)
return false;
QDir directory(dir);
if (!directory.exists())
return false;
QDir origDirectory(origDir);
if (dir != origDir) {
QuaZipFile dirZipFile(zip);
std::unique_ptr<QuaZipNewInfo> qzni;
if (options.getDateTime().isNull()) {
qzni = std::make_unique<QuaZipNewInfo>(origDirectory.relativeFilePath(dir)
+ QLatin1String("/"),
dir);
} else {
qzni = std::make_unique<QuaZipNewInfo>(origDirectory.relativeFilePath(dir)
+ QLatin1String("/"),
dir,
options.getDateTime());
}
if (!dirZipFile.open(QIODevice::WriteOnly, *qzni, nullptr, 0, 0)) {
return false;
}
dirZipFile.close();
}
// For each subfolder
QFileInfoList subfiles = directory.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot
| QDir::Hidden | QDir::Dirs);
for (const auto &file : std::as_const(subfiles)) {
if (!compressSubDir(
zip, file.absoluteFilePath(), origDir, options, total, progress, callback)) {
return false;
}
}
// For each file in directory
QFileInfoList files = directory.entryInfoList(QDir::Hidden | QDir::Files);
for (const auto &file : std::as_const(files)) {
// If it's not a file or it's the compressed file being created
if (!file.isFile() || file.absoluteFilePath() == zip->getZipName())
continue;
// Create relative name for the compressed file
QString filename = origDirectory.relativeFilePath(file.absoluteFilePath());
// Compress the file
if (!compressFile(zip, file.absoluteFilePath(), filename, options, total, progress, callback)) {
return false;
}
}
return true;
}
bool compressFile(QuaZip *zip,
QString fileName,
QString fileDest,
const JlCompress::Options &options,
std::size_t total,
std::size_t &progress,
QtCommon::QtProgressCallback callback)
{
// zip: object where to add the file
// fileName: real file name
// fileDest: file name inside the zip object
if (!zip)
return false;
if (zip->getMode() != QuaZip::mdCreate && zip->getMode() != QuaZip::mdAppend
&& zip->getMode() != QuaZip::mdAdd)
return false;
QuaZipFile outFile(zip);
if (options.getDateTime().isNull()) {
if (!outFile.open(QIODevice::WriteOnly,
QuaZipNewInfo(fileDest, fileName),
nullptr,
0,
options.getCompressionMethod(),
options.getCompressionLevel()))
return false;
} else {
if (!outFile.open(QIODevice::WriteOnly,
QuaZipNewInfo(fileDest, fileName, options.getDateTime()),
nullptr,
0,
options.getCompressionMethod(),
options.getCompressionLevel()))
return false;
}
QFileInfo input(fileName);
if (quazip_is_symlink(input)) {
// Not sure if we should use any specialized codecs here.
// After all, a symlink IS just a byte array. And
// this is mostly for Linux, where UTF-8 is ubiquitous these days.
QString path = quazip_symlink_target(input);
QString relativePath = input.dir().relativeFilePath(path);
outFile.write(QFile::encodeName(relativePath));
} else {
QFile inFile;
inFile.setFileName(fileName);
if (!inFile.open(QIODevice::ReadOnly)) {
return false;
}
if (!copyData(inFile, outFile, total, progress, callback) || outFile.getZipError() != UNZ_OK) {
return false;
}
inFile.close();
}
outFile.close();
return outFile.getZipError() == UNZ_OK;
}
bool copyData(QIODevice &inFile,
QIODevice &outFile,
std::size_t total,
std::size_t &progress,
QtProgressCallback callback)
{
while (!inFile.atEnd()) {
char buf[4096];
qint64 readLen = inFile.read(buf, 4096);
if (readLen <= 0)
return false;
if (outFile.write(buf, readLen) != readLen)
return false;
progress += readLen;
if (!callback(total, progress)) {
return false;
}
}
return true;
}
QStringList extractDir(QString fileCompressed, QString dir, QtCommon::QtProgressCallback callback)
{
// Open zip
QuaZip zip(fileCompressed);
return extractDir(zip, dir, callback);
}
QStringList extractDir(QuaZip &zip, const QString &dir, QtCommon::QtProgressCallback callback)
{
if (!zip.open(QuaZip::mdUnzip)) {
return QStringList();
}
QString cleanDir = QDir::cleanPath(dir);
QDir directory(cleanDir);
QString absCleanDir = directory.absolutePath();
if (!absCleanDir.endsWith(QLatin1Char('/'))) // It only ends with / if it's the FS root.
absCleanDir += QLatin1Char('/');
QStringList extracted;
if (!zip.goToFirstFile()) {
return QStringList();
}
std::size_t total = 0;
for (const QuaZipFileInfo64 &info : zip.getFileInfoList64()) {
total += info.uncompressedSize;
}
std::size_t progress = 0;
callback(total, progress);
do {
QString name = zip.getCurrentFileName();
QString absFilePath = directory.absoluteFilePath(name);
QString absCleanPath = QDir::cleanPath(absFilePath);
if (!absCleanPath.startsWith(absCleanDir))
continue;
if (!extractFile(&zip, QLatin1String(""), absFilePath, total, progress, callback)) {
removeFile(extracted);
return QStringList();
}
extracted.append(absFilePath);
} while (zip.goToNextFile());
// Close zip
zip.close();
if (zip.getZipError() != 0) {
removeFile(extracted);
return QStringList();
}
return extracted;
}
bool extractFile(QuaZip *zip,
QString fileName,
QString fileDest,
std::size_t total,
std::size_t &progress,
QtCommon::QtProgressCallback callback)
{
// zip: object where to add the file
// filename: real file name
// fileincompress: file name of the compressed file
if (!zip)
return false;
if (zip->getMode() != QuaZip::mdUnzip)
return false;
if (!fileName.isEmpty())
zip->setCurrentFile(fileName);
QuaZipFile inFile(zip);
if (!inFile.open(QIODevice::ReadOnly) || inFile.getZipError() != UNZ_OK)
return false;
// Check existence of resulting file
QDir curDir;
if (fileDest.endsWith(QLatin1String("/"))) {
if (!curDir.mkpath(fileDest)) {
return false;
}
} else {
if (!curDir.mkpath(QFileInfo(fileDest).absolutePath())) {
return false;
}
}
QuaZipFileInfo64 info;
if (!zip->getCurrentFileInfo(&info))
return false;
QFile::Permissions srcPerm = info.getPermissions();
if (fileDest.endsWith(QLatin1String("/")) && QFileInfo(fileDest).isDir()) {
if (srcPerm != 0) {
QFile(fileDest).setPermissions(srcPerm);
}
return true;
}
if (info.isSymbolicLink()) {
QString target = QFile::decodeName(inFile.readAll());
return QFile::link(target, fileDest);
}
// Open resulting file
QFile outFile;
outFile.setFileName(fileDest);
if (!outFile.open(QIODevice::WriteOnly))
return false;
// Copy data
if (!copyData(inFile, outFile, total, progress, callback) || inFile.getZipError() != UNZ_OK) {
outFile.close();
removeFile(QStringList(fileDest));
return false;
}
outFile.close();
// Close file
inFile.close();
if (inFile.getZipError() != UNZ_OK) {
removeFile(QStringList(fileDest));
return false;
}
if (srcPerm != 0) {
outFile.setPermissions(srcPerm);
}
return true;
}
bool removeFile(QStringList listFile)
{
bool ret = true;
// For each file
for (int i = 0; i < listFile.count(); i++) {
// Remove
ret = ret && QFile::remove(listFile.at(i));
}
return ret;
}
} // namespace QtCommon::Compress

View file

@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QDir>
#include <QString>
#include <JlCompress.h>
#include "qt_common/qt_common.h"
/** This is a modified version of JlCompress **/
namespace QtCommon::Compress {
/**
* @brief Compress an entire directory and report its progress.
* @param fileCompressed Destination file
* @param dir The directory to compress
* @param options Compression level, etc
* @param callback Callback that takes in two std::size_t (total, progress) and returns false if the current operation should be cancelled.
*/
bool compressDir(QString fileCompressed,
QString dir,
const JlCompress::Options& options = JlCompress::Options(),
QtCommon::QtProgressCallback callback = {});
// Internal //
bool compressSubDir(QuaZip *zip,
QString dir,
QString origDir,
const JlCompress::Options &options,
std::size_t total,
std::size_t &progress,
QtCommon::QtProgressCallback callback);
bool compressFile(QuaZip *zip,
QString fileName,
QString fileDest,
const JlCompress::Options &options,
std::size_t total,
std::size_t &progress,
QtCommon::QtProgressCallback callback);
bool copyData(QIODevice &inFile,
QIODevice &outFile,
std::size_t total,
std::size_t &progress,
QtCommon::QtProgressCallback callback);
// Extract //
/**
* @brief Extract a zip file and report its progress.
* @param fileCompressed Compressed file
* @param dir The directory to push the results to
* @param callback Callback that takes in two std::size_t (total, progress) and returns false if the current operation should be cancelled.
*/
QStringList extractDir(QString fileCompressed, QString dir, QtCommon::QtProgressCallback callback = {});
// Internal //
QStringList extractDir(QuaZip &zip, const QString &dir, QtCommon::QtProgressCallback callback);
bool extractFile(QuaZip *zip,
QString fileName,
QString fileDest,
std::size_t total,
std::size_t &progress,
QtCommon::QtProgressCallback callback);
bool removeFile(QStringList listFile);
}

View file

@ -0,0 +1,544 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "qt_common/util/content.h"
#include "qt_common/util/game.h"
#include "common/fs/fs.h"
#include "core/hle/service/acc/profile_manager.h"
#include "frontend_common/content_manager.h"
#include "frontend_common/data_manager.h"
#include "frontend_common/firmware_manager.h"
#include "qt_common/qt_common.h"
#include "compress.h"
#include "qt_common/abstract/qt_progress_dialog.h"
#include "qt_common/abstract/qt_frontend_util.h"
#include <QFuture>
#include <QtConcurrentRun>
#include <JlCompress.h>
#include <QFutureWatcher>
namespace QtCommon::Content {
bool CheckGameFirmware(u64 program_id, QObject* parent)
{
if (FirmwareManager::GameRequiresFirmware(program_id)
&& !FirmwareManager::CheckFirmwarePresence(*system)) {
auto result = QtCommon::Frontend::ShowMessage(
QMessageBox::Warning,
tr("Game Requires Firmware"),
tr("The game you are trying to launch requires firmware to boot or to get past the "
"opening menu. Please <a href='https://yuzu-mirror.github.io/help/quickstart'>"
"dump and install firmware</a>, or press \"OK\" to launch anyways."),
QMessageBox::Ok | QMessageBox::Cancel,
parent);
return result == QMessageBox::Ok;
}
return true;
}
void InstallFirmware(const QString& location, bool recursive)
{
QtCommon::Frontend::QtProgressDialog progress(tr("Installing Firmware..."),
tr("Cancel"),
0,
100,
rootObject);
progress.setWindowModality(Qt::WindowModal);
progress.setMinimumDuration(100);
progress.setAutoClose(false);
progress.setAutoReset(false);
progress.show();
// Declare progress callback.
auto callback = [&](size_t total_size, size_t processed_size) {
progress.setValue(static_cast<int>((processed_size * 100) / total_size));
return progress.wasCanceled();
};
static constexpr const char* failedTitle = "Firmware Install Failed";
static constexpr const char* successTitle = "Firmware Install Succeeded";
QMessageBox::Icon icon;
FirmwareInstallResult result;
const auto ShowMessage = [&]() {
QtCommon::Frontend::ShowMessage(icon,
tr(failedTitle),
tr(GetFirmwareInstallResultString(result)));
};
LOG_INFO(Frontend, "Installing firmware from {}", location.toStdString());
// Check for a reasonable number of .nca files (don't hardcode them, just see if there's some in
// there.)
std::filesystem::path firmware_source_path = location.toStdString();
if (!Common::FS::IsDir(firmware_source_path)) {
return;
}
std::vector<std::filesystem::path> out;
const Common::FS::DirEntryCallable dir_callback =
[&out](const std::filesystem::directory_entry& entry) {
if (entry.path().has_extension() && entry.path().extension() == ".nca") {
out.emplace_back(entry.path());
}
return true;
};
callback(100, 10);
if (recursive) {
Common::FS::IterateDirEntriesRecursively(firmware_source_path,
dir_callback,
Common::FS::DirEntryFilter::File);
} else {
Common::FS::IterateDirEntries(firmware_source_path,
dir_callback,
Common::FS::DirEntryFilter::File);
}
if (out.size() <= 0) {
result = FirmwareInstallResult::NoNCAs;
icon = QMessageBox::Warning;
ShowMessage();
return;
}
// Locate and erase the content of nand/system/Content/registered/*.nca, if any.
auto sysnand_content_vdir = system->GetFileSystemController().GetSystemNANDContentDirectory();
if (sysnand_content_vdir->IsWritable()
&& !sysnand_content_vdir->CleanSubdirectoryRecursive("registered")) {
result = FirmwareInstallResult::FailedDelete;
icon = QMessageBox::Critical;
ShowMessage();
return;
}
LOG_INFO(Frontend,
"Cleaned nand/system/Content/registered folder in preparation for new firmware.");
callback(100, 20);
auto firmware_vdir = sysnand_content_vdir->GetDirectoryRelative("registered");
bool success = true;
int i = 0;
for (const auto& firmware_src_path : out) {
i++;
auto firmware_src_vfile = vfs->OpenFile(firmware_src_path.generic_string(),
FileSys::OpenMode::Read);
auto firmware_dst_vfile = firmware_vdir->CreateFileRelative(
firmware_src_path.filename().string());
if (!VfsRawCopy(firmware_src_vfile, firmware_dst_vfile)) {
LOG_ERROR(Frontend,
"Failed to copy firmware file {} to {} in registered folder!",
firmware_src_path.generic_string(),
firmware_src_path.filename().string());
success = false;
}
if (callback(100, 20 + static_cast<int>(((i) / static_cast<float>(out.size())) * 70.0))) {
result = FirmwareInstallResult::FailedCorrupted;
icon = QMessageBox::Warning;
ShowMessage();
return;
}
}
if (!success) {
result = FirmwareInstallResult::FailedCopy;
icon = QMessageBox::Critical;
ShowMessage();
return;
}
// Re-scan VFS for the newly placed firmware files.
system->GetFileSystemController().CreateFactories(*vfs);
auto VerifyFirmwareCallback = [&](size_t total_size, size_t processed_size) {
progress.setValue(90 + static_cast<int>((processed_size * 10) / total_size));
return progress.wasCanceled();
};
auto results = ContentManager::VerifyInstalledContents(*QtCommon::system,
*QtCommon::provider,
VerifyFirmwareCallback,
true);
if (results.size() > 0) {
const auto failed_names = QString::fromStdString(
fmt::format("{}", fmt::join(results, "\n")));
progress.close();
QtCommon::Frontend::Critical(
tr("Firmware integrity verification failed!"),
tr("Verification failed for the following files:\n\n%1").arg(failed_names));
return;
}
progress.close();
const auto pair = FirmwareManager::GetFirmwareVersion(*system);
const auto firmware_data = pair.first;
const std::string display_version(firmware_data.display_version.data());
result = FirmwareInstallResult::Success;
QtCommon::Frontend::Information(
rootObject,
tr(successTitle),
tr(GetFirmwareInstallResultString(result)).arg(QString::fromStdString(display_version)));
}
QString UnzipFirmwareToTmp(const QString& location)
{
namespace fs = std::filesystem;
fs::path tmp{fs::temp_directory_path()};
if (!fs::create_directories(tmp / "eden" / "firmware")) {
return QString();
}
tmp /= "eden";
tmp /= "firmware";
QString qCacheDir = QString::fromStdString(tmp.string());
QFile zip(location);
// TODO(crueter): use QtCompress
QStringList result = JlCompress::extractDir(&zip, qCacheDir);
if (result.isEmpty()) {
return QString();
}
return qCacheDir;
}
// Content //
void VerifyGameContents(const std::string& game_path)
{
QtCommon::Frontend::QtProgressDialog progress(tr("Verifying integrity..."),
tr("Cancel"),
0,
100,
rootObject);
progress.setWindowModality(Qt::WindowModal);
progress.setMinimumDuration(100);
progress.setAutoClose(false);
progress.setAutoReset(false);
const auto callback = [&](size_t total_size, size_t processed_size) {
progress.setValue(static_cast<int>((processed_size * 100) / total_size));
return progress.wasCanceled();
};
const auto result = ContentManager::VerifyGameContents(*system, game_path, callback);
switch (result) {
case ContentManager::GameVerificationResult::Success:
QtCommon::Frontend::Information(rootObject,
tr("Integrity verification succeeded!"),
tr("The operation completed successfully."));
break;
case ContentManager::GameVerificationResult::Failed:
QtCommon::Frontend::Critical(rootObject,
tr("Integrity verification failed!"),
tr("File contents may be corrupt or missing."));
break;
case ContentManager::GameVerificationResult::NotImplemented:
QtCommon::Frontend::Warning(
rootObject,
tr("Integrity verification couldn't be performed"),
tr("Firmware installation cancelled, firmware may be in a bad state or corrupted. "
"File contents could not be checked for validity."));
}
}
void InstallKeys()
{
const QString key_source_location
= QtCommon::Frontend::GetOpenFileName(tr("Select Dumped Keys Location"),
{},
QStringLiteral("Decryption Keys (*.keys)"),
{},
QtCommon::Frontend::Option::ReadOnly);
if (key_source_location.isEmpty()) {
return;
}
FirmwareManager::KeyInstallResult result
= FirmwareManager::InstallKeys(key_source_location.toStdString(), "keys");
system->GetFileSystemController().CreateFactories(*QtCommon::vfs);
switch (result) {
case FirmwareManager::KeyInstallResult::Success:
QtCommon::Frontend::Information(tr("Decryption Keys install succeeded"),
tr("Decryption Keys were successfully installed"));
break;
default:
QtCommon::Frontend::Critical(tr("Decryption Keys install failed"),
tr(FirmwareManager::GetKeyInstallResultString(result)));
break;
}
}
void VerifyInstalledContents()
{
// Initialize a progress dialog.
QtCommon::Frontend::QtProgressDialog progress(tr("Verifying integrity..."),
tr("Cancel"),
0,
100,
QtCommon::rootObject);
progress.setWindowModality(Qt::WindowModal);
progress.setMinimumDuration(100);
progress.setAutoClose(false);
progress.setAutoReset(false);
// Declare progress callback.
auto QtProgressCallback = [&](size_t total_size, size_t processed_size) {
progress.setValue(static_cast<int>((processed_size * 100) / total_size));
return progress.wasCanceled();
};
const std::vector<std::string> result
= ContentManager::VerifyInstalledContents(*QtCommon::system,
*QtCommon::provider,
QtProgressCallback);
progress.close();
if (result.empty()) {
QtCommon::Frontend::Information(tr("Integrity verification succeeded!"),
tr("The operation completed successfully."));
} else {
const auto failed_names = QString::fromStdString(fmt::format("{}", fmt::join(result, "\n")));
QtCommon::Frontend::Critical(
tr("Integrity verification failed!"),
tr("Verification failed for the following files:\n\n%1").arg(failed_names));
}
}
void FixProfiles()
{
// Reset user save files after config is initialized and migration is done.
// Doing it at init time causes profiles to read from the wrong place entirely if NAND dir is not default
// TODO: better solution
system->GetProfileManager().ResetUserSaveFile();
std::vector<std::string> orphaned = system->GetProfileManager().FindOrphanedProfiles();
// no orphaned dirs--all good :)
if (orphaned.empty())
return;
// otherwise, let the user know
QString qorphaned;
// max. of 8 orphaned profiles is fair, I think
// 33 = 32 (UUID) + 1 (\n)
qorphaned.reserve(8 * 33);
for (const std::string& s : orphaned) {
qorphaned = qorphaned % QStringLiteral("\n") % QString::fromStdString(s);
}
QtCommon::Frontend::Critical(
tr("Orphaned Profiles Detected!"),
tr("UNEXPECTED BAD THINGS MAY HAPPEN IF YOU DON'T READ THIS!\n"
"Eden has detected the following save directories with no attached profile:\n"
"%1\n\n"
"Click \"OK\" to open your save folder and fix up your profiles.\n"
"Hint: copy the contents of the largest or last-modified folder elsewhere, "
"delete all orphaned profiles, and move your copied contents to the good profile.")
.arg(qorphaned));
QtCommon::Game::OpenSaveFolder();
}
void ClearDataDir(FrontendCommon::DataManager::DataDir dir, const std::string& user_id)
{
auto result = QtCommon::Frontend::Warning(tr("Really clear data?"),
tr("Important data may be lost!"),
QMessageBox::Yes | QMessageBox::No);
if (result != QMessageBox::Yes)
return;
result = QtCommon::Frontend::Warning(
tr("Are you REALLY sure?"),
tr("Once deleted, your data will NOT come back!\n"
"Only do this if you're 100% sure you want to delete this data."),
QMessageBox::Yes | QMessageBox::No);
if (result != QMessageBox::Yes)
return;
QtCommon::Frontend::QtProgressDialog dialog(tr("Clearing..."), QString(), 0, 0);
dialog.show();
FrontendCommon::DataManager::ClearDir(dir, user_id);
dialog.close();
}
void ExportDataDir(FrontendCommon::DataManager::DataDir data_dir,
const std::string& user_id,
const QString& name,
std::function<void()> callback)
{
using namespace QtCommon::Frontend;
const std::string dir = FrontendCommon::DataManager::GetDataDir(data_dir, user_id);
const QString zip_dump_location = GetSaveFileName(tr("Select Export Location"),
tr("%1.zip").arg(name),
tr("Zipped Archives (*.zip)"));
if (zip_dump_location.isEmpty())
return;
QtProgressDialog* progress = new QtProgressDialog(
tr("Exporting data. This may take a while..."), tr("Cancel"), 0, 100, rootObject);
progress->setWindowTitle(tr("Exporting"));
progress->setWindowModality(Qt::WindowModal);
progress->setMinimumDuration(100);
progress->setAutoClose(false);
progress->setAutoReset(false);
progress->show();
QGuiApplication::processEvents();
auto progress_callback = [=](size_t total_size, size_t processed_size) {
QMetaObject::invokeMethod(progress,
&QtProgressDialog::setValue,
static_cast<int>((processed_size * 100) / total_size));
return !progress->wasCanceled();
};
QFuture<bool> future = QtConcurrent::run([=]() {
return QtCommon::Compress::compressDir(zip_dump_location,
QString::fromStdString(dir),
JlCompress::Options(),
progress_callback);
});
QFutureWatcher<bool>* watcher = new QFutureWatcher<bool>(rootObject);
QObject::connect(watcher, &QFutureWatcher<bool>::finished, rootObject, [=]() {
progress->close();
if (watcher->result()) {
Information(tr("Exported Successfully"), tr("Data was exported successfully."));
} else if (progress->wasCanceled()) {
Information(tr("Export Cancelled"), tr("Export was cancelled by the user."));
} else {
Critical(
tr("Export Failed"),
tr("Ensure you have write permissions on the targeted directory and try again."));
}
progress->deleteLater();
watcher->deleteLater();
if (callback)
callback();
});
watcher->setFuture(future);
}
void ImportDataDir(FrontendCommon::DataManager::DataDir data_dir,
const std::string& user_id,
std::function<void()> callback)
{
const std::string dir = FrontendCommon::DataManager::GetDataDir(data_dir, user_id);
using namespace QtCommon::Frontend;
const QString zip_dump_location = GetOpenFileName(tr("Select Import Location"),
{},
tr("Zipped Archives (*.zip)"));
if (zip_dump_location.isEmpty())
return;
StandardButton button = Warning(
tr("Import Warning"),
tr("All previous data in this directory will be deleted. Are you sure you wish to "
"proceed?"),
StandardButton::Yes | StandardButton::No);
if (button != QMessageBox::Yes)
return;
QtProgressDialog* progress = new QtProgressDialog(
tr("Importing data. This may take a while..."), tr("Cancel"), 0, 100, rootObject);
progress->setWindowTitle(tr("Importing"));
progress->setWindowModality(Qt::WindowModal);
progress->setMinimumDuration(100);
progress->setAutoClose(false);
progress->setAutoReset(false);
progress->show();
progress->setValue(0);
QGuiApplication::processEvents();
// to prevent GUI mangling we have to run this in a thread as well
QFuture<bool> delete_future = QtConcurrent::run([=]() {
FrontendCommon::DataManager::ClearDir(data_dir, user_id);
return !progress->wasCanceled();
});
QFutureWatcher<bool>* delete_watcher = new QFutureWatcher<bool>(rootObject);
delete_watcher->setFuture(delete_future);
QObject::connect(delete_watcher, &QFutureWatcher<bool>::finished, rootObject, [=]() {
auto progress_callback = [=](size_t total_size, size_t processed_size) {
QMetaObject::invokeMethod(progress,
&QtProgressDialog::setValue,
static_cast<int>((processed_size * 100) / total_size));
return !progress->wasCanceled();
};
QFuture<bool> future = QtConcurrent::run([=]() {
return !QtCommon::Compress::extractDir(zip_dump_location,
QString::fromStdString(dir),
progress_callback)
.empty();
});
QFutureWatcher<bool>* watcher = new QFutureWatcher<bool>(rootObject);
QObject::connect(watcher, &QFutureWatcher<bool>::finished, rootObject, [=]() {
progress->close();
// this sucks
if (watcher->result()) {
Information(tr("Imported Successfully"), tr("Data was imported successfully."));
} else if (progress->wasCanceled()) {
Information(tr("Import Cancelled"), tr("Import was cancelled by the user."));
} else {
Critical(tr("Import Failed"),
tr("Ensure you have read permissions on the targeted directory and try "
"again."));
}
progress->deleteLater();
watcher->deleteLater();
if (callback)
callback();
});
watcher->setFuture(future);
});
}
} // namespace QtCommon::Content

View file

@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef QT_CONTENT_UTIL_H
#define QT_CONTENT_UTIL_H
#include <QObject>
#include "common/common_types.h"
#include "frontend_common/data_manager.h"
namespace QtCommon::Content {
//
bool CheckGameFirmware(u64 program_id, QObject *parent);
static constexpr std::array<const char *, 6> FIRMWARE_RESULTS
= {"Successfully installed firmware version %1",
"",
"Unable to locate potential firmware NCA files",
"Failed to delete one or more firmware files.",
"One or more firmware files failed to copy into NAND.",
"Firmware installation cancelled, firmware may be in a bad state or corrupted."
"Restart Eden or re-install firmware."};
enum class FirmwareInstallResult {
Success,
NoOp,
NoNCAs,
FailedDelete,
FailedCopy,
FailedCorrupted,
};
inline constexpr const char *GetFirmwareInstallResultString(FirmwareInstallResult result)
{
return FIRMWARE_RESULTS.at(static_cast<std::size_t>(result));
}
void InstallFirmware(const QString &location, bool recursive);
QString UnzipFirmwareToTmp(const QString &location);
// Keys //
void InstallKeys();
// Content //
void VerifyGameContents(const std::string &game_path);
void VerifyInstalledContents();
void ClearDataDir(FrontendCommon::DataManager::DataDir dir, const std::string &user_id = "");
void ExportDataDir(FrontendCommon::DataManager::DataDir dir,
const std::string &user_id = "",
const QString &name = QStringLiteral("export"),
std::function<void()> callback = {});
void ImportDataDir(FrontendCommon::DataManager::DataDir dir, const std::string &user_id = "", std::function<void()> callback = {});
// Profiles //
void FixProfiles();
}
#endif // QT_CONTENT_UTIL_H

583
src/qt_common/util/game.cpp Normal file
View file

@ -0,0 +1,583 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "qt_common/util/game.h"
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "core/file_sys/savedata_factory.h"
#include "core/hle/service/am/am_types.h"
#include "frontend_common/content_manager.h"
#include "qt_common/qt_common.h"
#include "qt_common/config/uisettings.h"
#include "qt_common/abstract/qt_frontend_util.h"
#include "yuzu/util/util.h"
#include <QDesktopServices>
#include <QStandardPaths>
#include <QUrl>
#ifdef _WIN32
#include "common/scope_exit.h"
#include "common/string_util.h"
#include <shlobj.h>
#include <windows.h>
#else
#include "fmt/ostream.h"
#include <fstream>
#endif
namespace QtCommon::Game {
bool CreateShortcutLink(const std::filesystem::path& shortcut_path,
const std::string& comment,
const std::filesystem::path& icon_path,
const std::filesystem::path& command,
const std::string& arguments,
const std::string& categories,
const std::string& keywords,
const std::string& name)
try {
#ifdef _WIN32 // Windows
HRESULT hr = CoInitialize(nullptr);
if (FAILED(hr)) {
LOG_ERROR(Frontend, "CoInitialize failed");
return false;
}
SCOPE_EXIT
{
CoUninitialize();
};
IShellLinkW* ps1 = nullptr;
IPersistFile* persist_file = nullptr;
SCOPE_EXIT
{
if (persist_file != nullptr) {
persist_file->Release();
}
if (ps1 != nullptr) {
ps1->Release();
}
};
HRESULT hres = CoCreateInstance(CLSID_ShellLink,
nullptr,
CLSCTX_INPROC_SERVER,
IID_IShellLinkW,
reinterpret_cast<void**>(&ps1));
if (FAILED(hres)) {
LOG_ERROR(Frontend, "Failed to create IShellLinkW instance");
return false;
}
hres = ps1->SetPath(command.c_str());
if (FAILED(hres)) {
LOG_ERROR(Frontend, "Failed to set path");
return false;
}
if (!arguments.empty()) {
hres = ps1->SetArguments(Common::UTF8ToUTF16W(arguments).data());
if (FAILED(hres)) {
LOG_ERROR(Frontend, "Failed to set arguments");
return false;
}
}
if (!comment.empty()) {
hres = ps1->SetDescription(Common::UTF8ToUTF16W(comment).data());
if (FAILED(hres)) {
LOG_ERROR(Frontend, "Failed to set description");
return false;
}
}
if (std::filesystem::is_regular_file(icon_path)) {
hres = ps1->SetIconLocation(icon_path.c_str(), 0);
if (FAILED(hres)) {
LOG_ERROR(Frontend, "Failed to set icon location");
return false;
}
}
hres = ps1->QueryInterface(IID_IPersistFile, reinterpret_cast<void**>(&persist_file));
if (FAILED(hres)) {
LOG_ERROR(Frontend, "Failed to get IPersistFile interface");
return false;
}
hres = persist_file->Save(std::filesystem::path{shortcut_path / (name + ".lnk")}.c_str(), TRUE);
if (FAILED(hres)) {
LOG_ERROR(Frontend, "Failed to save shortcut");
return false;
}
return true;
#elif defined(__unix__) && !defined(__APPLE__) && !defined(__ANDROID__) // Any desktop NIX
std::filesystem::path shortcut_path_full = shortcut_path / (name + ".desktop");
std::ofstream shortcut_stream(shortcut_path_full, std::ios::binary | std::ios::trunc);
if (!shortcut_stream.is_open()) {
LOG_ERROR(Frontend, "Failed to create shortcut");
return false;
}
// TODO: Migrate fmt::print to std::print in futures STD C++ 23.
fmt::print(shortcut_stream, "[Desktop Entry]\n");
fmt::print(shortcut_stream, "Type=Application\n");
fmt::print(shortcut_stream, "Version=1.0\n");
fmt::print(shortcut_stream, "Name={}\n", name);
if (!comment.empty()) {
fmt::print(shortcut_stream, "Comment={}\n", comment);
}
if (std::filesystem::is_regular_file(icon_path)) {
fmt::print(shortcut_stream, "Icon={}\n", icon_path.string());
}
fmt::print(shortcut_stream, "TryExec={}\n", command.string());
fmt::print(shortcut_stream, "Exec={} {}\n", command.string(), arguments);
if (!categories.empty()) {
fmt::print(shortcut_stream, "Categories={}\n", categories);
}
if (!keywords.empty()) {
fmt::print(shortcut_stream, "Keywords={}\n", keywords);
}
return true;
#else // Unsupported platform
return false;
#endif
} catch (const std::exception& e) {
LOG_ERROR(Frontend, "Failed to create shortcut: {}", e.what());
return false;
}
bool MakeShortcutIcoPath(const u64 program_id,
const std::string_view game_file_name,
std::filesystem::path& out_icon_path)
{
// Get path to Yuzu icons directory & icon extension
std::string ico_extension = "png";
#if defined(_WIN32)
out_icon_path = Common::FS::GetEdenPath(Common::FS::EdenPath::IconsDir);
ico_extension = "ico";
#elif defined(__linux__) || defined(__FreeBSD__)
out_icon_path = Common::FS::GetDataDirectory("XDG_DATA_HOME") / "icons/hicolor/256x256";
#endif
// Create icons directory if it doesn't exist
if (!Common::FS::CreateDirs(out_icon_path)) {
out_icon_path.clear();
return false;
}
// Create icon file path
out_icon_path /= (program_id == 0 ? fmt::format("eden-{}.{}", game_file_name, ico_extension)
: fmt::format("eden-{:016X}.{}", program_id, ico_extension));
return true;
}
void OpenEdenFolder(const Common::FS::EdenPath& path)
{
QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(Common::FS::GetEdenPathString(path))));
}
void OpenRootDataFolder()
{
OpenEdenFolder(Common::FS::EdenPath::EdenDir);
}
void OpenNANDFolder()
{
OpenEdenFolder(Common::FS::EdenPath::NANDDir);
}
void OpenSaveFolder()
{
const auto path = Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir) / "user/save/0000000000000000";
QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(path.string())));
}
void OpenSDMCFolder()
{
OpenEdenFolder(Common::FS::EdenPath::SDMCDir);
}
void OpenModFolder()
{
OpenEdenFolder(Common::FS::EdenPath::LoadDir);
}
void OpenLogFolder()
{
OpenEdenFolder(Common::FS::EdenPath::LogDir);
}
static QString GetGameListErrorRemoving(QtCommon::Game::InstalledEntryType type)
{
switch (type) {
case QtCommon::Game::InstalledEntryType::Game:
return tr("Error Removing Contents");
case QtCommon::Game::InstalledEntryType::Update:
return tr("Error Removing Update");
case QtCommon::Game::InstalledEntryType::AddOnContent:
return tr("Error Removing DLC");
default:
return QStringLiteral("Error Removing <Invalid Type>");
}
}
// Game Content //
void RemoveBaseContent(u64 program_id, InstalledEntryType type)
{
const auto res = ContentManager::RemoveBaseContent(system->GetFileSystemController(),
program_id);
if (res) {
QtCommon::Frontend::Information(rootObject,
tr("Successfully Removed"),
tr("Successfully removed the installed base game."));
} else {
QtCommon::Frontend::Warning(
rootObject,
GetGameListErrorRemoving(type),
tr("The base game is not installed in the NAND and cannot be removed."));
}
}
void RemoveUpdateContent(u64 program_id, InstalledEntryType type)
{
const auto res = ContentManager::RemoveUpdate(system->GetFileSystemController(), program_id);
if (res) {
QtCommon::Frontend::Information(rootObject,
tr("Successfully Removed"),
tr("Successfully removed the installed update."));
} else {
QtCommon::Frontend::Warning(rootObject,
GetGameListErrorRemoving(type),
tr("There is no update installed for this title."));
}
}
void RemoveAddOnContent(u64 program_id, InstalledEntryType type)
{
const size_t count = ContentManager::RemoveAllDLC(*system, program_id);
if (count == 0) {
QtCommon::Frontend::Warning(rootObject,
GetGameListErrorRemoving(type),
tr("There are no DLCs installed for this title."));
return;
}
QtCommon::Frontend::Information(rootObject,
tr("Successfully Removed"),
tr("Successfully removed %1 installed DLC.").arg(count));
}
// Global Content //
void RemoveTransferableShaderCache(u64 program_id, GameListRemoveTarget target)
{
const auto target_file_name = [target] {
switch (target) {
case GameListRemoveTarget::GlShaderCache:
return "opengl.bin";
case GameListRemoveTarget::VkShaderCache:
return "vulkan.bin";
default:
return "";
}
}();
const auto shader_cache_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::ShaderDir);
const auto shader_cache_folder_path = shader_cache_dir / fmt::format("{:016x}", program_id);
const auto target_file = shader_cache_folder_path / target_file_name;
if (!Common::FS::Exists(target_file)) {
QtCommon::Frontend::Warning(rootObject,
tr("Error Removing Transferable Shader Cache"),
tr("A shader cache for this title does not exist."));
return;
}
if (Common::FS::RemoveFile(target_file)) {
QtCommon::Frontend::Information(rootObject,
tr("Successfully Removed"),
tr("Successfully removed the transferable shader cache."));
} else {
QtCommon::Frontend::Warning(rootObject,
tr("Error Removing Transferable Shader Cache"),
tr("Failed to remove the transferable shader cache."));
}
}
void RemoveVulkanDriverPipelineCache(u64 program_id)
{
static constexpr std::string_view target_file_name = "vulkan_pipelines.bin";
const auto shader_cache_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::ShaderDir);
const auto shader_cache_folder_path = shader_cache_dir / fmt::format("{:016x}", program_id);
const auto target_file = shader_cache_folder_path / target_file_name;
if (!Common::FS::Exists(target_file)) {
return;
}
if (!Common::FS::RemoveFile(target_file)) {
QtCommon::Frontend::Warning(rootObject,
tr("Error Removing Vulkan Driver Pipeline Cache"),
tr("Failed to remove the driver pipeline cache."));
}
}
void RemoveAllTransferableShaderCaches(u64 program_id)
{
const auto shader_cache_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::ShaderDir);
const auto program_shader_cache_dir = shader_cache_dir / fmt::format("{:016x}", program_id);
if (!Common::FS::Exists(program_shader_cache_dir)) {
QtCommon::Frontend::Warning(rootObject,
tr("Error Removing Transferable Shader Caches"),
tr("A shader cache for this title does not exist."));
return;
}
if (Common::FS::RemoveDirRecursively(program_shader_cache_dir)) {
QtCommon::Frontend::Information(rootObject,
tr("Successfully Removed"),
tr("Successfully removed the transferable shader caches."));
} else {
QtCommon::Frontend::Warning(
rootObject,
tr("Error Removing Transferable Shader Caches"),
tr("Failed to remove the transferable shader cache directory."));
}
}
void RemoveCustomConfiguration(u64 program_id, const std::string& game_path)
{
const auto file_path = std::filesystem::path(Common::FS::ToU8String(game_path));
const auto config_file_name = program_id == 0
? Common::FS::PathToUTF8String(file_path.filename())
.append(".ini")
: fmt::format("{:016X}.ini", program_id);
const auto custom_config_file_path = Common::FS::GetEdenPath(Common::FS::EdenPath::ConfigDir)
/ "custom" / config_file_name;
if (!Common::FS::Exists(custom_config_file_path)) {
QtCommon::Frontend::Warning(rootObject,
tr("Error Removing Custom Configuration"),
tr("A custom configuration for this title does not exist."));
return;
}
if (Common::FS::RemoveFile(custom_config_file_path)) {
QtCommon::Frontend::Information(rootObject,
tr("Successfully Removed"),
tr("Successfully removed the custom game configuration."));
} else {
QtCommon::Frontend::Warning(rootObject,
tr("Error Removing Custom Configuration"),
tr("Failed to remove the custom game configuration."));
}
}
void RemoveCacheStorage(u64 program_id)
{
const auto nand_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir);
auto vfs_nand_dir = vfs->OpenDirectory(Common::FS::PathToUTF8String(nand_dir),
FileSys::OpenMode::Read);
const auto cache_storage_path
= FileSys::SaveDataFactory::GetFullPath({},
vfs_nand_dir,
FileSys::SaveDataSpaceId::User,
FileSys::SaveDataType::Cache,
0 /* program_id */,
{},
0);
const auto path = Common::FS::ConcatPathSafe(nand_dir, cache_storage_path);
// Not an error if it wasn't cleared.
Common::FS::RemoveDirRecursively(path);
}
// Metadata //
void ResetMetadata(bool show_message)
{
const QString title = tr("Reset Metadata Cache");
if (!Common::FS::Exists(Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir)
/ "game_list/")) {
if (show_message) QtCommon::Frontend::Warning(rootObject, title, tr("The metadata cache is already empty."));
} else if (Common::FS::RemoveDirRecursively(
Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "game_list")) {
if (show_message) QtCommon::Frontend::Information(rootObject,
title,
tr("The operation completed successfully."));
UISettings::values.is_game_list_reload_pending.exchange(true);
} else {
if (show_message) QtCommon::Frontend::Warning(
rootObject,
title,
tr("The metadata cache couldn't be deleted. It might be in use or non-existent."));
}
}
// Uhhh //
// Messages in pre-defined message boxes for less code spaghetti
inline constexpr bool CreateShortcutMessagesGUI(ShortcutMessages imsg, const QString& game_title)
{
int result = 0;
QMessageBox::StandardButtons buttons;
switch (imsg) {
case ShortcutMessages::Fullscreen:
buttons = QMessageBox::Yes | QMessageBox::No;
result
= QtCommon::Frontend::Information(tr("Create Shortcut"),
tr("Do you want to launch the game in fullscreen?"),
buttons);
return result == QMessageBox::Yes;
case ShortcutMessages::Success:
QtCommon::Frontend::Information(tr("Shortcut Created"),
tr("Successfully created a shortcut to %1").arg(game_title));
return false;
case ShortcutMessages::Volatile:
buttons = QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel;
result = QtCommon::Frontend::Warning(
tr("Shortcut may be Volatile!"),
tr("This will create a shortcut to the current AppImage. This may "
"not work well if you update. Continue?"),
buttons);
return result == QMessageBox::Ok;
default:
buttons = QMessageBox::Ok;
QtCommon::Frontend::Critical(tr("Failed to Create Shortcut"),
tr("Failed to create a shortcut to %1").arg(game_title),
buttons);
return false;
}
}
void CreateShortcut(const std::string& game_path,
const u64 program_id,
const std::string& game_title_,
const ShortcutTarget &target,
std::string arguments_,
const bool needs_title)
{
// Get path to Eden executable
std::filesystem::path command = GetEdenCommand();
// Shortcut path
std::filesystem::path shortcut_path = GetShortcutPath(target);
if (!std::filesystem::exists(shortcut_path)) {
CreateShortcutMessagesGUI(ShortcutMessages::Failed,
QString::fromStdString(shortcut_path.generic_string()));
LOG_ERROR(Frontend, "Invalid shortcut target {}", shortcut_path.generic_string());
return;
}
const FileSys::PatchManager pm{program_id, QtCommon::system->GetFileSystemController(),
QtCommon::system->GetContentProvider()};
const auto control = pm.GetControlMetadata();
const auto loader =
Loader::GetLoader(*QtCommon::system, QtCommon::vfs->OpenFile(game_path, FileSys::OpenMode::Read));
std::string game_title{game_title_};
// Delete illegal characters from title
if (needs_title) {
game_title = fmt::format("{:016X}", program_id);
if (control.first != nullptr) {
game_title = control.first->GetApplicationName();
} else {
loader->ReadTitle(game_title);
}
}
const std::string illegal_chars = "<>:\"/\\|?*.";
for (auto it = game_title.rbegin(); it != game_title.rend(); ++it) {
if (illegal_chars.find(*it) != std::string::npos) {
game_title.erase(it.base() - 1);
}
}
const QString qgame_title = QString::fromStdString(game_title);
// Get icon from game file
std::vector<u8> icon_image_file{};
if (control.second != nullptr) {
icon_image_file = control.second->ReadAllBytes();
} else if (loader->ReadIcon(icon_image_file) != Loader::ResultStatus::Success) {
LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path);
}
QImage icon_data =
QImage::fromData(icon_image_file.data(), static_cast<int>(icon_image_file.size()));
std::filesystem::path out_icon_path;
if (QtCommon::Game::MakeShortcutIcoPath(program_id, game_title, out_icon_path)) {
if (!SaveIconToFile(out_icon_path, icon_data)) {
LOG_ERROR(Frontend, "Could not write icon to file");
}
} else {
QtCommon::Frontend::Critical(
tr("Create Icon"),
tr("Cannot create icon file. Path \"%1\" does not exist and cannot be created.")
.arg(QString::fromStdString(out_icon_path.string())));
}
#if defined(__unix__) && !defined(__APPLE__) && !defined(__ANDROID__)
// Special case for AppImages
// Warn once if we are making a shortcut to a volatile AppImage
if (command.string().ends_with(".AppImage") && !UISettings::values.shortcut_already_warned) {
if (!CreateShortcutMessagesGUI(ShortcutMessages::Volatile, qgame_title)) {
return;
}
UISettings::values.shortcut_already_warned = true;
}
#endif
// Create shortcut
std::string arguments{arguments_};
if (CreateShortcutMessagesGUI(ShortcutMessages::Fullscreen, qgame_title)) {
arguments = "-f " + arguments;
}
const std::string comment = fmt::format("Start {:s} with the Eden Emulator", game_title);
const std::string categories = "Game;Emulator;Qt;";
const std::string keywords = "Switch;Nintendo;";
if (QtCommon::Game::CreateShortcutLink(shortcut_path, comment, out_icon_path, command,
arguments, categories, keywords, game_title)) {
CreateShortcutMessagesGUI(ShortcutMessages::Success,
qgame_title);
return;
}
CreateShortcutMessagesGUI(ShortcutMessages::Failed,
qgame_title);
}
constexpr std::string GetShortcutPath(ShortcutTarget target) {
{
std::string shortcut_path{};
if (target == ShortcutTarget::Desktop) {
shortcut_path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)
.toStdString();
} else if (target == ShortcutTarget::Applications) {
shortcut_path = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation)
.toStdString();
}
return shortcut_path;
}
}
void CreateHomeMenuShortcut(ShortcutTarget target) {
constexpr u64 QLaunchId = static_cast<u64>(Service::AM::AppletProgramId::QLaunch);
auto bis_system = QtCommon::system->GetFileSystemController().GetSystemNANDContents();
if (!bis_system) {
QtCommon::Frontend::Warning(tr("No firmware available"),
tr("Please install firmware to use the home menu."));
return;
}
auto qlaunch_nca = bis_system->GetEntry(QLaunchId, FileSys::ContentRecordType::Program);
if (!qlaunch_nca) {
QtCommon::Frontend::Warning(tr("Home Menu Applet"),
tr("Home Menu is not available. Please reinstall firmware."));
return;
}
auto qlaunch_applet_nca = bis_system->GetEntry(QLaunchId, FileSys::ContentRecordType::Program);
const auto game_path = qlaunch_applet_nca->GetFullPath();
// TODO(crueter): Make this use the Eden icon
CreateShortcut(game_path, QLaunchId, "Switch Home Menu", target, "-qlaunch", false);
}
} // namespace QtCommon::Game

86
src/qt_common/util/game.h Normal file
View file

@ -0,0 +1,86 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef QT_GAME_UTIL_H
#define QT_GAME_UTIL_H
#include <QObject>
#include <QStandardPaths>
#include "common/fs/path_util.h"
namespace QtCommon::Game {
enum class InstalledEntryType {
Game,
Update,
AddOnContent,
};
enum class GameListRemoveTarget {
GlShaderCache,
VkShaderCache,
AllShaderCache,
CustomConfiguration,
CacheStorage,
};
enum class ShortcutTarget {
Desktop,
Applications,
};
enum class ShortcutMessages{
Fullscreen = 0,
Success = 1,
Volatile = 2,
Failed = 3
};
bool CreateShortcutLink(const std::filesystem::path& shortcut_path,
const std::string& comment,
const std::filesystem::path& icon_path,
const std::filesystem::path& command,
const std::string& arguments,
const std::string& categories,
const std::string& keywords,
const std::string& name);
bool MakeShortcutIcoPath(const u64 program_id,
const std::string_view game_file_name,
std::filesystem::path& out_icon_path);
void OpenEdenFolder(const Common::FS::EdenPath &path);
void OpenRootDataFolder();
void OpenNANDFolder();
void OpenSaveFolder();
void OpenSDMCFolder();
void OpenModFolder();
void OpenLogFolder();
void RemoveBaseContent(u64 program_id, InstalledEntryType type);
void RemoveUpdateContent(u64 program_id, InstalledEntryType type);
void RemoveAddOnContent(u64 program_id, InstalledEntryType type);
void RemoveTransferableShaderCache(u64 program_id, GameListRemoveTarget target);
void RemoveVulkanDriverPipelineCache(u64 program_id);
void RemoveAllTransferableShaderCaches(u64 program_id);
void RemoveCustomConfiguration(u64 program_id, const std::string& game_path);
void RemoveCacheStorage(u64 program_id);
// Metadata //
void ResetMetadata(bool show_message = true);
// Shortcuts //
void CreateShortcut(const std::string& game_path,
const u64 program_id,
const std::string& game_title_,
const ShortcutTarget& target,
std::string arguments_,
const bool needs_title);
constexpr std::string GetShortcutPath(ShortcutTarget target);
void CreateHomeMenuShortcut(ShortcutTarget target);
}
#endif // QT_GAME_UTIL_H

View file

@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "qt_common/util/meta.h"
#include "common/common_types.h"
#include "core/core.h"
#include "core/frontend/applets/cabinet.h"
#include "core/frontend/applets/controller.h"
#include "core/frontend/applets/profile_select.h"
#include "core/frontend/applets/software_keyboard.h"
#include "core/hle/service/am/frontend/applet_web_browser_types.h"
namespace QtCommon::Meta {
void RegisterMetaTypes()
{
// Register integral and floating point types
qRegisterMetaType<u8>("u8");
qRegisterMetaType<u16>("u16");
qRegisterMetaType<u32>("u32");
qRegisterMetaType<u64>("u64");
qRegisterMetaType<u128>("u128");
qRegisterMetaType<s8>("s8");
qRegisterMetaType<s16>("s16");
qRegisterMetaType<s32>("s32");
qRegisterMetaType<s64>("s64");
qRegisterMetaType<f32>("f32");
qRegisterMetaType<f64>("f64");
// Register string types
qRegisterMetaType<std::string>("std::string");
qRegisterMetaType<std::wstring>("std::wstring");
qRegisterMetaType<std::u8string>("std::u8string");
qRegisterMetaType<std::u16string>("std::u16string");
qRegisterMetaType<std::u32string>("std::u32string");
qRegisterMetaType<std::string_view>("std::string_view");
qRegisterMetaType<std::wstring_view>("std::wstring_view");
qRegisterMetaType<std::u8string_view>("std::u8string_view");
qRegisterMetaType<std::u16string_view>("std::u16string_view");
qRegisterMetaType<std::u32string_view>("std::u32string_view");
// Register applet types
// Cabinet Applet
qRegisterMetaType<Core::Frontend::CabinetParameters>("Core::Frontend::CabinetParameters");
qRegisterMetaType<std::shared_ptr<Service::NFC::NfcDevice>>(
"std::shared_ptr<Service::NFC::NfcDevice>");
// Controller Applet
qRegisterMetaType<Core::Frontend::ControllerParameters>("Core::Frontend::ControllerParameters");
// Profile Select Applet
qRegisterMetaType<Core::Frontend::ProfileSelectParameters>(
"Core::Frontend::ProfileSelectParameters");
// Software Keyboard Applet
qRegisterMetaType<Core::Frontend::KeyboardInitializeParameters>(
"Core::Frontend::KeyboardInitializeParameters");
qRegisterMetaType<Core::Frontend::InlineAppearParameters>(
"Core::Frontend::InlineAppearParameters");
qRegisterMetaType<Core::Frontend::InlineTextParameters>("Core::Frontend::InlineTextParameters");
qRegisterMetaType<Service::AM::Frontend::SwkbdResult>("Service::AM::Frontend::SwkbdResult");
qRegisterMetaType<Service::AM::Frontend::SwkbdTextCheckResult>(
"Service::AM::Frontend::SwkbdTextCheckResult");
qRegisterMetaType<Service::AM::Frontend::SwkbdReplyType>(
"Service::AM::Frontend::SwkbdReplyType");
// Web Browser Applet
qRegisterMetaType<Service::AM::Frontend::WebExitReason>("Service::AM::Frontend::WebExitReason");
// Register loader types
qRegisterMetaType<Core::SystemResultStatus>("Core::SystemResultStatus");
}
}

15
src/qt_common/util/meta.h Normal file
View file

@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef QT_META_H
#define QT_META_H
#include <QObject>
namespace QtCommon::Meta {
//
void RegisterMetaTypes();
}
#endif // QT_META_H

View file

@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "qt_common/util/path.h"
#include <QDesktopServices>
#include <QString>
#include <QUrl>
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "qt_common/abstract/qt_frontend_util.h"
#include <fmt/format.h>
namespace QtCommon::Path {
bool OpenShaderCache(u64 program_id, QObject *parent)
{
const auto shader_cache_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::ShaderDir);
const auto shader_cache_folder_path{shader_cache_dir / fmt::format("{:016x}", program_id)};
if (!Common::FS::CreateDirs(shader_cache_folder_path)) {
QtCommon::Frontend::ShowMessage(QMessageBox::Warning,
tr("Error Opening Shader Cache"),
tr("Failed to create or open shader cache for this title, "
"ensure your app data directory has write permissions."),
QMessageBox::Ok,
parent);
}
const auto shader_path_string{Common::FS::PathToUTF8String(shader_cache_folder_path)};
const auto qt_shader_cache_path = QString::fromStdString(shader_path_string);
return QDesktopServices::openUrl(QUrl::fromLocalFile(qt_shader_cache_path));
}
}

12
src/qt_common/util/path.h Normal file
View file

@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef QT_PATH_UTIL_H
#define QT_PATH_UTIL_H
#include "common/common_types.h"
#include <QObject>
namespace QtCommon::Path { bool OpenShaderCache(u64 program_id, QObject *parent); }
#endif // QT_PATH_UTIL_H

View file

@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "qt_common/util/rom.h"
#include <QCoreApplication>
namespace QtCommon::ROM {
bool RomFSRawCopy(size_t total_size,
size_t& read_size,
QtProgressCallback callback,
const FileSys::VirtualDir& src,
const FileSys::VirtualDir& dest,
bool full)
{
// TODO(crueter)
// if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable())
// return false;
// if (dialog.wasCanceled())
// return false;
// std::vector<u8> buffer(CopyBufferSize);
// auto last_timestamp = std::chrono::steady_clock::now();
// const auto QtRawCopy = [&](const FileSys::VirtualFile& src_file,
// const FileSys::VirtualFile& dest_file) {
// if (src_file == nullptr || dest_file == nullptr) {
// return false;
// }
// if (!dest_file->Resize(src_file->GetSize())) {
// return false;
// }
// for (std::size_t i = 0; i < src_file->GetSize(); i += buffer.size()) {
// if (dialog.wasCanceled()) {
// dest_file->Resize(0);
// return false;
// }
// using namespace std::literals::chrono_literals;
// const auto new_timestamp = std::chrono::steady_clock::now();
// if ((new_timestamp - last_timestamp) > 33ms) {
// last_timestamp = new_timestamp;
// dialog.setValue(
// static_cast<int>(std::min(read_size, total_size) * 100 / total_size));
// QCoreApplication::processEvents();
// }
// const auto read = src_file->Read(buffer.data(), buffer.size(), i);
// dest_file->Write(buffer.data(), read, i);
// read_size += read;
// }
// return true;
// };
// if (full) {
// for (const auto& file : src->GetFiles()) {
// const auto out = VfsDirectoryCreateFileWrapper(dest, file->GetName());
// if (!QtRawCopy(file, out))
// return false;
// }
// }
// for (const auto& dir : src->GetSubdirectories()) {
// const auto out = dest->CreateSubdirectory(dir->GetName());
// if (!RomFSRawCopy(total_size, read_size, dialog, dir, out, full))
// return false;
// }
// return true;
return true;
}
}

20
src/qt_common/util/rom.h Normal file
View file

@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef QT_ROM_UTIL_H
#define QT_ROM_UTIL_H
#include "qt_common/qt_common.h"
#include <cstddef>
namespace QtCommon::ROM {
bool RomFSRawCopy(size_t total_size,
size_t& read_size,
QtProgressCallback callback,
const FileSys::VirtualDir& src,
const FileSys::VirtualDir& dest,
bool full);
}
#endif // QT_ROM_UTIL_H