From d364bc2053b2c42cfa5ab8bf647451853df88ea9 Mon Sep 17 00:00:00 2001 From: crueter Date: Thu, 6 Nov 2025 15:58:24 -0500 Subject: [PATCH] [fs] fix issues with Ryujinx Link Notably: - Paths with spaces would cause mklink to fail - Add support for portable directories - Symlink detection was incorrect sometimes(????) - Some other stuff I'm forgetting I do need to test this on MinGW, putting this build out here for now though Signed-off-by: crueter --- src/common/CMakeLists.txt | 2 +- src/common/fs/ryujinx_compat.cpp | 15 +++++-- src/common/fs/ryujinx_compat.h | 8 ++-- src/common/fs/symlink.cpp | 16 ++++--- src/qt_common/abstract/frontend.cpp | 11 ++++- src/qt_common/abstract/frontend.h | 4 ++ src/qt_common/config/qt_config.cpp | 25 +++++++++++ src/qt_common/config/uisettings.h | 2 + src/qt_common/util/fs.cpp | 69 +++++++++++++++++++---------- src/qt_common/util/fs.h | 10 ++--- src/yuzu/deps_dialog.cpp | 4 +- src/yuzu/main_window.cpp | 51 ++++++++++++++++----- 12 files changed, 158 insertions(+), 59 deletions(-) diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 17d6a42f37..738624b6f0 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -252,7 +252,7 @@ if(CXX_CLANG) endif() if (BOOST_NO_HEADERS) - target_link_libraries(common PUBLIC Boost::algorithm Boost::icl Boost::pool) + target_link_libraries(common PUBLIC Boost::algorithm Boost::icl Boost::pool Boost::filesystem) else() target_link_libraries(common PUBLIC Boost::headers) endif() diff --git a/src/common/fs/ryujinx_compat.cpp b/src/common/fs/ryujinx_compat.cpp index 610ba736a7..d292891ead 100644 --- a/src/common/fs/ryujinx_compat.cpp +++ b/src/common/fs/ryujinx_compat.cpp @@ -14,16 +14,24 @@ namespace fs = std::filesystem; fs::path GetKvdbPath() { - return GetLegacyPath(EmuPath::RyujinxDir) / "bis" / "system" / "save" / "8000000000000000" / "0" + return GetKvdbPath(GetLegacyPath(EmuPath::RyujinxDir)); +} + +fs::path GetKvdbPath(const fs::path& path) { + return path / "bis" / "system" / "save" / "8000000000000000" / "0" / "imkvdb.arc"; } fs::path GetRyuSavePath(const u64 &save_id) { + return GetRyuSavePath(GetLegacyPath(EmuPath::RyujinxDir), save_id); +} + +std::filesystem::path GetRyuSavePath(const std::filesystem::path& path, const u64& save_id) { std::string hex = fmt::format("{:016x}", save_id); - // TODO: what's the difference between 0 and 1? - return GetLegacyPath(EmuPath::RyujinxDir) / "bis" / "user" / "save" / hex / "0"; + // TODO: what's the difference between 0 and 1? + return path / "bis" / "user" / "save" / hex / "0"; } IMENReadResult ReadKvdb(const fs::path &path, std::vector &imens) @@ -90,4 +98,5 @@ IMENReadResult ReadKvdb(const fs::path &path, std::vector &imens) return IMENReadResult::Success; } + } // namespace Common::FS diff --git a/src/common/fs/ryujinx_compat.h b/src/common/fs/ryujinx_compat.h index 6273754074..b8520989d1 100644 --- a/src/common/fs/ryujinx_compat.h +++ b/src/common/fs/ryujinx_compat.h @@ -7,16 +7,18 @@ #include #include -namespace fs = std::filesystem; - namespace Common::FS { +namespace fs = std::filesystem; + constexpr const char IMEN_MAGIC[4] = {0x49, 0x4d, 0x45, 0x4e}; constexpr const char IMKV_MAGIC[4] = {0x49, 0x4d, 0x4b, 0x56}; constexpr const u8 IMEN_SIZE = 0x8c; fs::path GetKvdbPath(); -fs::path GetRyuSavePath(const u64 &program_id); +fs::path GetKvdbPath(const fs::path &path); +fs::path GetRyuSavePath(const u64 &save_id); +fs::path GetRyuSavePath(const fs::path &path, const u64 &save_id); enum class IMENReadResult { Nonexistent, // ryujinx not found diff --git a/src/common/fs/symlink.cpp b/src/common/fs/symlink.cpp index 9c9fe3d50b..a9a2d6893a 100644 --- a/src/common/fs/symlink.cpp +++ b/src/common/fs/symlink.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later +#include #include "symlink.h" #ifdef _WIN32 @@ -8,6 +9,8 @@ #include #endif +#include + namespace fs = std::filesystem; // The sole purpose of this file is to treat symlinks like symlinks on POSIX, @@ -15,13 +18,17 @@ namespace fs = std::filesystem; // This is because, for some inexplicable reason, Microsoft has locked symbolic // links behind a "security policy", whereas directory junctions--functionally identical // for directories, by the way--are not. Why? I don't know. +// And no, they do NOT provide a standard API for this (at least to my knowledge). +// CreateSymbolicLink, even when EXPLICITLY TOLD to create a junction, still fails +// because of their security policy. +// I don't know what kind of drugs the Windows developers have been on since NT started. namespace Common::FS { bool CreateSymlink(const fs::path &from, const fs::path &to) { #ifdef _WIN32 - const std::string command = fmt::format("mklink /J {} {}", to.string(), from.string()); + const std::string command = fmt::format("mklink /J \"{}\" \"{}\"", to.string(), from.string()); return system(command.c_str()) == 0; #else std::error_code ec; @@ -32,12 +39,7 @@ bool CreateSymlink(const fs::path &from, const fs::path &to) bool IsSymlink(const fs::path &path) { -#ifdef _WIN32 - auto attributes = GetFileAttributesW(path.wstring().c_str()); - return attributes & FILE_ATTRIBUTE_REPARSE_POINT; -#else - return fs::is_symlink(path); -#endif + return boost::filesystem::is_symlink(boost::filesystem::path{path}); } } // namespace Common::FS diff --git a/src/qt_common/abstract/frontend.cpp b/src/qt_common/abstract/frontend.cpp index 2ed76f8ea9..a0ce943538 100644 --- a/src/qt_common/abstract/frontend.cpp +++ b/src/qt_common/abstract/frontend.cpp @@ -28,7 +28,7 @@ const QString GetOpenFileName(const QString &title, Options options) { #ifdef YUZU_QT_WIDGETS - return QFileDialog::getOpenFileName((QWidget *) rootObject, title, dir, filter, selectedFilter, options); + return QFileDialog::getOpenFileName(rootObject, title, dir, filter, selectedFilter, options); #endif } @@ -39,7 +39,14 @@ const QString GetSaveFileName(const QString &title, Options options) { #ifdef YUZU_QT_WIDGETS - return QFileDialog::getSaveFileName((QWidget *) rootObject, title, dir, filter, selectedFilter, options); + return QFileDialog::getSaveFileName(rootObject, title, dir, filter, selectedFilter, options); +#endif +} + +const QString GetExistingDirectory(const QString& caption, const QString& dir, + Options options) { +#ifdef YUZU_QT_WIDGETS + return QFileDialog::getExistingDirectory(rootObject, caption, dir, options); #endif } diff --git a/src/qt_common/abstract/frontend.h b/src/qt_common/abstract/frontend.h index b496b37cbe..0ef9ea97e4 100644 --- a/src/qt_common/abstract/frontend.h +++ b/src/qt_common/abstract/frontend.h @@ -135,5 +135,9 @@ const QString GetSaveFileName(const QString &title, QString *selectedFilter = nullptr, Options options = Options()); +const QString GetExistingDirectory(const QString &caption = QString(), + const QString &dir = QString(), + Options options = Option::ShowDirsOnly); + } // namespace QtCommon::Frontend #endif // FRONTEND_H diff --git a/src/qt_common/config/qt_config.cpp b/src/qt_common/config/qt_config.cpp index f787873cf6..bdea8f2589 100644 --- a/src/qt_common/config/qt_config.cpp +++ b/src/qt_common/config/qt_config.cpp @@ -294,6 +294,17 @@ void QtConfig::ReadUIGamelistValues() { } EndArray(); + const int linked_size = BeginArray("ryujinx_linked"); + for (int i = 0; i < linked_size; ++i) { + SetArrayIndex(i); + + QDir ryu_dir = QString::fromStdString(ReadStringSetting("ryujinx_path")); + u64 program_id = ReadUnsignedIntegerSetting("program_id"); + + UISettings::values.ryujinx_link_paths.insert(program_id, ryu_dir); + } + EndArray(); + EndGroup(); } @@ -499,6 +510,20 @@ void QtConfig::SaveUIGamelistValues() { } EndArray(); // favorites + BeginArray(std::string("ryujinx_linked")); + int i = 0; + QMapIterator iter(UISettings::values.ryujinx_link_paths); + + while (iter.hasNext()) { + iter.next(); + SetArrayIndex(i); + WriteIntegerSetting("program_id", iter.key()); + WriteStringSetting("ryujinx_path", iter.value().absolutePath().toStdString()); + ++i; + } + + EndArray(); // ryujinx + EndGroup(); } diff --git a/src/qt_common/config/uisettings.h b/src/qt_common/config/uisettings.h index 042dad16df..5ca1a362f7 100644 --- a/src/qt_common/config/uisettings.h +++ b/src/qt_common/config/uisettings.h @@ -14,6 +14,7 @@ #include #include #include +#include #include "common/common_types.h" #include "common/settings.h" #include "common/settings_enums.h" @@ -201,6 +202,7 @@ struct Values { Setting cache_game_list{linkage, true, "cache_game_list", Category::UiGameList}; Setting favorites_expanded{linkage, true, "favorites_expanded", Category::UiGameList}; QVector favorited_ids; + QMap ryujinx_link_paths; // Compatibility List Setting show_compat{linkage, true, "show_compat", Category::UiGameList}; diff --git a/src/qt_common/util/fs.cpp b/src/qt_common/util/fs.cpp index da59a4e675..7a9d3d1b15 100644 --- a/src/qt_common/util/fs.cpp +++ b/src/qt_common/util/fs.cpp @@ -2,10 +2,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include -#include "fs.h" +#include "common/fs/path_util.h" #include "common/fs/ryujinx_compat.h" #include "common/fs/symlink.h" #include "frontend_common/data_manager.h" +#include "fs.h" #include "qt_common/abstract/frontend.h" #include "qt_common/qt_string_lookup.h" @@ -56,6 +57,9 @@ bool CheckUnlink(const fs::path &eden_dir, const fs::path &ryu_dir) orig = eden_dir; } + linked.make_preferred(); + orig.make_preferred(); + // first cleanup the symlink/junction, try { // NB: do NOT use remove_all, as Windows treats this as a remove_all to the target, @@ -84,17 +88,53 @@ bool CheckUnlink(const fs::path &eden_dir, const fs::path &ryu_dir) return true; } -u64 GetRyujinxSaveID(const u64 &program_id) +const fs::path GetRyujinxSavePath(const fs::path &path_hint, const u64 &program_id) { - auto path = Common::FS::GetKvdbPath(); + auto ryu_path = path_hint; + + auto kvdb_path = Common::FS::GetKvdbPath(ryu_path); + + if (!fs::exists(kvdb_path)) { + using namespace QtCommon::Frontend; + auto res = Warning( + tr("Could not find Ryujinx installation"), + tr("Could not find a valid Ryujinx installation. This may typically occur if you are " + "using Ryujinx in portable mode.\n\nWould you like to manually select a portable " + "folder to use?"), StandardButton::Yes | StandardButton::No); + + if (res == StandardButton::Yes) { + auto selected_path = GetExistingDirectory(tr("Ryujinx Portable Location"), QDir::homePath()).toStdString(); + if (selected_path.empty()) + return fs::path{}; + + ryu_path = selected_path; + // In case the user selects the actual ryujinx installation dir INSTEAD OF + // the portable dir + if (fs::exists(ryu_path / "portable")) { + ryu_path = ryu_path / "portable"; + } + + kvdb_path = Common::FS::GetKvdbPath(ryu_path); + + if (!fs::exists(kvdb_path)) { + QtCommon::Frontend::Critical( + tr("Not a valid Ryujinx directory"), + tr("The specified directory does not contain valid Ryujinx data.")); + return fs::path{}; + } + } else { + return fs::path{}; + } + } + std::vector imens; - Common::FS::IMENReadResult res = Common::FS::ReadKvdb(path, imens); + Common::FS::IMENReadResult res = Common::FS::ReadKvdb(kvdb_path, imens); if (res == Common::FS::IMENReadResult::Success) { // TODO: this can probably be done with std::find_if but I'm lazy for (const Common::FS::IMEN &imen : imens) { if (imen.title_id == program_id) - return imen.save_id; + return Common::FS::GetRyuSavePath(ryu_path, imen.save_id); } QtCommon::Frontend::Critical( @@ -107,24 +147,7 @@ u64 GetRyujinxSaveID(const u64 &program_id) QtCommon::Frontend::Critical(tr("Could not find Ryujinx save data"), caption); } - return -1; -} - -std::optional > GetEmuPaths( - const u64 program_id, const u64 save_id, const std::string &user_id) -{ - fs::path ryu_dir = Common::FS::GetRyuSavePath(save_id); - - if (user_id.empty()) - return std::nullopt; - - std::string hex_program = fmt::format("{:016X}", program_id); - fs::path eden_dir - = FrontendCommon::DataManager::GetDataDir(FrontendCommon::DataManager::DataDir::Saves, - user_id) - / hex_program; - - return std::make_pair(eden_dir, ryu_dir); + return fs::path{}; } } // namespace QtCommon::FS diff --git a/src/qt_common/util/fs.h b/src/qt_common/util/fs.h index ee8859a2e5..277eab3535 100644 --- a/src/qt_common/util/fs.h +++ b/src/qt_common/util/fs.h @@ -3,20 +3,16 @@ #include "common/common_types.h" #include -#include #pragma once namespace QtCommon::FS { void LinkRyujinx(std::filesystem::path &from, std::filesystem::path &to); -u64 GetRyujinxSaveID(const u64 &program_id); - -/// @brief {eden, ryu} -std::optional> GetEmuPaths( - const u64 program_id, const u64 save_id, const std::string &user_id); +const std::filesystem::path GetRyujinxSavePath(const std::filesystem::path &path_hint, const u64 &program_id); /// returns FALSE if the dirs are NOT linked -bool CheckUnlink(const std::filesystem::path &eden_dir, const std::filesystem::path &ryu_dir); +bool CheckUnlink(const std::filesystem::path& eden_dir, + const std::filesystem::path& ryu_dir); } // namespace QtCommon::FS diff --git a/src/yuzu/deps_dialog.cpp b/src/yuzu/deps_dialog.cpp index 4cef05e054..ffbd0a0e74 100644 --- a/src/yuzu/deps_dialog.cpp +++ b/src/yuzu/deps_dialog.cpp @@ -18,7 +18,7 @@ DepsDialog::DepsDialog(QWidget* parent) { ui->setupUi(this); - constexpr size_t rows = Common::dep_hashes.size(); + constexpr int rows = (int) Common::dep_hashes.size(); ui->tableDeps->setRowCount(rows); QStringList labels; @@ -29,7 +29,7 @@ DepsDialog::DepsDialog(QWidget* parent) ui->tableDeps->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeMode::Fixed); ui->tableDeps->horizontalHeader()->setMinimumSectionSize(200); - for (size_t i = 0; i < rows; ++i) { + for (int i = 0; i < rows; ++i) { const std::string name = Common::dep_names.at(i); const std::string sha = Common::dep_hashes.at(i); const std::string url = Common::dep_urls.at(i); diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp index 0cf40198c7..7015220204 100644 --- a/src/yuzu/main_window.cpp +++ b/src/yuzu/main_window.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later +#include "common/fs/symlink.h" #include "main_window.h" #include "network/network.h" #include "qt_common/discord/discord.h" @@ -160,10 +161,9 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #ifdef _WIN32 #include "core/core_timing.h" #include "common/windows/timer_resolution.h" -#endif -#ifdef _WIN32 #include +#include #include #include #ifdef _MSC_VER @@ -2859,22 +2859,51 @@ std::string MainWindow::GetProfileID() void MainWindow::OnLinkToRyujinx(const u64& program_id) { - u64 save_id = QtCommon::FS::GetRyujinxSaveID(program_id); - if (save_id == (u64) -1) - return; + namespace fs = std::filesystem; const std::string user_id = GetProfileID(); + const std::string hex_program = fmt::format("{:016X}", program_id); - auto paths = QtCommon::FS::GetEmuPaths(program_id, save_id, user_id); - if (!paths) - return; + const fs::path eden_dir + = FrontendCommon::DataManager::GetDataDir(FrontendCommon::DataManager::DataDir::Saves, + user_id) + / hex_program; - auto eden_dir = paths.value().first; - auto ryu_dir = paths.value().second; + fs::path ryu_dir; + // If the Eden directory is a symlink we can just read that and use it as our Ryu dir + if (Common::FS::IsSymlink(eden_dir)) { + ryu_dir = fs::read_symlink(eden_dir); + + // Fallback: if the Eden save dir is symlinked to a nonexistent location, + // just delete and recreate it to remove the symlink. + if (!fs::exists(ryu_dir)) { + fs::remove(eden_dir); + fs::create_directories(eden_dir); + ryu_dir = fs::path{}; + } + } + + // Otherwise, prompt the user + if (ryu_dir.empty()) { + const fs::path existing_path = + UISettings::values.ryujinx_link_paths + .value(program_id, QDir(Common::FS::GetLegacyPath(Common::FS::RyujinxDir))) + .filesystemAbsolutePath(); + + ryu_dir = QtCommon::FS::GetRyujinxSavePath(existing_path, program_id); + } + + // CheckUnlink basically just checks to see if one or both are linked, and prompts the user to + // unlink if this is the case. + // If it returns false, neither dir is linked so it's fine to continue if (!QtCommon::FS::CheckUnlink(eden_dir, ryu_dir)) { RyujinxDialog dialog(eden_dir, ryu_dir, this); - dialog.exec(); + if (dialog.exec() == QDialog::Accepted) { + UISettings::values.ryujinx_link_paths.insert(program_id, QString::fromStdString(ryu_dir.string())); + } + } else { + UISettings::values.ryujinx_link_paths.remove(program_id); } }