diff --git a/docs/user/Architectures.md b/docs/user/Architectures.md index 6d60716684..240feb666d 100644 --- a/docs/user/Architectures.md +++ b/docs/user/Architectures.md @@ -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. diff --git a/src/common/fs/fs_types.h b/src/common/fs/fs_types.h index 900f85d24e..7b7359fa6f 100644 --- a/src/common/fs/fs_types.h +++ b/src/common/fs/fs_types.h @@ -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 #include #include "common/common_funcs.h" diff --git a/src/core/file_sys/savedata_factory.cpp b/src/core/file_sys/savedata_factory.cpp index edf51e74de..dda8d526d3 100644 --- a/src/core/file_sys/savedata_factory.cpp +++ b/src/core/file_sys/savedata_factory.cpp @@ -4,7 +4,6 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include #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]); diff --git a/src/core/hle/service/acc/profile_manager.cpp b/src/core/hle/service/acc/profile_manager.cpp index a4394046fa..4a892f7c65 100644 --- a/src/core/hle/service/acc/profile_manager.cpp +++ b/src/core/hle/service/acc/profile_manager.cpp @@ -4,13 +4,17 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include +#include +#include #include #include #include "common/fs/file.h" #include "common/fs/fs.h" +#include "common/fs/fs_types.h" #include "common/fs/path_util.h" #include #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 ProfileManager::FindOrphanedProfiles() +{ + std::vector 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 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) diff --git a/src/core/hle/service/acc/profile_manager.h b/src/core/hle/service/acc/profile_manager.h index d64e42715c..b164ed011a 100644 --- a/src/core/hle/service/acc/profile_manager.h +++ b/src/core/hle/service/acc/profile_manager.h @@ -103,10 +103,15 @@ public: void WriteUserSaveFile(); + void ResetUserSaveFile(); + + std::vector FindOrphanedProfiles(); + private: void ParseUserSaveFile(); std::optional AddToProfiles(const ProfileInfo& profile); bool RemoveProfileAtIndex(std::size_t index); + void RemoveAllProfiles(); bool is_save_needed{}; std::array profiles{}; diff --git a/src/qt_common/qt_content_util.cpp b/src/qt_common/qt_content_util.cpp index e4625aa423..2f659cf1b2 100644 --- a/src/qt_common/qt_content_util.cpp +++ b/src/qt_common/qt_content_util.cpp @@ -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 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 diff --git a/src/qt_common/qt_content_util.h b/src/qt_common/qt_content_util.h index b572c1c4a3..b95e78c0a0 100644 --- a/src/qt_common/qt_content_util.h +++ b/src/qt_common/qt_content_util.h @@ -45,5 +45,8 @@ void InstallKeys(); // Content // void VerifyGameContents(const std::string &game_path); void VerifyInstalledContents(); + +// Profiles // +void FixProfiles(); } #endif // QT_CONTENT_UTIL_H diff --git a/src/qt_common/qt_game_util.cpp b/src/qt_common/qt_game_util.cpp index 5d0b4d8ae7..ac922ea967 100644 --- a/src/qt_common/qt_game_util.cpp +++ b/src/qt_common/qt_game_util.cpp @@ -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 diff --git a/src/qt_common/qt_game_util.h b/src/qt_common/qt_game_util.h index 0a21208659..5c6bb24910 100644 --- a/src/qt_common/qt_game_util.h +++ b/src/qt_common/qt_game_util.h @@ -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, diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 901a39cc9f..44ed29f141 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -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(); } diff --git a/src/yuzu/migration_worker.cpp b/src/yuzu/migration_worker.cpp index 42ec006026..95f205ec0c 100644 --- a/src/yuzu/migration_worker.cpp +++ b/src/yuzu/migration_worker.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include "common/fs/path_util.h"