Browse Source

[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 <crueter@eden-emu.dev>
pull/2929/head
crueter 3 months ago
parent
commit
d364bc2053
  1. 2
      src/common/CMakeLists.txt
  2. 15
      src/common/fs/ryujinx_compat.cpp
  3. 8
      src/common/fs/ryujinx_compat.h
  4. 16
      src/common/fs/symlink.cpp
  5. 11
      src/qt_common/abstract/frontend.cpp
  6. 4
      src/qt_common/abstract/frontend.h
  7. 25
      src/qt_common/config/qt_config.cpp
  8. 2
      src/qt_common/config/uisettings.h
  9. 69
      src/qt_common/util/fs.cpp
  10. 10
      src/qt_common/util/fs.h
  11. 4
      src/yuzu/deps_dialog.cpp
  12. 51
      src/yuzu/main_window.cpp

2
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()

15
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<IMEN> &imens)
@ -90,4 +98,5 @@ IMENReadResult ReadKvdb(const fs::path &path, std::vector<IMEN> &imens)
return IMENReadResult::Success;
}
} // namespace Common::FS

8
src/common/fs/ryujinx_compat.h

@ -7,16 +7,18 @@
#include <filesystem>
#include <vector>
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

16
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 <iostream>
#include "symlink.h"
#ifdef _WIN32
@ -8,6 +9,8 @@
#include <fmt/format.h>
#endif
#include <boost/filesystem.hpp>
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

11
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
}

4
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

25
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();
}

2
src/qt_common/config/uisettings.h

@ -14,6 +14,7 @@
#include <QString>
#include <QStringList>
#include <QVector>
#include <qdir.h>
#include "common/common_types.h"
#include "common/settings.h"
#include "common/settings_enums.h"
@ -201,6 +202,7 @@ struct Values {
Setting<bool> cache_game_list{linkage, true, "cache_game_list", Category::UiGameList};
Setting<bool> favorites_expanded{linkage, true, "favorites_expanded", Category::UiGameList};
QVector<u64> favorited_ids;
QMap<u64, QDir> ryujinx_link_paths;
// Compatibility List
Setting<bool> show_compat{linkage, true, "show_compat", Category::UiGameList};

69
src/qt_common/util/fs.cpp

@ -2,10 +2,11 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include <filesystem>
#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<Common::FS::IMEN> 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<std::pair<fs::path, fs::path> > 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

10
src/qt_common/util/fs.h

@ -3,20 +3,16 @@
#include "common/common_types.h"
#include <filesystem>
#include <optional>
#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<std::pair<std::filesystem::path, std::filesystem::path>> 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

4
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);

51
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 <QPlatformSurfaceEvent>
#include <QSettings>
#include <dwmapi.h>
#include <windows.h>
#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);
}
}

Loading…
Cancel
Save