2025-09-15 17:21:18 +02:00
|
|
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
[desktop] fix save data location, orphaned profiles finder (#2678)
Previously, if the user had their NAND in a nonstandard location,
profiles.dat would be read from the standard Eden path and thus return
effectively garbage data. What this would result in is:
- The Qt profile manager would be completely nonfunctional
- "Open Save Data Location" would put you into the completely wrong
place
- Games would read from incorrect locations for their saves
To solve this, I made it so that profiles.dat is re-read *after*
QtConfig initializes. It's not the perfect solution, but it works.
Additionally, this adds an orphaned profiles finder:
- walks through the save folders in nand/user/save/000.../
- for each subdirectory, checks to see if profiles.dat contains a
corresponding UUID
- If not, the profile is "orphaned". It may contain legit save data, so
let the user decide how to handle it (famous last words)
- Empty profiles are just removed. If they really matter, they're
instantly recreated anyways.
The orphaned profiles check runs right *after* the decryption keys
check, but before the game list ever gets populated
Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2678
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
2025-10-07 01:32:09 +02:00
|
|
|
#include "qt_common/qt_game_util.h"
|
2025-09-15 17:21:18 +02:00
|
|
|
#include "qt_content_util.h"
|
|
|
|
#include "common/fs/fs.h"
|
[desktop] fix save data location, orphaned profiles finder (#2678)
Previously, if the user had their NAND in a nonstandard location,
profiles.dat would be read from the standard Eden path and thus return
effectively garbage data. What this would result in is:
- The Qt profile manager would be completely nonfunctional
- "Open Save Data Location" would put you into the completely wrong
place
- Games would read from incorrect locations for their saves
To solve this, I made it so that profiles.dat is re-read *after*
QtConfig initializes. It's not the perfect solution, but it works.
Additionally, this adds an orphaned profiles finder:
- walks through the save folders in nand/user/save/000.../
- for each subdirectory, checks to see if profiles.dat contains a
corresponding UUID
- If not, the profile is "orphaned". It may contain legit save data, so
let the user decide how to handle it (famous last words)
- Empty profiles are just removed. If they really matter, they're
instantly recreated anyways.
The orphaned profiles check runs right *after* the decryption keys
check, but before the game list ever gets populated
Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2678
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
2025-10-07 01:32:09 +02:00
|
|
|
#include "core/hle/service/acc/profile_manager.h"
|
2025-09-15 17:21:18 +02:00
|
|
|
#include "frontend_common/content_manager.h"
|
|
|
|
#include "frontend_common/firmware_manager.h"
|
|
|
|
#include "qt_common/qt_common.h"
|
|
|
|
#include "qt_common/qt_progress_dialog.h"
|
|
|
|
#include "qt_frontend_util.h"
|
|
|
|
|
|
|
|
#include <JlCompress.h>
|
|
|
|
|
|
|
|
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,
|
|
|
|
"Game Requires Firmware",
|
|
|
|
"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,
|
|
|
|
failedTitle,
|
|
|
|
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 "";
|
|
|
|
}
|
|
|
|
|
|
|
|
tmp /= "eden";
|
|
|
|
tmp /= "firmware";
|
|
|
|
|
|
|
|
QString qCacheDir = QString::fromStdString(tmp.string());
|
|
|
|
|
|
|
|
QFile zip(location);
|
|
|
|
|
|
|
|
QStringList result = JlCompress::extractDir(&zip, qCacheDir);
|
|
|
|
if (result.isEmpty()) {
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
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));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
[desktop] fix save data location, orphaned profiles finder (#2678)
Previously, if the user had their NAND in a nonstandard location,
profiles.dat would be read from the standard Eden path and thus return
effectively garbage data. What this would result in is:
- The Qt profile manager would be completely nonfunctional
- "Open Save Data Location" would put you into the completely wrong
place
- Games would read from incorrect locations for their saves
To solve this, I made it so that profiles.dat is re-read *after*
QtConfig initializes. It's not the perfect solution, but it works.
Additionally, this adds an orphaned profiles finder:
- walks through the save folders in nand/user/save/000.../
- for each subdirectory, checks to see if profiles.dat contains a
corresponding UUID
- If not, the profile is "orphaned". It may contain legit save data, so
let the user decide how to handle it (famous last words)
- Empty profiles are just removed. If they really matter, they're
instantly recreated anyways.
The orphaned profiles check runs right *after* the decryption keys
check, but before the game list ever gets populated
Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2678
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
2025-10-07 01:32:09 +02:00
|
|
|
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 += "\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();
|
|
|
|
}
|
|
|
|
|
2025-09-15 17:21:18 +02:00
|
|
|
} // namespace QtCommon::Content
|