[desktop] fix save data location, orphaned profiles finder #2678

Merged
crueter merged 2 commits from fix/profiles into master 2025-10-07 01:32:10 +02:00
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. 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
Windows 10 and 11 are supported. Support for Windows 8.x is unknown, and Windows 7 support is unlikely to ever be added. 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-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"