[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: eden-emu/eden#2678
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
This commit is contained in:
crueter 2025-10-07 01:32:09 +02:00
parent 6a4fa11ac3
commit badd913bee
Signed by untrusted user: crueter
GPG key ID: 425ACD2D4830EBC6
11 changed files with 156 additions and 17 deletions

View file

@ -63,6 +63,8 @@ While all modern Linux distributions are supported (Fedora >40, Ubuntu >24.04, D
Intel and Nvidia GPU support is limited. AMD (RADV) drivers receive first-class testing and are known to provide the most stable Eden experience possible.
Wayland is not recommended. Testing has shown significantly worse performance on most Wayland compositors compared to X11, alongside mysterious bugs and compatibility errors. For now, set `QT_QPA_PLATFORM=xcb` when running Eden, or pass `-platform xcb` to the launch arguments.
## Windows
Windows 10 and 11 are supported. Support for Windows 8.x is unknown, and Windows 7 support is unlikely to ever be added.

View file

@ -1,8 +1,12 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <filesystem>
#include <functional>
#include "common/common_funcs.h"

View file

@ -4,7 +4,6 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <memory>
#include "common/assert.h"
#include "common/common_types.h"
#include "common/logging/log.h"
@ -129,10 +128,6 @@ std::string SaveDataFactory::GetFullPath(ProgramId program_id, VirtualDir dir,
std::string out = GetSaveDataSpaceIdPath(space);
LOG_INFO(Common_Filesystem, "Save ID: {:016X}", save_id);
LOG_INFO(Common_Filesystem, "User ID[1]: {:016X}", user_id[1]);
LOG_INFO(Common_Filesystem, "User ID[0]: {:016X}", user_id[0]);
switch (type) {
case SaveDataType::System:
return fmt::format("{}save/{:016X}/{:016X}{:016X}", out, save_id, user_id[1], user_id[0]);

View file

@ -4,13 +4,17 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <cstring>
#include <filesystem>
#include <iostream>
#include <random>
#include <fmt/ranges.h>
#include "common/fs/file.h"
#include "common/fs/fs.h"
#include "common/fs/fs_types.h"
#include "common/fs/path_util.h"
#include <ranges>
#include "common/settings.h"
@ -90,6 +94,11 @@ bool ProfileManager::RemoveProfileAtIndex(std::size_t index) {
return true;
}
void ProfileManager::RemoveAllProfiles()
{
profiles = {};
}
/// Helper function to register a user to the system
Result ProfileManager::AddUser(const ProfileInfo& user) {
if (!AddToProfiles(user)) {
@ -259,8 +268,9 @@ void ProfileManager::CloseUser(UUID uuid) {
/// Gets all valid user ids on the system
UserIDArray ProfileManager::GetAllUsers() const {
UserIDArray output{};
std::ranges::transform(profiles, output.begin(),
[](const ProfileInfo& p) { return p.user_uuid; });
std::ranges::transform(profiles, output.begin(), [](const ProfileInfo& p) {
return p.user_uuid;
});
return output;
}
@ -387,18 +397,19 @@ bool ProfileManager::SetProfileBaseAndData(Common::UUID uuid, const ProfileBase&
void ProfileManager::ParseUserSaveFile() {
const auto save_path(FS::GetEdenPath(FS::EdenPath::NANDDir) / ACC_SAVE_AVATORS_BASE_PATH /
"profiles.dat");
const FS::IOFile save(save_path, FS::FileAccessMode::Read, FS::FileType::BinaryFile);
if (!save.IsOpen()) {
LOG_WARNING(Service_ACC, "Failed to load profile data from save data... Generating new "
"user 'eden' with random UUID.");
"user 'Eden' with random UUID.");
return;
}
ProfileDataRaw data;
if (!save.ReadObject(data)) {
LOG_WARNING(Service_ACC, "profiles.dat is smaller than expected... Generating new user "
"'eden' with random UUID.");
"'Eden' with random UUID.");
return;
}
@ -471,6 +482,79 @@ void ProfileManager::WriteUserSaveFile() {
is_save_needed = false;
}
void ProfileManager::ResetUserSaveFile()
{
RemoveAllProfiles();
ParseUserSaveFile();
}
std::vector<std::string> ProfileManager::FindOrphanedProfiles()
{
std::vector<std::string> good_uuids;
for (const ProfileInfo& p : profiles) {
std::string uuid_string = [p]() -> std::string {
auto uuid = p.user_uuid;
// "ignore" invalid uuids
if (uuid.IsInvalid()) {
return "0";
}
auto user_id = uuid.AsU128();
return fmt::format("{:016X}{:016X}", user_id[1], user_id[0]);
}();
good_uuids.emplace_back(uuid_string);
}
// TODO: fetch save_id programmatically
const auto path = Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir)
/ "user/save/0000000000000000";
std::vector<std::string> orphaned_profiles;
Common::FS::IterateDirEntries(
path,
[&good_uuids, &orphaned_profiles](const std::filesystem::directory_entry& entry) -> bool {
const std::string uuid = entry.path().stem().string();
// first off, we should always clear empty profiles
// 99% of the time these are useless. If not, they are recreated anyways...
namespace fs = std::filesystem;
const auto is_empty = [&entry]() -> bool {
try {
for (const auto& file : fs::recursive_directory_iterator(entry.path())) {
if (file.is_regular_file()) {
return true;
}
}
} catch (const fs::filesystem_error& e) {
// if we get an error--no worries, just pretend it's not empty
return false;
}
return false;
}();
if (!is_empty) {
fs::remove_all(entry);
return true;
}
// if profiles.dat contains the UUID--all good
// if not--it's an orphaned profile and should be resolved by the user
if (std::find(good_uuids.begin(), good_uuids.end(), uuid) == good_uuids.end()) {
orphaned_profiles.emplace_back(uuid);
}
return true;
},
Common::FS::DirEntryFilter::Directory);
return orphaned_profiles;
}
void ProfileManager::SetUserPosition(u64 position, Common::UUID uuid) {
auto idxOpt = GetUserIndex(uuid);
if (!idxOpt)

View file

@ -103,10 +103,15 @@ public:
void WriteUserSaveFile();
void ResetUserSaveFile();
std::vector<std::string> FindOrphanedProfiles();
private:
void ParseUserSaveFile();
std::optional<std::size_t> AddToProfiles(const ProfileInfo& profile);
bool RemoveProfileAtIndex(std::size_t index);
void RemoveAllProfiles();
bool is_save_needed{};
std::array<ProfileInfo, MAX_USERS> profiles{};

View file

@ -1,8 +1,10 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "qt_common/qt_game_util.h"
#include "qt_content_util.h"
#include "common/fs/fs.h"
#include "core/hle/service/acc/profile_manager.h"
#include "frontend_common/content_manager.h"
#include "frontend_common/firmware_manager.h"
#include "qt_common/qt_common.h"
@ -310,4 +312,40 @@ void VerifyInstalledContents() {
}
}
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();
}
} // namespace QtCommon::Content

View file

@ -45,5 +45,8 @@ void InstallKeys();
// Content //
void VerifyGameContents(const std::string &game_path);
void VerifyInstalledContents();
// Profiles //
void FixProfiles();
}
#endif // QT_CONTENT_UTIL_H

View file

@ -178,6 +178,12 @@ 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);
@ -379,21 +385,21 @@ void RemoveCacheStorage(u64 program_id)
}
// Metadata //
void ResetMetadata()
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/")) {
QtCommon::Frontend::Warning(rootObject, title, tr("The metadata cache is already empty."));
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")) {
QtCommon::Frontend::Information(rootObject,
if (show_message) QtCommon::Frontend::Information(rootObject,
title,
tr("The operation completed successfully."));
UISettings::values.is_game_list_reload_pending.exchange(true);
} else {
QtCommon::Frontend::Warning(
if (show_message) QtCommon::Frontend::Warning(
rootObject,
title,
tr("The metadata cache couldn't be deleted. It might be in use or non-existent."));
@ -573,5 +579,4 @@ void CreateHomeMenuShortcut(ShortcutTarget target) {
CreateShortcut(game_path, QLaunchId, "Switch Home Menu", target, "-qlaunch", false);
}
} // namespace QtCommon::Game

View file

@ -52,6 +52,7 @@ bool MakeShortcutIcoPath(const u64 program_id,
void OpenEdenFolder(const Common::FS::EdenPath &path);
void OpenRootDataFolder();
void OpenNANDFolder();
void OpenSaveFolder();
void OpenSDMCFolder();
void OpenModFolder();
void OpenLogFolder();
@ -67,7 +68,7 @@ void RemoveCustomConfiguration(u64 program_id, const std::string& game_path);
void RemoveCacheStorage(u64 program_id);
// Metadata //
void ResetMetadata();
void ResetMetadata(bool show_message = true);
// Shortcuts //
void CreateShortcut(const std::string& game_path,

View file

@ -545,6 +545,9 @@ GMainWindow::GMainWindow(bool has_broken_vulkan)
// Gen keys if necessary
OnCheckFirmwareDecryption();
// Check for orphaned profiles and reset profile data if necessary
QtCommon::Content::FixProfiles();
game_list->LoadCompatibilityList();
// force reload on first load to ensure add-ons get updated
game_list->PopulateAsync(UISettings::values.game_dirs);
@ -3947,7 +3950,7 @@ void GMainWindow::OnToggleStatusBar() {
void GMainWindow::OnGameListRefresh()
{
// Resets metadata cache and reloads
QtCommon::Game::ResetMetadata();
QtCommon::Game::ResetMetadata(false);
game_list->RefreshGameDirectory();
SetFirmwareVersion();
}

View file

@ -7,7 +7,6 @@
#include <boost/algorithm/string/predicate.hpp>
#include <boost/algorithm/string/replace.hpp>
#include <filesystem>
#include <qdebug.h>
#include "common/fs/path_util.h"