[desktop] fix save data location, orphaned profiles finder
All checks were successful
eden-license / license-header (pull_request) Successful in 23s

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>
This commit is contained in:
crueter 2025-10-05 17:46:37 -04:00
parent f6d99e5032
commit 33b7f39f65
Signed by: crueter
GPG key ID: 425ACD2D4830EBC6
10 changed files with 154 additions and 17 deletions

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-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include <filesystem>
#include <functional> #include <functional>
#include "common/common_funcs.h" #include "common/common_funcs.h"

View file

@ -4,7 +4,6 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <memory>
#include "common/assert.h" #include "common/assert.h"
#include "common/common_types.h" #include "common/common_types.h"
#include "common/logging/log.h" #include "common/logging/log.h"
@ -129,10 +128,6 @@ std::string SaveDataFactory::GetFullPath(ProgramId program_id, VirtualDir dir,
std::string out = GetSaveDataSpaceIdPath(space); 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) { switch (type) {
case SaveDataType::System: case SaveDataType::System:
return fmt::format("{}save/{:016X}/{:016X}{:016X}", out, save_id, user_id[1], user_id[0]); 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-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <cstring> #include <cstring>
#include <filesystem>
#include <iostream>
#include <random> #include <random>
#include <fmt/ranges.h> #include <fmt/ranges.h>
#include "common/fs/file.h" #include "common/fs/file.h"
#include "common/fs/fs.h" #include "common/fs/fs.h"
#include "common/fs/fs_types.h"
#include "common/fs/path_util.h" #include "common/fs/path_util.h"
#include <ranges> #include <ranges>
#include "common/settings.h" #include "common/settings.h"
@ -90,6 +94,11 @@ bool ProfileManager::RemoveProfileAtIndex(std::size_t index) {
return true; return true;
} }
void ProfileManager::RemoveAllProfiles()
{
profiles = {};
}
/// Helper function to register a user to the system /// Helper function to register a user to the system
Result ProfileManager::AddUser(const ProfileInfo& user) { Result ProfileManager::AddUser(const ProfileInfo& user) {
if (!AddToProfiles(user)) { if (!AddToProfiles(user)) {
@ -259,8 +268,9 @@ void ProfileManager::CloseUser(UUID uuid) {
/// Gets all valid user ids on the system /// Gets all valid user ids on the system
UserIDArray ProfileManager::GetAllUsers() const { UserIDArray ProfileManager::GetAllUsers() const {
UserIDArray output{}; UserIDArray output{};
std::ranges::transform(profiles, output.begin(), std::ranges::transform(profiles, output.begin(), [](const ProfileInfo& p) {
[](const ProfileInfo& p) { return p.user_uuid; }); return p.user_uuid;
});
return output; return output;
} }
@ -387,18 +397,19 @@ bool ProfileManager::SetProfileBaseAndData(Common::UUID uuid, const ProfileBase&
void ProfileManager::ParseUserSaveFile() { void ProfileManager::ParseUserSaveFile() {
const auto save_path(FS::GetEdenPath(FS::EdenPath::NANDDir) / ACC_SAVE_AVATORS_BASE_PATH / const auto save_path(FS::GetEdenPath(FS::EdenPath::NANDDir) / ACC_SAVE_AVATORS_BASE_PATH /
"profiles.dat"); "profiles.dat");
const FS::IOFile save(save_path, FS::FileAccessMode::Read, FS::FileType::BinaryFile); const FS::IOFile save(save_path, FS::FileAccessMode::Read, FS::FileType::BinaryFile);
if (!save.IsOpen()) { if (!save.IsOpen()) {
LOG_WARNING(Service_ACC, "Failed to load profile data from save data... Generating new " 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; return;
} }
ProfileDataRaw data; ProfileDataRaw data;
if (!save.ReadObject(data)) { if (!save.ReadObject(data)) {
LOG_WARNING(Service_ACC, "profiles.dat is smaller than expected... Generating new user " LOG_WARNING(Service_ACC, "profiles.dat is smaller than expected... Generating new user "
"'eden' with random UUID."); "'Eden' with random UUID.");
return; return;
} }
@ -471,6 +482,79 @@ void ProfileManager::WriteUserSaveFile() {
is_save_needed = false; 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) { void ProfileManager::SetUserPosition(u64 position, Common::UUID uuid) {
auto idxOpt = GetUserIndex(uuid); auto idxOpt = GetUserIndex(uuid);
if (!idxOpt) if (!idxOpt)

View file

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

View file

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

View file

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

View file

@ -178,6 +178,12 @@ void OpenNANDFolder()
OpenEdenFolder(Common::FS::EdenPath::NANDDir); 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() void OpenSDMCFolder()
{ {
OpenEdenFolder(Common::FS::EdenPath::SDMCDir); OpenEdenFolder(Common::FS::EdenPath::SDMCDir);
@ -379,21 +385,21 @@ void RemoveCacheStorage(u64 program_id)
} }
// Metadata // // Metadata //
void ResetMetadata() void ResetMetadata(bool show_message)
{ {
const QString title = tr("Reset Metadata Cache"); const QString title = tr("Reset Metadata Cache");
if (!Common::FS::Exists(Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) if (!Common::FS::Exists(Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir)
/ "game_list/")) { / "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( } else if (Common::FS::RemoveDirRecursively(
Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "game_list")) { Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "game_list")) {
QtCommon::Frontend::Information(rootObject, if (show_message) QtCommon::Frontend::Information(rootObject,
title, title,
tr("The operation completed successfully.")); tr("The operation completed successfully."));
UISettings::values.is_game_list_reload_pending.exchange(true); UISettings::values.is_game_list_reload_pending.exchange(true);
} else { } else {
QtCommon::Frontend::Warning( if (show_message) QtCommon::Frontend::Warning(
rootObject, rootObject,
title, title,
tr("The metadata cache couldn't be deleted. It might be in use or non-existent.")); 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); CreateShortcut(game_path, QLaunchId, "Switch Home Menu", target, "-qlaunch", false);
} }
} // namespace QtCommon::Game } // namespace QtCommon::Game

View file

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

View file

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

View file

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